From 80e459d479f40e8f24b2b786c378aa24666c3d4f Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Mon, 22 Jun 2026 12:25:10 +0100 Subject: [PATCH 01/10] rename tab handle --- resources/js/components/forms/builder/FieldInspector.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/js/components/forms/builder/FieldInspector.vue b/resources/js/components/forms/builder/FieldInspector.vue index 5cc45f23cd..644fe35d30 100644 --- a/resources/js/components/forms/builder/FieldInspector.vue +++ b/resources/js/components/forms/builder/FieldInspector.vue @@ -29,7 +29,7 @@ const errors = computed(() => { enum FieldInspectorTabs { Settings = 'settings', - Conditions = 'conditions', + Logic = 'conditions', Validation = 'validation', } @@ -205,7 +205,7 @@ onMounted(() => load()); - + @@ -242,7 +242,7 @@ onMounted(() => load()); - +
From 0c7a5714e29453ee2bb818c5f296ad499595b775 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Mon, 22 Jun 2026 14:35:38 +0100 Subject: [PATCH 02/10] Add "always hidden" option to logic dropdown --- .../components/field-conditions/Builder.vue | 36 +++++++++++++++---- .../forms/builder/FieldInspector.vue | 1 + 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/resources/js/components/field-conditions/Builder.vue b/resources/js/components/field-conditions/Builder.vue index d2ad8fadd7..2788652af5 100644 --- a/resources/js/components/field-conditions/Builder.vue +++ b/resources/js/components/field-conditions/Builder.vue @@ -12,6 +12,7 @@ const props = defineProps({ config: { type: Object, required: true }, suggestableFields: { type: Array, required: true }, allowCustomConditions: { type: Boolean, default: true }, + showAlwaysHideOption: { type: Boolean, default: false }, showAlwaysSave: { type: Boolean, default: true }, size: { type: String, default: 'base' }, }); @@ -23,11 +24,14 @@ const customMethod = ref(null); const conditions = ref([]); const alwaysSave = ref(false); -const whenOptions = [ - { label: __('Always show'), value: 'always', icon: 'eye' }, - { label: __('Show when'), value: 'if', icon: 'eye' }, - { label: __('Hide when'), value: 'unless', icon: 'eye-closed' }, -]; +const whenOptions = computed(() => { + return [ + { label: __('Always show'), value: 'always', icon: 'eye' }, + props.showAlwaysHideOption ? { label: __('Always hide'), value: 'always_hide', icon: 'eye-closed' } : null, + { label: __('Show when'), value: 'if', icon: 'eye' }, + { label: __('Hide when'), value: 'unless', icon: 'eye-closed' }, + ]; +}); const joinOptions = [ { label: __('All of the conditions pass'), short_label: __('And'), value: 'all' }, @@ -36,7 +40,7 @@ const joinOptions = [ ]; const isCustom = computed(() => type.value === 'custom'); -const hasConditions = computed(() => when.value !== 'always'); +const hasConditions = computed(() => when.value !== 'always' && when.value !== 'always_hide'); const add = () => { conditions.value.push({ @@ -78,6 +82,12 @@ const prepareEditableOperator = (operator) => { return operator; }; +const getInitialWhenState = () => { + if (props.showAlwaysHideOption) { + when.value = props.config.hidden ? 'always_hide' : 'always'; + } +}; + const getInitialConditions = () => { const key = KEYS.find((k) => props.config[k]); const configConditions = key ? props.config[key] : null; @@ -102,6 +112,19 @@ const getInitialConditions = () => { const getInitialAlwaysSaveState = () => alwaysSave.value = props.config?.always_save ?? false; +watch(() => props.config.hidden, (hidden) => { + when.value = hidden ? 'always_hide' : 'always'; + + if (initialized.value) { + KEYS.forEach((key) => delete props.config[key]); + getInitialConditions(); + } +}); + +watch(when, (value) => { + if (initialized.value) emit('updated', { hidden: value === 'always_hide' }); +}); + watch(saveableConditions, (conditions) => { if (initialized.value) emit('updated', conditions); }, { deep: true }); @@ -111,6 +134,7 @@ watch(alwaysSave, (value) => { }); onMounted(() => { + getInitialWhenState(); getInitialConditions(); getInitialAlwaysSaveState(); if (conditions.value.length === 0) add(); diff --git a/resources/js/components/forms/builder/FieldInspector.vue b/resources/js/components/forms/builder/FieldInspector.vue index 644fe35d30..ee38f2208c 100644 --- a/resources/js/components/forms/builder/FieldInspector.vue +++ b/resources/js/components/forms/builder/FieldInspector.vue @@ -261,6 +261,7 @@ onMounted(() => load()); :config="field.config" :suggestable-fields="suggestableConditionFields" :allow-custom-conditions="false" + :show-always-hide-option="true" size="sm" @updated="updateFieldConditions" > From 63d5915d18054bd703b63876767083cb45a10bb5 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Mon, 22 Jun 2026 14:44:46 +0100 Subject: [PATCH 03/10] fix error when `showAlwaysHideOption` prop isn't true --- resources/js/components/field-conditions/Builder.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/js/components/field-conditions/Builder.vue b/resources/js/components/field-conditions/Builder.vue index 2788652af5..53c1faee64 100644 --- a/resources/js/components/field-conditions/Builder.vue +++ b/resources/js/components/field-conditions/Builder.vue @@ -30,7 +30,7 @@ const whenOptions = computed(() => { props.showAlwaysHideOption ? { label: __('Always hide'), value: 'always_hide', icon: 'eye-closed' } : null, { label: __('Show when'), value: 'if', icon: 'eye' }, { label: __('Hide when'), value: 'unless', icon: 'eye-closed' }, - ]; + ].filter(Boolean); }); const joinOptions = [ From 62d09bd41c70f8f6dbabb628d88c7932a9f2ce81 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Mon, 22 Jun 2026 14:58:21 +0100 Subject: [PATCH 04/10] show hidden fields in logic builder --- resources/js/components/forms/logic/FieldLogic.vue | 7 +++++-- resources/js/components/forms/logic/FieldLogicRule.vue | 3 ++- resources/js/pages/forms/Logic.vue | 1 + src/Http/Controllers/CP/Forms/FormLogicController.php | 7 ++++++- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/resources/js/components/forms/logic/FieldLogic.vue b/resources/js/components/forms/logic/FieldLogic.vue index 4ff3cfec9c..244645bd35 100644 --- a/resources/js/components/forms/logic/FieldLogic.vue +++ b/resources/js/components/forms/logic/FieldLogic.vue @@ -4,6 +4,7 @@ import AddLogicRuleButton from './AddLogicRuleButton.vue'; import FieldLogicRule from './FieldLogicRule.vue'; import { computed, nextTick, ref, watch } from 'vue'; import { categories, categoryColorClasses } from '@/components/forms/builder/categories'; +import { KEYS } from '@/components/field-conditions/Constants.js'; const emit = defineEmits(['update:fields']); @@ -17,13 +18,13 @@ const collapsed = ref([]); const fieldsWithLogic = computed(() => { return props.fields.filter(field => { - return field.if || field.unless || field.if_any || field.unless_any; + return field.hidden || KEYS.some(key => field[key] && Object.keys(field[key]).length > 0); }); }); const fieldsWithoutLogic = computed(() => { return props.fields.filter(field => { - return !field.if && !field.unless && !field.if_any && !field.unless_any; + return !field.hidden && !KEYS.some(key => field[key] && Object.keys(field[key]).length > 0); }); }); @@ -63,6 +64,7 @@ const getFieldConfig = (field) => ({ const getConditionsConfig = (field) => ({ handle: field.handle, + hidden: field.hidden, if: field.if, unless: field.unless, if_any: field.if_any, @@ -100,6 +102,7 @@ const updateConditions = (fieldId, conditions) => { field._id === fieldId ? { ...field, + hidden: conditions.hidden ?? field.hidden, if: conditions.if || null, unless: conditions.unless || null, if_any: conditions.if_any || null, diff --git a/resources/js/components/forms/logic/FieldLogicRule.vue b/resources/js/components/forms/logic/FieldLogicRule.vue index e841fc3e5b..0b79d84c64 100644 --- a/resources/js/components/forms/logic/FieldLogicRule.vue +++ b/resources/js/components/forms/logic/FieldLogicRule.vue @@ -77,7 +77,7 @@ const previewParts = computed(() => { }); const collapsedSummary = computed(() => { - if (!hasConditions.value) return __('Always show'); + if (!hasConditions.value) return props.conditions.hidden ? __('Always hide') : __('Always show'); if (!previewParts.value) return __('Configure conditions'); return null; }); @@ -185,6 +185,7 @@ const onAlwaysSaveUpdated = (alwaysSave) => emit('update:conditions', { ...props :suggestable-fields :fieldtypes :allow-custom-conditions="false" + :show-always-hide-option="true" :show-always-save="false" size="sm" @updated="onConditionsUpdated" diff --git a/resources/js/pages/forms/Logic.vue b/resources/js/pages/forms/Logic.vue index 5a845f65d8..0ceb3001cf 100644 --- a/resources/js/pages/forms/Logic.vue +++ b/resources/js/pages/forms/Logic.vue @@ -58,6 +58,7 @@ const save = () => { })), fields: fields.value.map(field => ({ _id: field._id, + hidden: field.hidden, if: field.if, unless: field.unless, if_any: field.if_any, diff --git a/src/Http/Controllers/CP/Forms/FormLogicController.php b/src/Http/Controllers/CP/Forms/FormLogicController.php index 2e935245e6..265bee7dbd 100644 --- a/src/Http/Controllers/CP/Forms/FormLogicController.php +++ b/src/Http/Controllers/CP/Forms/FormLogicController.php @@ -83,6 +83,7 @@ private function fieldsToVue($formFields): array 'icon' => $fieldtype?->icon() ?? 'generic-field', 'category' => $fieldtype->categories()[0] ?? 'other', 'fieldtype' => $field['type'] ?? 'short_answer', + 'hidden' => $field['hidden'] ?? false, 'if' => $field['if'] ?? null, 'unless' => $field['unless'] ?? null, 'if_any' => $field['if_any'] ?? null, @@ -147,7 +148,11 @@ private function mergeLogicIntoForm(Request $request, Form $form): void $conditions = $fieldConditions->get($handle); $field = $fieldConfig['field'] ?? []; - unset($field['if'], $field['unless'], $field['if_any'], $field['unless_any'], $field['always_save']); + unset($field['hidden'], $field['if'], $field['unless'], $field['if_any'], $field['unless_any'], $field['always_save']); + + if (! empty($conditions['hidden'])) { + $field['hidden'] = $conditions['hidden']; + } if (! empty($conditions['if'])) { $field['if'] = $conditions['if']; From 3325fb7bcf9706fd63c5a73d383c4c1c85a297c7 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Mon, 22 Jun 2026 15:00:47 +0100 Subject: [PATCH 05/10] add test --- tests/Feature/Forms/FormLogicTest.php | 70 +++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/tests/Feature/Forms/FormLogicTest.php b/tests/Feature/Forms/FormLogicTest.php index 5a3818193a..1a3d93bd0a 100644 --- a/tests/Feature/Forms/FormLogicTest.php +++ b/tests/Feature/Forms/FormLogicTest.php @@ -289,6 +289,76 @@ public function it_can_update_field_conditions() $this->assertTrue($fields[1]['field']['always_save']); } + #[Test] + public function it_can_update_the_hidden_state_of_a_field() + { + Composer::shouldReceive('isInstalled')->with('statamic/forms-pro')->andReturn(true); + + $this->setTestRoles(['test' => ['access cp', 'configure forms']]); + $user = User::make()->assignRole('test')->save(); + + $form = tap(Form::make('test')->formFields([ + 'pages' => [ + [ + 'id' => 'page1', + 'sections' => [ + [ + 'display' => 'Section', + 'fields' => [ + [ + 'handle' => 'name', + 'field' => [ + 'type' => 'short_answer', + 'display' => 'Name', + 'hidden' => true, + ], + ], + [ + 'handle' => 'email', + 'field' => [ + 'type' => 'short_answer', + 'display' => 'Email', + ], + ], + ], + ], + ], + ], + ], + ]))->save(); + + // The hidden state should be provided when loading the logic page. + $this + ->actingAs($user) + ->get(cp_route('forms.logic.edit', $form->handle())) + ->assertSuccessful() + ->assertInertia(fn ($page) => $page + ->where('fields.0.hidden', true) + ->where('fields.1.hidden', false) + ); + + $payload = [ + 'pages' => [], + 'fields' => [ + ['_id' => 'name', 'hidden' => false], + ['_id' => 'email', 'hidden' => true], + ], + ]; + + $this + ->actingAs($user) + ->patch(cp_route('forms.logic.update', $form->handle()), $payload) + ->assertSuccessful(); + + $fields = Form::find('test')->formFields()->pages()[0]['sections'][0]['fields']; + + // The now-visible field shouldn't persist `hidden` since false is the default. + $this->assertArrayNotHasKey('hidden', $fields[0]['field']); + + // The now-hidden field should persist `hidden: true`. + $this->assertTrue($fields[1]['field']['hidden']); + } + #[Test] public function it_denies_update_without_permission() { From 2e5857d2b95c0125b0d6a7f77e1397b96c4994a4 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Mon, 22 Jun 2026 15:07:20 +0100 Subject: [PATCH 06/10] wip --- tests/Feature/Forms/FormLogicTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Feature/Forms/FormLogicTest.php b/tests/Feature/Forms/FormLogicTest.php index 1a3d93bd0a..c37272de21 100644 --- a/tests/Feature/Forms/FormLogicTest.php +++ b/tests/Feature/Forms/FormLogicTest.php @@ -354,7 +354,7 @@ public function it_can_update_the_hidden_state_of_a_field() // The now-visible field shouldn't persist `hidden` since false is the default. $this->assertArrayNotHasKey('hidden', $fields[0]['field']); - + // The now-hidden field should persist `hidden: true`. $this->assertTrue($fields[1]['field']['hidden']); } From 8dc7c9fa6804ff4adbbcef40c9da22509c5818d0 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Mon, 22 Jun 2026 15:07:25 +0100 Subject: [PATCH 07/10] wip --- resources/js/components/field-conditions/Builder.vue | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/resources/js/components/field-conditions/Builder.vue b/resources/js/components/field-conditions/Builder.vue index 53c1faee64..9188f61767 100644 --- a/resources/js/components/field-conditions/Builder.vue +++ b/resources/js/components/field-conditions/Builder.vue @@ -113,7 +113,11 @@ const getInitialConditions = () => { const getInitialAlwaysSaveState = () => alwaysSave.value = props.config?.always_save ?? false; watch(() => props.config.hidden, (hidden) => { - when.value = hidden ? 'always_hide' : 'always'; + if (hidden) { + when.value = 'always_hide'; + } else if (when.value === 'always_hide') { + when.value = 'always'; + } if (initialized.value) { KEYS.forEach((key) => delete props.config[key]); From b8ed3daf289d9302981af6ab8d3def1df725e5fd Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Mon, 22 Jun 2026 15:19:14 +0100 Subject: [PATCH 08/10] wip --- resources/js/components/field-conditions/Builder.vue | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/resources/js/components/field-conditions/Builder.vue b/resources/js/components/field-conditions/Builder.vue index 9188f61767..b24221cef1 100644 --- a/resources/js/components/field-conditions/Builder.vue +++ b/resources/js/components/field-conditions/Builder.vue @@ -56,7 +56,9 @@ const remove = (index) => conditions.value.splice(index, 1); const toggleCustom = () => type.value = isCustom.value ? 'all' : 'custom'; const saveableConditions = computed(() => { - const result = {}; + const result = Object.fromEntries(KEYS.map((key) => [key, null])); + if (!hasConditions.value) return result; + const key = type.value === 'any' ? `${when.value}_any` : when.value; const filtered = conditions.value.filter((c) => c.field && c.value); const prepared = new Converter().toBlueprint(filtered); @@ -118,11 +120,6 @@ watch(() => props.config.hidden, (hidden) => { } else if (when.value === 'always_hide') { when.value = 'always'; } - - if (initialized.value) { - KEYS.forEach((key) => delete props.config[key]); - getInitialConditions(); - } }); watch(when, (value) => { From fa79a3cc4aaef943cf0ecc9e4f82682666a58775 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Mon, 22 Jun 2026 15:23:59 +0100 Subject: [PATCH 09/10] wip --- resources/js/components/forms/builder/FieldInspector.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/js/components/forms/builder/FieldInspector.vue b/resources/js/components/forms/builder/FieldInspector.vue index ee38f2208c..1c4f1bbf1f 100644 --- a/resources/js/components/forms/builder/FieldInspector.vue +++ b/resources/js/components/forms/builder/FieldInspector.vue @@ -29,7 +29,7 @@ const errors = computed(() => { enum FieldInspectorTabs { Settings = 'settings', - Logic = 'conditions', + Logic = 'logic', Validation = 'validation', } From b87be2b8794dd2f9b3623fd7d0b3b38c0695e361 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Mon, 22 Jun 2026 15:24:31 +0100 Subject: [PATCH 10/10] rename tab here too --- resources/js/components/forms/builder/PageInspector.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/js/components/forms/builder/PageInspector.vue b/resources/js/components/forms/builder/PageInspector.vue index 13828dcfcd..c0f75e696a 100644 --- a/resources/js/components/forms/builder/PageInspector.vue +++ b/resources/js/components/forms/builder/PageInspector.vue @@ -8,7 +8,7 @@ const { deletePage, dirty, fieldtypes, inspecting: page, pages } = injectBuilder enum PageInspectorTabs { Settings = 'settings', - Conditions = 'conditions', + Logic = 'logic', } const confirmingDelete = ref(false); @@ -72,7 +72,7 @@ watch(() => page.value.rules, dirty, { deep: true }); - + @@ -106,7 +106,7 @@ watch(() => page.value.rules, dirty, { deep: true });
- +