feat(web): add Columns slide-over with Stampli HTML defaults (AP-45) (#18)

* feat(web): fix shell chrome, logo home, and live nav badges

Refs: AP-42

* feat(web): add invoice grid lines and bold headers

Refs: AP-44

* feat(web): support auto row height for multi-line invoice cells

Refs: AP-44

* feat(web): expand mock invoice fields for Stampli column catalog

Refs: AP-47

* feat(web): add Action and Vendor filters to invoice processing

Refs: AP-43

* feat(web): add multi-line Stampli invoice cell renderers

Refs: AP-47

* feat(web): align filter toolbar layout, field shapes, and tabs

Refs: AP-43

* feat(web): add Columns slide-over with Stampli HTML defaults

Refs: AP-45

* fix(web): address review feedback

* fix(web): address review feedback

* fix(web): address review feedback

* ci: remove the pr policy caller

* fix(web): keep mock catalog dates and updates in sync

* fix(web): render the attention banner as text

* fix(web): keep pay date editable on the approved grid

* fix(web): make the attention banner non-interactive copy

* fix(web): derive mock action labels from invoice status

* fix(web): restore link-style attention banner copy

* fix(ci): drop the unused catalog refresh index

* fix(web): assert the vendor column by its grid field

* fix(ci): format invoice mock helpers

* fix(web): hide Payment Date when the drawer omits it

* fix(web): count filtered rows on invoice tabs

* fix(web): close the columns drawer before editing pay date
This commit is contained in:
Adam Moussa 2026-09-25 22:28:05 +00:00 • committed by GitHub
parent fad8461274
commit ee2f7a9902
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 529 additions and 10 deletions

View file

@ -53,6 +53,23 @@ test.describe("dashboard grids", () => {
/Pay run stub: [2-9]\d* invoice\(s\) selected/,
);
await page.getByTestId("columns-toggle").click();
const drawer = page.getByTestId("columns-drawer");
await drawer.getByTestId("columns-hidden-search").fill("Payment Date");
await drawer
.getByTestId("columns-hidden-paymentDate")
.locator('input[type="checkbox"]')
.evaluate((el: HTMLInputElement) => {
el.click();
});
await expect(
readyGrid.locator('.MuiDataGrid-columnHeader[data-field="payDate"]'),
).toBeVisible();
await page.getByRole("button", { name: "Close columns" }).evaluate((el: HTMLElement) => {
el.click();
});
await expect(drawer).toBeHidden();
const payDateCell = readyGrid.locator('[data-field="payDate"]').nth(1);
await payDateCell.dblclick();
const editor = readyGrid

View file

@ -0,0 +1,64 @@
import { expect, test } from "@playwright/test";
test.describe("Invoice Processing columns drawer (AP-45)", () => {
test("Columns drawer can hide Tagging and Reset restores it", async ({ page }) => {
await page.goto("/");
await expect(
page.getByTestId("grid-pending").locator('.MuiDataGrid-columnHeader[data-field="tagging"]'),
).toBeVisible();
await page.getByTestId("columns-toggle").click();
const drawer = page.getByTestId("columns-drawer");
await expect(drawer).toBeVisible();
const taggingRow = drawer.getByTestId("columns-visible-tagging");
await taggingRow.scrollIntoViewIfNeeded();
await taggingRow.locator('input[type="checkbox"]').evaluate((el: HTMLInputElement) => {
el.click();
});
await expect(
page.getByTestId("grid-pending").locator('.MuiDataGrid-columnHeader[data-field="tagging"]'),
).toHaveCount(0);
await drawer.getByTestId("columns-selector-reset-button").evaluate((el: HTMLElement) => {
el.click();
});
await expect(
page.getByTestId("grid-pending").locator('.MuiDataGrid-columnHeader[data-field="tagging"]'),
).toBeVisible();
});
test("locked Vendor column stays visible and Reset clears the hidden search", async ({
page,
}) => {
await page.goto("/");
await page.getByTestId("columns-toggle").click();
const drawer = page.getByTestId("columns-drawer");
const vendorBox = drawer
.getByTestId("columns-visible-vendor")
.locator('input[type="checkbox"]');
await expect(vendorBox).toBeDisabled();
await vendorBox.evaluate((el: HTMLInputElement) => {
el.disabled = false;
el.click();
});
await expect(
page
.getByTestId("grid-pending")
.locator('.MuiDataGrid-columnHeader[data-field="vendorName"]'),
).toBeVisible();
await page.getByTestId("tab-ready").evaluate((el: HTMLElement) => {
el.click();
});
await expect(
page.getByTestId("grid-ready").locator('.MuiDataGrid-columnHeader[data-field="vendorName"]'),
).toBeVisible();
const search = drawer.getByTestId("columns-hidden-search");
await search.fill("no-such-column");
await drawer.getByTestId("columns-selector-reset-button").evaluate((el: HTMLElement) => {
el.click();
});
await expect(search).toHaveValue("");
});
});

56
package-lock.json generated
View file

@ -11,6 +11,9 @@
"packages/*"
],
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@fontsource/ibm-plex-sans": "^5.3.0",
@ -842,6 +845,59 @@
"node": ">=20.19.0"
}
},
"node_modules/@dnd-kit/accessibility": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
"integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/core": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
"license": "MIT",
"dependencies": {
"@dnd-kit/accessibility": "^3.1.1",
"@dnd-kit/utilities": "^3.2.2",
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@dnd-kit/sortable": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz",
"integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==",
"license": "MIT",
"dependencies": {
"@dnd-kit/utilities": "^3.2.2",
"tslib": "^2.0.0"
},
"peerDependencies": {
"@dnd-kit/core": "^6.3.0",
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/utilities": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
"integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@drizzle-team/brocli": {
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz",

View file

@ -34,6 +34,9 @@
},
"packageManager": "npm@12.0.2",
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@fontsource/ibm-plex-sans": "^5.3.0",

View file

@ -0,0 +1,207 @@
import {
DndContext,
PointerSensor,
closestCenter,
type DragEndEvent,
useSensor,
useSensors,
} from "@dnd-kit/core";
import {
SortableContext,
arrayMove,
useSortable,
verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import CloseIcon from "@mui/icons-material/Close";
import DragIndicatorIcon from "@mui/icons-material/DragIndicator";
import {
Box,
Button,
Checkbox,
Drawer,
FormControlLabel,
IconButton,
Stack,
TextField,
Typography,
} from "@mui/material";
import type { CSSProperties, ReactElement } from "react";
import { useMemo, useState } from "react";
import type { InvoiceColumnKey } from "@/dashboard/column-catalog";
import {
catalogLabel,
hiddenColumnKeys,
isColumnLocked,
type ColumnPrefs,
} from "@/dashboard/column-prefs";
type ColumnsDrawerProps = {
open: boolean;
onClose: () => void;
prefs: ColumnPrefs;
onPrefsChange: (next: ColumnPrefs) => void;
onReset: () => void;
};
function SortableVisibleRow({
columnKey,
onToggle,
}: {
columnKey: InvoiceColumnKey;
onToggle: (key: InvoiceColumnKey, checked: boolean) => void;
}): ReactElement {
const locked = isColumnLocked(columnKey);
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({
id: columnKey,
});
const style: CSSProperties = {
transform: CSS.Transform.toString(transform),
transition,
display: "flex",
alignItems: "center",
gap: 8,
};
return (
<Box ref={setNodeRef} style={style} data-testid={`columns-visible-${columnKey}`}>
<IconButton size="small" {...attributes} {...listeners} aria-label={`Reorder ${columnKey}`}>
<DragIndicatorIcon fontSize="small" />
</IconButton>
<FormControlLabel
control={
<Checkbox
checked
disabled={locked}
onChange={(event) => onToggle(columnKey, event.target.checked)}
/>
}
label={catalogLabel(columnKey)}
/>
</Box>
);
}
export function ColumnsDrawer({
open,
onClose,
prefs,
onPrefsChange,
onReset,
}: ColumnsDrawerProps): ReactElement {
const [hiddenQuery, setHiddenQuery] = useState("");
const [viewAllHidden, setViewAllHidden] = useState(false);
const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 4 } }));
const hidden = useMemo(() => hiddenColumnKeys(prefs.visibleOrder), [prefs.visibleOrder]);
const filteredHidden = useMemo(() => {
const q = hiddenQuery.trim().toLowerCase();
const list = q ? hidden.filter((key) => catalogLabel(key).toLowerCase().includes(q)) : hidden;
if (viewAllHidden || q) {
return list;
}
return list.slice(0, 5);
}, [hidden, hiddenQuery, viewAllHidden]);
const onDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
if (!over || active.id === over.id) {
return;
}
const oldIndex = prefs.visibleOrder.indexOf(active.id as InvoiceColumnKey);
const newIndex = prefs.visibleOrder.indexOf(over.id as InvoiceColumnKey);
if (oldIndex < 0 || newIndex < 0) {
return;
}
onPrefsChange({ visibleOrder: arrayMove(prefs.visibleOrder, oldIndex, newIndex) });
};
const onToggle = (key: InvoiceColumnKey, checked: boolean) => {
if (isColumnLocked(key)) {
return;
}
if (checked) {
if (prefs.visibleOrder.includes(key)) {
return;
}
onPrefsChange({ visibleOrder: [...prefs.visibleOrder, key] });
return;
}
onPrefsChange({ visibleOrder: prefs.visibleOrder.filter((item) => item !== key) });
};
return (
<Drawer
anchor="right"
open={open}
onClose={onClose}
data-testid="columns-drawer"
slotProps={{ paper: { sx: { width: 360, p: 2 } } }}
>
<Stack direction="row" sx={{ alignItems: "center", justifyContent: "space-between", mb: 2 }}>
<Typography variant="h6" data-testid="columns-selector-header">
Columns
</Typography>
<Stack direction="row" spacing={1}>
<Button
size="small"
data-testid="columns-selector-reset-button"
onClick={() => {
setHiddenQuery("");
setViewAllHidden(false);
onReset();
}}
>
Reset
</Button>
<IconButton aria-label="Close columns" onClick={onClose} size="small">
<CloseIcon />
</IconButton>
</Stack>
</Stack>
<Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 700 }}>
Visible
</Typography>
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={onDragEnd}>
<SortableContext items={prefs.visibleOrder} strategy={verticalListSortingStrategy}>
<Stack spacing={0.5} sx={{ mb: 2 }}>
{prefs.visibleOrder.map((key) => (
<SortableVisibleRow key={key} columnKey={key} onToggle={onToggle} />
))}
</Stack>
</SortableContext>
</DndContext>
<Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 700 }}>
Hidden
</Typography>
<TextField
size="small"
fullWidth
placeholder="Search"
value={hiddenQuery}
onChange={(event) => setHiddenQuery(event.target.value)}
sx={{ mb: 1 }}
slotProps={{ htmlInput: { "data-testid": "columns-hidden-search" } }}
/>
<Stack spacing={0.5} sx={{ mb: 1 }}>
{filteredHidden.map((key) => (
<FormControlLabel
key={key}
data-testid={`columns-hidden-${key}`}
control={
<Checkbox checked={false} onChange={(event) => onToggle(key, event.target.checked)} />
}
label={catalogLabel(key)}
/>
))}
</Stack>
{!viewAllHidden && !hiddenQuery && hidden.length > 5 ? (
<Button size="small" onClick={() => setViewAllHidden(true)}>
View all
</Button>
) : null}
</Drawer>
);
}

