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