feat(uat-batch): Group Q — platform refactors
Q58, Q59, Q61 from the 2026-05-21 plan. Q57 + Q60 (sweep-scope) parked.
Shipped:
Q58 SelectTrigger size variant. <SelectTrigger> now accepts
`size?: 'default' | 'sm'`. Default = `h-11` so the trigger
matches <Input>'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. <PortDocumensoConfig> 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 <ResolvedCreds> exposes the source flags;
the 401/403 branch surfaces them in the
`DOCUMENSO_AUTH_FAILURE` internalMessage so operators see
"api key source: env, port: <id>" 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 — <FieldLabel> 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) <noreply@anthropic.com>
This commit is contained in:
@@ -344,11 +344,26 @@ export function DataTable<TData>({
|
|||||||
className={cn(onRowClick && 'cursor-pointer', getRowClassName?.(row.original))}
|
className={cn(onRowClick && 'cursor-pointer', getRowClassName?.(row.original))}
|
||||||
onClick={() => onRowClick?.(row.original)}
|
onClick={() => onRowClick?.(row.original)}
|
||||||
>
|
>
|
||||||
{row.getVisibleCells().map((cell) => (
|
{row.getVisibleCells().map((cell) => {
|
||||||
<TableCell key={cell.id}>
|
// Default text cells to `whitespace-nowrap` so a long
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
// value (URL, address, name) doesn't wrap into 4-5
|
||||||
</TableCell>
|
// 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 (
|
||||||
|
<TableCell
|
||||||
|
key={cell.id}
|
||||||
|
className={cn(meta?.wrap ? undefined : 'whitespace-nowrap')}
|
||||||
|
style={size ? { minWidth: size } : undefined}
|
||||||
|
>
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</TableCell>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -12,14 +12,27 @@ const SelectGroup = SelectPrimitive.Group;
|
|||||||
|
|
||||||
const SelectValue = SelectPrimitive.Value;
|
const SelectValue = SelectPrimitive.Value;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Size variant mirroring Button's idiom. Default `h-11` (44px) pairs
|
||||||
|
* with `<Input>`'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<
|
const SelectTrigger = React.forwardRef<
|
||||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> & {
|
||||||
>(({ className, children, ...props }, ref) => (
|
size?: SelectTriggerSize;
|
||||||
|
}
|
||||||
|
>(({ className, children, size = 'default', ...props }, ref) => (
|
||||||
<SelectPrimitive.Trigger
|
<SelectPrimitive.Trigger
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex h-9 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',
|
'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,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -12,7 +12,15 @@ interface DocumensoCreds {
|
|||||||
apiVersion: DocumensoApiVersion;
|
apiVersion: DocumensoApiVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resolveCreds(portId?: string): Promise<DocumensoCreds> {
|
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<ResolvedCreds> {
|
||||||
// env.DOCUMENSO_API_URL / env.DOCUMENSO_API_KEY are now optional — the
|
// env.DOCUMENSO_API_URL / env.DOCUMENSO_API_KEY are now optional — the
|
||||||
// canonical config lives in admin settings. Empty fallbacks let the call
|
// canonical config lives in admin settings. Empty fallbacks let the call
|
||||||
// proceed; if both env + admin are blank, the downstream fetch hits an
|
// proceed; if both env + admin are blank, the downstream fetch hits an
|
||||||
@@ -23,6 +31,8 @@ async function resolveCreds(portId?: string): Promise<DocumensoCreds> {
|
|||||||
baseUrl: env.DOCUMENSO_API_URL ?? '',
|
baseUrl: env.DOCUMENSO_API_URL ?? '',
|
||||||
apiKey: env.DOCUMENSO_API_KEY ?? '',
|
apiKey: env.DOCUMENSO_API_KEY ?? '',
|
||||||
apiVersion: env.DOCUMENSO_API_VERSION,
|
apiVersion: env.DOCUMENSO_API_VERSION,
|
||||||
|
apiKeySource: env.DOCUMENSO_API_KEY ? 'env' : 'none',
|
||||||
|
apiUrlSource: env.DOCUMENSO_API_URL ? 'env' : 'none',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const cfg = await getPortDocumensoConfig(portId);
|
const cfg = await getPortDocumensoConfig(portId);
|
||||||
@@ -30,6 +40,8 @@ async function resolveCreds(portId?: string): Promise<DocumensoCreds> {
|
|||||||
baseUrl: cfg.apiUrl ?? '',
|
baseUrl: cfg.apiUrl ?? '',
|
||||||
apiKey: cfg.apiKey ?? '',
|
apiKey: cfg.apiKey ?? '',
|
||||||
apiVersion: cfg.apiVersion,
|
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');
|
logger.error({ path, status: res.status, err, portId }, 'Documenso API error');
|
||||||
if (res.status === 401 || res.status === 403) {
|
if (res.status === 401 || res.status === 403) {
|
||||||
// Auth failures are not retryable — wrong key won't fix itself.
|
// 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(
|
throw new AbortError(
|
||||||
new CodedError('DOCUMENSO_AUTH_FAILURE', {
|
new CodedError('DOCUMENSO_AUTH_FAILURE', {
|
||||||
internalMessage: `${path} → ${res.status}`,
|
internalMessage: `${path} → ${res.status} (api key source: ${apiKeySource}, api url source: ${apiUrlSource}, port: ${portId ?? 'global'})`,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -302,6 +302,12 @@ export interface PortDocumensoConfig {
|
|||||||
apiUrl: string;
|
apiUrl: string;
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
apiVersion: DocumensoApiVersion;
|
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;
|
eoiTemplateId: number;
|
||||||
defaultPathway: EoiPathway;
|
defaultPathway: EoiPathway;
|
||||||
/** Documenso template recipient slot IDs (per-instance numeric). */
|
/** Documenso template recipient slot IDs (per-instance numeric). */
|
||||||
@@ -414,6 +420,13 @@ export async function getPortDocumensoConfig(portId: string): Promise<PortDocume
|
|||||||
readSetting<string>(SETTING_KEYS.documensoRedirectUrl, portId),
|
readSetting<string>(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 {
|
return {
|
||||||
// Env values are now optional (admin is canonical). Empty / zero
|
// Env values are now optional (admin is canonical). Empty / zero
|
||||||
// defaults let consumers proceed and fail at the actual API call with
|
// defaults let consumers proceed and fail at the actual API call with
|
||||||
@@ -421,6 +434,8 @@ export async function getPortDocumensoConfig(portId: string): Promise<PortDocume
|
|||||||
apiUrl: apiUrl ?? env.DOCUMENSO_API_URL ?? '',
|
apiUrl: apiUrl ?? env.DOCUMENSO_API_URL ?? '',
|
||||||
apiKey: apiKey ?? env.DOCUMENSO_API_KEY ?? '',
|
apiKey: apiKey ?? env.DOCUMENSO_API_KEY ?? '',
|
||||||
apiVersion: apiVersion ?? env.DOCUMENSO_API_VERSION,
|
apiVersion: apiVersion ?? env.DOCUMENSO_API_VERSION,
|
||||||
|
apiKeySource,
|
||||||
|
apiUrlSource,
|
||||||
eoiTemplateId: toIntOrNull(eoiTemplateId) ?? env.DOCUMENSO_TEMPLATE_ID_EOI ?? 0,
|
eoiTemplateId: toIntOrNull(eoiTemplateId) ?? env.DOCUMENSO_TEMPLATE_ID_EOI ?? 0,
|
||||||
clientRecipientId: toIntOrNull(clientRecipientId) ?? env.DOCUMENSO_CLIENT_RECIPIENT_ID ?? 0,
|
clientRecipientId: toIntOrNull(clientRecipientId) ?? env.DOCUMENSO_CLIENT_RECIPIENT_ID ?? 0,
|
||||||
developerRecipientId:
|
developerRecipientId:
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ function configurePort(version: 'v1' | 'v2'): void {
|
|||||||
vi.mocked(portConfig.getPortDocumensoConfig).mockResolvedValue({
|
vi.mocked(portConfig.getPortDocumensoConfig).mockResolvedValue({
|
||||||
apiUrl: 'https://documenso.test',
|
apiUrl: 'https://documenso.test',
|
||||||
apiKey: 'sk_test',
|
apiKey: 'sk_test',
|
||||||
|
apiKeySource: 'port',
|
||||||
|
apiUrlSource: 'port',
|
||||||
apiVersion: version,
|
apiVersion: version,
|
||||||
eoiTemplateId: 8,
|
eoiTemplateId: 8,
|
||||||
defaultPathway: 'documenso-template',
|
defaultPathway: 'documenso-template',
|
||||||
|
|||||||
Reference in New Issue
Block a user