From f258fbb35c3bc23c8276ad6329cd33335b751b16 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Wed, 27 Jul 2022 16:21:39 -0400 Subject: [PATCH 1/7] Add `always_save` field config. --- src/Http/Controllers/CP/Fields/FieldsController.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Http/Controllers/CP/Fields/FieldsController.php b/src/Http/Controllers/CP/Fields/FieldsController.php index 46a08ee4995..663dc97724c 100644 --- a/src/Http/Controllers/CP/Fields/FieldsController.php +++ b/src/Http/Controllers/CP/Fields/FieldsController.php @@ -140,6 +140,13 @@ protected function blueprint($blueprint) 'type' => 'select', 'width' => 33, ], + 'always_save' => [ + 'display' => __('Always Save'), + 'instructions' => __('statamic::messages.fields_always_save_instructions'), + 'type' => 'toggle', + 'validate' => 'boolean', + 'default' => false, + ], ]); foreach ($prepends->reverse() as $handle => $prepend) { From 1af4bee4a47f32e45da05e9c4e52672cae8dd467 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Wed, 27 Jul 2022 16:21:52 -0400 Subject: [PATCH 2/7] Clarify using instructions that this overrides data flow when using field conditions. --- resources/lang/en/messages.php | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/lang/en/messages.php b/resources/lang/en/messages.php index 8e27a31697d..803f77ce5d0 100644 --- a/resources/lang/en/messages.php +++ b/resources/lang/en/messages.php @@ -72,6 +72,7 @@ 'fields_instructions_position_instructions' => 'Show instructions above or below the field.', 'fields_listable_instructions' => 'Control the listing column visibility.', 'fields_visibility_instructions' => 'Control field visibility on publish forms.', + 'fields_always_save_instructions' => 'Always save field value, regardless of how field conditions are evaluated.', 'fieldset_import_fieldset_instructions' => 'The fieldset to be imported.', 'fieldset_import_prefix_instructions' => 'The prefix that should be applied to each field when they are imported. eg. hero_', 'fieldset_intro' => 'Fieldsets are an optional companion to blueprints, acting as reusable partials that can be used within blueprints.', From 1ed59d0087eb03a47f82fdfdc8c32bf656075f18 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Wed, 27 Jul 2022 16:22:37 -0400 Subject: [PATCH 3/7] Pass this config to vue-land. --- src/Fields/Field.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Fields/Field.php b/src/Fields/Field.php index a566be5c140..3253da4b41c 100644 --- a/src/Fields/Field.php +++ b/src/Fields/Field.php @@ -101,6 +101,11 @@ public function visibility() return $visibility ?? 'visible'; } + public function alwaysSave() + { + return Arr::get($this->config, 'always_save', false); + } + public function rules() { $rules = [$this->handle => $this->addNullableRule(array_merge( @@ -231,6 +236,7 @@ public function toPublishArray() 'required' => $this->isRequired(), 'visibility' => $this->visibility(), 'read_only' => $this->visibility() === 'read_only', // Deprecated: Addon fieldtypes should now reference new `visibility` state. + 'always_save' => $this->alwaysSave(), ]); } From 13714704a50b7435fac605e958aaeeceb7a74968 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Wed, 27 Jul 2022 16:23:17 -0400 Subject: [PATCH 4/7] Never omit value on submission when field config has `always_save: true`. --- .../js/components/field-conditions/ValidatorMixin.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/resources/js/components/field-conditions/ValidatorMixin.js b/resources/js/components/field-conditions/ValidatorMixin.js index b325a0f5cde..800eaf7d4f1 100644 --- a/resources/js/components/field-conditions/ValidatorMixin.js +++ b/resources/js/components/field-conditions/ValidatorMixin.js @@ -28,6 +28,17 @@ export default { let validator = new Validator(field, this.values, this.$store, this.storeName); let passes = validator.passesConditions(); + // If the field is configured to always save, never omit value. + if (field.always_save === true) { + this.$store.commit(`publish/${this.storeName}/setHiddenField`, { + dottedKey: dottedFieldPath, + hidden: ! passes, + omitValue: false, + }); + + return passes; + } + // Ensure DOM is updated to ensure all revealers are properly loaded and tracked before committing to store. this.$nextTick(() => { let hasRevealerCondition = validator.hasRevealerCondition(dottedPrefix); From 846aaa6beda7820f1131949335bf3c301ff14f21 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Wed, 27 Jul 2022 16:54:10 -0400 Subject: [PATCH 5/7] Pass tests again. --- tests/Fields/BlueprintTest.php | 4 ++++ tests/Fields/FieldTest.php | 1 + tests/Fields/FieldsTest.php | 4 ++++ tests/Fields/SectionTest.php | 2 ++ tests/Fieldtypes/NestedFieldsTest.php | 1 + 5 files changed, 12 insertions(+) diff --git a/tests/Fields/BlueprintTest.php b/tests/Fields/BlueprintTest.php index 106ac0987b1..833f74f1746 100644 --- a/tests/Fields/BlueprintTest.php +++ b/tests/Fields/BlueprintTest.php @@ -365,6 +365,7 @@ public function converts_to_array_suitable_for_rendering_fields_in_publish_compo 'default' => null, 'visibility' => 'visible', 'read_only' => false, // deprecated + 'always_save' => false, ], ], ], @@ -388,6 +389,7 @@ public function converts_to_array_suitable_for_rendering_fields_in_publish_compo 'default' => null, 'visibility' => 'visible', 'read_only' => false, // deprecated + 'always_save' => false, ], ], ], @@ -464,6 +466,7 @@ public function converts_to_array_suitable_for_rendering_prefixed_conditional_fi 'default' => null, 'visibility' => 'visible', 'read_only' => false, // deprecated + 'always_save' => false, ], [ 'handle' => 'nested_deeper_two', @@ -482,6 +485,7 @@ public function converts_to_array_suitable_for_rendering_prefixed_conditional_fi 'default' => null, 'visibility' => 'visible', 'read_only' => false, // deprecated + 'always_save' => false, ], ], ], diff --git a/tests/Fields/FieldTest.php b/tests/Fields/FieldTest.php index 87aefe0cb57..dfbd7726eac 100644 --- a/tests/Fields/FieldTest.php +++ b/tests/Fields/FieldTest.php @@ -326,6 +326,7 @@ public function preProcess($data) 'validate' => 'required', 'visibility' => 'visible', 'read_only' => false, // deprecated + 'always_save' => false, 'component' => 'example', 'a_config_field_with_pre_processing' => 'foo preprocessed', 'a_config_field_without_pre_processing' => 'foo', diff --git a/tests/Fields/FieldsTest.php b/tests/Fields/FieldsTest.php index 8a8ec0d31bd..d542d2e7fc3 100644 --- a/tests/Fields/FieldsTest.php +++ b/tests/Fields/FieldsTest.php @@ -424,6 +424,7 @@ public function converts_to_array_suitable_for_rendering_fields_in_publish_compo 'default' => null, 'visibility' => 'visible', 'read_only' => false, // deprecated + 'always_save' => false, ], [ 'handle' => 'two', @@ -440,6 +441,7 @@ public function converts_to_array_suitable_for_rendering_fields_in_publish_compo 'default' => null, 'visibility' => 'visible', 'read_only' => false, // deprecated + 'always_save' => false, ], ], $fields->toPublishArray()); } @@ -497,6 +499,7 @@ public function converts_to_array_suitable_for_rendering_prefixed_conditional_fi 'default' => null, 'visibility' => 'visible', 'read_only' => false, // deprecated + 'always_save' => false, ], [ 'handle' => 'nested_deeper_two', @@ -515,6 +518,7 @@ public function converts_to_array_suitable_for_rendering_prefixed_conditional_fi 'default' => null, 'visibility' => 'visible', 'read_only' => false, // deprecated + 'always_save' => false, ], ], $fields->toPublishArray()); } diff --git a/tests/Fields/SectionTest.php b/tests/Fields/SectionTest.php index ed11886efcb..4ab4aac0ef6 100644 --- a/tests/Fields/SectionTest.php +++ b/tests/Fields/SectionTest.php @@ -150,6 +150,7 @@ public function converts_to_array_suitable_for_rendering_fields_in_publish_compo 'default' => null, 'visibility' => 'visible', 'read_only' => false, // deprecated + 'always_save' => false, ], [ 'handle' => 'two', @@ -166,6 +167,7 @@ public function converts_to_array_suitable_for_rendering_fields_in_publish_compo 'default' => null, 'visibility' => 'visible', 'read_only' => false, // deprecated + 'always_save' => false, ], ], ], $section->toPublishArray()); diff --git a/tests/Fieldtypes/NestedFieldsTest.php b/tests/Fieldtypes/NestedFieldsTest.php index 01a1c83fe2a..c718ed67a9a 100644 --- a/tests/Fieldtypes/NestedFieldsTest.php +++ b/tests/Fieldtypes/NestedFieldsTest.php @@ -84,6 +84,7 @@ public function preProcess($data) 'required' => true, 'visibility' => 'visible', 'read_only' => false, // deprecated + 'always_save' => false, ], ], $actual); } From 89bb65e5c3d62e4345833d9ccb45e4111cd29d09 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Thu, 4 Aug 2022 17:42:48 -0400 Subject: [PATCH 6/7] Pull new test case changes in from #6443. --- .../js/tests/FieldConditionsValidator.test.js | 127 +++++++++++------- 1 file changed, 75 insertions(+), 52 deletions(-) diff --git a/resources/js/tests/FieldConditionsValidator.test.js b/resources/js/tests/FieldConditionsValidator.test.js index 86988174862..b9dc9b34799 100644 --- a/resources/js/tests/FieldConditionsValidator.test.js +++ b/resources/js/tests/FieldConditionsValidator.test.js @@ -4,36 +4,51 @@ import ValidatesFieldConditions from '../components/field-conditions/ValidatorMi Vue.use(Vuex); const Store = new Vuex.Store({ - state: { - publish: { - base: { - values: {}, - hiddenFields: {}, - } - }, + modules: { statamic: { - conditions: {}, + namespaced: true, + state: { + conditions: {}, + }, + mutations: { + setCondition(state, payload) { + state.conditions[payload.name] = payload.condition; + }, + }, }, + publish: { + namespaced: true, + modules: { + base: { + namespaced: true, + state: { + values: {}, + hiddenFields: {}, + revealerFields: [], + }, + mutations: { + setValues(state, values) { + state.values = values; + }, + setHiddenField(state, field) { + state.hiddenFields[field.dottedKey] = { + hidden: field.hidden, + omitValue: field.omitValue, + }; + }, + setRevealerField(state, dottedKey) { + state.revealerFields.push(dottedKey); + }, + } + } + } + } }, - mutations: { - setValues(state, values) { - state.publish.base.values = values; - }, - setHiddenField(state, field) { - state.publish.base.hiddenFields[field.dottedKey] = { - hidden: field.hidden, - omitValue: field.omitValue, - }; - }, - setCondition(state, payload) { - state.statamic.conditions[payload.name] = payload.condition; - }, - } }); const Statamic = { $conditions: { - add: (name, condition) => Store.commit('setCondition', {name, condition}) + add: (name, condition) => Store.commit('statamic/setCondition', {name, condition}) } }; @@ -47,13 +62,31 @@ const Fields = new Vue({ } }, methods: { - setValues(values) { + setValues(values, nestedKey) { this.values = values; - Store.commit('setValues', values); + let storeValues = {}; + if (nestedKey) { + storeValues[nestedKey] = values; + } else { + storeValues = values; + } + Store.commit('publish/base/setValues', storeValues); }, setStoreValues(values) { - Store.commit('setValues', values); - } + Store.commit('publish/base/setValues', values); + }, + setHiddenField(payload) { + Store.commit('publish/base/setHiddenField', payload); + }, + setHiddenFieldsState: async (fieldConfigs, dottedPrefix) => { + fieldConfigs.filter(fieldConfig => fieldConfig.type === 'revealer').forEach(fieldConfig => { + Store.commit('publish/base/setRevealerField', dottedPrefix ? `${dottedPrefix}.${fieldConfig.handle}`: fieldConfig.handle) + }); + fieldConfigs.forEach(fieldConfig => { + Fields.showField(fieldConfig, dottedPrefix ? `${dottedPrefix}.${fieldConfig.handle}`: null) + }); + await Vue.nextTick(); + }, } }); @@ -271,15 +304,18 @@ test('it shows or hides when any of the conditions are met', () => { test('it can run conditions on nested data', () => { Fields.setValues({ - user: { - address: { - country: 'Canada' - } + name: 'Han', + address: { + country: 'Canada' } - }); - - expect(showFieldIf({'user.address.country': 'Canada'})).toBe(true); - expect(showFieldIf({'user.address.country': 'Australia'})).toBe(false); + }, 'user'); + + expect(showFieldIf({'name': 'Han'})).toBe(true); + expect(showFieldIf({'name': 'Chewy'})).toBe(false); + expect(showFieldIf({'address.country': 'Canada'})).toBe(true); + expect(showFieldIf({'address.country': 'Australia'})).toBe(false); + expect(showFieldIf({'root.user.address.country': 'Canada'})).toBe(true); + expect(showFieldIf({'root.user.address.country': 'Australia'})).toBe(false); }); test('it can run conditions on root store values', () => { @@ -398,31 +434,18 @@ test('it can externally force hide a field before validator conditions are evalu expect(Fields.showField({handle: 'some_field'})).toBe(true); expect(Fields.showField({handle: 'last_name', if: {first_name: 'Jesse'}})).toBe(true); - Store.commit('setHiddenField', { + Fields.setHiddenField({ dottedKey: 'last_name', hidden: 'force', - omitValue: true, + omitValue: false, }); - Store.commit('setHiddenField', { + Fields.setHiddenField({ dottedKey: 'some_field', hidden: 'force', - omitValue: true, + omitValue: false, }); expect(Fields.showField({handle: 'some_field'})).toBe(false); expect(Fields.showField({handle: 'last_name', if: {first_name: 'Jesse'}})).toBe(false); }); - -// TODO: Implement wildcards using asterisks? Is this useful? -// test('it can run conditions on nested data using wildcards', () => { -// Fields.setValues({ -// related_posts: [ -// {title: 'Learning Laravel', slug: 'learning-laravel'}, -// {title: 'Learning Vue', slug: 'learning-vue'}, -// ] -// }); - -// expect(showFieldIf({'related_posts.*.title': 'Learning Vue'})).toBe(true); -// expect(showFieldIf({'related_posts.*.title': 'Learning Vim'})).toBe(false); -// }); From 48949cce721599cf19b4a9be9c350adea468b4eb Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Thu, 4 Aug 2022 17:48:25 -0400 Subject: [PATCH 7/7] Add test coverage for `always_save` config. --- .../js/tests/FieldConditionsValidator.test.js | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/resources/js/tests/FieldConditionsValidator.test.js b/resources/js/tests/FieldConditionsValidator.test.js index b9dc9b34799..088d7b6830f 100644 --- a/resources/js/tests/FieldConditionsValidator.test.js +++ b/resources/js/tests/FieldConditionsValidator.test.js @@ -449,3 +449,37 @@ test('it can externally force hide a field before validator conditions are evalu expect(Fields.showField({handle: 'some_field'})).toBe(false); expect(Fields.showField({handle: 'last_name', if: {first_name: 'Jesse'}})).toBe(false); }); + +test('it never omits fields with always_save config', async () => { + Fields.setValues({ + is_online_event: false, + venue: false, + }); + + await Fields.setHiddenFieldsState([ + {handle: 'is_online_event'}, + {handle: 'venue', if: {is_online_event: true}, always_save: true}, + ]); + + expect(Store.state.publish.base.hiddenFields['is_online_event'].hidden).toBe(false); + expect(Store.state.publish.base.hiddenFields['venue'].hidden).toBe(true); + expect(Store.state.publish.base.hiddenFields['is_online_event'].omitValue).toBe(false); + expect(Store.state.publish.base.hiddenFields['venue'].omitValue).toBe(false); +}); + +test('it never omits nested fields with always_save config', async () => { + Fields.setValues({ + is_online_event: false, + venue: false, + }, 'nested'); + + await Fields.setHiddenFieldsState([ + {handle: 'is_online_event'}, + {handle: 'venue', if: {is_online_event: true}, always_save: true}, + ], 'nested'); + + expect(Store.state.publish.base.hiddenFields['nested.is_online_event'].hidden).toBe(false); + expect(Store.state.publish.base.hiddenFields['nested.venue'].hidden).toBe(true); + expect(Store.state.publish.base.hiddenFields['nested.is_online_event'].omitValue).toBe(false); + expect(Store.state.publish.base.hiddenFields['nested.venue'].omitValue).toBe(false); +});