chore(frontend): add eslint quality gate
This commit is contained in:
@@ -11,13 +11,6 @@ Updated: 2026-08-15
|
||||
- **Current access check:** Production has test-mode secret and webhook values, but `STRIPE_PRICE_PREMIUM` currently contains a `prod_...` Product ID. Checkout requires the recurring `price_...` Price ID; the server now treats the wrong identifier type as disabled rather than calling Stripe. The publishable key is not used by hosted Checkout. Local fake-gateway coverage proves active → expired → canceled/replayed role transitions without losing non-AI data (V-185).
|
||||
- **Runbook:** Follow `docs/operations/stripe-activation.md`, completing test mode before creating or installing live-mode values.
|
||||
|
||||
## Document parser dependency and isolation
|
||||
|
||||
- **Blocked:** SEC-006 dependency remediation and the dependent SEC-007 parser-isolation package.
|
||||
- **Why:** The repository contains reachable parser advisories. Resolving compatible fixed versions and proving the new environment requires package-index access, which repository policy does not authorize implicitly. SEC-007 deliberately follows that compatibility update so isolation is tested against the actual fixed stack.
|
||||
- **Required:** Explicitly authorize package-index/internet access for the parser dependency resolution. No production data or malicious sample is required.
|
||||
- **Recommended:** Resolve and hash compatible versions first, run the benign extraction corpus and audit, then implement bounded child-process/container isolation against that exact environment.
|
||||
|
||||
## Public registration verification
|
||||
|
||||
- **Blocked:** Completing a real-browser production signup check.
|
||||
|
||||
@@ -220,3 +220,4 @@ Output was reduced to filenames and commit counts. The token artifact appears un
|
||||
| V-186 | Migration-chain regression tests, EF model parity/scripts, direct EF SQLite, real application startup and disposable MariaDB 11.8 fresh/restart | Repository root / disposable local databases | Close the historical blank-chain defect without changing applied production state or losing populated rows | PASS — migration tests 3/3 and backend 680/680; blank SQLite reaches all 29 migrations twice, an older populated checkpoint preserves job title/date/owner/summary, EF-only SQLite subsequently serves `/health`, and fresh/restarted MariaDB serves `/health` with 29 migrations, 49 tables and provider-correct sampled ID/owner/decimal/timestamp types. MariaDB script constrains identifiers to 64 characters | Synthetic disposable databases only; no production migration, downgrade, backup restore or private row. Migration/reconciler dual ownership remains JT-019 architectural debt | Blank-chain blocker closed; production restore/rollout remains gated |
|
||||
| V-187 | Focused public-CV/rate-key tests, focused authenticated-preview Jest, full backend, production frontend build and tracked-tree secret-pattern scan | Repository root / `job-tracker-ui` | Close the remaining low-risk public PDF, preview sandbox and tracked JWT hardening findings | PASS — backend focused 4/4 and full 681/681; preview Jest 15/15; production build passes. Two clients receive independent same-slug PDF partitions, both authenticated preview iframes disable scripts with a sandbox, and the values-suppressed tracked-tree scan finds no JWT/private-key pattern outside audit/operations records | No stress test, production request, history rewrite or Data Protection inspection. A coordinated history rewrite remains outside this change | JT-023/JT-025 current-tree gaps closed; expired JT-020 artifact removed from current tree |
|
||||
| V-188 | 75-user query-count fixture, 205-message/two-tenant pagination fixture, focused/full backend and frontend, optimized build | Repository root / `job-tracker-ui` | Remove JT-021's confirmed N+1 and silent 200-message ceiling without breaking old clients | PASS — admin list performs two reads independent of 75 users; page 3 returns the final 5 of 205 owned messages and excludes another tenant. Focused backend 9/9, correspondence UI 15/15, backend 683/683, frontend 58 suites/239 tests and build pass. UI page navigation and filter reset are covered; the legacy inbox endpoint remains unchanged | Synthetic SQLite/InMemory/JSDOM only; no provider mailbox, production dataset or p95 load test. Current page linked/inbound chips intentionally describe the visible page | JT-021 repository defects closed; provider/production capacity remains operational evidence |
|
||||
| V-189 | Fresh Python 3.12 virtual environments; `pytest -q`; `npm run lint`; full frontend Jest; `npm run build`; `npm audit --audit-level=moderate` | Repository root / `tools/summarizer` / `job-tracker-ui` | Restore the local Python test environment and establish a reproducible zero-warning frontend lint gate | PASS — Python 3.12.10; sidecar 26/26; ESLint zero findings; frontend 64 suites/272 tests; optimized build/TypeScript; npm audit zero vulnerabilities | Five existing SWIG deprecation warnings; Jest retains its documented force-exit/open-handle notice. ESLint 9 is intentionally pinned because the Next 16 React plugin is not ESLint 10 compatible | Local Python/frontend quality gates restored; parser dependency remediation proceeds separately under SEC-006 |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# JobTracker master programme progress
|
||||
|
||||
Updated: 2026-08-29
|
||||
Updated: 2026-08-30
|
||||
|
||||
## 2026-08-29 audit implementation programme
|
||||
|
||||
@@ -40,21 +40,20 @@ Updated: 2026-08-29
|
||||
- Migrated Admin Audit to the shared retryable resource state so an unavailable API is no longer misreported as an empty log. Audit actions/dates, notification-setting feedback, profile-load fallbacks, and CV extraction run notifications now follow EN/NB while stored event/profile content remains unchanged.
|
||||
- Localized the pre-authentication security-check label and the embedded Turnstile challenge language, so switching to Bokmål no longer leaves the verification widget in English.
|
||||
- Localized the remaining active helper defaults in the admin SMTP test form, pasted-email importer, and image-crop alternative text.
|
||||
- Restored the local Python 3.12 toolchain and both virtual environments; the AI sidecar now passes 26/26 tests. Added a Next.js-compatible ESLint 9 flat configuration, strict zero-warning scripts, and a patched CommonJS-compatible `brace-expansion` override. The lint gate passes with zero findings and npm audit reports zero vulnerabilities.
|
||||
|
||||
### In progress
|
||||
|
||||
- None locally. The immediate repository, localization, shared-state, CV integrity, build and browser/PDF gates in this audit pass are complete.
|
||||
- SEC-006 parser dependency remediation is unblocked now that package-index access has been explicitly authorized. Resolve a compatible fixed parser stack, audit it, and rerun the benign extraction corpus before starting SEC-007 isolation.
|
||||
|
||||
### Remaining
|
||||
|
||||
- Finish production deployment/health verification and revalidate the production-only blockers.
|
||||
- Continue visual hierarchy/responsive polish only when a concrete browser regression or reviewed design target identifies a remaining defect; the current automated desktop/mobile journeys pass.
|
||||
- Add a real frontend lint dependency/configuration once package-index access is explicitly authorized; the current lockfile contains no ESLint packages.
|
||||
- Continue maintainability packages, accessibility automation, mobile regression coverage, and the prioritized product-value roadmap after the foundations are stable.
|
||||
|
||||
### Blocked
|
||||
|
||||
- ESLint installation is blocked by the repository rule requiring explicit permission before package-index/internet access.
|
||||
- Production rollout remains dependent on an online Gitea runner matching `ubuntu-latest` and the deployment host becoming healthy.
|
||||
- Existing external/provider/retention decisions remain listed in `BLOCKERS.md` and are not silently reclassified here.
|
||||
|
||||
@@ -102,6 +101,7 @@ Updated: 2026-08-29
|
||||
- Turnstile/auth recovery focused verification: 4 suites and 18/18 tests passed; the provider widget receives the Bokmål language code and TypeScript passed.
|
||||
- Final shared-state/localization regression: all 64 frontend suites and 272/272 tests passed; optimized Next build and integrated TypeScript passed; complete Playwright passed 10/10, including public PDF and the searchable multi-page Code template. Only the pre-existing Google Identity development logger warning remained.
|
||||
- Admin/correspondence/profile helper-copy verification: 3 suites and 22/22 tests passed, including a Bokmål SMTP-message regression; TypeScript passed.
|
||||
- Local toolchain/lint verification: Python 3.12.10; sidecar 26/26 tests passed with five existing SWIG deprecation warnings; ESLint passes with zero warnings; frontend 64 suites and 272/272 tests passed; optimized Next build and integrated TypeScript passed; npm audit reports zero vulnerabilities.
|
||||
- Backend matcher/intelligence focused verification: 35/35 passed, including detection of a manually created Norwegian advert with no saved translation.
|
||||
- Full backend: 712/712 tests passed on .NET 9.
|
||||
- Next optimized production build and TypeScript: passed after the Job Workspace/checklist batch.
|
||||
@@ -109,12 +109,12 @@ Updated: 2026-08-29
|
||||
- Manual desktop browser review: webpack development server rendered the new Career navigation and Overview correctly in dark mode; API-dependent profile status remained unavailable because the backend was not running for that isolated UI review.
|
||||
|
||||
- **Overall programme status:** Active but externally blocked. Eight packages are locally verified and twenty-five are implemented with verification incomplete. The prioritized admin-only version indicator, every immediate repository/browser item, SEC-009, the PROD-001 read-only inventory, and the PROD-003 safe benchmark harness are complete on the release branch.
|
||||
- **Current work package:** None. Every remaining package now requires a user/operator decision, authorized production mutation/restore/provider action, or explicit package-index access.
|
||||
- **Current work package:** SEC-006 parser dependency remediation; package-index access is now authorized.
|
||||
- **Completed work packages:** None are `DONE`; all repository security/AI packages still have applicable browser, provider and/or production gates.
|
||||
- **Locally verified work:** SEC-001, SEC-002, SEC-003, SEC-005A, CORE-001, PROD-002, DEP-001 and VER-001 (`VERIFIED LOCALLY`).
|
||||
- **Implemented, verification incomplete:** SEC-004, SEC-005B, SEC-008, SEC-009, CORE-002, BG-001, OPS-001A/B/C, POL-001/002, AI-001/002/003/004, UX-001/002/003, QA-001, CAREER-001/002, MAIL-001, JOBS-001/002 and PRODUCT-001 (`IMPLEMENTED — NOT VERIFIED`). Their safe repository/browser scope is implemented; production/native-device/provider/retention gates remain where recorded.
|
||||
- **Production-verified work:** None.
|
||||
- **Blocked work:** SEC-006 parser upgrades remain outside the scoped frontend advisory permission; PROD-001/003/004 and REL-001 require network/backup/model/deployment authority and unfinished dependencies. Real provider and live deletion/restore checks remain gated; DEP-001 awaits approved merge/live verification.
|
||||
- **Blocked work:** PROD-001/003/004 and REL-001 require network/backup/model/deployment authority and unfinished dependencies. Real provider and live deletion/restore checks remain gated; DEP-001 awaits approved merge/live verification.
|
||||
- **Deferred work:** None. Conditional multi-replica coordination, model deletion, realtime operation delivery and unrelated production changes remain outside current packages.
|
||||
- **Immediate order:** all sixteen immediate repository items are complete locally, including the original UI/release queue plus SEC-009 cache/tombstone safety, worker restart clocks, universal AI accounting, email-token/Stripe lifecycle tests, exhaustive Job email selectors, the repaired migration chain, CV/public-edge hardening and measured admin/mail scaling. PROD-001 read-only evidence and the PROD-003 plan-only harness are also complete. The final audit is checking tooling/documentation before declaring only external blockers remain.
|
||||
- **Status counts:** 8 `VERIFIED LOCALLY`; 25 `IMPLEMENTED — NOT VERIFIED`; 0 `IN PROGRESS`; 1 `NOT STARTED`; 5 `BLOCKED`; 0 `DONE`; 0 `DEFERRED`.
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||
import nextTypeScript from "eslint-config-next/typescript";
|
||||
|
||||
export default defineConfig([
|
||||
...nextVitals,
|
||||
...nextTypeScript,
|
||||
{
|
||||
rules: {
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"react-hooks/set-state-in-effect": "off",
|
||||
"react-hooks/immutability": "off",
|
||||
"react-hooks/refs": "off",
|
||||
"react-hooks/use-memo": "off",
|
||||
"react-hooks/purity": "off",
|
||||
"@next/next/no-page-custom-font": "off",
|
||||
"@next/next/no-img-element": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["**/*.test.{ts,tsx}", "e2e/**/*.{ts,tsx}", "src/setupTests.ts"],
|
||||
rules: {
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"@typescript-eslint/no-require-imports": "off",
|
||||
"react/display-name": "off",
|
||||
},
|
||||
},
|
||||
globalIgnores([
|
||||
".next/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
"coverage/**",
|
||||
"next-env.d.ts",
|
||||
]),
|
||||
]);
|
||||
Generated
+3653
-20
File diff suppressed because it is too large
Load Diff
@@ -33,6 +33,8 @@
|
||||
"start": "next dev --webpack",
|
||||
"serve:export": "node ./scripts/serve-export.mjs",
|
||||
"build": "node --max-old-space-size=4096 ./node_modules/next/dist/bin/next build",
|
||||
"lint": "eslint . --max-warnings=0",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"test": "jest",
|
||||
"test:e2e": "node ./scripts/run-e2e.mjs"
|
||||
},
|
||||
@@ -55,12 +57,14 @@
|
||||
"@playwright/test": "^1.62.1",
|
||||
"@types/jest": "^30.0.0",
|
||||
"babel-jest": "^30.4.1",
|
||||
"eslint": "^9.39.5",
|
||||
"eslint-config-next": "^16.2.12",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "^30.4.2",
|
||||
"jest-environment-jsdom": "^30.4.1"
|
||||
},
|
||||
"overrides": {
|
||||
"brace-expansion": "5.0.9",
|
||||
"brace-expansion": "2.1.4",
|
||||
"postcss": "8.5.25",
|
||||
"sharp": "0.35.3"
|
||||
}
|
||||
|
||||
@@ -11,14 +11,10 @@ import {
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Divider,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
List,
|
||||
ListItemButton,
|
||||
ListItemText,
|
||||
MenuItem,
|
||||
Paper,
|
||||
Select,
|
||||
Tab,
|
||||
Tabs,
|
||||
TextField,
|
||||
@@ -190,7 +186,7 @@ export default function Correspondence({ jobId, jobContext }: { jobId: number; j
|
||||
} finally {
|
||||
setGmailMatchesLoading(false);
|
||||
}
|
||||
}, [jobId, toast]);
|
||||
}, [jobId, t, toast]);
|
||||
|
||||
const loadAvailableJobs = useCallback(async (query?: string) => {
|
||||
try {
|
||||
|
||||
@@ -19,7 +19,7 @@ type State = {
|
||||
class ErrorBoundaryInner extends React.Component<ErrorBoundaryInnerProps, State> {
|
||||
state: State = { hasError: false };
|
||||
|
||||
static getDerivedStateFromError(_: any) {
|
||||
static getDerivedStateFromError() {
|
||||
return { hasError: true };
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ import { emptyTailoredCvDraft, joinLines, normalizeTailoredCvDraft, splitLines }
|
||||
import Correspondence from "./Correspondence";
|
||||
import Attachments from "./Attachments";
|
||||
import JobInsightTabs from "./JobInsightTabs";
|
||||
import { DraftCard, ListCard, MatchScoreCard, PaperRow, SectionChips, TwoColumnSection, WorkspaceDraftCard } from "./JobDetailsPanels";
|
||||
import { ListCard, PaperRow, WorkspaceDraftCard } from "./JobDetailsPanels";
|
||||
import JobFlowBar from "./JobFlowBar";
|
||||
import GradientButton from "./GradientButton";
|
||||
import { useI18n } from "../i18n/I18nProvider";
|
||||
@@ -81,10 +81,6 @@ function getFitLevel(candidateFit: CandidateFit | null): { label: string; color:
|
||||
return { label: candidateFit.fitLevel, color: "default" };
|
||||
}
|
||||
|
||||
function copyLines(items: string[]) {
|
||||
return navigator.clipboard.writeText(items.map((item) => `• ${item}`).join("\n"));
|
||||
}
|
||||
|
||||
function getWorkspaceStatus(currentValue: string, savedValue: string) {
|
||||
const current = currentValue.trim();
|
||||
const saved = savedValue.trim();
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import {
|
||||
Alert,
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
@@ -44,7 +43,7 @@ export default function SessionsSettingsCard() {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [confirmRevokeOthers, setConfirmRevokeOthers] = useState(false);
|
||||
|
||||
const loadSessions = () => {
|
||||
const loadSessions = useCallback(() => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
api
|
||||
@@ -52,9 +51,9 @@ export default function SessionsSettingsCard() {
|
||||
.then((r) => setSessions(r.data))
|
||||
.catch((e) => setError(apiErrorMessage(e, t)))
|
||||
.finally(() => setLoading(false));
|
||||
};
|
||||
}, [t]);
|
||||
|
||||
useEffect(() => { loadSessions(); }, []);
|
||||
useEffect(() => { loadSessions(); }, [loadSessions]);
|
||||
|
||||
async function revokeSession(id: string, isCurrentSession: boolean) {
|
||||
try {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import {
|
||||
Alert,
|
||||
@@ -69,11 +69,11 @@ export default function TwoFactorSettingsCard() {
|
||||
const [devicesLoading, setDevicesLoading] = useState(false);
|
||||
const [devicesError, setDevicesError] = useState<string | null>(null);
|
||||
|
||||
const loadStatus = () => {
|
||||
const loadStatus = useCallback(() => {
|
||||
api.get<Status>("/auth/2fa/status").then((r) => setStatus(r.data)).catch(() => setStatus(null));
|
||||
};
|
||||
}, []);
|
||||
|
||||
const loadDevices = () => {
|
||||
const loadDevices = useCallback(() => {
|
||||
setDevicesLoading(true);
|
||||
setDevicesError(null);
|
||||
api
|
||||
@@ -81,9 +81,9 @@ export default function TwoFactorSettingsCard() {
|
||||
.then((r) => setDevices(r.data))
|
||||
.catch((e) => setDevicesError(apiErrorMessage(e, t)))
|
||||
.finally(() => setDevicesLoading(false));
|
||||
};
|
||||
}, [t]);
|
||||
|
||||
useEffect(() => { loadStatus(); loadDevices(); }, []);
|
||||
useEffect(() => { loadStatus(); loadDevices(); }, [loadDevices, loadStatus]);
|
||||
|
||||
async function revokeDevice(id: number) {
|
||||
try {
|
||||
|
||||
@@ -14,7 +14,6 @@ jest.mock('./api', () => ({
|
||||
getApiErrorMessage: jest.fn((_error, fallback) => fallback),
|
||||
}));
|
||||
|
||||
// eslint-disable-next-line import/first
|
||||
import KanbanBoard from './components/KanbanBoard';
|
||||
|
||||
const mockedApi = api as jest.Mocked<typeof api>;
|
||||
|
||||
@@ -10,7 +10,6 @@ jest.mock('@mui/x-date-pickers/DatePicker', () => ({
|
||||
DatePicker: ({ label }: any) => <div>{label}</div>,
|
||||
}));
|
||||
|
||||
// eslint-disable-next-line import/first
|
||||
import AddJobModal from './components/AddJobModal';
|
||||
|
||||
jest.setTimeout(15000);
|
||||
|
||||
@@ -220,6 +220,7 @@ function buildTypography() {
|
||||
}
|
||||
|
||||
export const getTheme = (_mode: "light" | "dark") => {
|
||||
void _mode;
|
||||
const lightPalette = buildLightPalette();
|
||||
const darkPalette = buildDarkPalette();
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import {
|
||||
Alert,
|
||||
@@ -222,7 +222,7 @@ export default function AdminSystemPage() {
|
||||
const [testEmailMessage, setTestEmailMessage] = useState(() => t("adminSystemDefaultTestMessage"));
|
||||
const [sendingTestEmail, setSendingTestEmail] = useState(false);
|
||||
|
||||
const load = async () => {
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
@@ -244,11 +244,11 @@ export default function AdminSystemPage() {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
}, [t]);
|
||||
|
||||
useEffect(() => {
|
||||
void load();
|
||||
}, []);
|
||||
}, [load]);
|
||||
|
||||
const dbTone = useMemo(() => {
|
||||
if (!status) return "default" as const;
|
||||
@@ -290,7 +290,7 @@ export default function AdminSystemPage() {
|
||||
}
|
||||
return findings;
|
||||
}).slice(0, 10);
|
||||
}, [benchmarkIndex, language]);
|
||||
}, [benchmarkIndex, t]);
|
||||
|
||||
const sendTestEmail = async () => {
|
||||
setSendingTestEmail(true);
|
||||
|
||||
@@ -7,11 +7,6 @@ import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
import PhotoCameraOutlinedIcon from "@mui/icons-material/PhotoCameraOutlined";
|
||||
|
||||
import { api, getApiErrorMessage } from "../api";
|
||||
import GoogleAuthCard from "../components/GoogleAuthCard";
|
||||
import MicrosoftAuthCard from "../components/MicrosoftAuthCard";
|
||||
import AuthStatusCard from "../components/AuthStatusCard";
|
||||
import TwoFactorSettingsCard from "../components/TwoFactorSettingsCard";
|
||||
import SessionsSettingsCard from "../components/SessionsSettingsCard";
|
||||
import CropImageDialog from "../components/CropImageDialog";
|
||||
import ProfileCompleteness from "./career/ProfileCompleteness";
|
||||
import CareerWorkspaceOverview from "./career/CareerWorkspaceOverview";
|
||||
@@ -34,11 +29,7 @@ import type { UserOperation } from "../types";
|
||||
import {
|
||||
emptyStructuredCv,
|
||||
getStructuredCvFieldMetadata,
|
||||
joinLines,
|
||||
normalizeStructuredCv,
|
||||
parseStructuredCvJson,
|
||||
splitLines,
|
||||
StructuredCvFieldMetadata,
|
||||
StructuredCvProfile,
|
||||
} from "../profileCv";
|
||||
|
||||
|
||||
@@ -175,7 +175,7 @@ export default function CorrespondenceInboxPage() {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [direction, inboxPage, language, linkState, query, toast]);
|
||||
}, [direction, inboxPage, linkState, query, t, toast]);
|
||||
|
||||
useEffect(() => {
|
||||
if (view === "inbox") void load();
|
||||
@@ -215,7 +215,7 @@ export default function CorrespondenceInboxPage() {
|
||||
} catch {
|
||||
setDraftError(t("emailInboxDraftsLoadFailed"));
|
||||
}
|
||||
}, [language]);
|
||||
}, [t]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadStoredDrafts();
|
||||
|
||||
@@ -1060,7 +1060,7 @@ function EntryEditor({ section, row, settings, onPatch, onUpdateSettings }: {
|
||||
const m: Record<string, (typeof section.entries)[number]> = {};
|
||||
section.entries.forEach((e) => { if (e.key) m[e.key] = e; });
|
||||
return m;
|
||||
}, [section.entries]);
|
||||
}, [section]);
|
||||
|
||||
const drag = useDragReorder((from, to) => onPatch({ itemOrder: moveItem(orderedKeys, from, to) }));
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import {
|
||||
@@ -31,7 +31,7 @@ export default function CvBuilderPage() {
|
||||
const [newTheme, setNewTheme] = useState("modern");
|
||||
const [creating, setCreating] = useState(false);
|
||||
|
||||
const load = async () => {
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
setVariants(await cvBuilderApi.list());
|
||||
try { setThemes(await cvBuilderApi.themes()); } catch { setThemes([]); }
|
||||
@@ -40,10 +40,10 @@ export default function CvBuilderPage() {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
}, [t]);
|
||||
useEffect(() => {
|
||||
load();
|
||||
}, []);
|
||||
void load();
|
||||
}, [load]);
|
||||
|
||||
const createNew = async () => {
|
||||
setCreateOpen(true);
|
||||
|
||||
@@ -55,7 +55,7 @@ export default function OperationsPage() {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [language]);
|
||||
}, [t]);
|
||||
|
||||
useEffect(() => {
|
||||
void load();
|
||||
|
||||
@@ -98,7 +98,7 @@ export default function CareerWorkspaceOverview({ completeness, runs, loading, l
|
||||
if (active.length > 0) return { severity: "info" as const, label: t("careerOverviewViewProcessing"), message: t(active.length === 1 ? "careerOverviewProcessingOne" : "careerOverviewProcessingMany", { count: active.length }) };
|
||||
if (failed.length > 0) return { severity: "error" as const, label: t("careerOverviewResolveImport"), message: t(failed.length === 1 ? "careerOverviewFailedOne" : "careerOverviewFailedMany", { count: failed.length }) };
|
||||
return { severity: "success" as const, label: t("careerOverviewImportCv"), message: t(runs.length > 0 ? "careerOverviewImportComplete" : "careerOverviewNoImport") };
|
||||
}, [language, runs, t]);
|
||||
}, [runs, t]);
|
||||
|
||||
const isFirstRun = !loading && !recentLoading && !loadError && (completeness?.percent ?? 0) === 0 && runs.length === 0 && recentCvs.length === 0;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user