From f1b98384b5693a5308c8a47fafbeaab2d2c984a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joachim=20R=C3=BCtter?= Date: Tue, 20 Oct 2026 07:39:33 +0000 Subject: [PATCH] Make field ids unique across publish containers Co-Authored-By: Claude Opus 5.5 --- resources/js/components/ui/Publish/Field.vue | 4 +++- resources/js/tests/browser/GroupLayout.test.js | 10 +++++----- resources/js/tests/components/Fields.test.js | 11 ++++++++++- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/resources/js/components/ui/Publish/Field.vue b/resources/js/components/ui/Publish/Field.vue index 3b7c538524e..135b89a1a7d 100644 --- a/resources/js/components/ui/Publish/Field.vue +++ b/resources/js/components/ui/Publish/Field.vue @@ -48,6 +48,7 @@ const { focusField, blurField, container, + name: containerName, direction: contentDirection, } = injectContainerContext(); const { @@ -88,7 +89,8 @@ watch( { immediate: true }, ); -const fieldId = computed(() => `field_${fullPath.value.replaceAll('.', '_')}`); +// Include the container name so stacked containers (e.g. nested field settings) don't share ids. +const fieldId = computed(() => `field_${containerName.value}_${fullPath.value.replaceAll('.', '_')}`); const namePrefix = ''; const isRequired = computed(() => props.config.required); const fieldtype = useTemplateRef('fieldtype'); diff --git a/resources/js/tests/browser/GroupLayout.test.js b/resources/js/tests/browser/GroupLayout.test.js index dd6524fd70b..a713a89693f 100644 --- a/resources/js/tests/browser/GroupLayout.test.js +++ b/resources/js/tests/browser/GroupLayout.test.js @@ -45,7 +45,7 @@ async function render(fields, width, values = {}, meta = {}) { document.body.append(host); wrapper = mount(Container, { attachTo: host, - props: { blueprint: { tabs: [] }, modelValue: values, meta, site: 'default' }, + props: { name: 'test', blueprint: { tabs: [] }, modelValue: values, meta, site: 'default' }, global: { components: { ...components, @@ -70,7 +70,7 @@ async function render(fields, width, values = {}, meta = {}) { } function rectangle(id) { - return document.getElementById(`field_${id}`).getBoundingClientRect(); + return document.getElementById(`field_test_${id}`).getBoundingClientRect(); } function assertStacked(first, second) { @@ -83,7 +83,7 @@ function assertStacked(first, second) { test.each([600, 900, 1200])('a 25% group stays inside a %ipx publish panel', async width => { await render([text('heading', 75), group('options', [text('tag'), text('design')], { width: 25 })], width); - const groupElement = document.getElementById('field_options_tag').closest('.group-fieldtype').getBoundingClientRect(); + const groupElement = document.getElementById('field_test_options_tag').closest('.group-fieldtype').getBoundingClientRect(); for (const name of ['options_tag', 'options_design']) { const input = rectangle(name); expect(input.right).toBeLessThanOrEqual(groupElement.right + 1); @@ -109,7 +109,7 @@ test.each([false, true])('a wide group preserves 50/50 columns with border=%s', test('nested groups do not inherit a parent column span', async () => { await render([group('outer', [text('heading', 75), group('inner', [text('first', 50), text('second', 50)], { width: 25 })])], 1000); assertStacked('outer_inner_first', 'outer_inner_second'); - const input = document.getElementById('field_outer_inner_first'); + const input = document.getElementById('field_test_outer_inner_first'); const bounds = input.closest('.group-fieldtype').getBoundingClientRect(); expect(Math.abs(input.getBoundingClientRect().width - bounds.width)).toBeLessThan(1); }); @@ -145,7 +145,7 @@ test.each([511, 512])('a group uses its own %ipx breakpoint width', async width test('editing a nested field updates its form value', async () => { await render([group('options', [text('first'), text('second')], { width: 25 })], 1000, { options: { first: 'Original', second: 'Retained' } }); - await wrapper.find('#field_options_first').setValue('Updated'); + await wrapper.find('#field_test_options_first').setValue('Updated'); expect(wrapper.vm.values).toEqual({ options: { first: 'Updated', second: 'Retained' } }); }); diff --git a/resources/js/tests/components/Fields.test.js b/resources/js/tests/components/Fields.test.js index 188653c09b4..660f89f2040 100644 --- a/resources/js/tests/components/Fields.test.js +++ b/resources/js/tests/components/Fields.test.js @@ -33,12 +33,13 @@ const Probe = defineComponent({ }, }); -async function mountFields(values, provide = {}) { +async function mountFields(values, provide = {}, props = {}) { const wrapper = mount(Container, { props: { blueprint: { tabs: [] }, modelValue: values, site: 'default', + ...props, }, global: { components: { 'hidden-fieldtype': HiddenFieldtype }, @@ -83,3 +84,11 @@ test('a hidden fieldtype does not get a replicator preview', async () => { expect(previews.value).toEqual({}); }); + +test('field ids are unique across publish containers', async () => { + const outer = await mountFields({ protected: true, scheme: 'password' }, {}, { name: 'outer' }); + const inner = await mountFields({ protected: true, scheme: 'password' }, {}, { name: 'inner' }); + + expect(outer.find('#field_outer_scheme').exists()).toBe(true); + expect(inner.find('#field_inner_scheme').exists()).toBe(true); +});