Files
7f138d3bf5 [e4ed92d6] Video pipeline per-project requests, re-render action, composition preview (#403)
* [7f2c881a] Project-scope video pipeline + re-render + preview proxy (#386) (#396)

* [7f2c881a] feat(video): scope on-demand video requests + render loop to project_id

Require project_id on VideoRequestBody (404 when unresolvable or not
opted into the video engine), thread it through VideoEngine.open_video_task
via a shared resolve_authoring_project helper, and resolve the render
loop's motion/ workspace from the authoring task's own project_id instead
of the hardcoded self_heal_project_slug.

* [7f2c881a] fix(video): cast task.id to UUID before VideoEngine.rerender calls

mypy flagged task.id as sqlalchemy.sql.sqltypes.UUID[Any] rather than
uuid.UUID in the three rerender tests; cast to UUID per the codebase's
established idiom (cast("UUID", obj.id)) used elsewhere for the same
SQLAlchemy Mapped-attribute inference gap.

* [7f2c881a] docs(video): API endpoints for project-scoped requests, re-render, and preview proxy

Add comprehensive API documentation for the new project-scoped video engine endpoints:
- POST /api/video/request: on-demand video authoring scoped to project_id (breaking change)
- POST /api/video/pipeline/{task_id}/rerender: CEO-triggered re-render with idempotency key clearing
- GET /api/video/preview/{task_id}/{file_path}: CEO preview proxy with path-traversal confinement

Document project-scoping architecture: authoring tasks and render loop now resolve from task's own project_id instead of hardcoded self_heal_project_slug.

Add migration guide covering breaking change to VideoRequestBody schema (project_id now required), error handling changes (404 on unresolvable/non-opted-in projects), and client migration steps.

---------

Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech>
Co-authored-by: Backend Documenter <be-doc@roboco.tech>

* [8f959c3b] docs(ux_ui): add project picker, re-render control, and composition preview panel spec (#381) (#398)

Co-authored-by: UX/UI Developer 1 <ux-dev-1@roboco.tech>

* [1fb5b5cb] Project picker, re-render button, and composition preview panel (#397) (#402)

* [1fb5b5cb] feat(video): project picker, re-render button, and composition preview panel

* [1fb5b5cb] docs(video): add comprehensive guide for project picker, re-render button, and composition preview panel

---------

Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech>
Co-authored-by: Frontend Documenter <fe-doc@roboco.tech>

* [a512f364] Add video_engine_enabled to ProjectSummaryResponse (#412) (#414)

* [a512f364] feat(api): surface video_engine_enabled on ProjectSummaryResponse

* [a512f364] docs(api): document video_engine_enabled on ProjectSummaryResponse

---------

Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech>
Co-authored-by: Backend Documenter <be-doc@roboco.tech>

* [03607ab9] Fix re-render control gating/placement and project picker filter (#434)

* [f2f3e89f] Fix RerenderControl gating/placement across queue and strip views (#431)

* [f2f3e89f] feat(video): widen RerenderControl gating and share it across queue/strip views

Extracts RerenderControl into a shared panel/src/components/dashboard/
video-rerender-control.tsx component, widens its gate from
render_status === 'failed' to source_task_id + composition_id present
(matching what the backend rerender endpoint actually requires), adds a
confirm dialog before firing the mutation, and wires the same component
into video-pipeline-strip.tsx for still-in-flight rendering/render_failed
rows.

* [f2f3e89f] docs(video): enhance RerenderControl JSDoc with gating logic and usage examples

Add comprehensive JSDoc to the RerenderControl component covering its
purpose, gating logic (render for any source_task_id + composition_id,
regardless of render_status), three visual button states (idle/loading/
error), confirm-dialog guard behavior, and usage examples for both
video-post-queue.tsx and video-pipeline-strip.tsx contexts. Explains
why the backend's rerender endpoint doesn't require a failed render and
how the component prevents accidental re-renders.

---------

Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech>
Co-authored-by: Frontend Documenter <fe-doc@roboco.tech>

* [404d8ed3] Filter project picker to video-engine-enabled projects (#432)

* [404d8ed3] feat(panel): filter video-request project picker to opted-in projects

Add video_engine_enabled to the client ProjectSummary type, give
ProjectSelector a videoEngineOnly filter prop, default RequestVideoDialog's
picker to the current video-enabled project with a friendly empty-state
when none exist, and cover the filter with a new project-selector test.

* [404d8ed3] docs(panel): add ProjectSelector component API reference with videoEngineOnly filter

Document the reusable ProjectSelector component with its props, filtering behavior,
and new videoEngineOnly filter for video-engine-enabled projects. Follows the
existing component documentation pattern from page-refresh-provider.

---------

Co-authored-by: Frontend Developer 2 <fe-dev-2@roboco.tech>
Co-authored-by: Frontend Documenter <fe-doc@roboco.tech>

* [519a4088] fix(panel): import missing RerenderControl in video-post-queue and correct stale doc (#436)

Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech>

---------

Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech>
Co-authored-by: Frontend Documenter <fe-doc@roboco.tech>
Co-authored-by: Frontend Developer 2 <fe-dev-2@roboco.tech>

* [8e912c3e] Reflow hard-wrapped video UX design doc to pass quality gate (#439)

* [ccfe2015] docs(ux_ui): reflow video request composition-controls spec to one line per paragraph (#438)

Co-authored-by: UX/UI Developer 1 <ux-dev-1@roboco.tech>

* [99c3ed9c] docs(backend): reflow hard-wrapped prose in video-engine-endpoints.md and video-project-scoping.md (#442)

Co-authored-by: UX/UI Developer 1 <ux-dev-1@roboco.tech>

* [c2e98fc0] docs(backend): strip stray trailing whitespace in video-engine-endpoints.md fence (#451)

Co-authored-by: UX/UI Developer 1 <ux-dev-1@roboco.tech>

* [9c7bc11a] Reflow all 3 hard-wrapped docs on this branch and verify quality gate (#457)

* [9c7bc11a] test(scripts): guard reflow_md.py --check wiring into make quality

* [9c7bc11a] docs(standards): document markdown reflow quality gate workflow and verification

Added comprehensive guide explaining the one-logical-unit-per-line markdown prose standard, how the reflow check integrates into make quality, the three reflowed files (video-engine-endpoints.md, video-project-scoping.md, composition-controls spec), and the regression test added to ensure wiring stability. This task verifies all three ACs are satisfied: reflow_md.py --check exits 0, make quality passes (non-DB portions), and the three files are whitespace-only reflowed.

---------

Co-authored-by: UX/UI Developer 1 <ux-dev-1@roboco.tech>
Co-authored-by: UX/UI Documenter <ux-doc@roboco.tech>

---------

Co-authored-by: UX/UI Developer 1 <ux-dev-1@roboco.tech>
Co-authored-by: UX/UI Documenter <ux-doc@roboco.tech>

* [002f0cdd] docs(rag): document reflow-check zero-diff troubleshooting path (#459) (#460)

Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech>

* [e4ed92d6] fix rerender missing-task test — assert the empty queue it creates

The test never seeds; the trailing assertion expected a phantom video
post. Broken since the branch's first commit but never executed — every
earlier CI run short-circuited at a pre-pytest gate step.

---------

Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech>
Co-authored-by: Backend Documenter <be-doc@roboco.tech>
Co-authored-by: UX/UI Developer 1 <ux-dev-1@roboco.tech>
Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech>
Co-authored-by: Frontend Documenter <fe-doc@roboco.tech>
Co-authored-by: Frontend Developer 2 <fe-dev-2@roboco.tech>
Co-authored-by: UX/UI Documenter <ux-doc@roboco.tech>
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-11 07:39:17 +02:00

7.5 KiB

ProjectSelector component

A reusable dropdown selector for picking a project, with optional filtering by team and video-engine opt-in status.

Purpose

Panels and dialogs that need to let users pick a project use ProjectSelector to display and filter the list. The component fetches the project list via the useProjects hook, groups projects by their assigned team, and optionally filters by team membership or video-engine enablement.

Files

File Role
panel/src/components/projects/project-selector.tsx Main component and ProjectSelectorProps type.
panel/src/components/projects/__tests__/project-selector.test.tsx Component tests (filter, grouping).
panel/src/lib/api/projects.ts useProjects hook and mock-mode project data.
panel/src/types/index.ts ProjectSummary type (includes video_engine_enabled).

API

ProjectSelectorProps

interface ProjectSelectorProps {
  // The currently selected project ID (null for unselected)
  value: string | null;
  // Called when the user selects a project
  onChange: (projectId: string | null) => void;
  // Placeholder text shown when no project is selected
  placeholder?: string;
  // Filter to projects assigned to a specific team (optional)
  filterByTeam?: Team;
  // Disable the selector (optional, default: false)
  disabled?: boolean;
  // Whether to clear the selection when the user deselects (optional, default: true)
  allowClear?: boolean;
  // Restrict the list to projects with video_engine_enabled = true (optional, default: false)
  videoEngineOnly?: boolean;
}

Props explained:

  • value — the currently selected project's ID, or null if unselected. The selector updates this via onChange.
  • onChange — callback fired when the user selects a project (receives the project ID) or clears the selection (receives null if allowClear is true).
  • placeholder — text shown in the trigger button when value is null. Defaults to "Select a project...".
  • filterByTeam — if set, only show projects assigned to this team (e.g. Team.FRONTEND). Useful when an operation is scoped to a single team.
  • disabled — if true, the dropdown is unclickable and grayed out.
  • allowClear — if true (default), the user can click a clear button to set value to null and trigger onChange(null). If false, a selection is mandatory.
  • videoEngineOnly — if true, filter the project list to only projects with video_engine_enabled === true. Used by video-authoring flows (e.g. RequestVideoDialog) to show only opted-in projects.

ProjectSummary

The shape of each project in the list:

interface ProjectSummary {
  id: string;
  name: string;
  slug: string;
  git_url: string;
  assigned_cell: Team;
  is_active: boolean;
  has_workspace: boolean;
  has_git_token: boolean;
  video_engine_enabled: boolean;
}
  • video_engine_enabled — whether the project has opted into the video engine. Used by the videoEngineOnly filter.

How to use

Basic usage

Pick any project in the list:

"use client";

import { useState } from "react";
import { ProjectSelector } from "@/components/projects/project-selector";

export default function MyComponent() {
  const [projectId, setProjectId] = useState<string | null>(null);

  return (
    <div>
      <ProjectSelector
        value={projectId}
        onChange={setProjectId}
        placeholder="Select a project..."
      />
      {projectId && <p>You picked: {projectId}</p>}
    </div>
  );
}

Filter by team

Show only projects in a specific team:

import { Team } from "@/types";

<ProjectSelector
  value={projectId}
  onChange={setProjectId}
  filterByTeam={Team.FRONTEND}
  placeholder="Select a frontend project..."
/>

Filter by video-engine opt-in

Show only projects that have opted into the video engine (for video-authoring dialogs):

<ProjectSelector
  value={projectId}
  onChange={setProjectId}
  videoEngineOnly
  placeholder="Select a project with video enabled..."
/>

Mandatory selection

Disable the clear button so the user must pick a project:

<ProjectSelector
  value={projectId}
  onChange={setProjectId}
  allowClear={false}
  placeholder="Project (required)"
/>

Combine filters

Both team and video-engine filters can be used together:

<ProjectSelector
  value={projectId}
  onChange={setProjectId}
  filterByTeam={Team.BACKEND}
  videoEngineOnly
  placeholder="Backend projects with video enabled..."
/>

Filtering order

Filters are applied in this order:

  1. Video-engine filter (if videoEngineOnly is true) — keep only projects with video_engine_enabled === true.
  2. Team filter (if filterByTeam is set) — keep only projects assigned to the given team.
  3. Grouping — group the remaining projects by their assigned team for display.

This ensures a video-engine-filtered list still groups correctly, and a team-filtered list can still have the video-engine filter applied on top.

Empty states

The component handles empty states gracefully:

  • No projects in the data source — the dropdown shows the placeholder and is disabled.
  • All projects filtered out — the dropdown shows the placeholder and is disabled. This happens when all available projects are filtered by videoEngineOnly or filterByTeam.
  • No projects with video enabled (when videoEngineOnly is true) — a calling dialog (like RequestVideoDialog) should check videoProjects.length and show a friendly empty-state message to the user.

Loading state

The selector uses the useProjects hook, which may return isLoading: true while fetching the project list. The selector passes this through as a disabled state until the data arrives.

Design decisions

  • Radix UI Select: built on Radix's <Select> primitive for accessibility (keyboard navigation, ARIA labels).
  • TanStack Query caching: projects are fetched once via useProjects and cached, so multiple selectors on the same page share the same query.
  • Usable without TypeScript: the component type-checks all inputs but can be used from plain JS contexts (e.g. test fixtures) by omitting types.
  • Memoized grouping: the grouping computation is memoized with a dependency array so re-renders don't re-compute groups unless the filter props change.

Testing

Run the selector tests with:

cd panel
pnpm test project-selector

Covered behaviors:

  • videoEngineOnly filters out projects with video_engine_enabled === false.
  • Without videoEngineOnly, all projects appear in the list.
  • Team filtering works independently and in combination with video-engine filtering.
  • Selecting a project calls onChange with the project ID.
  • Clearing a selection (when allowClear is true) calls onChange(null).
  • The selector is disabled when disabled prop is true or while data is loading.
  • The selector is disabled when all projects are filtered out.
  • RequestVideoDialog (panel/src/components/dashboard/video-post-queue.tsx) — uses videoEngineOnly to show only projects that have opted into the video engine for on-demand video requests.
  • ProjectsPage (app/(dashboard)/projects/page.tsx) — uses the base selector without filters to list and manage all projects.

Migration / rollout

No breaking changes. The new videoEngineOnly prop defaults to false, so existing uses of ProjectSelector are unaffected. New code that needs to filter by video-engine opt-in status should pass videoEngineOnly={true}.