chore(ci): clear the lint debt and make lint blocking (#79)
CI ran lint with continue-on-error because 23 errors predated it. All are fixed, so the flag is gone and a typecheck step is added beside it — nothing else in CI compiles the test files, so a type error in a fixture could only ever be found on a developer's machine. Real fixes, no blanket disables and no suppressions: - react-hooks/set-state-in-effect (6): each was "reset or seed state when an input changes", so each becomes a state adjustment during render behind a previous-value guard — the documented React pattern for it. Two were also latent bugs. transactions/page.tsx depended on `others`, rebuilt by .filter() every render, so the effect already ran every render, one commit late. shared/page.tsx depended on the `tagIds` array identity, so the row window was reset more often than "changing any filter" claims — including after "Show more", the one interaction it must not undo; the key now joins by value. csv-import-modal reads localStorage once at mount, which is a lazy useState initializer, not an effect (SSR-guarded, though the modal is mounted from client state and never prerenders). - react-hooks/static-components (3): SortIcon was declared inside SharedPage, making it a new component type on every render. Hoisted to module scope with the sort state passed as props. - @typescript-eslint/no-explicit-any (9): the dynamically imported route handlers in two integration tests. Typed as their real signatures with one `as unknown as NextRequest` cast at the fixture boundary, so the call sites and response bodies get checked instead of opted out. - react/no-unescaped-entities (5): exact-equivalent entities (', "), so no rendered text changes. Also fixes the one pre-existing tsc error (unit/order-ingestion.test.ts:511): `options` is optional, and `?? 0` keeps the assertion failing on an absent list rather than silencing it with a non-null bang. 9 warnings remain (no-unused-vars, exhaustive-deps). eslint exits 0 on warnings, so lint blocks on errors only — deliberately, since none of the remaining warnings should stop a push.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState } from "react";
|
||||
import { useParticipants, useSetSplits, useTransactionSplits, useBulkAction, useCreateRule, useCurrentUser } from "@/lib/hooks";
|
||||
|
||||
interface Split {
|
||||
@@ -33,22 +33,32 @@ export function SplitModal({ transactionId, transactionIds, amount, description,
|
||||
const [saveAsRule, setSaveAsRule] = useState(false);
|
||||
const [ruleSaved, setRuleSaved] = useState(false);
|
||||
|
||||
// Initialise: bulk always defaults to 100% Me; single loads existing splits
|
||||
useEffect(() => {
|
||||
if (!participants || participants.length === 0 || !currentUser) return;
|
||||
if (isBulk) {
|
||||
setSplitsState([{ participant_id: currentUser.id, share_percent: 100 }]);
|
||||
} else if (existingSplits && existingSplits.length > 0) {
|
||||
setSplitsState(
|
||||
existingSplits.map((s: { participant_id: number; share_percent: number }) => ({
|
||||
participant_id: s.participant_id,
|
||||
share_percent: Number(s.share_percent),
|
||||
}))
|
||||
);
|
||||
} else {
|
||||
setSplitsState([{ participant_id: currentUser.id, share_percent: 100 }]);
|
||||
}
|
||||
}, [existingSplits, participants, isBulk, currentUser]);
|
||||
// Initialise: bulk always defaults to 100% Me; single loads existing splits.
|
||||
//
|
||||
// Seeded during render behind a previous-value guard rather than in an
|
||||
// effect. Both queries resolve after the first paint, so as an effect this
|
||||
// rendered an empty split table and then replaced it — and, worse, it re-ran
|
||||
// on every identity change of `participants`/`existingSplits`, which would
|
||||
// discard edits in progress if either query refetched while the modal was
|
||||
// open. Keying on the inputs that actually define the seed fixes both.
|
||||
const seedKey =
|
||||
!participants || participants.length === 0 || !currentUser
|
||||
? null
|
||||
: `${isBulk ? "bulk" : singleId}:${currentUser.id}:${
|
||||
existingSplits ? JSON.stringify(existingSplits) : "none"
|
||||
}`;
|
||||
const [seededFrom, setSeededFrom] = useState<string | null>(null);
|
||||
if (seedKey !== null && seedKey !== seededFrom) {
|
||||
setSeededFrom(seedKey);
|
||||
setSplitsState(
|
||||
!isBulk && existingSplits && existingSplits.length > 0
|
||||
? existingSplits.map((s: { participant_id: number; share_percent: number }) => ({
|
||||
participant_id: s.participant_id,
|
||||
share_percent: Number(s.share_percent),
|
||||
}))
|
||||
: [{ participant_id: currentUser!.id, share_percent: 100 }]
|
||||
);
|
||||
}
|
||||
|
||||
const total = splits.reduce((sum, s) => sum + s.share_percent, 0);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user