From 829665a99bf6c88c8ea60c10890abde2f742c5ae Mon Sep 17 00:00:00 2001 From: Oliver Mesieh Date: Thu, 22 Oct 2026 14:32:02 +0200 Subject: [PATCH] Copy set values when syncing them into the Bard editor --- .../js/components/fieldtypes/bard/Set.vue | 6 +- .../fieldtypes/bard/BardFieldtype.test.js | 63 ++++++++++++++++++- 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/resources/js/components/fieldtypes/bard/Set.vue b/resources/js/components/fieldtypes/bard/Set.vue index ee293fa8e9c..7019a1dbf9e 100644 --- a/resources/js/components/fieldtypes/bard/Set.vue +++ b/resources/js/components/fieldtypes/bard/Set.vue @@ -443,9 +443,11 @@ export default { () => data_get(this.publishContainer.values.value, this.fieldPathPrefix), (values) => { if (! values) return; - if (JSON.stringify(values) === JSON.stringify(this.node.attrs.values)) return; + const json = JSON.stringify(values); + if (json === JSON.stringify(this.node.attrs.values)) return; - this.updateAttributes({ values }); + // A copy, so later edits to the container's object can't change the node without a transaction. + this.updateAttributes({ values: JSON.parse(json) }); }, { deep: true } ); diff --git a/resources/js/tests/components/fieldtypes/bard/BardFieldtype.test.js b/resources/js/tests/components/fieldtypes/bard/BardFieldtype.test.js index 6430c1c2cae..597833b8085 100644 --- a/resources/js/tests/components/fieldtypes/bard/BardFieldtype.test.js +++ b/resources/js/tests/components/fieldtypes/bard/BardFieldtype.test.js @@ -1,10 +1,13 @@ import { mount } from '@vue/test-utils'; import { afterEach, beforeEach, expect, test, vi } from 'vitest'; +import { nextTick, ref } from 'vue'; import { Editor } from '@tiptap/vue-3'; +import { closeHistory } from '@tiptap/pm/history'; import * as Globals from '@/bootstrap/globals'; import BardFieldtype from '@/components/fieldtypes/bard/BardFieldtype.vue'; import Provider from '@/components/portals/Provider.vue'; import { containerContextKey } from '@/components/ui/Publish/Container.vue'; +import { UPDATE_DEBOUNCE_MS } from '@/components/fieldtypes/constants.js'; Object.keys(Globals).forEach((fn) => (window[fn] = Globals[fn])); window.cp_url = (url) => url; @@ -21,7 +24,7 @@ function set(id, label) { return { type: 'set', attrs: { id, enabled: true, values: { type: 'card', label } } }; } -async function mountField(config = {}, initialValue = value) { +async function mountField(config = {}, initialValue = value, container = {}) { const wrapper = mount(BardFieldtype, { props: { value: initialValue, @@ -51,6 +54,7 @@ async function mountField(config = {}, initialValue = value) { errors: { value: {} }, setFieldValue: vi.fn(), setFieldMeta: vi.fn(), + ...container, }, }, }, @@ -62,6 +66,24 @@ async function mountField(config = {}, initialValue = value) { return wrapper; } +async function mountFieldInContainer(config, initialValue) { + const values = ref({ content: initialValue }); + const wrapper = await mountField(config, initialValue, { values }); + + await wrapper.setProps({ + 'onUpdate:value': (value) => { + data_set(values.value, 'content', value); + wrapper.setProps({ value }); + }, + }); + + return { wrapper, values }; +} + +function waitForDebounce() { + return new Promise((resolve) => setTimeout(resolve, UPDATE_DEBOUNCE_MS + 50)); +} + async function type(wrapper, text) { wrapper.vm.editor.commands.insertContent(text); await wrapper.vm.$nextTick(); @@ -171,3 +193,42 @@ test('the editor is rebuilt when the value changes while the editor is not focus expect(wrapper.vm.editor.getJSON().content).toEqual([set('set-a', 'Changed'), { type: 'paragraph' }]); }); + +async function editSetLabel(values, label) { + data_set(values.value, 'content.1.attrs.values.label', label); + await nextTick(); +} + +test('set field edits made while an update is debounced are kept', async () => { + const { wrapper, values } = await mountFieldInContainer({ sets }, [ + { type: 'paragraph', content: [{ type: 'text', text: 'One' }] }, + set('set-a', 'A'), + ]); + + await editSetLabel(values, 'AB'); + await editSetLabel(values, 'ABC'); + await waitForDebounce(); + + expect(values.value.content[1].attrs.values.label).toBe('ABC'); + expect(wrapper.vm.editor.getJSON().content[1].attrs.values.label).toBe('ABC'); +}); + +test('each set field edit can be undone', async () => { + const { wrapper, values } = await mountFieldInContainer({ sets }, [ + { type: 'paragraph', content: [{ type: 'text', text: 'One' }] }, + set('set-a', 'A'), + ]); + const editor = wrapper.vm.editor; + + for (const label of ['AB', 'ABC', 'ABCD']) { + await editSetLabel(values, label); + await waitForDebounce(); + editor.view.dispatch(closeHistory(editor.state.tr)); + } + + editor.commands.undo(); + await waitForDebounce(); + + expect(editor.getJSON().content[1].attrs.values.label).toBe('ABC'); + expect(values.value.content[1].attrs.values.label).toBe('ABC'); +});