Skip to content

Commit 755a90f

Browse files
authored
fix: frontend polish - dialog layout, empty states (#1438)
* fix: restructure saga create dialog for sticky footer Replace overflow-y-auto on DialogContent with flex column layout. Form content scrolls independently while footer stays fixed at the bottom with border-t separator. * fix: treat NOT_FOUND and UNIMPLEMENTED as empty results in payments and reconciliation Wrap list queries in try/catch so pages render an empty table instead of an error state when the backend returns NOT_FOUND or UNIMPLEMENTED. --------- Co-authored-by: Ben Coombs <bjcoombs@users.noreply.github.com>
1 parent 9a727b1 commit 755a90f

3 files changed

Lines changed: 174 additions & 150 deletions

File tree

frontend/src/features/payments/hooks/use-payments.ts

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useQuery } from '@tanstack/react-query'
2+
import { ConnectError, Code } from '@connectrpc/connect'
23
import { useApiClients } from '@/api/context'
34
import { useTenantSlug } from '@/hooks/use-tenant-context'
45
import { tenantKeys } from '@/lib/query-keys'
@@ -21,27 +22,37 @@ export function usePaymentsTable() {
2122
): Promise<DataTableResult<PaymentOrder>> {
2223
if (!tenantSlug) return { items: [] }
2324

24-
const response = await clients.paymentOrder.listPaymentOrders({
25-
pagination: {
26-
pageSize: params.pageSize,
27-
pageToken: params.pageToken ?? '',
28-
},
29-
...(params.filters?.status ? { status: params.filters.status } : {}),
30-
})
25+
try {
26+
const response = await clients.paymentOrder.listPaymentOrders({
27+
pagination: {
28+
pageSize: params.pageSize,
29+
pageToken: params.pageToken ?? '',
30+
},
31+
...(params.filters?.status ? { status: params.filters.status } : {}),
32+
})
3133

32-
const items: PaymentOrder[] = (response.paymentOrders ?? []).map((p) => ({
33-
paymentOrderId: p.paymentOrderId ?? '',
34-
debtorAccountId: p.debtorAccountId ?? '',
35-
creditorReference: p.creditorReference ?? '',
36-
amount: p.amount ?? '',
37-
currency: p.currency ?? '',
38-
status: p.status ?? '',
39-
createdAt: p.createdAt ?? null,
40-
}))
34+
const items: PaymentOrder[] = (response.paymentOrders ?? []).map((p) => ({
35+
paymentOrderId: p.paymentOrderId ?? '',
36+
debtorAccountId: p.debtorAccountId ?? '',
37+
creditorReference: p.creditorReference ?? '',
38+
amount: p.amount ?? '',
39+
currency: p.currency ?? '',
40+
status: p.status ?? '',
41+
createdAt: p.createdAt ?? null,
42+
}))
4143

42-
return {
43-
items,
44-
nextPageToken: response.pagination?.nextPageToken || undefined,
44+
return {
45+
items,
46+
nextPageToken: response.pagination?.nextPageToken || undefined,
47+
}
48+
} catch (error) {
49+
if (
50+
error instanceof ConnectError &&
51+
(error.code === Code.NotFound || error.code === Code.Unimplemented)
52+
) {
53+
return { items: [] }
54+
}
55+
throw error
4556
}
4657
}
4758

frontend/src/features/reconciliation/hooks/use-reconciliation.ts

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useQuery } from '@tanstack/react-query'
2+
import { ConnectError, Code } from '@connectrpc/connect'
23
import { useApiClients } from '@/api/context'
34
import { useTenantSlug } from '@/hooks/use-tenant-context'
45
import { tenantKeys } from '@/lib/query-keys'
@@ -20,27 +21,37 @@ export function useReconciliationRunsTable() {
2021
): Promise<DataTableResult<ReconciliationRun>> {
2122
if (!tenantSlug) return { items: [] }
2223

23-
const response = await clients.accountReconciliation.listReconciliationRuns({
24-
pageSize: params.pageSize,
25-
pageToken: params.pageToken ?? '',
26-
...(params.filters?.status ? { status: params.filters.status } : {}),
27-
...(params.filters?.account_id ? { accountId: params.filters.account_id } : {}),
28-
})
24+
try {
25+
const response = await clients.accountReconciliation.listReconciliationRuns({
26+
pageSize: params.pageSize,
27+
pageToken: params.pageToken ?? '',
28+
...(params.filters?.status ? { status: params.filters.status } : {}),
29+
...(params.filters?.account_id ? { accountId: params.filters.account_id } : {}),
30+
})
2931

30-
const items: ReconciliationRun[] = (response.runs ?? []).map((run) => ({
31-
runId: run.runId ?? '',
32-
accountId: run.accountId ?? '',
33-
scope: (run.scope ?? '').replace('RECONCILIATION_SCOPE_', ''),
34-
settlementType: (run.settlementType ?? '').replace('SETTLEMENT_TYPE_', ''),
35-
status: (run.status ?? '').replace('RUN_STATUS_', ''),
36-
varianceCount: run.varianceCount ?? 0,
37-
periodStart: run.periodStart ?? '',
38-
periodEnd: run.periodEnd ?? '',
39-
}))
32+
const items: ReconciliationRun[] = (response.runs ?? []).map((run) => ({
33+
runId: run.runId ?? '',
34+
accountId: run.accountId ?? '',
35+
scope: (run.scope ?? '').replace('RECONCILIATION_SCOPE_', ''),
36+
settlementType: (run.settlementType ?? '').replace('SETTLEMENT_TYPE_', ''),
37+
status: (run.status ?? '').replace('RUN_STATUS_', ''),
38+
varianceCount: run.varianceCount ?? 0,
39+
periodStart: run.periodStart ?? '',
40+
periodEnd: run.periodEnd ?? '',
41+
}))
4042

41-
return {
42-
items,
43-
nextPageToken: response.nextPageToken || undefined,
43+
return {
44+
items,
45+
nextPageToken: response.nextPageToken || undefined,
46+
}
47+
} catch (error) {
48+
if (
49+
error instanceof ConnectError &&
50+
(error.code === Code.NotFound || error.code === Code.Unimplemented)
51+
) {
52+
return { items: [] }
53+
}
54+
throw error
4455
}
4556
}
4657

frontend/src/features/sagas/pages/create-saga-draft-dialog.tsx

Lines changed: 114 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -166,133 +166,135 @@ export function CreateSagaDraftDialog({ open, onOpenChange }: CreateSagaDraftDia
166166

167167
return (
168168
<Dialog open={open} onOpenChange={handleOpenChange}>
169-
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
170-
<DialogHeader>
169+
<DialogContent className="max-w-2xl max-h-[90vh] flex flex-col p-0">
170+
<DialogHeader className="p-6 pb-0 flex-shrink-0">
171171
<DialogTitle>Create Saga Draft</DialogTitle>
172172
<DialogDescription>
173173
Define a new Starlark saga workflow. It will be created in DRAFT status and must be
174174
activated before it can be executed.
175175
</DialogDescription>
176176
</DialogHeader>
177177

178-
<form onSubmit={(e) => void handleSubmit(e)} id="create-saga-draft-form">
179-
<div className="space-y-4 py-2">
180-
{errors.general && (
181-
<div
182-
role="alert"
183-
className="rounded-md border border-destructive/50 bg-destructive/10 px-3 py-2 text-sm text-destructive"
184-
>
185-
{errors.general}
186-
</div>
187-
)}
188-
189-
<div className="space-y-1">
190-
<label htmlFor="saga-name" className="text-sm font-medium">
191-
Name <span className="text-destructive">*</span>
192-
</label>
193-
<Input
194-
id="saga-name"
195-
value={formData.name}
196-
onChange={handleChange('name')}
197-
placeholder="savings.withdraw"
198-
aria-label="Name"
199-
aria-describedby={
200-
errors.name ? 'saga-name-error' : 'saga-name-hint'
201-
}
202-
maxLength={100}
203-
/>
204-
<p id="saga-name-hint" className="text-xs text-muted-foreground">
205-
Use <code className="font-mono">prefix.operation</code> format to link with an
206-
account type (e.g., <code className="font-mono">savings.withdraw</code>).
207-
</p>
208-
{errors.name && (
209-
<p id="saga-name-error" className="text-sm text-destructive">
210-
{errors.name}
211-
</p>
178+
<div className="flex-1 overflow-y-auto px-6 py-4 min-h-0">
179+
<form onSubmit={(e) => void handleSubmit(e)} id="create-saga-draft-form">
180+
<div className="space-y-4">
181+
{errors.general && (
182+
<div
183+
role="alert"
184+
className="rounded-md border border-destructive/50 bg-destructive/10 px-3 py-2 text-sm text-destructive"
185+
>
186+
{errors.general}
187+
</div>
212188
)}
213-
</div>
214189

215-
<div className="space-y-1">
216-
<label htmlFor="saga-display-name" className="text-sm font-medium">
217-
Display Name{' '}
218-
<span className="font-normal text-muted-foreground">(optional)</span>
219-
</label>
220-
<Input
221-
id="saga-display-name"
222-
value={formData.displayName}
223-
onChange={handleChange('displayName')}
224-
placeholder="Savings Withdrawal"
225-
aria-label="Display Name"
226-
aria-describedby={errors.displayName ? 'saga-display-name-error' : undefined}
227-
maxLength={255}
228-
/>
229-
{errors.displayName && (
230-
<p id="saga-display-name-error" className="text-sm text-destructive">
231-
{errors.displayName}
190+
<div className="space-y-1">
191+
<label htmlFor="saga-name" className="text-sm font-medium">
192+
Name <span className="text-destructive">*</span>
193+
</label>
194+
<Input
195+
id="saga-name"
196+
value={formData.name}
197+
onChange={handleChange('name')}
198+
placeholder="savings.withdraw"
199+
aria-label="Name"
200+
aria-describedby={
201+
errors.name ? 'saga-name-error' : 'saga-name-hint'
202+
}
203+
maxLength={100}
204+
/>
205+
<p id="saga-name-hint" className="text-xs text-muted-foreground">
206+
Use <code className="font-mono">prefix.operation</code> format to link with an
207+
account type (e.g., <code className="font-mono">savings.withdraw</code>).
232208
</p>
233-
)}
234-
</div>
209+
{errors.name && (
210+
<p id="saga-name-error" className="text-sm text-destructive">
211+
{errors.name}
212+
</p>
213+
)}
214+
</div>
235215

236-
<div className="space-y-1">
237-
<label htmlFor="saga-description" className="text-sm font-medium">
238-
Description{' '}
239-
<span className="font-normal text-muted-foreground">(optional)</span>
240-
</label>
241-
<textarea
242-
id="saga-description"
243-
value={formData.description}
244-
onChange={handleChange('description')}
245-
placeholder="Describe what this saga does..."
246-
aria-label="Description"
247-
aria-describedby={errors.description ? 'saga-description-error' : undefined}
248-
maxLength={1000}
249-
rows={3}
250-
className="w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs resize-none focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring placeholder:text-muted-foreground"
251-
/>
252-
{errors.description && (
253-
<p id="saga-description-error" className="text-sm text-destructive">
254-
{errors.description}
255-
</p>
256-
)}
257-
</div>
216+
<div className="space-y-1">
217+
<label htmlFor="saga-display-name" className="text-sm font-medium">
218+
Display Name{' '}
219+
<span className="font-normal text-muted-foreground">(optional)</span>
220+
</label>
221+
<Input
222+
id="saga-display-name"
223+
value={formData.displayName}
224+
onChange={handleChange('displayName')}
225+
placeholder="Savings Withdrawal"
226+
aria-label="Display Name"
227+
aria-describedby={errors.displayName ? 'saga-display-name-error' : undefined}
228+
maxLength={255}
229+
/>
230+
{errors.displayName && (
231+
<p id="saga-display-name-error" className="text-sm text-destructive">
232+
{errors.displayName}
233+
</p>
234+
)}
235+
</div>
258236

259-
<div className="space-y-1">
260-
<label className="text-sm font-medium">
261-
Script <span className="text-destructive">*</span>
262-
</label>
263-
<StarlarkEditor
264-
value={formData.script}
265-
onChange={handleScriptChange}
266-
className="min-h-[200px]"
267-
/>
268-
{errors.script && (
269-
<p id="saga-script-error" className="text-sm text-destructive">
270-
{errors.script}
271-
</p>
272-
)}
273-
</div>
237+
<div className="space-y-1">
238+
<label htmlFor="saga-description" className="text-sm font-medium">
239+
Description{' '}
240+
<span className="font-normal text-muted-foreground">(optional)</span>
241+
</label>
242+
<textarea
243+
id="saga-description"
244+
value={formData.description}
245+
onChange={handleChange('description')}
246+
placeholder="Describe what this saga does..."
247+
aria-label="Description"
248+
aria-describedby={errors.description ? 'saga-description-error' : undefined}
249+
maxLength={1000}
250+
rows={3}
251+
className="w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs resize-none focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring placeholder:text-muted-foreground"
252+
/>
253+
{errors.description && (
254+
<p id="saga-description-error" className="text-sm text-destructive">
255+
{errors.description}
256+
</p>
257+
)}
258+
</div>
274259

275-
<div className="space-y-1">
276-
<label htmlFor="saga-preconditions" className="text-sm font-medium">
277-
Preconditions CEL{' '}
278-
<span className="font-normal text-muted-foreground">(optional)</span>
279-
</label>
280-
<Input
281-
id="saga-preconditions"
282-
value={formData.preconditionsCel}
283-
onChange={handleChange('preconditionsCel')}
284-
placeholder="amount > 0"
285-
aria-label="Preconditions CEL"
286-
className="font-mono text-sm"
287-
/>
288-
<p className="text-xs text-muted-foreground">
289-
CEL expression evaluated before saga execution. Leave empty for no preconditions.
290-
</p>
260+
<div className="space-y-1">
261+
<label className="text-sm font-medium">
262+
Script <span className="text-destructive">*</span>
263+
</label>
264+
<StarlarkEditor
265+
value={formData.script}
266+
onChange={handleScriptChange}
267+
className="min-h-[200px]"
268+
/>
269+
{errors.script && (
270+
<p id="saga-script-error" className="text-sm text-destructive">
271+
{errors.script}
272+
</p>
273+
)}
274+
</div>
275+
276+
<div className="space-y-1">
277+
<label htmlFor="saga-preconditions" className="text-sm font-medium">
278+
Preconditions CEL{' '}
279+
<span className="font-normal text-muted-foreground">(optional)</span>
280+
</label>
281+
<Input
282+
id="saga-preconditions"
283+
value={formData.preconditionsCel}
284+
onChange={handleChange('preconditionsCel')}
285+
placeholder="amount > 0"
286+
aria-label="Preconditions CEL"
287+
className="font-mono text-sm"
288+
/>
289+
<p className="text-xs text-muted-foreground">
290+
CEL expression evaluated before saga execution. Leave empty for no preconditions.
291+
</p>
292+
</div>
291293
</div>
292-
</div>
293-
</form>
294+
</form>
295+
</div>
294296

295-
<DialogFooter className="sticky bottom-0 bg-background pt-4 border-t">
297+
<DialogFooter className="flex-shrink-0 p-6 pt-4 border-t bg-background">
296298
<Button variant="outline" type="button" onClick={() => onOpenChange(false)}>
297299
Cancel
298300
</Button>

0 commit comments

Comments
 (0)