import type { Author } from "@bindings/cob/Author"; import type { CreateReviewArgs } from "@bindings/cob/patch/CreateReviewArgs"; import type { Patch } from "@bindings/cob/patch/Patch"; import type { Verdict } from "@bindings/cob/patch/Verdict"; import type { CodeLocation } from "@bindings/cob/thread/CodeLocation"; import type { CodeRange } from "@bindings/cob/thread/CodeRange"; import type { Comment } from "@bindings/cob/thread/Comment"; import { z } from "zod"; import useLocalStorage from "@app/lib/useLocalStorage.svelte"; import { invoke } from "./invoke"; // This is different from the stored draft review to align it with a // published `Review`. export interface DraftReview { id: string; draft: true; rid: string; author: Author; revisionId: string; verdict?: Verdict; summary?: string; labels: string[]; comments: Array>; } const codeRangeSchema: z.Schema = z.union([ z.object({ type: z.literal("lines"), range: z.object({ start: z.number(), end: z.number() }), }), z.object({ type: z.literal("chars"), line: z.number(), range: z.object({ start: z.number(), end: z.number() }), }), ]); const draftReviewStoredSchema = z.object({ id: z.string(), rid: z.string(), revision: z.string(), verdict: z.union([z.literal("accept"), z.literal("reject")]).optional(), summary: z.string().default(""), labels: z.array(z.string()), comments: z.array( z.object({ id: z.string(), body: z.string(), location: z .object({ commit: z.string(), path: z.string(), old: codeRangeSchema.nullable(), new: codeRangeSchema.nullable(), }) .optional(), }), ), }); type DraftReviewStored = z.infer; const storage = useLocalStorage( "repo.patches.draftReviews", z.record(z.string(), draftReviewStoredSchema), {}, ); export const draftReviewStorage = { get(id: string, author: Author): DraftReview | undefined { const draftReviewStored = storage.value[id]; if (!draftReviewStored) { return undefined; } return draftReviewFromStored(draftReviewStored, author); }, getForRevision(revisionId: string, author: Author): DraftReview | undefined { const draftReviewStored = Object.values(storage.value).find( draftReview => draftReview.revision === revisionId, ); if (draftReviewStored) { return draftReviewFromStored(draftReviewStored, author); } }, create(rid: string, revisionId: string): string { const id = crypto.randomUUID(); storage.update(reviews => { reviews[id] = { id, rid, revision: revisionId, summary: "", labels: [], comments: [], }; return reviews; }); return id; }, hasForRevision(revisionId: string): boolean { return Object.values(storage.value).some( review => review.revision === revisionId, ); }, update( id: string, props: { summary: string; verdict: Verdict | undefined; labels: string[] }, ) { updateStoredDraftReview(id, review => { return Object.assign(review, props); }); }, delete(id: string): DraftReviewStored | undefined { const review = storage.value[id]; storage.update(reviews => { delete reviews[id]; return reviews; }); return review; }, deleteComment(id: string, commentId: string) { updateStoredDraftReview(id, review => { const index = review.comments.findIndex( comment => comment.id === commentId, ); review.comments.splice(index, 1); return review; }); }, addComment( id: string, comment: { body: string; location: CodeLocation; }, ): string { const commentId = crypto.randomUUID(); updateStoredDraftReview(id, review => { review.comments.push({ id: commentId, body: comment.body, location: comment.location, }); return review; }); return commentId; }, updateComment( id: string, commentId: string, comment: { body: string; }, ) { updateStoredDraftReview(id, review => { const storedComment = review.comments.find( comment => comment.id === commentId, ); if (!storedComment) { throw new Error( `Comment ${commentId} does not exist for draft review ${id}`, ); } storedComment!.body = comment.body; return review; }); }, async publish(id: string) { const draftReviewStored = draftReviewStorage.delete(id); if (!draftReviewStored) { throw new Error( `Failed to publish draft review: Review ${id} does not exist`, ); } await invoke("create_patch_review", { args: { rid: draftReviewStored.rid, revision: draftReviewStored.revision, verdict: draftReviewStored.verdict ?? null, summary: draftReviewStored.summary, labels: draftReviewStored.labels, comments: draftReviewStored.comments.map(storedComment => ({ body: storedComment.body, location: storedComment.location ?? null, })), } satisfies CreateReviewArgs, }); }, }; function updateStoredDraftReview( id: string, update: (draftReviewStored: DraftReviewStored) => DraftReviewStored, ): void { storage.update(reviews => { const review = reviews[id]; if (!review) { throw new Error(`Draft review ${id} does not exist`); } reviews[id] = update(review); return reviews; }); } function draftReviewFromStored( draftReviewStored: z.infer, author: Author, ): DraftReview { return { id: draftReviewStored.id, draft: true, rid: draftReviewStored.rid, summary: draftReviewStored.summary, author, revisionId: draftReviewStored.revision, verdict: draftReviewStored.verdict, labels: draftReviewStored.labels, comments: draftReviewStored.comments.map(rawComment => ({ id: rawComment.id, author, edits: [ { author, timestamp: 0, body: rawComment.body, }, ], reactions: [], replyTo: null, location: rawComment.location ?? null, resolved: false, })), }; }