From a8d3f52d09c4b4269e1fe2cd398c3edbef3ea631 Mon Sep 17 00:00:00 2001 From: Lukas May Date: Tue, 10 Feb 2026 11:19:48 +0100 Subject: [PATCH] feat: Re-add initiative branch field and add projects settings page Allow users to specify a custom branch when creating initiatives (auto-generated if left blank). Add updateProject tRPC procedure and /settings/projects page with inline-editable defaultBranch. --- docs/server-api.md | 3 +- .../src/components/CreateInitiativeDialog.tsx | 17 ++ packages/web/src/routeTree.gen.ts | 21 ++ packages/web/src/routes/settings.tsx | 1 + packages/web/src/routes/settings/projects.tsx | 179 ++++++++++++++++++ src/trpc/routers/initiative.ts | 2 + src/trpc/routers/project.ts | 18 ++ 7 files changed, 240 insertions(+), 1 deletion(-) create mode 100644 packages/web/src/routes/settings/projects.tsx diff --git a/docs/server-api.md b/docs/server-api.md index 82b8b13..7f5c889 100644 --- a/docs/server-api.md +++ b/docs/server-api.md @@ -83,7 +83,7 @@ Each procedure uses `require*Repository(ctx)` helpers that throw `TRPCError(INTE ### Initiatives | Procedure | Type | Description | |-----------|------|-------------| -| createInitiative | mutation | Create with optional projectIds, auto-creates root page | +| createInitiative | mutation | Create with optional branch/projectIds, auto-creates root page | | listInitiatives | query | Filter by status | | getInitiative | query | With projects array | | updateInitiative | mutation | Name, status | @@ -143,6 +143,7 @@ Each procedure uses `require*Repository(ctx)` helpers that throw `TRPCError(INTE | registerProject | mutation | Clone git repo, create record | | listProjects | query | All projects | | getProject | query | Single project | +| updateProject | mutation | Update project settings (defaultBranch) | | deleteProject | mutation | Delete clone and record | | getInitiativeProjects | query | Projects for initiative | | updateInitiativeProjects | mutation | Sync junction table | diff --git a/packages/web/src/components/CreateInitiativeDialog.tsx b/packages/web/src/components/CreateInitiativeDialog.tsx index 338e23d..079d195 100644 --- a/packages/web/src/components/CreateInitiativeDialog.tsx +++ b/packages/web/src/components/CreateInitiativeDialog.tsx @@ -31,6 +31,7 @@ export function CreateInitiativeDialog({ onOpenChange, }: CreateInitiativeDialogProps) { const [name, setName] = useState(""); + const [branch, setBranch] = useState(""); const [projectIds, setProjectIds] = useState([]); const [executionMode, setExecutionMode] = useState<"yolo" | "review_per_phase">("review_per_phase"); const [error, setError] = useState(null); @@ -70,6 +71,7 @@ export function CreateInitiativeDialog({ useEffect(() => { if (open) { setName(""); + setBranch(""); setProjectIds([]); setExecutionMode("review_per_phase"); setError(null); @@ -81,6 +83,7 @@ export function CreateInitiativeDialog({ setError(null); createMutation.mutate({ name: name.trim(), + branch: branch.trim() || null, projectIds: projectIds.length > 0 ? projectIds : undefined, executionMode, }); @@ -108,6 +111,20 @@ export function CreateInitiativeDialog({ autoFocus /> +
+ + setBranch(e.target.value)} + /> +
setEditValue(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') saveEdit() + if (e.key === 'Escape') { + setEditing(false) + setEditValue(project.defaultBranch) + } + }} + onBlur={saveEdit} + autoFocus + disabled={updateMutation.isPending} + /> + ) : ( + + )} +
+ + + + + ) +} diff --git a/src/trpc/routers/initiative.ts b/src/trpc/routers/initiative.ts index 6e0b68d..7369248 100644 --- a/src/trpc/routers/initiative.ts +++ b/src/trpc/routers/initiative.ts @@ -12,6 +12,7 @@ export function initiativeProcedures(publicProcedure: ProcedureBuilder) { createInitiative: publicProcedure .input(z.object({ name: z.string().min(1), + branch: z.string().nullable().optional(), projectIds: z.array(z.string().min(1)).min(1).optional(), executionMode: z.enum(['yolo', 'review_per_phase']).optional(), })) @@ -35,6 +36,7 @@ export function initiativeProcedures(publicProcedure: ProcedureBuilder) { name: input.name, status: 'active', ...(input.executionMode && { executionMode: input.executionMode }), + ...(input.branch && { branch: input.branch }), }); if (input.projectIds && input.projectIds.length > 0) { diff --git a/src/trpc/routers/project.ts b/src/trpc/routers/project.ts index f7431d5..0433751 100644 --- a/src/trpc/routers/project.ts +++ b/src/trpc/routers/project.ts @@ -90,6 +90,24 @@ export function projectProcedures(publicProcedure: ProcedureBuilder) { return { success: true }; }), + updateProject: publicProcedure + .input(z.object({ + id: z.string().min(1), + defaultBranch: z.string().min(1).optional(), + })) + .mutation(async ({ ctx, input }) => { + const repo = requireProjectRepository(ctx); + const { id, ...data } = input; + const existing = await repo.findById(id); + if (!existing) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: `Project '${id}' not found`, + }); + } + return repo.update(id, data); + }), + getInitiativeProjects: publicProcedure .input(z.object({ initiativeId: z.string().min(1) })) .query(async ({ ctx, input }) => {