Skip to content

[cueweb] Group management (Cuetopia group tree)#2404

Draft
mvallido wants to merge 11 commits into
AcademySoftwareFoundation:masterfrom
mvallido:cueweb/group-tree
Draft

[cueweb] Group management (Cuetopia group tree)#2404
mvallido wants to merge 11 commits into
AcademySoftwareFoundation:masterfrom
mvallido:cueweb/group-tree

Conversation

@mvallido

@mvallido mvallido commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Related Issues

Fixes #2305

Summary

Adds a tree view of show → groups → subgroups → jobs with drag-to-reparent.

  • A Shows index (/shows) listing every show, sorted, with a Refresh control.
  • A per-show group tree (/shows/[showName]) rendering the full group hierarchy down to jobs,
    with rolled-up stats (running / pending / dead) per group.
  • Drag-and-drop reparenting (via @dnd-kit/react): drag a group or job onto a target group to
    move 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.
  • Expand/collapse state lives in the URL, so a tree view is shareable and survives reload.

Mirrors the reparent semantics of CueGUI's GroupDialog.py / CueJobMonitorTree.py.

Acceptance criteria

  • Reparenting a group or job persists to Cuebot.
  • Tree expand state is reflected in the URL.

Screenshots

Shows page

shows_index

Drag-and-drop reparent

drag_and_drop

How it works

  • buildTreeFromGroups assembles the flat group list into a tree and computes rolled-up stats. Orphaned/unreachable groups are dropped.
  • Reparenting is guarded against invalid moves like self-drop, no-op (drop on current parent), and cycles (moving a group under its own descendant), and serializes in-flight reparents to avoid corrupting the tree. The client checks are a UX guard.
  • New Next.js API routes + typed wrappers wrap the Cuebot REST gateway (reparentgroups/reparentjobs, getgroups/getjobs, findshow).

Testing

  • npx tsc --noEmit -p . — clean.
  • npx jest21 suites / 171 tests passing.
  • Coverage includes tree-building, cycle prevention, optimistic-reparent + rollback, the in-flight
    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

  • Scoped to the issue's intent; no changes to existing pages beyond the shared utils that were extracted for reuse (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

    • Added Shows page with browsable list of shows, sorted by active status then alphabetically
    • Added Show detail page with interactive group tree hierarchy supporting drag-and-drop reparenting
    • Added Show search/lookup functionality by name
    • Added collapsible group tree with URL-synced expansion state
    • Added drag-and-drop support for moving groups and jobs to different parents
  • Bug Fixes

    • Improved memoization in group tree components to prevent unnecessary re-renders
  • Dependencies

    • Added drag-and-drop library support

mvallido added 11 commits May 21, 2026 14:33
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
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>
@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This 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.

Changes

Group Tree Feature

Layer / File(s) Summary
Data types and utility helpers
cueweb/app/utils/get_utils.ts, cueweb/app/utils/job_state.ts, cueweb/app/utils/group_defaults.ts, cueweb/app/shows/sort-shows.ts, cueweb/app/jobs/columns.tsx
New TypeScript types (Group, GroupStats), pure functions for deriving job state display strings and Tailwind colors, formatting group defaults (department/priority/cores), and case-insensitive active-first show sorting; columns.tsx refactored to import getState instead of defining it locally.
API route handlers
cueweb/app/api/show/getgroups/route.ts, cueweb/app/api/show/findshow/route.ts, cueweb/app/api/group/getjobs/route.ts, cueweb/app/api/group/action/reparent*.../route.ts
POST endpoints validating and forwarding requests to Cuebot backends: show group/job queries return typed data arrays, show lookup returns single show or null, group/job reparent endpoints persist changes and return status/error responses.
Tree building and drag-drop logic
cueweb/components/group-tree/build-tree.ts, cueweb/components/group-tree/dnd-helpers.ts, cueweb/components/group-tree/expanded-param.ts
Tree construction from flat group lists with rollup stat aggregation; reparent validation (cycles, descendants, self-moves); immutable state updates for group/job moves; URL param serialization for expanded group IDs.
React components
cueweb/components/group-tree/group-tree-context.tsx, cueweb/components/group-tree/group-node.tsx, cueweb/components/group-tree/job-leaf.tsx, cueweb/components/group-tree/drag-preview.tsx
Context provider for tree state, memoized collapsible group nodes with lazy job loading and drag-drop wiring, memoized draggable job rows with state dots and progress bars, drag-overlay icon/name previews.
GroupTree orchestrator
cueweb/components/group-tree/group-tree.tsx
Client component fetching show groups, syncing expanded state to URL, lazy-loading jobs per group, computing drop validity, applying optimistic local updates on drag end, deferring persistence with rollback on failure, and refetching groups on success.
Show pages
cueweb/app/shows/page.tsx, cueweb/app/shows/[showName]/page.tsx
Shows list page fetching and sorting shows with active/inactive badges and counts; individual show page resolving show name, rendering not-found/loading/show states, and mounting GroupTree with reload-nonce forcing re-initialization.
Action helpers, config, and initial tests
cueweb/app/utils/action_utils.ts, cueweb/jest.config.js, cueweb/package.json, cueweb/app/__tests__/api/utils/action_utils.test.ts
New reparent action helpers calling API endpoints and showing success toasts; Jest config updated to broaden @/ alias resolution and use react-jsx transform; @dnd-kit/react added to dependencies; test suite for action utilities.
Comprehensive test coverage
cueweb/app/__tests__/api/utils/, cueweb/app/__tests__/components/group-tree/, cueweb/app/__tests__/shows/
Unit tests for get/action utilities, group defaults, job state, show sorting; tree building and DnD helper validation; expanded-param serialization; GroupTree integration (reparent success/failure/rollback/refetch); component memoization; ShowsPage load/refresh flow.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • DiegoTavares
  • lithorus

