diff --git a/CHANGELOG.txt b/CHANGELOG.txt index f1f867d64b..8cd4afaa38 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -68,6 +68,7 @@ Changelog * Fix: CSS build scripts now output to the correct directory paths on Windows (Vince Salvino) * Fix: Capture log output from style fallback to avoid noise in unit tests (Matt Westcott) * Fix: Switch widgets on/off states are now visually different for high-contrast mode users (Sakshi Uppoor) + * Fix: Nested InlinePanel usage no longer fails to save when creating two or more items (Indresh P, Rinish Sam) 2.14.1 (12.08.2021) diff --git a/CONTRIBUTORS.rst b/CONTRIBUTORS.rst index 0d82accb9c..6326bc23ac 100644 --- a/CONTRIBUTORS.rst +++ b/CONTRIBUTORS.rst @@ -545,6 +545,8 @@ Contributors * Joe Howard * Jochen Wersdörfer * Sakshi Uppoor +* Indresh P +* Rinish Sam Translators =========== diff --git a/client/src/entrypoints/admin/__snapshots__/expanding-formset.test.js.snap b/client/src/entrypoints/admin/__snapshots__/expanding-formset.test.js.snap new file mode 100644 index 0000000000..ed7bff7185 --- /dev/null +++ b/client/src/entrypoints/admin/__snapshots__/expanding-formset.test.js.snap @@ -0,0 +1,33 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`buildExpandingFormset should add an expanded item if the add button is not disabled 1`] = ` +<li + data-contentpath-disabled="" + data-inline-panel-child="" + id="inline_child_form_fields-2" +> + + + <input + id="id_form_fields-2-label" + name="form_fields-2-label" + type="text" + /> + + + <input + id="id_form_fields-2-id" + name="form_fields-2-id" + type="hidden" + /> + + + <input + id="id_form_fields-2-DELETE" + name="form_fields-2-DELETE" + type="hidden" + /> + + +</li> +`; diff --git a/client/src/entrypoints/admin/expanding-formset.test.js b/client/src/entrypoints/admin/expanding-formset.test.js new file mode 100644 index 0000000000..9827904179 --- /dev/null +++ b/client/src/entrypoints/admin/expanding-formset.test.js @@ -0,0 +1,213 @@ +/* global buildExpandingFormset */ +import $ from 'jquery'; +window.$ = $; + +import './expanding_formset'; + +describe('buildExpandingFormset', () => { + it('exposes module as global', () => { + expect(window.buildExpandingFormset).toBeDefined(); + }); + + + it('should add an expanded item if the add button is not disabled', () => { + const prefix = 'id_form_fields'; + document.body.innerHTML = ` + <div class="object" id="content"> + <input type="hidden" name="form_fields-TOTAL_FORMS" value="2" id="${prefix}-TOTAL_FORMS"> + <ul id="${prefix}-FORMS"> + ${[0, 1].map(id => ` + <li id="inline_child_form_fields-${id}" data-inline-panel-child data-contentpath-disabled> + <input type="text" name="form_fields-${id}-label" value="Subject" id="id_form_fields-${id}-label"> + <input type="hidden" name="form_fields-${id}-id" value="${id + 1}" id="id_form_fields-${id}-id"> + <input type="hidden" name="form_fields-${id}-DELETE" id="id_form_fields-${id}-DELETE"> + </li> + `)} + </ul> + <button class="button" id="${prefix}-ADD" type="button"> + Add form fields + </button> + <script type="text/django-form-template" id="${prefix}-EMPTY_FORM_TEMPLATE"> + <li id="inline_child_form_fields-__prefix__" data-inline-panel-child data-contentpath-disabled> + <input type="text" name="form_fields-__prefix__-label" id="id_form_fields-__prefix__-label"> + <input type="hidden" name="form_fields-__prefix__-id" id="id_form_fields-__prefix__-id"> + <input type="hidden" name="form_fields-__prefix__-DELETE" id="id_form_fields-__prefix__-DELETE"> + </li> + </script> + </div>`; + + const onAdd = jest.fn(); + const onInit = jest.fn(); + + expect(document.getElementById(`${prefix}-TOTAL_FORMS`).value).toEqual('2'); + expect(document.querySelectorAll('[data-inline-panel-child]')).toHaveLength(2); + expect(onAdd).not.toHaveBeenCalled(); + expect(onInit).not.toHaveBeenCalled(); + + // initialise expanding formset + buildExpandingFormset(prefix, { onInit, onAdd }); + + // check that init calls only were made for existing items + expect(onAdd).not.toHaveBeenCalled(); + expect(onInit).toHaveBeenCalledTimes(2); + expect(onInit).toHaveBeenNthCalledWith(1, 0); // zero indexed + expect(onInit).toHaveBeenNthCalledWith(2, 1); + + // click the 'add' button + document.getElementById(`${prefix}-ADD`).dispatchEvent(new MouseEvent('click')); + + // check that template was generated and additional onInit / onAdd called + expect(onAdd).toHaveBeenCalledWith(2); // zero indexed + expect(onInit).toHaveBeenCalledTimes(3); + expect(onInit).toHaveBeenLastCalledWith(2); + expect(document.getElementById(`${prefix}-TOTAL_FORMS`).value).toEqual('3'); + expect(document.querySelectorAll('[data-inline-panel-child]')).toHaveLength(3); + + + // check template was created into a new form item or malformed + expect(document.getElementById('inline_child_form_fields-__prefix__')).toBeNull(); + const newFormHtml = document.getElementById(`inline_child_form_fields-${2}`); + expect(newFormHtml.querySelectorAll('[id*="__prefix__"]')).toHaveLength(0); + expect(newFormHtml.querySelectorAll(`[id*="form_fields-${2}"]`)).toHaveLength(3); + + expect(newFormHtml).toMatchSnapshot(); + }); + + it('should not add an expanded item if the add button is disabled', () => { + const prefix = 'id_form_fields'; + document.body.innerHTML = ` + <div class="object" id="content"> + <input type="hidden" name="form_fields-TOTAL_FORMS" value="2" id="${prefix}-TOTAL_FORMS"> + <ul id="${prefix}-FORMS"> + ${[0, 1].map(id => ` + <li id="inline_child_form_fields-${id}" data-inline-panel-child data-contentpath-disabled> + <input type="text" name="form_fields-${id}-label" value="Subject" id="id_form_fields-${id}-label"> + <input type="hidden" name="form_fields-${id}-id" value="${id + 1}" id="id_form_fields-${id}-id"> + <input type="hidden" name="form_fields-${id}-DELETE" id="id_form_fields-${id}-DELETE"> + </li> + `)} + </ul> + <button class="button disabled" id="${prefix}-ADD" type="button"> + Add form fields (DISABLED) + </button> + <script type="text/django-form-template" id="${prefix}-EMPTY_FORM_TEMPLATE"> + <li id="inline_child_form_fields-__prefix__" data-inline-panel-child data-contentpath-disabled> + <input type="text" name="form_fields-__prefix__-label" id="id_form_fields-__prefix__-label"> + <input type="hidden" name="form_fields-__prefix__-id" id="id_form_fields-__prefix__-id"> + <input type="hidden" name="form_fields-__prefix__-DELETE" id="id_form_fields-__prefix__-DELETE"> + </li> + </script> + </div>`; + + const onAdd = jest.fn(); + const onInit = jest.fn(); + + expect(document.getElementById(`${prefix}-TOTAL_FORMS`).value).toEqual('2'); + expect(document.querySelectorAll('[data-inline-panel-child]')).toHaveLength(2); + expect(onAdd).not.toHaveBeenCalled(); + expect(onInit).not.toHaveBeenCalled(); + + // initialise expanding formset + buildExpandingFormset(prefix, { onInit, onAdd }); + + // check that init calls only were made for existing items + expect(onInit).toHaveBeenCalledTimes(2); + expect(onInit).toHaveBeenNthCalledWith(1, 0); // zero indexed + expect(onInit).toHaveBeenNthCalledWith(2, 1); + + // click the 'add' button + document.getElementById(`${prefix}-ADD`).dispatchEvent(new MouseEvent('click')); + + // check that no template was generated and additional onInit / onAdd not called + expect(onAdd).not.toHaveBeenCalled(); + expect(onInit).toHaveBeenCalledTimes(2); + expect(document.getElementById(`${prefix}-TOTAL_FORMS`).value).toEqual('2'); + expect(document.querySelectorAll('[data-inline-panel-child]')).toHaveLength(2); + + // check template was not created into a new form item or malformed + expect(document.getElementById('inline_child_form_fields-__prefix__')).toBeNull(); + }); + + it('should replace the __prefix__ correctly for nested formset templates', () => { + const prefix = 'id_venues'; + const nestedPrefix = 'events'; + + const nestedTemplate = ` +<script type="text/django-form-template" id="${prefix}-__prefix__-events-EMPTY_FORM_TEMPLATE"> + <ul class="controls"> + <li> + <button type="button" class="button" id="${prefix}-__prefix__-${nestedPrefix}-__prefix__-DELETE-button"> + Delete + </button> + </li> + </ul> + <fieldset> + <legend>Events</legend> + <input type="text" name="venues-__prefix__-events-__prefix__-name" id="id_venues-__prefix__-events-__prefix__-name"> + </fieldset> +<-/script> + `; + + + document.body.innerHTML = ` + <div class="object" id="content"> + <input type="hidden" name="form_fields-TOTAL_FORMS" value="2" id="${prefix}-TOTAL_FORMS"> + <ul id="${prefix}-FORMS"> + ${[0, 1].map(id => ` + <li id="inline_child_form_fields-${id}" data-inline-panel-child data-contentpath-disabled> + <input type="text" name="form_fields-${id}-label" value="Subject" id="id_form_fields-${id}-label"> + <input type="hidden" name="form_fields-${id}-id" value="${id + 1}" id="id_form_fields-${id}-id"> + <input type="hidden" name="form_fields-${id}-DELETE" id="id_form_fields-${id}-DELETE"> + </li> + `)} + </ul> + <button class="button" id="${prefix}-ADD" type="button"> + Add Venue + </button> + <script type="text/django-form-template" id="${prefix}-EMPTY_FORM_TEMPLATE"> + <li id="inline_child_form_fields-__prefix__" data-inline-panel-child data-contentpath-disabled> + <input type="text" name="form_fields-__prefix__-label" id="id_form_fields-__prefix__-label"> + <input type="hidden" name="form_fields-__prefix__-id" id="id_form_fields-__prefix__-id"> + <input type="hidden" name="form_fields-__prefix__-DELETE" id="id_form_fields-__prefix__-DELETE"> + </li> + ${nestedTemplate} + </script> + </div>`; + + + const onAdd = jest.fn(); + const onInit = jest.fn(); + + expect(document.getElementById(`${prefix}-TOTAL_FORMS`).value).toEqual('2'); + expect(document.querySelectorAll('[data-inline-panel-child]')).toHaveLength(2); + expect(onAdd).not.toHaveBeenCalled(); + expect(onInit).not.toHaveBeenCalled(); + + // initialise expanding formset + buildExpandingFormset(prefix, { onInit, onAdd }); + + // check that init calls only were made for existing items + expect(onAdd).not.toHaveBeenCalled(); + expect(onInit).toHaveBeenCalledTimes(2); + expect(onInit).toHaveBeenNthCalledWith(1, 0); // zero indexed + expect(onInit).toHaveBeenNthCalledWith(2, 1); + + // click the 'add' button + document.getElementById(`${prefix}-ADD`).dispatchEvent(new MouseEvent('click')); + + // check that template was generated and additional onInit / onAdd called + expect(onAdd).toHaveBeenCalledWith(2); // zero indexed + expect(onInit).toHaveBeenCalledTimes(3); + expect(onInit).toHaveBeenLastCalledWith(2); + expect(document.getElementById(`${prefix}-TOTAL_FORMS`).value).toEqual('3'); + expect(document.querySelectorAll('[data-inline-panel-child]')).toHaveLength(3); + + // check the nested template was created with the correct prefixes + const newTemplate = document.getElementById(`${prefix}-2-events-EMPTY_FORM_TEMPLATE`); + expect(newTemplate).toBeTruthy(); + expect(newTemplate.textContent).toContain('id="id_venues-2-events-__prefix__-DELETE-button"'); + expect(newTemplate.textContent).toContain( + '<input type="text" name="venues-2-events-__prefix__-name" id="id_venues-2-events-__prefix__-name">' + ); + }); +}); diff --git a/client/src/entrypoints/admin/expanding_formset.js b/client/src/entrypoints/admin/expanding_formset.js index d9fcddcbec..e68cc1be64 100644 --- a/client/src/entrypoints/admin/expanding_formset.js +++ b/client/src/entrypoints/admin/expanding_formset.js @@ -23,7 +23,7 @@ function buildExpandingFormset(prefix, opts = {}) { addButton.on('click', () => { if (addButton.hasClass('disabled')) return false; const newFormHtml = emptyFormTemplate - .replace(/__prefix__/g, formCount) + .replace(/__prefix__(.*?['"])/g, formCount + '$1') .replace(/<-(-*)\/script>/g, '<$1/script>'); formContainer.append(newFormHtml); if (opts.onAdd) opts.onAdd(formCount); diff --git a/client/src/entrypoints/admin/page-editor.js b/client/src/entrypoints/admin/page-editor.js index 0c2c3c5864..50c8d29847 100644 --- a/client/src/entrypoints/admin/page-editor.js +++ b/client/src/entrypoints/admin/page-editor.js @@ -165,7 +165,7 @@ function InlinePanel(opts) { // lgtm[js/unused-local-variable] // eslint-disable-next-line no-undef buildExpandingFormset(opts.formsetPrefix, { onAdd(formCount) { - const newChildPrefix = opts.emptyChildFormPrefix.replace(/__prefix__/g, formCount); + const newChildPrefix = opts.emptyChildFormPrefix.replace(/__prefix__(.*?['"])/g, formCount + '$1'); self.initChildControls(newChildPrefix); if (opts.canOrder) { /* NB form hidden inputs use 0-based index and only increment formCount *after* this function is run. diff --git a/docs/releases/2.15.rst b/docs/releases/2.15.rst index 85fe92f4e0..59d25281c5 100644 --- a/docs/releases/2.15.rst +++ b/docs/releases/2.15.rst @@ -88,6 +88,7 @@ Bug fixes * CSS build scripts now output to the correct directory paths on Windows (Vince Salvino) * Capture log output from style fallback to avoid noise in unit tests (Matt Westcott) * Switch widgets on/off states are now visually different for high-contrast mode users (Sakshi Uppoor) + * Nested InlinePanel usage no longer fails to save when creating two or more items (Indresh P, Rinish Sam) Upgrade considerations ======================