[cueweb] Group management (Cuetopia group tree)#2404
Conversation
Signed-off-by: Michael Vallido <vallido.michael@gmail.com>
…ansion Signed-off-by: Michael Vallido <vallido.michael@gmail.com>
# Conflicts: # cueweb/app/utils/action_utils.ts # cueweb/app/utils/get_utils.ts
# Conflicts: # cueweb/app/utils/get_utils.ts
Drag-and-drop library backing the group tree's reparent interactions. Signed-off-by: Michael Vallido <vallido.michael@gmail.com>
Override the base tsconfig's jsx:preserve so .tsx tests compile under ts-jest. Tests only; production uses Next's compiler. Signed-off-by: Michael Vallido <vallido.michael@gmail.com>
Move getState into app/utils/job_state.ts (re-exported from jobs/columns) and add getJobStateDotColor; add formatGroupDefaults. Pure, unit-tested helpers the group-tree rows reuse without importing the JSX-heavy columns module. Signed-off-by: Michael Vallido <vallido.michael@gmail.com>
Tree view of show -> groups -> subgroups -> jobs at /shows/[showName], with drag to reparent jobs/groups (dnd-kit). Optimistic updates with rollback, serialized reparents to avoid parentage cycles, cycle-safe traversal, whole-region drop targets with highlight, rolled-up stats, memoized rows. The show page renders full-width with a Refresh button and a stable-chrome loading skeleton. Implements AcademySoftwareFoundation#2305. Signed-off-by: Michael Vallido <vallido.michael@gmail.com>
Full-width landing list at /shows of all shows (active first, then alphabetical) with a Refresh button, linking into each show's group tree. sortShows is pure and unit-tested; the page's load+refresh behavior is covered. Signed-off-by: Michael Vallido <vallido.michael@gmail.com>
# Conflicts: # cueweb/app/utils/action_utils.ts
A successful reparent's refetch (getShowGroups) returns [] when the request fails, since accessGetApi swallows errors. setGroups([]) then blanked the tree to "No groups in this show." despite the move succeeding. A show always returns its root group, so [] only means the refetch failed — guard the write to keep the optimistic state in both the group and job persist paths. Add group-tree.test.tsx covering the orchestrator (AcademySoftwareFoundation#2305 acceptance criteria): optimistic reparent + refetch, rollback on failure, the blank-out guard, and the URL expand-state round-trip — paths that had no tests. Remove the unused getSubgroups helper and /api/group/getgroups route; the tree loads the whole show at once via getShowGroups. Signed-off-by: Michael Vallido <vallido.michael@gmail.com>
📝 WalkthroughWalkthroughThis PR implements a hierarchical group tree component for CueWeb with drag-and-drop reparenting. It adds type definitions, API routes, tree-building logic, React components, show pages, and comprehensive tests supporting optimistic UI updates, rollback persistence, and URL-synced expansion state. ChangesGroup Tree Feature
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (2)
cueweb/app/utils/job_state.ts (1)
21-39: 💤 Low valueConsider removing optional chaining since
jobparameter is typed as non-nullable.The function signature accepts
job: Job, notjob: Job | null | undefined, but uses optional chaining (job?.state,job?.isPaused, etc.) throughout. If the type contract guarantees a non-nullJob, the optional chaining adds defensive overhead without benefit. Ifjobcan actually be null at runtime, update the signature tojob: Job | null | undefinedfor accuracy.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@cueweb/app/utils/job_state.ts` around lines 21 - 39, getState currently uses optional chaining on the non-nullable job parameter; remove the unnecessary "?" usages and access properties directly (use job.state, job.isPaused, job.jobStats.deadFrames, job.jobStats.dependFrames, job.jobStats.pendingFrames, job.jobStats.runningFrames) so the function matches its Job signature — if job can actually be null/undefined instead, update the signature to job: Job | null | undefined and add an explicit null-check at the top of getState.cueweb/app/utils/action_utils.ts (1)
219-235: 💤 Low valueConsider adding empty-array guards for consistency.
Functions like
addHostTags(line 284) andremoveHostTags(line 292) check for empty arrays and returnfalseearly to avoid unnecessary API calls. The reparent functions would benefit from the same guard.♻️ Suggested refactor
export async function reparentGroups(newParentId: string, groupIds: string[]) { + if (groupIds.length === 0) return false; const endpoint = "/api/group/action/reparentgroups"; const body = JSON.stringify({ group: { id: newParentId }, groups: { groups: groupIds.map(id => ({ id })) }, }); return performAction(endpoint, [body], `Reparented ${groupIds.length} group(s)`); } export async function reparentJobs(newParentId: string, jobIds: string[]) { + if (jobIds.length === 0) return false; const endpoint = "/api/group/action/reparentjobs"; const body = JSON.stringify({ group: { id: newParentId }, jobs: { jobs: jobIds.map(id => ({ id })) }, }); return performAction(endpoint, [body], `Reparented ${jobIds.length} job(s)`); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@cueweb/app/utils/action_utils.ts` around lines 219 - 235, The reparentGroups and reparentJobs functions make API calls even when given empty arrays; add the same empty-array guard used by addHostTags/removeHostTags so they return false immediately if groupIds or jobIds is empty to avoid unnecessary performAction calls. Modify reparentGroups (function name) to check if groupIds.length === 0 and return false, and similarly modify reparentJobs (function name) to check jobIds.length === 0 and return false before constructing the body and calling performAction.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@cueweb/app/api/show/getgroups/route.ts`:
- Around line 36-37: The handler is re-wrapping the upstream response and
embedding a non-existent responseData.status in the JSON body while losing the
original HTTP status, causing all errors to appear as 200; update the branches
that call NextResponse.json(...) (the checks around response.ok and the success
return) to stop relying on responseData.status and instead return the upstream
HTTP status via NextResponse.json(body, { status: response.status }), include
the actual error payload (responseData.error) in the body for failures, and
ensure any reference to responseData.status is removed; check usages around
NextResponse.json, response.ok, responseData and the handleRoute serialization
to make sure the real HTTP status is preserved.
In `@cueweb/app/shows/`[showName]/page.tsx:
- Line 34: The code is using React's use() hook (const { showName } =
use(params);) which requires React 19+, but the project is pinned to React 18;
replace the use(params) call by reading params directly (e.g., const { showName
} = params) or otherwise await/unwrap any promise before the component boundary
so you don't rely on React's use(); update the statement that destructures
showName from params and remove the use() import/usage.
In `@cueweb/app/utils/get_utils.ts`:
- Around line 301-306: The function findShowByName returns the raw accessGetApi
result which can be undefined, violating the declared Promise<Show | null>
contract; update findShowByName to call accessGetApi(ENDPOINT, body), check the
response and explicitly return null when undefined (or when the value is not a
valid Show), otherwise return the response cast/typed as Show so callers always
receive either a Show or null; reference the findShowByName function and
accessGetApi call when making this change.
In `@cueweb/components/group-tree/group-node.tsx`:
- Around line 162-188: The CollapsibleTrigger is wrapping a non-interactive div
which prevents keyboard users from focusing or toggling groups; update the
trigger child (the element using setRowRef and containing the folder/label) to
be keyboard-accessible by making it an interactive element (preferably a
<button>-like element) or adding tabIndex={0}, role="button" and an onKeyDown
handler that calls onToggle(node.group.id, nextState) for Enter/Space, and
ensure the existing onMouseEnter/onFocus still call
requestJobsFor(node.group.id) and that click/stopPropagation behavior (the
handleRef/grip button) remains unchanged so drag handle still functions.
---
Nitpick comments:
In `@cueweb/app/utils/action_utils.ts`:
- Around line 219-235: The reparentGroups and reparentJobs functions make API
calls even when given empty arrays; add the same empty-array guard used by
addHostTags/removeHostTags so they return false immediately if groupIds or
jobIds is empty to avoid unnecessary performAction calls. Modify reparentGroups
(function name) to check if groupIds.length === 0 and return false, and
similarly modify reparentJobs (function name) to check jobIds.length === 0 and
return false before constructing the body and calling performAction.
In `@cueweb/app/utils/job_state.ts`:
- Around line 21-39: getState currently uses optional chaining on the
non-nullable job parameter; remove the unnecessary "?" usages and access
properties directly (use job.state, job.isPaused, job.jobStats.deadFrames,
job.jobStats.dependFrames, job.jobStats.pendingFrames,
job.jobStats.runningFrames) so the function matches its Job signature — if job
can actually be null/undefined instead, update the signature to job: Job | null
| undefined and add an explicit null-check at the top of getState.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: d4f8e222-d03b-41d9-a9c0-e96f5ba86ded
⛔ Files ignored due to path filters (1)
cueweb/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (34)
cueweb/app/__tests__/api/utils/action_utils.test.tscueweb/app/__tests__/api/utils/get_utils.test.tscueweb/app/__tests__/api/utils/group_defaults.test.tscueweb/app/__tests__/api/utils/job_state.test.tscueweb/app/__tests__/components/group-tree/build-tree.test.tscueweb/app/__tests__/components/group-tree/dnd-helpers.test.tscueweb/app/__tests__/components/group-tree/expanded-param.test.tscueweb/app/__tests__/components/group-tree/group-tree.test.tsxcueweb/app/__tests__/components/group-tree/memoization.test.tsxcueweb/app/__tests__/shows/shows-page.test.tsxcueweb/app/__tests__/shows/sort-shows.test.tscueweb/app/api/group/action/reparentgroups/route.tscueweb/app/api/group/action/reparentjobs/route.tscueweb/app/api/group/getjobs/route.tscueweb/app/api/show/findshow/route.tscueweb/app/api/show/getgroups/route.tscueweb/app/jobs/columns.tsxcueweb/app/shows/[showName]/page.tsxcueweb/app/shows/page.tsxcueweb/app/shows/sort-shows.tscueweb/app/utils/action_utils.tscueweb/app/utils/get_utils.tscueweb/app/utils/group_defaults.tscueweb/app/utils/job_state.tscueweb/components/group-tree/build-tree.tscueweb/components/group-tree/dnd-helpers.tscueweb/components/group-tree/drag-preview.tsxcueweb/components/group-tree/expanded-param.tscueweb/components/group-tree/group-node.tsxcueweb/components/group-tree/group-tree-context.tsxcueweb/components/group-tree/group-tree.tsxcueweb/components/group-tree/job-leaf.tsxcueweb/jest.config.jscueweb/package.json
| if (!response.ok) return NextResponse.json({ error: responseData.error, status: response.status }); | ||
| return NextResponse.json({ data: responseData.data?.groups?.groups ?? [], status: responseData.status }); |
There was a problem hiding this comment.
Root cause: response re-wrapping drops real HTTP status and references a non-existent responseData.status.
All affected handlers convert upstream failures to HTTP 200 by omitting { status: response.status } in NextResponse.json(...). They also try to return responseData.status, but handleRoute only serializes { data } / { error }. This breaks API contracts and can cause optimistic reparent flows to treat failures as success.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@cueweb/app/api/show/getgroups/route.ts` around lines 36 - 37, The handler is
re-wrapping the upstream response and embedding a non-existent
responseData.status in the JSON body while losing the original HTTP status,
causing all errors to appear as 200; update the branches that call
NextResponse.json(...) (the checks around response.ok and the success return) to
stop relying on responseData.status and instead return the upstream HTTP status
via NextResponse.json(body, { status: response.status }), include the actual
error payload (responseData.error) in the body for failures, and ensure any
reference to responseData.status is removed; check usages around
NextResponse.json, response.ok, responseData and the handleRoute serialization
to make sure the real HTTP status is preserved.
| }: { | ||
| params: Promise<{ showName: string }>; | ||
| }) { | ||
| const { showName } = use(params); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check the React version in package.json to confirm compatibility with use() hook
cd cueweb
cat package.json | jq '.dependencies.react, .devDependencies.react'Repository: AcademySoftwareFoundation/OpenCue
Length of output: 88
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Inspect the page file around the use(params) line
cd /repo || cd .
# Ensure we're at repo root (the sandbox often starts there, but keep safe)
ls >/dev/null 2>&1 || cd .
FILE="cueweb/app/shows/[showName]/page.tsx"
echo "---- $FILE (lines 1-120) ----"
sed -n '1,120p' "$FILE" | nl -ba | sed -n '1,120p'
# 2) Check versions in cueweb/package.json (react/next)
echo "---- cueweb/package.json react/next ----"
cd cueweb
cat package.json | jq '{dependencies: .dependencies, devDependencies: .devDependencies, react: .dependencies.react, next: (.dependencies.next // .devDependencies.next // null)}'Repository: AcademySoftwareFoundation/OpenCue
Length of output: 256
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Inspect the page file around the use(params) line
FILE="cueweb/app/shows/[showName]/page.tsx"
echo "---- $FILE (lines 1-160) ----"
sed -n '1,160p' "$FILE" | nl -ba
# 2) Check versions in cueweb/package.json (react/next)
echo "---- cueweb/package.json (react/next) ----"
cd cueweb
cat package.json | jq '{dependencies: (.dependencies // {}), devDependencies: (.devDependencies // {}), react: (.dependencies.react // .devDependencies.react // null), next: (.dependencies.next // .devDependencies.next // null)}'Repository: AcademySoftwareFoundation/OpenCue
Length of output: 199
🌐 Web query:
When was the use() hook (for unwrapping promises) introduced in React, and is it available in React 18 (react package)?
💡 Result:
The use() hook was introduced in React 19 [1][2]. It is not available in the stable release of React 18 [1][3][2]. While React 18 supported various experimental features, the use API (used for reading resources like Promises or Context) is a built-in feature of the React 19 release [1][2]. If you are working in a React 18 environment and require this functionality, you must rely on community-provided shims or polyfills, such as react18-use, to achieve similar behavior [1][3]. Official documentation for use specifies that it requires React 19 or higher [2].
Citations:
- 1: https://reactuse.com/state/use/
- 2: https://facebook-react.mintlify.app/api/hooks/use
- 3: https://registry.npmjs.org/react18-use
React use() requires React 19+, but this app is pinned to React 18
cueweb/package.json declares react: "^18" (with no devDependencies.react). The use() hook for unwrapping promises/resources is a React 19+ feature, so cueweb/app/shows/[showName]/page.tsx’s const { showName } = use(params); would be incompatible and can cause a runtime error unless React is upgraded to 19 (or the code is refactored to avoid use()).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@cueweb/app/shows/`[showName]/page.tsx at line 34, The code is using React's
use() hook (const { showName } = use(params);) which requires React 19+, but the
project is pinned to React 18; replace the use(params) call by reading params
directly (e.g., const { showName } = params) or otherwise await/unwrap any
promise before the component boundary so you don't rely on React's use(); update
the statement that destructures showName from params and remove the use()
import/usage.
| export async function findShowByName(showName: string): Promise<Show | null> { | ||
| const ENDPOINT = "/api/show/findshow"; | ||
| const body = JSON.stringify({ name: showName }); | ||
| const response = await accessGetApi(ENDPOINT, body); | ||
| return response; | ||
| } |
There was a problem hiding this comment.
Normalize findShowByName to its declared Show | null contract.
Line 305 returns raw accessGetApi output; this can leak undefined instead of null and break strict contract expectations for callers.
Suggested fix
export async function findShowByName(showName: string): Promise<Show | null> {
const ENDPOINT = "/api/show/findshow";
const body = JSON.stringify({ name: showName });
const response = await accessGetApi(ENDPOINT, body);
- return response;
+ return response ?? null;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export async function findShowByName(showName: string): Promise<Show | null> { | |
| const ENDPOINT = "/api/show/findshow"; | |
| const body = JSON.stringify({ name: showName }); | |
| const response = await accessGetApi(ENDPOINT, body); | |
| return response; | |
| } | |
| export async function findShowByName(showName: string): Promise<Show | null> { | |
| const ENDPOINT = "/api/show/findshow"; | |
| const body = JSON.stringify({ name: showName }); | |
| const response = await accessGetApi(ENDPOINT, body); | |
| return response ?? null; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@cueweb/app/utils/get_utils.ts` around lines 301 - 306, The function
findShowByName returns the raw accessGetApi result which can be undefined,
violating the declared Promise<Show | null> contract; update findShowByName to
call accessGetApi(ENDPOINT, body), check the response and explicitly return null
when undefined (or when the value is not a valid Show), otherwise return the
response cast/typed as Show so callers always receive either a Show or null;
reference the findShowByName function and accessGetApi call when making this
change.
| <Collapsible open={isOpen} onOpenChange={(next) => onToggle(node.group.id, next)}> | ||
| <CollapsibleTrigger asChild> | ||
| <div | ||
| ref={setRowRef} | ||
| className="group flex items-center w-full cursor-pointer hover:bg-muted/50 transition-colors" | ||
| style={rowStyle} | ||
| title={defaultsSummary || undefined} | ||
| onMouseEnter={() => requestJobsFor(node.group.id)} | ||
| onFocus={() => requestJobsFor(node.group.id)} | ||
| > | ||
| <button | ||
| ref={handleRef} | ||
| type="button" | ||
| onClick={(e) => e.stopPropagation()} | ||
| aria-label={`Drag group ${node.group.name}`} | ||
| className="w-6 shrink-0 flex items-center justify-center py-1.5 cursor-grab text-muted-foreground hover:text-foreground" | ||
| > | ||
| <GripVertical className="h-4 w-4" /> | ||
| </button> | ||
| <div className="flex-1 flex items-center py-1.5 pr-3"> | ||
| <ChevronRight className="h-4 w-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-90" /> | ||
| <Folder className="h-4 w-4 shrink-0 ml-2 text-muted-foreground" /> | ||
| <span className="font-medium truncate ml-2">{node.group.name}</span> | ||
| {stats} | ||
| </div> | ||
| </div> | ||
| </CollapsibleTrigger> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show the current code around the referenced lines
echo "=== group-node.tsx (relevant lines) ==="
nl -ba cueweb/components/group-tree/group-node.tsx | sed -n '130,220p'
echo
echo "=== collapsible.tsx (trigger export/implementation) ==="
# Find the collapsible wrapper file
ls -la cueweb/components/ui | rg -n "collapsible" || true
nl -ba cueweb/components/ui/collapsible.tsx | sed -n '1,220p' || true
echo
echo "=== All occurrences of CollapsibleTrigger asChild in cueweb ==="
rg -n "CollapsibleTrigger\s+asChild" -S cueweb | head -n 200
echo
echo "=== Any occurrences where asChild wraps a button ==="
rg -n "CollapsibleTrigger\s+asChild[\s\S]{0,200}button" -S cueweb | head -n 50Repository: AcademySoftwareFoundation/OpenCue
Length of output: 178
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Print with line numbers using awk (since nl is unavailable)
echo "=== group-node.tsx (relevant lines) ==="
awk 'NR>=130 && NR<=220 {printf "%6d %s\n", NR, $0}' cueweb/components/group-tree/group-node.tsx
echo
echo "=== collapsible.tsx (trigger export/implementation) ==="
ls -la cueweb/components/ui | rg -n "collapsible" || true
awk 'NR<=260 {printf "%6d %s\n", NR, $0}' cueweb/components/ui/collapsible.tsx
echo
echo "=== All occurrences of CollapsibleTrigger asChild in cueweb ==="
rg -n "CollapsibleTrigger\\s+asChild" -S cueweb | head -n 200
echo
echo "=== Occurrences of CollapsibleTrigger asChild wrapping a button (within 200 chars) ==="
# This is a heuristic text search; it won't be perfect, but it's useful.
rg -n "CollapsibleTrigger\\s+asChild[\\s\\S]{0,200}(<button|<Button)" -S cueweb | head -n 50Repository: AcademySoftwareFoundation/OpenCue
Length of output: 4681
🌐 Web query:
Radix UI react-collapsible CollapsibleTrigger asChild accessibility role tabIndex onKeyDown props forwarded to child element when not button
💡 Result:
When you use the asChild prop on a Radix UI Collapsible.Trigger, the component merges its functionality, event handlers, and ARIA attributes onto the immediate child element instead of rendering a default button [1][2]. Key Considerations for Non-Button Elements: 1. Responsibility for Accessibility: Radix UI primitives are designed with native HTML elements (like button) in mind to ensure accessibility [3]. If you change the underlying element to something non-interactive (e.g., a div or span), Radix will still forward the necessary props (such as aria-expanded, aria-controls, and event listeners for onClick, onKeyDown, etc.), but it is your responsibility to ensure the element remains accessible [3][4]. 2. Keyboard and Focus Management: Non-interactive elements do not handle focus or keyboard events automatically [4]. If you use a non-button element, you must: - Manually set tabIndex={0} to make the element focusable [4]. - Ensure the element can handle keyboard interaction (e.g., triggering the action on Enter or Space), as Radix's event handlers may not be sufficient if the element does not naturally receive focus or respond to these inputs [4]. 3. Prop Forwarding: For this composition to work, the custom child component must: - Accept and spread all incoming props (including event handlers like onKeyDown and onClick) onto the underlying DOM element [3][5]. - Forward its ref correctly to allow Radix to manage focus and measurements [5][6]. In summary, while asChild allows you to use your own custom components or different DOM tags, switching away from a native requires you to manually replicate the missing interactive and accessible behaviors of a button [3][6][4]. Using a native is strongly recommended whenever possible [4].
Citations:
- 1: https://radix-ui-primitives.mintlify.app/concepts/composability
- 2: https://radix-ui-primitives.mintlify.app/api/primitive
- 3: https://www.radix-ui.com/primitives/docs/guides/composition
- 4: Dialog doesn't open on Trigger's keyboard action when asChild={true} radix-ui/primitives#1941
- 5: Using a custom component `asChild` of DropdownMenu.Trigger does not work radix-ui/primitives#1521
- 6: AccordionItemTriggers only triggers with an in-line reference radix-ui/primitives#1972
Make the expand/collapse trigger keyboard-accessible
CollapsibleTrigger asChild is currently using a non-interactive <div> as the trigger child (no tabIndex/role), so keyboard-only users may not be able to focus and expand/collapse the group.
Suggested fix
- <CollapsibleTrigger asChild>
- <div
- ref={setRowRef}
- className="group flex items-center w-full cursor-pointer hover:bg-muted/50 transition-colors"
- style={rowStyle}
- title={defaultsSummary || undefined}
- onMouseEnter={() => requestJobsFor(node.group.id)}
- onFocus={() => requestJobsFor(node.group.id)}
- >
+ <div
+ ref={setRowRef}
+ className="group flex items-center w-full hover:bg-muted/50 transition-colors"
+ style={rowStyle}
+ title={defaultsSummary || undefined}
+ onMouseEnter={() => requestJobsFor(node.group.id)}
+ >
<button
ref={handleRef}
type="button"
onClick={(e) => e.stopPropagation()}
aria-label={`Drag group ${node.group.name}`}
className="w-6 shrink-0 flex items-center justify-center py-1.5 cursor-grab text-muted-foreground hover:text-foreground"
>
<GripVertical className="h-4 w-4" />
</button>
- <div className="flex-1 flex items-center py-1.5 pr-3">
+ <CollapsibleTrigger asChild>
+ <button
+ type="button"
+ className="flex-1 flex items-center py-1.5 pr-3 text-left"
+ onFocus={() => requestJobsFor(node.group.id)}
+ >
<ChevronRight className="h-4 w-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-90" />
<Folder className="h-4 w-4 shrink-0 ml-2 text-muted-foreground" />
<span className="font-medium truncate ml-2">{node.group.name}</span>
{stats}
- </div>
- </div>
- </CollapsibleTrigger>
+ </button>
+ </CollapsibleTrigger>
+ </div>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <Collapsible open={isOpen} onOpenChange={(next) => onToggle(node.group.id, next)}> | |
| <CollapsibleTrigger asChild> | |
| <div | |
| ref={setRowRef} | |
| className="group flex items-center w-full cursor-pointer hover:bg-muted/50 transition-colors" | |
| style={rowStyle} | |
| title={defaultsSummary || undefined} | |
| onMouseEnter={() => requestJobsFor(node.group.id)} | |
| onFocus={() => requestJobsFor(node.group.id)} | |
| > | |
| <button | |
| ref={handleRef} | |
| type="button" | |
| onClick={(e) => e.stopPropagation()} | |
| aria-label={`Drag group ${node.group.name}`} | |
| className="w-6 shrink-0 flex items-center justify-center py-1.5 cursor-grab text-muted-foreground hover:text-foreground" | |
| > | |
| <GripVertical className="h-4 w-4" /> | |
| </button> | |
| <div className="flex-1 flex items-center py-1.5 pr-3"> | |
| <ChevronRight className="h-4 w-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-90" /> | |
| <Folder className="h-4 w-4 shrink-0 ml-2 text-muted-foreground" /> | |
| <span className="font-medium truncate ml-2">{node.group.name}</span> | |
| {stats} | |
| </div> | |
| </div> | |
| </CollapsibleTrigger> | |
| <Collapsible open={isOpen} onOpenChange={(next) => onToggle(node.group.id, next)}> | |
| <div | |
| ref={setRowRef} | |
| className="group flex items-center w-full hover:bg-muted/50 transition-colors" | |
| style={rowStyle} | |
| title={defaultsSummary || undefined} | |
| onMouseEnter={() => requestJobsFor(node.group.id)} | |
| > | |
| <button | |
| ref={handleRef} | |
| type="button" | |
| onClick={(e) => e.stopPropagation()} | |
| aria-label={`Drag group ${node.group.name}`} | |
| className="w-6 shrink-0 flex items-center justify-center py-1.5 cursor-grab text-muted-foreground hover:text-foreground" | |
| > | |
| <GripVertical className="h-4 w-4" /> | |
| </button> | |
| <CollapsibleTrigger asChild> | |
| <button | |
| type="button" | |
| className="flex-1 flex items-center py-1.5 pr-3 text-left" | |
| onFocus={() => requestJobsFor(node.group.id)} | |
| > | |
| <ChevronRight className="h-4 w-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-90" /> | |
| <Folder className="h-4 w-4 shrink-0 ml-2 text-muted-foreground" /> | |
| <span className="font-medium truncate ml-2">{node.group.name}</span> | |
| {stats} | |
| </button> | |
| </CollapsibleTrigger> | |
| </div> | |
| </Collapsible> |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@cueweb/components/group-tree/group-node.tsx` around lines 162 - 188, The
CollapsibleTrigger is wrapping a non-interactive div which prevents keyboard
users from focusing or toggling groups; update the trigger child (the element
using setRowRef and containing the folder/label) to be keyboard-accessible by
making it an interactive element (preferably a <button>-like element) or adding
tabIndex={0}, role="button" and an onKeyDown handler that calls
onToggle(node.group.id, nextState) for Enter/Space, and ensure the existing
onMouseEnter/onFocus still call requestJobsFor(node.group.id) and that
click/stopPropagation behavior (the handleRef/grip button) remains unchanged so
drag handle still functions.
|
Hi @mvallido, When you have a chance, please resolve the merge conflicts and rebase/merge the latest Once everything is ready for review, please change the PR status from Draft to Open, and I'll review it as soon as possible. Thanks! |
Related Issues
Fixes #2305
Summary
Adds a tree view of show → groups → subgroups → jobs with drag-to-reparent.
/shows) listing every show, sorted, with a Refresh control./shows/[showName]) rendering the full group hierarchy down to jobs,with rolled-up stats (running / pending / dead) per group.
@dnd-kit/react): drag a group or job onto a target group tomove it. Updates apply optimistically, roll back if the backend rejects them, and retain the
optimistic result if a post-success refetch fails — the tree is never left blank.
Mirrors the reparent semantics of CueGUI's
GroupDialog.py/CueJobMonitorTree.py.Acceptance criteria
Screenshots
Shows page
Drag-and-drop reparent
How it works
buildTreeFromGroupsassembles the flat group list into a tree and computes rolled-up stats. Orphaned/unreachable groups are dropped.reparentgroups/reparentjobs,getgroups/getjobs,findshow).Testing
npx tsc --noEmit -p .— clean.npx jest— 21 suites / 171 tests passing.serialization guard, the blank-out-on-refetch-failure guard, URL expand-state round-trip
(init + write), row memoization, and the API wrappers.
Manual: created groups/subgroups/jobs in a dev show, dragged jobs and groups between groups, confirmed moves persist after refresh, expand state restores from the URL, and invalid drops (cycles / self) are rejected.
Notes
job_state,group_defaults).LLM usage disclosure
Assisted-by: Claude / Opus 4.7
Used for implementation planning, initial code drafting, and writing unit tests.
Summary by CodeRabbit
Release Notes
New Features
Bug Fixes
Dependencies