feat: Redesign review tab with phase selection, commit navigation, and consolidated toolbar
- Add BranchManager.listCommits() and diffCommit() for commit-level navigation - Add getPhaseReviewCommits and getCommitDiff tRPC procedures - New ReviewHeader: consolidated toolbar with phase selector pills, branch info, stats, integrated preview controls, and approve/reject actions - New CommitNav: horizontal commit strip with "All changes" + individual commits, each showing hash, message, and change stats - Slim down ReviewSidebar: file list only with dimming for out-of-scope files when viewing a single commit - ReviewTab orchestrates all pieces in a single bordered card layout
This commit is contained in:
@@ -234,5 +234,69 @@ export function phaseProcedures(publicProcedure: ProcedureBuilder) {
|
||||
await orchestrator.approveAndMergePhase(input.phaseId);
|
||||
return { success: true };
|
||||
}),
|
||||
|
||||
getPhaseReviewCommits: publicProcedure
|
||||
.input(z.object({ phaseId: z.string().min(1) }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const phaseRepo = requirePhaseRepository(ctx);
|
||||
const initiativeRepo = requireInitiativeRepository(ctx);
|
||||
const projectRepo = requireProjectRepository(ctx);
|
||||
const branchManager = requireBranchManager(ctx);
|
||||
|
||||
const phase = await phaseRepo.findById(input.phaseId);
|
||||
if (!phase) {
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: `Phase '${input.phaseId}' not found` });
|
||||
}
|
||||
if (phase.status !== 'pending_review') {
|
||||
throw new TRPCError({ code: 'BAD_REQUEST', message: `Phase is not pending review (status: ${phase.status})` });
|
||||
}
|
||||
|
||||
const initiative = await initiativeRepo.findById(phase.initiativeId);
|
||||
if (!initiative?.branch) {
|
||||
throw new TRPCError({ code: 'BAD_REQUEST', message: 'Initiative has no branch configured' });
|
||||
}
|
||||
|
||||
const initBranch = initiative.branch;
|
||||
const phBranch = phaseBranchName(initBranch, phase.name);
|
||||
const projects = await projectRepo.findProjectsByInitiativeId(phase.initiativeId);
|
||||
|
||||
const allCommits: Array<{ hash: string; shortHash: string; message: string; author: string; date: string; filesChanged: number; insertions: number; deletions: number }> = [];
|
||||
|
||||
for (const project of projects) {
|
||||
const clonePath = await ensureProjectClone(project, ctx.workspaceRoot!);
|
||||
const commits = await branchManager.listCommits(clonePath, initBranch, phBranch);
|
||||
allCommits.push(...commits);
|
||||
}
|
||||
|
||||
return { commits: allCommits, sourceBranch: phBranch, targetBranch: initBranch };
|
||||
}),
|
||||
|
||||
getCommitDiff: publicProcedure
|
||||
.input(z.object({ phaseId: z.string().min(1), commitHash: z.string().min(1) }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const phaseRepo = requirePhaseRepository(ctx);
|
||||
const projectRepo = requireProjectRepository(ctx);
|
||||
const branchManager = requireBranchManager(ctx);
|
||||
|
||||
const phase = await phaseRepo.findById(input.phaseId);
|
||||
if (!phase) {
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: `Phase '${input.phaseId}' not found` });
|
||||
}
|
||||
|
||||
const projects = await projectRepo.findProjectsByInitiativeId(phase.initiativeId);
|
||||
let rawDiff = '';
|
||||
|
||||
for (const project of projects) {
|
||||
const clonePath = await ensureProjectClone(project, ctx.workspaceRoot!);
|
||||
try {
|
||||
const diff = await branchManager.diffCommit(clonePath, input.commitHash);
|
||||
if (diff) rawDiff += diff + '\n';
|
||||
} catch {
|
||||
// commit not in this project clone
|
||||
}
|
||||
}
|
||||
|
||||
return { rawDiff };
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user