From 9b247e4f09305a278887aa1b21c7b87743ca843e Mon Sep 17 00:00:00 2001 From: "[._.]/ Adam Eivy" Date: Wed, 9 Sep 2026 14:51:06 +0000 Subject: [PATCH 1/2] feat: make creative director projects searchable and prioritize unfinished work --- .../creative-director/ProjectLibrary.jsx | 74 +++++++++++++++++++ client/src/pages/CreativeDirector.jsx | 20 +++-- client/src/pages/CreativeDirector.test.jsx | 52 +++++++++++++ 3 files changed, 139 insertions(+), 7 deletions(-) create mode 100644 client/src/components/creative-director/ProjectLibrary.jsx diff --git a/client/src/components/creative-director/ProjectLibrary.jsx b/client/src/components/creative-director/ProjectLibrary.jsx new file mode 100644 index 0000000000..38b1233ee1 --- /dev/null +++ b/client/src/components/creative-director/ProjectLibrary.jsx @@ -0,0 +1,74 @@ +import { useMemo } from 'react'; +import useUrlParams from '../../hooks/useUrlParams.js'; +import { matchHaystack, tokenizeQuery } from '../../lib/mediaSearch.js'; + +const SORTS = { unfinished: 'Unfinished first', newest: 'Newest created', updated: 'Recently updated', name: 'Name A–Z' }; + +export default function ProjectLibrary({ projects, children }) { + const [params, updateParams] = useUrlParams(); + const query = params.get('q') || ''; + const statuses = useMemo(() => [...new Set(projects.map(p => p.status).filter(Boolean))].sort(), [projects]); + const requestedStatus = params.get('status'); + const status = requestedStatus === 'unfinished' || statuses.includes(requestedStatus) ? requestedStatus : 'all'; + const sort = Object.hasOwn(SORTS, params.get('sort')) ? params.get('sort') : 'unfinished'; + const compact = params.get('layout') === 'list'; + const visible = useMemo(() => { + const tokens = tokenizeQuery(query); + return projects.filter(p => ( + (status === 'all' || (status === 'unfinished' ? p.status !== 'complete' : p.status === status)) && + matchHaystack([p.name, p.id, p.status, p.modelId, p.directive?.goal, p.userStory, p.styleSpec].filter(Boolean).join(' ').toLowerCase(), tokens) + )).sort((a, b) => { + if (sort === 'unfinished') { + const completed = Number(a.status === 'complete') - Number(b.status === 'complete'); + if (completed) return completed; + } + if (sort !== 'name') { + const aTime = (sort === 'updated' && Date.parse(a.updatedAt)) || Date.parse(a.createdAt) || 0; + const bTime = (sort === 'updated' && Date.parse(b.updatedAt)) || Date.parse(b.createdAt) || 0; + if (aTime !== bTime) return bTime - aTime; + } + return (a.name || '').localeCompare(b.name || '') || a.id.localeCompare(b.id); + }); + }, [projects, query, status, sort]); + const controlClass = 'w-full bg-port-bg border border-port-border rounded px-3 py-2 text-sm'; + + return ( +
+
+
+ + updateParams({ q: e.target.value }, { replace: true })} placeholder="Name, brief, model, or ID…" className={controlClass} /> +
+
+ + +
+
+ + +
+
+ + +
+
+
+ Showing {visible.length} of {projects.length} projects + {(query || status !== 'all') && } +
+ {projects.length > 0 && visible.length === 0 &&

No projects match your search and filters.

} +
+ {visible.map(p => children(p, compact))} +
+
+ ); +} diff --git a/client/src/pages/CreativeDirector.jsx b/client/src/pages/CreativeDirector.jsx index 6dc8c104e9..7c58ecf448 100644 --- a/client/src/pages/CreativeDirector.jsx +++ b/client/src/pages/CreativeDirector.jsx @@ -23,6 +23,7 @@ import Drawer from '../components/Drawer'; import DirectiveComposer from '../components/creative-director/DirectiveComposer.jsx'; import CreativeDirectorModelsDrawer from '../components/creative-director/CreativeDirectorModelsDrawer.jsx'; import VideoDraftDrawer from '../components/creative-director/VideoDraftDrawer.jsx'; +import ProjectLibrary from '../components/creative-director/ProjectLibrary.jsx'; import ProjectPreview from '../components/creative-director/ProjectPreview.jsx'; const ASPECT_RATIOS = ['16:9', '9:16', '1:1']; @@ -161,7 +162,12 @@ export default function CreativeDirector({ basePath = '/creative-director', brow }; try { const created = await createCreativeDirectorProject(payload, { silent: true }); - setProjects((prev) => [...prev, created]); + setProjects((prev) => [created, ...prev]); + setSearchParams(prev => { + const next = new URLSearchParams(prev); + ['q', 'status', 'sort'].forEach(key => next.delete(key)); + return next; + }, { replace: true }); setShowForm(false); setForm((f) => ({ ...f, name: '', styleSpec: '', userStory: '', startingImageFile: '' })); toast.success(`Created "${created.name}"`); @@ -493,12 +499,12 @@ export default function CreativeDirector({ basePath = '/creative-director', brow No projects yet. Open Creative Director to create a video draft. )} -
- {projects.map((p) => ( + + {(p, compact) => (
- + {!compact && }
- +
{p.name}
{p.id}
@@ -530,8 +536,8 @@ export default function CreativeDirector({ basePath = '/creative-director', brow }
- ))} -
+ )} + { + const projects = [ + { id: 'finished', name: 'Alpha finished', status: 'complete', createdAt: '2026-09-08', updatedAt: '2026-09-09' }, + { id: 'older', name: 'Beta paused', status: 'paused', createdAt: '2026-09-01', updatedAt: '2026-09-08', userStory: 'Mountain traveler' }, + { id: 'new', name: 'Zeta draft', status: 'draft', createdAt: '2026-09-07', updatedAt: '2026-09-07' }, + { id: 'legacy', name: 'Legacy project', status: 'failed' }, + ]; + const names = () => screen.getAllByRole('link', { name: /^Open (Alpha|Beta|Zeta|Legacy)/ }).map(link => link.getAttribute('aria-label')); + beforeEach(() => { vi.clearAllMocks(); cdApi.listCreativeDirectorProjects.mockResolvedValue(projects); }); + + it('prioritizes new unfinished work and supports alternate ordering and a compact view', async () => { + const user = userEvent.setup(); + await renderPage(); + expect(names()).toEqual(['Open Zeta draft', 'Open Beta paused', 'Open Legacy project', 'Open Alpha finished']); + await user.selectOptions(screen.getByLabelText('Sort projects'), 'newest'); + expect(names()[0]).toBe('Open Alpha finished'); + await user.selectOptions(screen.getByLabelText('Sort projects'), 'updated'); + expect(names().slice(0, 3)).toEqual(['Open Alpha finished', 'Open Beta paused', 'Open Zeta draft']); + await user.selectOptions(screen.getByLabelText('Sort projects'), 'name'); + await user.selectOptions(screen.getByLabelText('View'), 'list'); + expect(names()).toEqual(['Open Alpha finished', 'Open Beta paused', 'Open Legacy project', 'Open Zeta draft']); + expect(screen.queryByText('no render yet')).toBeNull(); + expect(screen.getByRole('link', { name: 'Open Zeta draft' })).toHaveAttribute('href', '/creative-director/new/overview'); + }); + + it('combines bookmarkable text and status filters and recovers from no matches', async () => { + const user = userEvent.setup(); + render(); + await screen.findByText('Showing 1 of 4 projects'); + expect(names()).toEqual(['Open Beta paused']); + await user.selectOptions(screen.getByLabelText('Status'), 'draft'); + expect(screen.getByText('No projects match your search and filters.')).toBeTruthy(); + await user.click(screen.getByRole('button', { name: 'Clear filters' })); + expect(names()).toHaveLength(4); + expect(screen.getByLabelText('View')).toHaveValue('list'); + expect(cdApi.listCreativeDirectorProjects).toHaveBeenCalledTimes(1); + }); + + it('shows newly created work even when the previous filters hid drafts', async () => { + const user = userEvent.setup(); + cdApi.createCreativeDirectorProject.mockResolvedValue({ id: 'created', name: 'Newly created', status: 'draft', createdAt: '2026-09-09' }); + render(); + await user.click(await screen.findByRole('button', { name: 'New project' })); + await user.type(screen.getByLabelText('Name'), 'Newly created'); + await user.click(screen.getByRole('button', { name: 'Create' })); + await screen.findByRole('link', { name: 'Open Newly created' }); + expect(screen.getAllByRole('link', { name: /^Open / })[0]).toHaveAttribute('aria-label', 'Open Newly created'); + expect(screen.getByLabelText('Search projects')).toHaveValue(''); + expect(screen.getByLabelText('Status')).toHaveValue('all'); + }); +}); From 3cc7cdfe503954ec9a5279406ae6c7a5ff223c3f Mon Sep 17 00:00:00 2001 From: "[._.]/ Adam Eivy" Date: Wed, 9 Sep 2026 14:54:54 +0000 Subject: [PATCH 2/2] fix: reveal test clip projects after creation --- client/src/pages/CreativeDirector.jsx | 13 ++++++++----- client/src/pages/CreativeDirector.test.jsx | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/client/src/pages/CreativeDirector.jsx b/client/src/pages/CreativeDirector.jsx index 7c58ecf448..07d963fa63 100644 --- a/client/src/pages/CreativeDirector.jsx +++ b/client/src/pages/CreativeDirector.jsx @@ -140,6 +140,12 @@ export default function CreativeDirector({ basePath = '/creative-director', brow // a later, unrelated project created from this list page (#1808 review). const clearRemix = () => { setRemixIds([]); setRemixIngredients([]); }; + const clearProjectFilters = () => setSearchParams(prev => { + const next = new URLSearchParams(prev); + ['q', 'status', 'sort'].forEach(key => next.delete(key)); + return next; + }, { replace: true }); + const handleCreate = async (e) => { e.preventDefault(); if (!form.name.trim() || !form.modelId) { @@ -163,11 +169,7 @@ export default function CreativeDirector({ basePath = '/creative-director', brow try { const created = await createCreativeDirectorProject(payload, { silent: true }); setProjects((prev) => [created, ...prev]); - setSearchParams(prev => { - const next = new URLSearchParams(prev); - ['q', 'status', 'sort'].forEach(key => next.delete(key)); - return next; - }, { replace: true }); + clearProjectFilters(); setShowForm(false); setForm((f) => ({ ...f, name: '', styleSpec: '', userStory: '', startingImageFile: '' })); toast.success(`Created "${created.name}"`); @@ -280,6 +282,7 @@ export default function CreativeDirector({ basePath = '/creative-director', brow if (!created) return; toast.success('Test clip render started'); setProjects((prev) => [created, ...prev]); + clearProjectFilters(); }; if (loading) { diff --git a/client/src/pages/CreativeDirector.test.jsx b/client/src/pages/CreativeDirector.test.jsx index 34988cd602..02bb3f3db2 100644 --- a/client/src/pages/CreativeDirector.test.jsx +++ b/client/src/pages/CreativeDirector.test.jsx @@ -230,3 +230,18 @@ describe('Project library discovery', () => { expect(screen.getByLabelText('Status')).toHaveValue('all'); }); }); + + it('reveals a smoke-test project when started from a filtered library', async () => { + vi.clearAllMocks(); + cdApi.listCreativeDirectorProjects.mockResolvedValue([{ id: 'finished', name: 'Finished example', status: 'complete' }]); + cdApi.createSmokeTestCreativeDirectorProject.mockResolvedValue({ id: 'smoke-new', name: 'New test clip', status: 'planning', createdAt: '2026-09-09' }); + const user = userEvent.setup(); + render(); + await screen.findByRole('button', { name: 'New project' }); + await openMenu(user); + await user.click(screen.getByRole('menuitem', { name: ITEM_LABEL })); + await user.click(screen.getByRole('button', { name: CONFIRM_LABEL })); + await screen.findByRole('link', { name: 'Open New test clip' }); + expect(screen.getByLabelText('Status')).toHaveValue('all'); + expect(screen.getByLabelText('Search projects')).toHaveValue(''); +});