Files
kandev/apps/web/components/task/task-changes-panel-state.ts
T
José Almeida d5da5d0022 fix(web): select pull request on review surface (#1857)
* fix(web): select pull request on review surface

* fix(web): address review feedback

* test(web): stabilize tablet review target assertion

* fix(web): address late review feedback

* test(web): cover PR review boundary branches
2026-07-22 17:52:58 +01:00

196 lines
6.5 KiB
TypeScript

"use client";
import { useEffect, useMemo, useRef, type MutableRefObject, type RefObject } from "react";
import { reviewFileKey, splitReviewFileKey, type ReviewFile } from "@/components/review/types";
import type { ReviewSource } from "@/hooks/domains/session/use-review-sources";
export function shouldDeferReviewStateForPR(
hasSelectedPR: boolean,
prDiffLoading: boolean,
sourceFilter: "all" | ReviewSource,
): boolean {
const usesPRDiff = sourceFilter === "all" || sourceFilter === "pr";
return hasSelectedPR && prDiffLoading && usesPRDiff;
}
export function shouldBlockChangesForPR(
sourceFilter: "all" | ReviewSource,
visibleFiles: ReviewFile[],
): boolean {
if (sourceFilter === "pr") return true;
if (sourceFilter !== "all") return false;
return !visibleFiles.some((file) => file.source !== "pr");
}
export function resolveSelectedFileRepositoryName(
sourceFilter: "all" | ReviewSource,
prKey: string | undefined,
fileRepositoryName: string | undefined,
visibleFileRepositoryName: string | undefined,
): string | undefined {
return prKey && sourceFilter === "pr" ? visibleFileRepositoryName : fileRepositoryName;
}
export function shouldCloseFileDiffPanel(
gitStatus: { files?: Record<string, { diff?: string }> } | undefined,
filePath: string,
): boolean {
if (!gitStatus) return false;
return !gitStatus.files?.[filePath]?.diff;
}
function shouldCloseFileDiffPanelAggregate(
prevFileSeenRef: MutableRefObject<boolean>,
gitStatus: { files?: Record<string, { diff?: string }> } | undefined,
filePath: string,
onBecameEmpty: (() => void) | undefined,
): boolean {
const shouldClose = shouldCloseFileDiffPanel(gitStatus, filePath);
if (prevFileSeenRef.current && shouldClose) {
onBecameEmpty?.();
return true;
}
if (!shouldClose) prevFileSeenRef.current = true;
return false;
}
export function useAutoCloseWhenEmpty(opts: {
mode: "all" | "file";
filePath: string | undefined;
sourceFilter: "all" | ReviewSource;
gitStatus: { files?: Record<string, { diff?: string }> } | undefined;
visibleCount: number;
prDiffLoading: boolean;
onBecameEmpty: (() => void) | undefined;
}) {
const { mode, filePath, sourceFilter, gitStatus, visibleCount, prDiffLoading, onBecameEmpty } =
opts;
const prevVisibleCountRef = useRef<number | null>(null);
const prevFileSeenRef = useRef(false);
const prevSourceFilterRef = useRef<typeof sourceFilter | null>(null);
useEffect(() => {
if (!onBecameEmpty) return;
if (prDiffLoading) {
prevVisibleCountRef.current = visibleCount;
return;
}
if (mode === "file" && filePath) {
if (sourceFilter === "all") {
shouldCloseFileDiffPanelAggregate(prevFileSeenRef, gitStatus, filePath, onBecameEmpty);
return;
}
if (prevSourceFilterRef.current !== sourceFilter) {
prevSourceFilterRef.current = sourceFilter;
prevVisibleCountRef.current = null;
}
const prevCount = prevVisibleCountRef.current;
prevVisibleCountRef.current = visibleCount;
if (prevCount !== null && prevCount > 0 && visibleCount === 0) onBecameEmpty();
return;
}
if (sourceFilter !== "all") return;
const prevCount = prevVisibleCountRef.current;
if (prevCount !== null && prevCount > 0 && visibleCount === 0) onBecameEmpty();
prevVisibleCountRef.current = visibleCount;
}, [mode, filePath, sourceFilter, gitStatus, onBecameEmpty, visibleCount, prDiffLoading]);
}
type FilterVisibleFilesOpts = {
mode: "all" | "file";
filePath: string | undefined;
fileRepositoryName: string | undefined;
sourceFilter: "all" | ReviewSource;
rawPRFiles?: ReviewFile[];
prKey?: string;
};
export function filterVisibleFiles(
allFiles: ReviewFile[],
opts: FilterVisibleFilesOpts,
): ReviewFile[] {
const { mode, filePath, fileRepositoryName, sourceFilter, rawPRFiles, prKey } = opts;
const repositoryFilter = prKey && sourceFilter === "pr" ? undefined : fileRepositoryName;
if (mode === "file" && filePath && sourceFilter === "pr" && rawPRFiles?.length) {
let prFiles = rawPRFiles.filter((file) => file.path === filePath && file.source === "pr");
if (repositoryFilter !== undefined) {
prFiles = prFiles.filter((file) => (file.repository_name ?? "") === repositoryFilter);
}
if (prFiles.length > 0) return prFiles;
}
let files = allFiles;
if (mode === "file" && filePath) {
files = files.filter((file) => file.path === filePath);
if (repositoryFilter !== undefined) {
files = files.filter((file) => (file.repository_name ?? "") === repositoryFilter);
}
}
if (sourceFilter !== "all") files = files.filter((file) => file.source === sourceFilter);
return files;
}
export function useVisibleDiffState(opts: {
allFiles: ReviewFile[];
rawPRFiles: ReviewFile[];
mode: "all" | "file";
filePath: string | undefined;
fileRepositoryName: string | undefined;
sourceFilter: "all" | ReviewSource;
prKey: string | undefined;
fileRefs: Map<string, RefObject<HTMLDivElement | null>>;
reviewedFiles: Set<string>;
staleFiles: Set<string>;
}) {
const {
allFiles,
rawPRFiles,
mode,
filePath,
fileRepositoryName,
sourceFilter,
prKey,
fileRefs,
reviewedFiles,
staleFiles,
} = opts;
const visibleFiles = useMemo(
() =>
filterVisibleFiles(allFiles, {
mode,
filePath,
fileRepositoryName,
sourceFilter,
rawPRFiles,
prKey,
}),
[allFiles, mode, filePath, fileRepositoryName, sourceFilter, rawPRFiles, prKey],
);
const repositoryFilter = prKey && sourceFilter === "pr" ? undefined : fileRepositoryName;
const visibleFileRefs = useMemo(() => {
if (mode !== "file" || !filePath) return fileRefs;
const refs = new Map<string, RefObject<HTMLDivElement | null>>();
for (const [key, ref] of fileRefs.entries()) {
const split = splitReviewFileKey(key);
if (split.path !== filePath) continue;
if (repositoryFilter !== undefined && (split.repositoryName || "") !== repositoryFilter) {
continue;
}
refs.set(key, ref);
}
return refs;
}, [mode, filePath, fileRefs, repositoryFilter]);
const reviewedCount = useMemo(
() =>
visibleFiles.reduce((count, file) => {
const key = reviewFileKey(file);
return !staleFiles.has(key) && reviewedFiles.has(key) ? count + 1 : count;
}, 0),
[visibleFiles, reviewedFiles, staleFiles],
);
const totalCount = visibleFiles.length;
const progressPercent = totalCount > 0 ? (reviewedCount / totalCount) * 100 : 0;
return { visibleFiles, visibleFileRefs, reviewedCount, totalCount, progressPercent };
}