diff --git a/resources/js/components/fieldtypes/bard/BardFieldtype.vue b/resources/js/components/fieldtypes/bard/BardFieldtype.vue index e682303b6f1..acfacc43a41 100644 --- a/resources/js/components/fieldtypes/bard/BardFieldtype.vue +++ b/resources/js/components/fieldtypes/bard/BardFieldtype.vue @@ -601,10 +601,9 @@ export default { duplicateSet(old_id, attrs, getPos) { const id = uniqid(); const enabled = attrs.enabled; - const deepCopy = JSON.parse(JSON.stringify(attrs.values)); - const values = Object.assign({}, deepCopy); + const { values, meta } = this.duplicateValues(attrs.values, this.meta.existing[old_id]); - this.updateSetMeta(id, this.meta.existing[old_id]); + this.updateSetMeta(id, meta); this.debounceNextUpdate = false; @@ -618,13 +617,12 @@ export default { }, async pasteSet(attrs) { - const old_id = attrs.id; const id = uniqid(); const enabled = attrs.enabled; - const values = Object.assign({}, attrs.values); + const { values, meta } = this.duplicateValues(attrs.values, this.meta.existing[attrs.id]); - if (this.meta.existing[old_id]) { - this.updateSetMeta(id, this.meta.existing[old_id]); + if (meta) { + this.updateSetMeta(id, meta); } else { const data = await this.fetchSet(values.type); this.updateSetMeta(id, data.new); diff --git a/resources/js/components/fieldtypes/grid/Grid.vue b/resources/js/components/fieldtypes/grid/Grid.vue index 34f7ad76e67..b1b5ec93db3 100644 --- a/resources/js/components/fieldtypes/grid/Grid.vue +++ b/resources/js/components/fieldtypes/grid/Grid.vue @@ -216,11 +216,10 @@ export default { }, duplicate(index) { - const row = clone(this.value[index]); - const old_id = row._id; - row._id = uniqid(); + const original = this.value[index]; + const { values: row, meta } = this.duplicateValues(original, this.meta.existing[original._id]); - this.updateRowMeta(row._id, this.meta.existing[old_id]); + this.updateRowMeta(row._id, meta); this.update([...this.value, row]); }, diff --git a/resources/js/components/fieldtypes/grid/ManagesRowMeta.js b/resources/js/components/fieldtypes/grid/ManagesRowMeta.js index 9910fd27ae6..31646ace031 100644 --- a/resources/js/components/fieldtypes/grid/ManagesRowMeta.js +++ b/resources/js/components/fieldtypes/grid/ManagesRowMeta.js @@ -1,3 +1,5 @@ +import { nanoid as uniqid } from 'nanoid'; + export default { methods: { updateRowMeta(row, value) { @@ -15,5 +17,43 @@ export default { this.updateMeta({ ...this.meta, existing }); }, + + duplicateValues(values, meta) { + const ids = {}; + + const regenerateValueIds = (value) => { + if (Array.isArray(value)) return value.map(regenerateValueIds); + + if (value === null || typeof value !== 'object') return value; + + const row = Object.fromEntries( + Object.entries(value).map(([key, item]) => [key, regenerateValueIds(item)]), + ); + + if (row._id) row._id = ids[row._id] = uniqid(); + + if (row.type === 'set' && row.attrs?.id) row.attrs.id = ids[row.attrs.id] = uniqid(); + + return row; + }; + + const regenerateMetaIds = (value) => { + if (Array.isArray(value)) { + return value.map((item) => { + if (typeof item === 'string') return ids[item] ?? item; + + return regenerateMetaIds(item); + }); + } + + if (value === null || typeof value !== 'object') return value; + + return Object.fromEntries( + Object.entries(value).map(([key, item]) => [ids[key] ?? key, regenerateMetaIds(item)]), + ); + }; + + return { values: regenerateValueIds(values), meta: regenerateMetaIds(meta) }; + }, }, }; diff --git a/resources/js/components/fieldtypes/replicator/Replicator.vue b/resources/js/components/fieldtypes/replicator/Replicator.vue index 085eafa56f7..d2843c3f0ba 100644 --- a/resources/js/components/fieldtypes/replicator/Replicator.vue +++ b/resources/js/components/fieldtypes/replicator/Replicator.vue @@ -293,13 +293,9 @@ export default { if (!this.canAddSet) return; const index = this.value.findIndex((v) => v._id === old_id); - const old = this.value[index]; - const set = { - ...JSON.parse(JSON.stringify(old)), - _id: uniqid(), - }; + const { values: set, meta } = this.duplicateValues(this.value[index], this.meta.existing[old_id]); - this.updateSetMeta(set._id, this.meta.existing[old_id]); + this.updateSetMeta(set._id, meta); this.update([...this.value.slice(0, index + 1), set, ...this.value.slice(index + 1)]); diff --git a/resources/js/tests/components/fieldtypes/grid/ManagesRowMeta.test.js b/resources/js/tests/components/fieldtypes/grid/ManagesRowMeta.test.js new file mode 100644 index 00000000000..5d3cf461919 --- /dev/null +++ b/resources/js/tests/components/fieldtypes/grid/ManagesRowMeta.test.js @@ -0,0 +1,71 @@ +import { expect, test } from 'vitest'; +import ManagesRowMeta from '@/components/fieldtypes/grid/ManagesRowMeta'; + +const { duplicateValues } = ManagesRowMeta.methods; + +test('it leaves values without ids alone', () => { + const { values } = duplicateValues({ type: 'text', text: 'Hello', tags: ['one', 'two'], link: null }, {}); + + expect(values).toEqual({ type: 'text', text: 'Hello', tags: ['one', 'two'], link: null }); +}); + +test('it gives grid rows nested in a set new ids', () => { + const { values, meta } = duplicateValues( + { + _id: 'set-1', + type: 'my_set', + grid: [ + { _id: 'row-1', text: 'One' }, + { _id: 'row-2', text: 'Two' }, + ], + }, + { grid: { existing: { 'row-1': { text: {} }, 'row-2': { text: {} } } } }, + ); + + const [first, second] = values.grid.map((row) => row._id); + + expect(values._id).not.toBe('set-1'); + expect(first).not.toBe('row-1'); + expect(second).not.toBe('row-2'); + expect(first).not.toBe(second); + expect(Object.keys(meta.grid.existing)).toEqual([first, second]); +}); + +test('it gives bard sets nested in a set new ids', () => { + const { values, meta } = duplicateValues( + { + _id: 'set-1', + type: 'my_set', + bard: [ + { type: 'paragraph', content: [{ type: 'text', text: 'Hello' }] }, + { type: 'set', attrs: { id: 'bard-1', enabled: true, values: { type: 'quote' } } }, + ], + }, + { bard: { existing: { 'bard-1': { quote: {} } }, collapsed: ['bard-1'] } }, + ); + + const nestedId = values.bard[1].attrs.id; + + expect(nestedId).not.toBe('bard-1'); + expect(values.bard[0]).toEqual({ type: 'paragraph', content: [{ type: 'text', text: 'Hello' }] }); + expect(Object.keys(meta.bard.existing)).toEqual([nestedId]); + expect(meta.bard.collapsed).toEqual([nestedId]); +}); + +test('it regenerates ids at every level of nesting', () => { + const { values } = duplicateValues({ _id: 'set-1', replicator: [{ _id: 'set-2', replicator: [{ _id: 'set-3' }] }] }, {}); + + expect(values._id).not.toBe('set-1'); + expect(values.replicator[0]._id).not.toBe('set-2'); + expect(values.replicator[0].replicator[0]._id).not.toBe('set-3'); +}); + +test('it does not modify the values or meta it was given', () => { + const values = { _id: 'set-1', grid: [{ _id: 'row-1' }] }; + const meta = { grid: { existing: { 'row-1': {} } } }; + + duplicateValues(values, meta); + + expect(values).toEqual({ _id: 'set-1', grid: [{ _id: 'row-1' }] }); + expect(meta).toEqual({ grid: { existing: { 'row-1': {} } } }); +});