Skip to content

feat: cross-record (relation-path) visibility conditions [3.x]#161

Merged
ManukMinasyan merged 16 commits into
3.xfrom
feat/cross-record-visibility-conditions
Jun 9, 2026
Merged

feat: cross-record (relation-path) visibility conditions [3.x]#161
ManukMinasyan merged 16 commits into
3.xfrom
feat/cross-record-visibility-conditions

Conversation

@ManukMinasyan

Copy link
Copy Markdown
Collaborator

Summary

Extends the package so a custom-field/section visibility condition can reference a related record via a one/two-hop relation path (e.g. household.projects), not just a field on the same record.

Driving use case (ClickUp 868jtwph3, customer: Los Angeles): show an external-ID Section on Household Member only when the member's Household is associated with specific Projects — i.e. HouseholdMember → Household → Projects.

What's new

  • New condition source ConditionSource::RelationAttribute — the dotted relation path is stored in the existing field_code (no schema/migration change).
  • New operators IS_IN / IS_NOT_IN, set-intersection semantics (string-normalized), exposed only for the relation source.
  • RelationConditionResolver — walks a ≤2-hop dotted path on a record to the set of terminal related keys; reflects the terminal related model (no DB query) for the value picker.
  • Server-side evaluation (VisibilityData::evaluateCondition) — relation conditions resolve against the record's relations and never emit client JS (FrontendVisibilityService bails to null); form fields/sections fall back to a server-side visible() closure. Create form (no record) → fail-open.
  • Management UI (VisibilityComponent) — relation source option, path picker (from config allowlist), IS_IN/IS_NOT_IN operator menu, and a related-record multi-select value picker (with edit-time hydration).
  • Opt-in scoping (RelationConditionConfig, config('custom-fields.visibility')) — per-entity allowlist of relation paths; restrict_to_configured (default false) governs the own-column model-attribute source so existing flag behavior is unchanged.

Backward compatibility

  • No public interface/contract signature changed (the ?Model $record param is on concrete classes only, optional).
  • Inert by default: shipped config does not enable MODEL_ATTRIBUTE_CONDITIONS; restrict_to_configured defaults to legacy behavior.
  • Same-record custom-field conditions keep their exact reactive client-JS path.
  • Ships as a minor (intended v3.2.0).

Test plan

  • vendor/bin/pest --parallel → 685 passed, 0 failed (2633 assertions)
  • vendor/bin/phpstan analyse → no errors (254 files)
  • vendor/bin/pest --type-coverage --min=100 → 100%
  • vendor/bin/pint --test / vendor/bin/rector --dry-run → clean

New coverage: operator set-intersection; resolver (2-hop, null intermediate, empty terminal, invalid path); backend eval (matching / non-matching / no-relation / fail-open); section + field server-side wiring; JS exclusion; per-entity scoping + backward-compat defaults; and a consolidated field/section/backend/JS parity test. Adds a Post belongsToMany Tag test fixture (prior fixtures had only BelongsTo).

Notes

  • N+1 if a relation-conditioned field is placed in a table/export (evaluated per row) — documented inline; eager-load the configured paths there if needed.

Wires RelationAttribute conditions into VisibilityData.evaluate():
relation branch resolves related keys via RelationConditionResolver
before the model-attribute branch; null record fails open. Generalises
the flag-skip guard to cover both relation and model-attribute sources.
Adds hasRelationAttributeConditions() helper. Enables
MODEL_ATTRIBUTE_CONDITIONS in the test environment so both new and
existing condition tests run under the same flag gate.
Apply Rector RemoveNullArgOnNullDefaultParamRector and EncapsedStringsToSprintfRector
to our changed files; fix Pint import ordering and spacing in RelationConditionResolver.
…show without the model-attribute flag

buildConditionSchema() previously gated the source Select at build time using
getEntityType() without $get, which resolved to null in Livewire action contexts
(e.g. the Add Section modal) where request()/route() params are absent. This
caused isRelationSourceAvailable() to return false and fell back to Hidden, hiding
the relation source option for any entity that has conditionRelations.

Replace the build-time if/else block with a single always-declared Select whose
->visible() closure calls getAvailableSourceOptions($get) — which already resolves
the entity correctly at render time — and returns true when count > 1. Drop the
now-unused $modelAttrsEnabled/$relationsAvailable/$showSourceSelect conditionals
and replace the $fieldCodeSpan/$operatorSpan/$valueSpan expressions with fixed
literals (3/2/4), matching the previous "source shown" column layout.
@ManukMinasyan ManukMinasyan merged commit 1d01043 into 3.x Jun 9, 2026
4 checks passed
@ManukMinasyan ManukMinasyan deleted the feat/cross-record-visibility-conditions branch June 9, 2026 19:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant