From d2f2308e547d9b22f88d809b9bef97c442a974ab Mon Sep 17 00:00:00 2001 From: Rias Date: Wed, 7 Oct 2026 10:08:55 +0200 Subject: [PATCH] Save draft-less nested elements in place Saving a nested element through a slideout redirected it into the owner's draft whenever an owner draft was prepared, which needs a draft of the nested element. Element types without drafts never get one, so the save failed with "Could not save the nested element draft." They now save in place, matching 5.x. Co-authored-by: Nathaniel Hammond --- .../js/common/slideouts/useElementEditor.ts | 3 +- .../composables/useElementEditor.test.ts | 26 +++++++++++++++++ .../elements/composables/useElementEditor.ts | 3 +- .../NestedElementsControl.test.ts | 28 +++++++++++++++++++ .../useNestedElementOperations.ts | 6 +++- 5 files changed, 63 insertions(+), 3 deletions(-) diff --git a/resources/js/common/slideouts/useElementEditor.ts b/resources/js/common/slideouts/useElementEditor.ts index 85fbeed48d3..9ac67f3088d 100644 --- a/resources/js/common/slideouts/useElementEditor.ts +++ b/resources/js/common/slideouts/useElementEditor.ts @@ -445,7 +445,8 @@ export function useElementEditor( panel.nestedOwnerId = await panel.prepareNestedOwner(); } - if (panel?.nestedOwnerId) { + // Element types without drafts (e.g. variants) save in place. + if (panel?.nestedOwnerId && instance.settings.canCreateDrafts) { if (!instance.settings.draftId) { await instance.saveDraft(); } diff --git a/resources/js/modules/elements/composables/useElementEditor.test.ts b/resources/js/modules/elements/composables/useElementEditor.test.ts index ddcf9bc1355..0d620574f5f 100644 --- a/resources/js/modules/elements/composables/useElementEditor.test.ts +++ b/resources/js/modules/elements/composables/useElementEditor.test.ts @@ -1937,6 +1937,32 @@ describe('useElementEditor', () => { }); }); + it('saves draft-less elements in place when the opener prepares an owner draft', async () => { + const slideout = handledSlideout(); + const prepareNestedOwner = vi.fn().mockResolvedValue(73); + Object.assign(slideout.instance, {prepareNestedOwner}); + const request = stubSaveRequest(() => Promise.resolve({data: {}})); + const {editor} = mount( + payload({ + canAutosave: false, + nestedContext, + saveForDerivativeUrl: + '/actions/elements/save-nested-element-for-derivative', + }), + slideout + ); + + editor.save(); + + await vi.waitFor(() => expect(request).toHaveBeenCalledOnce()); + expect(prepareNestedOwner).toHaveBeenCalledOnce(); + expect(postSpy).not.toHaveBeenCalled(); + expect(request.mock.calls[0]![0]).toMatchObject({ + url: '/actions/entries/save-entry', + data: expect.not.objectContaining({newOwnerId: expect.anything()}), + }); + }); + it('announces invalid nested elements when a save fails', async () => { const displayError = vi.fn(); vi.stubGlobal('Craft', {cp: {displayError}}); diff --git a/resources/js/modules/elements/composables/useElementEditor.ts b/resources/js/modules/elements/composables/useElementEditor.ts index c20a546df4a..0f5afcd58ad 100644 --- a/resources/js/modules/elements/composables/useElementEditor.ts +++ b/resources/js/modules/elements/composables/useElementEditor.ts @@ -671,7 +671,8 @@ export function useElementEditor({saveData, root, transform}: Options = {}) { try { const ownerId = await slideout!.instance.prepareNestedOwner!(); - if (!ownerId) { + // Element types without drafts (e.g. variants) save in place. + if (!ownerId || !props.canAutosave) { return true; } diff --git a/resources/js/modules/forms/nested-elements/NestedElementsControl.test.ts b/resources/js/modules/forms/nested-elements/NestedElementsControl.test.ts index 0d004f0d031..a38c33e7e99 100644 --- a/resources/js/modules/forms/nested-elements/NestedElementsControl.test.ts +++ b/resources/js/modules/forms/nested-elements/NestedElementsControl.test.ts @@ -163,6 +163,7 @@ describe('NestedElementsControl', () => { ) { const legacyElementEditor = { settings: { + canCreateDrafts: true, draftId: 6, saveParams: null as Record | null, }, @@ -590,6 +591,33 @@ describe('NestedElementsControl', () => { }); }); + it('saves draft-less elements in place in the legacy editor', async () => { + const {createElementEditor, legacyElementEditor} = mount({ + vueSlideout: false, + cards: [ + nestedElement({ + id: 18, + siteId: 1, + ownerId: 31, + editUrl: + '/admin/actions/elements/edit?elementId=18&siteId=1&fieldId=7&ownerId=31', + cardAttributes: {data: {editable: true}}, + }), + ], + }); + legacyElementEditor.settings.canCreateDrafts = false; + legacyElementEditor.settings.draftId = null as unknown as number; + await nextTick(); + + root.querySelector('a[href]')!.click(); + await vi.waitFor(() => expect(createElementEditor).toHaveBeenCalledOnce()); + + const settings = createElementEditor.mock.lastCall![1]; + await expect(settings.onBeforeSubmit()).resolves.toBeUndefined(); + expect(legacyElementEditor.saveDraft).not.toHaveBeenCalled(); + expect(legacyElementEditor.settings.saveParams).toBeNull(); + }); + it('opens the entry’s own edit page in a new tab on a modified click', async () => { const open = vi.spyOn(window, 'open').mockReturnValue(null); mount({ diff --git a/resources/js/modules/forms/nested-elements/useNestedElementOperations.ts b/resources/js/modules/forms/nested-elements/useNestedElementOperations.ts index db0e7a9d70b..43b86d5378c 100644 --- a/resources/js/modules/forms/nested-elements/useNestedElementOperations.ts +++ b/resources/js/modules/forms/nested-elements/useNestedElementOperations.ts @@ -43,6 +43,7 @@ export interface LegacyElementEditorSlideout { ): void; elementEditor: { settings: { + canCreateDrafts?: boolean; draftId?: number | null; saveParams?: Record | null; }; @@ -335,7 +336,10 @@ export function useNestedElementOperations( prepareNestedOwner ? async (slideout) => { const preparedOwnerId = await prepareNestedOwner(); - if (!preparedOwnerId) { + if ( + !preparedOwnerId || + !slideout.elementEditor.settings.canCreateDrafts + ) { return; }