feat(web): config provider — fetch /api/config, default UI language from instance
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,38 @@
|
|||||||
|
import { expect, test, beforeEach } from "vitest";
|
||||||
|
import { screen, waitFor, render } from "@testing-library/react";
|
||||||
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
import i18n, { LOCALE_KEY } from "../i18n";
|
||||||
|
import { ConfigProvider, useConfig } from "./config-context";
|
||||||
|
|
||||||
|
function Probe() {
|
||||||
|
const config = useConfig();
|
||||||
|
return <span data-testid="lang">{config.default_language}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderProvider() {
|
||||||
|
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||||
|
return render(
|
||||||
|
<QueryClientProvider client={qc}>
|
||||||
|
<ConfigProvider><Probe /></ConfigProvider>
|
||||||
|
</QueryClientProvider>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
localStorage.clear();
|
||||||
|
void i18n.changeLanguage("en");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("exposes config and applies default language when no stored preference", async () => {
|
||||||
|
renderProvider();
|
||||||
|
expect(await screen.findByTestId("lang")).toHaveTextContent("sv");
|
||||||
|
await waitFor(() => expect(i18n.language).toBe("sv"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a stored locale preference wins over the instance default", async () => {
|
||||||
|
localStorage.setItem(LOCALE_KEY, "en");
|
||||||
|
void i18n.changeLanguage("en");
|
||||||
|
renderProvider();
|
||||||
|
await screen.findByTestId("lang");
|
||||||
|
await waitFor(() => expect(i18n.language).toBe("en"));
|
||||||
|
});
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { createContext, useContext, useEffect, type ReactNode } from "react";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import type { components } from "../api/schema";
|
||||||
|
import { api } from "../api/client";
|
||||||
|
import i18n, { LOCALE_KEY } from "../i18n";
|
||||||
|
|
||||||
|
type ConfigView = components["schemas"]["ConfigView"];
|
||||||
|
|
||||||
|
const DEFAULTS: ConfigView = {
|
||||||
|
app_name: "Collection Management System",
|
||||||
|
default_language: "sv",
|
||||||
|
default_timezone: "Europe/Stockholm",
|
||||||
|
};
|
||||||
|
|
||||||
|
const ConfigContext = createContext<ConfigView>(DEFAULTS);
|
||||||
|
|
||||||
|
export function useConfig(): ConfigView {
|
||||||
|
return useContext(ConfigContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ConfigProvider({ children }: { children: ReactNode }) {
|
||||||
|
const { data } = useQuery({
|
||||||
|
queryKey: ["config"],
|
||||||
|
queryFn: async (): Promise<ConfigView> => {
|
||||||
|
const { data, error } = await api.GET("/api/config");
|
||||||
|
|
||||||
|
if (error || !data) throw new Error("failed to load config");
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
staleTime: Infinity,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Default the UI language to the instance default, unless the user has chosen one for
|
||||||
|
// this browser (LangSwitch persists to localStorage[LOCALE_KEY]).
|
||||||
|
useEffect(() => {
|
||||||
|
if (data && !localStorage.getItem(LOCALE_KEY)) {
|
||||||
|
void i18n.changeLanguage(data.default_language);
|
||||||
|
}
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
return <ConfigContext.Provider value={data ?? DEFAULTS}>{children}</ConfigContext.Provider>;
|
||||||
|
}
|
||||||
+4
-1
@@ -3,6 +3,7 @@ import { createRoot } from "react-dom/client";
|
|||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { App } from "./app";
|
import { App } from "./app";
|
||||||
|
import { ConfigProvider } from "./config/config-context";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
import "./i18n";
|
import "./i18n";
|
||||||
|
|
||||||
@@ -13,7 +14,9 @@ const queryClient = new QueryClient({
|
|||||||
createRoot(document.getElementById("root")!).render(
|
createRoot(document.getElementById("root")!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<App />
|
<ConfigProvider>
|
||||||
|
<App />
|
||||||
|
</ConfigProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,6 +3,14 @@ import { http, HttpResponse } from "msw";
|
|||||||
import { amphora, fibula, fieldDefinitions, materialTerms, objectsPage, personAuthorities, searchHits, vocabularies } from "./fixtures";
|
import { amphora, fibula, fieldDefinitions, materialTerms, objectsPage, personAuthorities, searchHits, vocabularies } from "./fixtures";
|
||||||
|
|
||||||
export const handlers = [
|
export const handlers = [
|
||||||
|
http.get("/api/config", () =>
|
||||||
|
HttpResponse.json({
|
||||||
|
app_name: "Test Museum",
|
||||||
|
default_language: "sv",
|
||||||
|
default_timezone: "Europe/Stockholm",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
|
||||||
http.get("/api/admin/me", () =>
|
http.get("/api/admin/me", () =>
|
||||||
HttpResponse.json({ id: "u1", email: "editor@example.com", role: "editor" }),
|
HttpResponse.json({ id: "u1", email: "editor@example.com", role: "editor" }),
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user