Telepath: Validation of block counts

pull/6931/head
Karl Hobley 2021-02-08 17:47:21 +00:00 zatwierdzone przez Matt Westcott
rodzic 4ee65760fe
commit 0360cf4c2f
3 zmienionych plików z 447 dodań i 1 usunięć

Wyświetl plik

@ -69,7 +69,8 @@ export class BaseSequenceChild {
this.element = dom.get(0);
const blockElement = dom.find('[data-streamfield-block]').get(0);
dom.find('button[data-duplicate-button]').click(() => {
this.duplicateButton = dom.find('button[data-duplicate-button]');
this.duplicateButton.click(() => {
if (this.onRequestDuplicate) this.onRequestDuplicate(this.index);
});
@ -107,6 +108,12 @@ export class BaseSequenceChild {
}
}
enableDuplication() {
this.duplicateButton.removeAttr('disabled');
}
disableDuplication() {
this.duplicateButton.attr('disabled', 'true');
}
enableMoveUp() {
this.moveUpButton.removeAttr('disabled');
}

Wyświetl plik

@ -62,6 +62,8 @@ class StreamBlockMenu extends BaseInsertionControl {
this.innerContainer = dom.find('[data-streamblock-menu-inner]');
this.hasRenderedMenu = false;
this.isOpen = false;
this.canAddBlock = true;
this.disabledBlockTypes = new Set();
this.close({ animate: false });
}
@ -94,6 +96,34 @@ class StreamBlockMenu extends BaseInsertionControl {
});
});
});
// Disable buttons for any disabled block types
this.disabledBlockTypes.forEach(blockType => {
$(`button.action-add-block-${h(blockType)}`, this.innerContainer).attr('disabled', 'true');
});
}
setNewBlockRestrictions(canAddBlock, disabledBlockTypes) {
this.canAddBlock = canAddBlock;
this.disabledBlockTypes = disabledBlockTypes;
// Disable/enable menu open button
if (this.canAddBlock) {
this.addButton.removeAttr('disabled');
} else {
this.addButton.attr('disabled', 'true');
}
// Close menu if its open and we no longer can add blocks
if (!canAddBlock && this.isOpen) {
this.close({ animate: true });
}
// Disable/enable individual block type buttons
$('button', this.innerContainer).removeAttr('disabled');
disabledBlockTypes.forEach(blockType => {
$(`button.action-add-block-${h(blockType)}`, this.innerContainer).attr('disabled', 'true');
});
}
toggle() {
@ -104,6 +134,10 @@ class StreamBlockMenu extends BaseInsertionControl {
}
}
open(opts) {
if (!this.canAddBlock) {
return;
}
this.renderMenu();
if (opts && opts.animate) {
this.outerContainer.slideDown();
@ -176,6 +210,51 @@ export class StreamBlock extends BaseSequenceBlock {
}
}
/*
* Called whenever a block is added or removed
*
* Updates the state of add / duplicate block buttons to prevent too many blocks being inserted.
*/
checkBlockCounts() {
this.canAddBlock = true;
if (typeof this.blockDef.meta.maxNum === 'number' && this.children.length >= this.blockDef.meta.maxNum) {
this.canAddBlock = false;
}
// If we can add blocks, check if there are any block types that have count limits
this.disabledBlockTypes = new Set();
if (this.canAddBlock) {
// eslint-disable-next-line no-restricted-syntax
for (const blockType in this.blockDef.meta.blockCounts) {
if (this.blockDef.meta.blockCounts.hasOwnProperty(blockType)) {
const counts = this.blockDef.meta.blockCounts[blockType];
if (typeof counts.max_num === 'number') {
const currentBlockCount = this.children.filter(child => child.type === blockType).length;
if (currentBlockCount >= counts.max_num) {
this.disabledBlockTypes.add(blockType);
}
}
}
}
}
for (let i = 0; i < this.children.length; i++) {
const canDuplicate = this.canAddBlock && !this.disabledBlockTypes.has(this.children[i].type);
if (canDuplicate) {
this.children[i].enableDuplication();
} else {
this.children[i].disableDuplication();
}
}
for (let i = 0; i < this.inserters.length; i++) {
this.inserters[i].setNewBlockRestrictions(this.canAddBlock, this.disabledBlockTypes);
}
}
_createChild(blockDef, placeholder, prefix, index, id, initialState, opts) {
return new StreamChild(blockDef, placeholder, prefix, index, id, initialState, opts);
}
@ -191,6 +270,12 @@ export class StreamBlock extends BaseSequenceBlock {
return this._insert(childBlockDef, value, id, index, opts);
}
_insert(childBlockDef, value, id, index, opts) {
const result = super._insert(childBlockDef, value, id, index, opts);
this.checkBlockCounts();
return result;
}
_getChildDataForInsertion({ type }) {
/* Called when an 'insert new block' action is triggered: given a dict of data from the insertion control,
return the block definition and initial state to be used for the new block.
@ -201,11 +286,23 @@ export class StreamBlock extends BaseSequenceBlock {
return [blockDef, initialState];
}
clear() {
super.clear();
this.checkBlockCounts();
}
duplicateBlock(index) {
const childState = this.children[index].getState();
childState.id = null;
this.insert(childState, index + 1, { animate: true });
this.children[index + 1].focus();
this.checkBlockCounts();
}
deleteBlock(index) {
super.deleteBlock(index);
this.checkBlockCounts();
}
setState(values) {

Wyświetl plik

@ -353,3 +353,345 @@ describe('telepath: wagtail.blocks.StreamBlock with labels that need escaping',
expect(document.body.innerHTML).toMatchSnapshot();
});
});
describe('telepath: wagtail.blocks.StreamBlock with maxNum set', () => {
// Define a test block
const blockDef = new StreamBlockDefinition(
'',
[
['', [
new FieldBlockDefinition(
'test_block_a',
new DummyWidgetDefinition('Block A widget'),
{
label: 'Test Block <A>',
required: true,
icon: 'placeholder',
classname: 'field char_field widget-text_input fieldname-test_charblock'
}
),
new FieldBlockDefinition(
'test_block_b',
new DummyWidgetDefinition('Block B widget'),
{
label: 'Test Block <B>',
required: true,
icon: 'pilcrow',
classname: 'field char_field widget-admin_auto_height_text_input fieldname-test_textblock'
}
),
]]
],
{
test_block_a: 'Block A options',
test_block_b: 'Block B options',
},
{
label: '',
required: true,
icon: 'placeholder',
classname: null,
helpText: 'use <strong>plenty</strong> of these',
helpIcon: '<div class="icon-help">?</div>',
maxNum: 3,
minNum: null,
blockCounts: {},
strings: {
MOVE_UP: 'Move up',
MOVE_DOWN: 'Move down',
DELETE: 'Delete & kill with fire',
DUPLICATE: 'Duplicate',
ADD: 'Add',
},
}
);
const assertCanAddBlock = () => {
// Test duplicate button
// querySelector always returns the first element it sees so this only checks the first block
expect(document.querySelector('button[data-duplicate-button]').getAttribute('disabled')).toBe(null);
// Test menu
expect(document.querySelector('button[data-streamblock-menu-open]').getAttribute('disabled')).toBe(null);
};
const assertCannotAddBlock = () => {
// Test duplicate button
// querySelector always returns the first element it sees so this only checks the first block
expect(document.querySelector('button[data-duplicate-button]').getAttribute('disabled')).toEqual('disabled');
// Test menu
expect(document.querySelector('button[data-streamblock-menu-open]').getAttribute('disabled')).toEqual('disabled');
};
test('test can add block when under limit', () => {
document.body.innerHTML = '<div id="placeholder"></div>';
const boundBlock = blockDef.render($('#placeholder'), 'the-prefix', [
{
id: '1',
type: 'test_block_a',
value: 'First value'
},
{
id: '2',
type: 'test_block_b',
value: 'Second value'
},
]);
boundBlock.inserters[0].open();
assertCanAddBlock();
});
test('initialising at maxNum disables adding new block and duplication', () => {
document.body.innerHTML = '<div id="placeholder"></div>';
const boundBlock = blockDef.render($('#placeholder'), 'the-prefix', [
{
id: '1',
type: 'test_block_a',
value: 'First value'
},
{
id: '2',
type: 'test_block_b',
value: 'Second value'
},
{
id: '3',
type: 'test_block_b',
value: 'Third value'
},
]);
boundBlock.inserters[0].open();
assertCannotAddBlock();
});
test('insert disables new block', () => {
document.body.innerHTML = '<div id="placeholder"></div>';
const boundBlock = blockDef.render($('#placeholder'), 'the-prefix', [
{
id: '1',
type: 'test_block_a',
value: 'First value'
},
{
id: '2',
type: 'test_block_b',
value: 'Second value'
},
]);
boundBlock.inserters[0].open();
assertCanAddBlock();
boundBlock.insert({
id: '3',
type: 'test_block_b',
value: 'Third value'
}, 2);
assertCannotAddBlock();
});
test('delete enables new block', () => {
document.body.innerHTML = '<div id="placeholder"></div>';
const boundBlock = blockDef.render($('#placeholder'), 'the-prefix', [
{
id: '1',
type: 'test_block_a',
value: 'First value'
},
{
id: '2',
type: 'test_block_b',
value: 'Second value'
},
{
id: '3',
type: 'test_block_b',
value: 'Third value'
},
]);
boundBlock.inserters[0].open();
assertCannotAddBlock();
boundBlock.deleteBlock(2);
assertCanAddBlock();
});
});
describe('telepath: wagtail.blocks.StreamBlock with blockCounts.max_num set', () => {
// Define a test block
const blockDef = new StreamBlockDefinition(
'',
[
['', [
new FieldBlockDefinition(
'test_block_a',
new DummyWidgetDefinition('Block A widget'),
{
label: 'Test Block <A>',
required: true,
icon: 'placeholder',
classname: 'field char_field widget-text_input fieldname-test_charblock'
}
),
new FieldBlockDefinition(
'test_block_b',
new DummyWidgetDefinition('Block B widget'),
{
label: 'Test Block <B>',
required: true,
icon: 'pilcrow',
classname: 'field char_field widget-admin_auto_height_text_input fieldname-test_textblock'
}
),
]]
],
{
test_block_a: 'Block A options',
test_block_b: 'Block B options',
},
{
label: '',
required: true,
icon: 'placeholder',
classname: null,
helpText: 'use <strong>plenty</strong> of these',
helpIcon: '<div class="icon-help">?</div>',
maxNum: null,
minNum: null,
blockCounts: {
test_block_a: {
max_num: 2
}
},
strings: {
MOVE_UP: 'Move up',
MOVE_DOWN: 'Move down',
DELETE: 'Delete & kill with fire',
DUPLICATE: 'Duplicate',
ADD: 'Add',
},
}
);
const assertCanAddBlock = () => {
// Test duplicate button
// querySelector always returns the first element it sees so this only checks the first block
expect(document.querySelector('button[data-duplicate-button]').getAttribute('disabled')).toBe(null);
// Test menu item
expect(document.querySelector('button.action-add-block-test_block_a').getAttribute('disabled')).toBe(null);
};
const assertCannotAddBlock = () => {
// Test duplicate button
// querySelector always returns the first element it sees so this only checks the first block
expect(document.querySelector('button[data-duplicate-button]').getAttribute('disabled')).toEqual('disabled');
// Test menu item
expect(document.querySelector('button.action-add-block-test_block_a').getAttribute('disabled')).toEqual('disabled');
};
test('single instance allows creation of new block and duplication', () => {
document.body.innerHTML = '<div id="placeholder"></div>';
const boundBlock = blockDef.render($('#placeholder'), 'the-prefix', [
{
id: '1',
type: 'test_block_a',
value: 'First value'
},
{
id: '2',
type: 'test_block_b',
value: 'Second value'
},
]);
boundBlock.inserters[0].open();
assertCanAddBlock();
});
test('initialising at max_num disables adding new block of that type and duplication', () => {
document.body.innerHTML = '<div id="placeholder"></div>';
const boundBlock = blockDef.render($('#placeholder'), 'the-prefix', [
{
id: '1',
type: 'test_block_a',
value: 'First value'
},
{
id: '2',
type: 'test_block_b',
value: 'Second value'
},
{
id: '3',
type: 'test_block_a',
value: 'Third value'
},
]);
boundBlock.inserters[0].open();
assertCannotAddBlock();
});
test('insert disables new block', () => {
document.body.innerHTML = '<div id="placeholder"></div>';
const boundBlock = blockDef.render($('#placeholder'), 'the-prefix', [
{
id: '1',
type: 'test_block_a',
value: 'First value'
},
{
id: '2',
type: 'test_block_b',
value: 'Second value'
},
]);
boundBlock.inserters[0].open();
assertCanAddBlock();
boundBlock.insert({
id: '3',
type: 'test_block_a',
value: 'Third value'
}, 2);
assertCannotAddBlock();
});
test('delete enables new block', () => {
document.body.innerHTML = '<div id="placeholder"></div>';
const boundBlock = blockDef.render($('#placeholder'), 'the-prefix', [
{
id: '1',
type: 'test_block_a',
value: 'First value'
},
{
id: '2',
type: 'test_block_b',
value: 'Second value'
},
{
id: '3',
type: 'test_block_a',
value: 'Third value'
},
]);
boundBlock.inserters[0].open();
assertCannotAddBlock();
boundBlock.deleteBlock(2);
assertCanAddBlock();
});
});