merge(web): keep the columns drawer on main

This commit is contained in:
Adam Moussa 2026-09-25 18:20:10 -04:00
commit 56fcbe2df3
No known key found for this signature in database
23 changed files with 2094 additions and 1484 deletions

View file

@ -1,24 +0,0 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
groups:
minor-and-patch:
update-types:
- "minor"
- "patch"
commit-message:
prefix: "chore(deps)"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
groups:
minor-and-patch:
update-types:
- "minor"
- "patch"
commit-message:
prefix: "chore(deps)"

View file

@ -3,6 +3,7 @@ name: CI
on:
pull_request:
branches: [main]
merge_group:
permissions:
contents: read
@ -11,6 +12,6 @@ jobs:
ci:
# Job id MUST stay `ci`: the reusable's job is also `ci`, so the check
# context resolves to the required `ci / ci`.
uses: Sea-Haven-Industries/.github/.github/workflows/ci-typescript-frontend.yaml@7ac3528750b346f181347bb09f6af927a1c0aa14 # v1.0.6
uses: Sea-Haven-Industries/.github/.github/workflows/ci-typescript-frontend.yaml@9781774f04b824b1182ff41638687f1c01c04361 # v1.0.11
with:
node-version: "24"

View file

@ -7,4 +7,4 @@ permissions:
jobs:
review:
uses: Sea-Haven-Industries/.github/.github/workflows/callable-dependency-review.yaml@7ac3528750b346f181347bb09f6af927a1c0aa14 # v1.0.6
uses: Sea-Haven-Industries/.github/.github/workflows/callable-dependency-review.yaml@9781774f04b824b1182ff41638687f1c01c04361 # v1.0.11

View file

@ -10,4 +10,4 @@ permissions:
jobs:
label:
uses: Sea-Haven-Industries/.github/.github/workflows/callable-labeler.yaml@7ac3528750b346f181347bb09f6af927a1c0aa14 # v1.0.6
uses: Sea-Haven-Industries/.github/.github/workflows/callable-labeler.yaml@9781774f04b824b1182ff41638687f1c01c04361 # v1.0.11

View file

@ -1,6 +1,6 @@
services:
postgres:
image: postgres:16-alpine
image: postgres:18-alpine@sha256:d3e1620b530c944afa6e887d22eb899824da68e19c52024bf98f5220c88a65b2
ports:
- "5432:5432"
environment:
@ -16,7 +16,7 @@ services:
retries: 10
minio:
image: minio/minio:RELEASE.2025-04-22T22-12-26Z
image: minio/minio:RELEASE.2025-04-22T22-12-26Z@sha256:a1ea29fa28355559ef137d71fc570e508a214ec84ff8083e39bc5428980b015e
command: server /data --console-address ":9001"
ports:
- "9000:9000"
@ -33,7 +33,7 @@ services:
retries: 10
minio-init:
image: minio/mc:RELEASE.2025-04-16T18-13-26Z
image: minio/mc:RELEASE.2025-04-16T18-13-26Z@sha256:aead63c77f9db9107f1696fb08ecb0faeda23729cde94b0f663edf4fe09728e3
depends_on:
minio:
condition: service_started

View file

