kopia lustrzana https://github.com/wagtail/wagtail
Telepath: Validation of block counts
rodzic
4ee65760fe
commit
0360cf4c2f
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
Ładowanie…
Reference in New Issue