feat(admin-settings): radio field type + adopt for Documenso signing-order + send-mode
Adds a 'radio' SettingType the registry-driven admin form can render. Same shape as 'select' (options list, enum validation, resolved/source badges), but renders inline radio cards instead of a dropdown so each option's consequences sit side-by-side for the admin. Adopted on the two highest-stakes Documenso behaviour toggles: - `eoi_send_mode` — Manual vs Auto signing-invitation dispatch - `documenso_signing_order` — Parallel vs Sequential recipient flow Both choices are binary and materially different (one auto-sends mail, the other doesn't; one routes signing serially, the other in parallel), so the upfront comparison beats a hidden dropdown. `documenso_redirect_url` keeps its url-input — it's already a single free-text field with no enum. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,7 @@ type SettingType =
|
|||||||
| 'number'
|
| 'number'
|
||||||
| 'boolean'
|
| 'boolean'
|
||||||
| 'select'
|
| 'select'
|
||||||
|
| 'radio'
|
||||||
| 'url'
|
| 'url'
|
||||||
| 'email'
|
| 'email'
|
||||||
| 'textarea'
|
| 'textarea'
|
||||||
@@ -526,6 +527,36 @@ function FieldInput({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (entry.type === 'radio' && entry.options) {
|
||||||
|
// Inline radio group — clearer than a dropdown for 2–3 mutually-exclusive
|
||||||
|
// options where each choice has materially different consequences (e.g.
|
||||||
|
// auto-vs-manual signing dispatch). Falls through to plain Select when
|
||||||
|
// the option count grows.
|
||||||
|
const current = value == null ? null : String(value);
|
||||||
|
return (
|
||||||
|
<div role="radiogroup" aria-labelledby={`${entry.key}-label`} className="space-y-2">
|
||||||
|
{entry.options.map((o) => {
|
||||||
|
const checked = current === o.value;
|
||||||
|
return (
|
||||||
|
<label
|
||||||
|
key={o.value}
|
||||||
|
className="flex cursor-pointer items-start gap-2 rounded-md border border-input bg-background p-2 text-sm hover:bg-accent/40 has-[:checked]:border-primary has-[:checked]:bg-accent/60"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name={entry.key}
|
||||||
|
value={o.value}
|
||||||
|
checked={checked}
|
||||||
|
onChange={() => onChange(o.value)}
|
||||||
|
className="mt-1 h-4 w-4 cursor-pointer accent-primary"
|
||||||
|
/>
|
||||||
|
<span className="flex-1">{o.label}</span>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
if (entry.type === 'select' && entry.options) {
|
if (entry.type === 'select' && entry.options) {
|
||||||
// Radix Select rejects an empty-string `value` because that's its internal
|
// Radix Select rejects an empty-string `value` because that's its internal
|
||||||
// sentinel for "cleared". Pass `undefined` instead so the placeholder
|
// sentinel for "cleared". Pass `undefined` instead so the placeholder
|
||||||
|
|||||||
@@ -210,10 +210,10 @@ export const REGISTRY: SettingEntry[] = [
|
|||||||
label: 'Initial signing-invitation email behaviour',
|
label: 'Initial signing-invitation email behaviour',
|
||||||
description:
|
description:
|
||||||
'Auto = the system sends the branded "please sign" email immediately when an EOI/contract/reservation is generated. Manual = the document is generated and the signing URL appears in the UI; a rep clicks "Send invitation" to dispatch. Applies to all document types, not just EOI.',
|
'Auto = the system sends the branded "please sign" email immediately when an EOI/contract/reservation is generated. Manual = the document is generated and the signing URL appears in the UI; a rep clicks "Send invitation" to dispatch. Applies to all document types, not just EOI.',
|
||||||
type: 'select',
|
type: 'radio',
|
||||||
options: [
|
options: [
|
||||||
{ value: 'manual', label: 'Manual (rep clicks Send after generation)' },
|
{ value: 'manual', label: 'Manual — rep clicks Send after generation' },
|
||||||
{ value: 'auto', label: 'Auto (send branded email on generate)' },
|
{ value: 'auto', label: 'Auto — send branded email on generate' },
|
||||||
],
|
],
|
||||||
scope: 'port',
|
scope: 'port',
|
||||||
defaultValue: 'manual',
|
defaultValue: 'manual',
|
||||||
@@ -241,11 +241,11 @@ export const REGISTRY: SettingEntry[] = [
|
|||||||
section: 'documenso.behavior',
|
section: 'documenso.behavior',
|
||||||
label: 'Signing order',
|
label: 'Signing order',
|
||||||
description:
|
description:
|
||||||
'PARALLEL = all recipients can sign at once. SEQUENTIAL = each waits for the previous (v2 only - v1 always parallel).',
|
'PARALLEL = all recipients can sign at once. SEQUENTIAL = each waits for the previous (v2 only — v1 always parallel).',
|
||||||
type: 'select',
|
type: 'radio',
|
||||||
options: [
|
options: [
|
||||||
{ value: 'PARALLEL', label: 'Parallel - all recipients sign concurrently' },
|
{ value: 'PARALLEL', label: 'Parallel — all recipients sign concurrently' },
|
||||||
{ value: 'SEQUENTIAL', label: 'Sequential - order matters (v2 only)' },
|
{ value: 'SEQUENTIAL', label: 'Sequential — order matters (v2 only)' },
|
||||||
],
|
],
|
||||||
scope: 'port',
|
scope: 'port',
|
||||||
defaultValue: 'PARALLEL',
|
defaultValue: 'PARALLEL',
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ function defaultValidator(entry: SettingEntry): z.ZodTypeAny {
|
|||||||
case 'boolean':
|
case 'boolean':
|
||||||
return z.coerce.boolean();
|
return z.coerce.boolean();
|
||||||
case 'select':
|
case 'select':
|
||||||
|
case 'radio':
|
||||||
if (entry.options) {
|
if (entry.options) {
|
||||||
return z.enum(entry.options.map((o) => o.value) as [string, ...string[]]);
|
return z.enum(entry.options.map((o) => o.value) as [string, ...string[]]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export type SettingType =
|
|||||||
| 'number'
|
| 'number'
|
||||||
| 'boolean'
|
| 'boolean'
|
||||||
| 'select'
|
| 'select'
|
||||||
|
| 'radio'
|
||||||
| 'url'
|
| 'url'
|
||||||
| 'email'
|
| 'email'
|
||||||
| 'textarea'
|
| 'textarea'
|
||||||
|
|||||||
Reference in New Issue
Block a user