Rewrite reviewed-finding localStorage hooks.
Dashboard count and report toggle still migrate legacy keys and skip the first persist write.
This commit is contained in:
@@ -0,0 +1,33 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
legacyReviewedFindingsKey,
|
||||||
|
migrateLocalKey,
|
||||||
|
reviewedFindingsKey,
|
||||||
|
} from "@/lib/storage-keys";
|
||||||
|
|
||||||
|
/** Dashboard-only: length of the reviewed-finding id list in localStorage. */
|
||||||
|
export function useReviewedCount(projectId: string): number {
|
||||||
|
const [count, setCount] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
try {
|
||||||
|
const raw = migrateLocalKey(
|
||||||
|
reviewedFindingsKey(projectId),
|
||||||
|
legacyReviewedFindingsKey(projectId),
|
||||||
|
);
|
||||||
|
if (!raw) {
|
||||||
|
setCount(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const parsed: unknown = JSON.parse(raw);
|
||||||
|
setCount(Array.isArray(parsed) ? parsed.length : 0);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("useReviewedCount: could not read reviewed ids", err);
|
||||||
|
setCount(0);
|
||||||
|
}
|
||||||
|
}, [projectId]);
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import type { Finding } from "@/lib/types";
|
||||||
|
import { getFindingKey } from "@/lib/utils";
|
||||||
|
import {
|
||||||
|
legacyReviewedFindingsKey,
|
||||||
|
migrateLocalKey,
|
||||||
|
reviewedFindingsKey,
|
||||||
|
} from "@/lib/storage-keys";
|
||||||
|
|
||||||
|
function readStoredIds(projectId: string): Set<string> {
|
||||||
|
try {
|
||||||
|
const raw = migrateLocalKey(
|
||||||
|
reviewedFindingsKey(projectId),
|
||||||
|
legacyReviewedFindingsKey(projectId),
|
||||||
|
);
|
||||||
|
if (!raw) return new Set();
|
||||||
|
const parsed: unknown = JSON.parse(raw);
|
||||||
|
if (!Array.isArray(parsed)) return new Set();
|
||||||
|
return new Set(parsed.filter((id): id is string => typeof id === "string"));
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("useReviewedFindings: could not read reviewed ids", err);
|
||||||
|
return new Set();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useReviewedFindings(projectId: string, findings: Finding[]) {
|
||||||
|
const liveKeys = useMemo(() => {
|
||||||
|
const keys = new Set<string>();
|
||||||
|
findings.forEach((finding, index) => keys.add(getFindingKey(finding, index)));
|
||||||
|
return keys;
|
||||||
|
}, [findings]);
|
||||||
|
|
||||||
|
const [reviewedIds, setReviewedIds] = useState<Set<string>>(() => {
|
||||||
|
if (typeof window === "undefined") return new Set();
|
||||||
|
return readStoredIds(projectId);
|
||||||
|
});
|
||||||
|
|
||||||
|
const skipFirstWrite = useRef(true);
|
||||||
|
useEffect(() => {
|
||||||
|
if (skipFirstWrite.current) {
|
||||||
|
skipFirstWrite.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
localStorage.setItem(reviewedFindingsKey(projectId), JSON.stringify([...reviewedIds]));
|
||||||
|
}, [projectId, reviewedIds]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (findings.length === 0) return;
|
||||||
|
setReviewedIds((prev) => {
|
||||||
|
let dropped = false;
|
||||||
|
const next = new Set<string>();
|
||||||
|
for (const key of prev) {
|
||||||
|
if (liveKeys.has(key)) next.add(key);
|
||||||
|
else dropped = true;
|
||||||
|
}
|
||||||
|
return dropped ? next : prev;
|
||||||
|
});
|
||||||
|
}, [findings.length, liveKeys]);
|
||||||
|
|
||||||
|
const toggleReviewed = useCallback((key: string) => {
|
||||||
|
setReviewedIds((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(key)) next.delete(key);
|
||||||
|
else next.add(key);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const isReviewed = useCallback((key: string) => reviewedIds.has(key), [reviewedIds]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
reviewedIds,
|
||||||
|
toggleReviewed,
|
||||||
|
isReviewed,
|
||||||
|
reviewedCount: reviewedIds.size,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
"""Reviewed-finding localStorage hooks live under periscope/src."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
SRC = ROOT / "periscope" / "src" / "frontend" / "src"
|
||||||
|
|
||||||
|
|
||||||
|
def test_reviewed_hooks_are_src():
|
||||||
|
for rel in ("hooks/use-reviewed-count.ts", "hooks/use-reviewed-findings.ts"):
|
||||||
|
text = (SRC / rel).read_text(encoding="utf-8")
|
||||||
|
assert "Native Periscope overlay" not in text[:400]
|
||||||
|
|
||||||
|
|
||||||
|
def test_reviewed_count_reads_migrated_key():
|
||||||
|
text = (SRC / "hooks/use-reviewed-count.ts").read_text(encoding="utf-8")
|
||||||
|
assert "export function useReviewedCount" in text
|
||||||
|
assert "reviewedFindingsKey" in text
|
||||||
|
assert "legacyReviewedFindingsKey" in text
|
||||||
|
assert "migrateLocalKey" in text
|
||||||
|
|
||||||
|
|
||||||
|
def test_reviewed_findings_return_shape():
|
||||||
|
text = (SRC / "hooks/use-reviewed-findings.ts").read_text(encoding="utf-8")
|
||||||
|
assert "export function useReviewedFindings" in text
|
||||||
|
assert "toggleReviewed" in text
|
||||||
|
assert "isReviewed" in text
|
||||||
|
assert "reviewedCount" in text
|
||||||
|
assert "reviewedIds" in text
|
||||||
|
assert "getFindingKey" in text
|
||||||
|
assert "skipFirstWrite" in text or "hydrated" in text
|
||||||
Reference in New Issue
Block a user