From 7d4ddf11d39c25048433c902f6f766cbcae47c21 Mon Sep 17 00:00:00 2001 From: Oliver Mesieh Date: Fri, 23 Oct 2026 19:03:10 +0200 Subject: [PATCH] Stop deep-watching publish container meta --- .../js/components/ui/Publish/Container.vue | 9 ++- .../js/tests/components/Container.test.js | 65 ++++++++++++++++++- 2 files changed, 69 insertions(+), 5 deletions(-) diff --git a/resources/js/components/ui/Publish/Container.vue b/resources/js/components/ui/Publish/Container.vue index 5bae1670409..c38a920362a 100644 --- a/resources/js/components/ui/Publish/Container.vue +++ b/resources/js/components/ui/Publish/Container.vue @@ -186,10 +186,12 @@ watch( { deep: true }, ); +// Cheaper than deep-watching meta, which traverses the whole meta tree on each change. +const metaVersion = ref(0); + watch( - meta, - (meta) => emit('update:meta', meta), - { deep: true }, + [meta, metaVersion], + () => emit('update:meta', meta.value), ); const avoidTrackingDirtyState = ref(false); @@ -229,6 +231,7 @@ function setMeta(newMeta) { function setFieldMeta(path, value) { data_set(meta.value, path, value); + metaVersion.value++; } function syncField(path) { diff --git a/resources/js/tests/components/Container.test.js b/resources/js/tests/components/Container.test.js index 97902d64bbe..c8a049be09a 100644 --- a/resources/js/tests/components/Container.test.js +++ b/resources/js/tests/components/Container.test.js @@ -1,8 +1,8 @@ import { mount } from '@vue/test-utils'; import { afterEach, beforeEach, expect, test } from 'vitest'; -import { defineComponent, h } from 'vue'; +import { defineComponent, h, nextTick } from 'vue'; import * as Globals from '@/bootstrap/globals'; -import Container from '@/components/ui/Publish/Container.vue'; +import Container, { injectContainerContext } from '@/components/ui/Publish/Container.vue'; import { useContentDirection } from '@/composables/content-direction'; Object.keys(Globals).forEach((fn) => (window[fn] = Globals[fn])); @@ -63,3 +63,64 @@ test('content direction falls back to the UI direction when the site cannot be r expect(wrapper.find('[data-direction]').attributes('data-direction')).toBe('rtl'); }); + +function mountWithMeta(meta) { + let context; + + const ContextProbe = defineComponent({ + setup() { + context = injectContainerContext(); + return () => null; + }, + }); + + const wrapper = mount(Container, { + props: { + blueprint: { tabs: [] }, + site: 'default', + meta, + }, + slots: { + default: () => h(ContextProbe), + }, + }); + + return { wrapper, context }; +} + +test('setting field meta emits the updated meta', async () => { + const { wrapper, context } = mountWithMeta({ blocks: { collapsed: [] } }); + + context.setFieldMeta('blocks', { collapsed: ['set-1'] }); + await nextTick(); + + expect(wrapper.emitted('update:meta')).toHaveLength(1); + expect(wrapper.emitted('update:meta')[0][0].blocks.collapsed).toEqual(['set-1']); +}); + +test('replacing the meta emits the new meta', async () => { + const { wrapper, context } = mountWithMeta({ blocks: { collapsed: [] } }); + + context.setMeta({ blocks: { collapsed: ['set-2'] } }); + await nextTick(); + + expect(wrapper.emitted('update:meta')).toHaveLength(1); + expect(wrapper.emitted('update:meta')[0][0].blocks.collapsed).toEqual(['set-2']); +}); + +test('setting field meta does not read the rest of the meta', async () => { + let reads = 0; + const other = { + get value() { + reads++; + return 'x'; + }, + }; + const { context } = mountWithMeta({ blocks: { collapsed: [] }, other }); + const readsAfterMount = reads; + + context.setFieldMeta('blocks', { collapsed: ['set-1'] }); + await nextTick(); + + expect(reads).toBe(readsAfterMount); +});