The project folders feature (workspace-scoped folders that organize projects) shipped as an MVP and has accumulated technical debt. The three main areas needing attention are: incomplete error handling in dashboard components, service-layer code quality issues, and gaps in test coverage. This plan covers a full cleanup pass.
fix: add error handling and rollback to project folder rename operationsrefactor: clean up project-folder service validation and remove wrapper methodsfix: add query param validation and consolidate route access checksfix: align frontend ProjectFolder types with API contracttest: add missing project-folder service tests for edge casesdocs: update project folder spec files with error handling and edge cases
project-folder-list-row.tsx— rename has no try/catch at all. IfupdateProjectFolderfails, the error propagates uncaught and mirror state diverges from server.project-folder-card.tsx— rename catches errors but doesn't roll back the optimistic mirror update on failure.
dashboard/src/features/projects-grid/project-folder-list-row.tsx
- Wrap the rename handler in try/catch
- Add
toast.error()notification on failure (matching the card component pattern) - Roll back mirror state on API failure by restoring previous name
dashboard/src/features/projects-grid/project-folder-card.tsx
- Add mirror rollback in the catch block — restore previous folder name if API call fails
- Ensure toast error message is user-friendly
// Before (list row - no error handling)
const handleRename = async (newName: string) => {
mirror.update(folder.id, { name: newName })
await updateProjectFolder(folder.id, { name: newName })
}
// After (with rollback)
const handleRename = async (newName: string) => {
const previousName = folder.name
mirror.update(folder.id, { name: newName })
try {
await updateProjectFolder(folder.id, { name: newName })
} catch (error) {
mirror.update(folder.id, { name: previousName })
toast.error('Failed to rename folder')
}
}dashboard/src/features/projects-grid/project-folder-list-row.tsxdashboard/src/features/projects-grid/project-folder-card.tsx
hasSiblingNameConflictis a wrapper that exists in bothproject-folder.service.tsandfolder.service.tswith similar logiccheckWouldCreateCycleis a trivial wrapper around the shared utilitycreateFolderdoesn't check if the parent folder is soft-deletedmoveProjectsToFoldersilently succeeds with empty array — should validatedeleteFolderevent publishing errors are logged but could leave dashboard out of sync
apps/api/src/services/project-folder.service.ts
-
Remove
checkWouldCreateCyclewrapper — callwouldCreateCycle()fromfolder-utils.tsdirectly inupdateFolder() -
Inline
hasSiblingNameConflict— simplify by querying directly where used (createFolder, updateFolder). The logic is straightforward: query for sibling with normalized name match. -
Add soft-delete validation to
createFolder— whenparentFolderIdis provided, checkdeletedAt IS NULLon the parent -
Validate
moveProjectsToFolderinput — throw ifprojectIdsarray is empty -
Improve
deleteFolderevent error handling — log atwarnlevel instead of silently catching, and include folder ID in log context
apps/api/src/services/project-folder.service.ts
- Every endpoint individually checks workspace access with similar boilerplate
- Query params
limitandoffsetaccept any string (including negative numbers) parentFolderId === 'null'string comparison is fragile
apps/api/src/routes/project-folders.ts
-
Add numeric validation to query params — use Elysia's
t.Numeric()withminimum: 0for limit/offset -
Fix
parentFolderIdnull handling — acceptparentFolderIdast.Optional(t.Union([t.String(), t.Null()]))and handle properly instead of string comparison -
Extract workspace access check — create a
resolveAndAuthorizehelper within the route file that handles the common pattern of: get folder -> check workspace access -> return folder. Reduces ~10 lines per endpoint to ~1 line.
apps/api/src/routes/project-folders.ts
ProjectFolderinterface in dashboard declaresparentFolderId?: string | null(optional) but API always sends the field (it'sstring | null, not optional)moveProjectsToFolderconvertsnullto'root'on the client, then API converts back — unnecessary round-trip
dashboard/src/api/project-folders.ts
- Fix
ProjectFolder.parentFolderIdtype tostring | null(not optional) - Fix
ProjectFolder.descriptiontype tostring | null(not optional) - Remove client-side
null -> 'root'conversion inmoveProjectsToFolder— send null directly and let API handle it - Use ky's
searchParamsoption instead of manualURLSearchParamsconstruction
dashboard/src/api/project-folders.ts
Test file (project-folder.service.test.ts, 314 lines) only covers basic happy paths. Missing coverage for:
- Name conflict detection (case-insensitive matching)
- Cycle detection when moving folders
- Soft-deleted parent validation
- Event publishing verification
moveProjectsToFolderwith empty arraydeleteFoldercascading behavior
apps/api/src/services/project-folder.service.test.ts
Add test cases:
createFolder— rejects when parent is soft-deletedcreateFolder— rejects when sibling has same normalized nameupdateFolder— rejects when move would create cycleupdateFolder— rejects name conflict with existing siblingmoveProjectsToFolder— rejects empty projectIds arraydeleteFolder— verifies event is published to workspacedeleteFolder— succeeds even when event publishing fails
apps/api/src/services/project-folder.service.test.ts
Existing spec files are minimal (11-17 lines each) and don't document error handling, edge cases, or the rename rollback pattern.
dashboard/src/features/projects-grid/project-folder-card.spec.md
- Document optimistic update + rollback pattern
- Document error states and toast notifications
- Add edge cases (empty name, network failure)
dashboard/src/features/projects-grid/project-folder-list-row.spec.md
- Document rename behavior matching card pattern
- Document loading states during API calls
dashboard/src/features/projects-grid/project-folder-card.spec.mddashboard/src/features/projects-grid/project-folder-list-row.spec.md
- Lint & typecheck:
bun obvious check --changed - Run project folder tests:
bun obvious test --files apps/api/src/services/project-folder.service.test.ts - Run dashboard tests:
bun obvious test --changed(for any affected dashboard test files) - Manual verification: Start dashboard with
bun obvious up --dashboard-only, navigate to a workspace with project folders enabled, and test:- Rename a folder, verify success
- Trigger a rename failure (e.g., disconnect network), verify rollback + toast
- Create nested folders, move them around
- Delete a folder with contents
| File | Action |
|---|---|
dashboard/src/features/projects-grid/project-folder-list-row.tsx |
Fix error handling |
dashboard/src/features/projects-grid/project-folder-card.tsx |
Fix rollback |
apps/api/src/services/project-folder.service.ts |
Refactor validation, remove wrappers |
apps/api/src/routes/project-folders.ts |
Tighten validation, extract helper |
dashboard/src/api/project-folders.ts |
Fix types, simplify client |
apps/api/src/services/project-folder.service.test.ts |
Add edge case tests |
dashboard/src/features/projects-grid/project-folder-card.spec.md |
Update docs |
dashboard/src/features/projects-grid/project-folder-list-row.spec.md |
Update docs |