Poem

🐰 A tree of groups now springs to life,
With branches dragged and dropped with delight,
Expand each node, load jobs in flight,
Rollback on fail—state kept just right!
URL-synced expansion shines so bright. 🌳✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 43.75% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main addition: group management with a tree UI (Cuetopia group tree), which is the primary feature delivered in this PR.
Linked Issues check ✅ Passed All requirements from #2305 are met: tree view implemented (show → groups → subgroups → jobs), drag-to-reparent with persistence, expand state persisted in URL, dnd-kit integration, and tests validating key behaviors.
Out of Scope Changes check ✅ Passed All changes are scoped to the linked issue #2305. Extracted utilities (job_state, group_defaults) are shared helpers supporting the feature. No unrelated refactoring or feature scope creep detected.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@mvallido mvallido marked this pull request as ready for review June 10, 2026 07:12

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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 (2)
cueweb/app/utils/job_state.ts (1)

21-39: 💤 Low value

Consider removing optional chaining since job parameter is typed as non-nullable.

The function signature accepts job: Job, not job: Job | null | undefined, but uses optional chaining (job?.state, job?.isPaused, etc.) throughout. If the type contract guarantees a non-null Job, the optional chaining adds defensive overhead without benefit. If job can actually be null at runtime, update the signature to job: Job | null | undefined for 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 value

Consider adding empty-array guards for consistency.

Functions like addHostTags (line 284) and removeHostTags (line 292) check for empty arrays and return false early 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

📥 Commits

Reviewing files that changed from the base of the PR and between e504bc9 and e9dcd72.

⛔ Files ignored due to path filters (1)
  • cueweb/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (34)
  • cueweb/app/__tests__/api/utils/action_utils.test.ts
  • cueweb/app/__tests__/api/utils/get_utils.test.ts
  • cueweb/app/__tests__/api/utils/group_defaults.test.ts
  • cueweb/app/__tests__/api/utils/job_state.test.ts
  • cueweb/app/__tests__/components/group-tree/build-tree.test.ts
  • cueweb/app/__tests__/components/group-tree/dnd-helpers.test.ts
  • cueweb/app/__tests__/components/group-tree/expanded-param.test.ts
  • cueweb/app/__tests__/components/group-tree/group-tree.test.tsx
  • cueweb/app/__tests__/components/group-tree/memoization.test.tsx
  • cueweb/app/__tests__/shows/shows-page.test.tsx
  • cueweb/app/__tests__/shows/sort-shows.test.ts
  • cueweb/app/api/group/action/reparentgroups/route.ts
  • cueweb/app/api/group/action/reparentjobs/route.ts
  • cueweb/app/api/group/getjobs/route.ts
  • cueweb/app/api/show/findshow/route.ts
  • cueweb/app/api/show/getgroups/route.ts
  • cueweb/app/jobs/columns.tsx
  • cueweb/app/shows/[showName]/page.tsx
  • cueweb/app/shows/page.tsx
  • cueweb/app/shows/sort-shows.ts
  • cueweb/app/utils/action_utils.ts
  • cueweb/app/utils/get_utils.ts
  • cueweb/app/utils/group_defaults.ts
  • cueweb/app/utils/job_state.ts
  • cueweb/components/group-tree/build-tree.ts
  • cueweb/components/group-tree/dnd-helpers.ts
  • cueweb/components/group-tree/drag-preview.tsx
  • cueweb/components/group-tree/expanded-param.ts
  • cueweb/components/group-tree/group-node.tsx
  • cueweb/components/group-tree/group-tree-context.tsx
  • cueweb/components/group-tree/group-tree.tsx
  • cueweb/components/group-tree/job-leaf.tsx
  • cueweb/jest.config.js
  • cueweb/package.json

Comment on lines +36 to +37
if (!response.ok) return NextResponse.json({ error: responseData.error, status: response.status });
return NextResponse.json({ data: responseData.data?.groups?.groups ?? [], status: responseData.status });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 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:


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.

Comment on lines +301 to +306
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;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Suggested change
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.

Comment on lines +162 to +188
<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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 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 50

Repository: 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 50

Repository: 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:


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.

Suggested 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>
<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.

@ramonfigueiredo

Copy link
Copy Markdown
Collaborator

Hi @mvallido,

When you have a chance, please resolve the merge conflicts and rebase/merge the latest master branch into your PR.

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!

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[cueweb] Show, Subscription, Service, Limits: Group management (Cuetopia group tree)

2 participants