Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tree-view-form-state-dynamic-nodes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@openproject/primer-view-components': patch
---

Fix TreeView hidden form inputs falling out of sync when checked nodes are inserted or removed after the component connects.
16 changes: 16 additions & 0 deletions app/components/primer/alpha/tree_view/tree_view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ export class TreeViewElement extends HTMLElement {
// nodes based on the component's select strategy. These two observers can conflict and cause infinite
// looping, so we make sure something actually changed before computing inputs again.
const somethingChanged = mutations.some(m => {
if (m.type === 'childList') {
return this.#containsCheckedTreeItem(m.addedNodes) || this.#containsCheckedTreeItem(m.removedNodes)
}

if (!(m.target instanceof HTMLElement)) return false
return m.target.getAttribute('aria-checked') !== m.oldValue
})
Comment thread
myabc marked this conversation as resolved.
Expand All @@ -51,6 +55,7 @@ export class TreeViewElement extends HTMLElement {
childList: true,
subtree: true,
attributeFilter: ['aria-checked'],
attributeOldValue: true,
})

// Correctly initialize the form
Expand All @@ -65,6 +70,17 @@ export class TreeViewElement extends HTMLElement {
})
}

#containsCheckedTreeItem(nodes: NodeList): boolean {
return [...nodes].some(node => {
if (!(node instanceof Element)) return false

return (
node.matches('[role=treeitem][aria-checked=true]') ||
Boolean(node.querySelector('[role=treeitem][aria-checked=true]'))
)
})
}

rootLeafNodes(): NodeListOf<HTMLElement> {
return this.querySelectorAll(':scope > ul > li > .TreeViewItemContainer [role=treeitem]')
}
Expand Down
47 changes: 47 additions & 0 deletions test/system/alpha/tree_view_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -952,6 +952,53 @@ def test_initial_form_state
assert_equal "{\"path\":[\"src\",\"button.rb\"],\"nodeId\":\"src-button-rb\",\"value\":\"1\"}", response.dig("form_params", "folder_structure", 0)
end

def test_form_state_updates_when_checked_nodes_are_inserted
visit_preview(:form_input, expanded: true, route_format: :json)

evaluate_multiline_script(<<~JS)
const tree = document.querySelector("tree-view")
const checkedNode = tree.querySelector("[role=treeitem][aria-checked=true]")
const clone = checkedNode.closest("li").cloneNode(true)
const clonedTreeItem = clone.querySelector("[role=treeitem]")

clonedTreeItem.removeAttribute("id")
clonedTreeItem.removeAttribute("data-node-id")
clonedTreeItem.removeAttribute("data-value")
clonedTreeItem.setAttribute("data-path", JSON.stringify(["async.rb"]))

tree.querySelector(":scope > ul").append(clone)
JS

assert_selector("[data-target='tree-view.formInputContainer'] input", count: 2, visible: :all)

find("button[type=submit]").click

response = JSON.parse(find("pre").text)

assert_includes response.dig("form_params", "folder_structure"), "{\"path\":[\"async.rb\"]}"
end

def test_form_state_updates_when_checked_nodes_are_removed
visit_preview(:form_input, expanded: true, route_format: :json)

assert_selector("[data-target='tree-view.formInputContainer'] input", count: 1, visible: :all)

evaluate_multiline_script(<<~JS)
const tree = document.querySelector("tree-view")
const checkedNode = tree.querySelector("[role=treeitem][aria-checked=true]")

checkedNode.closest("li").remove()
JS

assert_selector("[data-target='tree-view.formInputContainer'] input", count: 0, visible: :all)

find("button[type=submit]").click

response = JSON.parse(find("pre").text)

assert_nil response.dig("form_params", "folder_structure")
end

def test_form_submission_with_single_select_variant
visit_preview(:form_input, expanded: true, select_variant: :single, route_format: :json)

Expand Down
Loading