Skip to content

Custom field conditions with a target field are inverted twice under unless and hide_when #15532

Closed

Description

@joshdaugherty

Bug description

A custom condition that names a target field is inverted twice under unless, unless_any, hide_when and hide_when_any, so the inversions cancel and the field is shown exactly when the blueprint says to hide it (and hidden when it says to show it). There is no error.

showOnPass is false for those four keys, and resources/js/components/field-conditions/Validator.js applies it in two places on the same evaluation. On 6.x (0a58bd5ff), passesCustomCondition() inverts the custom function's result:

passesCustomCondition(condition) {
    // ...
    let passes = customFunction({ /* ... */ });

    return this.showOnPass ? passes : !passes;
}

and passesConditions() inverts again whatever the all/any pass returned:

let passes = this.passOnAny ? this.passesAnyConditions(conditions) : this.passesAllConditions(conditions);

return this.showOnPass ? passes : !passes;

A targeted custom condition (unless: { some_field: 'custom myCondition' }) goes through passesAllConditions() or passesAnyConditions() → passesCondition() → passesCustomCondition(), so it is inverted once inside and once outside.

The bare-string form (unless: 'custom myCondition') is already correct and must stay so. passesConditions() returns it straight from passesCustomCondition() without reaching the outer inversion, so for that path the inner inversion is the only one:

} else if (this.isCustomConditionWithoutTarget(conditions)) {
    return this.passesCustomCondition(this.prepareCondition(conditions));
}

So simply deleting the inner inversion would break the bare-string form. Standard operators (is, contains, ...) are unaffected, because passesCondition() returns the raw comparison and only passesConditions() inverts it.

The same code is in the latest release, v6.34.1 (lines 55-61 and 295 of the same file).

How to reproduce

  1. Register a custom condition that always passes:

    Statamic.$conditions.add('alwaysTrue', () => true);
  2. Add two fields to a blueprint: a text field referral_source, and a field with a targeted custom condition under unless:

    -
      handle: notes
      field:
        type: textarea
        unless:
          referral_source: 'custom alwaysTrue'
  3. The validator reports notes as visible. It should be hidden: the condition passes, and unless means hide when it passes.

For contrast, unless: 'custom alwaysTrue' (no target field) hides notes as expected, and if: { referral_source: 'custom alwaysTrue' } shows it as expected.

As a test in resources/js/tests/FieldConditionsValidator.test.js, beside "it can call a custom function on a specific field":

Statamic.$conditions.add('lovesAnimals', ({ target }) => target.length > 3);
setValues({ favorite_animals: ['cats', 'dogs', 'rats', 'bats'] });

expect(Fields.showField({ unless: { favorite_animals: 'custom lovesAnimals' } })).toBe(false);

On unchanged 6.x this returns true, and hide_when, unless_any and hide_when_any do the same. With a custom condition that fails, unless returns false where it should return true.

Environment

Environment
Laravel Version: 13.24.0
PHP Version: 8.4.23
Composer Version: 2.10.2
Environment: local
Debug Mode: ENABLED
Maintenance Mode: OFF
Timezone: UTC
Locale: en

Cache
Config: NOT CACHED
Events: NOT CACHED
Routes: NOT CACHED
Views: NOT CACHED

Drivers
Broadcasting: log
Cache: file
Database: sqlite
Logs: stack / single
Mail: smtp
Queue: sync
Session: file

Storage
public/storage: NOT LINKED

Statamic
Addons: 9
License Key: Not set
Sites: 255 (names redacted)
Stache Watcher: Enabled (auto)
Static Caching: Disabled
Version: 6.26.0 PRO

Statamic Addons
el-schneider/statamic-admin-bar: 0.3.2
jacksleight/statamic-bard-mutator: 3.0.5
jacksleight/statamic-bard-texstyle: 4.1.1
mitydigital/sitemapamic: 3.5.1
statamic/eloquent-driver: 5.10.0
statamic/ssg: 4.1.0
stillat/relationships: 2.4.0
studio1902/statamic-peak-tools: 9.2.0
uams-web/wordpress-importer: 0.19.0

Statamic Eloquent Driver
Addon Settings: file
Asset Containers: file
Assets: eloquent
Blueprints: file
Collection Trees: file
Collections: file
Entries: file
Fieldsets: file
Form Submissions: file
Forms: file
Global Sets: file
Global Variables: file
Navigation Trees: file
Navigations: file
Revisions: file
Sites: file
Taxonomies: file
Terms: file
Tokens: file

Installation

Other (please explain)

Reproduced in the statamic/cms repository's own test suite rather than in a site.

Additional details

A fix that makes passesConditions() the only place the inversion happens: passesCustomCondition() returns the custom function's result as-is, and the bare-string branch applies the inversion itself.

} else if (this.isCustomConditionWithoutTarget(conditions)) {
    let passes = this.passesCustomCondition(this.prepareCondition(conditions));

    return this.showOnPass ? passes : !passes;
}

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions