style(ui): open application rows and workspace sections
65 targeted tests and focused six-state phone pilot pass. Full 216-state visual qualification remains pending; final owner acceptance is separate.
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
# Application list and workspace visual correction — active
|
||||
|
||||
4 November 2026. Approved IM10 continuation from published/remotely verified `af2df360624d5c48ae03d80e7281704372d860ee` (secondary source checkpoint `69621788ee53d842c82cab307be129ba8806ca26`). No owner permission is outstanding for ordinary visual correction.
|
||||
|
||||
Material before evidence: `evidence/applications` has36 configurations/144 successful controlled accessibility states but still shows a bordered collection enclosing large mobile job cards and a large boxed detail header. These are functional/accessibility results, not final A3 visual acceptance. Approved A3 applications390 and detail1440 reference images were inspected without restarting design selection.
|
||||
|
||||
Uncommitted correction:
|
||||
|
||||
- Mobile rows use quiet separators and full available width, inline existing facts and direct44px named icon actions. Every edit/status/delete/restore handler and confirmation remains. Column/density preferences, selection, tags/signals, pagination, role/tabIndex, interactive-target guards, keyboard navigation and return state remain.
|
||||
- Workspace header, continuous document/notes/calendar/correspondence/next-action/recent-activity sections and expanded recorded details/advert use open sections. Two-column phone preparation links retain all five destinations. Header/Accordion padding reduced; right-side context grid and jump/return targets unchanged.
|
||||
-65 tests/six suites passed, including EN/NB mobile actions independent of row navigation and existing cover-letter preservation. Types/lint/build passed. A native test initially queried background controls before the confirmation dialog finished exiting; corrected to await accessible restoration. Its failed report is retained.
|
||||
- Independent source review found no actionable findings; native touch/icon discoverability and focus visibility still require rendered inspection.
|
||||
|
||||
Browser evidence is deliberately distinct: `applications-flat` is the successor directory. The first36-configuration run covers five states per configuration (180 intended captures) but does **not** exercise a focused table row. A new focused-row pilot reproduced a real clipped outline in the borderless mobile collection; see `FOCUS-CLIP-FAILURE.json`. The responsible collection overflow was changed to visible on phones, retaining desktop containment. The new verifier adds a focused-row state and an explicit outline/clipping assertion: final required successor is **36 configurations /216 states**, not180. Do not attribute the older180-state run to the focus fix or mark this package complete until the final export and216-state run pass.
|
||||
|
||||
Successor update: the older non-focused run completed180 states but cannot close the focus gap; retained as `PREDECESSOR-180.json` without claiming an immutable source manifest or using it for final qualification. Focus correction export built successfully; the new six-state phone pilot passed, including an explicit visible/unclipped row outline assertion. Final36-config/216-state run is next. Types/build and full lint passed for this correction. Do not add pilot/predecessor results to final totals.
|
||||
|
||||
Remaining work: run full216, inspect phone/tablet/desktop and EN/NB/dark expanded detail captures, review source/evidence identities, publish/read back validated source and evidence checkpoints. Backend/physical PDF/provider/model/native/final security/release qualifications remain separate. Only app chrome changed; do not reopen accepted physical PDF metrics without an affected renderer change.
|
||||
|
||||
Next eligible packages after this bounded correction: account continuous sections (V10), remaining route visual review/connected workflows, and the bounded literal-date localisation defect (V11). Whole34-route/57-group acceptance and final owner review remain OPEN. No orchestrator, main merge, production or deferred import-engine work.
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+37
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"screens": [
|
||||
{
|
||||
"width": 320,
|
||||
"language": "en",
|
||||
"mode": "light",
|
||||
"state": "table",
|
||||
"violations": []
|
||||
}
|
||||
],
|
||||
"journeys": [],
|
||||
"aliases": [],
|
||||
"focusChecks": [
|
||||
{
|
||||
"width": 320,
|
||||
"language": "en",
|
||||
"mode": "light",
|
||||
"outlineStyle": "solid",
|
||||
"outlineWidth": "3px",
|
||||
"clippedBy": [
|
||||
"DIV"
|
||||
]
|
||||
}
|
||||
],
|
||||
"errors": [],
|
||||
"unexpectedWrites": [],
|
||||
"mockedWrites": [],
|
||||
"unavailableReads": [],
|
||||
"blockedExternal": [
|
||||
"https://fonts.googleapis.com"
|
||||
],
|
||||
"limits": [
|
||||
"controlled fixtures, not real backend integration",
|
||||
"secondary service reads may return honest unavailable states",
|
||||
"no native device, provider, model, security or release qualification"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
{
|
||||
"screens": [
|
||||
{
|
||||
"width": 320,
|
||||
"language": "en",
|
||||
"mode": "light",
|
||||
"state": "table",
|
||||
"violations": []
|
||||
},
|
||||
{
|
||||
"width": 320,
|
||||
"language": "en",
|
||||
"mode": "light",
|
||||
"state": "table-focused",
|
||||
"violations": []
|
||||
},
|
||||
{
|
||||
"width": 320,
|
||||
"language": "en",
|
||||
"mode": "light",
|
||||
"state": "add-job",
|
||||
"violations": []
|
||||
},
|
||||
{
|
||||
"width": 320,
|
||||
"language": "en",
|
||||
"mode": "light",
|
||||
"state": "board",
|
||||
"violations": []
|
||||
},
|
||||
{
|
||||
"width": 320,
|
||||
"language": "en",
|
||||
"mode": "light",
|
||||
"state": "detail",
|
||||
"violations": []
|
||||
},
|
||||
{
|
||||
"width": 320,
|
||||
"language": "en",
|
||||
"mode": "light",
|
||||
"state": "detail-expanded",
|
||||
"violations": []
|
||||
}
|
||||
],
|
||||
"journeys": [
|
||||
{
|
||||
"width": 320,
|
||||
"language": "en",
|
||||
"mode": "light",
|
||||
"searchBoardPreciseStatusDetailContextReturn": "PASS"
|
||||
}
|
||||
],
|
||||
"aliases": [
|
||||
{
|
||||
"legacy": "/kanban?q=nurse#retained",
|
||||
"destination": "/jobs",
|
||||
"queryPreserved": "PASS"
|
||||
},
|
||||
{
|
||||
"legacy": "/applications/42?section=overview",
|
||||
"destination": "/jobs/42",
|
||||
"queryPreserved": "PASS"
|
||||
},
|
||||
{
|
||||
"legacy": "/correspondence/review?q=recorded#retained",
|
||||
"destination": "/correspondence",
|
||||
"queryPreserved": "PASS"
|
||||
}
|
||||
],
|
||||
"focusChecks": [
|
||||
{
|
||||
"width": 320,
|
||||
"language": "en",
|
||||
"mode": "light",
|
||||
"outlineStyle": "solid",
|
||||
"outlineWidth": "3px",
|
||||
"clippedBy": []
|
||||
}
|
||||
],
|
||||
"errors": [],
|
||||
"unexpectedWrites": [],
|
||||
"mockedWrites": [
|
||||
{
|
||||
"width": 320,
|
||||
"language": "en",
|
||||
"mode": "light",
|
||||
"status": "Waiting"
|
||||
}
|
||||
],
|
||||
"unavailableReads": [],
|
||||
"blockedExternal": [
|
||||
"https://fonts.googleapis.com",
|
||||
"https://fonts.googleapis.com",
|
||||
"https://fonts.googleapis.com",
|
||||
"https://fonts.googleapis.com"
|
||||
],
|
||||
"limits": [
|
||||
"controlled fixtures, not real backend integration",
|
||||
"secondary service reads may return honest unavailable states",
|
||||
"no native device, provider, model, security or release qualification"
|
||||
]
|
||||
}
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
@@ -5,11 +5,13 @@ import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
const visualMatrix = Boolean(process.env.A3_VISUAL_MATRIX);
|
||||
const flatWorkspace = Boolean(process.env.A3_FLAT_WORKSPACE);
|
||||
assert.ok(!flatWorkspace || visualMatrix, 'Flat workspace qualification uses the visual matrix.');
|
||||
const widths = visualMatrix ? [320, 360, 390, 430, 768, 1024, 1280, 1440, 1920] : [320, 390, 768, 1440];
|
||||
const configurations = process.env.A3_PILOT ? [[320, 'en', 'light']] : widths.flatMap(width => ['en', 'nb'].flatMap(language => ['light', 'dark'].map(mode => [width, language, mode])));
|
||||
const out = path.resolve(import.meta.dirname, visualMatrix ? '../../docs/ui-modernisation/visual-correction/evidence/applications' : '../../docs/ui-modernisation/evidence/applications/browser');
|
||||
const out = path.resolve(import.meta.dirname, flatWorkspace ? '../../docs/ui-modernisation/visual-correction/evidence/applications-flat' : visualMatrix ? '../../docs/ui-modernisation/visual-correction/evidence/applications' : '../../docs/ui-modernisation/evidence/applications/browser');
|
||||
fs.mkdirSync(out, { recursive: true });
|
||||
const report = { screens: [], journeys: [], aliases: [], errors: [], unexpectedWrites: [], mockedWrites: [], unavailableReads: [], blockedExternal: [] };
|
||||
const report = { screens: [], journeys: [], aliases: [], focusChecks: [], errors: [], unexpectedWrites: [], mockedWrites: [], unavailableReads: [], blockedExternal: [] };
|
||||
const browser = await chromium.launch();
|
||||
try {
|
||||
for (const [width, language, mode] of configurations) {
|
||||
@@ -55,6 +57,24 @@ try {
|
||||
const search = page.getByRole('textbox', { name: language === 'en' ? 'Search' : 'Søk', exact: true }); await search.waitFor();
|
||||
await page.locator('[data-job-row-id="42"], [data-job-card-id="42"]').first().waitFor();
|
||||
await scan('table');
|
||||
if (flatWorkspace) {
|
||||
await page.keyboard.press('Tab');
|
||||
const row = page.locator('[data-job-row-id="42"]').first(); await row.focus(); await expect(row).toBeFocused();
|
||||
const focus = await row.evaluate(element => {
|
||||
const style = getComputedStyle(element), rect = element.getBoundingClientRect();
|
||||
const extension = parseFloat(style.outlineWidth) + parseFloat(style.outlineOffset);
|
||||
const clippedBy = [];
|
||||
for (let parent = element.parentElement; parent; parent = parent.parentElement) {
|
||||
const bounds = parent.getBoundingClientRect(), overflow = getComputedStyle(parent).overflowX;
|
||||
if (['hidden', 'clip', 'auto', 'scroll'].includes(overflow) && (rect.left - extension < bounds.left || rect.right + extension > bounds.right)) clippedBy.push(parent.tagName);
|
||||
}
|
||||
return { outlineStyle: style.outlineStyle, outlineWidth: style.outlineWidth, clippedBy };
|
||||
});
|
||||
report.focusChecks.push({ width, language, mode, ...focus });
|
||||
assert.notEqual(focus.outlineStyle, 'none');
|
||||
if (width < 768) assert.deepEqual(focus.clippedBy, [], 'mobile row focus ring is clipped');
|
||||
await scan('table-focused');
|
||||
}
|
||||
if (visualMatrix) {
|
||||
await page.getByRole('button', { name: language === 'en' ? 'Add Job' : 'Legg til jobb', exact: true }).click();
|
||||
await expect(page.getByRole('dialog')).toBeVisible(); await scan('add-job');
|
||||
@@ -78,6 +98,11 @@ try {
|
||||
const main = await page.getByRole('tabpanel').boundingBox(), side = await aside.boundingBox();
|
||||
assert.ok(width > 1000 ? side.x > main.x + main.width - 2 && Math.abs(side.y - main.y) < 2 : side.y >= main.y + main.height - 2);
|
||||
await scan('detail');
|
||||
if (flatWorkspace) {
|
||||
await page.getByRole('button', { name: language === 'en' ? 'Job details' : 'Stillingsdetaljer', exact: true }).click();
|
||||
await expect(page.getByText('Recorded source advertisement.', { exact: true })).toBeVisible();
|
||||
await scan('detail-expanded');
|
||||
}
|
||||
await page.getByRole('button', { name: language === 'en' ? 'Back to applications' : 'Tilbake til søknader', exact: true }).click();
|
||||
await page.waitForURL(/view=board/); assert.equal(new URL(page.url()).searchParams.get('q'), 'nurse');
|
||||
await page.locator('[data-kanban-card-id="42"] [role="button"]').waitFor();
|
||||
@@ -93,7 +118,7 @@ try {
|
||||
}
|
||||
await context.close();
|
||||
}
|
||||
assert.equal(report.screens.length, configurations.length * (visualMatrix ? 4 : 3)); assert.equal(report.journeys.length, configurations.length); assert.equal(report.mockedWrites.length, configurations.length); assert.equal(report.aliases.length, visualMatrix ? 3 : 0);
|
||||
assert.equal(report.screens.length, configurations.length * (flatWorkspace ? 6 : visualMatrix ? 4 : 3)); assert.equal(report.journeys.length, configurations.length); assert.equal(report.mockedWrites.length, configurations.length); assert.equal(report.aliases.length, visualMatrix ? 3 : 0);
|
||||
assert.deepEqual(report.errors, []); assert.deepEqual(report.unexpectedWrites, []);
|
||||
assert.ok(report.blockedExternal.every(origin => origin === 'https://fonts.googleapis.com'));
|
||||
console.log(`PASS:${report.journeys.length} controlled table/board/detail journeys,${report.screens.length} axe scans/captures;${report.mockedWrites.length} mocked status writes.`);
|
||||
|
||||
@@ -673,15 +673,16 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
data-content-density={density}
|
||||
sx={{
|
||||
mt: 1.5,
|
||||
overflow: "hidden",
|
||||
borderRadius: 1,
|
||||
border: "1px solid",
|
||||
overflow: isMobile ? "visible" : "hidden",
|
||||
borderRadius: isMobile ? 0 : 1,
|
||||
border: isMobile ? 0 : "1px solid",
|
||||
borderColor: "divider",
|
||||
backgroundColor: isMobile ? "transparent" : "background.paper",
|
||||
boxShadow: "none",
|
||||
}}
|
||||
>
|
||||
{isMobile ? (
|
||||
<Stack spacing="var(--app-list-gap)" sx={{ p: 1.25 }}>
|
||||
<Stack spacing={0}>
|
||||
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 1, px: 0.5 }}>
|
||||
<FormControlLabel control={<Checkbox checked={selectedAllOnPage} indeterminate={selectedIds.length > 0 && !selectedAllOnPage} onChange={(e) => toggleSelectAll(e.target.checked)} />} label={t("jobTableSelectAll")} sx={{ mr: 0 }} />
|
||||
</Box>
|
||||
@@ -690,7 +691,7 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
{!jobsResource.loading && !jobsResource.error && rowModels.map(({ job, toneName, actionSignals, tags, appliedDateLabel, isSelected }) => {
|
||||
const compactTags = tags.slice(0, 6);
|
||||
return (
|
||||
<Paper
|
||||
<Box
|
||||
key={job.id}
|
||||
data-job-row-id={job.id}
|
||||
role={mode === "jobs" && !job.isDeleted ? "link" : undefined}
|
||||
@@ -705,12 +706,11 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
openJob(job.id);
|
||||
}
|
||||
}}
|
||||
variant="outlined"
|
||||
sx={{
|
||||
p: "var(--app-list-item-padding)",
|
||||
borderRadius: 2,
|
||||
py: "var(--app-list-item-padding)",
|
||||
borderBottom: "1px solid",
|
||||
cursor: mode === "jobs" && !job.isDeleted ? "pointer" : "default",
|
||||
backgroundColor: isSelected ? "action.selected" : "background.paper",
|
||||
backgroundColor: isSelected ? "action.selected" : "transparent",
|
||||
borderColor: "divider",
|
||||
boxShadow: "none",
|
||||
"&:hover": mode === "jobs" && !job.isDeleted ? { backgroundColor: "action.hover" } : undefined,
|
||||
@@ -733,7 +733,7 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
{columns.status ? <Chip label={statusLabel(t, job.status)} size="small" color={toneName as any} variant="outlined" sx={{ fontWeight: 700, maxWidth: "100%", height: "auto", minHeight: 24, "& .MuiChip-label": { whiteSpace: "normal", overflowWrap: "anywhere", py: 0.5 } }} /> : null}
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: "flex", gap: 0.75, flexWrap: "wrap" }}>
|
||||
{actionSignals.length > 0 && <Box sx={{ display: "flex", gap: 0.75, flexWrap: "wrap" }}>
|
||||
{actionSignals.map((signal) => (
|
||||
<Chip
|
||||
key={`${job.id}-${signal.label}-${signal.detail}`}
|
||||
@@ -747,27 +747,27 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
sx={{ fontWeight: 700 }}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Box>}
|
||||
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 1.25 }}>
|
||||
{columns.dateApplied ? (
|
||||
<Box>
|
||||
<Typography variant="overline" sx={{ color: "text.secondary" }}>{t("jobTableDateApplied")}</Typography>
|
||||
<Box sx={{ display: "flex", flexWrap: "wrap", columnGap: 0.5, alignItems: "baseline" }}>
|
||||
<Typography variant="caption" sx={{ color: "text.secondary" }}>{t("jobTableDateApplied")}</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, fontVariantNumeric: "tabular-nums" }}>{appliedDateLabel}</Typography>
|
||||
</Box>
|
||||
) : null}
|
||||
{columns.daysSince ? (
|
||||
<Box>
|
||||
<Typography variant="overline" sx={{ color: "text.secondary" }}>{t("jobTableDays")}</Typography>
|
||||
<Box sx={{ display: "flex", flexWrap: "wrap", columnGap: 0.5, alignItems: "baseline" }}>
|
||||
<Typography variant="caption" sx={{ color: "text.secondary" }}>{t("jobTableDays")}</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 700, fontVariantNumeric: "tabular-nums" }}>{job.daysSince}</Typography>
|
||||
</Box>
|
||||
) : null}
|
||||
<Box>
|
||||
<Typography variant="overline" sx={{ color: "text.secondary" }}>{t("jobTableLocation")}</Typography>
|
||||
<Box sx={{ display: "flex", flexWrap: "wrap", columnGap: 0.5, alignItems: "baseline" }}>
|
||||
<Typography variant="caption" sx={{ color: "text.secondary" }}>{t("jobTableLocation")}</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>{job.location ?? "-"}</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="overline" sx={{ color: "text.secondary" }}>{t("addJobModalSalary")}</Typography>
|
||||
<Box sx={{ display: "flex", flexWrap: "wrap", columnGap: 0.5, alignItems: "baseline" }}>
|
||||
<Typography variant="caption" sx={{ color: "text.secondary" }}>{t("addJobModalSalary")}</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, fontVariantNumeric: "tabular-nums" }}>{formatSalary(job) ?? "-"}</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -785,25 +785,17 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
</Box>
|
||||
) : null}
|
||||
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: "repeat(2, minmax(0, 1fr))", gap: 1, "& .MuiButton-root": { minHeight: 44, whiteSpace: "normal", lineHeight: 1.25 } }}>
|
||||
<Button variant="outlined" startIcon={<EditOutlinedIcon />} onClick={() => setEditJobId(job.id)} sx={{ minHeight: 42, fontWeight: 700 }}>
|
||||
{t("jobTableEdit")}
|
||||
</Button>
|
||||
<Button variant="outlined" startIcon={<MoreHorizIcon />} onClick={(e) => { setStatusJobId(job.id); setStatusAnchor(e.currentTarget); }} sx={{ minHeight: 42, fontWeight: 700 }}>
|
||||
{t("jobTableQuickStatus")}
|
||||
</Button>
|
||||
<Box sx={{ display: "flex", gap: 0.5, "& .MuiIconButton-root": { width: 44, height: 44 } }}>
|
||||
<Tooltip title={t("jobTableEdit")}><IconButton aria-label={t("jobTableEdit")} onClick={() => setEditJobId(job.id)}><EditOutlinedIcon /></IconButton></Tooltip>
|
||||
<Tooltip title={t("jobTableQuickStatus")}><IconButton aria-label={t("jobTableQuickStatus")} onClick={(e) => { setStatusJobId(job.id); setStatusAnchor(e.currentTarget); }}><MoreHorizIcon /></IconButton></Tooltip>
|
||||
{(mode === "trash" || (includeDeleted && job.isDeleted)) ? (
|
||||
<Button variant="outlined" startIcon={<RestoreFromTrashOutlinedIcon />} onClick={() => void restore(job.id)} sx={{ minHeight: 42, fontWeight: 700 }}>
|
||||
{t("jobTableRestore")}
|
||||
</Button>
|
||||
<Tooltip title={t("jobTableRestore")}><IconButton aria-label={t("jobTableRestore")} onClick={() => void restore(job.id)}><RestoreFromTrashOutlinedIcon /></IconButton></Tooltip>
|
||||
) : (
|
||||
<Button color="error" variant="outlined" startIcon={<DeleteOutlineIcon />} onClick={() => void softDelete(job)} sx={{ minHeight: 42, fontWeight: 700 }}>
|
||||
{t("jobTableSoftDelete")}
|
||||
</Button>
|
||||
<Tooltip title={t("jobTableSoftDelete")}><IconButton color="error" aria-label={t("jobTableSoftDelete")} onClick={() => void softDelete(job)}><DeleteOutlineIcon /></IconButton></Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
{filteredJobs.length === 0 && !jobsResource.loading && !jobsResource.error ? (
|
||||
|
||||
@@ -51,6 +51,38 @@ test("row status/edit/delete controls retain selection and do not open the works
|
||||
expect(screen.getByLabelText("Current route")).toHaveTextContent(/^\/jobs$/);
|
||||
});
|
||||
|
||||
test.each(["en", "nb"] as const)("%s mobile row actions remain direct, named and independent of row navigation", async language => {
|
||||
const originalMedia = Object.getOwnPropertyDescriptor(window, "matchMedia");
|
||||
Object.defineProperty(window, "matchMedia", { configurable: true, writable: true, value: (query: string) => ({
|
||||
matches: query.includes("max-width:767.95px"), media: query, onchange: null,
|
||||
addListener: jest.fn(), removeListener: jest.fn(), addEventListener: jest.fn(), removeEventListener: jest.fn(), dispatchEvent: () => true,
|
||||
}) });
|
||||
try {
|
||||
localStorage.setItem("uiLanguage", language);
|
||||
const copy = translations[language];
|
||||
show("jobs", "?q=platform");
|
||||
const row = await screen.findByRole("link", { name: copy.jobTableOpenJob.replace("{title}", job.jobTitle).replace("{company}", job.company.name) });
|
||||
fireEvent.click(within(row).getByRole("checkbox"));
|
||||
fireEvent.click(within(row).getByRole("button", { name: copy.jobTableEdit }));
|
||||
expect(screen.getByRole("dialog", { name: "Edit job" })).toBeVisible();
|
||||
fireEvent.click(screen.getByText("Close editor"));
|
||||
expect(within(row).getByRole("checkbox")).toBeChecked();
|
||||
fireEvent.click(within(row).getByRole("button", { name: copy.jobTableSoftDelete }));
|
||||
fireEvent.click(within(await screen.findByRole("dialog")).getByRole("button", { name: copy.cancel }));
|
||||
expect(mockedApi.delete).not.toHaveBeenCalled();
|
||||
fireEvent.click(await within(row).findByRole("button", { name: copy.jobTableQuickStatus }));
|
||||
fireEvent.click(await screen.findByRole("menuitem", { name: copy.jobTableSetStatus.replace("{status}", copy.statusInterview) }));
|
||||
await waitFor(() => expect(mockedApi.patch).toHaveBeenCalledWith("/jobapplications/42/status", { status: "Interview" }));
|
||||
expect(screen.getByLabelText("Current route")).toHaveTextContent("/jobs?q=platform");
|
||||
fireEvent.keyDown(row, { key: "Enter" });
|
||||
expect(screen.getByLabelText("Current route")).toHaveTextContent(/^\/jobs\/42$/);
|
||||
expect(window.history.state.jobListFocusId).toBe(job.id);
|
||||
} finally {
|
||||
if (originalMedia) Object.defineProperty(window, "matchMedia", originalMedia);
|
||||
else delete (window as Partial<Window>).matchMedia;
|
||||
}
|
||||
});
|
||||
|
||||
test("table sort, page-size and optional columns keep their existing independent contracts", async () => {
|
||||
show("jobs", "?q=platform&location=Troms%C3%B8");
|
||||
await screen.findByRole("row", { name: /open platform engineer/i });
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
} from "react-router-dom";
|
||||
|
||||
import {
|
||||
Accordion, AccordionDetails, AccordionSummary, Alert, Box, Button, Chip, IconButton, Paper,
|
||||
Accordion, AccordionDetails, AccordionSummary, Alert, Box, Button, Chip, IconButton,
|
||||
Skeleton, Stack, Tab, Tabs, Tooltip, Typography,
|
||||
} from "@mui/material";
|
||||
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
||||
@@ -218,14 +218,14 @@ export function ApplicationWorkspace({
|
||||
return (
|
||||
<Box sx={{ display: "grid", gap: "var(--app-section-gap)", minWidth: 0 }}>
|
||||
<ViewStateNotice title={t("workspaceLoadFailed")} loading={loading && !overview} compact />
|
||||
<Paper variant="outlined" sx={{ minWidth: 0, borderRadius: 1 }}>
|
||||
<Stack direction="row" alignItems="center" spacing={0.5} sx={{ px: { xs: 1, sm: 2 }, pt: 1.5 }}>
|
||||
<Box sx={{ minWidth: 0 }}>
|
||||
<Stack direction="row" alignItems="center" spacing={0.5} sx={{ pt: 0.5 }}>
|
||||
<Tooltip title={t("workspaceBack")}>
|
||||
<IconButton size="small" aria-label={t("workspaceBack")} onClick={close}>
|
||||
<ArrowBackIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Typography variant="caption" sx={{ fontWeight: 800, letterSpacing: ".08em", textTransform: "uppercase", color: "text.secondary" }}>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{t("workspace")}
|
||||
</Typography>
|
||||
{overview ? <Button size="small" onClick={() => contextTitle.current?.focus()} sx={{ ml: "auto" }}>{t("workspaceJumpContext")}</Button> : null}
|
||||
@@ -252,11 +252,11 @@ export function ApplicationWorkspace({
|
||||
variant="scrollable"
|
||||
scrollButtons="auto"
|
||||
aria-label={t("workspaceSections")}
|
||||
sx={{ px: { xs: 0.5, sm: 1.5 }, borderTop: 1, borderColor: "divider", minHeight: 46 }}
|
||||
sx={{ borderBottom: 1, borderColor: "divider", minHeight: 46 }}
|
||||
>
|
||||
{WORKSPACE_SECTIONS.map((s) => <Tab key={s.key} id={`${sectionId}-${s.key}`} aria-controls={`${sectionId}-panel`} value={s.key} label={workspaceSectionLabel(t, s.key)} sx={{ minHeight: 46, fontWeight: 700 }} />)}
|
||||
</Tabs>
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
<Box data-application-layout sx={{ display: "grid", gap: 3, minWidth: 0, alignItems: "start", ...(!onClose && overview ? { "@media (min-width:1001px)": { gridTemplateColumns: "minmax(0, 1fr) minmax(16rem, 20rem)" } } : {}) }}>
|
||||
<Box ref={panelRef} role="tabpanel" id={`${sectionId}-panel`} aria-labelledby={`${sectionId}-${section}`} tabIndex={0} sx={{ display: "grid", gap: 2, minWidth: 0 }}>
|
||||
@@ -288,22 +288,22 @@ export function ApplicationWorkspace({
|
||||
/>}
|
||||
{section === "application" && subsection === "submission" && jobId > 0 && <SubmittedPackagesSection key={jobId} jobId={jobId} />}
|
||||
{section === "timeline" && subsection === "activity" && jobId > 0 && <ApplicationTimeline jobId={jobId} />}
|
||||
{section === "timeline" && subsection === "correspondence" && jobId > 0 && <PageSection surface title={t("workspaceCommunication")} level="h3"><Correspondence jobId={jobId} jobContext={{ companyName: overview?.company, jobTitle: overview?.jobTitle }} /></PageSection>}
|
||||
{section === "timeline" && subsection === "correspondence" && jobId > 0 && <PageSection title={t("workspaceCommunication")} level="h3"><Correspondence jobId={jobId} jobContext={{ companyName: overview?.company, jobTitle: overview?.jobTitle }} /></PageSection>}
|
||||
</Box>
|
||||
</>}
|
||||
{section === "documents" && jobId > 0 && <>
|
||||
<PageSection surface title={t("workspaceCurrentDocuments")} description={t("workspaceCurrentDocumentsHelp")} actions={<>
|
||||
<PageSection title={t("workspaceCurrentDocuments")} description={t("workspaceCurrentDocumentsHelp")} actions={<>
|
||||
<Button onClick={() => go("application", "cv")}>{t("workspaceCv")}</Button>
|
||||
<Button onClick={() => go("application", "letter")}>{t("workspaceCoverLetter")}</Button>
|
||||
</>}><Attachments jobId={jobId} /></PageSection>
|
||||
<SubmittedPackagesSection key={jobId} jobId={jobId} allowCapture={false} />
|
||||
</>}
|
||||
{section === "tasks" && jobId > 0 && <>
|
||||
<PageSection surface title={t("workspaceNotes")} actions={<Button startIcon={<EditOutlinedIcon />} onClick={() => setEditOpen(true)}>{t("workspaceEdit")}</Button>}>
|
||||
<PageSection title={t("workspaceNotes")} actions={<Button startIcon={<EditOutlinedIcon />} onClick={() => setEditOpen(true)}>{t("workspaceEdit")}</Button>}>
|
||||
<Typography sx={{ whiteSpace: "pre-wrap", overflowWrap: "anywhere" }}>{overview?.notes || t("workspaceNoNotes")}</Typography>
|
||||
</PageSection>
|
||||
<PageSection title={t("workspaceChecklist")}><ApplicationChecklist jobId={jobId} onChanged={load} /></PageSection>
|
||||
<PageSection surface title={t("calendarTitle")}><ApplicationCalendarActions jobId={jobId} followUpAt={overview?.followUpAt ?? null} deadline={overview?.deadline ?? null} /></PageSection>
|
||||
<PageSection title={t("calendarTitle")}><ApplicationCalendarActions jobId={jobId} followUpAt={overview?.followUpAt ?? null} deadline={overview?.deadline ?? null} /></PageSection>
|
||||
</>}
|
||||
</Box>
|
||||
{overview ? <Box component="aside" aria-label={t("workspaceContext")} sx={{ minWidth: 0, ...(!onClose ? { "@media (min-width:1001px)": { borderInlineStart: "1px solid", borderColor: "divider", pl: 3 } } : {}) }}>
|
||||
@@ -358,7 +358,7 @@ function WorkspaceHeader({ overview, onEdit }: { overview: WorkspaceOverview | n
|
||||
const { language, t } = useI18n();
|
||||
if (!overview) return <Box sx={{ p: 2 }} aria-hidden><Skeleton width="65%" height={34} /><Skeleton width="40%" /></Box>;
|
||||
return (
|
||||
<Box sx={{ px: { xs: 2, sm: 3 }, pt: 1, pb: 2 }}>
|
||||
<Box sx={{ pt: 0.5, pb: 1.5 }}>
|
||||
<PageHeader level="h2" title={overview.jobTitle} description={overview.company ?? undefined} actions={<>
|
||||
<Button variant="outlined" startIcon={<EditOutlinedIcon />} onClick={onEdit}>{t("workspaceEditApplication")}</Button>
|
||||
{overview.jobUrl ? <Button component="a" href={overview.jobUrl} target="_blank" rel="noopener noreferrer" endIcon={<OpenInNewIcon />}>{t("workspaceOpenAdvert")}</Button> : null}
|
||||
@@ -440,7 +440,7 @@ function OverviewSection({ overview, onGo, onReload, onEdit }: {
|
||||
return (
|
||||
<Stack spacing={2} sx={{ minWidth: 0 }}>
|
||||
{overview.nextStep ? (
|
||||
<PageSection surface title={t("workspaceNextAction")}>
|
||||
<PageSection title={t("workspaceNextAction")}>
|
||||
<Typography component="h3" variant="subtitle1" sx={{ fontWeight: 700 }}>{nextStepLabel}</Typography>
|
||||
<Typography color="text.secondary" sx={{ mb: 1.5 }}>{nextStepReason}</Typography>
|
||||
<Button variant="contained" endIcon={<ArrowForwardIcon />}
|
||||
@@ -455,11 +455,11 @@ function OverviewSection({ overview, onGo, onReload, onEdit }: {
|
||||
)}
|
||||
|
||||
<PageSection title={t("workspacePreparation")}>
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", sm: "repeat(2, minmax(0, 1fr))", lg: "repeat(5, minmax(0, 1fr))" }, gap: 1 }}>
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "repeat(2, minmax(0, 1fr))", lg: "repeat(5, minmax(0, 1fr))" }, gap: 1 }}>
|
||||
{stats.map((s) => (
|
||||
<Button key={s.label} variant="outlined"
|
||||
<Button key={s.label} variant="text"
|
||||
onClick={() => onGo(s.go)}
|
||||
sx={{ p: 1.5, display: "block", textAlign: "left", color: "text.primary", borderColor: "divider", minWidth: 0, whiteSpace: "normal", overflowWrap: "anywhere" }}>
|
||||
sx={{ px: 0, py: 1, display: "block", textAlign: "left", color: "text.primary", borderBottom: 1, borderColor: "divider", borderRadius: 0, minWidth: 0, whiteSpace: "normal", overflowWrap: "anywhere" }}>
|
||||
<Stack direction="row" spacing={0.75} alignItems="center" sx={{ color: s.ok ? "success.main" : "text.secondary" }}>
|
||||
{s.icon}
|
||||
<Typography variant="caption" sx={{ fontWeight: 700 }}>{s.label}</Typography>
|
||||
@@ -470,7 +470,7 @@ function OverviewSection({ overview, onGo, onReload, onEdit }: {
|
||||
</Box>
|
||||
</PageSection>
|
||||
|
||||
<PageSection surface title={t("workspaceRecentActivity")} actions={<Button size="small" onClick={onReload}>{t("workspaceRefresh")}</Button>}>
|
||||
<PageSection title={t("workspaceRecentActivity")} actions={<Button size="small" onClick={onReload}>{t("workspaceRefresh")}</Button>}>
|
||||
{overview.recentActivity.length === 0 ? (
|
||||
<Typography variant="body2" color="text.secondary">{t("workspaceNoActivity")}</Typography>
|
||||
) : (
|
||||
@@ -510,11 +510,11 @@ function OverviewDetails({ overview, onEdit }: { overview: WorkspaceOverview; on
|
||||
return (
|
||||
<Box>
|
||||
{panels.map((panel) => (
|
||||
<Accordion key={panel.id} expanded={!!expanded[panel.id]} onChange={(_, open) => setExpanded((current) => ({ ...current, [panel.id]: open }))} disableGutters elevation={0} sx={{ border: 1, borderColor: "divider", "&:not(:last-child)": { borderBottom: 0 }, "&:before": { display: "none" } }}>
|
||||
<Accordion key={panel.id} expanded={!!expanded[panel.id]} onChange={(_, open) => setExpanded((current) => ({ ...current, [panel.id]: open }))} disableGutters elevation={0} sx={{ borderTop: 1, borderBottom: 1, borderInline: 0, borderColor: "divider", borderRadius: 0, "&:first-of-type, &:last-of-type": { borderRadius: 0 }, "&:not(:last-child)": { borderBottom: 0 }, "&:before": { display: "none" } }}>
|
||||
<AccordionSummary ref={(node: HTMLDivElement | null) => { triggers.current[panel.id] = node; }} expandIcon={<ExpandMoreIcon />} aria-controls={`${prefix}-${panel.id}-content`} id={`${prefix}-${panel.id}-header`}>
|
||||
<Typography sx={{ fontWeight: 750 }}>{panel.title}</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails sx={{ px: { xs: 1, sm: 2 }, pb: 2 }}>{panel.content}</AccordionDetails>
|
||||
<AccordionDetails sx={{ px: 0, pb: 2 }}>{panel.content}</AccordionDetails>
|
||||
</Accordion>
|
||||
))}
|
||||
</Box>
|
||||
@@ -552,7 +552,7 @@ function JobDetailsSection({ overview, onEdit }: { overview: WorkspaceOverview |
|
||||
];
|
||||
return (
|
||||
<Stack spacing={2}>
|
||||
<Paper sx={{ p: 2.5, borderRadius: 3 }}>
|
||||
<Box sx={{ py: 1 }}>
|
||||
<Stack direction="row" alignItems="center" justifyContent="space-between" gap={1} sx={{ mb: 1.5 }}>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 800 }}>{t("workspaceApplicationInfo")}</Typography>
|
||||
<Button size="small" startIcon={<EditOutlinedIcon />} onClick={onEdit}>{t("workspaceEdit")}</Button>
|
||||
@@ -576,9 +576,9 @@ function JobDetailsSection({ overview, onEdit }: { overview: WorkspaceOverview |
|
||||
<Typography variant="body2" sx={{ mt: 0.5, whiteSpace: "pre-wrap", overflowWrap: "anywhere" }}>{overview.notes}</Typography>
|
||||
</Box>
|
||||
) : null}
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
<Paper sx={{ p: { xs: 2, sm: 2.5 }, borderRadius: 3 }}>
|
||||
<Box sx={{ py: 1 }}>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 800, mb: 1.5 }}>{t("workspaceJobDescription")}</Typography>
|
||||
{!overview.hasJobDescription ? (
|
||||
<Alert severity="warning" sx={{ borderRadius: 2 }} action={<Button color="inherit" size="small" onClick={onEdit}>{t("workspaceAddAdvert")}</Button>}>
|
||||
@@ -600,7 +600,7 @@ function JobDetailsSection({ overview, onEdit }: { overview: WorkspaceOverview |
|
||||
) : null}
|
||||
</Stack>
|
||||
)}
|
||||
</Paper>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user