@aiaiai-pt/design-system 0.10.1 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
// DS-internal sibling imports (this component lives in the DS now, #73 73f —
|
|
3
3
|
// converged so admin preview AND portal runtime mount the SAME renderer).
|
|
4
4
|
import Badge from "./Badge.svelte";
|
|
5
|
+
import Button from "./Button.svelte";
|
|
6
|
+
import Alert from "./Alert.svelte";
|
|
5
7
|
import CodeBlock from "./CodeBlock.svelte";
|
|
6
8
|
import Input from "./Input.svelte";
|
|
9
|
+
import MapPicker from "./MapPicker.svelte";
|
|
7
10
|
import Select from "./Select.svelte";
|
|
8
11
|
import Tag from "./Tag.svelte";
|
|
12
|
+
import type { Snippet } from "svelte";
|
|
9
13
|
import {
|
|
10
14
|
buildActionPayload,
|
|
11
15
|
placementConsequenceRows as buildPlacementConsequenceRows,
|
|
@@ -39,6 +43,15 @@
|
|
|
39
43
|
criteria?: Entity[];
|
|
40
44
|
};
|
|
41
45
|
|
|
46
|
+
/**
|
|
47
|
+
* The `apply` seam (see dev_docs/adl/action-form-apply-seam.md). The renderer
|
|
48
|
+
* owns the form + validation + submit button + state; the CONSUMER injects how
|
|
49
|
+
* to actually send it. Foundry's `applyAction` model: every consumer converges
|
|
50
|
+
* on the one action engine; only transport/auth/captcha differ. Receives the
|
|
51
|
+
* normalized ActionPayload; resolves `{ ok }` (and may navigate on success).
|
|
52
|
+
*/
|
|
53
|
+
type ApplyResult = { ok: boolean; status?: number; error?: string };
|
|
54
|
+
|
|
42
55
|
interface Props {
|
|
43
56
|
action: Entity | null;
|
|
44
57
|
placement?: Entity | null;
|
|
@@ -46,6 +59,13 @@
|
|
|
46
59
|
criteria?: Entity[];
|
|
47
60
|
mode?: RendererMode;
|
|
48
61
|
schema?: ActionSchema | null;
|
|
62
|
+
/** Injected apply. Required for a working submit button; absent → no button
|
|
63
|
+
* (so admin-preview / adapter-preview stay read-only). */
|
|
64
|
+
onApply?: (payload: Record<string, unknown>) => Promise<ApplyResult>;
|
|
65
|
+
/** Environment-specific captcha (e.g. Turnstile), rendered above the button
|
|
66
|
+
* in submit modes. */
|
|
67
|
+
captcha?: Snippet;
|
|
68
|
+
submitLabel?: string;
|
|
49
69
|
}
|
|
50
70
|
|
|
51
71
|
let {
|
|
@@ -55,10 +75,24 @@
|
|
|
55
75
|
criteria = [],
|
|
56
76
|
mode = "admin-preview",
|
|
57
77
|
schema = null,
|
|
78
|
+
onApply = undefined,
|
|
79
|
+
captcha = undefined,
|
|
80
|
+
submitLabel = "Submit",
|
|
58
81
|
}: Props = $props();
|
|
59
82
|
|
|
60
83
|
let values = $state<Record<string, unknown>>({});
|
|
61
84
|
|
|
85
|
+
// Submit state (apply seam). Only meaningful in submit modes with an onApply.
|
|
86
|
+
let submitting = $state(false);
|
|
87
|
+
let submitError = $state<string | null>(null);
|
|
88
|
+
let submitted = $state(false);
|
|
89
|
+
|
|
90
|
+
// A form is a SUBMIT form (button shown) only in the submit modes AND when the
|
|
91
|
+
// consumer wired an apply. Preview modes (admin-preview/adapter-preview) and a
|
|
92
|
+
// missing onApply stay read-only — the button never renders.
|
|
93
|
+
const isSubmitMode = $derived(mode === "public-submit" || mode === "admin-execute");
|
|
94
|
+
const showSubmit = $derived(isSubmitMode && onApply !== undefined);
|
|
95
|
+
|
|
62
96
|
const renderedAction = $derived((schema?.action as Entity | null | undefined) ?? action);
|
|
63
97
|
const renderedPlacement = $derived((schema?.placement as Entity | null | undefined) ?? placement);
|
|
64
98
|
const renderedParameters = $derived((schema?.parameters as Entity[] | undefined) ?? parameters);
|
|
@@ -80,6 +114,36 @@
|
|
|
80
114
|
const sections = $derived(schemaSections ?? groupIntoSections(visibleParameters));
|
|
81
115
|
const payload = $derived(buildPayload());
|
|
82
116
|
const payloadJson = $derived(JSON.stringify(payload, null, 2));
|
|
117
|
+
|
|
118
|
+
// Client-side submit gate (layer 1 — see the ADL): every required, visible
|
|
119
|
+
// parameter must have a non-empty value. Server-side submission criteria are
|
|
120
|
+
// enforced by the BFF on submit and surfaced via `submitError`.
|
|
121
|
+
function isEmpty(value: unknown): boolean {
|
|
122
|
+
return value === undefined || value === null || value === "";
|
|
123
|
+
}
|
|
124
|
+
const canSubmit = $derived(
|
|
125
|
+
visibleParameters
|
|
126
|
+
.filter((parameter) => parameter.required)
|
|
127
|
+
.every((parameter) => !isEmpty(values[parameterKey(parameter)])),
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
async function handleSubmit(): Promise<void> {
|
|
131
|
+
if (!onApply || submitting || !canSubmit) return;
|
|
132
|
+
submitError = null;
|
|
133
|
+
submitting = true;
|
|
134
|
+
try {
|
|
135
|
+
const result = await onApply(buildPayload());
|
|
136
|
+
if (result.ok) {
|
|
137
|
+
submitted = true; // a consumer that navigates (portal → tracker) supersedes this
|
|
138
|
+
} else {
|
|
139
|
+
submitError = result.error ?? "We couldn't submit your form. Please try again.";
|
|
140
|
+
}
|
|
141
|
+
} catch {
|
|
142
|
+
submitError = "Something went wrong. Please try again.";
|
|
143
|
+
} finally {
|
|
144
|
+
submitting = false;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
83
147
|
const criteriaSummary = $derived(
|
|
84
148
|
renderedCriteria
|
|
85
149
|
.filter((criterion) => criterion.is_active !== false)
|
|
@@ -286,6 +350,23 @@
|
|
|
286
350
|
]}
|
|
287
351
|
onchange={(value: string) => setValue(key, value === "true")}
|
|
288
352
|
/>
|
|
353
|
+
{:else if type === "geo"}
|
|
354
|
+
<!-- A `geo` parameter renders the DS map-picker; its value is the
|
|
355
|
+
[lon, lat] the BFF's GEO_PARSE binding transform turns into a Point.
|
|
356
|
+
The renderer stays generic — geo is just another param type, the
|
|
357
|
+
occurrence/location coupling lives entirely in the ontology binding. -->
|
|
358
|
+
<MapPicker
|
|
359
|
+
mode="point"
|
|
360
|
+
height="20rem"
|
|
361
|
+
label={String(parameter.label ?? key)}
|
|
362
|
+
center={Array.isArray(parameter.default_value)
|
|
363
|
+
? (parameter.default_value as [number, number])
|
|
364
|
+
: undefined}
|
|
365
|
+
value={Array.isArray(values[key])
|
|
366
|
+
? (values[key] as [number, number])
|
|
367
|
+
: undefined}
|
|
368
|
+
onchange={(coords: [number, number]) => setValue(key, coords)}
|
|
369
|
+
/>
|
|
289
370
|
{:else}
|
|
290
371
|
<Input
|
|
291
372
|
label={String(parameter.label ?? key)}
|
|
@@ -332,6 +413,31 @@
|
|
|
332
413
|
</form>
|
|
333
414
|
{/if}
|
|
334
415
|
|
|
416
|
+
<!-- Submit (apply seam) — only in submit modes with an injected onApply, so
|
|
417
|
+
preview modes never render a button. Captcha snippet sits above it. -->
|
|
418
|
+
{#if showSubmit && renderedAction && orderedParameters.length > 0}
|
|
419
|
+
<div class="submit-area">
|
|
420
|
+
{#if submitted}
|
|
421
|
+
<Alert variant="success">Your submission was received.</Alert>
|
|
422
|
+
{:else}
|
|
423
|
+
{#if submitError}
|
|
424
|
+
<Alert variant="error">{submitError}</Alert>
|
|
425
|
+
{/if}
|
|
426
|
+
{#if captcha}
|
|
427
|
+
<div class="submit-captcha">{@render captcha()}</div>
|
|
428
|
+
{/if}
|
|
429
|
+
<Button
|
|
430
|
+
variant="primary"
|
|
431
|
+
loading={submitting}
|
|
432
|
+
disabled={submitting || !canSubmit}
|
|
433
|
+
onclick={handleSubmit}
|
|
434
|
+
>
|
|
435
|
+
{submitLabel}
|
|
436
|
+
</Button>
|
|
437
|
+
{/if}
|
|
438
|
+
</div>
|
|
439
|
+
{/if}
|
|
440
|
+
|
|
335
441
|
{#if mode === "admin-preview"}
|
|
336
442
|
<div class="admin-preview">
|
|
337
443
|
<section class="preview-block">
|
|
@@ -420,12 +526,22 @@
|
|
|
420
526
|
|
|
421
527
|
.rendered-form,
|
|
422
528
|
.admin-preview,
|
|
423
|
-
.preview-block
|
|
529
|
+
.preview-block,
|
|
530
|
+
.submit-area {
|
|
424
531
|
display: flex;
|
|
425
532
|
flex-direction: column;
|
|
426
533
|
gap: var(--space-md);
|
|
427
534
|
}
|
|
428
535
|
|
|
536
|
+
.submit-area {
|
|
537
|
+
align-items: flex-start;
|
|
538
|
+
gap: var(--space-lg);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
.submit-captcha {
|
|
542
|
+
min-height: var(--space-4xl);
|
|
543
|
+
}
|
|
544
|
+
|
|
429
545
|
.preview-block h4 {
|
|
430
546
|
margin: 0;
|
|
431
547
|
font-size: var(--type-body-size);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Snippet } from "svelte";
|
|
1
2
|
import { type RendererMode } from "./action-form-renderer-payload";
|
|
2
3
|
/**
|
|
3
4
|
* The renderer treats every parameter/action/placement as a loose record
|
|
@@ -22,6 +23,18 @@ type ActionSchema = {
|
|
|
22
23
|
parameters?: Entity[];
|
|
23
24
|
criteria?: Entity[];
|
|
24
25
|
};
|
|
26
|
+
/**
|
|
27
|
+
* The `apply` seam (see dev_docs/adl/action-form-apply-seam.md). The renderer
|
|
28
|
+
* owns the form + validation + submit button + state; the CONSUMER injects how
|
|
29
|
+
* to actually send it. Foundry's `applyAction` model: every consumer converges
|
|
30
|
+
* on the one action engine; only transport/auth/captcha differ. Receives the
|
|
31
|
+
* normalized ActionPayload; resolves `{ ok }` (and may navigate on success).
|
|
32
|
+
*/
|
|
33
|
+
type ApplyResult = {
|
|
34
|
+
ok: boolean;
|
|
35
|
+
status?: number;
|
|
36
|
+
error?: string;
|
|
37
|
+
};
|
|
25
38
|
interface Props {
|
|
26
39
|
action: Entity | null;
|
|
27
40
|
placement?: Entity | null;
|
|
@@ -29,6 +42,13 @@ interface Props {
|
|
|
29
42
|
criteria?: Entity[];
|
|
30
43
|
mode?: RendererMode;
|
|
31
44
|
schema?: ActionSchema | null;
|
|
45
|
+
/** Injected apply. Required for a working submit button; absent → no button
|
|
46
|
+
* (so admin-preview / adapter-preview stay read-only). */
|
|
47
|
+
onApply?: (payload: Record<string, unknown>) => Promise<ApplyResult>;
|
|
48
|
+
/** Environment-specific captcha (e.g. Turnstile), rendered above the button
|
|
49
|
+
* in submit modes. */
|
|
50
|
+
captcha?: Snippet;
|
|
51
|
+
submitLabel?: string;
|
|
32
52
|
}
|
|
33
53
|
declare const ActionFormRenderer: import("svelte").Component<Props, {}, "">;
|
|
34
54
|
type ActionFormRenderer = ReturnType<typeof ActionFormRenderer>;
|