Add useAppSettings hook for parsing app config day ranges

This commit is contained in:
2026-04-17 00:17:55 +03:00
parent c19309a828
commit a1dfa5f628
2 changed files with 153 additions and 0 deletions
+85
View File
@@ -0,0 +1,85 @@
// @vitest-environment jsdom
import { describe, it, expect, vi, beforeEach } from "vitest";
import { renderHook, waitFor } from "@testing-library/react";
import { useAppSettings } from "./useAppSettings.js";
import type { AppSettingsResponse } from "@/shared/api/appSettings.js";
const mockGetAppSettings = vi.fn();
vi.mock("@/shared/api/appSettings.js", () => ({
getAppSettings: (...args: unknown[]) => mockGetAppSettings(...args),
}));
vi.mock("@/shared/api/provider.js", () => ({
useApiClient: () => ({ get: vi.fn() }),
}));
describe("useAppSettings", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("parses searchFrom/searchTo strings into numbers", async () => {
const response: AppSettingsResponse = {
uiOptions: {
filter: {
onlineboard: { searchFrom: "2d", searchTo: "14d" },
schedule: { searchFrom: "30d", searchTo: "45d" },
},
},
};
mockGetAppSettings.mockResolvedValue(response);
const { result } = renderHook(() => useAppSettings());
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.onlineboardSearchFrom).toBe(2);
expect(result.current.onlineboardSearchTo).toBe(14);
expect(result.current.scheduleSearchFrom).toBe(30);
expect(result.current.scheduleSearchTo).toBe(45);
expect(result.current.error).toBeNull();
});
it("returns defaults when uiOptions.filter is missing", async () => {
const response: AppSettingsResponse = {};
mockGetAppSettings.mockResolvedValue(response);
const { result } = renderHook(() => useAppSettings());
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.onlineboardSearchFrom).toBe(2);
expect(result.current.onlineboardSearchTo).toBe(14);
expect(result.current.scheduleSearchFrom).toBe(30);
expect(result.current.scheduleSearchTo).toBe(30);
});
it("returns defaults when a specific value does not match /\\d+d/", async () => {
const response: AppSettingsResponse = {
uiOptions: {
filter: {
onlineboard: { searchFrom: "blah", searchTo: "14d" },
schedule: { searchFrom: "30d", searchTo: "30d" },
},
},
};
mockGetAppSettings.mockResolvedValue(response);
const { result } = renderHook(() => useAppSettings());
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.onlineboardSearchFrom).toBe(2);
expect(result.current.onlineboardSearchTo).toBe(14);
});
it("starts with loading=true and returns error on failure", async () => {
const err = new Error("network");
mockGetAppSettings.mockRejectedValue(err);
const { result } = renderHook(() => useAppSettings());
expect(result.current.loading).toBe(true);
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.error).toBe(err);
expect(result.current.onlineboardSearchFrom).toBe(2);
});
});
+68
View File
@@ -0,0 +1,68 @@
import { useEffect, useState } from "react";
import { useApiClient } from "@/shared/api/provider.js";
import { getAppSettings } from "@/shared/api/appSettings.js";
const DAYS_PATTERN = /^(\d+)d$/;
const DEFAULTS = {
onlineboardSearchFrom: 2,
onlineboardSearchTo: 14,
scheduleSearchFrom: 30,
scheduleSearchTo: 30,
} as const;
function parseDays(value: string | undefined, fallback: number): number {
if (!value) return fallback;
const match = DAYS_PATTERN.exec(value);
if (!match) return fallback;
return parseInt(match[1]!, 10);
}
export interface UseAppSettingsResult {
onlineboardSearchFrom: number;
onlineboardSearchTo: number;
scheduleSearchFrom: number;
scheduleSearchTo: number;
loading: boolean;
error: Error | null;
}
/**
* Fetches the global app settings and exposes day-range numbers.
* On error or parse failure, returns default values (2/14/30/30).
*/
export function useAppSettings(): UseAppSettingsResult {
const client = useApiClient();
const [state, setState] = useState<Omit<UseAppSettingsResult, "loading" | "error">>(DEFAULTS);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
let cancelled = false;
getAppSettings(client)
.then((response) => {
if (cancelled) return;
const ob = response.uiOptions?.filter?.onlineboard;
const sc = response.uiOptions?.filter?.schedule;
setState({
onlineboardSearchFrom: parseDays(ob?.searchFrom, DEFAULTS.onlineboardSearchFrom),
onlineboardSearchTo: parseDays(ob?.searchTo, DEFAULTS.onlineboardSearchTo),
scheduleSearchFrom: parseDays(sc?.searchFrom, DEFAULTS.scheduleSearchFrom),
scheduleSearchTo: parseDays(sc?.searchTo, DEFAULTS.scheduleSearchTo),
});
setLoading(false);
})
.catch((err: Error) => {
if (cancelled) return;
setError(err);
setLoading(false);
});
return () => {
cancelled = true;
};
}, [client]);
return { ...state, loading, error };
}