Skip to content

Comments

Added expand/collapse functionalities to relationship formwidget#1374

Open
damsfx wants to merge 14 commits intowintercms:developfrom
damsfx:collapsing-relation-formwidget
Open

Added expand/collapse functionalities to relationship formwidget#1374
damsfx wants to merge 14 commits intowintercms:developfrom
damsfx:collapsing-relation-formwidget

Conversation

@damsfx
Copy link
Contributor

@damsfx damsfx commented Jun 24, 2025

This allow relation model with nested tree trait to display a expandable/collapsable list of checkboxes.

relation_with_tree

The field config allow two new options :

  • displayTree: (bool) display the related model as a tree structure
  • quickTreeActions: (bool) show tree quick actions (expand all nodes, collapse all nodes and expand only selected nodes)

Summary by CodeRabbit

  • New Features
    • Tree-structured relation fields with expand/collapse, per-item toggles and scroll handling.
    • Quick tree actions: "Open all", "Close all", "Open selected".
    • Option to toggle tree rendering for relation checkbox lists.
  • Style
    • New styles for nested checkbox lists, transitions, chevrons and open-state visuals.
  • Chores
    • Frontend build step and assets added for the relation widget.
  • Localization
    • Added English and French strings for new tree controls.

@bennothommo
Copy link
Member

@damsfx ooo, I like this. And in Snowboard too! 😍

Couple of quick comments/questions:

  • How performant is it with a whole heap of items (like, say, 100s of items)?
  • Could the animation be a touch faster? Might be just on the border of being a bit too slow to feel "snappy".

@damsfx
Copy link
Contributor Author

damsfx commented Jun 27, 2025

@bennothommo

And in Snowboard too! 😍

Definitely yes!
The more I practice, the more I love it! It's good to be back in a vanilla javascript environment.

