From 9a57f7f9b0a04ab8dfd8248e1ad8ddb911c749f3 Mon Sep 17 00:00:00 2001 From: Oliver Mesieh Date: Fri, 23 Oct 2026 23:08:57 +0200 Subject: [PATCH 1/2] Don't emit set field edits back from Bard --- .../fieldtypes/bard/BardFieldtype.vue | 9 +++- .../fieldtypes/bard/BardFieldtype.test.js | 46 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/resources/js/components/fieldtypes/bard/BardFieldtype.vue b/resources/js/components/fieldtypes/bard/BardFieldtype.vue index 86b8004a09e..f1889b7ebf2 100644 --- a/resources/js/components/fieldtypes/bard/BardFieldtype.vue +++ b/resources/js/components/fieldtypes/bard/BardFieldtype.vue @@ -472,7 +472,7 @@ export default { } this.editor.commands.clearContent(false); - this.editor.commands.setContent(this.valueToContent(value), true); + this.editor.commands.setContent(this.valueToContent(clone(value)), true); }, readOnly(readOnly) { @@ -1004,6 +1004,13 @@ export default { this.editor.view.dispatch(tr); }, + // Set field edits are already in the container. Emitting them back would hand each field a copy of its own value. + update(value) { + if (this.editor && this.valueMatchesEditor(this.value) && this.valueMatchesEditor(value)) return; + + this.$emit('update:value', value); + }, + // Parsing normalizes attrs that the stored value omits but ProseMirror serializes. valueMatchesEditor(value) { const content = this.valueToContent(value); diff --git a/resources/js/tests/components/fieldtypes/bard/BardFieldtype.test.js b/resources/js/tests/components/fieldtypes/bard/BardFieldtype.test.js index f143c44b0b5..ecfaa61287c 100644 --- a/resources/js/tests/components/fieldtypes/bard/BardFieldtype.test.js +++ b/resources/js/tests/components/fieldtypes/bard/BardFieldtype.test.js @@ -277,6 +277,52 @@ test('set field edits made while an update is debounced are kept', async () => { expect(wrapper.vm.editor.getJSON().content[1].attrs.values.label).toBe('ABC'); }); +test('set field edits are not emitted back into the container', async () => { + const { wrapper, values } = await mountFieldInContainer({ sets }, [ + { type: 'paragraph', content: [{ type: 'text', text: 'One' }] }, + // As saved, without attribute defaults. + { type: 'set', attrs: { id: 'set-a', values: { type: 'card', label: 'A' } } }, + ]); + + await editSetLabel(values, 'AB'); + await waitForDebounce(); + + expect(wrapper.vm.editor.getJSON().content[1].attrs.values.label).toBe('AB'); + expect(wrapper.emitted('update:value')).toBeUndefined(); +}); + +test('set attributes changed in the editor are still emitted', async () => { + const { wrapper, values } = await mountFieldInContainer({ sets }, [ + { type: 'paragraph', content: [{ type: 'text', text: 'One' }] }, + set('set-a', 'A'), + ]); + const editor = wrapper.vm.editor; + + editor.view.dispatch(editor.state.tr.setNodeAttribute(editor.state.doc.child(0).nodeSize, 'enabled', false)); + await waitForDebounce(); + + expect(values.value.content[1].attrs.enabled).toBe(false); +}); + +test('set field edits made after the editor was rebuilt are kept', async () => { + const { wrapper, values } = await mountFieldInContainer({ sets }, [ + { type: 'paragraph', content: [{ type: 'text', text: 'One' }] }, + set('set-a', 'A'), + ]); + + data_set(values.value, 'content', [{ type: 'paragraph', content: [{ type: 'text', text: 'Two' }] }, set('set-a', 'A')]); + await wrapper.setProps({ value: values.value.content }); + await waitForDebounce(); + + wrapper.vm.editor.commands.insertContentAt(1, 'Then '); + await nextTick(); + await editSetLabel(values, 'AB'); + await waitForDebounce(); + + expect(values.value.content[0].content[0].text).toBe('Then Two'); + expect(values.value.content[1].attrs.values.label).toBe('AB'); +}); + test('each set field edit can be undone', async () => { const { wrapper, values } = await mountFieldInContainer({ sets }, [ { type: 'paragraph', content: [{ type: 'text', text: 'One' }] }, From 144ffa3ed7ef41a510715c28ea9feefa42b3c032 Mon Sep 17 00:00:00 2001 From: Oliver Mesieh Date: Fri, 23 Oct 2026 23:08:57 +0200 Subject: [PATCH 2/2] Keep debounced Bard edits when the same value arrives again --- .../js/components/fieldtypes/bard/BardFieldtype.vue | 5 ++++- .../components/fieldtypes/bard/BardFieldtype.test.js | 11 +++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/resources/js/components/fieldtypes/bard/BardFieldtype.vue b/resources/js/components/fieldtypes/bard/BardFieldtype.vue index f1889b7ebf2..019173a80a5 100644 --- a/resources/js/components/fieldtypes/bard/BardFieldtype.vue +++ b/resources/js/components/fieldtypes/bard/BardFieldtype.vue @@ -452,11 +452,14 @@ export default { } }, - value(value) { + value(value, oldValue) { if (!this.editor) return; if (this.valueMatchesEditor(value)) return; + // A repeat of the old value would undo typing that's still debounced. + if (JSON.stringify(value) === JSON.stringify(oldValue)) return; + const oldContent = this.editor.getJSON(); // Sets sync their own values into the editor, so a rebuild for those would only destroy their fields. diff --git a/resources/js/tests/components/fieldtypes/bard/BardFieldtype.test.js b/resources/js/tests/components/fieldtypes/bard/BardFieldtype.test.js index ecfaa61287c..b40305e6807 100644 --- a/resources/js/tests/components/fieldtypes/bard/BardFieldtype.test.js +++ b/resources/js/tests/components/fieldtypes/bard/BardFieldtype.test.js @@ -213,6 +213,17 @@ test('the editor is left alone when the value only differs by omitted attribute expect(wrapper.vm.editor.state.doc).toBe(doc); }); +test('typing still waiting for the debounced update is kept when the same value arrives again', async () => { + const wrapper = await mountField(); + + await type(wrapper, 'Zero. '); + await wrapper.setProps({ value: clone(value) }); + await waitForDebounce(); + + expect(wrapper.vm.editor.getText()).toBe('Zero. One two three four five.'); + expect(wrapper.emitted('update:value')?.at(-1)).toEqual([wrapper.vm.editor.getJSON().content]); +}); + // With remove_empty_nodes, a save strips empty nodes. Deleting them in place keeps the set fields alive. test('empty nodes the value no longer has are deleted in place', async () => { const text = { type: 'paragraph', content: [{ type: 'text', text: 'Text' }] };