@ -47,7 +47,9 @@ test.describe("Invoice Processing columns drawer (AP-45)", () => {
.getByTestId("grid-pending")
.locator('.MuiDataGrid-columnHeader[data-field="vendorName"]'),
).toBeVisible();
await page.getByTestId("tab-ready").click();
await page.getByTestId("tab-ready").evaluate((el: HTMLElement) => {
el.click();
});
await expect(
page.getByTestId("grid-ready").locator('.MuiDataGrid-columnHeader[data-field="vendorName"]'),
).toBeVisible();

3208
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -30,9 +30,9 @@
"verify": "npm run format:check && npm run lint && npm run build && npm test"
},
"engines": {
"node": ">=22.22.1"
"node": "^22.22.2 || ^24.15.0 || >=26.0.0"
},
"packageManager": "npm@11.16.0",
"packageManager": "npm@12.0.2",
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
@ -42,37 +42,37 @@
"@fontsource/ibm-plex-sans": "^5.3.0",
"@fontsource/ibm-plex-serif": "^5.3.0",
"@generouted/react-router": "^1.20.2",
"@mui/icons-material": "^7.3.2",
"@mui/material": "^7.3.2",
"@mui/x-data-grid": "^8.11.0",
"@mui/icons-material": "^9.4.0",
"@mui/material": "^9.4.0",
"@mui/x-data-grid": "^9.14.0",
"@seahaven-ap/shared": "workspace:*",
"@tanstack/react-query": "^5.85.5",
"@tanstack/react-query-devtools": "^5.85.5",
"ky": "^1.10.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-router": "^8.3.0"
"@tanstack/react-query": "^5.103.1",
"@tanstack/react-query-devtools": "^5.103.1",
"ky": "^2.1.0",
"react": "^19.3.0",
"react-dom": "^19.3.0",
"react-router": "^8.4.0"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@playwright/test": "^1.55.0",
"@redocly/cli": "^2.44.1",
"@testing-library/jest-dom": "^7.0.0",
"@testing-library/react": "^16.3.0",
"@types/node": "^24.3.0",
"@types/react": "^19.1.12",
"@types/react-dom": "^19.1.9",
"@vitejs/plugin-react": "^5.0.2",
"eslint": "^10.8.1",
"@playwright/test": "^1.63.0",
"@redocly/cli": "^2.53.3",
"@testing-library/jest-dom": "^7.0.1",
"@testing-library/react": "^16.3.3",
"@types/node": "^24.13.5",
"@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0",
"@vitejs/plugin-react": "^6.1.1",
"eslint": "^10.10.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.3",
"globals": "^16.3.0",
"jsdom": "^26.1.0",
"prettier": "^3.6.2",
"typescript": "^5.9.2",
"typescript-eslint": "^8.41.0",
"vite": "^7.1.3",
"vitest": "^3.2.4"
"eslint-plugin-react-refresh": "^0.5.7",
"globals": "^17.12.0",
"jsdom": "^30.1.0",
"prettier": "^3.9.8",
"typescript": "^5.9.3",
"typescript-eslint": "^8.70.0",
"vite": "^8.3.0",
"vitest": "^4.1.11"
}
}

View file

@ -30,24 +30,24 @@
"test:watch": "vitest"
},
"dependencies": {
"@aws-sdk/client-rds-data": "^3.848.0",
"@hono/node-server": "^2.1.0",
"@aws-sdk/client-rds-data": "^3.1135.0",
"@hono/node-server": "^2.1.1",
"@seahaven-ap/shared": "*",
"drizzle-orm": "^0.45.2",
"hono": "^4.13.1",
"jose": "^6.0.11",
"pg": "^8.16.3"
"hono": "^4.13.8",
"jose": "^6.2.12",
"pg": "^8.23.0"
},
"devDependencies": {
"@faker-js/faker": "^9.9.0",
"@types/node": "^24.3.0",
"@types/pg": "^8.15.4",
"@faker-js/faker": "^10.6.0",
"@types/node": "^24.13.5",
"@types/pg": "^8.23.1",
"drizzle-kit": "^0.31.10",
"tsx": "^4.20.3",
"typescript": "^5.9.2",
"vitest": "^3.2.4"
"tsx": "^4.23.13",
"typescript": "^5.9.3",
"vitest": "^4.1.11"
},
"engines": {
"node": ">=22.22.1"
"node": ">=24.21.0"
}
}

View file

@ -22,10 +22,10 @@
"test:watch": "vitest"
},
"devDependencies": {
"typescript": "^5.9.2",
"vitest": "^3.2.4"
"typescript": "^5.9.3",
"vitest": "^4.1.11"
},
"engines": {
"node": ">=22.22.1"
"node": ">=24.21.0"
}
}

View file

@ -18,15 +18,36 @@ export function ModuleStubPage({ title, headline, body, testId }: ModuleStubProp
py: 6,
}}
>
<Stack spacing={2} maxWidth={640}>
<Typography color="primary.dark" fontWeight={700}>
<Stack
spacing={2}
sx={{
maxWidth: 640,
}}
>
<Typography
sx={{
color: "primary.dark",
fontWeight: 700,
}}
>
{title}
</Typography>
<Typography variant="h3" component="h1" sx={{ fontWeight: 800, color: "text.primary" }}>
{headline}
</Typography>
<Typography color="text.secondary">{body}</Typography>
<Typography variant="caption" color="text.secondary">
<Typography
sx={{
color: "text.secondary",
}}
>
{body}
</Typography>
<Typography
variant="caption"
sx={{
color: "text.secondary",
}}
>
parity stub — interactive UI without live module backend
</Typography>
<Box>