Despite this, a function written in jQuery is still called to update the scroll bar. :o(

How performant is it with a whole heap of items (like, say, 100s of items)?

The tree structure used for my tests and visible in the animation above contains 550 nodes spread over 4 levels of depth.
I can always do a test with more nodes.

Could the animation be a touch faster? Might be just on the border of being a bit too slow to feel "snappy".

For sure.

@damsfx
Copy link
Contributor Author

damsfx commented Jun 27, 2025

@bennothommo

Latest update :

  • Use promise to adjust srcollbar after expand/collaspe node(s)
  • CloseAll() close only opened nodes instead of all nodes
  • Reduce animation timmings
  • Use CSS animation end to trigger scrollbar update

@bennothommo
Copy link
Member

@damsfx don't be too concerned if you need to fall back on jQuery to do something in the Backend. A lot of the Backend still uses it and the goal of Snowboard was to remove the dependency of jQuery for the CMS side in particular.

@LukeTowers LukeTowers requested a review from bennothommo July 16, 2025 22:15
@LukeTowers
Copy link
Member

@bennothommo are you happy with this as it is?

@damsfx is there a related docs PR?

@LukeTowers LukeTowers added needs review Issues/PRs that require a review from a maintainer needs docs Issues/PRs that require changes to the documentation to be completed labels Jul 16, 2025
@damsfx
Copy link
Contributor Author

damsfx commented Jul 16, 2025

@damsfx is there a related docs PR?

Not yet, but I can add a pending PR for this.

damsfx and others added 2 commits July 17, 2025 01:19
@damsfx
Copy link
Contributor Author

damsfx commented Jul 18, 2025

@damsfx is there a related docs PR?

@LukeTowers Done : wintercms/docs#243

damsfx added a commit to Hounddd/wn-enhancedrelation-plugin that referenced this pull request Aug 8, 2025
@damsfx
Copy link
Contributor Author

damsfx commented Sep 5, 2025

@bennothommo I know your time is precious, but if you have 5 mins to take a look and give me a little feedback if you see any improvements to be made.
Thank you for your time.

@bennothommo
Copy link
Member

@damsfx sorry for the delay. Looks great, and the timing of the animation is much better.

Only issue I can see now is that it appears that some plugins and themes were added in your most recent commit:
90fbd40

Could you make sure these are not included in your branch? Once that's done, @LukeTowers should be able to merge if he's happy with it.

@bennothommo bennothommo removed the needs review Issues/PRs that require a review from a maintainer label Sep 9, 2025
Copy link
Member

@bennothommo bennothommo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved, pending removal of plugins and themes in recent commit

@damsfx
Copy link
Contributor Author

damsfx commented Sep 9, 2025

pending removal of plugins and themes in recent commit

😱🙄 Oh, I was really distracted!
Thanks for reminding me.

Done !

@damsfx
Copy link
Contributor Author

damsfx commented Feb 19, 2026

@LukeTowers @bennothommo Is this still an interesting PR for WinterCMS !?

@LukeTowers
Copy link
Member

@damsfx yes, I had completely forgotten about this. Can you resolve the comments and ping me when it's ready to review?

@LukeTowers LukeTowers added this to the 1.3.0 milestone Feb 20, 2026
@LukeTowers LukeTowers added needs review Issues/PRs that require a review from a maintainer and removed needs docs Issues/PRs that require changes to the documentation to be completed labels Feb 20, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 20, 2026

Walkthrough

Adds a tree-capable Relation form widget: backend config and asset loading, new frontend Relation JS plugin for expand/collapse and selection actions, template updates for tree rendering, styles for nested checkbox lists, and translation keys for tree controls.

Changes

Cohort / File(s) Summary
Relation Widget Backend
modules/backend/formwidgets/Relation.php, modules/backend/formwidgets/relation/partials/_relation.php
Adds public $displayTree, loadAssets() and includes displayTree in config. Markup adds data-control="relation". Options rendering now selects between toNestedArray, listsNested, or lists depending on tree usage and displayTree.
Frontend JS Plugin
modules/backend/formwidgets/relation/assets/js/src/Relation.js
New Snowboard plugin implementing tree checkbox UI with defaults, attach/detach events, expand/collapse/expand-checked controls, node toggling, scrollbar updates, and plugin/widget registration.
Styling (LESS/CSS)
modules/backend/formwidgets/relation/assets/less/relation.less, modules/backend/formwidgets/relation/assets/less/relation.css
Adds styles for nested checkbox lists, chevron toggle, open/close transitions, spacing, and scrollable container behavior for tree UI.
Checkbox List Template
modules/backend/widgets/form/partials/_field_checkboxlist.php
Introduces displayTree and quickTreeActions, makes list scrollable when tree enabled, adds recursive renderCheckboxLine for nested rendering, and renders quick-tree controls (Expand all, Expand checked, Collapse all).
Localization
modules/backend/lang/en/lang.php, modules/backend/lang/fr/lang.php
Adds translation keys expand_all, collapse_all, expand_checked (English and French).
Build Configuration
modules/backend/winter.mix.js
Adds build step to compile the Relation frontend asset into the distribution bundle.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant Backend
participant Browser
participant RelationJS as Relation Plugin
participant User

Backend->>Browser: Render form (includes `data-control="relation"` and options JSON)
Browser->>Browser: Load assets (relation.js, relation.css)
Browser->>RelationJS: Initialize plugin on control
RelationJS->>RelationJS: Parse config, attach event handlers
User->>RelationJS: Click "Expand all" / Toggle node / Select checkbox
RelationJS->>RelationJS: Open/close levels, update checked state, update scrollbar
RelationJS->>Browser: Update DOM (classes, aria, checkbox states)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding expand/collapse functionalities to the relationship formwidget, which is the primary objective demonstrated throughout the PR with multiple file additions for tree rendering, JavaScript interactions, and styling.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
modules/backend/formwidgets/Relation.php (1)

184-196: ⚠️ Potential issue | 🟠 Major

Duplicate $field->options assignment — the first one (lines 184–186) is dead code.

Lines 184–186 set $field->options, then lines 188–196 immediately overwrite it with the new tree-aware logic. The original assignment should be removed.

Proposed fix
-            $field->options = $usesTree
-                ? $result->listsNested($nameFrom, $primaryKeyName)
-                : $result->lists($nameFrom, $primaryKeyName);
-
             if ($usesTree) {
                 if ($this->displayTree) {
                     $field->options = $result->toNestedArray($nameFrom, $primaryKeyName);
                 } else {
                     $field->options = $result->listsNested($nameFrom, $primaryKeyName);
                 }
             } else {
                 $field->options = $result->lists($nameFrom, $primaryKeyName);
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/backend/formwidgets/Relation.php` around lines 184 - 196, Remove the
dead initial assignment to $field->options that uses the ternary based on
$usesTree; instead keep the subsequent conditional block which correctly handles
$usesTree and $this->displayTree and calls $result->toNestedArray,
$result->listsNested, or $result->lists as appropriate. Specifically, delete the
first assignment (the ternary setting $field->options) and rely on the existing
if ($usesTree) { ... } else { ... } logic in Relation.php so $field->options is
only set once with the correct tree-aware method.
🧹 Nitpick comments (7)
modules/backend/formwidgets/relation/assets/less/relation.less (1)

16-19: Prefer flex-start over start for justify-content.

justify-content: start is a CSS Box Alignment value, while flex-start is the original Flexbox-specific value with broader browser support. Given the project's browser targets, flex-start is the safer choice.

Proposed fix
-            justify-content: start;
+            justify-content: flex-start;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/backend/formwidgets/relation/assets/less/relation.less` around lines
16 - 19, Replace the CSS value "start" with the Flexbox-specific "flex-start"
for the justify-content declaration in relation.less (the block that sets
display: flex; flex-direction: row; align-items: center; justify-content:
start;), changing justify-content to "flex-start" to ensure broader browser
support.
modules/backend/lang/en/lang.php (1)

326-328: Minor terminology inconsistency with existing dashboard translations.

The dashboard section (lines 107–108) uses "Collapse all" / "Expand all", while these new form keys use "Close all" / "Open all". This isn't a bug, but you might want to unify the wording for a consistent user experience across the backend UI.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/backend/lang/en/lang.php` around lines 326 - 328, The wording in the
form translation keys is inconsistent with the dashboard translations: update
the values for 'expand_all', 'collapse_all', and 'expand_checked' in the
lang.php translations so they match the dashboard terminology (use "Expand all"
instead of "Open all", "Collapse all" instead of "Close all", and "Expand
selected" instead of "Open selected") to ensure consistent UX across the backend
UI.
modules/backend/formwidgets/relation/assets/js/src/Relation.js (3)

183-199: Parameter el in onToggle is actually an Event, not an HTMLElement.

The addEventListener('click', this.events.toggle) passes the Event object. The parameter is named el (and documented as @param {HTMLElement} el), but el.target.parentElement is used, confirming it's an Event. Rename for clarity.

Proposed fix
-        onToggle(el) {
-            const tooglePromise = new Promise((resolve, reject) => {
-                let parent = el.target.parentElement;
+        onToggle(event) {
+            const togglePromise = new Promise((resolve, reject) => {
+                let parent = event.target.parentElement;

Also fixes the tooglePromise typo → togglePromise.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/backend/formwidgets/relation/assets/js/src/Relation.js` around lines
183 - 199, The onToggle method currently treats its parameter as an HTMLElement
and names it `el`, but it receives an Event from addEventListener('click',
this.events.toggle); rename the parameter (e.g., `event` or `evt`) and update
usages to `event.target.parentElement` (or safely `event.currentTarget` if
appropriate) and correct the variable typo `tooglePromise` to `togglePromise` in
the onToggle function so the code is clearer and accurate; ensure JSDoc/@param
is updated from HTMLElement to Event as well.

88-95: querySelectorAll matches all descendants, not just direct children.

el.querySelectorAll('.checkboxlist-children')[0] will match deeply nested .checkboxlist-children in addition to the direct child. Use :scope > .checkboxlist-children to restrict to direct children and avoid unintended matches in deeply nested trees.

Proposed fix for openLevel and closeLevel
         openLevel(el) {
             el.classList.add('open');
 
-            let child = el.querySelectorAll('.checkboxlist-children')[0];
+            let child = el.querySelector(':scope > .checkboxlist-children');
             if (child) {
                 child.classList.add('open');
             }
         }
 
         closeLevel(el) {
             el.classList.remove('open');
 
-            let child = el.querySelectorAll('.checkboxlist-children')[0];
+            let child = el.querySelector(':scope > .checkboxlist-children');
             if (child) {
                 child.classList.remove('open');
             }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/backend/formwidgets/relation/assets/js/src/Relation.js` around lines
88 - 95, openLevel (and the sibling closeLevel) currently use
el.querySelectorAll('.checkboxlist-children')[0], which can return deeply nested
descendants; change these to select only direct children by using
el.querySelector(':scope > .checkboxlist-children') (or an equivalent
direct-child selection) so only the immediate .checkboxlist-children element is
toggled in openLevel and closeLevel; update both functions (openLevel and
closeLevel) to use the :scope > selector and keep the existing
classList.add/remove logic.

116-130: Promises add no value here — all operations are synchronous.

onExpandAll, onCollapseAll, onExpandChecked, and onToggle all wrap synchronous DOM class manipulations in new Promise(...). Since the executor runs synchronously, the .then() callback fires in the same microtask batch. Plain sequential calls would be clearer.

Example simplification for onExpandAll
         onExpandAll() {
-            const openPromise = new Promise((resolve, reject) => {
-                let animatedNodes = this.getExpandableNodes();
-
-                animatedNodes.forEach((item) => {
-                    this.openLevel(item);
-                });
-
-                resolve([].slice.call(animatedNodes).pop());
-            });
-
-            openPromise.then((el) => {
-                this.updateScollBar(el);
-            });
+            const animatedNodes = this.getExpandableNodes();
+            animatedNodes.forEach((item) => this.openLevel(item));
+            this.updateScrollBar(animatedNodes[animatedNodes.length - 1]);
         }

Same pattern applies to onCollapseAll, onExpandChecked, and onToggle.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/backend/formwidgets/relation/assets/js/src/Relation.js` around lines
116 - 130, The Promise wrapper is unnecessary because getExpandableNodes(),
openLevel(), etc. are synchronous; in onExpandAll remove the new Promise(...)
and simply iterate over this.getExpandableNodes(), call this.openLevel(item) for
each, then call this.updateScollBar(...) directly with the last element; apply
the same simplification to onCollapseAll, onExpandChecked, and onToggle (remove
new Promise, perform DOM class changes synchronously, then call the
corresponding this.updateScollBar/updateScrollBar invocation). Use the existing
method names getExpandableNodes, openLevel, onCollapseAll, onExpandChecked,
onToggle, and updateScollBar when locating code to change.
modules/backend/formwidgets/Relation.php (2)

52-55: Minor docblock typo.

"rendered has a displayTree" → "rendered as a tree".

Proposed fix
     /**
-     * `@var` bool Define if the widget must be rendered has a displayTree.
+     * `@var` bool Define if the widget must be rendered as a tree.
      */
     public $displayTree;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/backend/formwidgets/Relation.php` around lines 52 - 55, Docblock for
the Relation widget property $displayTree contains a typo ("rendered has a
displayTree"); update the comment for the public $displayTree property in
Relation.php to read clearly (e.g., "Define if the widget must be rendered as a
tree") so the docblock accurately describes the boolean flag and its purpose.

163-164: class_uses() doesn't traverse parent classes — use class_uses_recursive() instead.

class_uses($relationModel) only returns traits directly used by the class, not inherited ones. If a model extends a base that uses NestedTree, this check will silently fail and displayTree will have no effect. This is pre-existing code, but the new displayTree feature now depends on it. The codebase already uses class_uses_recursive() elsewhere (InspectableContainer.php), establishing the correct pattern.

Proposed fix
-            $usesTree = count(array_intersect($treeTraits, class_uses($relationModel))) > 0;
+            $usesTree = count(array_intersect($treeTraits, class_uses_recursive($relationModel))) > 0;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/backend/formwidgets/Relation.php` around lines 163 - 164, The check
that determines whether the relation model uses a tree trait uses
class_uses($relationModel), which only returns traits declared on the class
itself; replace that call with class_uses_recursive($relationModel) so inherited
traits like Winter\Storm\Database\Traits\NestedTree or SimpleTree are detected;
update the $usesTree assignment (which compares $treeTraits against the
class_uses result) to call class_uses_recursive($relationModel) instead of
class_uses($relationModel) so displayTree behavior works for models inheriting
tree traits.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@modules/backend/formwidgets/Relation.php`:
- Around line 188-190: The code calls a non-existent method toNestedArray on the
collection ($result->toNestedArray($nameFrom, $primaryKeyName)) when
$this->displayTree is true; replace that call with the existing, tested
collection helper listsNested (i.e. use $result->listsNested($nameFrom,
$primaryKeyName)) or implement an equivalent toNestedArray on the collection,
and keep the fallback logic for non-tree relations intact so Relation.php uses
listsNested instead of toNestedArray when building $field->options.

In `@modules/backend/formwidgets/relation/assets/js/src/Relation.js`:
- Around line 73-81: destruct() currently assumes the tree action controls exist
and calls removeEventListener unconditionally, causing a TypeError when
expandAllControl/collapseAllControl/expandCheckedControl are null; update
destruct() to mirror attachEvents() by null-checking each of expandAllControl,
collapseAllControl, and expandCheckedControl before calling removeEventListener,
and keep the existing loop for this.toggles that removes events.toggle;
reference the destruct() and attachEvents() methods and the properties
expandAllControl, collapseAllControl, expandCheckedControl, toggles, and
events.toggle when making the change.
- Around line 206-216: The method updateScollBar has a typo and can throw a
TypeError when openedLevel is null; rename the method to updateScrollBar and
update every call site that uses updateScollBar, then inside the (now)
updateScrollBar method compute openedLevel as before but guard it with a null
check (if openedLevel is null/undefined return) before calling
openedLevel.addEventListener; keep the existing transitionend handler and
options unchanged. Ensure all references in this file that called updateScollBar
(the earlier calls around the checkbox/children handling) are renamed to
updateScrollBar so usages and exports stay consistent.

In `@modules/backend/formwidgets/relation/assets/less/relation.less`:
- Around line 4-9: Remove the empty line between the rule opening brace and the
comment in relation.less: locate the selector ".field-checkboxlist
.checkboxlist-controls > div:nth-child(even)" and the comment "// Top widget
controls" and place the comment immediately after the opening brace (no blank
line) so the scss/double-slash-comment-empty-line-before lint rule is satisfied.

In `@modules/backend/widgets/form/partials/_field_checkboxlist.php`:
- Line 136: The call to e(renderCheckboxLine(...)) is incorrect because
renderCheckboxLine() echoes HTML and returns void; remove the unnecessary e()
wrapper and call renderCheckboxLine($field, $checkedValues, $children,
$nameFrom, $readOnly, $index) directly in the template so the output is rendered
as intended, and if any escaping is actually required wrap the producing
function to return a string instead of echoing before escaping.
- Around line 96-143: The named function renderCheckboxLine causes a fatal
redeclaration when the partial is included multiple times; replace it with a
recursive closure: define $renderCheckboxLine = function($field, array
$checkedValues, array $fieldOptions, string $nameFrom, bool $readOnly, int
&$index) use (&$renderCheckboxLine) { ... } containing the same body, and update
the recursive call and any invocations to call $renderCheckboxLine(...) (remove
the wrapping e(...) used for renderCheckboxLine since the closure echoes
directly) so the template uses the closure instead of a global function.
- Line 2: The partial currently unconditionally reads $this->nameFrom which
doesn't exist on Form widget; change the assignment so it only reads
$this->nameFrom when available and otherwise uses a passed-in fallback (e.g. a
local $nameFrom variable) or null — do not access $this->nameFrom on Form
contexts. Ensure the conditional access is applied before any use (notably where
displayTree is handled around the block that references nameFrom later), and
update the partial to accept/use a provided $nameFrom when rendered from widgets
that don't expose $this->nameFrom.

---

Outside diff comments:
In `@modules/backend/formwidgets/Relation.php`:
- Around line 184-196: Remove the dead initial assignment to $field->options
that uses the ternary based on $usesTree; instead keep the subsequent
conditional block which correctly handles $usesTree and $this->displayTree and
calls $result->toNestedArray, $result->listsNested, or $result->lists as
appropriate. Specifically, delete the first assignment (the ternary setting
$field->options) and rely on the existing if ($usesTree) { ... } else { ... }
logic in Relation.php so $field->options is only set once with the correct
tree-aware method.

---

Nitpick comments:
In `@modules/backend/formwidgets/Relation.php`:
- Around line 52-55: Docblock for the Relation widget property $displayTree
contains a typo ("rendered has a displayTree"); update the comment for the
public $displayTree property in Relation.php to read clearly (e.g., "Define if
the widget must be rendered as a tree") so the docblock accurately describes the
boolean flag and its purpose.
- Around line 163-164: The check that determines whether the relation model uses
a tree trait uses class_uses($relationModel), which only returns traits declared
on the class itself; replace that call with class_uses_recursive($relationModel)
so inherited traits like Winter\Storm\Database\Traits\NestedTree or SimpleTree
are detected; update the $usesTree assignment (which compares $treeTraits
against the class_uses result) to call class_uses_recursive($relationModel)
instead of class_uses($relationModel) so displayTree behavior works for models
inheriting tree traits.

In `@modules/backend/formwidgets/relation/assets/js/src/Relation.js`:
- Around line 183-199: The onToggle method currently treats its parameter as an
HTMLElement and names it `el`, but it receives an Event from
addEventListener('click', this.events.toggle); rename the parameter (e.g.,
`event` or `evt`) and update usages to `event.target.parentElement` (or safely
`event.currentTarget` if appropriate) and correct the variable typo
`tooglePromise` to `togglePromise` in the onToggle function so the code is
clearer and accurate; ensure JSDoc/@param is updated from HTMLElement to Event
as well.
- Around line 88-95: openLevel (and the sibling closeLevel) currently use
el.querySelectorAll('.checkboxlist-children')[0], which can return deeply nested
descendants; change these to select only direct children by using
el.querySelector(':scope > .checkboxlist-children') (or an equivalent
direct-child selection) so only the immediate .checkboxlist-children element is
toggled in openLevel and closeLevel; update both functions (openLevel and
closeLevel) to use the :scope > selector and keep the existing
classList.add/remove logic.
- Around line 116-130: The Promise wrapper is unnecessary because
getExpandableNodes(), openLevel(), etc. are synchronous; in onExpandAll remove
the new Promise(...) and simply iterate over this.getExpandableNodes(), call
this.openLevel(item) for each, then call this.updateScollBar(...) directly with
the last element; apply the same simplification to onCollapseAll,
onExpandChecked, and onToggle (remove new Promise, perform DOM class changes
synchronously, then call the corresponding this.updateScollBar/updateScrollBar
invocation). Use the existing method names getExpandableNodes, openLevel,
onCollapseAll, onExpandChecked, onToggle, and updateScollBar when locating code
to change.

In `@modules/backend/formwidgets/relation/assets/less/relation.less`:
- Around line 16-19: Replace the CSS value "start" with the Flexbox-specific
"flex-start" for the justify-content declaration in relation.less (the block
that sets display: flex; flex-direction: row; align-items: center;
justify-content: start;), changing justify-content to "flex-start" to ensure
broader browser support.

In `@modules/backend/lang/en/lang.php`:
- Around line 326-328: The wording in the form translation keys is inconsistent
with the dashboard translations: update the values for 'expand_all',
'collapse_all', and 'expand_checked' in the lang.php translations so they match
the dashboard terminology (use "Expand all" instead of "Open all", "Collapse
all" instead of "Close all", and "Expand selected" instead of "Open selected")
to ensure consistent UX across the backend UI.

-Treeview nw compatible with a custom SQL select statement to use for the name
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (6)
modules/backend/formwidgets/Relation.php (2)

52-55: $displayTree should be explicitly initialised to false.

The property is left at null, relying on PHP's falsy behaviour. The doc-comment says "Define if the widget must be rendered has a displayTree" (boolean), so false better communicates intent and is consistent with the documented type.

🛠️ Proposed fix
-    public $displayTree;
+    public $displayTree = false;

Also, the doc comment has a grammar slip: "rendered has a displayTree""rendered as a tree".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/backend/formwidgets/Relation.php` around lines 52 - 55, The public
property $displayTree in class Relation is left uninitialized (null) but
documented as a boolean; explicitly initialize it to false (public $displayTree
= false;) to match the doc type and intent, and update the doc-comment text from
"rendered has a displayTree" to "rendered as a tree" to fix the grammar and
clarify meaning.

163-164: class_uses() misses tree traits inherited from parent classes — use class_uses_recursive().

class_uses($relationModel) only returns traits used directly on that class. If a model extends a base class that uses NestedTree or SimpleTree, the detection silently returns false and the tree path is never taken. Laravel's class_uses_recursive() traverses the entire inheritance chain.

🛠️ Proposed fix
-            $usesTree = count(array_intersect($treeTraits, class_uses($relationModel))) > 0;
+            $usesTree = count(array_intersect($treeTraits, class_uses_recursive($relationModel))) > 0;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/backend/formwidgets/Relation.php` around lines 163 - 164, The trait
check for tree models currently uses class_uses($relationModel) which only
inspects traits on the class itself; replace it with
class_uses_recursive($relationModel) so inherited traits on parent classes are
detected. Update the $usesTree computation (the line using $treeTraits and
$usesTree) to call class_uses_recursive($relationModel) instead of
class_uses($relationModel) so NestedTree and SimpleTree traits inherited from
base models are recognized.
modules/backend/formwidgets/relation/assets/js/src/Relation.js (2)

190-190: Typo: tooglePromisetogglePromise.

🛠️ Proposed fix
-            const tooglePromise = new Promise((resolve, reject) => {
+            const togglePromise = new Promise((resolve, reject) => {
-            tooglePromise.then((parent) => {
+            togglePromise.then((parent) => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/backend/formwidgets/relation/assets/js/src/Relation.js` at line 190,
Rename the misspelled variable tooglePromise to togglePromise in Relation.js:
update the Promise declaration (const tooglePromise) and all usages/references
to that variable (including any places that await, resolve, reject, or return
it) so the identifier is consistent throughout the Relation component (e.g.,
where tooglePromise is created and later referenced).

104-104: Doc typo: "signle" → "single".

-         * Close an signle level of the tree
+         * Close a single level of the tree
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/backend/formwidgets/relation/assets/js/src/Relation.js` at line 104,
Fix the documentation typo in Relation.js: change the comment "Close an signle
level of the tree" to "Close a single level of the tree" (the comment
immediately above the function that closes a tree level, e.g., the
closeLevel/closeOneLevel method in Relation.js). Ensure only the wording is
corrected and no code logic is changed.
modules/backend/formwidgets/relation/assets/less/relation.less (1)

13-13: Extraneous space before colon in custom property declaration.

--background-padding : 10px should not have a space before the colon.

🛠️ Proposed fix
-            --background-padding : 10px;
+            --background-padding: 10px;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/backend/formwidgets/relation/assets/less/relation.less` at line 13,
The custom CSS property declaration "--background-padding : 10px" has an
extraneous space before the colon; update the declaration of the
--background-padding variable in relation.less (the line defining
--background-padding) to remove the space before the colon so it follows the
correct "--background-padding: 10px" formatting.
modules/backend/formwidgets/relation/assets/less/relation.css (1)

1-55: relation.css in the less/ directory duplicates relation.less output — risk of divergence.

Relation.js already imports relation.less directly for the build pipeline, and loadAssets() only enqueues JS. The relation.css file appears to be a manually maintained flat-CSS copy of the compiled LESS rules. If relation.less is updated without updating relation.css, they will silently diverge. Clarify the intended role of this file, or remove it in favour of the LESS source alone.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/backend/formwidgets/relation/assets/less/relation.css` around lines 1
- 55, The relation.css file is a duplicated compiled output of relation.less and
risks divergence; update the codebase so there's a single source-of-truth:
either delete relation.css and ensure Relation.js (or the asset build pipeline
invoked by loadAssets()) imports/serves the compiled relation.less output at
build time, or mark relation.css as a generated artifact and add a build step
that compiles relation.less into relation.css automatically. Modify
Relation.js/loadAssets() or the build config to reference the generated CSS (not
a manually maintained relation.css) and remove the manual file if you choose the
first option.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@modules/backend/formwidgets/relation/assets/js/src/Relation.js`:
- Around line 212-226: The updateScrollBar method uses a global selector and
calls .data('oc.scrollbar').update() unsafely; change it to scope the scrollbar
lookup to this.element (e.g. use this.element.find('[data-control=scrollbar]'))
and, after selecting the jQuery element, retrieve the plugin instance and guard
before calling update (check that the jQuery selection is non-empty and that
selection.data('oc.scrollbar') is defined) inside the transitionend handler in
updateScrollBar so expanding/collapsing only updates the local scrollbar and
avoids TypeError when no plugin exists.

In `@modules/backend/formwidgets/relation/assets/less/relation.css`:
- Line 7: Stylelint flags are due to missing blank lines after CSS custom
property declarations before the next regular declarations; locate the custom
property declarations in relation.css (the --* lines) that precede declarations
like the "display: flex;" rule and insert a single empty line between each
custom-property block and the following regular property(s) so the
declaration-empty-line-before rule is satisfied.

In `@modules/backend/widgets/form/partials/_field_checkboxlist.php`:
- Line 146: Remove the leftover commented debug statements in the checkbox list
partial: delete the lines containing "// debug($fieldOptions)" and "//
debug($option)" in the _field_checkboxlist.php partial (and any other similar
"// debug(...)" calls in that file) so the production code contains no
commented-out debug artifacts.
- Line 93: The tree rendering path initializes $index = 1 which causes tree
checkbox IDs to start at 2; change the tree-mode initialization of the $index
variable from 1 to 0 so it matches flat mode and yields the first checkbox ID as
checkbox_..._1 (update the $index assignment in the tree branch inside
_field_checkboxlist.php).

---

Nitpick comments:
In `@modules/backend/formwidgets/Relation.php`:
- Around line 52-55: The public property $displayTree in class Relation is left
uninitialized (null) but documented as a boolean; explicitly initialize it to
false (public $displayTree = false;) to match the doc type and intent, and
update the doc-comment text from "rendered has a displayTree" to "rendered as a
tree" to fix the grammar and clarify meaning.
- Around line 163-164: The trait check for tree models currently uses
class_uses($relationModel) which only inspects traits on the class itself;
replace it with class_uses_recursive($relationModel) so inherited traits on
parent classes are detected. Update the $usesTree computation (the line using
$treeTraits and $usesTree) to call class_uses_recursive($relationModel) instead
of class_uses($relationModel) so NestedTree and SimpleTree traits inherited from
base models are recognized.

In `@modules/backend/formwidgets/relation/assets/js/src/Relation.js`:
- Line 190: Rename the misspelled variable tooglePromise to togglePromise in
Relation.js: update the Promise declaration (const tooglePromise) and all
usages/references to that variable (including any places that await, resolve,
reject, or return it) so the identifier is consistent throughout the Relation
component (e.g., where tooglePromise is created and later referenced).
- Line 104: Fix the documentation typo in Relation.js: change the comment "Close
an signle level of the tree" to "Close a single level of the tree" (the comment
immediately above the function that closes a tree level, e.g., the
closeLevel/closeOneLevel method in Relation.js). Ensure only the wording is
corrected and no code logic is changed.

In `@modules/backend/formwidgets/relation/assets/less/relation.css`:
- Around line 1-55: The relation.css file is a duplicated compiled output of
relation.less and risks divergence; update the codebase so there's a single
source-of-truth: either delete relation.css and ensure Relation.js (or the asset
build pipeline invoked by loadAssets()) imports/serves the compiled
relation.less output at build time, or mark relation.css as a generated artifact
and add a build step that compiles relation.less into relation.css
automatically. Modify Relation.js/loadAssets() or the build config to reference
the generated CSS (not a manually maintained relation.css) and remove the manual
file if you choose the first option.

In `@modules/backend/formwidgets/relation/assets/less/relation.less`:
- Line 13: The custom CSS property declaration "--background-padding : 10px" has
an extraneous space before the colon; update the declaration of the
--background-padding variable in relation.less (the line defining
--background-padding) to remove the space before the colon so it follows the
correct "--background-padding: 10px" formatting.

}
div[data-control="relation"] .checkboxlist-item {
--background-padding: 10px;
display: flex;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Stylelint declaration-empty-line-before errors (lines 7 and 50).

After a CSS custom property declaration, Stylelint requires an empty line before the following regular declaration.

🛠️ Proposed fix
 div[data-control="relation"] .checkboxlist-item {
   --background-padding: 10px;
+
   display: flex;
 div[data-control="relation"] .checkboxlist-item .checkboxlist-item {
   --background-padding: 20px;
+
   margin-left: 10px;

Also applies to: 50-50

🧰 Tools
🪛 Stylelint (17.3.0)

[error] 7-7: Expected empty line before declaration (declaration-empty-line-before)

(declaration-empty-line-before)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/backend/formwidgets/relation/assets/less/relation.css` at line 7,
Stylelint flags are due to missing blank lines after CSS custom property
declarations before the next regular declarations; locate the custom property
declarations in relation.css (the --* lines) that precede declarations like the
"display: flex;" rule and insert a single empty line between each
custom-property block and the following regular property(s) so the
declaration-empty-line-before rule is satisfied.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
modules/backend/widgets/form/partials/_field_checkboxlist.php (1)

11-43: ⚠️ Potential issue | 🟠 Major

Read-only path silently drops all checked nested (children) items.

The foreach at line 16 iterates only the top-level keys of $fieldOptions. When displayTree is enabled, the options contain a 'children' key with nested descendants. Any item that is checked at depth > 1 passes the in_array($value, $checkedValues) guard (correctly), but the loop never visits it — so it is never rendered. A user will see an empty or partial read-only list even though nested values are persisted.

🐛 Proposed fix — recurse through tree children in the read-only path
 <?php if ($readOnly && $field->value): ?>

     <div class="field-checkboxlist">
         <?php
         $index = 0;
-        foreach ($fieldOptions as $value => $option):
-            $index++;
-            $checkboxId = 'checkbox_'.$field->getId().'_'.$index;
-            if (!in_array($value, $checkedValues)) {
-                continue;
-            }
-            if (!is_array($option)) {
-                $option = [$option];
-            }
-            ?>
-            <div class="checkbox custom-checkbox">
-                <input
-                    type="checkbox"
-                    id="<?= $checkboxId ?>"
-                    name="<?= $field->getName() ?>[]"
-                    value="<?= e($value) ?>"
-                    disabled="disabled"
-                    checked="checked">
-
-                <label for="<?= $checkboxId ?>">
-                    <?= e(trans($option[0])) ?>
-                </label>
-                <?php if (isset($option[1])): ?>
-                    <p class="help-block"><?= e(trans($option[1])) ?></p>
-                <?php endif ?>
-            </div>
-        <?php endforeach ?>
+        $renderReadOnlyLine = function (array $options) use (&$renderReadOnlyLine, &$index, $field, $checkedValues): void {
+            foreach ($options as $value => $option):
+                $index++;
+                $checkboxId = 'checkbox_'.$field->getId().'_'.$index;
+                if (!is_array($option)) {
+                    $option = [$option];
+                }
+                $children = array_get($option, 'children', []);
+                if (in_array($value, $checkedValues)):
+                    ?>
+                    <div class="checkbox custom-checkbox">
+                        <input
+                            type="checkbox"
+                            id="<?= $checkboxId ?>"
+                            name="<?= $field->getName() ?>[]"
+                            value="<?= e($value) ?>"
+                            disabled="disabled"
+                            checked="checked">
+                        <label for="<?= $checkboxId ?>">
+                            <?= e(trans($option[0])) ?>
+                        </label>
+                        <?php if (isset($option[1])): ?>
+                            <p class="help-block"><?= e(trans($option[1])) ?></p>
+                        <?php endif ?>
+                    </div>
+                    <?php
+                endif;
+                if (!empty($children)) {
+                    $renderReadOnlyLine($children);
+                }
+            endforeach;
+        };
+        $renderReadOnlyLine($fieldOptions);
         ?>
     </div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/backend/widgets/form/partials/_field_checkboxlist.php` around lines
11 - 43, The read-only loop over $fieldOptions only iterates top-level items and
thus skips nested 'children' entries; fix by replacing the flat foreach with a
recursive renderer (e.g. a local function/closure like renderCheckedOptions)
that accepts $options and a reference to $index, walks each node, checks
in_array($value, $checkedValues) and renders the checkbox/label/help-block when
matched, and if an item has a 'children' key calls itself on that children
array; ensure $checkboxId uses the shared $index (incremented in the recursive
function) so IDs remain unique.
🧹 Nitpick comments (2)
modules/backend/widgets/form/partials/_field_checkboxlist.php (2)

124-124: Inconsistency: reset($option) for label vs $option[0] used everywhere else.

The tree path uses reset($option) to retrieve the label, while the flat editable path (line 162) and the flat read-only path (line 36) both use $option[0]. For the expected array shape ['Label', 'children' => [...]] they produce the same value, but reset() mutates the internal array pointer and signals a different access pattern to readers. Use $option[0] for consistency.

♻️ Proposed fix
-                                        <?= e(trans(reset($option))) ?>
+                                        <?= e(trans($option[0])) ?>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/backend/widgets/form/partials/_field_checkboxlist.php` at line 124,
The template uses reset($option) to render the label in the tree path, which
mutates the array pointer and is inconsistent with other places that use
$option[0]; replace the reset($option) usage in _field_checkboxlist.php with
$option[0] so the label is accessed non-destructively and matches the flat
editable/read-only paths that reference $option[0].

112-138: Tree nodes silently discard $option[1] help text.

The flat rendering path (lines 164–166) renders a .help-block paragraph when $option[1] is set. The tree rendering closure has no equivalent — option arrays with a description at index 1 will silently lose that text for tree nodes.

♻️ Proposed fix — add help text after the label in the tree path
                                     <label for="<?= $checkboxId ?>">
                                         <?= e(trans($option[0])) ?>
                                     </label>
+                                    <?php if (isset($option[1])): ?>
+                                        <p class="help-block"><?= e(trans($option[1])) ?></p>
+                                    <?php endif ?>
                                 </div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/backend/widgets/form/partials/_field_checkboxlist.php` around lines
112 - 138, The tree rendering path for checkbox list items omits option help
text stored in $option[1]; update the tree branch inside the checkboxlist-item
(the block that renders the <input> and <label> where $checkboxId, $option and
$children are used) to output the same help paragraph as the flat path — render
a <p class="help-block"> with the escaped/transformed $option[1] immediately
after the <label> when isset($option[1])/!empty($option[1]); ensure this change
mirrors the flat rendering behavior and does not affect the existing
$renderCheckboxLine recursive call for children.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@modules/backend/widgets/form/partials/_field_checkboxlist.php`:
- Around line 64-72: The partial _field_checkboxlist.php renders tree controls
(data-field-checkboxlist-expand-all, data-field-checkboxlist-expand-checked,
data-field-checkboxlist-collapse-all and checkboxlist-item-toggle anchors)
whenever field.displayTree is truthy, but the click handlers live only in
modules/backend/formwidgets/relation/assets/js/src/Relation.js and are not
loaded for other widgets; fix by either validating/enforcing displayTree to be
used only with the Relation widget at form field creation (e.g., in
createFormField or the FormField factory: check field.displayTree and throw or
clear it unless the widget is Relation) or extract the tree logic from
Relation.js into a standalone asset (e.g., checkboxlist-tree.js) and load that
script from the partial when field.displayTree is true so the handlers for the
data-field-checkboxlist-* and checkboxlist-item-toggle attributes are always
registered.

---

Outside diff comments:
In `@modules/backend/widgets/form/partials/_field_checkboxlist.php`:
- Around line 11-43: The read-only loop over $fieldOptions only iterates
top-level items and thus skips nested 'children' entries; fix by replacing the
flat foreach with a recursive renderer (e.g. a local function/closure like
renderCheckedOptions) that accepts $options and a reference to $index, walks
each node, checks in_array($value, $checkedValues) and renders the
checkbox/label/help-block when matched, and if an item has a 'children' key
calls itself on that children array; ensure $checkboxId uses the shared $index
(incremented in the recursive function) so IDs remain unique.

---

Nitpick comments:
In `@modules/backend/widgets/form/partials/_field_checkboxlist.php`:
- Line 124: The template uses reset($option) to render the label in the tree
path, which mutates the array pointer and is inconsistent with other places that
use $option[0]; replace the reset($option) usage in _field_checkboxlist.php with
$option[0] so the label is accessed non-destructively and matches the flat
editable/read-only paths that reference $option[0].
- Around line 112-138: The tree rendering path for checkbox list items omits
option help text stored in $option[1]; update the tree branch inside the
checkboxlist-item (the block that renders the <input> and <label> where
$checkboxId, $option and $children are used) to output the same help paragraph
as the flat path — render a <p class="help-block"> with the escaped/transformed
$option[1] immediately after the <label> when
isset($option[1])/!empty($option[1]); ensure this change mirrors the flat
rendering behavior and does not affect the existing $renderCheckboxLine
recursive call for children.

@damsfx
Copy link
Contributor Author

damsfx commented Feb 20, 2026

@LukeTowers Done some CodeRabbit's suggestions after a merge from develop branch.

About him, he's a bit temperamental because every commit causes a bit of hysteria.
Once he asks for a line to be added to the minified CSS—but that's not possible—and another time he invents formwidget types that don't exist in Winter!

Minor change:
The nameFrom field's property is no more used in the rendering, the treeview mode is now compatible with a custom SQL select statement to use for the name.

Pending related PRs:

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is compiling this file?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch!
Completely useless file, probably compiled by an extension when opening the less file.

Sorry for not paying attention to that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs review Issues/PRs that require a review from a maintainer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants