diff --git a/package.json b/package.json index 4db1e66ab6..32863fb915 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "svgo": "svgo -f ./resources/svg/ -r", "test": "vitest run", "test-watch": "npm run test -- --watch --notify", - "frontend-dev": "vite -c vite-frontend.config.js", + "frontend-dev": "vite build -c vite-frontend.config.js --watch", "frontend-build": "vite build -c vite-frontend.config.js", "prettier:format": "prettier --write \"**/*.{js,ts,jsx,tsx,cjs,cts,mjs,mts,vue,blade.php,antlers.html,css}\"", "prettier:check": "prettier --check \"**/*.{js,ts,jsx,tsx,cjs,cts,mjs,mts,vue,blade.php,antlers.html,css}\"", diff --git a/resources/js/components/field-conditions/Validator.js b/resources/js/components/field-conditions/Validator.js index 9c59455e1f..93480e0a44 100644 --- a/resources/js/components/field-conditions/Validator.js +++ b/resources/js/components/field-conditions/Validator.js @@ -15,10 +15,10 @@ const isEmpty = (value) => { const isString = (str) => str != null && typeof str.valueOf() === 'string'; export default class { - constructor(field, values, dottedFieldPath, store) { + constructor(field, values, currentFieldPath, store) { this.field = field; this.values = values; - this.dottedFieldPath = dottedFieldPath; + this.currentFieldPath = currentFieldPath; this.store = store; this.rootValues = store ? store.values : false; this.passOnAny = false; @@ -26,6 +26,20 @@ export default class { this.converter = new Converter(); } + usingRootValues() { + if (!this.currentFieldPath) { + throw new Error('[currentFieldPath] constructor param required for `usingRootValues()`'); + } + + this.rootValues = this.values; + + if (this.currentFieldPath.includes('.')) { + return this.scopeValuesToParent(); + } + + return this; + } + passesConditions(specificConditions) { let conditions = specificConditions || this.getConditions(); @@ -185,7 +199,7 @@ export default class { getFieldValue(field) { if (field.startsWith('$parent.')) { - field = new ParentResolver(this.dottedFieldPath).resolve(field); + field = new ParentResolver(this.currentFieldPath).resolve(field); } if (field.startsWith('$root.') || field.startsWith('root.')) { @@ -249,7 +263,7 @@ export default class { values: this.values, root: this.rootValues, store: this.store, - fieldPath: this.dottedFieldPath, + fieldPath: this.currentFieldPath, }); return this.showOnPass ? passes : !passes; @@ -273,7 +287,7 @@ export default class { relativeLhsToAbsoluteFieldPath(lhs, dottedPrefix) { if (lhs.startsWith('$parent.')) { - lhs = new ParentResolver(this.dottedFieldPath).resolve(lhs); + lhs = new ParentResolver(this.currentFieldPath).resolve(lhs); } if (lhs.startsWith('$root.') || lhs.startsWith('root.')) { @@ -282,4 +296,12 @@ export default class { return dottedPrefix ? dottedPrefix + '.' + lhs : lhs; } + + scopeValuesToParent() { + let scope = this.currentFieldPath.replace(new RegExp('\.[^\.]+$'), ''); + + this.values = data_get(this.rootValues, scope); + + return this; + } } diff --git a/resources/js/frontend/components/FieldConditions.js b/resources/js/frontend/components/FieldConditions.js index f592215062..328c1ea445 100644 --- a/resources/js/frontend/components/FieldConditions.js +++ b/resources/js/frontend/components/FieldConditions.js @@ -1,7 +1,7 @@ import Validator from '../../components/field-conditions/Validator.js'; export default class { - showField(field, data) { - return new Validator(field, data).passesConditions(); + showField(conditions, data, currentFieldPath) { + return new Validator(conditions, data, currentFieldPath).usingRootValues().passesConditions(); } } diff --git a/resources/js/tests/Frontend/FieldConditionsTest.test.js b/resources/js/tests/Frontend/FieldConditionsTest.test.js index 8030086a33..52f8c11e5a 100644 --- a/resources/js/tests/Frontend/FieldConditionsTest.test.js +++ b/resources/js/tests/Frontend/FieldConditionsTest.test.js @@ -2,28 +2,67 @@ import { test, expect } from 'vitest'; import '../../frontend/helpers.js'; let formData = { + target: 'test', first_name: 'Bilbo', last_name: 'Baggins', hobby: '', bio: null, + group_field: { + dwelling: 'Bag End', + village: 'Hobbiton', + nested_group_field: { + birthday: 'Sept 22', + age: '111', + }, + }, +}; + +let showField = function (conditions, dottedFieldPath = null) { + return Statamic.$conditions.showField(conditions, formData, dottedFieldPath || 'target'); }; test('it shows field by default', () => { - expect(Statamic.$conditions.showField([], formData)).toBe(true); - expect(Statamic.$conditions.showField({}, formData)).toBe(true); + expect(showField([])).toBe(true); + expect(showField({})).toBe(true); }); test('it can show field based on empty checks', () => { - expect(Statamic.$conditions.showField({ if: { hobby: 'empty' } }, formData)).toBe(true); - expect(Statamic.$conditions.showField({ if: { bio: 'empty' } }, formData)).toBe(true); - expect(Statamic.$conditions.showField({ if: { first_name: 'empty' } }, formData)).toBe(false); - expect(Statamic.$conditions.showField({ if: { first_name: 'not empty' } }, formData)).toBe(true); + expect(showField({ if: { hobby: 'empty' } })).toBe(true); + expect(showField({ if: { bio: 'empty' } })).toBe(true); + expect(showField({ if: { first_name: 'empty' } })).toBe(false); + expect(showField({ if: { first_name: 'not empty' } })).toBe(true); }); test('it can show field if multiple conditions are met', () => { - expect(Statamic.$conditions.showField({ if: { first_name: 'Bilbo', last_name: 'Baggins' } }, formData)).toBe(true); - expect(Statamic.$conditions.showField({ if: { first_name: 'Frodo', last_name: 'Baggins' } }, formData)).toBe(false); - expect(Statamic.$conditions.showField({ if_any: { first_name: 'Frodo', last_name: 'Baggins' } }, formData)).toBe( + expect(showField({ if: { first_name: 'Bilbo', last_name: 'Baggins' } })).toBe(true); + expect(showField({ if: { first_name: 'Frodo', last_name: 'Baggins' } })).toBe(false); + expect(showField({ if_any: { first_name: 'Frodo', last_name: 'Baggins' } })).toBe(true); +}); + +test('it can show field nested in group based on sibling field value', () => { + expect(showField({ if: { village: 'Hobbiton' } }, 'group_field.dwelling')).toBe(true); + expect(showField({ if: { village: 'Mordor' } }, 'group_field.dwelling')).toBe(false); +}); + +test('it can show deeply field nested in group based on sibling field value', () => { + expect(showField({ if: { age: 111 } }, 'group_field.nested_group_field.birthday')).toBe(true); + expect(showField({ if: { age: 112 } }, 'group_field.nested_group_field.birthday')).toBe(false); +}); + +test('it can show deeply field nested in group based on parent field value', () => { + expect(showField({ if: { '$parent.village': 'Hobbiton' } }, 'group_field.nested_group_field.birthday')).toBe(true); + expect(showField({ if: { '$parent.village': 'Mordor' } }, 'group_field.nested_group_field.birthday')).toBe(false); + expect( + showField({ if: { '$parent.$parent.first_name': 'not empty' } }, 'group_field.nested_group_field.birthday'), + ).toBe(true); + expect(showField({ if: { '$parent.$parent.hobby': 'not empty' } }, 'group_field.nested_group_field.birthday')).toBe( + false, + ); +}); + +test('it can show deeply field nested in group based on root field value', () => { + expect(showField({ if: { '$root.first_name': 'not empty' } }, 'group_field.nested_group_field.birthday')).toBe( true, ); + expect(showField({ if: { '$root.hobby': 'not empty' } }, 'group_field.nested_group_field.birthday')).toBe(false); }); diff --git a/resources/views/extend/forms/fields/assets.antlers.html b/resources/views/extend/forms/fields/assets.antlers.html index 3b5e1d1ea5..4e88a6b117 100644 --- a/resources/views/extend/forms/fields/assets.antlers.html +++ b/resources/views/extend/forms/fields/assets.antlers.html @@ -1,7 +1,7 @@ + {{ foreach:options as="option|label" }}