Merge pull request #235 from amal66/olp-pr/lint-burndown

[Lint 04] fix: burn down frontend eslint errors; make CI lint blocking
This commit is contained in:
Will Chen 2026-07-22 16:33:52 +08:00 committed by GitHub
commit 7d2ba4b87f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 27 additions and 12 deletions

View file

@ -7,7 +7,7 @@
# on a tree without those pieces the test steps no-op and the build still
# gates the merge. Beyond the fork version this also builds the frontend
# (placeholder NEXT_PUBLIC_* env — verified sufficient for `next build`) and
# runs eslint as an advisory step (see the comment on that step).
# runs eslint as a blocking gate (the error backlog is at zero).
name: CI
on:
@ -61,11 +61,9 @@ jobs:
# harness PR merges); runs the suite once it exists.
- run: npm test --if-present
# Advisory only for now: main currently carries 23 eslint errors, so a
# blocking gate would turn every PR red on inherited debt. Flip this to
# blocking (remove continue-on-error) once the backlog is burned down.
# Blocking gate: the eslint error backlog was burned down in this PR
# (0 errors; warnings do not fail the step), so any new error fails CI.
- run: npm run lint
continue-on-error: true
# Production build. NEXT_PUBLIC_* values are inlined at build time and
# only need to be well-formed here — nothing is contacted during build.

View file

@ -242,6 +242,7 @@ export function AskInputPopup({
};
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect -- auto-submit when every question is answered; submit() sets state as part of the side effect
if (canSubmit) submit();
});

View file

