Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5e431421aa | |||
| b29c78e86d |
@@ -0,0 +1,15 @@
|
||||
# Discovery title order follows UI language
|
||||
|
||||
10 November 2026. Baseline `aac0f3b3a9b452b48126ddaf26e1837eeadd2f6d`, branch `codex/i18n-discovery-order`.
|
||||
|
||||
The title sort used the browser's default collation and its memo omitted the UI language. Switching English to Bokmål left Å-prefixed titles before Z. The focused regression reproduced this on the unchanged baseline.
|
||||
|
||||
`JobDiscoveryPage` now passes its existing English/Bokmål display locale to `localeCompare` and includes that locale in the memo dependencies. The same locale still formats recorded timestamps. The change reaches ordinary and saved-search result arrays through their shared title sort.
|
||||
|
||||
The regression switches English → Bokmål → English with Å/Z fixtures, verifies each order, and checks that the role, municipality and title sort remain selected. Switching adds zero API reads and triggers no writes. Existing submitted search and provider contracts are retained.
|
||||
|
||||
24 tests in `job-discovery.test.tsx`, TypeScript without emit and ESLint on the two changed files passed. The initial red run selected one test and skipped the other 23. It is retained separately from the final suite result in `evidence/discovery-title-order/`.
|
||||
|
||||
Export build, rendered browser, native language/device and live-provider verification were not run in this isolated increment. Existing whole-application, European locale and final acceptance gates remain open. The root integration owner will review and publish this branch separately.
|
||||
|
||||
Rollback is the inverse of the two source/test changes. There are no schema, dependency or persisted-data changes.
|
||||
@@ -0,0 +1,33 @@
|
||||
|
||||
> job-tracker-ui@0.1.0 test
|
||||
> jest --runInBand job-discovery.test.tsx -t title sorting follows UI language changes
|
||||
|
||||
FAIL src/job-discovery.test.tsx
|
||||
● title sorting follows UI language changes without refetching or changing discovery filters
|
||||
|
||||
expect(received).toEqual(expected) // deep equality
|
||||
|
||||
- Expected - 1
|
||||
+ Received + 1
|
||||
|
||||
Array [
|
||||
- "Zoologist",
|
||||
"Åker agronom",
|
||||
+ "Zoologist",
|
||||
]
|
||||
|
||||
115 |
|
||||
116 | act(() => { window.dispatchEvent(new StorageEvent("storage", { key: "uiLanguage", newValue: "nb" })); });
|
||||
> 117 | expect(titles()).toEqual(["Zoologist", "Åker agronom"]);
|
||||
| ^
|
||||
118 | expect(screen.getByLabelText("Rolle eller bedrift")).toHaveValue("science");
|
||||
119 | expect(screen.getByLabelText("Kommune")).toHaveValue("Oslo");
|
||||
120 | expect(screen.getByRole("combobox", { name: translations.nb.jobDiscoverySort })).toHaveTextContent(translations.nb.jobDiscoverySortTitle);
|
||||
|
||||
at Object.toEqual (src/job-discovery.test.tsx:117:20)
|
||||
|
||||
Test Suites: 1 failed, 1 total
|
||||
Tests: 1 failed, 23 skipped, 24 total
|
||||
Snapshots: 0 total
|
||||
Time: 5.888 s
|
||||
Ran all test suites matching job-discovery.test.tsx with tests matching "title sorting follows UI language changes".
|
||||
@@ -0,0 +1,9 @@
|
||||
|
||||
> job-tracker-ui@0.1.0 test
|
||||
> jest --runInBand job-discovery.test.tsx
|
||||
|
||||
Test Suites: 1 passed, 1 total
|
||||
Tests: 24 passed, 24 total
|
||||
Snapshots: 0 total
|
||||
Time: 8.499 s
|
||||
Ran all test suites matching job-discovery.test.tsx.
|
||||
@@ -0,0 +1,4 @@
|
||||
|
||||
> job-tracker-ui@0.1.0 typecheck
|
||||
> tsc --noEmit --incremental false
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# Discovery mobile filters
|
||||
|
||||
The approved A3 refinement calls for a mobile filter drawer while retaining the desktop search rail. This package starts from locale-sort checkpoint `b29c78e86d256f0c4e29f104bcd0c45572116635` on isolated branch `codex/a3-discovery-mobile-filters`.
|
||||
|
||||
At widths up to 1000 px, the existing municipality, source disclosure and saved-search controls open from a named Filters button. Wider layouts retain the permanent rail. Search drafts and saved-search definitions remain in their existing state owner. The municipality control and drawer submit button target the original search form. Search and Jump to results close the drawer and move focus to results. Escape/Close return focus to the trigger. Crossing to the desktop rail closes the modal and focuses the query. Failed saved-search writes remain visible inside the drawer with their draft.
|
||||
|
||||
The initial mobile regression failed three cases against the previous page. The final 28 discovery cases passed, including EN/NB draft/submit/focus, breakpoint transitions, saved-write failure and existing permission, ordering and tracker behaviour. Type-check and scoped lint passed. Raw logs are in `evidence/discovery-mobile-filters/`.
|
||||
|
||||
This package uses the current NAV search contract. New geography, taxonomy, map, providers and data persistence retain their roadmap dependencies. Exported browser, visual and accessibility validation is pending integration. Native-device and final owner acceptance remain open.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,9 @@
|
||||
|
||||
> job-tracker-ui@0.1.0 test
|
||||
> jest --runInBand job-discovery.test.tsx
|
||||
|
||||
Test Suites: 1 passed, 1 total
|
||||
Tests: 28 passed, 28 total
|
||||
Snapshots: 0 total
|
||||
Time: 9.563 s, estimated 10 s
|
||||
Ran all test suites matching job-discovery.test.tsx.
|
||||
@@ -0,0 +1,4 @@
|
||||
|
||||
> job-tracker-ui@0.1.0 typecheck
|
||||
> tsc --noEmit --incremental false
|
||||
|
||||
@@ -11,6 +11,8 @@ import { translations } from './i18n/translations';
|
||||
|
||||
jest.mock("./api", () => ({ api: { get: jest.fn(), post: jest.fn(), patch: jest.fn(), delete: jest.fn() } }));
|
||||
const mockedApi = api as jest.Mocked<typeof api>;
|
||||
let mockNarrow = false;
|
||||
jest.mock("@mui/material/useMediaQuery", () => ({ __esModule: true, default: () => mockNarrow }));
|
||||
|
||||
function renderPage() {
|
||||
return render(<I18nProvider><ConfirmProvider><PromptProvider><MemoryRouter><JobDiscoveryPage /></MemoryRouter></PromptProvider></ConfirmProvider></I18nProvider>);
|
||||
@@ -20,9 +22,83 @@ function searchResponse(data: any[]) {
|
||||
mockedApi.get.mockImplementation((url) => Promise.resolve({ data: String(url).endsWith("saved-searches") ? [] : data } as any));
|
||||
}
|
||||
|
||||
beforeEach(() => { jest.resetAllMocks(); window.localStorage.clear(); searchResponse([]); });
|
||||
beforeEach(() => { jest.resetAllMocks(); window.localStorage.clear(); mockNarrow = false; searchResponse([]); });
|
||||
afterEach(() => jest.clearAllMocks());
|
||||
|
||||
test.each(["en", "nb"] as const)("%s mobile filters retain drafts, restore focus and submit the existing search", async (language) => {
|
||||
mockNarrow = true;
|
||||
localStorage.setItem("uiLanguage", language);
|
||||
const copy = translations[language];
|
||||
renderPage();
|
||||
const form = screen.getByRole("form", { name: copy.jobDiscoverySearchNav });
|
||||
fireEvent.change(screen.getByLabelText(copy.jobDiscoveryRoleCompany), { target: { value: " nurse " } });
|
||||
const trigger = screen.getByRole("button", { name: copy.jobDiscoveryFilters });
|
||||
expect(trigger).toHaveAttribute("aria-expanded", "false");
|
||||
expect(screen.queryByLabelText(copy.jobDiscoveryMunicipality)).not.toBeInTheDocument();
|
||||
act(() => { trigger.focus(); });
|
||||
fireEvent.click(trigger);
|
||||
let drawer = await screen.findByRole("dialog", { name: copy.jobDiscoveryFilters });
|
||||
await waitFor(() => expect(within(drawer).getByRole("button", { name: copy.close })).toHaveFocus());
|
||||
const municipality = within(drawer).getByLabelText(copy.jobDiscoveryMunicipality) as HTMLInputElement;
|
||||
expect(municipality.form).toBe(form);
|
||||
fireEvent.change(municipality, { target: { value: " Oslo " } });
|
||||
fireEvent.change(within(drawer).getByLabelText(copy.jobDiscoverySavedName), { target: { value: "Care roles" } });
|
||||
fireEvent.keyDown(drawer, { key: "Escape", code: "Escape" });
|
||||
await waitFor(() => expect(screen.queryByRole("dialog")).not.toBeInTheDocument());
|
||||
await waitFor(() => expect(trigger).toHaveFocus());
|
||||
fireEvent.click(trigger);
|
||||
drawer = await screen.findByRole("dialog", { name: copy.jobDiscoveryFilters });
|
||||
expect(within(drawer).getByLabelText(copy.jobDiscoveryMunicipality)).toHaveValue(" Oslo ");
|
||||
expect(within(drawer).getByLabelText(copy.jobDiscoverySavedName)).toHaveValue("Care roles");
|
||||
fireEvent.click(within(drawer).getByRole("button", { name: copy.jobDiscoverySearchNav }));
|
||||
await waitFor(() => expect(screen.queryByRole("dialog")).not.toBeInTheDocument());
|
||||
await screen.findByText(copy.jobDiscoveryNoResults);
|
||||
await waitFor(() => expect(screen.getByRole("region", { name: copy.jobDiscoveryResults })).toHaveFocus());
|
||||
expect(mockedApi.get.mock.calls.map(([url]) => url)).toEqual(["/job-discovery/saved-searches", "/job-discovery/search"]);
|
||||
expect(mockedApi.get).toHaveBeenLastCalledWith("/job-discovery/search", { params: { q: "nurse", location: "Oslo" } });
|
||||
expect(mockedApi.post).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("crossing the filter breakpoint closes the drawer and preserves drafts and a reachable focus target", async () => {
|
||||
mockNarrow = true;
|
||||
const view = renderPage();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Search filters" }));
|
||||
const drawer = await screen.findByRole("dialog", { name: "Search filters" });
|
||||
fireEvent.change(within(drawer).getByLabelText("Municipality"), { target: { value: "Bergen" } });
|
||||
fireEvent.change(within(drawer).getByLabelText("Search name"), { target: { value: "Local care" } });
|
||||
mockNarrow = false;
|
||||
view.rerender(<I18nProvider><ConfirmProvider><PromptProvider><MemoryRouter><JobDiscoveryPage /></MemoryRouter></PromptProvider></ConfirmProvider></I18nProvider>);
|
||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Role or company")).toHaveFocus();
|
||||
const filters = screen.getByRole("complementary", { name: "Search filters" });
|
||||
expect(within(filters).getByLabelText("Municipality")).toHaveValue("Bergen");
|
||||
expect(within(filters).getByLabelText("Search name")).toHaveValue("Local care");
|
||||
mockNarrow = true;
|
||||
view.rerender(<I18nProvider><ConfirmProvider><PromptProvider><MemoryRouter><JobDiscoveryPage /></MemoryRouter></PromptProvider></ConfirmProvider></I18nProvider>);
|
||||
const trigger = screen.getByRole("button", { name: "Search filters" });
|
||||
expect(trigger).toHaveAttribute("aria-expanded", "false");
|
||||
fireEvent.click(trigger);
|
||||
const reopened = await screen.findByRole("dialog", { name: "Search filters" });
|
||||
expect(within(reopened).getByLabelText("Municipality")).toHaveValue("Bergen");
|
||||
expect(within(reopened).getByLabelText("Search name")).toHaveValue("Local care");
|
||||
expect(mockedApi.get).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("a failed mobile saved-search write remains visible inside the drawer with its draft", async () => {
|
||||
mockNarrow = true;
|
||||
mockedApi.post.mockRejectedValueOnce(new Error("synthetic save failure"));
|
||||
renderPage();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Search filters" }));
|
||||
const drawer = await screen.findByRole("dialog", { name: "Search filters" });
|
||||
await within(drawer).findByText("No saved searches yet.");
|
||||
fireEvent.change(within(drawer).getByLabelText("Search name"), { target: { value: "Care roles" } });
|
||||
fireEvent.click(within(drawer).getByRole("button", { name: "Save search" }));
|
||||
expect(await within(drawer).findByText("The search could not be saved.")).toBeVisible();
|
||||
expect(within(drawer).getByLabelText("Search name")).toHaveValue("Care roles");
|
||||
expect(within(drawer).getByRole("button", { name: "Save search" })).toBeEnabled();
|
||||
expect(mockedApi.post).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test.each([
|
||||
['en', 'Search filters', 'Jump to results', 'Return to search', 'Sources and coverage'],
|
||||
['nb', 'Søkefiltre', 'Gå til resultater', 'Tilbake til søk', 'Kilder og dekning'],
|
||||
@@ -97,6 +173,36 @@ test("sorts returned vacancies by title", async () => {
|
||||
expect(screen.getAllByRole("heading", { level: 3 }).map((element) => element.textContent)).toEqual(["Analyst", "Zoologist"]);
|
||||
});
|
||||
|
||||
test("title sorting follows UI language changes without refetching or changing discovery filters", async () => {
|
||||
searchResponse([
|
||||
{ id: "1", title: "Åker agronom", url: "https://example.test/1", source: "nav", countryCode: "NO" },
|
||||
{ id: "2", title: "Zoologist", url: "https://example.test/2", source: "nav", countryCode: "NO" },
|
||||
]);
|
||||
renderPage();
|
||||
fireEvent.change(screen.getByLabelText("Role or company"), { target: { value: "science" } });
|
||||
fireEvent.change(screen.getByLabelText("Municipality"), { target: { value: "Oslo" } });
|
||||
fireEvent.click(screen.getByRole("button", { name: "Search NAV" }));
|
||||
await screen.findByText("2 vacancies found");
|
||||
fireEvent.mouseDown(screen.getByRole("combobox", { name: "Sort results" }));
|
||||
fireEvent.click(await screen.findByRole("option", { name: "Title A–Z" }));
|
||||
const titles = () => screen.getAllByRole("heading", { level: 3 }).map((element) => element.textContent);
|
||||
expect(titles()).toEqual(["Åker agronom", "Zoologist"]);
|
||||
const reads = mockedApi.get.mock.calls.length;
|
||||
|
||||
act(() => { window.dispatchEvent(new StorageEvent("storage", { key: "uiLanguage", newValue: "nb" })); });
|
||||
expect(titles()).toEqual(["Zoologist", "Åker agronom"]);
|
||||
expect(screen.getByLabelText("Rolle eller bedrift")).toHaveValue("science");
|
||||
expect(screen.getByLabelText("Kommune")).toHaveValue("Oslo");
|
||||
expect(screen.getByRole("combobox", { name: translations.nb.jobDiscoverySort })).toHaveTextContent(translations.nb.jobDiscoverySortTitle);
|
||||
|
||||
act(() => { window.dispatchEvent(new StorageEvent("storage", { key: "uiLanguage", newValue: "en" })); });
|
||||
expect(titles()).toEqual(["Åker agronom", "Zoologist"]);
|
||||
expect(mockedApi.get).toHaveBeenCalledTimes(reads);
|
||||
expect(mockedApi.post).not.toHaveBeenCalled();
|
||||
expect(mockedApi.patch).not.toHaveBeenCalled();
|
||||
expect(mockedApi.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("renders the discovery workflow in Norwegian Bokmål", async () => {
|
||||
window.localStorage.setItem("uiLanguage", "nb");
|
||||
renderPage();
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { FormEvent, useEffect, useId, useMemo, useRef, useState } from "react";
|
||||
import { Alert, Box, Button, Card, CardActions, CardContent, Chip, CircularProgress, FormControlLabel, MenuItem, Stack, Switch, TextField, Typography } from "@mui/material";
|
||||
import { Alert, Box, Button, Card, CardActions, CardContent, Chip, CircularProgress, Drawer, FormControlLabel, MenuItem, Stack, Switch, TextField, Typography } from "@mui/material";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import FilterListIcon from "@mui/icons-material/FilterList";
|
||||
import SearchIcon from "@mui/icons-material/Search";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import BookmarkAddOutlinedIcon from "@mui/icons-material/BookmarkAddOutlined";
|
||||
@@ -37,6 +39,9 @@ type SortOrder = "updated" | "deadline" | "title";
|
||||
export default function JobDiscoveryPage() {
|
||||
const navigate = useNavigate();
|
||||
const searchFormId = useId();
|
||||
const filtersId = useId();
|
||||
const isNarrow = useMediaQuery("(max-width:1000px)");
|
||||
const [filtersOpen, setFiltersOpen] = useState(false);
|
||||
const { language, t } = useI18n();
|
||||
const { confirmAction } = useDialogActions();
|
||||
const [query, setQuery] = useState("");
|
||||
@@ -58,9 +63,29 @@ export default function JobDiscoveryPage() {
|
||||
const pending = useRef(false);
|
||||
const resultsRef = useRef<HTMLDivElement>(null);
|
||||
const queryRef = useRef<HTMLInputElement>(null);
|
||||
const filtersTriggerRef = useRef<HTMLButtonElement>(null);
|
||||
const focusResultsAfterDrawer = useRef(false);
|
||||
const drawerActive = useRef(false);
|
||||
const removedControlRef = useRef<Element | null>(null);
|
||||
const busy = loading || savingSearch || updatingSavedSearchId !== null || mutating;
|
||||
|
||||
useEffect(() => {
|
||||
if (!isNarrow) {
|
||||
const needsFocus = filtersOpen || drawerActive.current;
|
||||
focusResultsAfterDrawer.current = false;
|
||||
drawerActive.current = false;
|
||||
if (filtersOpen) setFiltersOpen(false);
|
||||
if (needsFocus) queryRef.current?.focus();
|
||||
}
|
||||
}, [isNarrow, filtersOpen]);
|
||||
|
||||
const showResults = () => {
|
||||
if (isNarrow && filtersOpen) {
|
||||
focusResultsAfterDrawer.current = true;
|
||||
setFiltersOpen(false);
|
||||
} else resultsRef.current?.focus();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const control = removedControlRef.current;
|
||||
removedControlRef.current = null;
|
||||
@@ -99,6 +124,7 @@ export default function JobDiscoveryPage() {
|
||||
|
||||
const search = (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
if (isNarrow && filtersOpen) showResults();
|
||||
void runSearch(query.trim(), location.trim());
|
||||
};
|
||||
|
||||
@@ -117,6 +143,7 @@ export default function JobDiscoveryPage() {
|
||||
|
||||
const runSavedSearch = async (saved: SavedSearch, applyFilters = true) => {
|
||||
if (pending.current) return;
|
||||
if (isNarrow && filtersOpen) showResults();
|
||||
pending.current = true;
|
||||
setLoading(true); setError(null); setJobs([]); setHasSearched(true); setActiveSavedSearchId(saved.id);
|
||||
if (applyFilters) { setQuery(saved.query); setLocation(saved.location); }
|
||||
@@ -171,16 +198,17 @@ export default function JobDiscoveryPage() {
|
||||
finally { pending.current = false; setMutating(false); }
|
||||
};
|
||||
|
||||
const locale = language === "nb" ? "nb-NO" : "en";
|
||||
const sortedJobs = useMemo(() => [...jobs].sort((left, right) => {
|
||||
if (sortOrder === "title") return left.title.localeCompare(right.title);
|
||||
if (sortOrder === "title") return left.title.localeCompare(right.title, locale);
|
||||
if (sortOrder === "deadline") {
|
||||
const leftDeadline = left.deadline ? new Date(left.deadline).getTime() : Number.MAX_SAFE_INTEGER;
|
||||
const rightDeadline = right.deadline ? new Date(right.deadline).getTime() : Number.MAX_SAFE_INTEGER;
|
||||
return leftDeadline - rightDeadline;
|
||||
}
|
||||
return new Date(right.modifiedAt || 0).getTime() - new Date(left.modifiedAt || 0).getTime();
|
||||
}), [jobs, sortOrder]);
|
||||
const formatDate = (value?: string) => value ? new Date(value).toLocaleDateString(language === "nb" ? "nb-NO" : "en") : null;
|
||||
}), [jobs, sortOrder, locale]);
|
||||
const formatDate = (value?: string) => value ? new Date(value).toLocaleDateString(locale) : null;
|
||||
|
||||
return (
|
||||
<Stack spacing={3} sx={{ minWidth: 0 }}>
|
||||
@@ -188,10 +216,22 @@ export default function JobDiscoveryPage() {
|
||||
<TextField inputRef={queryRef} label={t("jobDiscoveryRoleCompany")} value={query} disabled={busy} onChange={(event) => setQuery(event.target.value)} />
|
||||
<Button type="submit" variant="contained" startIcon={loading ? <CircularProgress aria-hidden="true" size={18} color="inherit" /> : <SearchIcon />} disabled={busy}>{t("jobDiscoverySearchNav")}</Button>
|
||||
</Box>
|
||||
{isNarrow ? <Button ref={filtersTriggerRef} startIcon={<FilterListIcon />} aria-haspopup="dialog" aria-expanded={filtersOpen} aria-controls={filtersId} onClick={() => setFiltersOpen(true)} sx={{ alignSelf: "flex-start", minHeight: 44 }}>{t("jobDiscoveryFilters")}</Button> : null}
|
||||
<Box data-discovery-layout sx={{ display: "grid", gap: 3, minWidth: 0, alignItems: "start", "@media (min-width:1001px)": { gridTemplateColumns: "minmax(16rem, 20rem) minmax(0, 1fr)" } }}>
|
||||
<Drawer variant={isNarrow ? "temporary" : "permanent"} open={!isNarrow || filtersOpen} onClose={() => setFiltersOpen(false)} ModalProps={{ disableRestoreFocus: true }}
|
||||
slotProps={{ paper: { id: filtersId, role: isNarrow ? "dialog" : undefined, "aria-modal": isNarrow ? true : undefined, "aria-label": isNarrow ? t("jobDiscoveryFilters") : undefined }, transition: { onEnter: () => { drawerActive.current = true; }, onExited: () => {
|
||||
drawerActive.current = false;
|
||||
if (focusResultsAfterDrawer.current) {
|
||||
focusResultsAfterDrawer.current = false;
|
||||
resultsRef.current?.focus();
|
||||
} else filtersTriggerRef.current?.focus();
|
||||
} } }}
|
||||
sx={{ minWidth: 0, "& .MuiDrawer-paper": isNarrow ? { width: "min(26rem, 100vw)", maxWidth: "100vw", p: 2, boxSizing: "border-box" } : { position: "static", border: 0, bgcolor: "transparent", overflow: "visible" } }}>
|
||||
<Box component="aside" aria-label={t("jobDiscoveryFilters")} sx={{ display: "grid", gap: 2, minWidth: 0 }}>
|
||||
<PageHeader level="h2" title={t("jobDiscoveryFilters")} actions={<Button onClick={() => resultsRef.current?.focus()}>{t("jobDiscoveryJumpResults")}</Button>} />
|
||||
<PageHeader level="h2" title={t("jobDiscoveryFilters")} actions={<Stack direction="row" spacing={1}>{isNarrow ? <Button autoFocus onClick={() => setFiltersOpen(false)}>{t("close")}</Button> : null}<Button onClick={showResults}>{t("jobDiscoveryJumpResults")}</Button></Stack>} />
|
||||
{isNarrow && error && error !== "jobDiscoveryUnavailable" ? <ViewStateNotice compact title={t(error)} error={{ kind: "error", message: t(error), retryable: false }} /> : null}
|
||||
<TextField label={t("jobDiscoveryMunicipality")} value={location} disabled={busy} onChange={(event) => setLocation(event.target.value)} slotProps={{ htmlInput: { form: searchFormId } }} />
|
||||
{isNarrow ? <Button type="submit" form={searchFormId} variant="contained" disabled={busy} startIcon={<SearchIcon />}>{t("jobDiscoverySearchNav")}</Button> : null}
|
||||
<Box component="details">
|
||||
<Typography component="summary" variant="body2" sx={{ cursor: "pointer", py: 0.5, "@media (pointer: coarse)": { minHeight: 44 } }}>{t("jobDiscoverySourceCoverage")}</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.5 }}>{t("jobDiscoverySourceHelp")}</Typography>
|
||||
@@ -233,6 +273,7 @@ export default function JobDiscoveryPage() {
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Drawer>
|
||||
<Box role="region" aria-label={t("jobDiscoveryResults")} tabIndex={-1} ref={resultsRef} aria-busy={loading} sx={{ display: "grid", gap: 2, minWidth: 0 }}>
|
||||
<PageHeader level="h2" title={t("jobDiscoveryResults")} actions={<Button disabled={busy} onClick={() => queryRef.current?.focus()}>{t("jobDiscoveryReturnFilters")}</Button>} />
|
||||
<ViewStateNotice compact loading={loading} title={t(error ?? "jobDiscoveryUnavailable")}
|
||||
|
||||
Reference in New Issue
Block a user