8.7 KiB
| phase | plan | type | wave | depends_on | files_modified | autonomous | gap_closure | requirements | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 25-file-system | 05 | execute | 1 |
|
|
true | true |
|
|
Purpose: FILE-12 requires that chat-scoped files can be promoted to project scope. The schema already has a nullable projectId FK on chatFiles, but there is no API endpoint or UI to set it. This plan adds a PATCH /files/:fileId/promote endpoint and a promote button on ChatFileCard.
Output: Service method, API endpoint, UI promote button, updated REQUIREMENTS.md
<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/25-file-system/25-01-SUMMARY.md From packages/db/src/schema/chat_files.ts: ```typescript export const chatFiles = pgTable("chat_files", { id: uuid("id").primaryKey().defaultRandom(), projectId: uuid("project_id").references(() => projects.id, { onDelete: "set null" }), // ... other columns }); ```From server/src/services/chat-files.ts:
export function chatFileService(db: Db) {
return {
create(...), getById(...), listByConversation(...), listByMessage(...),
createReference(...), listReferences(...), attachToMessage(...)
};
}
From ui/src/api/chat.ts:
export const chatApi = {
uploadFile(...), attachFilesToMessage(...)
};
Task 1: Add promoteToProject service method and API endpoint
server/src/services/chat-files.ts, server/src/routes/chat-files.ts
- server/src/services/chat-files.ts
- server/src/routes/chat-files.ts
- packages/db/src/schema/chat_files.ts
1. In `server/src/services/chat-files.ts`, add a new method `promoteToProject` to the returned object:
```typescript
promoteToProject(fileId: string, projectId: string) {
return db
.update(chatFiles)
.set({ projectId, updatedAt: new Date() })
.where(eq(chatFiles.id, fileId))
.returning()
.then((rows) => rows[0] ?? null);
},
```
- In
server/src/routes/chat-files.ts, add a new route BEFORE the existingPATCH /files/:fileIdroute:
// PATCH /files/:fileId/promote — Promote chat file to project scope
router.patch("/files/:fileId/promote", async (req, res) => {
assertBoard(req);
const fileId = req.params.fileId as string;
const chatFile = await fileSvc.getById(fileId);
if (!chatFile) {
res.status(404).json({ error: "File not found" });
return;
}
assertCompanyAccess(req, chatFile.companyId);
const { projectId } = req.body ?? {};
if (!projectId || typeof projectId !== "string") {
res.status(400).json({ error: "projectId is required" });
return;
}
const updated = await fileSvc.promoteToProject(fileId, projectId);
if (!updated) {
res.status(404).json({ error: "File not found" });
return;
}
res.json(updated);
});
Place this route BEFORE the PATCH /files/:fileId route so Express matches /files/:fileId/promote before the catch-all /files/:fileId.
cd /opt/nexus && grep -n "promoteToProject" server/src/services/chat-files.ts server/src/routes/chat-files.ts
<acceptance_criteria>
- server/src/services/chat-files.ts contains promoteToProject(fileId: string, projectId: string) method
- server/src/routes/chat-files.ts contains router.patch("/files/:fileId/promote" route
- The promote route appears BEFORE the generic router.patch("/files/:fileId" route
- Route validates projectId is present and is a string
- Route calls fileSvc.promoteToProject(fileId, projectId)
</acceptance_criteria>
PATCH /files/:fileId/promote endpoint exists, validates input, updates projectId on chat file
-
In
ui/src/components/ChatFileCard.tsx:- Add optional props:
projectId?: string | nullandonPromoted?: (file: ChatFile) => voidto ChatFileCardProps - Import
FolderUpfrom lucide-react (for the promote icon) - Import
chatApifrom../api/chat - Import
ChatFilefrom@paperclipai/shared - Add state:
const [promoting, setPromoting] = useState(false) - Add a promote button that appears ONLY when
file.projectId === null && projectId && onPromoted:{file.projectId === null && projectId && onPromoted && ( <button onClick={async (e) => { e.stopPropagation(); setPromoting(true); try { const updated = await chatApi.promoteFile(file.id, projectId); onPromoted(updated); } finally { setPromoting(false); } }} disabled={promoting} className="shrink-0 rounded p-1 text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50" aria-label="Promote to project" title="Promote to project" > <FolderUp className="h-4 w-4" /> </button> )} - Place this button before the download button
- Add optional props:
-
Update
.planning/REQUIREMENTS.md:- Change FILE-12 line from
- [ ] **FILE-12**to- [x] **FILE-12** - In Traceability table, change FILE-12 status from
PendingtoCompletecd /opt/nexus && grep -n "promoteFile" ui/src/api/chat.ts && grep -n "FolderUp|onPromoted|promoteToProject" ui/src/components/ChatFileCard.tsx && grep "FILE-12" .planning/REQUIREMENTS.md | head -3 <acceptance_criteria> ui/src/api/chat.tscontainspromoteFile(fileId: string, projectId: string)methodui/src/components/ChatFileCard.tsxcontainsFolderUpimport from lucide-reactui/src/components/ChatFileCard.tsxcontainsonPromotedpropui/src/components/ChatFileCard.tsxcontainschatApi.promoteFilecallui/src/components/ChatFileCard.tsxconditionally renders promote button only whenfile.projectId === null && projectId && onPromoted.planning/REQUIREMENTS.mdcontains- [x] **FILE-12**</acceptance_criteria> Chat files can be promoted to project scope via UI button; FILE-12 marked Complete
- Change FILE-12 line from
<success_criteria>
- PATCH /files/:fileId/promote endpoint sets projectId on a chat file
- ChatFileCard shows a promote button for chat-scoped files when a project context is available
- FILE-12 marked Complete in REQUIREMENTS.md
- TypeScript compiles without errors </success_criteria>