diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 1db055a319..7df58c0919 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -15,6 +15,7 @@ Changelog * Add `AbstractImage.get_renditions()` for efficient generation of multiple renditions (Andy Babic) * Optimise queries in collection permission policies using cache on the user object (Sage Abdullah) * Phone numbers entered via a link chooser will now have any spaces stripped out, ensuring a valid href="tel:..." attribute (Sahil Jangra) + * Auto-select the `StreamField` block when only one block type is declared (Sébastien Corbin) * Fix: Prevent choosers from failing when initial value is an unrecognised ID, e.g. when moving a page from a location where `parent_page_types` would disallow it (Dan Braghis) * Fix: Move comment notifications toggle to the comments side panel (Sage Abdullah) * Fix: Remove comment button on InlinePanel fields (Sage Abdullah) diff --git a/client/src/components/StreamField/blocks/StreamBlock.js b/client/src/components/StreamField/blocks/StreamBlock.js index 495589d2ed..ff6c5bfff6 100644 --- a/client/src/components/StreamField/blocks/StreamBlock.js +++ b/client/src/components/StreamField/blocks/StreamBlock.js @@ -85,6 +85,18 @@ class StreamBlockMenu extends BaseInsertionControl { $(placeholder).replaceWith(dom); this.element = dom.get(0); this.addButton = dom.find('button'); + + const blockItems = this.blockItems; + if (blockItems.length === 1 && blockItems[0].items.length === 1) { + // Only one child type can be added, bypass the combobox + this.addButton.click(() => { + if (this.onRequestInsert) { + this.onRequestInsert(this.index, blockItems[0].items[0]); + } + }); + return; + } + this.combobox = document.createElement('div'); this.canAddBlock = true; this.disabledBlockTypes = new Set(); @@ -104,8 +116,8 @@ class StreamBlockMenu extends BaseInsertionControl { }); } - renderMenu() { - const items = this.groupedChildBlockDefs.map(([group, blockDefs]) => { + get blockItems() { + return this.groupedChildBlockDefs.map(([group, blockDefs]) => { const groupItems = blockDefs // Allow adding all blockDefs even when disabled, so validation only impedes when saving. // Keeping the previous filtering here for future reference. @@ -122,12 +134,15 @@ class StreamBlockMenu extends BaseInsertionControl { items: groupItems, }; }); + } + renderMenu() { + const blockItems = this.blockItems; ReactDOM.render( <ComboBox label={comboBoxLabel} placeholder={comboBoxLabel} - items={items} + items={blockItems} getItemLabel={(type, item) => item.label} getItemDescription={(item) => item.label} getSearchFields={(item) => [item.label, item.type]} diff --git a/client/src/components/StreamField/blocks/StreamBlock.test.js b/client/src/components/StreamField/blocks/StreamBlock.test.js index 37b3fb0e3f..c45c5c8389 100644 --- a/client/src/components/StreamField/blocks/StreamBlock.test.js +++ b/client/src/components/StreamField/blocks/StreamBlock.test.js @@ -925,3 +925,77 @@ describe('telepath: wagtail.blocks.StreamBlock with blockCounts.max_num set', () assertCanAddBlock(); }); }); + +describe('telepath: wagtail.blocks.StreamBlock with unique block type', () => { + let boundBlock; + + beforeEach(() => { + // Create mocks for callbacks + constructor = jest.fn(); + setState = jest.fn(); + getState = jest.fn(); + getValue = jest.fn(); + focus = jest.fn(); + + // 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: 'w-field w-field--char_field w-field--text_input', + }, + ), + ], + ], + ], + { + test_block_a: 'Block A options', + }, + { + label: '', + required: true, + icon: 'placeholder', + classname: null, + helpText: 'use <strong>plenty</strong> of this', + helpIcon: '<svg></svg>', + maxNum: null, + minNum: null, + blockCounts: {}, + strings: { + MOVE_UP: 'Move up', + MOVE_DOWN: 'Move down', + DELETE: 'Delete', + DUPLICATE: 'Duplicate', + ADD: 'Add', + }, + }, + ); + + // Render it + document.body.innerHTML = '<div id="placeholder"></div>'; + boundBlock = blockDef.render($('#placeholder'), 'the-prefix', []); + }); + + test('it renders correctly without combobox', () => { + expect(document.body.innerHTML).toMatchSnapshot(); + expect(document.querySelector('[role="listbox"]')).toBe(null); + expect(boundBlock.children.length).toEqual(0); + }); + + test('it can add block', () => { + boundBlock.inserters[0].addButton.click(); + + expect(document.body.innerHTML).toMatchSnapshot(); + expect(boundBlock.children.length).toEqual(1); + expect(boundBlock.children[0].type).toEqual('test_block_a'); + }); +}); diff --git a/client/src/components/StreamField/blocks/__snapshots__/StreamBlock.test.js.snap b/client/src/components/StreamField/blocks/__snapshots__/StreamBlock.test.js.snap index 0f28c14612..006acd9143 100644 --- a/client/src/components/StreamField/blocks/__snapshots__/StreamBlock.test.js.snap +++ b/client/src/components/StreamField/blocks/__snapshots__/StreamBlock.test.js.snap @@ -923,3 +923,97 @@ exports[`telepath: wagtail.blocks.StreamBlock setError renders error messages 1` `; exports[`telepath: wagtail.blocks.StreamBlock with labels that need escaping it renders correctly 1`] = `"<div class=\\"w-combobox__optgroup\\"><div role=\\"option\\" aria-selected=\\"false\\" id=\\"downshift-2-item-0\\" class=\\"w-combobox__option w-combobox__option--col1\\"><div class=\\"w-combobox__option-icon\\"><svg class=\\"icon icon-placeholder \\" aria-hidden=\\"true\\"><use href=\\"#icon-placeholder\\"></use></svg></div><div class=\\"w-combobox__option-text\\">Test Block <A></div></div><div role=\\"option\\" aria-selected=\\"false\\" id=\\"downshift-2-item-1\\" class=\\"w-combobox__option w-combobox__option--col2\\"><div class=\\"w-combobox__option-icon\\"><svg class=\\"icon icon-pilcrow \\" aria-hidden=\\"true\\"><use href=\\"#icon-pilcrow\\"></use></svg></div><div class=\\"w-combobox__option-text\\">Test Block <B></div></div></div>"`; + +exports[`telepath: wagtail.blocks.StreamBlock with unique block type it can add block 1`] = ` +"<div class=\\"c-sf-help\\"> + <div class=\\"help\\"> + use <strong>plenty</strong> of this + </div> + </div><div class=\\"\\"> + <input type=\\"hidden\\" name=\\"the-prefix-count\\" data-streamfield-stream-count=\\"\\" value=\\"1\\"> + <div data-streamfield-stream-container=\\"\\"><div> + <button type=\\"button\\" title=\\"Insert a block\\" class=\\"c-sf-add-button\\"> + <svg class=\\"icon icon-plus\\" aria-hidden=\\"true\\"><use href=\\"#icon-plus\\"></use></svg> + </button> + </div><div data-contentpath=\\"fake-uuid-v4-value\\" style=\\"display: none;\\"> + <input type=\\"hidden\\" name=\\"the-prefix-0-deleted\\" value=\\"\\"> + <input type=\\"hidden\\" name=\\"the-prefix-0-order\\" value=\\"0\\"> + <input type=\\"hidden\\" name=\\"the-prefix-0-type\\" value=\\"test_block_a\\"> + <input type=\\"hidden\\" name=\\"the-prefix-0-id\\" value=\\"fake-uuid-v4-value\\"> + <section class=\\"w-panel w-panel--nested\\" id=\\"block-fake-uuid-v4-value-section\\" aria-labelledby=\\"block-fake-uuid-v4-value-heading\\" data-panel=\\"\\"> + <div class=\\"w-panel__header\\"> + <a class=\\"w-panel__anchor w-panel__anchor--prefix\\" href=\\"#block-fake-uuid-v4-value-section\\" aria-labelledby=\\"block-fake-uuid-v4-value-heading\\" data-panel-anchor=\\"\\"> + <svg class=\\"icon icon-link w-panel__icon\\" aria-hidden=\\"true\\"> + <use href=\\"#icon-link\\"></use> + </svg> + </a> + <button class=\\"w-panel__toggle\\" type=\\"button\\" aria-label=\\"Toggle section\\" aria-describedby=\\"block-fake-uuid-v4-value-heading\\" data-panel-toggle=\\"\\" aria-controls=\\"block-fake-uuid-v4-value-content\\" aria-expanded=\\"true\\"> + <svg class=\\"icon icon-placeholder w-panel__icon\\" aria-hidden=\\"true\\"> + <use href=\\"#icon-placeholder\\"></use> + </svg> + </button> + <h2 class=\\"w-panel__heading w-panel__heading--label\\" aria-level=\\"3\\" id=\\"block-fake-uuid-v4-value-heading\\" data-panel-heading=\\"\\"> + <span data-panel-heading-text=\\"\\" class=\\"c-sf-block__title\\"></span> + <span class=\\"c-sf-block__type\\">Test Block A</span> + <span class=\\"w-required-mark\\" data-panel-required=\\"\\">*</span> + </h2> + <a class=\\"w-panel__anchor w-panel__anchor--suffix\\" href=\\"#block-fake-uuid-v4-value-section\\" aria-labelledby=\\"block-fake-uuid-v4-value-heading\\"> + <svg class=\\"icon icon-link w-panel__icon\\" aria-hidden=\\"true\\"> + <use href=\\"#icon-link\\"></use> + </svg> + </a> + <div class=\\"w-panel__divider\\"></div> + <div class=\\"w-panel__controls\\" data-panel-controls=\\"\\"><button type=\\"button\\" class=\\"button button--icon text-replace white\\" title=\\"Move up\\" disabled=\\"disabled\\"> + <svg class=\\"icon icon-arrow-up\\" aria-hidden=\\"true\\"> + <use href=\\"#icon-arrow-up\\"></use> + </svg> + </button><button type=\\"button\\" class=\\"button button--icon text-replace white\\" title=\\"Move down\\" disabled=\\"disabled\\"> + <svg class=\\"icon icon-arrow-down\\" aria-hidden=\\"true\\"> + <use href=\\"#icon-arrow-down\\"></use> + </svg> + </button><button type=\\"button\\" class=\\"button button--icon text-replace white\\" title=\\"Duplicate\\"> + <svg class=\\"icon icon-copy\\" aria-hidden=\\"true\\"> + <use href=\\"#icon-copy\\"></use> + </svg> + </button><button type=\\"button\\" class=\\"button button--icon text-replace white\\" title=\\"Delete\\"> + <svg class=\\"icon icon-bin\\" aria-hidden=\\"true\\"> + <use href=\\"#icon-bin\\"></use> + </svg> + </button></div> + </div> + <div id=\\"block-fake-uuid-v4-value-content\\" class=\\"w-panel__content\\"> + <div class=\\"w-field__wrapper\\" data-field-wrapper=\\"\\"> + <div class=\\"w-field w-field--char_field w-field--text_input\\" data-field=\\"\\"> + <div class=\\"w-field__errors\\" id=\\"the-prefix-0-value-errors\\" data-field-errors=\\"\\"> + <svg class=\\"icon icon-warning w-field__errors-icon\\" aria-hidden=\\"true\\" hidden=\\"\\"><use href=\\"#icon-warning\\"></use></svg> + </div> + <div class=\\"w-field__help\\" id=\\"the-prefix-0-value-helptext\\" data-field-help=\\"\\"></div> + <div class=\\"w-field__input\\" data-field-input=\\"\\"> + <p name=\\"the-prefix-0-value\\" id=\\"the-prefix-0-value\\">Block A widget</p> + </div> + </div> + </div> + </div> + </section> + </div><div> + <button type=\\"button\\" title=\\"Insert a block\\" class=\\"c-sf-add-button\\"> + <svg class=\\"icon icon-plus\\" aria-hidden=\\"true\\"><use href=\\"#icon-plus\\"></use></svg> + </button> + </div></div> + </div>" +`; + +exports[`telepath: wagtail.blocks.StreamBlock with unique block type it renders correctly without combobox 1`] = ` +"<div class=\\"c-sf-help\\"> + <div class=\\"help\\"> + use <strong>plenty</strong> of this + </div> + </div><div class=\\"\\"> + <input type=\\"hidden\\" name=\\"the-prefix-count\\" data-streamfield-stream-count=\\"\\" value=\\"0\\"> + <div data-streamfield-stream-container=\\"\\"><div> + <button type=\\"button\\" title=\\"Insert a block\\" class=\\"c-sf-add-button\\"> + <svg class=\\"icon icon-plus\\" aria-hidden=\\"true\\"><use href=\\"#icon-plus\\"></use></svg> + </button> + </div></div> + </div>" +`; diff --git a/docs/releases/5.1.md b/docs/releases/5.1.md index 162c610ecb..019fc77fab 100644 --- a/docs/releases/5.1.md +++ b/docs/releases/5.1.md @@ -39,6 +39,7 @@ Thank you to Damilola for his work, and to Google for sponsoring this project. * Add [`AbstractImage.get_renditions()`](image_renditions_multiple) for efficient generation of multiple renditions (Andy Babic) * Optimise queries in collection permission policies using cache on the user object (Sage Abdullah) * Phone numbers entered via a link chooser will now have any spaces stripped out, ensuring a valid `href="tel:..."` attribute (Sahil Jangra) + * Auto-select the `StreamField` block when only one block type is declared (Sébastien Corbin) ### Bug fixes