View file

@ -1,14 +1,17 @@
import { useMemo, useState } from "react";
import { Alert, Box, CircularProgress, Stack, Tab, Tabs, Typography } from "@mui/material";
import type { GridRowParams, GridRowSelectionModel } from "@mui/x-data-grid";
import { ColumnsDrawer } from "@/dashboard/ColumnsDrawer";
import { InvoiceFilterToolbar } from "@/dashboard/InvoiceFilterToolbar";
import { CreatePayRunButton } from "@/dashboard/CreatePayRunButton";
import { InvoiceDataGrid } from "@/dashboard/InvoiceDataGrid";
import { columnsForApprovedGrid, columnsFromCatalogKeys } from "@/dashboard/columns";
import {
allInvoicesColumns,
invoiceAttentionColumns,
readyToPayColumns,
} from "@/dashboard/columns";
loadColumnPrefs,
resetColumnPrefs,
saveColumnPrefs,
type ColumnPrefs,
} from "@/dashboard/column-prefs";
import { EMPTY_INVOICE_FILTERS, type InvoiceFilterState } from "@/dashboard/invoice-filters";
import { InvoiceTabLabel } from "@/dashboard/invoice-tabs";
import { countSelectedRows } from "@/dashboard/selection";
@ -72,11 +75,21 @@ export function DashboardGrids() {
const [tab, setTab] = useState<InvoiceTab>("attention");
const [selection, setSelection] = useState<GridRowSelectionModel>(emptySelection);
const [filters, setFilters] = useState<InvoiceFilterState>(emptyFilters);
const [, setColumnsDrawerOpen] = useState(false);
const [columnsDrawerOpen, setColumnsDrawerOpen] = useState(false);
const [, setFilterDrawerOpen] = useState(false);
const [columnPrefs, setColumnPrefs] = useState<ColumnPrefs>(() => loadColumnPrefs());
const { data: invoices = [], isLoading, isError, error } = useInvoices();
const updatePayDate = useUpdateInvoicePayDate();
const catalogColumns = useMemo(
() => columnsFromCatalogKeys(columnPrefs.visibleOrder),
[columnPrefs.visibleOrder],
);
const approvedColumns = useMemo(
() => columnsForApprovedGrid(columnPrefs.visibleOrder),
[columnPrefs.visibleOrder],
);
const openInvoice = (params: GridRowParams<MockInvoice>) => {
navigate("/invoices/:id", { params: { id: params.row.id } });
};
@ -129,6 +142,11 @@ export function DashboardGrids() {
return updatePayDate.mutateAsync({ id: newRow.id, payDate: nextPayDate });
};
const updateColumnPrefs = (next: ColumnPrefs) => {
setColumnPrefs(next);
saveColumnPrefs(next);
};
if (isLoading) {
return (
<Box sx={{ display: "flex", justifyContent: "center", py: 6 }}>
@ -245,7 +263,7 @@ export function DashboardGrids() {
<InvoiceDataGrid
data-testid="grid-pending"
rows={filteredAttention}
columns={invoiceAttentionColumns}
columns={catalogColumns}
onRowClick={openInvoice}
sx={rowClickSx}
/>
@ -255,7 +273,7 @@ export function DashboardGrids() {
<InvoiceDataGrid
data-testid="grid-all"
rows={filteredAll}
columns={allInvoicesColumns}
columns={catalogColumns}
onRowClick={openInvoice}
sx={rowClickSx}
/>
@ -265,7 +283,7 @@ export function DashboardGrids() {
<InvoiceDataGrid
data-testid="grid-ready"
rows={filteredApproved}
columns={readyToPayColumns}
columns={approvedColumns}
checkboxSelection
rowSelectionModel={selection}
onRowSelectionModelChange={(model) => setSelection(model)}
@ -281,7 +299,7 @@ export function DashboardGrids() {
<InvoiceDataGrid
data-testid="grid-rejected"
rows={filteredRejected}
columns={allInvoicesColumns}
columns={catalogColumns}
onRowClick={openInvoice}
sx={rowClickSx}
/>
@ -291,7 +309,7 @@ export function DashboardGrids() {
<InvoiceDataGrid
data-testid="grid-late"
rows={filteredLate}
columns={allInvoicesColumns}
columns={catalogColumns}
onRowClick={openInvoice}
sx={rowClickSx}
/>
@ -301,6 +319,17 @@ export function DashboardGrids() {
Mocks {areMocksEnabled() ? "enabled" : "disabled"} — {invoices.length} invoices
</Typography>
</Box>
<ColumnsDrawer
open={columnsDrawerOpen}
onClose={() => setColumnsDrawerOpen(false)}
prefs={columnPrefs}
onPrefsChange={updateColumnPrefs}
onReset={() => {
const next = resetColumnPrefs();
setColumnPrefs(next);
}}
/>
</Stack>
);
}

View file

@ -0,0 +1,9 @@
import type { GridColDef } from "@mui/x-data-grid";
import type { MockInvoice } from "@/mocks";
/** Mark a visible pay-date column editable. A hidden Payment Date stays omitted. */
export function withEditablePayDate(columns: GridColDef<MockInvoice>[]): GridColDef<MockInvoice>[] {
return columns.map((column) =>
column.field === "payDate" ? { ...column, editable: true } : column,
);
}

View file

@ -0,0 +1,42 @@
import { afterEach, describe, expect, it } from "vitest";
import {
COLUMN_PREFS_STORAGE_KEY,
defaultColumnPrefs,
hiddenColumnKeys,
loadColumnPrefs,
resetColumnPrefs,
saveColumnPrefs,
} from "@/dashboard/column-prefs";
import { DEFAULT_VISIBLE_COLUMN_KEYS } from "@/dashboard/column-catalog";
describe("column-prefs", () => {
afterEach(() => {
sessionStorage.removeItem(COLUMN_PREFS_STORAGE_KEY);
});
it("defaults to HTML Visible order", () => {
expect(defaultColumnPrefs().visibleOrder).toEqual([...DEFAULT_VISIBLE_COLUMN_KEYS]);
});
it("persists and reloads prefs", () => {
saveColumnPrefs({ visibleOrder: ["invoiceId", "vendor", "dueDate"] });
expect(loadColumnPrefs().visibleOrder).toEqual([
"bolt",
"bell",
"invoiceId",
"vendor",
"dueDate",
]);
});
it("reset restores defaults", () => {
saveColumnPrefs({ visibleOrder: ["invoiceId"] });
expect(resetColumnPrefs().visibleOrder).toEqual([...DEFAULT_VISIBLE_COLUMN_KEYS]);
});
it("computes hidden keys from visible order", () => {
const hidden = hiddenColumnKeys(["invoiceId", "vendor"]);
expect(hidden).toContain("tagging");
expect(hidden).not.toContain("invoiceId");
});
});

View file

@ -0,0 +1,66 @@
import {
DEFAULT_VISIBLE_COLUMN_KEYS,
INVOICE_COLUMN_CATALOG,
LOCKED_COLUMN_KEYS,
type InvoiceColumnKey,
} from "@/dashboard/column-catalog";
export const COLUMN_PREFS_STORAGE_KEY = "seahaven-ap.invoice-columns.v1";
export type ColumnPrefs = {
visibleOrder: InvoiceColumnKey[];
};
export function defaultColumnPrefs(): ColumnPrefs {
return { visibleOrder: [...DEFAULT_VISIBLE_COLUMN_KEYS] };
}
export function loadColumnPrefs(): ColumnPrefs {
if (typeof sessionStorage === "undefined") {
return defaultColumnPrefs();
}
try {
const raw = sessionStorage.getItem(COLUMN_PREFS_STORAGE_KEY);
if (!raw) {
return defaultColumnPrefs();
}
const parsed = JSON.parse(raw) as ColumnPrefs;
if (!Array.isArray(parsed.visibleOrder) || parsed.visibleOrder.length === 0) {
return defaultColumnPrefs();
}
const known = new Set(INVOICE_COLUMN_CATALOG.map((entry) => entry.key));
const visibleOrder = parsed.visibleOrder.filter((key) => known.has(key));
const missingLocked = LOCKED_COLUMN_KEYS.filter((key) => !visibleOrder.includes(key));
return { visibleOrder: [...missingLocked, ...visibleOrder] };
} catch {
return defaultColumnPrefs();
}
}
export function saveColumnPrefs(prefs: ColumnPrefs): void {
if (typeof sessionStorage === "undefined") {
return;
}
sessionStorage.setItem(COLUMN_PREFS_STORAGE_KEY, JSON.stringify(prefs));
}
export function resetColumnPrefs(): ColumnPrefs {
const prefs = defaultColumnPrefs();
saveColumnPrefs(prefs);
return prefs;
}
export function hiddenColumnKeys(visibleOrder: InvoiceColumnKey[]): InvoiceColumnKey[] {
const visible = new Set(visibleOrder);
return INVOICE_COLUMN_CATALOG.filter((entry) => !visible.has(entry.key)).map(
(entry) => entry.key,
);
}
export function isColumnLocked(key: InvoiceColumnKey): boolean {
return LOCKED_COLUMN_KEYS.includes(key);
}
export function catalogLabel(key: InvoiceColumnKey): string {
return INVOICE_COLUMN_CATALOG.find((entry) => entry.key === key)?.label ?? key;
}

View file

@ -1,4 +1,7 @@
import type { GridColDef } from "@mui/x-data-grid";
import { describe, expect, it } from "vitest";
import { withEditablePayDate } from "@/dashboard/approved-columns";
import type { MockInvoice } from "@/mocks";
import { formatRelativeFromIsoDate, formatSlashDate } from "@/dashboard/invoice-date-format";
describe("invoice column date helpers (AP-47)", () => {
@ -26,3 +29,20 @@ describe("invoice column date helpers (AP-47)", () => {
expect(formatRelativeFromIsoDate("2026-10-11", now)).toBe("in 2 months");
});
});
describe("withEditablePayDate", () => {
const hidden: GridColDef<MockInvoice> = { field: "vendorName", headerName: "Vendor" };
const payDate: GridColDef<MockInvoice> = { field: "payDate", headerName: "Payment Date" };
it("leaves Payment Date hidden when the catalog omits it", () => {
const columns = withEditablePayDate([hidden]);
expect(columns.some((column) => column.field === "payDate")).toBe(false);
});
it("keeps a single editable pay date when Payment Date is already visible", () => {
const columns = withEditablePayDate([hidden, { ...payDate, editable: false }]);
const matches = columns.filter((column) => column.field === "payDate");
expect(matches).toHaveLength(1);
expect(matches[0]?.editable).toBe(true);
});
});

View file

@ -4,6 +4,7 @@ import NotificationsNoneOutlinedIcon from "@mui/icons-material/NotificationsNone
import VisibilityOutlinedIcon from "@mui/icons-material/VisibilityOutlined";
import { Box, Link as MuiLink, Stack, Typography } from "@mui/material";
import type { GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
import { withEditablePayDate } from "@/dashboard/approved-columns";
import type { InvoiceColumnKey } from "@/dashboard/column-catalog";
import { formatRelativeFromIsoDate, formatSlashDate } from "@/dashboard/invoice-date-format";
import type { MockInvoice } from "@/mocks";
@ -250,6 +251,11 @@ export function columnsFromCatalogKeys(keys: InvoiceColumnKey[]): GridColDef<Moc
return keys.map((key) => catalogColumnDefs[key]);
}
/** Approved tab keeps pay date editable when the drawer shows Payment Date. */
export function columnsForApprovedGrid(keys: InvoiceColumnKey[]): GridColDef<MockInvoice>[] {
return withEditablePayDate(columnsFromCatalogKeys(keys));
}
export const readyToPayColumns: GridColDef<MockInvoice>[] = [
{
field: "invoiceNumber",