From 07ca56663b4163665dae31cfc3a0ca1736998497 Mon Sep 17 00:00:00 2001 From: Florian Brinkmann Date: Wed, 24 Jan 2024 09:41:38 +0100 Subject: [PATCH 01/25] feat: allow selection of parent level in field condition via `parent` keyword --- .../components/field-conditions/Converter.js | 2 +- .../components/field-conditions/Validator.js | 23 +++++++++++++++---- .../field-conditions/ValidatorMixin.js | 2 +- .../js/tests/FieldConditionsConverter.test.js | 14 +++++++++++ .../js/tests/FieldConditionsValidator.test.js | 13 ++++++++--- 5 files changed, 45 insertions(+), 9 deletions(-) diff --git a/resources/js/components/field-conditions/Converter.js b/resources/js/components/field-conditions/Converter.js index 6b18fe67a9..cecebd8898 100644 --- a/resources/js/components/field-conditions/Converter.js +++ b/resources/js/components/field-conditions/Converter.js @@ -32,7 +32,7 @@ export default class { } getScopedFieldHandle(field, prefix) { - if (field.startsWith('root.') || ! prefix) { + if (field.startsWith('root.') || field.startsWith('parent.') || ! prefix) { return field; } diff --git a/resources/js/components/field-conditions/Validator.js b/resources/js/components/field-conditions/Validator.js index b393e3856c..92a3df7c21 100644 --- a/resources/js/components/field-conditions/Validator.js +++ b/resources/js/components/field-conditions/Validator.js @@ -20,8 +20,9 @@ const NUMBER_SPECIFIC_COMPARISONS = [ ]; export default class { - constructor(field, values, store, storeName) { + constructor(field, values, store, storeName, dottedFieldPath = '') { this.field = field; + this.dottedFieldPath = dottedFieldPath; this.values = values; this.rootValues = store ? store.state.publish[storeName].values : false; this.store = store; @@ -204,9 +205,23 @@ export default class { } getFieldValue(field) { - return field.startsWith('root.') - ? data_get(this.rootValues, field.replace(new RegExp('^root.'), '')) - : data_get(this.values, field); + if (field.startsWith('root.')) { + return data_get(this.rootValues, field.replace(new RegExp('^root.'), '')); + } + + if (field.startsWith('parent.')) { + const fieldHandle = field.replace(new RegExp('^parent.'), ''); + // Regex for fields like replicators, where the path ends with `parent_field_handle.0.field_handle`. + let regex = new RegExp('.[^\.]+\.[0-9]+\.[^\.]*$'); + if (this.dottedFieldPath.match(regex)) { + return data_get(this.rootValues, `${this.dottedFieldPath.replace(regex, '')}.${fieldHandle}`); + } + + // We dont’t have a regex field or similar, so the end of the field path looks like `parent_field_handle.field_handle`. + return data_get(this.rootValues, `${this.dottedFieldPath.replace(new RegExp('.[^\.]+\.[^\.]*$'), '')}.${fieldHandle}`); + } + + return data_get(this.values, field); } passesCondition(condition) { diff --git a/resources/js/components/field-conditions/ValidatorMixin.js b/resources/js/components/field-conditions/ValidatorMixin.js index e408247379..466f97973c 100644 --- a/resources/js/components/field-conditions/ValidatorMixin.js +++ b/resources/js/components/field-conditions/ValidatorMixin.js @@ -25,7 +25,7 @@ export default { } // Use validation to determine whether field should be shown. - let validator = new Validator(field, this.values, this.$store, this.storeName); + let validator = new Validator(field, this.values, this.$store, this.storeName, dottedFieldPath); let passes = validator.passesConditions(); // If the field is configured to always save, never omit value. diff --git a/resources/js/tests/FieldConditionsConverter.test.js b/resources/js/tests/FieldConditionsConverter.test.js index a2772b33fe..eba19d6545 100644 --- a/resources/js/tests/FieldConditionsConverter.test.js +++ b/resources/js/tests/FieldConditionsConverter.test.js @@ -49,6 +49,20 @@ test('it converts from blueprint format and does not apply prefix to root field expect(converted).toEqual(expected); }); +test('it converts from blueprint format and does not apply prefix to parent field conditions', () => { + let converted = FieldConditionsConverter.fromBlueprint({ + 'name': 'isnt joe', + 'parent.title': 'not empty', + }, 'nested_'); + + let expected = [ + {field: 'nested_name', operator: 'not', value: 'joe'}, + {field: 'parent.title', operator: 'not', value: 'empty'} + ]; + + expect(converted).toEqual(expected); +}); + test('it converts to blueprint format', () => { let converted = FieldConditionsConverter.toBlueprint([ {field: 'name', operator: 'isnt', value: 'joe'}, diff --git a/resources/js/tests/FieldConditionsValidator.test.js b/resources/js/tests/FieldConditionsValidator.test.js index 285b453c60..e5365af8c0 100644 --- a/resources/js/tests/FieldConditionsValidator.test.js +++ b/resources/js/tests/FieldConditionsValidator.test.js @@ -95,8 +95,8 @@ const Fields = new Vue({ } }); -let showFieldIf = function (conditions=null) { - return Fields.showField(conditions ? {'if': conditions} : {}); +let showFieldIf = function (conditions=null, dottedFieldPath=null) { + return Fields.showField(conditions ? {'if': conditions} : {}, dottedFieldPath); }; afterEach(() => { @@ -322,6 +322,8 @@ test('it can run conditions on nested data', () => { 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); + expect(showFieldIf({'parent.name': 'Han'}, 'user.address.country')).toBe(true); + expect(showFieldIf({'parent.name': 'Chewy'}, 'user.address.country')).toBe(false); }); test('it can run conditions on root store values', () => { @@ -346,13 +348,18 @@ test('it can run conditions on prefixed fields', async () => { test('it can run conditions on nested prefixed fields', async () => { Fields.setValues({ prefixed_first_name: 'Rincess', - prefixed_last_name: 'Pleia' + prefixed_last_name: 'Pleia', + prefixed_address: { + home_planet: 'Alderaan' + } }, '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); + expect(Fields.showField({if: {'parent.prefixed_last_name': 'is Pleia'}}, 'nested.prefixed_address.home_planet')).toBe(true); + expect(Fields.showField({if: {'parent.prefixed_last_name': 'is Holo'}}, 'nested.prefixed_address.home_planet')).toBe(false); }); test('it can call a custom function', () => { From ad02cbff3e392862eb729d7113c15566fdab81d2 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Fri, 16 Aug 2024 15:51:45 -0400 Subject: [PATCH 02/25] =?UTF-8?q?Port=20#9960=E2=80=99s=20`fieldPath`=20pa?= =?UTF-8?q?ram=20into=20this=20PR.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/field-conditions/Validator.js | 7 ++-- .../field-conditions/ValidatorMixin.js | 2 +- .../js/tests/FieldConditionsValidator.test.js | 32 +++++++++++++++++-- 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/resources/js/components/field-conditions/Validator.js b/resources/js/components/field-conditions/Validator.js index 92a3df7c21..8e3d8ebced 100644 --- a/resources/js/components/field-conditions/Validator.js +++ b/resources/js/components/field-conditions/Validator.js @@ -20,13 +20,13 @@ const NUMBER_SPECIFIC_COMPARISONS = [ ]; export default class { - constructor(field, values, store, storeName, dottedFieldPath = '') { + constructor(field, values, dottedFieldPath, store, storeName) { this.field = field; - this.dottedFieldPath = dottedFieldPath; this.values = values; - this.rootValues = store ? store.state.publish[storeName].values : false; + this.dottedFieldPath = dottedFieldPath; this.store = store; this.storeName = storeName; + this.rootValues = store ? store.state.publish[storeName].values : false; this.passOnAny = false; this.showOnPass = true; this.converter = new Converter; @@ -279,6 +279,7 @@ export default class { root: this.rootValues, store: this.store, storeName: this.storeName, + fieldPath: this.dottedFieldPath, }); return this.showOnPass ? passes : ! passes; diff --git a/resources/js/components/field-conditions/ValidatorMixin.js b/resources/js/components/field-conditions/ValidatorMixin.js index 466f97973c..770dd499b5 100644 --- a/resources/js/components/field-conditions/ValidatorMixin.js +++ b/resources/js/components/field-conditions/ValidatorMixin.js @@ -25,7 +25,7 @@ export default { } // Use validation to determine whether field should be shown. - let validator = new Validator(field, this.values, this.$store, this.storeName, dottedFieldPath); + let validator = new Validator(field, this.values, dottedFieldPath, this.$store, this.storeName); let passes = validator.passesConditions(); // If the field is configured to always save, never omit value. diff --git a/resources/js/tests/FieldConditionsValidator.test.js b/resources/js/tests/FieldConditionsValidator.test.js index e5365af8c0..16457d9202 100644 --- a/resources/js/tests/FieldConditionsValidator.test.js +++ b/resources/js/tests/FieldConditionsValidator.test.js @@ -1,6 +1,7 @@ import Vue from 'vue'; import Vuex from 'vuex'; import ValidatesFieldConditions from '../components/field-conditions/ValidatorMixin.js'; +import { data_get } from '../bootstrap/globals' Vue.use(Vuex); const Store = new Vuex.Store({ @@ -96,6 +97,10 @@ const Fields = new Vue({ }); let showFieldIf = function (conditions=null, dottedFieldPath=null) { + if (dottedFieldPath === null && conditions && Object.keys(conditions).length === 1) { + dottedFieldPath = Object.keys(conditions)[0].replace(new RegExp('^root.'), ''); + } + return Fields.showField(conditions ? {'if': conditions} : {}, dottedFieldPath); }; @@ -381,6 +386,27 @@ test('it can call a custom function', () => { expect(Fields.showField({unless: 'custom reallyLovesAnimals'})).toBe(true); }); +test('it can call a custom function that uses fieldPath param to evaluate nested fields', () => { + Fields.setValues({ nested: + [ + { favorite_animals: ['cats', 'dogs'] }, + { favorite_animals: ['cats', 'dogs', 'giraffes', 'lions'] } + ] + }); + + Statamic.$conditions.add('reallyLovesAnimals', function ({ target, params, store, storeName, root, fieldPath }) { + expect(target).toBe(null); + expect(params).toEqual([]); + expect(store).toBe(Store); + expect(storeName).toBe('base'); + + return data_get(root, fieldPath).length > 3; + }); + + expect(showFieldIf({'favorite_animals': 'custom reallyLovesAnimals'}, 'nested.0.favorite_animals')).toBe(false); + expect(showFieldIf({'favorite_animals': 'custom reallyLovesAnimals'}, 'nested.1.favorite_animals')).toBe(true); +}); + test('it can call a custom function using params against root values', () => { Fields.setStoreValues({ favorite_foods: ['pizza', 'lasagna', 'asparagus', 'quinoa', 'peppers'], @@ -402,12 +428,13 @@ test('it can call a custom function on a specific field', () => { favorite_animals: ['cats', 'dogs', 'rats', 'bats'], }); - Statamic.$conditions.add('lovesAnimals', function ({ target, params, store, storeName, values }) { + Statamic.$conditions.add('lovesAnimals', function ({ target, params, store, storeName, values, fieldPath }) { expect(target).toEqual(['cats', 'dogs', 'rats', 'bats']); expect(values.favorite_animals).toEqual(['cats', 'dogs', 'rats', 'bats']); expect(params).toEqual([]); expect(store).toBe(Store); expect(storeName).toBe('base'); + expect(fieldPath).toBe('favorite_animals'); return values.favorite_animals.length > 3; }); @@ -419,11 +446,12 @@ test('it can call a custom function on a specific field using params against a r favorite_animals: ['cats', 'dogs', 'rats', 'bats'], }); - Statamic.$conditions.add('lovesAnimals', function ({ target, params, store, storeName, root }) { + Statamic.$conditions.add('lovesAnimals', function ({ target, params, store, storeName, root, fieldPath }) { expect(target).toEqual(['cats', 'dogs', 'rats', 'bats']); expect(root.favorite_animals).toEqual(['cats', 'dogs', 'rats', 'bats']); expect(store).toBe(Store); expect(storeName).toBe('base'); + expect(fieldPath).toBe('favorite_animals'); return target.length > (params[0] || 3); }); From aad328985352a3571dbd4ce9abe5e9df4c58bd5c Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Fri, 16 Aug 2024 16:20:57 -0400 Subject: [PATCH 03/25] Rename `root.` & `parent.` to `$root.` & `$parent.` (but still allow `root.` for backwards compat). --- .../components/field-conditions/Validator.js | 19 ++++--- .../js/tests/FieldConditionsValidator.test.js | 57 ++++++++++++++++--- 2 files changed, 60 insertions(+), 16 deletions(-) diff --git a/resources/js/components/field-conditions/Validator.js b/resources/js/components/field-conditions/Validator.js index 8e3d8ebced..414019e36a 100644 --- a/resources/js/components/field-conditions/Validator.js +++ b/resources/js/components/field-conditions/Validator.js @@ -205,12 +205,12 @@ export default class { } getFieldValue(field) { - if (field.startsWith('root.')) { - return data_get(this.rootValues, field.replace(new RegExp('^root.'), '')); + if (field.startsWith('$root.') || field.startsWith('root.')) { + return data_get(this.rootValues, field.replace(new RegExp('^\\$?root.'), '')); } - if (field.startsWith('parent.')) { - const fieldHandle = field.replace(new RegExp('^parent.'), ''); + if (field.startsWith('$parent.')) { + const fieldHandle = field.replace(new RegExp('^\\$parent.'), ''); // Regex for fields like replicators, where the path ends with `parent_field_handle.0.field_handle`. let regex = new RegExp('.[^\.]+\.[0-9]+\.[^\.]*$'); if (this.dottedFieldPath.match(regex)) { @@ -306,8 +306,13 @@ export default class { return lhs; } - return lhs.startsWith('root.') - ? lhs.replace(/^root\./, '') - : dottedPrefix + '.' + lhs; + // TODO: Add test coverage for this! + if (lhs.startsWith('$root.') || lhs.startsWith('root.')) { + return lhs.replace(new RegExp('^\\$?root.'), ''); + } + + // TODO: Also handle `$parent` usage? + + return dottedPrefix + '.' + lhs; } } diff --git a/resources/js/tests/FieldConditionsValidator.test.js b/resources/js/tests/FieldConditionsValidator.test.js index 16457d9202..b49bd4f694 100644 --- a/resources/js/tests/FieldConditionsValidator.test.js +++ b/resources/js/tests/FieldConditionsValidator.test.js @@ -98,7 +98,7 @@ const Fields = new Vue({ let showFieldIf = function (conditions=null, dottedFieldPath=null) { if (dottedFieldPath === null && conditions && Object.keys(conditions).length === 1) { - dottedFieldPath = Object.keys(conditions)[0].replace(new RegExp('^root.'), ''); + dottedFieldPath = Object.keys(conditions)[0].replace(new RegExp('^\\$?root.'), ''); } return Fields.showField(conditions ? {'if': conditions} : {}, dottedFieldPath); @@ -325,10 +325,22 @@ test('it can run conditions on nested data', () => { 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); + expect(showFieldIf({'$parent.name': 'Han'}, 'user.address.country')).toBe(true); + expect(showFieldIf({'$parent.name': 'Chewy'}, 'user.address.country')).toBe(false); +}); + +test('it can run conditions on nested data using `root.` without `$` for backwards compatibility', () => { + Fields.setValues({ + name: 'Han', + address: { + country: 'Canada' + } + }, 'user'); + expect(showFieldIf({'root.user.address.country': 'Canada'})).toBe(true); expect(showFieldIf({'root.user.address.country': 'Australia'})).toBe(false); - expect(showFieldIf({'parent.name': 'Han'}, 'user.address.country')).toBe(true); - expect(showFieldIf({'parent.name': 'Chewy'}, 'user.address.country')).toBe(false); }); test('it can run conditions on root store values', () => { @@ -337,6 +349,14 @@ test('it can run conditions on root store values', () => { }); expect(showFieldIf({'favorite_foods': 'contains lasagna'})).toBe(false); + expect(showFieldIf({'$root.favorite_foods': 'contains lasagna'})).toBe(true); +}); + +test('it can run conditions on root store values using `root.` without `$` for backwards compatibility', () => { + Fields.setStoreValues({ + favorite_foods: ['pizza', 'lasagna', 'asparagus', 'quinoa', 'peppers'], + }); + expect(showFieldIf({'root.favorite_foods': 'contains lasagna'})).toBe(true); }); @@ -355,16 +375,16 @@ test('it can run conditions on nested prefixed fields', async () => { prefixed_first_name: 'Rincess', prefixed_last_name: 'Pleia', prefixed_address: { - home_planet: 'Alderaan' + home_planet: 'Elderaan' } }, '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); - expect(Fields.showField({if: {'parent.prefixed_last_name': 'is Pleia'}}, 'nested.prefixed_address.home_planet')).toBe(true); - expect(Fields.showField({if: {'parent.prefixed_last_name': 'is Holo'}}, 'nested.prefixed_address.home_planet')).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); + expect(Fields.showField({if: {'$parent.prefixed_last_name': 'is Pleia'}}, 'nested.prefixed_address.home_planet')).toBe(true); + expect(Fields.showField({if: {'$parent.prefixed_last_name': 'is Holo'}}, 'nested.prefixed_address.home_planet')).toBe(false); }); test('it can call a custom function', () => { @@ -386,7 +406,7 @@ test('it can call a custom function', () => { expect(Fields.showField({unless: 'custom reallyLovesAnimals'})).toBe(true); }); -test('it can call a custom function that uses fieldPath param to evaluate nested fields', () => { +test('it can call a custom function that uses `fieldPath` param to evaluate nested fields', () => { Fields.setValues({ nested: [ { favorite_animals: ['cats', 'dogs'] }, @@ -455,6 +475,25 @@ test('it can call a custom function on a specific field using params against a r return target.length > (params[0] || 3); }); + expect(showFieldIf({'$root.favorite_animals': 'custom lovesAnimals'})).toBe(true); + expect(showFieldIf({'$root.favorite_animals': 'custom lovesAnimals:2'})).toBe(true); + expect(showFieldIf({'$root.favorite_animals': 'custom lovesAnimals:7'})).toBe(false); +}); + +test('it can call a custom function on a specific field using params against a root value using `root.` backwards compatibility', () => { + Fields.setStoreValues({ + favorite_animals: ['cats', 'dogs', 'rats', 'bats'], + }); + + Statamic.$conditions.add('lovesAnimals', function ({ target, params, store, storeName, root, fieldPath }) { + expect(target).toEqual(['cats', 'dogs', 'rats', 'bats']); + expect(root.favorite_animals).toEqual(['cats', 'dogs', 'rats', 'bats']); + expect(store).toBe(Store); + expect(storeName).toBe('base'); + expect(fieldPath).toBe('favorite_animals'); + return target.length > (params[0] || 3); + }); + expect(showFieldIf({'root.favorite_animals': 'custom lovesAnimals'})).toBe(true); expect(showFieldIf({'root.favorite_animals': 'custom lovesAnimals:2'})).toBe(true); expect(showFieldIf({'root.favorite_animals': 'custom lovesAnimals:7'})).toBe(false); From c5a4d84934f749ada57e50fe4912a92fe7aa130a Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Fri, 16 Aug 2024 17:46:45 -0400 Subject: [PATCH 04/25] Update blueprint converter as well. --- .../components/field-conditions/Converter.js | 10 +++++++-- .../js/tests/FieldConditionsConverter.test.js | 22 +++++++++++++++---- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/resources/js/components/field-conditions/Converter.js b/resources/js/components/field-conditions/Converter.js index cecebd8898..18d36c89c1 100644 --- a/resources/js/components/field-conditions/Converter.js +++ b/resources/js/components/field-conditions/Converter.js @@ -32,11 +32,17 @@ export default class { } getScopedFieldHandle(field, prefix) { - if (field.startsWith('root.') || field.startsWith('parent.') || ! prefix) { + if (field.startsWith('$root.') || field.startsWith('root.')) { return field; } - return prefix + field; + if (field.startsWith('$parent.')) { + return field; + } + + return prefix + ? prefix + field + : field; } getOperatorFromRhs(condition) { diff --git a/resources/js/tests/FieldConditionsConverter.test.js b/resources/js/tests/FieldConditionsConverter.test.js index eba19d6545..e64e1e2233 100644 --- a/resources/js/tests/FieldConditionsConverter.test.js +++ b/resources/js/tests/FieldConditionsConverter.test.js @@ -35,7 +35,21 @@ test('it converts from blueprint format and applies prefixes', () => { expect(converted).toEqual(expected); }); -test('it converts from blueprint format and does not apply prefix to root field conditions', () => { +test('it converts from blueprint format and does not apply prefix to `$root.` field conditions', () => { + let converted = FieldConditionsConverter.fromBlueprint({ + 'name': 'isnt joe', + '$root.title': 'not empty', + }, 'nested_'); + + let expected = [ + {field: 'nested_name', operator: 'not', value: 'joe'}, + {field: '$root.title', operator: 'not', value: 'empty'} + ]; + + expect(converted).toEqual(expected); +}); + +test('it converts from blueprint format and does not apply prefix to `root.` field conditions without `$` for backwards compatibility', () => { let converted = FieldConditionsConverter.fromBlueprint({ 'name': 'isnt joe', 'root.title': 'not empty', @@ -49,15 +63,15 @@ test('it converts from blueprint format and does not apply prefix to root field expect(converted).toEqual(expected); }); -test('it converts from blueprint format and does not apply prefix to parent field conditions', () => { +test('it converts from blueprint format and does not apply prefix to `$parent.` field conditions', () => { let converted = FieldConditionsConverter.fromBlueprint({ 'name': 'isnt joe', - 'parent.title': 'not empty', + '$parent.title': 'not empty', }, 'nested_'); let expected = [ {field: 'nested_name', operator: 'not', value: 'joe'}, - {field: 'parent.title', operator: 'not', value: 'empty'} + {field: '$parent.title', operator: 'not', value: 'empty'} ]; expect(converted).toEqual(expected); From 800e6d5e01f49b01f13bcd9a69ba9d5ba5466f1b Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Fri, 16 Aug 2024 18:01:53 -0400 Subject: [PATCH 05/25] Add test coverage and fix revealer issue. --- .../components/field-conditions/Validator.js | 9 +-- .../js/tests/FieldConditionsValidator.test.js | 70 +++++++++++++++++++ 2 files changed, 73 insertions(+), 6 deletions(-) diff --git a/resources/js/components/field-conditions/Validator.js b/resources/js/components/field-conditions/Validator.js index 414019e36a..08650279c0 100644 --- a/resources/js/components/field-conditions/Validator.js +++ b/resources/js/components/field-conditions/Validator.js @@ -302,17 +302,14 @@ export default class { } relativeLhsToAbsoluteFieldPath(lhs, dottedPrefix) { - if (! dottedPrefix) { - return lhs; - } - - // TODO: Add test coverage for this! if (lhs.startsWith('$root.') || lhs.startsWith('root.')) { return lhs.replace(new RegExp('^\\$?root.'), ''); } // TODO: Also handle `$parent` usage? - return dottedPrefix + '.' + lhs; + return dottedPrefix + ? dottedPrefix + '.' + lhs + : lhs; } } diff --git a/resources/js/tests/FieldConditionsValidator.test.js b/resources/js/tests/FieldConditionsValidator.test.js index b49bd4f694..08e94dd3f0 100644 --- a/resources/js/tests/FieldConditionsValidator.test.js +++ b/resources/js/tests/FieldConditionsValidator.test.js @@ -683,6 +683,42 @@ test('it tells omitter not omit revealer-hidden fields', async () => { expect(Store.state.publish.base.hiddenFields['venue'].omitValue).toBe(false); }); +test('it tells omitter not omit revealer-hidden fields using `$root.` in condition', async () => { + Fields.setValues({ + show_more_info: false, + event_venue: false, + }); + + await Fields.setHiddenFieldsState([ + {handle: 'show_more_info', type: 'revealer'}, + {handle: 'venue', if: {'$root.show_more_info': true}}, + ]); + + console.log(Store.state.publish.base.hiddenFields); + 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 revealer-hidden fields using `root.` without `$` for backwards compatibility', async () => { + Fields.setValues({ + show_more_info: false, + event_venue: false, + }); + + await Fields.setHiddenFieldsState([ + {handle: 'show_more_info', type: 'revealer'}, + {handle: 'venue', if: {'root.show_more_info': true}}, + ]); + + console.log(Store.state.publish.base.hiddenFields); + 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, @@ -700,6 +736,40 @@ test('it tells omitter not omit nested revealer-hidden fields', async () => { expect(Store.state.publish.base.hiddenFields['nested.venue'].omitValue).toBe(false); }); +test('it tells omitter not omit nested revealer-hidden fields using `$root.` in condition', async () => { + Fields.setValues({ + show_more_info: false, + event_venue: false, + }, 'nested'); + + await Fields.setHiddenFieldsState([ + {handle: 'show_more_info', type: 'revealer'}, + {handle: 'venue', if: {'$root.nested.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 nested revealer-hidden fields using `root.` in condition without `$` for backwards compatibility', async () => { + Fields.setValues({ + show_more_info: false, + event_venue: false, + }, 'nested'); + + await Fields.setHiddenFieldsState([ + {handle: 'show_more_info', type: 'revealer'}, + {handle: 'venue', if: {'root.nested.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, From b40e21cb7fd92ed65842b8798d95f533e3b7c53d Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Fri, 16 Aug 2024 18:34:41 -0400 Subject: [PATCH 06/25] =?UTF-8?q?Technically=20these=20should=20match,=20t?= =?UTF-8?q?hough=20it=20doesn=E2=80=99t=20matter=20for=20tests.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../js/tests/FieldConditionsValidator.test.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/resources/js/tests/FieldConditionsValidator.test.js b/resources/js/tests/FieldConditionsValidator.test.js index 08e94dd3f0..c50d9edd89 100644 --- a/resources/js/tests/FieldConditionsValidator.test.js +++ b/resources/js/tests/FieldConditionsValidator.test.js @@ -618,7 +618,7 @@ test('it tells omitter to omit hidden fields by default', async () => { test('it tells omitter to omit nested hidden fields by default', async () => { Fields.setValues({ is_online_event: false, - event_venue: false, + venue: false, }, 'nested'); await Fields.setHiddenFieldsState([ @@ -669,7 +669,7 @@ test('it tells omitter to omit nested revealer fields', async () => { test('it tells omitter not omit revealer-hidden fields', async () => { Fields.setValues({ show_more_info: false, - event_venue: false, + venue: false, }); await Fields.setHiddenFieldsState([ @@ -686,7 +686,7 @@ test('it tells omitter not omit revealer-hidden fields', async () => { test('it tells omitter not omit revealer-hidden fields using `$root.` in condition', async () => { Fields.setValues({ show_more_info: false, - event_venue: false, + venue: false, }); await Fields.setHiddenFieldsState([ @@ -704,7 +704,7 @@ test('it tells omitter not omit revealer-hidden fields using `$root.` in conditi test('it tells omitter not omit revealer-hidden fields using `root.` without `$` for backwards compatibility', async () => { Fields.setValues({ show_more_info: false, - event_venue: false, + venue: false, }); await Fields.setHiddenFieldsState([ @@ -722,7 +722,7 @@ test('it tells omitter not omit revealer-hidden fields using `root.` without `$` test('it tells omitter not omit nested revealer-hidden fields', async () => { Fields.setValues({ show_more_info: false, - event_venue: false, + venue: false, }, 'nested'); await Fields.setHiddenFieldsState([ @@ -739,7 +739,7 @@ test('it tells omitter not omit nested revealer-hidden fields', async () => { test('it tells omitter not omit nested revealer-hidden fields using `$root.` in condition', async () => { Fields.setValues({ show_more_info: false, - event_venue: false, + venue: false, }, 'nested'); await Fields.setHiddenFieldsState([ @@ -756,7 +756,7 @@ test('it tells omitter not omit nested revealer-hidden fields using `$root.` in test('it tells omitter not omit nested revealer-hidden fields using `root.` in condition without `$` for backwards compatibility', async () => { Fields.setValues({ show_more_info: false, - event_venue: false, + venue: false, }, 'nested'); await Fields.setHiddenFieldsState([ From 211840d49f26530c14c7b90c9aa9be097cdf9d16 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Fri, 16 Aug 2024 18:37:38 -0400 Subject: [PATCH 07/25] Escape dot. --- resources/js/components/field-conditions/Validator.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/js/components/field-conditions/Validator.js b/resources/js/components/field-conditions/Validator.js index 08650279c0..4cbd57562e 100644 --- a/resources/js/components/field-conditions/Validator.js +++ b/resources/js/components/field-conditions/Validator.js @@ -206,7 +206,7 @@ export default class { getFieldValue(field) { if (field.startsWith('$root.') || field.startsWith('root.')) { - return data_get(this.rootValues, field.replace(new RegExp('^\\$?root.'), '')); + return data_get(this.rootValues, field.replace(new RegExp('^\\$?root\\.'), '')); } if (field.startsWith('$parent.')) { @@ -303,7 +303,7 @@ export default class { relativeLhsToAbsoluteFieldPath(lhs, dottedPrefix) { if (lhs.startsWith('$root.') || lhs.startsWith('root.')) { - return lhs.replace(new RegExp('^\\$?root.'), ''); + return lhs.replace(new RegExp('^\\$?root\\.'), ''); } // TODO: Also handle `$parent` usage? From 5d633ce58b607a2e593e89797cbb19790f691a80 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Fri, 16 Aug 2024 18:43:53 -0400 Subject: [PATCH 08/25] Add failing test for handing of `$parent.` in revealer condition logic. --- .../js/tests/FieldConditionsValidator.test.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/resources/js/tests/FieldConditionsValidator.test.js b/resources/js/tests/FieldConditionsValidator.test.js index c50d9edd89..4da6d4c037 100644 --- a/resources/js/tests/FieldConditionsValidator.test.js +++ b/resources/js/tests/FieldConditionsValidator.test.js @@ -770,6 +770,25 @@ test('it tells omitter not omit nested revealer-hidden fields using `root.` in c expect(Store.state.publish.base.hiddenFields['nested.venue'].omitValue).toBe(false); }); +test('it tells omitter not omit nested revealer-hidden fields using `$parent.` in condition', async () => { + Fields.setValues({ + show_more_info: false, + nested: { + venue: false, + }, + }); + + await Fields.setHiddenFieldsState([ + {handle: 'show_more_info', type: 'revealer'}, + {handle: 'nested.venue', if: {'$parent.show_more_info': true}}, + ]); + + expect(Store.state.publish.base.hiddenFields['show_more_info'].hidden).toBe(false); + expect(Store.state.publish.base.hiddenFields['nested.venue'].hidden).toBe(true); + expect(Store.state.publish.base.hiddenFields['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, From c9f59d75dc1257a7b473e99e0ddf06b2d952104e Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Wed, 21 Aug 2024 15:28:00 -0400 Subject: [PATCH 09/25] Kick test. --- resources/js/tests/FieldConditionsValidator.test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/js/tests/FieldConditionsValidator.test.js b/resources/js/tests/FieldConditionsValidator.test.js index 4da6d4c037..41a15bdb01 100644 --- a/resources/js/tests/FieldConditionsValidator.test.js +++ b/resources/js/tests/FieldConditionsValidator.test.js @@ -770,6 +770,7 @@ test('it tells omitter not omit nested revealer-hidden fields using `root.` in c expect(Store.state.publish.base.hiddenFields['nested.venue'].omitValue).toBe(false); }); +// Kick test. test('it tells omitter not omit nested revealer-hidden fields using `$parent.` in condition', async () => { Fields.setValues({ show_more_info: false, From b4e8de683835c9a23241c60dc0201f619a6243d3 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Tue, 24 Sep 2024 10:23:23 -0400 Subject: [PATCH 10/25] Unkick test. --- resources/js/tests/FieldConditionsValidator.test.js | 1 - 1 file changed, 1 deletion(-) diff --git a/resources/js/tests/FieldConditionsValidator.test.js b/resources/js/tests/FieldConditionsValidator.test.js index 41a15bdb01..4da6d4c037 100644 --- a/resources/js/tests/FieldConditionsValidator.test.js +++ b/resources/js/tests/FieldConditionsValidator.test.js @@ -770,7 +770,6 @@ test('it tells omitter not omit nested revealer-hidden fields using `root.` in c expect(Store.state.publish.base.hiddenFields['nested.venue'].omitValue).toBe(false); }); -// Kick test. test('it tells omitter not omit nested revealer-hidden fields using `$parent.` in condition', async () => { Fields.setValues({ show_more_info: false, From 3f5a65afaccbfcd135c8b8749c05512c0f63fe09 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Thu, 26 Sep 2024 10:34:25 -0400 Subject: [PATCH 11/25] Add failing test for `$parent` at multiple levels in values payload. --- .../js/tests/FieldConditionsValidator.test.js | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/resources/js/tests/FieldConditionsValidator.test.js b/resources/js/tests/FieldConditionsValidator.test.js index 4da6d4c037..2fb8e79f27 100644 --- a/resources/js/tests/FieldConditionsValidator.test.js +++ b/resources/js/tests/FieldConditionsValidator.test.js @@ -331,6 +331,35 @@ test('it can run conditions on nested data', () => { expect(showFieldIf({'$parent.name': 'Chewy'}, 'user.address.country')).toBe(false); }); +test('it can run conditions on nested array data using parent', () => { + Fields.setValues({ + name: 'Han', + grid: [ + { text: 'Foo' }, + { text: 'Bar' }, + ], + nested: { + name: 'Chewy', + replicator: [ + { text: 'Foo' }, + { text: 'Bar' }, + ], + }, + }); + + // Test parent works to get to top level, if parent level is indeed top level + expect(showFieldIf({'$parent.name': 'Han'}, 'grid.0.text')).toBe(true); + expect(showFieldIf({'$parent.name': 'Chewy'}, 'grid.0.text')).toBe(false); + expect(showFieldIf({'$parent.name': 'Han'}, 'grid.1.text')).toBe(true); + expect(showFieldIf({'$parent.name': 'Chewy'}, 'grid.1.text')).toBe(false); + + // Test parent works in nested situation, when it should not go to top level + expect(showFieldIf({'$parent.name': 'Han'}, 'nested.replicator.0.text')).toBe(false); + expect(showFieldIf({'$parent.name': 'Chewy'}, 'nested.replicator.0.text')).toBe(true); + expect(showFieldIf({'$parent.name': 'Han'}, 'nested.replicator.1.text')).toBe(false); + expect(showFieldIf({'$parent.name': 'Chewy'}, 'nested.replicator.1.text')).toBe(true); +}); + test('it can run conditions on nested data using `root.` without `$` for backwards compatibility', () => { Fields.setValues({ name: 'Han', From d234df233e1563c78aeec6fe482bf244b0b1c740 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Thu, 26 Sep 2024 10:37:29 -0400 Subject: [PATCH 12/25] Extract and fix `$parent` resolving logic to pass test. --- .../components/field-conditions/Validator.js | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/resources/js/components/field-conditions/Validator.js b/resources/js/components/field-conditions/Validator.js index 4cbd57562e..3ea1c65b87 100644 --- a/resources/js/components/field-conditions/Validator.js +++ b/resources/js/components/field-conditions/Validator.js @@ -210,15 +210,7 @@ export default class { } if (field.startsWith('$parent.')) { - const fieldHandle = field.replace(new RegExp('^\\$parent.'), ''); - // Regex for fields like replicators, where the path ends with `parent_field_handle.0.field_handle`. - let regex = new RegExp('.[^\.]+\.[0-9]+\.[^\.]*$'); - if (this.dottedFieldPath.match(regex)) { - return data_get(this.rootValues, `${this.dottedFieldPath.replace(regex, '')}.${fieldHandle}`); - } - - // We dont’t have a regex field or similar, so the end of the field path looks like `parent_field_handle.field_handle`. - return data_get(this.rootValues, `${this.dottedFieldPath.replace(new RegExp('.[^\.]+\.[^\.]*$'), '')}.${fieldHandle}`); + return data_get(this.values, this.resolveParentInFieldPath(field)); } return data_get(this.values, field); @@ -312,4 +304,24 @@ export default class { ? dottedPrefix + '.' + lhs : lhs; } + + getParentFieldPath() { + const regex = new RegExp('(.*?[^\\.]+)(\\.[0-9]+)*\\.[^\\.]*$'); + + let parent = this.dottedFieldPath.replace(regex, '$1'); + + return parent.includes('.') + ? parent.replace(regex, '$1') + : ''; + } + + resolveParentInFieldPath(fieldPath) { + const fieldHandle = fieldPath.replace(new RegExp('^\\$parent.'), ''); + + const parentPath = this.getParentFieldPath(); + + return parentPath + ? `${parentPath}.${fieldHandle}` + : fieldHandle; + } } From d6e906864fc64c7e0e1cc12bef528fe98a9c7421 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Thu, 26 Sep 2024 10:42:37 -0400 Subject: [PATCH 13/25] These get treated as regex by my test runner, just word it better. --- resources/js/tests/FieldConditionsValidator.test.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/resources/js/tests/FieldConditionsValidator.test.js b/resources/js/tests/FieldConditionsValidator.test.js index 2fb8e79f27..9451ca27b8 100644 --- a/resources/js/tests/FieldConditionsValidator.test.js +++ b/resources/js/tests/FieldConditionsValidator.test.js @@ -331,7 +331,7 @@ test('it can run conditions on nested data', () => { expect(showFieldIf({'$parent.name': 'Chewy'}, 'user.address.country')).toBe(false); }); -test('it can run conditions on nested array data using parent', () => { +test('it can run conditions on nested array data using parent syntax', () => { Fields.setValues({ name: 'Han', grid: [ @@ -712,7 +712,7 @@ test('it tells omitter not omit revealer-hidden fields', async () => { expect(Store.state.publish.base.hiddenFields['venue'].omitValue).toBe(false); }); -test('it tells omitter not omit revealer-hidden fields using `$root.` in condition', async () => { +test('it tells omitter not omit revealer-hidden fields using root syntax in condition', async () => { Fields.setValues({ show_more_info: false, venue: false, @@ -730,7 +730,7 @@ test('it tells omitter not omit revealer-hidden fields using `$root.` in conditi expect(Store.state.publish.base.hiddenFields['venue'].omitValue).toBe(false); }); -test('it tells omitter not omit revealer-hidden fields using `root.` without `$` for backwards compatibility', async () => { +test('it tells omitter not omit revealer-hidden fields using legacy root syntax for backwards compatibility', async () => { Fields.setValues({ show_more_info: false, venue: false, @@ -765,7 +765,7 @@ test('it tells omitter not omit nested revealer-hidden fields', async () => { expect(Store.state.publish.base.hiddenFields['nested.venue'].omitValue).toBe(false); }); -test('it tells omitter not omit nested revealer-hidden fields using `$root.` in condition', async () => { +test('it tells omitter not omit nested revealer-hidden fields using root syntax in condition', async () => { Fields.setValues({ show_more_info: false, venue: false, @@ -782,7 +782,7 @@ test('it tells omitter not omit nested revealer-hidden fields using `$root.` in expect(Store.state.publish.base.hiddenFields['nested.venue'].omitValue).toBe(false); }); -test('it tells omitter not omit nested revealer-hidden fields using `root.` in condition without `$` for backwards compatibility', async () => { +test('it tells omitter not omit nested revealer-hidden fields using legacy root syntax for backwards compatibility', async () => { Fields.setValues({ show_more_info: false, venue: false, @@ -799,7 +799,7 @@ test('it tells omitter not omit nested revealer-hidden fields using `root.` in c expect(Store.state.publish.base.hiddenFields['nested.venue'].omitValue).toBe(false); }); -test('it tells omitter not omit nested revealer-hidden fields using `$parent.` in condition', async () => { +test('it tells omitter not omit nested revealer-hidden fields using wat in condition', async () => { Fields.setValues({ show_more_info: false, nested: { From b4a53156c72585e9acd089e74e2b9f614dad29dd Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Thu, 26 Sep 2024 10:47:30 -0400 Subject: [PATCH 14/25] Actually pass failing tests. --- resources/js/components/field-conditions/Validator.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/js/components/field-conditions/Validator.js b/resources/js/components/field-conditions/Validator.js index 3ea1c65b87..2474859274 100644 --- a/resources/js/components/field-conditions/Validator.js +++ b/resources/js/components/field-conditions/Validator.js @@ -210,7 +210,7 @@ export default class { } if (field.startsWith('$parent.')) { - return data_get(this.values, this.resolveParentInFieldPath(field)); + return data_get(this.rootValues, this.resolveParentInFieldPath(field)); } return data_get(this.values, field); From 7195c9d243265bda047a2584b5b58698debc3ee6 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Thu, 26 Sep 2024 11:02:02 -0400 Subject: [PATCH 15/25] Revealer tests wip. --- .../js/tests/FieldConditionsValidator.test.js | 42 +++++++++++-------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/resources/js/tests/FieldConditionsValidator.test.js b/resources/js/tests/FieldConditionsValidator.test.js index 9451ca27b8..912c5f3dce 100644 --- a/resources/js/tests/FieldConditionsValidator.test.js +++ b/resources/js/tests/FieldConditionsValidator.test.js @@ -799,24 +799,30 @@ test('it tells omitter not omit nested revealer-hidden fields using legacy root expect(Store.state.publish.base.hiddenFields['nested.venue'].omitValue).toBe(false); }); -test('it tells omitter not omit nested revealer-hidden fields using wat in condition', async () => { - Fields.setValues({ - show_more_info: false, - nested: { - venue: false, - }, - }); - - await Fields.setHiddenFieldsState([ - {handle: 'show_more_info', type: 'revealer'}, - {handle: 'nested.venue', if: {'$parent.show_more_info': true}}, - ]); - - expect(Store.state.publish.base.hiddenFields['show_more_info'].hidden).toBe(false); - expect(Store.state.publish.base.hiddenFields['nested.venue'].hidden).toBe(true); - expect(Store.state.publish.base.hiddenFields['show_more_info'].omitValue).toBe(true); - expect(Store.state.publish.base.hiddenFields['nested.venue'].omitValue).toBe(false); -}); +// TODO +// test('it tells omitter not omit nested revealer-hidden fields using parent syntax in condition', async () => { +// Fields.setValues({ +// foo: { +// show_more_info: false, +// nested: { +// venue: false, +// }, +// } +// }); + +// await Fields.setHiddenFieldsState([ +// {handle: 'foo.show_more_info', type: 'revealer'}, +// {handle: 'foo.nested.venue', if: {'$parent.show_more_info': true}}, +// ]); + +// console.log('HIDDDEN...'); +// console.log(Store.state.publish.base.hiddenFields); + +// expect(Store.state.publish.base.hiddenFields['foo.show_more_info'].hidden).toBe(false); +// expect(Store.state.publish.base.hiddenFields['foo.nested.venue'].hidden).toBe(true); +// expect(Store.state.publish.base.hiddenFields['foo.show_more_info'].omitValue).toBe(true); +// expect(Store.state.publish.base.hiddenFields['foo.nested.venue'].omitValue).toBe(false); +// }); test('it tells omitter not omit prefixed revealer-hidden fields', async () => { Fields.setValues({ From 92d1b5a9203e3f6c60bbb8a485f89825c6b5a052 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Thu, 26 Sep 2024 19:18:59 -0400 Subject: [PATCH 16/25] Flesh out `$parent` handling and test coverage (WIP). --- .../components/field-conditions/Validator.js | 19 ++++--- .../js/tests/FieldConditionsValidator.test.js | 57 ++++++++++++++----- 2 files changed, 54 insertions(+), 22 deletions(-) diff --git a/resources/js/components/field-conditions/Validator.js b/resources/js/components/field-conditions/Validator.js index 2474859274..c4fe33b827 100644 --- a/resources/js/components/field-conditions/Validator.js +++ b/resources/js/components/field-conditions/Validator.js @@ -305,23 +305,26 @@ export default class { : lhs; } - getParentFieldPath() { + getParentFieldPath(dottedFieldPath) { const regex = new RegExp('(.*?[^\\.]+)(\\.[0-9]+)*\\.[^\\.]*$'); - let parent = this.dottedFieldPath.replace(regex, '$1'); + let parent = dottedFieldPath.replace(regex, '$1'); return parent.includes('.') - ? parent.replace(regex, '$1') + ? parent.replace(regex, '$1$2') : ''; } - resolveParentInFieldPath(fieldPath) { - const fieldHandle = fieldPath.replace(new RegExp('^\\$parent.'), ''); + removeParentKeyword(dottedFieldPath) { + return dottedFieldPath.replace(new RegExp('^\\$parent.'), ''); + } - const parentPath = this.getParentFieldPath(); + resolveParentInFieldPath(dottedFieldPath) { + let parentPath = this.getParentFieldPath(this.dottedFieldPath); + let fieldPath = this.removeParentKeyword(dottedFieldPath); return parentPath - ? `${parentPath}.${fieldHandle}` - : fieldHandle; + ? `${parentPath}.${fieldPath}` + : fieldPath; } } diff --git a/resources/js/tests/FieldConditionsValidator.test.js b/resources/js/tests/FieldConditionsValidator.test.js index 912c5f3dce..9db090f870 100644 --- a/resources/js/tests/FieldConditionsValidator.test.js +++ b/resources/js/tests/FieldConditionsValidator.test.js @@ -331,33 +331,62 @@ test('it can run conditions on nested data', () => { expect(showFieldIf({'$parent.name': 'Chewy'}, 'user.address.country')).toBe(false); }); -test('it can run conditions on nested array data using parent syntax', () => { +test('it can run conditions on parent data using parent syntax', () => { Fields.setValues({ name: 'Han', - grid: [ + replicator: [ { text: 'Foo' }, { text: 'Bar' }, ], - nested: { + group: { name: 'Chewy', + text: 'Foo', replicator: [ { text: 'Foo' }, { text: 'Bar' }, + { + name: 'Luke', + replicator: [ + { text: 'Foo' }, + ], + group: { + name: 'Yoda', + replicator: [ + { text: 'Foo' }, + ], + }, + }, ], }, }); - // Test parent works to get to top level, if parent level is indeed top level - expect(showFieldIf({'$parent.name': 'Han'}, 'grid.0.text')).toBe(true); - expect(showFieldIf({'$parent.name': 'Chewy'}, 'grid.0.text')).toBe(false); - expect(showFieldIf({'$parent.name': 'Han'}, 'grid.1.text')).toBe(true); - expect(showFieldIf({'$parent.name': 'Chewy'}, 'grid.1.text')).toBe(false); - - // Test parent works in nested situation, when it should not go to top level - expect(showFieldIf({'$parent.name': 'Han'}, 'nested.replicator.0.text')).toBe(false); - expect(showFieldIf({'$parent.name': 'Chewy'}, 'nested.replicator.0.text')).toBe(true); - expect(showFieldIf({'$parent.name': 'Han'}, 'nested.replicator.1.text')).toBe(false); - expect(showFieldIf({'$parent.name': 'Chewy'}, 'nested.replicator.1.text')).toBe(true); + // Test parent works from replicator to top level + expect(showFieldIf({'$parent.name': 'Han'}, 'replicator.0.text')).toBe(true); + expect(showFieldIf({'$parent.name': 'Chewy'}, 'replicator.0.text')).toBe(false); + expect(showFieldIf({'$parent.name': 'Han'}, 'replicator.1.text')).toBe(true); + expect(showFieldIf({'$parent.name': 'Chewy'}, 'replicator.1.text')).toBe(false); + + // Test parent works from nested field group to top level + expect(showFieldIf({'$parent.name': 'Han'}, 'group.text')).toBe(true); + expect(showFieldIf({'$parent.name': 'Chewy'}, 'group.text')).toBe(false); + + // Test parent works in deeply nested situations through multiple replicators and field groups + expect(showFieldIf({'$parent.name': 'Han'}, 'group.replicator.0.text')).toBe(false); + expect(showFieldIf({'$parent.name': 'Chewy'}, 'group.replicator.0.text')).toBe(true); + expect(showFieldIf({'$parent.name': 'Han'}, 'group.replicator.1.text')).toBe(false); + expect(showFieldIf({'$parent.name': 'Chewy'}, 'group.replicator.1.text')).toBe(true); + expect(showFieldIf({'$parent.name': 'Luke'}, 'group.replicator.2.replicator.0.text')).toBe(true); + expect(showFieldIf({'$parent.name': 'Leia'}, 'group.replicator.2.replicator.0.text')).toBe(false); + expect(showFieldIf({'$parent.name': 'Yoda'}, 'group.replicator.2.group.replicator.0.text')).toBe(true); + expect(showFieldIf({'$parent.name': 'Leia'}, 'group.replicator.2.group.replicator.0.text')).toBe(false); + + // Test parent can be chained to check upwards through multiple levels of multiple replicators and field groups + // expect(showFieldIf({'$parent.$parent.name': 'Luke'}, 'group.replicator.2.group.replicator.0.text')).toBe(true); + // expect(showFieldIf({'$parent.$parent.name': 'Leia'}, 'group.replicator.2.group.replicator.0.text')).toBe(false); + // expect(showFieldIf({'$parent.$parent.$parent.name': 'Chewy'}, 'group.replicator.2.group.replicator.0.text')).toBe(true); + // expect(showFieldIf({'$parent.$parent.$parent.name': 'Leia'}, 'group.replicator.2.group.replicator.0.text')).toBe(false); + // expect(showFieldIf({'$parent.$parent.$parent.$parent.name': 'Han'}, 'group.replicator.2.group.replicator.0.text')).toBe(true); + // expect(showFieldIf({'$parent.$parent.$parent.$parent.name': 'Leia'}, 'group.replicator.2.group.replicator.0.text')).toBe(false); }); test('it can run conditions on nested data using `root.` without `$` for backwards compatibility', () => { From 4b82755ff3266b0a8b2cc32c6b77b018297d5f92 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Fri, 27 Sep 2024 14:41:47 -0400 Subject: [PATCH 17/25] Avoid regex symbols in test names for test runners. --- resources/js/tests/FieldConditionsConverter.test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/js/tests/FieldConditionsConverter.test.js b/resources/js/tests/FieldConditionsConverter.test.js index e64e1e2233..064772cd8b 100644 --- a/resources/js/tests/FieldConditionsConverter.test.js +++ b/resources/js/tests/FieldConditionsConverter.test.js @@ -35,7 +35,7 @@ test('it converts from blueprint format and applies prefixes', () => { expect(converted).toEqual(expected); }); -test('it converts from blueprint format and does not apply prefix to `$root.` field conditions', () => { +test('it converts from blueprint format and does not apply prefix to field conditions with root syntax', () => { let converted = FieldConditionsConverter.fromBlueprint({ 'name': 'isnt joe', '$root.title': 'not empty', @@ -49,7 +49,7 @@ test('it converts from blueprint format and does not apply prefix to `$root.` fi expect(converted).toEqual(expected); }); -test('it converts from blueprint format and does not apply prefix to `root.` field conditions without `$` for backwards compatibility', () => { +test('it converts from blueprint format and does not apply prefix to field conditions with legacy root syntax for backwards compatibility', () => { let converted = FieldConditionsConverter.fromBlueprint({ 'name': 'isnt joe', 'root.title': 'not empty', @@ -63,7 +63,7 @@ test('it converts from blueprint format and does not apply prefix to `root.` fie expect(converted).toEqual(expected); }); -test('it converts from blueprint format and does not apply prefix to `$parent.` field conditions', () => { +test('it converts from blueprint format and does not apply prefix to field conditions with parent syntax', () => { let converted = FieldConditionsConverter.fromBlueprint({ 'name': 'isnt joe', '$parent.title': 'not empty', From d5527b0c9f7f6f48b0c3bd0538d2069ebb27cc6a Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Fri, 27 Sep 2024 16:23:57 -0400 Subject: [PATCH 18/25] Okay, rip out all this in favour of `$parent` to `$root` resolving idea. --- .../components/field-conditions/Validator.js | 30 +------------------ 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/resources/js/components/field-conditions/Validator.js b/resources/js/components/field-conditions/Validator.js index c4fe33b827..4c16655e50 100644 --- a/resources/js/components/field-conditions/Validator.js +++ b/resources/js/components/field-conditions/Validator.js @@ -1,4 +1,5 @@ import Converter from './Converter.js'; +import ParentResolver from './ParentResolver.js'; import { KEYS } from './Constants.js'; import { data_get } from '../../bootstrap/globals.js' import isString from 'underscore/modules/isString.js' @@ -209,10 +210,6 @@ export default class { return data_get(this.rootValues, field.replace(new RegExp('^\\$?root\\.'), '')); } - if (field.startsWith('$parent.')) { - return data_get(this.rootValues, this.resolveParentInFieldPath(field)); - } - return data_get(this.values, field); } @@ -298,33 +295,8 @@ export default class { return lhs.replace(new RegExp('^\\$?root\\.'), ''); } - // TODO: Also handle `$parent` usage? - return dottedPrefix ? dottedPrefix + '.' + lhs : lhs; } - - getParentFieldPath(dottedFieldPath) { - const regex = new RegExp('(.*?[^\\.]+)(\\.[0-9]+)*\\.[^\\.]*$'); - - let parent = dottedFieldPath.replace(regex, '$1'); - - return parent.includes('.') - ? parent.replace(regex, '$1$2') - : ''; - } - - removeParentKeyword(dottedFieldPath) { - return dottedFieldPath.replace(new RegExp('^\\$parent.'), ''); - } - - resolveParentInFieldPath(dottedFieldPath) { - let parentPath = this.getParentFieldPath(this.dottedFieldPath); - let fieldPath = this.removeParentKeyword(dottedFieldPath); - - return parentPath - ? `${parentPath}.${fieldPath}` - : fieldPath; - } } From bd8256391ed67b77e12c7b4f5bc566d594807a1a Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Fri, 27 Sep 2024 16:24:25 -0400 Subject: [PATCH 19/25] Spec it all out with unit tests. --- .../FieldConditionsParentResolver.test.js | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 resources/js/tests/FieldConditionsParentResolver.test.js diff --git a/resources/js/tests/FieldConditionsParentResolver.test.js b/resources/js/tests/FieldConditionsParentResolver.test.js new file mode 100644 index 0000000000..b6cb7e7aee --- /dev/null +++ b/resources/js/tests/FieldConditionsParentResolver.test.js @@ -0,0 +1,68 @@ +import ParentResolver from '../components/field-conditions/ParentResolver.js'; + +let resolve = function (currentFieldPath, pathWithParent) { + return new ParentResolver(currentFieldPath).resolve(pathWithParent); +} + +test('it resolves from group to top level', () => { + expect(resolve('group.field', '$parent.name')).toEqual('$root.name'); +}); + +test('it resolves from set/row to top level', () => { + expect(resolve('replicator.0.field', '$parent.name')).toEqual('$root.name'); + expect(resolve('grid.0.field', '$parent.name')).toEqual('$root.name'); + expect(resolve('bard.0.field', '$parent.name')).toEqual('$root.name'); +}); + +test('it resolves from nested group to parent group', () => { + expect(resolve('group.nested_group.field', '$parent.name')).toEqual('$root.group.name'); +}); + +test('it resolves from nested set to parent set', () => { + expect(resolve('replicator.2.nested_replicator.1.field', '$parent.name')).toEqual('$root.replicator.2.name'); + expect(resolve('grid.2.nested_grid.1.field', '$parent.name')).toEqual('$root.grid.2.name'); + expect(resolve('bard.2.nested_bard.1.field', '$parent.name')).toEqual('$root.bard.2.name'); +}); + +test('it resolves from nested group to parent set', () => { + expect(resolve('replicator.1.group.field', '$parent.name')).toEqual('$root.replicator.1.name'); + expect(resolve('grid.1.group.field', '$parent.name')).toEqual('$root.grid.1.name'); + expect(resolve('bard.1.group.field', '$parent.name')).toEqual('$root.bard.1.name'); +}); + +test('it resolves from nested set to parent group', () => { + expect(resolve('group.replicator.1.field', '$parent.name')).toEqual('$root.group.name'); + expect(resolve('group.grid.1.field', '$parent.name')).toEqual('$root.group.name'); + expect(resolve('group.bard.1.field', '$parent.name')).toEqual('$root.group.name'); +}); + +test('it resolves from deeply nested groups all the way up to top level', () => { + let fromField = 'group.nested_group.deeper_group.deeeeeeeper_group.field'; + + expect(resolve(fromField, '$parent.name')).toEqual('$root.group.nested_group.deeper_group.name'); + expect(resolve(fromField, '$parent.$parent.name')).toEqual('$root.group.nested_group.name'); + expect(resolve(fromField, '$parent.$parent.$parent.name')).toEqual('$root.group.name'); + expect(resolve(fromField, '$parent.$parent.$parent.$parent.name')).toEqual('$root.name'); +}); + +test('it resolves from deeply nested sets all the way up to top level', () => { + let fromField = 'replicator.1.bard.4.grid.0.replicator.6.field'; + + expect(resolve(fromField, '$parent.name')).toEqual('$root.replicator.1.bard.4.grid.0.name'); + expect(resolve(fromField, '$parent.$parent.name')).toEqual('$root.replicator.1.bard.4.name'); + expect(resolve(fromField, '$parent.$parent.$parent.name')).toEqual('$root.replicator.1.name'); + expect(resolve(fromField, '$parent.$parent.$parent.$parent.name')).toEqual('$root.name'); +}); + +test('it resolves from deeply nested mix of everything all the way up to top level', () => { + let fromField = 'group.replicator.1.group.bard.4.grid.0.group.group.replicator.6.field'; + + expect(resolve(fromField, '$parent.name')).toEqual('$root.group.replicator.1.group.bard.4.grid.0.group.group.name'); + expect(resolve(fromField, '$parent.$parent.name')).toEqual('$root.group.replicator.1.group.bard.4.grid.0.group.name'); + expect(resolve(fromField, '$parent.$parent.$parent.name')).toEqual('$root.group.replicator.1.group.bard.4.grid.0.name'); + expect(resolve(fromField, '$parent.$parent.$parent.$parent.name')).toEqual('$root.group.replicator.1.group.bard.4.name'); + expect(resolve(fromField, '$parent.$parent.$parent.$parent.$parent.name')).toEqual('$root.group.replicator.1.group.name'); + expect(resolve(fromField, '$parent.$parent.$parent.$parent.$parent.$parent.name')).toEqual('$root.group.replicator.1.name'); + expect(resolve(fromField, '$parent.$parent.$parent.$parent.$parent.$parent.$parent.name')).toEqual('$root.group.name'); + expect(resolve(fromField, '$parent.$parent.$parent.$parent.$parent.$parent.$parent.$parent.name')).toEqual('$root.name'); +}); From 690ffa5aaec5b8f8c6e95ae297ff14d3330553f7 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Fri, 27 Sep 2024 16:24:36 -0400 Subject: [PATCH 20/25] Make it all work for real. --- .../field-conditions/ParentResolver.js | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 resources/js/components/field-conditions/ParentResolver.js diff --git a/resources/js/components/field-conditions/ParentResolver.js b/resources/js/components/field-conditions/ParentResolver.js new file mode 100644 index 0000000000..32710101fa --- /dev/null +++ b/resources/js/components/field-conditions/ParentResolver.js @@ -0,0 +1,42 @@ +export default class { + constructor(currentFieldPath) { + this.currentFieldPath = currentFieldPath; + } + + resolve(pathWithParent) { + let parentPath = this.getParentFieldPath(this.currentFieldPath, true); + let fieldPath = this.removeOneParentKeyword(pathWithParent); + + while (fieldPath.startsWith('$parent.')) { + parentPath = this.getParentFieldPath(parentPath); + fieldPath = this.removeOneParentKeyword(fieldPath); + } + + let resolved = parentPath + ? `${parentPath}.${fieldPath}` + : fieldPath; + + return `$root.${resolved}`; + } + + getParentFieldPath(dottedFieldPath, removeCurrentField) { + const regex = new RegExp('(.*?[^\\.]+)(\\.[0-9]+)*\\.[^\\.]*$'); + + if (removeCurrentField || this.isAtSetLevel(dottedFieldPath)) { + dottedFieldPath = dottedFieldPath.replace(regex, '$1'); + } + + return dottedFieldPath.includes('.') + ? dottedFieldPath.replace(regex, '$1$2') + : ''; + } + + isAtSetLevel(dottedFieldPath) { + return dottedFieldPath.match(new RegExp('(\\.[0-9]+)$')); + } + + removeOneParentKeyword(dottedFieldPath) { + return dottedFieldPath.replace(new RegExp('^\\$parent.'), ''); + } + +} From 0e4f6f2b2996b35adbbd59e49a7d46c65c6fe5ec Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Fri, 27 Sep 2024 16:25:27 -0400 Subject: [PATCH 21/25] Resolve `$parent` to absolute `$root` field path (since that already all works). --- resources/js/components/field-conditions/Validator.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/resources/js/components/field-conditions/Validator.js b/resources/js/components/field-conditions/Validator.js index 4c16655e50..d56d25a19f 100644 --- a/resources/js/components/field-conditions/Validator.js +++ b/resources/js/components/field-conditions/Validator.js @@ -206,6 +206,10 @@ export default class { } getFieldValue(field) { + if (field.startsWith('$parent.')) { + field = new ParentResolver(this.dottedFieldPath).resolve(field); + } + if (field.startsWith('$root.') || field.startsWith('root.')) { return data_get(this.rootValues, field.replace(new RegExp('^\\$?root\\.'), '')); } From c21f55a1913033d4bcae63f9ae838575abde7561 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Fri, 27 Sep 2024 16:25:52 -0400 Subject: [PATCH 22/25] =?UTF-8?q?Now=20this=20all=20just=20works!=20?= =?UTF-8?q?=F0=9F=8E=89=F0=9F=8E=89=F0=9F=8E=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- resources/js/tests/FieldConditionsValidator.test.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/resources/js/tests/FieldConditionsValidator.test.js b/resources/js/tests/FieldConditionsValidator.test.js index 9db090f870..08ff689798 100644 --- a/resources/js/tests/FieldConditionsValidator.test.js +++ b/resources/js/tests/FieldConditionsValidator.test.js @@ -381,12 +381,12 @@ test('it can run conditions on parent data using parent syntax', () => { expect(showFieldIf({'$parent.name': 'Leia'}, 'group.replicator.2.group.replicator.0.text')).toBe(false); // Test parent can be chained to check upwards through multiple levels of multiple replicators and field groups - // expect(showFieldIf({'$parent.$parent.name': 'Luke'}, 'group.replicator.2.group.replicator.0.text')).toBe(true); - // expect(showFieldIf({'$parent.$parent.name': 'Leia'}, 'group.replicator.2.group.replicator.0.text')).toBe(false); - // expect(showFieldIf({'$parent.$parent.$parent.name': 'Chewy'}, 'group.replicator.2.group.replicator.0.text')).toBe(true); - // expect(showFieldIf({'$parent.$parent.$parent.name': 'Leia'}, 'group.replicator.2.group.replicator.0.text')).toBe(false); - // expect(showFieldIf({'$parent.$parent.$parent.$parent.name': 'Han'}, 'group.replicator.2.group.replicator.0.text')).toBe(true); - // expect(showFieldIf({'$parent.$parent.$parent.$parent.name': 'Leia'}, 'group.replicator.2.group.replicator.0.text')).toBe(false); + expect(showFieldIf({'$parent.$parent.name': 'Luke'}, 'group.replicator.2.group.replicator.0.text')).toBe(true); + expect(showFieldIf({'$parent.$parent.name': 'Leia'}, 'group.replicator.2.group.replicator.0.text')).toBe(false); + expect(showFieldIf({'$parent.$parent.$parent.name': 'Chewy'}, 'group.replicator.2.group.replicator.0.text')).toBe(true); + expect(showFieldIf({'$parent.$parent.$parent.name': 'Leia'}, 'group.replicator.2.group.replicator.0.text')).toBe(false); + expect(showFieldIf({'$parent.$parent.$parent.$parent.name': 'Han'}, 'group.replicator.2.group.replicator.0.text')).toBe(true); + expect(showFieldIf({'$parent.$parent.$parent.$parent.name': 'Leia'}, 'group.replicator.2.group.replicator.0.text')).toBe(false); }); test('it can run conditions on nested data using `root.` without `$` for backwards compatibility', () => { From 5ea49a5c47a4d02631d38b012357ac0f9a81df4e Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Fri, 27 Sep 2024 17:15:17 -0400 Subject: [PATCH 23/25] Flesh this out a little more to cover douple replicator with multiple chained `$parent`s. --- resources/js/tests/FieldConditionsValidator.test.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/resources/js/tests/FieldConditionsValidator.test.js b/resources/js/tests/FieldConditionsValidator.test.js index 08ff689798..e25850a2ce 100644 --- a/resources/js/tests/FieldConditionsValidator.test.js +++ b/resources/js/tests/FieldConditionsValidator.test.js @@ -379,6 +379,8 @@ test('it can run conditions on parent data using parent syntax', () => { expect(showFieldIf({'$parent.name': 'Leia'}, 'group.replicator.2.replicator.0.text')).toBe(false); expect(showFieldIf({'$parent.name': 'Yoda'}, 'group.replicator.2.group.replicator.0.text')).toBe(true); expect(showFieldIf({'$parent.name': 'Leia'}, 'group.replicator.2.group.replicator.0.text')).toBe(false); + expect(showFieldIf({'$parent.name': 'Luke'}, 'group.replicator.2.replicator.0.text')).toBe(true); + expect(showFieldIf({'$parent.name': 'Leia'}, 'group.replicator.2.replicator.0.text')).toBe(false); // Test parent can be chained to check upwards through multiple levels of multiple replicators and field groups expect(showFieldIf({'$parent.$parent.name': 'Luke'}, 'group.replicator.2.group.replicator.0.text')).toBe(true); @@ -387,6 +389,8 @@ test('it can run conditions on parent data using parent syntax', () => { expect(showFieldIf({'$parent.$parent.$parent.name': 'Leia'}, 'group.replicator.2.group.replicator.0.text')).toBe(false); expect(showFieldIf({'$parent.$parent.$parent.$parent.name': 'Han'}, 'group.replicator.2.group.replicator.0.text')).toBe(true); expect(showFieldIf({'$parent.$parent.$parent.$parent.name': 'Leia'}, 'group.replicator.2.group.replicator.0.text')).toBe(false); + expect(showFieldIf({'$parent.$parent.name': 'Chewy'}, 'group.replicator.2.replicator.0.text')).toBe(true); + expect(showFieldIf({'$parent.$parent.name': 'Leia'}, 'group.replicator.2.replicator.0.text')).toBe(false); }); test('it can run conditions on nested data using `root.` without `$` for backwards compatibility', () => { From d3cc919a875abc200e4e1c8815592301556f8201 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Fri, 27 Sep 2024 19:08:01 -0400 Subject: [PATCH 24/25] Failing test to show data loss because of improper `revealer` hidden field handling. --- .../js/tests/FieldConditionsValidator.test.js | 96 ++++++++++++++----- 1 file changed, 72 insertions(+), 24 deletions(-) diff --git a/resources/js/tests/FieldConditionsValidator.test.js b/resources/js/tests/FieldConditionsValidator.test.js index e25850a2ce..08a1bc260a 100644 --- a/resources/js/tests/FieldConditionsValidator.test.js +++ b/resources/js/tests/FieldConditionsValidator.test.js @@ -832,30 +832,78 @@ test('it tells omitter not omit nested revealer-hidden fields using legacy root expect(Store.state.publish.base.hiddenFields['nested.venue'].omitValue).toBe(false); }); -// TODO -// test('it tells omitter not omit nested revealer-hidden fields using parent syntax in condition', async () => { -// Fields.setValues({ -// foo: { -// show_more_info: false, -// nested: { -// venue: false, -// }, -// } -// }); - -// await Fields.setHiddenFieldsState([ -// {handle: 'foo.show_more_info', type: 'revealer'}, -// {handle: 'foo.nested.venue', if: {'$parent.show_more_info': true}}, -// ]); - -// console.log('HIDDDEN...'); -// console.log(Store.state.publish.base.hiddenFields); - -// expect(Store.state.publish.base.hiddenFields['foo.show_more_info'].hidden).toBe(false); -// expect(Store.state.publish.base.hiddenFields['foo.nested.venue'].hidden).toBe(true); -// expect(Store.state.publish.base.hiddenFields['foo.show_more_info'].omitValue).toBe(true); -// expect(Store.state.publish.base.hiddenFields['foo.nested.venue'].omitValue).toBe(false); -// }); +test('it tells omitter not omit nested revealer-hidden fields using parent syntax in condition', async () => { + Fields.setValues({ + top_level_show_more_info: false, + replicator: [ + { text: 'Foo' }, + { text: 'Bar' }, + ], + group: { + show_more_info: false, + replicator: [ + { text: 'Foo' }, + { text: 'Bar' }, + { + show_more_info: false, + replicator: [ + { text: 'Foo' }, + ], + group: { + show_more_info: false, + replicator: [ + { text: 'Foo' }, + ], + }, + }, + ], + }, + }); + + // Track revealer toggles + await Fields.setHiddenFieldsState([ + {handle: 'top_level_show_more_info', type: 'revealer'}, + {handle: 'group.show_more_info', type: 'revealer'}, + {handle: 'group.replicator.2.show_more_info', type: 'revealer'}, + {handle: 'group.replicator.2.group.show_more_info', type: 'revealer'}, + ]); + + // Set revealer hidden fields using `$parent` syntax + await Fields.setHiddenFieldsState([ + {handle: 'replicator.1.text', if: {'$parent.top_level_show_more_info': true}}, + {handle: 'group.replicator.1.text', if: {'$parent.show_more_info': true}}, + ]); + + // Set revealer hidden fields using chained `$parent` syntax + await Fields.setHiddenFieldsState([ + {handle: 'group.replicator.2.replicator.0.text', if: {'$parent.$parent.$parent.top_level_show_more_info': true}}, + {handle: 'group.replicator.2.group.replicator.0.text', if: {'$parent.$parent.$parent.$parent.top_level_show_more_info': true}}, + ]); + + // Ensure revealer toggles should definitely hidden and omited from submitted payload + expect(Store.state.publish.base.hiddenFields['top_level_show_more_info'].hidden).toBe(false); + expect(Store.state.publish.base.hiddenFields['top_level_show_more_info'].omitValue).toBe(true); + expect(Store.state.publish.base.hiddenFields['group.show_more_info'].hidden).toBe(false); + expect(Store.state.publish.base.hiddenFields['group.show_more_info'].omitValue).toBe(true); + expect(Store.state.publish.base.hiddenFields['group.replicator.2.show_more_info'].hidden).toBe(false); + expect(Store.state.publish.base.hiddenFields['group.replicator.2.show_more_info'].omitValue).toBe(true); + expect(Store.state.publish.base.hiddenFields['group.replicator.2.group.show_more_info'].hidden).toBe(false); + expect(Store.state.publish.base.hiddenFields['group.replicator.2.group.show_more_info'].omitValue).toBe(true); + + // Ensure revealer hidden fields should be hiddden, but not omitted + expect(Store.state.publish.base.hiddenFields['replicator.1.text'].hidden).toBe(true); + expect(Store.state.publish.base.hiddenFields['replicator.1.text'].omitValue).toBe(false); + expect(Store.state.publish.base.hiddenFields['group.replicator.1.text'].hidden).toBe(true); + expect(Store.state.publish.base.hiddenFields['group.replicator.1.text'].omitValue).toBe(false); + expect(Store.state.publish.base.hiddenFields['group.replicator.2.replicator.0.text'].hidden).toBe(true); + expect(Store.state.publish.base.hiddenFields['group.replicator.2.replicator.0.text'].omitValue).toBe(false); + expect(Store.state.publish.base.hiddenFields['group.replicator.2.group.replicator.0.text'].hidden).toBe(true); + expect(Store.state.publish.base.hiddenFields['group.replicator.2.group.replicator.0.text'].omitValue).toBe(false); + + // Just a few extra assertions to ensure only sets with revealer conditions should be affected + expect('replicator.0.text' in Store.state.publish.base.hiddenFields).toBe(false); + expect('group.replicator.0.text' in Store.state.publish.base.hiddenFields).toBe(false); +}); test('it tells omitter not omit prefixed revealer-hidden fields', async () => { Fields.setValues({ From df8bf21236cba759b243454e3174ec3972325e49 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Fri, 27 Sep 2024 19:14:02 -0400 Subject: [PATCH 25/25] =?UTF-8?q?Prevent=20data=20loss=20with=20`$parent`?= =?UTF-8?q?=20syntax=20on=20revealers=20&=20pass=20failing=20test=20?= =?UTF-8?q?=F0=9F=8E=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- resources/js/components/field-conditions/Validator.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/resources/js/components/field-conditions/Validator.js b/resources/js/components/field-conditions/Validator.js index d56d25a19f..42b2a5b877 100644 --- a/resources/js/components/field-conditions/Validator.js +++ b/resources/js/components/field-conditions/Validator.js @@ -295,6 +295,10 @@ export default class { } relativeLhsToAbsoluteFieldPath(lhs, dottedPrefix) { + if (lhs.startsWith('$parent.')) { + lhs = new ParentResolver(this.dottedFieldPath).resolve(lhs); + } + if (lhs.startsWith('$root.') || lhs.startsWith('root.')) { return lhs.replace(new RegExp('^\\$?root\\.'), ''); }