Files
kandev/apps/web/lib/diff/comment-utils.ts
T
José Almeida d62b51b0be fix: route controlled diff comment updates (#1599)
* fix: route controlled diff comment updates

* fix: address review comments overview feedback

* fix: address automated review feedback

* fix: keep failed controlled edits open

* fix: guard controlled comment deletes

* fix: compare controlled delete handler

* test: cover controlled update rerender
2026-07-07 10:19:56 +01:00

112 lines
3.3 KiB
TypeScript

import { useMemo, useCallback } from "react";
import type { DiffComment, DiffCommentUpdate } from "./types";
/** Build a DiffComment object from common parameters */
export function buildDiffComment(params: {
filePath: string;
sessionId: string;
startLine: number;
endLine: number;
side: DiffComment["side"];
text: string;
codeContent?: string;
}): DiffComment {
return {
id: `${params.filePath}-${Date.now()}`,
source: "diff",
sessionId: params.sessionId,
filePath: params.filePath,
startLine: Math.min(params.startLine, params.endLine),
endLine: Math.max(params.startLine, params.endLine),
side: params.side,
codeContent: params.codeContent ?? "",
text: params.text,
createdAt: new Date().toISOString(),
status: "pending",
};
}
/** Compute the set of line numbers that have comments */
export function useCommentedLines(comments: DiffComment[]): Set<number> {
return useMemo(() => {
const set = new Set<number>();
for (const c of comments) {
for (let l = c.startLine; l <= c.endLine; l++) set.add(l);
}
return set;
}, [comments]);
}
/**
* Shared delete/update callbacks that handle external-vs-internal branching.
* Used by both Pierre diff-viewer and Monaco diff-viewer.
*/
export function useCommentActions(params: {
removeComment: (commentId: string) => void;
updateComment: (commentId: string, updates: DiffCommentUpdate) => void;
setEditingComment: (id: string | null) => void;
onCommentDelete?: (commentId: string) => void;
onCommentUpdate?: (commentId: string, updates: DiffCommentUpdate) => void;
externalComments?: DiffComment[];
}) {
const {
removeComment,
updateComment,
setEditingComment,
onCommentDelete,
onCommentUpdate,
externalComments,
} = params;
const handleCommentDelete = useCallback(
(commentId: string) => {
if (externalComments !== undefined) {
if (onCommentDelete) {
onCommentDelete(commentId);
} else if (isDevelopmentMode()) {
console.warn(
"[DiffViewer] `comments` is set without `onCommentDelete`; deleted comments must be handled by the controlled owner.",
);
}
} else {
removeComment(commentId);
}
},
[removeComment, onCommentDelete, externalComments],
);
const handleCommentUpdate = useCallback(
(commentId: string, content: string) => {
const updates = { text: content };
if (externalComments !== undefined) {
if (onCommentUpdate) {
onCommentUpdate(commentId, updates);
setEditingComment(null);
} else if (isDevelopmentMode()) {
console.warn(
"[DiffViewer] `comments` is set without `onCommentUpdate`; edited comments must be handled by the controlled owner.",
);
}
} else {
updateComment(commentId, updates);
setEditingComment(null);
}
},
[updateComment, onCommentUpdate, externalComments, setEditingComment],
);
return { handleCommentDelete, handleCommentUpdate };
}
function isDevelopmentMode(): boolean {
const viteEnv = (
import.meta as unknown as {
env?: { DEV?: boolean };
}
).env;
return (
viteEnv?.DEV === true ||
(typeof process !== "undefined" && process.env.NODE_ENV !== "production")
);
}