239 lines
8.7 KiB
Markdown
239 lines
8.7 KiB
Markdown
---
|
|
phase: 25-file-system
|
|
plan: 05
|
|
type: execute
|
|
wave: 1
|
|
depends_on: ["25-01"]
|
|
files_modified:
|
|
- server/src/services/chat-files.ts
|
|
- server/src/routes/chat-files.ts
|
|
- ui/src/components/ChatFileCard.tsx
|
|
- ui/src/api/chat.ts
|
|
- .planning/REQUIREMENTS.md
|
|
autonomous: true
|
|
gap_closure: true
|
|
requirements: [FILE-12]
|
|
|
|
must_haves:
|
|
truths:
|
|
- "User can promote a chat-scoped file to project scope via a single action"
|
|
- "PATCH /files/:fileId/promote endpoint sets projectId on a chat file"
|
|
artifacts:
|
|
- path: "server/src/services/chat-files.ts"
|
|
provides: "promoteToProject service method"
|
|
- path: "server/src/routes/chat-files.ts"
|
|
provides: "PATCH /files/:fileId/promote endpoint"
|
|
- path: "ui/src/components/ChatFileCard.tsx"
|
|
provides: "Promote button when file has no projectId"
|
|
key_links:
|
|
- from: "ui/src/components/ChatFileCard.tsx"
|
|
to: "ui/src/api/chat.ts"
|
|
via: "chatApi.promoteFile call"
|
|
pattern: "promoteFile"
|
|
- from: "server/src/routes/chat-files.ts"
|
|
to: "server/src/services/chat-files.ts"
|
|
via: "fileSvc.promoteToProject"
|
|
pattern: "promoteToProject"
|
|
---
|
|
|
|
<objective>
|
|
Add file scope promotion: a chat-scoped file can be promoted to a project scope.
|
|
|
|
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
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/ROADMAP.md
|
|
@.planning/STATE.md
|
|
@.planning/phases/25-file-system/25-01-SUMMARY.md
|
|
|
|
<interfaces>
|
|
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:
|
|
```typescript
|
|
export function chatFileService(db: Db) {
|
|
return {
|
|
create(...), getById(...), listByConversation(...), listByMessage(...),
|
|
createReference(...), listReferences(...), attachToMessage(...)
|
|
};
|
|
}
|
|
```
|
|
|
|
From ui/src/api/chat.ts:
|
|
```typescript
|
|
export const chatApi = {
|
|
uploadFile(...), attachFilesToMessage(...)
|
|
};
|
|
```
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Add promoteToProject service method and API endpoint</name>
|
|
<files>server/src/services/chat-files.ts, server/src/routes/chat-files.ts</files>
|
|
<read_first>
|
|
- server/src/services/chat-files.ts
|
|
- server/src/routes/chat-files.ts
|
|
- packages/db/src/schema/chat_files.ts
|
|
</read_first>
|
|
<action>
|
|
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);
|
|
},
|
|
```
|
|
|
|
2. In `server/src/routes/chat-files.ts`, add a new route BEFORE the existing `PATCH /files/:fileId` route:
|
|
```typescript
|
|
// 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`.
|
|
</action>
|
|
<verify>
|
|
<automated>cd /opt/nexus && grep -n "promoteToProject" server/src/services/chat-files.ts server/src/routes/chat-files.ts</automated>
|
|
</verify>
|
|
<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>
|
|
<done>PATCH /files/:fileId/promote endpoint exists, validates input, updates projectId on chat file</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Add promote button to ChatFileCard and API client method</name>
|
|
<files>ui/src/api/chat.ts, ui/src/components/ChatFileCard.tsx, .planning/REQUIREMENTS.md</files>
|
|
<read_first>
|
|
- ui/src/api/chat.ts
|
|
- ui/src/components/ChatFileCard.tsx
|
|
- .planning/REQUIREMENTS.md
|
|
</read_first>
|
|
<action>
|
|
1. In `ui/src/api/chat.ts`, add a new method to `chatApi`:
|
|
```typescript
|
|
promoteFile(fileId: string, projectId: string) {
|
|
return api.patch<ChatFile>(`/files/${fileId}/promote`, { projectId });
|
|
},
|
|
```
|
|
Also add `ChatFile` to the import from `@paperclipai/shared` if not already imported.
|
|
|
|
2. In `ui/src/components/ChatFileCard.tsx`:
|
|
- Add optional props: `projectId?: string | null` and `onPromoted?: (file: ChatFile) => void` to ChatFileCardProps
|
|
- Import `FolderUp` from lucide-react (for the promote icon)
|
|
- Import `chatApi` from `../api/chat`
|
|
- Import `ChatFile` from `@paperclipai/shared`
|
|
- Add state: `const [promoting, setPromoting] = useState(false)`
|
|
- Add a promote button that appears ONLY when `file.projectId === null && projectId && onPromoted`:
|
|
```tsx
|
|
{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
|
|
|
|
3. Update `.planning/REQUIREMENTS.md`:
|
|
- Change FILE-12 line from `- [ ] **FILE-12**` to `- [x] **FILE-12**`
|
|
- In Traceability table, change FILE-12 status from `Pending` to `Complete`
|
|
</action>
|
|
<verify>
|
|
<automated>cd /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</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- `ui/src/api/chat.ts` contains `promoteFile(fileId: string, projectId: string)` method
|
|
- `ui/src/components/ChatFileCard.tsx` contains `FolderUp` import from lucide-react
|
|
- `ui/src/components/ChatFileCard.tsx` contains `onPromoted` prop
|
|
- `ui/src/components/ChatFileCard.tsx` contains `chatApi.promoteFile` call
|
|
- `ui/src/components/ChatFileCard.tsx` conditionally renders promote button only when `file.projectId === null && projectId && onPromoted`
|
|
- `.planning/REQUIREMENTS.md` contains `- [x] **FILE-12**`
|
|
</acceptance_criteria>
|
|
<done>Chat files can be promoted to project scope via UI button; FILE-12 marked Complete</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
- `npx tsc --noEmit -p ui/tsconfig.json` passes
|
|
- `npx tsc --noEmit -p server/tsconfig.json` passes
|
|
- `grep "promoteToProject" server/src/services/chat-files.ts` matches
|
|
- `grep "promoteFile" ui/src/api/chat.ts` matches
|
|
- `grep "\[x\].*FILE-12" .planning/REQUIREMENTS.md` matches
|
|
</verification>
|
|
|
|
<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>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/25-file-system/25-05-SUMMARY.md`
|
|
</output>
|