feat(template): impl remix template w/ direct copy#72
Conversation
📝 WalkthroughWalkthroughThis PR implements template remixing functionality that derives and materializes projects from template versions, refactors project-service exports to support this, hardens user-deletion validation in project duplication, and removes disabled or incomplete frontend navigation features (Design Systems link and API Keys tab). ChangesTemplate Remixing and Project Service Refactoring
Navigation UI Removals
Placeholder Cleanup
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes The primary complexity stems from understanding the new 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
web/src/features/profile/components/ProfileSettings.tsx (1)
157-167: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick winClean up the API Keys path end-to-end (hide + unreachable view).
With this button removed,
activeTabno longer has a UI path to'API Keys', so theactiveTab === 'API Keys'branch (Line 239-Line 240) is effectively dead in this component. Could you either remove the API Keys render branch/import too, or wire this behind an explicit feature flag/deep-link entry so intent stays clear and testable?server/src/modules/project/project.service.ts (1)
62-67:⚠️ Potential issue | 🟠 Major | ⚡ Quick winFail fast when a manifest entry is missing in storage.
Falling back to
''here turns object-store drift into silent file corruption in download, duplicate, and remix flows. It would be safer to throw and fail the request than materialize empty files.Proposed fix
export const loadGeneratedFilesFromManifest = async (manifest: StoredProjectFile[]) => { const files = await Promise.all( - manifest.map(async (file) => [file.path, (await getTextFile(file.key)) ?? ''] as const) + manifest.map(async (file) => { + const content = await getTextFile(file.key) + if (content === null) { + throw new AppError(`missing stored file for "${file.path}"`, 500) + } + return [file.path, content] as const + }) ) return Object.fromEntries(files) }
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: b7ddc965-0939-475e-99c8-dbc897d571e9
📒 Files selected for processing (20)
server/src/modules/docs/docs.controller.tsserver/src/modules/docs/docs.routes.tsserver/src/modules/docs/docs.schema.tsserver/src/modules/docs/docs.service.tsserver/src/modules/docs/docs.utils.tsserver/src/modules/keys/keys.controller.tsserver/src/modules/keys/keys.routes.tsserver/src/modules/keys/keys.schema.tsserver/src/modules/keys/keys.service.tsserver/src/modules/keys/keys.utils.tsserver/src/modules/project/project.service.tsserver/src/modules/template/template.service.tsserver/tests/integration/docs/docs.routes.test.tsserver/tests/integration/docs/docs.service.test.tsserver/tests/integration/keys/keys.routes.test.tsserver/tests/integration/keys/keys.service.test.tsserver/tests/unit/docs.unit.test.tsserver/tests/unit/keys.unit.test.tsweb/src/features/navigation/components/Sidebar.tsxweb/src/features/profile/components/ProfileSettings.tsx
💤 Files with no reviewable changes (2)
- server/src/modules/docs/docs.routes.ts
- web/src/features/navigation/components/Sidebar.tsx
| const newProject = await prisma.project.create({ | ||
| data: { | ||
| name: `Remix of ${template.name}`, | ||
| description: template.description, | ||
| prompt: currentVersion?.sourcePrompt ?? template.prompt, | ||
| projectStatus: currentVersion ? 'READY' : template.projectStatus, | ||
| userId: userId, | ||
| }, | ||
| }) | ||
|
|
||
| if (!currentVersion) { | ||
| return newProject | ||
| } | ||
|
|
||
| const manifest = parseStoredProjectFiles(currentVersion.manifestJson) | ||
| const generatedFiles = await loadGeneratedFilesFromManifest(manifest) | ||
|
|
||
| const versionRecordId = crypto.randomUUID() | ||
|
|
||
| const savedFiles = await saveProjectFiles({ | ||
| projectId: newProject.id, | ||
| versionId: versionRecordId, | ||
| files: Object.entries(generatedFiles).map(([path, content]) => ({ | ||
| path, | ||
| content, | ||
| })), | ||
| }) | ||
|
|
||
| await prisma.projectVersion.create({ | ||
| data: { | ||
| id: versionRecordId, | ||
| projectId: newProject.id, | ||
| versionNumber: 1, | ||
| label: 'v1', | ||
| sourcePrompt: currentVersion.sourcePrompt, | ||
| status: 'READY', | ||
| objectStoragePrefix: `projects/${newProject.id}/v1/${versionRecordId}`, | ||
|
|
||
| manifestJson: savedFiles.map((file) => ({ | ||
| path: file.path, | ||
| key: file.key, | ||
| size: file.size, | ||
| ...(file.contentType ? { contentType: file.contentType } : {}), | ||
| })), | ||
| }, | ||
| }) | ||
|
|
||
| await prisma.project.update({ | ||
| where: { id: newProject.id }, | ||
| data: { | ||
| currentVersionId: versionRecordId, | ||
| versionCount: 1, | ||
| }, | ||
| }) |
There was a problem hiding this comment.
Make the remix creation all-or-nothing.
After project.create, any failure in file loading, file saving, projectVersion.create, or the final project.update leaves behind a partially initialized project, and in the currentVersion path it is created as READY before the copy actually succeeds. Could we wrap the DB writes in a transaction and add compensating cleanup for saved files, or keep the project non-ready until the copy completes?
| await prisma.projectVersion.create({ | ||
| data: { | ||
| id: versionRecordId, | ||
| projectId: newProject.id, | ||
| versionNumber: 1, | ||
| label: 'v1', | ||
| sourcePrompt: currentVersion.sourcePrompt, | ||
| status: 'READY', | ||
| objectStoragePrefix: `projects/${newProject.id}/v1/${versionRecordId}`, | ||
|
|
||
| manifestJson: savedFiles.map((file) => ({ | ||
| path: file.path, | ||
| key: file.key, | ||
| size: file.size, | ||
| ...(file.contentType ? { contentType: file.contentType } : {}), | ||
| })), | ||
| }, |
There was a problem hiding this comment.
Was dropping the version metadata intentional?
This persists the remixed version's file manifest, but it does not carry forward canvasStateJson, canvasAssetManifestJson, intentJson, or planJson from currentVersion. If the goal is a direct copy, templates created through the canvas flow will remix into a blank state.
Summary by CodeRabbit
New Features
Changes