From 0fe0cacc1595ad35f9c47a4517f3a17890c9f1f5 Mon Sep 17 00:00:00 2001 From: e3mrah <81884938+emrahbaysal@users.noreply.github.com> Date: Tue, 12 May 2026 13:30:31 +0400 Subject: [PATCH] fix(canvas): right-click menu actions actually work + clearer labels (#1441) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Operator reported "non of the right click functionalites working other than the open in new tab". Root cause: the previous handler only mutated urlFoldedSet, which had no visible effect when the clicked group was folded by the depth default (same class of bug toggleFold had before #1439). The menu items also had confusing labels ("Fold to level N" stepped GLOBAL depth, not subtree-relative). Rewrite to use the same compose-state pattern toggleFold uses: - "Show only this group" — switch to depth=all + fold every OTHER group. Only the clicked group's subtree expands; sibling groups stay collapsed. - "Hide this group" — switch to depth=default + add clicked group to urlFoldedSet. Group renders as a folded bubble; its subtree hidden. - "Expand subtree" — switch to depth=all + remove this group and all its descendant groups from urlFoldedSet. Fully unfolded subtree. - "Open in new tab" — unchanged (was working since #1435). Dropped the misleading "Fold to level N" item (was just stepDepth(-1)). The depth chip ◀▶ at the top-right is the canonical global depth control. Co-authored-by: e3mrah <1234567+e3mrah@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) --- .../ui/src/pages/sovereign/FlowPage.tsx | 98 ++++++++++++------- 1 file changed, 61 insertions(+), 37 deletions(-) diff --git a/products/catalyst/bootstrap/ui/src/pages/sovereign/FlowPage.tsx b/products/catalyst/bootstrap/ui/src/pages/sovereign/FlowPage.tsx index 1aaee889..9a9facc1 100644 --- a/products/catalyst/bootstrap/ui/src/pages/sovereign/FlowPage.tsx +++ b/products/catalyst/bootstrap/ui/src/pages/sovereign/FlowPage.tsx @@ -379,16 +379,16 @@ export function FlowPage({ const flowActions = useMemo( () => [ { - id: 'fold-subtree', - label: 'Fold subtree', + id: 'show-only-this', + label: 'Show only this group', }, { - id: 'fold-to-level', - label: 'Fold to level N', + id: 'hide-this', + label: 'Hide this group', }, { id: 'expand-all-under', - label: 'Expand all under here', + label: 'Expand subtree', }, { id: 'open-new-tab', @@ -401,46 +401,70 @@ export function FlowPage({ const handleNodeAction = useCallback( (nodeId: string, actionId: string) => { switch (actionId) { - case 'fold-subtree': { - if (adapter.groupIds.has(nodeId)) { - const next = new Set(urlFoldedSet) - next.add(nodeId) - setSearchPatch({ - folded: next.size > 0 ? [...next].join(',') : undefined, - }) + case 'show-only-this': { + // "Show only this group" — unfold the clicked group and fold + // every OTHER group on the canvas. Same composed-state pattern + // as toggleFold's unfold branch: switch to depth=all so the + // group itself stays addressable, then put every OTHER group + // into the explicit folded set. The operator gets exactly one + // expanded subtree with all sibling groups collapsed — + // matching the "expand only the respective parent" UX the + // operator asked for. + if (!adapter.groupIds.has(nodeId)) return + const otherGroups = new Set() + for (const gid of adapter.groupIds) { + if (gid !== nodeId) otherGroups.add(gid) } + const arr = [...otherGroups].filter(Boolean) + setSearchPatch({ + depth: 'all', + folded: arr.length > 0 ? arr.join(',') : undefined, + }) return } - case 'fold-to-level': { - // Folds to one level deeper than the clicked node's own depth. - // Best-effort: step the global chip up by one if there's room. - stepDepth(-1) + case 'hide-this': { + // "Hide this group" — collapse the clicked group regardless + // of current depth. Setting depth=2 puts other groups at the + // default elide state; adding the clicked group to the URL + // fold set keeps it visible as a folded bubble (its subtree + // hidden). Without depth=2 the FE's depth=all overrides would + // keep the rest of the canvas in fully-expanded mode. + if (!adapter.groupIds.has(nodeId)) return + const next = new Set(urlFoldedSet) + next.add(nodeId) + const arr = [...next].filter(Boolean) + setSearchPatch({ + depth: undefined, + folded: arr.length > 0 ? arr.join(',') : undefined, + }) return } case 'expand-all-under': { - if (adapter.groupIds.has(nodeId)) { - const next = new Set(urlFoldedSet) - next.delete(nodeId) - // Also remove any descendants of nodeId that were manually - // folded — best-effort using the live job graph. - const byId = new Map(adapter.jobs.map((j) => [j.id, j])) - const stack = [nodeId] - const seen = new Set() - while (stack.length > 0) { - const id = stack.pop()! - if (seen.has(id)) continue - seen.add(id) - const j = byId.get(id) - if (!j) continue - for (const c of j.childIds ?? []) { - next.delete(c) - stack.push(c) - } + // "Expand subtree" — fully unfold this group and any nested + // groups beneath it. Switch to depth=all + remove this group + // and all its descendant groups from the URL fold set. + if (!adapter.groupIds.has(nodeId)) return + const next = new Set(urlFoldedSet) + next.delete(nodeId) + const byId = new Map(adapter.jobs.map((j) => [j.id, j])) + const stack = [nodeId] + const seen = new Set() + while (stack.length > 0) { + const id = stack.pop()! + if (seen.has(id)) continue + seen.add(id) + const j = byId.get(id) + if (!j) continue + for (const c of j.childIds ?? []) { + next.delete(c) + stack.push(c) } - setSearchPatch({ - folded: next.size > 0 ? [...next].join(',') : undefined, - }) } + const arr = [...next].filter(Boolean) + setSearchPatch({ + depth: 'all', + folded: arr.length > 0 ? arr.join(',') : undefined, + }) return } case 'open-new-tab': {