feat(discovery): lead with the existing search controls
This commit is contained in:
@@ -564,7 +564,7 @@ export const translations = {
|
||||
languageEnglish: "English",
|
||||
languageNorwegianBokmal: "Norsk bokmål",
|
||||
discoverJobs: "Discover jobs",
|
||||
discoverJobsSubtitle: "Search official job-board feeds and save opportunities to your tracker.",
|
||||
discoverJobsSubtitle: "Search vacancies and save opportunities to your tracker.",
|
||||
landingSignIn: "Sign in",
|
||||
landingSignInArrow: "Sign in →",
|
||||
landingEyebrow: "AI-ASSISTED JOB SEARCH WORKSPACE",
|
||||
@@ -636,11 +636,12 @@ export const translations = {
|
||||
jobDiscoveryRoleCompany: "Role or company",
|
||||
jobDiscoveryFilters: "Search filters",
|
||||
jobDiscoveryJumpResults: "Jump to results",
|
||||
jobDiscoveryReturnFilters: "Return to search filters",
|
||||
jobDiscoveryReturnFilters: "Return to search",
|
||||
jobDiscoveryMunicipality: "Municipality",
|
||||
jobDiscoverySearchNav: "Search NAV",
|
||||
jobDiscoveryResults: "Search results",
|
||||
jobDiscoverySourceHelp: "Official Norwegian vacancies from NAV. FINN, Indeed and LinkedIn jobs can still be captured by URL.",
|
||||
jobDiscoverySourceCoverage: "Sources and coverage",
|
||||
jobDiscoverySourceHelp: "Search uses NAV for Norwegian vacancies. To track a job from another site, paste its URL and enter any details Jobjakt cannot retrieve.",
|
||||
jobDiscoveryUnavailable: "NAV job discovery is temporarily unavailable.",
|
||||
jobDiscoveryGuidance: "Search recent active vacancies by role, company, or municipality.",
|
||||
jobDiscoveryNoResults: "No active vacancies matched this search. Try a broader role, company, or municipality.",
|
||||
@@ -3787,7 +3788,7 @@ export const translations = {
|
||||
languageEnglish: "Engelsk",
|
||||
languageNorwegianBokmal: "Norsk bokmål",
|
||||
discoverJobs: "Finn jobber",
|
||||
discoverJobsSubtitle: "Søk i offisielle jobbportaler og lagre muligheter i oversikten din.",
|
||||
discoverJobsSubtitle: "Søk etter stillinger og lagre dem i jobboversikten din.",
|
||||
landingSignIn: "Logg inn",
|
||||
landingSignInArrow: "Logg inn →",
|
||||
landingEyebrow: "AI-STØTTET ARBEIDSOMRÅDE FOR JOBBSØK",
|
||||
@@ -3859,11 +3860,12 @@ export const translations = {
|
||||
jobDiscoveryRoleCompany: "Rolle eller bedrift",
|
||||
jobDiscoveryFilters: "Søkefiltre",
|
||||
jobDiscoveryJumpResults: "Gå til resultater",
|
||||
jobDiscoveryReturnFilters: "Tilbake til søkefiltre",
|
||||
jobDiscoveryReturnFilters: "Tilbake til søk",
|
||||
jobDiscoveryMunicipality: "Kommune",
|
||||
jobDiscoverySearchNav: "Søk i NAV",
|
||||
jobDiscoveryResults: "Søkeresultater",
|
||||
jobDiscoverySourceHelp: "Offisielle norske stillinger fra NAV. Stillinger fra FINN, Indeed og LinkedIn kan fortsatt hentes inn via nettadresse.",
|
||||
jobDiscoverySourceCoverage: "Kilder og dekning",
|
||||
jobDiscoverySourceHelp: "Søket bruker NAV for norske stillinger. For å følge en stilling fra et annet nettsted kan du lime inn nettadressen og fylle ut opplysningene Jobjakt ikke kan hente.",
|
||||
jobDiscoveryUnavailable: "NAVs jobbsøk er midlertidig utilgjengelig.",
|
||||
jobDiscoveryGuidance: "Søk i nylige aktive stillinger etter rolle, bedrift eller kommune.",
|
||||
jobDiscoveryNoResults: "Ingen aktive stillinger samsvarte med søket. Prøv en bredere rolle, bedrift eller kommune.",
|
||||
|
||||
@@ -24,20 +24,25 @@ beforeEach(() => { jest.resetAllMocks(); window.localStorage.clear(); searchResp
|
||||
afterEach(() => jest.clearAllMocks());
|
||||
|
||||
test.each([
|
||||
['en', 'Search filters', 'Jump to results', 'Return to search filters'],
|
||||
['nb', 'Søkefiltre', 'Gå til resultater', 'Tilbake til søkefiltre'],
|
||||
] as const)('keeps %s filters before results with keyboard jump/return and no automatic provider search', async (language, filtersName, jumpName, returnName) => {
|
||||
['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'],
|
||||
] as const)('keeps %s search before filters and results with keyboard jump/return and no automatic provider search', async (language, filtersName, jumpName, returnName, sourceName) => {
|
||||
localStorage.setItem('uiLanguage', language);
|
||||
renderPage();
|
||||
await screen.findByText(translations[language].jobDiscoveryNoSavedSearches);
|
||||
const filters = screen.getByRole('complementary', { name: filtersName });
|
||||
const form = screen.getByRole('form', { name: translations[language].jobDiscoverySearchNav });
|
||||
const query = within(form).getByRole('textbox', { name: translations[language].jobDiscoveryRoleCompany });
|
||||
const municipality = within(filters).getByRole('textbox', { name: translations[language].jobDiscoveryMunicipality }) as HTMLInputElement;
|
||||
const results = screen.getByRole('region', { name: translations[language].jobDiscoveryResults });
|
||||
expect(form.compareDocumentPosition(filters) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||
expect(filters.compareDocumentPosition(results) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||
expect(within(filters).getByRole('form', { name: filtersName })).toBeInTheDocument();
|
||||
expect(municipality.form).toBe(form);
|
||||
expect(within(filters).getByText(sourceName).closest('details')).not.toHaveAttribute('open');
|
||||
fireEvent.click(within(filters).getByRole('button', { name: jumpName }));
|
||||
expect(results).toHaveFocus();
|
||||
fireEvent.click(within(results).getByRole('button', { name: returnName }));
|
||||
expect(screen.getByRole('textbox', { name: translations[language].jobDiscoveryRoleCompany })).toHaveFocus();
|
||||
expect(query).toHaveFocus();
|
||||
expect(mockedApi.get.mock.calls.map(([url]) => url)).toEqual(['/job-discovery/saved-searches']);
|
||||
expect(mockedApi.post).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FormEvent, useEffect, useMemo, useRef, useState } from "react";
|
||||
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 SearchIcon from "@mui/icons-material/Search";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
@@ -36,6 +36,7 @@ type SortOrder = "updated" | "deadline" | "title";
|
||||
|
||||
export default function JobDiscoveryPage() {
|
||||
const navigate = useNavigate();
|
||||
const searchFormId = useId();
|
||||
const { language, t } = useI18n();
|
||||
const { confirmAction } = useDialogActions();
|
||||
const [query, setQuery] = useState("");
|
||||
@@ -183,14 +184,17 @@ export default function JobDiscoveryPage() {
|
||||
|
||||
return (
|
||||
<Stack spacing={3} sx={{ minWidth: 0 }}>
|
||||
<Alert severity="info" variant="outlined" sx={{ color: "text.primary", "& .MuiAlert-icon": { color: "info.main" } }}>{t("jobDiscoverySourceHelp")}</Alert>
|
||||
<Box component="form" id={searchFormId} aria-label={t("jobDiscoverySearchNav")} onSubmit={search} sx={{ display: "grid", gridTemplateColumns: { xs: "minmax(0, 1fr)", sm: "minmax(0, 1fr) auto" }, gap: 1.5 }}>
|
||||
<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>
|
||||
<Box data-discovery-layout sx={{ display: "grid", gap: 3, minWidth: 0, alignItems: "start", "@media (min-width:1001px)": { gridTemplateColumns: "minmax(16rem, 20rem) minmax(0, 1fr)" } }}>
|
||||
<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>} />
|
||||
<Box component="form" aria-label={t("jobDiscoveryFilters")} onSubmit={search} sx={{ display: "grid", gridTemplateColumns: "minmax(0, 1fr)", gap: 1.5 }}>
|
||||
<TextField inputRef={queryRef} label={t("jobDiscoveryRoleCompany")} value={query} disabled={busy} onChange={(event) => setQuery(event.target.value)} />
|
||||
<TextField label={t("jobDiscoveryMunicipality")} value={location} disabled={busy} onChange={(event) => setLocation(event.target.value)} />
|
||||
<Button type="submit" variant="contained" startIcon={loading ? <CircularProgress aria-hidden="true" size={18} color="inherit" /> : <SearchIcon />} disabled={busy}>{t("jobDiscoverySearchNav")}</Button>
|
||||
<TextField label={t("jobDiscoveryMunicipality")} value={location} disabled={busy} onChange={(event) => setLocation(event.target.value)} slotProps={{ htmlInput: { form: searchFormId } }} />
|
||||
<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>
|
||||
</Box>
|
||||
<Box aria-busy={savedSearchState === "loading"}>
|
||||
<Box sx={{ display: "grid", gap: 1.5 }}>
|
||||
|
||||
Reference in New Issue
Block a user