View file

@ -136,9 +136,9 @@ export function ColumnsDrawer({
open={open}
onClose={onClose}
data-testid="columns-drawer"
PaperProps={{ sx: { width: 360, p: 2 } }}
slotProps={{ paper: { sx: { width: 360, p: 2 } } }}
>
<Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ mb: 2 }}>
<Stack direction="row" sx={{ alignItems: "center", justifyContent: "space-between", mb: 2 }}>
<Typography variant="h6" data-testid="columns-selector-header">
Columns
</Typography>
@ -183,7 +183,7 @@ export function ColumnsDrawer({
value={hiddenQuery}
onChange={(event) => setHiddenQuery(event.target.value)}
sx={{ mb: 1 }}
data-testid="columns-hidden-search"
slotProps={{ htmlInput: { "data-testid": "columns-hidden-search" } }}
/>
<Stack spacing={0.5} sx={{ mb: 1 }}>
{filteredHidden.map((key) => (

View file

@ -28,14 +28,6 @@ const emptySelection: GridRowSelectionModel = {
const emptyFilters = EMPTY_INVOICE_FILTERS;
const rowClickSx = {
border: 1,
borderColor: "divider",
backgroundColor: "background.paper",
cursor: "pointer",
borderRadius: 1,
} as const;
const invoiceTabsSx = {
minHeight: 44,
"& .MuiTab-root": {
@ -57,6 +49,14 @@ const invoiceTabsSx = {
},
} as const;
const rowClickSx = {
border: 1,
borderColor: "divider",
backgroundColor: "background.paper",
cursor: "pointer",
borderRadius: 1,
} as const;
function applyInvoiceFilters(rows: MockInvoice[], filters: InvoiceFilterState): MockInvoice[] {
const vendorQuery = filters.vendorQuery.trim().toLowerCase();
return rows.filter((invoice) => {

View file

@ -42,21 +42,14 @@ export function InvoiceFilterToolbar({
return (
<Stack spacing={1.5} sx={{ mb: 2 }}>
<Stack direction="row" spacing={1} alignItems="center" flexWrap="wrap" useFlexGap>
<Stack direction="row" spacing={1} useFlexGap sx={{ alignItems: "center", flexWrap: "wrap" }}>
<Box data-testid="action-filter" sx={{ ...fieldSx, minWidth: 220 }}>
<Autocomplete
size="small"
options={[...INVOICE_ACTION_OPTIONS]}
value={filters.action}
onChange={(_event, value) => onFiltersChange({ ...filters, action: value })}
renderInput={(params) => (
<TextField
{...params}
placeholder="Search"
label={undefined}
InputLabelProps={{ shrink: false }}
/>
)}
renderInput={(params) => <TextField {...params} placeholder="Search" />}
/>
</Box>
<TextField
@ -113,7 +106,7 @@ export function InvoiceFilterToolbar({
</IconButton>
</Stack>
<Stack direction="row" spacing={1} alignItems="center">
<Stack direction="row" spacing={1} sx={{ alignItems: "center" }}>
<Button variant="contained" size="small" data-testid="review-all-items">
Review All Items
</Button>

View file

@ -143,7 +143,15 @@ export function PaymentProcessingGrids() {
Expedited payments are currently unavailable for you due to various factors. (parity stub)
</Alert>
<Stack direction="row" spacing={1} alignItems="center" sx={{ mb: 2 }} flexWrap="wrap">
<Stack
direction="row"
spacing={1}
sx={{
alignItems: "center",
flexWrap: "wrap",
mb: 2,
}}
>
<Chip label="Vendor Name" variant="outlined" size="small" />
<Chip label="Payment Method" variant="outlined" size="small" />
<Chip label="Bank Account" variant="outlined" size="small" />
@ -203,8 +211,19 @@ export function PaymentProcessingGrids() {
}}
data-testid="payment-empty-stub"
>
<Typography color="text.secondary">No Result Found</Typography>
<Typography variant="caption" color="text.secondary">
<Typography
sx={{
color: "text.secondary",
}}
>
No Result Found
</Typography>
<Typography
variant="caption"
sx={{
color: "text.secondary",
}}
>
parity stub
</Typography>
</Box>
@ -212,7 +231,12 @@ export function PaymentProcessingGrids() {
<Stack direction="row" spacing={4} sx={{ mt: 2 }} data-testid="payment-footer-totals">
<Typography variant="body2">
<Box component="span" color="text.secondary">
<Box
component="span"
sx={{
color: "text.secondary",
}}
>
Total Due:{" "}
</Box>
<strong>
@ -220,13 +244,23 @@ export function PaymentProcessingGrids() {
</strong>
</Typography>
<Typography variant="body2">
<Box component="span" color="text.secondary">
<Box
component="span"
sx={{
color: "text.secondary",
}}
>
Total To Pay:{" "}
</Box>
<strong>$0.00</strong>
</Typography>
<Typography variant="body2">
<Box component="span" color="text.secondary">
<Box
component="span"
sx={{
color: "text.secondary",
}}
>
Estimated USD:{" "}
</Box>
<strong>$0.00</strong>

View file

@ -1,4 +1,4 @@
import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline";
import ErrorOutlinedIcon from "@mui/icons-material/ErrorOutlined";
import { Box, Stack } from "@mui/material";
import type { ReactElement } from "react";
@ -14,9 +14,11 @@ export function InvoiceTabLabel({
urgency?: boolean;
}): ReactElement {
return (
<Stack direction="row" spacing={0.75} alignItems="center" component="span">
<Stack direction="row" spacing={0.75} component="span" sx={{ alignItems: "center" }}>
<Box component="span">{title}</Box>
{urgency ? <ErrorOutlineIcon sx={{ fontSize: 16, color: "error.main" }} aria-hidden /> : null}
{urgency ? (
<ErrorOutlinedIcon sx={{ fontSize: 16, color: "error.main" }} aria-hidden />
) : null}
<Box
component="span"
sx={{

View file

@ -158,7 +158,15 @@ export function AppShell({ children }: AppShellProps): ReactElement {
</Typography>
</Box>
<Stack direction="row" spacing={2} alignItems="center" sx={{ ml: "auto", flexShrink: 0 }}>
<Stack
direction="row"
spacing={2}
sx={{
alignItems: "center",
ml: "auto",
flexShrink: 0,
}}
>
<IconButton
color="inherit"
aria-label="Notifications"
@ -226,7 +234,13 @@ export function AppShell({ children }: AppShellProps): ReactElement {
<ChevronRightIcon fontSize="small" />
</IconButton>
</Box>
<Stack spacing={0.5} alignItems="stretch" px={0.5}>
<Stack
spacing={0.5}
sx={{
alignItems: "stretch",
px: 0.5,
}}
>
{navModules
.filter((item) => !brand.hideModuleTestIds.includes(item.testId as never))
.map((item) => {

View file

@ -70,7 +70,14 @@ export default function ApprovalsInboxPage() {
sortable: false,
filterable: false,
renderCell: (params: GridRenderCellParams<MockApprovalItem>) => (
<Stack direction="row" spacing={1} alignItems="center" sx={{ height: "100%" }}>
<Stack
direction="row"
spacing={1}
sx={{
alignItems: "center",
height: "100%",
}}
>
<Button
size="small"
variant="contained"
@ -112,7 +119,13 @@ export default function ApprovalsInboxPage() {
<Typography component="h1" variant="h1">
Approvals
</Typography>
<Typography color="text.secondary">Approver inbox of mock pending invoices.</Typography>
<Typography
sx={{
color: "text.secondary",
}}
>
Approver inbox of mock pending invoices.
</Typography>
</Stack>
{isLoading ? (

View file

@ -135,14 +135,20 @@ function InvoiceDetailView({ invoice }: InvoiceDetailViewProps) {
<Stack
direction={{ xs: "column", sm: "row" }}
spacing={2}
justifyContent="space-between"
alignItems={{ xs: "flex-start", sm: "center" }}
sx={{
justifyContent: "space-between",
alignItems: { xs: "flex-start", sm: "center" },
}}
>
<Stack spacing={0.5}>
<Typography component="h1" variant="h1" data-testid="invoice-detail-title">
{invoice.invoiceNumber}
</Typography>
<Typography color="text.secondary">
<Typography
sx={{
color: "text.secondary",
}}
>
Status: {invoice.status.replace(/_/g, " ")} · Viewing as {user.name} ({user.role})
</Typography>
</Stack>
@ -409,12 +415,23 @@ function InvoiceDetailView({ invoice }: InvoiceDetailViewProps) {
{feedTab === "comments" ? (
<Stack spacing={2} data-testid="invoice-comments-feed">
{invoice.comments.length === 0 ? (
<Typography color="text.secondary">No comments yet.</Typography>
<Typography
sx={{
color: "text.secondary",
}}
>
No comments yet.
</Typography>
) : (
invoice.comments.map((comment) => (
<Box key={comment.id} data-testid={`invoice-comment-${comment.id}`}>
<Typography variant="subtitle2">{comment.authorName}</Typography>
<Typography variant="body2" color="text.secondary">
<Typography
variant="body2"
sx={{
color: "text.secondary",
}}
>
{new Date(comment.createdAt).toLocaleString()}
</Typography>
<Typography variant="body1" sx={{ mt: 0.5 }}>
@ -445,12 +462,23 @@ function InvoiceDetailView({ invoice }: InvoiceDetailViewProps) {
) : (
<Stack spacing={2} data-testid="invoice-activity-feed">
{invoice.activity.length === 0 ? (
<Typography color="text.secondary">No activity yet.</Typography>
<Typography
sx={{
color: "text.secondary",
}}
>
No activity yet.
</Typography>
) : (
invoice.activity.map((entry) => (
<Box key={entry.id} data-testid={`invoice-activity-${entry.id}`}>
<Typography variant="subtitle2">{entry.actorName}</Typography>
<Typography variant="body2" color="text.secondary">
<Typography
variant="body2"
sx={{
color: "text.secondary",
}}
>
{new Date(entry.createdAt).toLocaleString()}
</Typography>
<Typography variant="body1" sx={{ mt: 0.5 }}>

View file

@ -25,7 +25,13 @@ export default function LoginPage() {
>
{brand.wordmark}
</Typography>
<Typography color="text.secondary">Sign in (mock auth — Cognito in AP-14)</Typography>
<Typography
sx={{
color: "text.secondary",
}}
>
Sign in (mock auth — Cognito in AP-14)
</Typography>
<TextField label="Email" size="small" fullWidth defaultValue="adam@seahavenind.com" />
<TextField label="Password" type="password" size="small" fullWidth defaultValue="mock" />
<Button

View file

@ -44,7 +44,11 @@ export default function SettingsHubPage() {
<Typography component="h1" variant="h1">
Settings
</Typography>
<Typography color="text.secondary">
<Typography
sx={{
color: "text.secondary",
}}
>
Mock CRUD shells for master data used across Sea Haven AP.
</Typography>
</Stack>

View file

@ -93,7 +93,12 @@ function SettingsFormFields({
{validationError ? <Alert severity="error">{validationError}</Alert> : null}
<Stack spacing={2} maxWidth={480}>
<Stack
spacing={2}
sx={{
maxWidth: 480,
}}
>
{fields.map((field) => {
if (field.type === "select") {
return (

View file

@ -46,14 +46,35 @@ export function SettingsListPage<R extends GridValidRowModel & { id: string }>({
return (
<Stack spacing={2}>
<Stack direction="row" justifyContent="space-between" alignItems="flex-start" spacing={2}>
<Stack
direction="row"
spacing={2}
sx={{
justifyContent: "space-between",
alignItems: "flex-start",
}}
>
<Stack spacing={0.5}>
<Typography component="h1" variant="h1">
{title}
</Typography>
{description ? <Typography color="text.secondary">{description}</Typography> : null}
{description ? (
<Typography
sx={{
color: "text.secondary",
}}
>
{description}
</Typography>
) : null}
</Stack>
<Stack direction="row" spacing={1} alignItems="center">
<Stack
direction="row"
spacing={1}
sx={{
alignItems: "center",
}}
>
{actions}
<Button variant="contained" onClick={onNew} data-testid={`${testId}-new`}>
New