From 02fe3c6a03158012887efccd88e81e5c005217d0 Mon Sep 17 00:00:00 2001 From: Daniel Weaver Date: Fri, 1 May 2026 18:06:18 -0400 Subject: [PATCH] Initial support for multiple conditions with the same target field --- .../components/field-conditions/Condition.vue | 7 ++--- .../components/field-conditions/Converter.js | 17 +++++++++-- .../js/tests/FieldConditionsConverter.test.js | 30 +++++++++++++++++++ .../js/tests/FieldConditionsValidator.test.js | 15 ++++++++++ 4 files changed, 62 insertions(+), 7 deletions(-) diff --git a/resources/js/components/field-conditions/Condition.vue b/resources/js/components/field-conditions/Condition.vue index 2b8590dd0ab..39330491fc0 100644 --- a/resources/js/components/field-conditions/Condition.vue +++ b/resources/js/components/field-conditions/Condition.vue @@ -130,15 +130,12 @@ export default { }, fieldOptions() { - const conditions = this.conditions.map((condition) => condition.field); - return this.suggestableFields .filter((field) => { return !( field.handle === this.config.handle || // Exclude the field you're adding a condition to. - this.condition.field === field.handle || // Exclude the field being used in the current condition. - conditions.includes(field.handle) - ); // Exclude fields already used in other conditions. + this.condition.field === field.handle // Exclude the field being used in the current condition. + ); }) .map((field) => { let display = field.config.display; diff --git a/resources/js/components/field-conditions/Converter.js b/resources/js/components/field-conditions/Converter.js index 78e35c711ed..55ebd022f29 100644 --- a/resources/js/components/field-conditions/Converter.js +++ b/resources/js/components/field-conditions/Converter.js @@ -2,19 +2,32 @@ import { OPERATORS, ALIASES } from './Constants.js'; export default class { fromBlueprint(conditions, prefix = null) { - return Object.entries(conditions).map(([field, condition]) => this.splitRhs(field, condition, prefix)); + return Object.entries(conditions).flatMap(([field, condition]) => + this.wrap(condition).map((condition) => this.splitRhs(field, condition, prefix)), + ); } toBlueprint(conditions) { let converted = {}; conditions.forEach((condition) => { - converted[condition.field] = this.combineRhs(condition); + const field = condition.field; + const value = this.combineRhs(condition); + + if (field in converted) { + converted[field] = this.wrap(converted[field]).concat(value); + } else { + converted[field] = value; + } }); return converted; } + wrap(value) { + return Array.isArray(value) ? value : [value]; + } + splitRhs(field, condition, prefix = null) { return { field: this.getScopedFieldHandle(field, prefix), diff --git a/resources/js/tests/FieldConditionsConverter.test.js b/resources/js/tests/FieldConditionsConverter.test.js index 8b68a76f191..8ca6b80c020 100644 --- a/resources/js/tests/FieldConditionsConverter.test.js +++ b/resources/js/tests/FieldConditionsConverter.test.js @@ -38,6 +38,21 @@ test('it converts from blueprint format and applies prefixes', () => { expect(converted).toEqual(expected); }); +test('it converts from blueprint format when a field has multiple conditions', () => { + let converted = FieldConditionsConverter.fromBlueprint({ + status: ['published', 'archived'], + audience: 'members', + }); + + let expected = [ + { field: 'status', operator: 'equals', value: 'published' }, + { field: 'status', operator: 'equals', value: 'archived' }, + { field: 'audience', operator: 'equals', value: 'members' }, + ]; + + expect(converted).toEqual(expected); +}); + test('it converts from blueprint format and does not apply prefix to field conditions with root syntax', () => { let converted = FieldConditionsConverter.fromBlueprint( { @@ -103,6 +118,21 @@ test('it converts to blueprint format', () => { expect(converted).toEqual(expected); }); +test('it converts to blueprint format when a field has multiple conditions', () => { + let converted = FieldConditionsConverter.toBlueprint([ + { field: 'status', operator: 'is', value: 'published' }, + { field: 'status', operator: 'is', value: 'archived' }, + { field: 'audience', operator: 'is', value: 'members' }, + ]); + + let expected = { + status: ['is published', 'is archived'], + audience: 'is members', + }; + + expect(converted).toEqual(expected); +}); + test('it converts and trims properly with empty operators', () => { let converted = FieldConditionsConverter.toBlueprint([ { field: 'name', operator: '', value: 'joe' }, diff --git a/resources/js/tests/FieldConditionsValidator.test.js b/resources/js/tests/FieldConditionsValidator.test.js index d3db54895a0..f364367e266 100644 --- a/resources/js/tests/FieldConditionsValidator.test.js +++ b/resources/js/tests/FieldConditionsValidator.test.js @@ -262,6 +262,21 @@ test('it only shows when multiple conditions are met', () => { expect(showFieldIf({ first_name: 'is San', last_name: 'is Holo', age: '> 40' })).toBe(false); }); +test('it supports multiple conditions targeting the same field', () => { + setValues({ + status: 'published', + audience: 'members', + age: 22, + }); + + expect(Fields.showField({ if_any: { status: ['is archived', 'is published'], audience: 'is guests' } })).toBe(true); + expect(Fields.showField({ if_any: { status: ['is archived', 'is draft'], audience: 'is guests' } })).toBe(false); + expect(Fields.showField({ if: { age: ['> 18', '< 65'], audience: 'is members' } })).toBe(true); + expect(Fields.showField({ if: { age: ['> 18', '< 21'], audience: 'is members' } })).toBe(false); + expect(Fields.showField({ unless_any: { status: ['is archived', 'is draft'], audience: 'is guests' } })).toBe(true); + expect(Fields.showField({ hide_when_any: { status: ['is archived', 'is published'], audience: 'is guests' } })).toBe(false); +}); + test('it shows or hides with parent key variants', () => { setValues({ first_name: 'Rincess',