diff --git a/resources/js/components/data-list/HasHiddenFields.js b/resources/js/components/data-list/HasHiddenFields.js index 85bc4c74815..7ce09bff2b5 100644 --- a/resources/js/components/data-list/HasHiddenFields.js +++ b/resources/js/components/data-list/HasHiddenFields.js @@ -13,12 +13,12 @@ export default { }, visibleValues() { - let hiddenFields = _.chain(this.hiddenFields) - .pick(field => field.hidden && field.omitValue) + let omittableFields = _.chain(this.hiddenFields) + .pick(field => field.omitValue) .keys() .value(); - return new HiddenValuesOmitter(this.values, this.jsonSubmittingFields).omit(hiddenFields); + return new HiddenValuesOmitter(this.values, this.jsonSubmittingFields).omit(omittableFields); }, } diff --git a/resources/js/components/field-conditions/Validator.js b/resources/js/components/field-conditions/Validator.js index ce839ea1130..3492158eb7f 100644 --- a/resources/js/components/field-conditions/Validator.js +++ b/resources/js/components/field-conditions/Validator.js @@ -31,17 +31,15 @@ export default class { this.converter = new Converter; } - passesConditions() { - let conditions = this.getConditions(); + passesConditions(specificConditions) { + let conditions = specificConditions || this.getConditions(); if (conditions === undefined) { return true; - } else if (isString(conditions)) { + } else if (this.isCustomConditionWithoutTarget(conditions)) { return this.passesCustomCondition(this.prepareCondition(conditions)); } - conditions = this.converter.fromBlueprint(conditions, this.field.prefix); - let passes = this.passOnAny ? this.passesAnyConditions(conditions) : this.passesAllConditions(conditions); @@ -67,7 +65,15 @@ export default class { this.showOnPass = false; } - return this.field[key]; + let conditions = this.field[key]; + + return this.isCustomConditionWithoutTarget(conditions) + ? conditions + : this.converter.fromBlueprint(conditions, this.field.prefix); + } + + isCustomConditionWithoutTarget(conditions) { + return isString(conditions); } passesAllConditions(conditions) { @@ -261,39 +267,29 @@ export default class { return this.showOnPass ? passes : ! passes; } - getCheckedFieldPaths(dottedPrefix) { + passesNonRevealerConditions(dottedPrefix) { let conditions = this.getConditions(); - if (conditions === undefined || isString(conditions)) { - return false; + if (this.isCustomConditionWithoutTarget(conditions)) { + return this.passesConditions(conditions); } - let checkedFields = this.converter - .fromBlueprint(conditions, this.field.prefix) - .map(field => field.field); + let revealerFields = data_get(this.store.state.publish[this.storeName], 'revealerFields', []); - if (dottedPrefix) { - checkedFields = checkedFields.map(field => { - return field.startsWith('root.') - ? field.replace(/^root\./, '') - : dottedPrefix + '.' + field; - }); - } + let nonRevealerConditions = chain(this.getConditions()) + .reject(condition => revealerFields.includes(this.relativeLhsToAbsoluteFieldPath(condition.field, dottedPrefix))) + .value(); - return checkedFields; + return this.passesConditions(nonRevealerConditions); } - hasRevealerCondition(dottedPrefix) { - if (! this.store || ! this.storeName) { - return false; - } - - let revealerFields = data_get(this.store.state.publish[this.storeName], 'revealerFields', []); - - if (! revealerFields.length) { - return false; + relativeLhsToAbsoluteFieldPath(lhs, dottedPrefix) { + if (! dottedPrefix) { + return lhs; } - return intersection(this.getCheckedFieldPaths(dottedPrefix), revealerFields).length > 0; + return lhs.startsWith('root.') + ? lhs.replace(/^root\./, '') + : dottedPrefix + '.' + lhs; } } diff --git a/resources/js/components/field-conditions/ValidatorMixin.js b/resources/js/components/field-conditions/ValidatorMixin.js index b325a0f5cde..4d3c1ee154b 100644 --- a/resources/js/components/field-conditions/ValidatorMixin.js +++ b/resources/js/components/field-conditions/ValidatorMixin.js @@ -30,12 +30,10 @@ export default { // Ensure DOM is updated to ensure all revealers are properly loaded and tracked before committing to store. this.$nextTick(() => { - let hasRevealerCondition = validator.hasRevealerCondition(dottedPrefix); - this.$store.commit(`publish/${this.storeName}/setHiddenField`, { dottedKey: dottedFieldPath, hidden: ! passes, - omitValue: (! passes) && (! hasRevealerCondition), + omitValue: field.type === 'revealer' || ! validator.passesNonRevealerConditions(dottedPrefix), }); }); diff --git a/resources/js/tests/FieldConditionsValidator.test.js b/resources/js/tests/FieldConditionsValidator.test.js index 86988174862..322e6a8d1a4 100644 --- a/resources/js/tests/FieldConditionsValidator.test.js +++ b/resources/js/tests/FieldConditionsValidator.test.js @@ -4,36 +4,56 @@ 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); + }, + reset(state) { + state.values = {}; + state.hiddenFields = {}; + state.revealerFields = []; + }, + } + } + } + } }, - 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 +67,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(); + }, } }); @@ -63,6 +101,7 @@ let showFieldIf = function (conditions=null) { afterEach(() => { Fields.values = {}; + Store.commit('publish/base/reset'); }); test('it shows field by default', () => { @@ -271,15 +310,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', () => { @@ -291,6 +333,28 @@ test('it can run conditions on root store values', () => { expect(showFieldIf({'root.favorite_foods': 'contains lasagna'})).toBe(true); }); +test('it can run conditions on prefixed fields', async () => { + Fields.setValues({ + prefixed_first_name: 'Rincess', + prefixed_last_name: 'Pleia' + }); + + expect(Fields.showField({prefix: 'prefixed_', if: {first_name: 'is Rincess', last_name: 'is Pleia'}})).toBe(true); + expect(Fields.showField({prefix: 'prefixed_', if: {first_name: 'is Rincess', last_name: 'is Holo'}})).toBe(false); +}); + +test('it can run conditions on nested prefixed fields', async () => { + Fields.setValues({ + prefixed_first_name: 'Rincess', + prefixed_last_name: 'Pleia' + }, 'nested'); + + expect(Fields.showField({prefix: 'prefixed_', if: {first_name: 'is Rincess', last_name: 'is Pleia'}})).toBe(true); + expect(Fields.showField({prefix: 'prefixed_', if: {first_name: 'is Rincess', last_name: 'is Holo'}})).toBe(false); + expect(Fields.showField({if: {'root.nested.prefixed_last_name': 'is Pleia'}})).toBe(true); + expect(Fields.showField({if: {'root.nested.prefixed_last_name': 'is Holo'}})).toBe(false); +}); + test('it can call a custom function', () => { Fields.setValues({ favorite_animals: ['cats', 'dogs'], @@ -398,31 +462,238 @@ 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'}, -// ] -// }); +test('it force hides fields with hidden visibility config', async () => { + await Fields.setHiddenFieldsState([ + {handle: 'first_name'}, + {handle: 'last_name', visibility: 'hidden'}, + ]); + + expect(Store.state.publish.base.hiddenFields['first_name'].hidden).toBe(false); + expect(Store.state.publish.base.hiddenFields['last_name'].hidden).toBe('force'); + expect(Store.state.publish.base.hiddenFields['first_name'].omitValue).toBe(false); + expect(Store.state.publish.base.hiddenFields['last_name'].omitValue).toBe(false); +}); + +test('it tells omitter to omit hidden fields by default', async () => { + Fields.setValues({ + is_online_event: false, + venue: false, + }); + + await Fields.setHiddenFieldsState([ + {handle: 'is_online_event'}, + {handle: 'venue', if: {is_online_event: true}}, + ]); -// expect(showFieldIf({'related_posts.*.title': 'Learning Vue'})).toBe(true); -// expect(showFieldIf({'related_posts.*.title': 'Learning Vim'})).toBe(false); -// }); + 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(true); +}); + +test('it tells omitter to omit nested hidden fields by default', async () => { + Fields.setValues({ + is_online_event: false, + event_venue: false, + }, 'nested'); + + await Fields.setHiddenFieldsState([ + {handle: 'is_online_event'}, + {handle: 'venue', if: {is_online_event: 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(true); +}); + +test('it tells omitter to omit revealer fields', async () => { + Fields.setValues({ + revealer_toggle: false, + regular_toggle: false, + }); + + await Fields.setHiddenFieldsState([ + {handle: 'revealer_toggle', type: 'revealer'}, + {handle: 'regular_toggle', type: 'regular'}, + ]); + + expect(Store.state.publish.base.hiddenFields['revealer_toggle'].hidden).toBe(false); + expect(Store.state.publish.base.hiddenFields['regular_toggle'].hidden).toBe(false); + expect(Store.state.publish.base.hiddenFields['revealer_toggle'].omitValue).toBe(true); + expect(Store.state.publish.base.hiddenFields['regular_toggle'].omitValue).toBe(false); +}); + +test('it tells omitter to omit nested revealer fields', async () => { + Fields.setValues({ + revealer_toggle: false, + regular_toggle: false, + }, 'nested'); + + await Fields.setHiddenFieldsState([ + {handle: 'revealer_toggle', type: 'revealer'}, + {handle: 'regular_toggle', type: 'regular'}, + ], 'nested'); + + expect(Store.state.publish.base.hiddenFields['nested.revealer_toggle'].hidden).toBe(false); + expect(Store.state.publish.base.hiddenFields['nested.regular_toggle'].hidden).toBe(false); + expect(Store.state.publish.base.hiddenFields['nested.revealer_toggle'].omitValue).toBe(true); + expect(Store.state.publish.base.hiddenFields['nested.regular_toggle'].omitValue).toBe(false); +}); + +test('it tells omitter not omit revealer-hidden fields', async () => { + Fields.setValues({ + show_more_info: false, + event_venue: false, + }); + + await Fields.setHiddenFieldsState([ + {handle: 'show_more_info', type: 'revealer'}, + {handle: 'venue', if: {show_more_info: true}}, + ]); + + expect(Store.state.publish.base.hiddenFields['show_more_info'].hidden).toBe(false); + expect(Store.state.publish.base.hiddenFields['venue'].hidden).toBe(true); + expect(Store.state.publish.base.hiddenFields['show_more_info'].omitValue).toBe(true); + expect(Store.state.publish.base.hiddenFields['venue'].omitValue).toBe(false); +}); + +test('it tells omitter not omit nested revealer-hidden fields', async () => { + Fields.setValues({ + show_more_info: false, + event_venue: false, + }, 'nested'); + + await Fields.setHiddenFieldsState([ + {handle: 'show_more_info', type: 'revealer'}, + {handle: 'venue', if: {show_more_info: true}}, + ], 'nested'); + + expect(Store.state.publish.base.hiddenFields['nested.show_more_info'].hidden).toBe(false); + expect(Store.state.publish.base.hiddenFields['nested.venue'].hidden).toBe(true); + expect(Store.state.publish.base.hiddenFields['nested.show_more_info'].omitValue).toBe(true); + expect(Store.state.publish.base.hiddenFields['nested.venue'].omitValue).toBe(false); +}); + +test('it tells omitter not omit prefixed revealer-hidden fields', async () => { + Fields.setValues({ + prefixed_show_more_info: false, + prefixed_event_venue: false, + }); + + await Fields.setHiddenFieldsState([ + {handle: 'prefixed_show_more_info', prefix: 'prefixed_', type: 'revealer'}, + {handle: 'prefixed_venue', prefix: 'prefixed_', if: {show_more_info: true}}, + ]); + + expect(Store.state.publish.base.hiddenFields['prefixed_show_more_info'].hidden).toBe(false); + expect(Store.state.publish.base.hiddenFields['prefixed_venue'].hidden).toBe(true); + expect(Store.state.publish.base.hiddenFields['prefixed_show_more_info'].omitValue).toBe(true); + expect(Store.state.publish.base.hiddenFields['prefixed_venue'].omitValue).toBe(false); +}); + +test('it tells omitter not omit nested prefixed revealer-hidden fields', async () => { + Fields.setValues({ + prefixed_show_more_info: false, + prefixed_event_venue: false, + }, 'nested'); + + await Fields.setHiddenFieldsState([ + {handle: 'prefixed_show_more_info', prefix: 'prefixed_', type: 'revealer'}, + {handle: 'prefixed_venue', prefix: 'prefixed_', if: {show_more_info: true}}, + ], 'nested'); + + expect(Store.state.publish.base.hiddenFields['nested.prefixed_show_more_info'].hidden).toBe(false); + expect(Store.state.publish.base.hiddenFields['nested.prefixed_venue'].hidden).toBe(true); + expect(Store.state.publish.base.hiddenFields['nested.prefixed_show_more_info'].omitValue).toBe(true); + expect(Store.state.publish.base.hiddenFields['nested.prefixed_venue'].omitValue).toBe(false); +}); + +test('it properly omits revealer-hidden fields when multiple conditions are set', async () => { + Fields.setValues({ + show_more_info: false, + has_second_event_venue: true, + has_third_event_venue: false, + event_venue_one: 'Stadium One', + event_venue_two: 'Stadium Two', + event_venue_three: false, + }); + + await Fields.setHiddenFieldsState([ + {handle: 'show_more_info', type: 'revealer'}, + {handle: 'has_second_event_venue', type: 'toggle', if: {show_more_info: true}}, + {handle: 'has_third_event_venue', type: 'toggle', if: {show_more_info: true}}, + {handle: 'event_venue_one', if: {show_more_info: true}}, + {handle: 'event_venue_two', if: {show_more_info: true, has_second_event_venue: true}}, + {handle: 'event_venue_three', if: {show_more_info: true, has_third_event_venue: true}}, + ]); + + expect(Store.state.publish.base.hiddenFields['show_more_info'].hidden).toBe(false); + expect(Store.state.publish.base.hiddenFields['has_second_event_venue'].hidden).toBe(true); + expect(Store.state.publish.base.hiddenFields['has_third_event_venue'].hidden).toBe(true); + expect(Store.state.publish.base.hiddenFields['event_venue_one'].hidden).toBe(true); + expect(Store.state.publish.base.hiddenFields['event_venue_two'].hidden).toBe(true); + expect(Store.state.publish.base.hiddenFields['event_venue_three'].hidden).toBe(true); + + expect(Store.state.publish.base.hiddenFields['show_more_info'].omitValue).toBe(true); + expect(Store.state.publish.base.hiddenFields['has_second_event_venue'].omitValue).toBe(false); + expect(Store.state.publish.base.hiddenFields['has_third_event_venue'].omitValue).toBe(false); + expect(Store.state.publish.base.hiddenFields['event_venue_one'].omitValue).toBe(false); + expect(Store.state.publish.base.hiddenFields['event_venue_two'].omitValue).toBe(false); + + // Though this third venue is hidden by a revealer, it's also disabled by a regular toggle condition, so it should actually be omitted... + expect(Store.state.publish.base.hiddenFields['event_venue_three'].omitValue).toBe(true); +}); + +test('it properly omits nested revealer-hidden fields when multiple conditions are set', async () => { + Fields.setValues({ + show_more_info: false, + has_second_event_venue: true, + has_third_event_venue: false, + event_venue_one: 'Stadium One', + event_venue_two: 'Stadium Two', + event_venue_three: false, + }, 'nested'); + + await Fields.setHiddenFieldsState([ + {handle: 'show_more_info', type: 'revealer'}, + {handle: 'has_second_event_venue', type: 'toggle', if: {show_more_info: true}}, + {handle: 'has_third_event_venue', type: 'toggle', if: {show_more_info: true}}, + {handle: 'event_venue_one', if: {show_more_info: true}}, + {handle: 'event_venue_two', if: {show_more_info: true, has_second_event_venue: true}}, + {handle: 'event_venue_three', if: {show_more_info: true, has_third_event_venue: true}}, + ], 'nested'); + + expect(Store.state.publish.base.hiddenFields['nested.show_more_info'].hidden).toBe(false); + expect(Store.state.publish.base.hiddenFields['nested.has_second_event_venue'].hidden).toBe(true); + expect(Store.state.publish.base.hiddenFields['nested.has_third_event_venue'].hidden).toBe(true); + expect(Store.state.publish.base.hiddenFields['nested.event_venue_one'].hidden).toBe(true); + expect(Store.state.publish.base.hiddenFields['nested.event_venue_two'].hidden).toBe(true); + expect(Store.state.publish.base.hiddenFields['nested.event_venue_three'].hidden).toBe(true); + + expect(Store.state.publish.base.hiddenFields['nested.show_more_info'].omitValue).toBe(true); + expect(Store.state.publish.base.hiddenFields['nested.has_second_event_venue'].omitValue).toBe(false); + expect(Store.state.publish.base.hiddenFields['nested.has_third_event_venue'].omitValue).toBe(false); + expect(Store.state.publish.base.hiddenFields['nested.event_venue_one'].omitValue).toBe(false); + expect(Store.state.publish.base.hiddenFields['nested.event_venue_two'].omitValue).toBe(false); + + // Though this third venue is hidden by a revealer, it's also disabled by a regular toggle condition, so it should actually be omitted... + expect(Store.state.publish.base.hiddenFields['nested.event_venue_three'].omitValue).toBe(true); +});