@ -216,6 +216,7 @@ export function CaseLawPanel({
useEffect(() => {
if (tab.opinions?.length) {
// eslint-disable-next-line react-hooks/set-state-in-effect -- sync path of an async fetch effect: serve prop/cache data without a loading flash
setOpinions(tab.opinions);
setLoading(false);
setError(null);
@ -269,10 +270,12 @@ export function CaseLawPanel({
orderOpinions(opinions).find(
({ opinion }) => typeof opinion.opinionId === "number",
)?.opinion.opinionId ?? null;
// eslint-disable-next-line react-hooks/set-state-in-effect -- reset active opinion after opinions load
setActiveOpinionId(firstOpinionId);
}, [opinions]);
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect -- sync quote list when the tab prop changes
setRelevantQuotes(tab.quotes ?? []);
}, [tab.quotes]);
@ -321,6 +324,7 @@ export function CaseLawPanel({
);
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect -- reset quote selection when the quote set changes
setQuoteIndexState({ cacheKey: quoteCacheKey, index: 0 });
const firstQuote = relevantQuotes[0];
setActiveQuoteKey(firstQuote ? relevantQuoteKey(firstQuote, 0) : null);

View file

@ -83,6 +83,7 @@ export function ChatView({
const panelCloseTimerRef = useRef<number | null>(null);
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect -- reset per-chat UI state when switching chats
setHiddenAskInputKeys(new Set());
}, [chatId]);
@ -519,6 +520,7 @@ export function ChatView({
const c = messagesContainerRef.current;
if (!c) return;
c.addEventListener("scroll", updateScrollButton);
// eslint-disable-next-line react-hooks/set-state-in-effect -- initial scroll-button state must be measured from the live DOM
updateScrollButton();
return () => c.removeEventListener("scroll", updateScrollButton);
}, [messages, updateScrollButton]);
@ -553,6 +555,7 @@ export function ChatView({
useEffect(() => {
if (messages.length === 0) {
hasScrolledRef.current = false;
// eslint-disable-next-line react-hooks/set-state-in-effect -- hide messages until scroll position is restored to avoid a visible jump
setMessagesVisible(false);
} else if (!hasScrolledRef.current) {
const userMsgCount = messages.filter(
@ -682,8 +685,8 @@ export function ChatView({
{msg.role === "user" ? (
<UserMessage
content={msg.content ?? ""}
files={(msg as any).files}
workflow={(msg as any).workflow}
files={msg.files}
workflow={msg.workflow}
/>
) : (
<AssistantMessage
@ -692,11 +695,11 @@ export function ChatView({
i === messages.length - 1 &&
isResponseLoading
}
isError={!!(msg as any).error}
isError={!!msg.error}
errorMessage={
typeof (msg as any)
.error === "string"
? (msg as any).error
typeof msg.error ===
"string"
? msg.error
: undefined
}
citations={msg.citations}

View file

@ -47,6 +47,7 @@ export function CitationQuotesHeader({
useEffect(() => {
if (!hasMultipleQuotes && viewMode === "list") {
// eslint-disable-next-line react-hooks/set-state-in-effect -- collapse list view when quotes drop to a single item
setViewMode("single");
}
}, [hasMultipleQuotes, viewMode]);

View file

@ -29,6 +29,7 @@ export function PreResponseWrapper({
useEffect(() => {
if (forceOpen) {
// eslint-disable-next-line react-hooks/set-state-in-effect -- streaming open/minimize latch (see comment above)
setIsOpen(true);
return;
}

View file

@ -61,6 +61,7 @@ export function Modal({
// Portals can't render during SSR, so a keep-mounted modal only renders
// (hidden) after the first client mount.
const [hasMounted, setHasMounted] = useState(false);
// eslint-disable-next-line react-hooks/set-state-in-effect -- SSR portal gate: must flip after first client mount
useEffect(() => setHasMounted(true), []);
const hasHeader = breadcrumbs?.length;
const hasFooter =

View file

@ -21,6 +21,7 @@ export function MfaLoginGate({ children }: { children: ReactNode }) {
useEffect(() => {
if (!user) {
// eslint-disable-next-line react-hooks/set-state-in-effect -- sync fast paths of the async MFA check effect
setGateState("idle");
return;
}
@ -64,6 +65,7 @@ export function MfaLoginGate({ children }: { children: ReactNode }) {
if (gateState === "required" && !isVerifyPage) {
if (hasRecentMfaVerification()) {
// eslint-disable-next-line react-hooks/set-state-in-effect -- clear gate when a recent MFA verification exists instead of redirecting
setGateState("verified");
return;
}

View file

@ -164,6 +164,7 @@ function TRResponseStatus({ isActive }: { isActive: boolean }) {
useEffect(() => {
if (wasActiveRef.current && !isActive) {
// eslint-disable-next-line react-hooks/set-state-in-effect -- timed 'Done' flash on the active->idle transition
setShowDone(true);
setDoneVisible(true);
const t = setTimeout(() => setDoneVisible(false), 1500);

View file

@ -73,6 +73,7 @@ export function ChatHistoryProvider({ children }: { children: ReactNode }) {
useEffect(() => {
if (!user) {
// eslint-disable-next-line react-hooks/set-state-in-effect -- clear chat state on logout inside the effect that loads chats
setChats([]);
setChatLimit(INITIAL_CHAT_LIMIT);
setHasMoreChats(false);

View file

@ -49,6 +49,7 @@ export function useFetchDocxBytes(
useEffect(() => {
if (!documentId) {
// eslint-disable-next-line react-hooks/set-state-in-effect -- clear stale bytes when documentId is removed, within the fetch effect
setBytes(null);
setDownloadUrl(null);
return;

View file

@ -16,6 +16,7 @@ export function useSelectedModel(): [string, (id: string) => void] {
const [model, setModelState] = useState<string>(DEFAULT_MODEL_ID);
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect -- hydration-safe localStorage read; SSR must render the default model
setModelState(readStored());
}, []);

View file

@ -228,7 +228,7 @@ export default function SupportPage() {
{/* Email Display (if logged in) */}
{user?.email && (
<div className="text-sm text-gray-500">
We'll respond to:{" "}
We&apos;ll respond to:{" "}
<span className="font-medium">
{user.email}
</span>