mirror of
https://seed.radicle.dev/rad:z4D5UCArafTzTQpDZNQRuqswh3ury.git
synced 2026-11-07 12:20:45 +08:00
253 lines
6.2 KiB
TypeScript
253 lines
6.2 KiB
TypeScript
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<Comment<CodeLocation>>;
|
|
}
|
|
|
|
const codeRangeSchema: z.Schema<CodeRange> = 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<typeof draftReviewStoredSchema>;
|
|
|
|
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<Patch>("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<typeof draftReviewStoredSchema>,
|
|
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,
|
|
})),
|
|
};
|
|
}
|