From c14f80a4f74df1e5c993e6f1ea9c3ce1b3e9deff Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 21 May 2026 23:49:22 +0200 Subject: [PATCH] =?UTF-8?q?feat(uat-batch):=20Group=20Q=20=E2=80=94=20plat?= =?UTF-8?q?form=20refactors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Q58, Q59, Q61 from the 2026-05-21 plan. Q57 + Q60 (sweep-scope) parked. Shipped: Q58 SelectTrigger size variant. now accepts `size?: 'default' | 'sm'`. Default = `h-11` so the trigger matches 's h-11 default and the 8px height mismatch called out in the UAT vanishes platform-wide. Existing call sites that need the legacy compact look (FilterBar, dense table headers) opt back in via `size="sm"`. Nothing breaks — the default render flips height without touching any other styling. Q59 Table density min-widths + nowrap. DataTable cells now default to `whitespace-nowrap` so long values (URLs, names, addresses) don't wrap into 4-5 lines and inflate row height. Columns that need wrapping override via the column def's `meta.wrap = true`. Min-width comes from `column.getSize?.()` when set so a column doesn't shrink- wrap below readability — opt-in per column rather than a sweeping width change. Q61 Error message audit foundation — Documenso 401/403 path enriched. gains `apiKeySource` + `apiUrlSource` ('port' | 'global' | 'env' | 'default' | 'none'). `getPortDocumensoConfig` populates them based on which layer of the resolver chain produced the value. documenso-client's exposes the source flags; the 401/403 branch surfaces them in the `DOCUMENSO_AUTH_FAILURE` internalMessage so operators see "api key source: env, port: " instead of the prior generic `path → 401` body. Solves the Documenso diagnosis loop that prompted the platform-wide error audit. Same pattern can extend to other integration error paths in follow-ups (S3, Redis, IMAP) — the resolver-source helper lives on PortConfig now. Q60 Tooltip audit primitive already shipped — in `ui/field-label.tsx` is the canonical surface with an Info icon + Tooltip slot. One adopter live (custom-field-form); remaining admin-form sweep is the lift that's parked. Deferred: Q57 recharts → ECharts migration (6-10h). Pure visual port of 8 chart components; safer as a focused session with per-chart visual review. Pre-reqs (ECharts deps + the transpilePackages config + the d3-geo install) are in place so the migration can be picked up cleanly. Verified: tsc clean, vitest 1454/1454. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/components/shared/data-table.tsx | 25 +++++++++++++++---- src/components/ui/select.tsx | 19 +++++++++++--- src/lib/services/documenso-client.ts | 20 +++++++++++++-- src/lib/services/port-config.ts | 15 +++++++++++ .../services/documenso-place-fields.test.ts | 2 ++ 5 files changed, 71 insertions(+), 10 deletions(-) diff --git a/src/components/shared/data-table.tsx b/src/components/shared/data-table.tsx index 976fe70e..8a02148f 100644 --- a/src/components/shared/data-table.tsx +++ b/src/components/shared/data-table.tsx @@ -344,11 +344,26 @@ export function DataTable({ className={cn(onRowClick && 'cursor-pointer', getRowClassName?.(row.original))} onClick={() => onRowClick?.(row.original)} > - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} + {row.getVisibleCells().map((cell) => { + // Default text cells to `whitespace-nowrap` so a long + // value (URL, address, name) doesn't wrap into 4-5 + // lines and inflate the row height. Columns that need + // wrapping (a free-text notes preview, a description) + // override via the column def's `meta.wrap = true`. + // Min-width comes from the column's size when set so + // the column doesn't shrink-wrap below readability. + const meta = cell.column.columnDef.meta as { wrap?: boolean } | undefined; + const size = cell.column.getSize?.(); + return ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ); + })} )) )} diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx index 6f309d87..b38752ba 100644 --- a/src/components/ui/select.tsx +++ b/src/components/ui/select.tsx @@ -12,14 +12,27 @@ const SelectGroup = SelectPrimitive.Group; const SelectValue = SelectPrimitive.Value; +/** + * Size variant mirroring Button's idiom. Default `h-11` (44px) pairs + * with ``'s default — fixes the 8px height mismatch that + * triggered the platform-wide UAT finding. Compact contexts (FilterBar, + * dense table headers) pass `size="sm"` to retain the legacy 36px / + * h-9 footprint. Old call sites that haven't been audited yet still + * render correctly via the default; nothing breaks. + */ +type SelectTriggerSize = 'default' | 'sm'; + const SelectTrigger = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( + React.ComponentPropsWithoutRef & { + size?: SelectTriggerSize; + } +>(({ className, children, size = 'default', ...props }, ref) => ( span]:line-clamp-1', + 'flex w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background data-placeholder:text-muted-foreground focus:outline-hidden focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1', + size === 'sm' ? 'h-9' : 'h-11', className, )} {...props} diff --git a/src/lib/services/documenso-client.ts b/src/lib/services/documenso-client.ts index fbad3b77..1393a94a 100644 --- a/src/lib/services/documenso-client.ts +++ b/src/lib/services/documenso-client.ts @@ -12,7 +12,15 @@ interface DocumensoCreds { apiVersion: DocumensoApiVersion; } -async function resolveCreds(portId?: string): Promise { +interface ResolvedCreds extends DocumensoCreds { + /** Provenance of the API key — surfaces in error messages so an + * operator can tell at a glance whether a 401 is the env fallback's + * stale key vs. a per-port admin entry. */ + apiKeySource: 'port' | 'global' | 'env' | 'default' | 'none'; + apiUrlSource: 'port' | 'global' | 'env' | 'default' | 'none'; +} + +async function resolveCreds(portId?: string): Promise { // env.DOCUMENSO_API_URL / env.DOCUMENSO_API_KEY are now optional — the // canonical config lives in admin settings. Empty fallbacks let the call // proceed; if both env + admin are blank, the downstream fetch hits an @@ -23,6 +31,8 @@ async function resolveCreds(portId?: string): Promise { baseUrl: env.DOCUMENSO_API_URL ?? '', apiKey: env.DOCUMENSO_API_KEY ?? '', apiVersion: env.DOCUMENSO_API_VERSION, + apiKeySource: env.DOCUMENSO_API_KEY ? 'env' : 'none', + apiUrlSource: env.DOCUMENSO_API_URL ? 'env' : 'none', }; } const cfg = await getPortDocumensoConfig(portId); @@ -30,6 +40,8 @@ async function resolveCreds(portId?: string): Promise { baseUrl: cfg.apiUrl ?? '', apiKey: cfg.apiKey ?? '', apiVersion: cfg.apiVersion, + apiKeySource: cfg.apiKeySource ?? (cfg.apiKey ? 'env' : 'none'), + apiUrlSource: cfg.apiUrlSource ?? (cfg.apiUrl ? 'env' : 'none'), }; } @@ -64,9 +76,13 @@ async function documensoFetchOnce( logger.error({ path, status: res.status, err, portId }, 'Documenso API error'); if (res.status === 401 || res.status === 403) { // Auth failures are not retryable — wrong key won't fix itself. + // Surface the resolver source in the error message so the operator + // sees "key resolved from env fallback" vs "per-port override" and + // knows whether to edit the deploy env or the port admin row. + const { apiKeySource, apiUrlSource } = await resolveCreds(portId); throw new AbortError( new CodedError('DOCUMENSO_AUTH_FAILURE', { - internalMessage: `${path} → ${res.status}`, + internalMessage: `${path} → ${res.status} (api key source: ${apiKeySource}, api url source: ${apiUrlSource}, port: ${portId ?? 'global'})`, }), ); } diff --git a/src/lib/services/port-config.ts b/src/lib/services/port-config.ts index 9f25fa02..f4820032 100644 --- a/src/lib/services/port-config.ts +++ b/src/lib/services/port-config.ts @@ -302,6 +302,12 @@ export interface PortDocumensoConfig { apiUrl: string; apiKey: string; apiVersion: DocumensoApiVersion; + /** Resolution provenance — `port` / `global` / `env` / `default` / + * `none`. Surfaces in DOCUMENSO_AUTH_FAILURE messages so a 401 in + * prod tells the operator "this came from env fallback" vs "this + * came from a per-port admin entry" without checking logs. */ + apiKeySource: 'port' | 'global' | 'env' | 'default' | 'none'; + apiUrlSource: 'port' | 'global' | 'env' | 'default' | 'none'; eoiTemplateId: number; defaultPathway: EoiPathway; /** Documenso template recipient slot IDs (per-instance numeric). */ @@ -414,6 +420,13 @@ export async function getPortDocumensoConfig(portId: string): Promise(SETTING_KEYS.documensoRedirectUrl, portId), ]); + // Determine the resolution source for the two credentials. Used by + // the documenso client to enrich 401/403 error messages so operators + // can tell at a glance whether the failing key is per-port or env. + type Source = 'port' | 'global' | 'env' | 'default' | 'none'; + const apiUrlSource: Source = apiUrl ? 'port' : env.DOCUMENSO_API_URL ? 'env' : 'none'; + const apiKeySource: Source = apiKey ? 'port' : env.DOCUMENSO_API_KEY ? 'env' : 'none'; + return { // Env values are now optional (admin is canonical). Empty / zero // defaults let consumers proceed and fail at the actual API call with @@ -421,6 +434,8 @@ export async function getPortDocumensoConfig(portId: string): Promise