@happyvertical/smrt-fields 0.40.59 → 0.40.61
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.
- package/AGENTS.md +58 -12
- package/README.md +90 -2
- package/dist/__smrt-register__.d.ts +2 -0
- package/dist/__smrt-register__.d.ts.map +1 -0
- package/dist/cache.d.ts +18 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/collections/FieldPolicyCollection.d.ts +95 -0
- package/dist/collections/FieldPolicyCollection.d.ts.map +1 -0
- package/dist/field-definitions.d.ts +77 -0
- package/dist/field-definitions.d.ts.map +1 -0
- package/dist/field-policy-resolver.d.ts +46 -0
- package/dist/field-policy-resolver.d.ts.map +1 -0
- package/dist/index.d.ts +9 -532
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +752 -55
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +45 -3
- package/dist/models/FieldPolicy.d.ts +163 -0
- package/dist/models/FieldPolicy.d.ts.map +1 -0
- package/dist/permissions.d.ts +9 -0
- package/dist/permissions.d.ts.map +1 -0
- package/dist/playground.d.ts +2 -0
- package/dist/playground.d.ts.map +1 -0
- package/dist/playground.js +39 -0
- package/dist/playground.js.map +1 -0
- package/dist/settings-catalog.d.ts +73 -0
- package/dist/settings-catalog.d.ts.map +1 -0
- package/dist/smrt-knowledge.json +50 -7
- package/dist/svelte/__tests__/FieldPolicyControlPanel.test.js +223 -0
- package/dist/svelte/__tests__/FieldPolicyGear.test.js +503 -0
- package/dist/svelte/__tests__/FormHelp.test.js +138 -0
- package/dist/svelte/__tests__/ModeUX.test.js +109 -0
- package/dist/svelte/__tests__/ObjectForm.test.js +556 -0
- package/dist/svelte/__tests__/ObjectFormGeneratedApi.integration.test.js +436 -0
- package/dist/svelte/__tests__/PolicyField.test.js +373 -0
- package/dist/svelte/__tests__/fixtures/AdvancedFieldsFixture.svelte +30 -0
- package/dist/svelte/__tests__/fixtures/AdvancedFieldsFixture.svelte.d.ts +9 -0
- package/dist/svelte/__tests__/fixtures/AdvancedFieldsFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/DomPrefillFixture.svelte +33 -0
- package/dist/svelte/__tests__/fixtures/DomPrefillFixture.svelte.d.ts +10 -0
- package/dist/svelte/__tests__/fixtures/DomPrefillFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/FormHelpFixture.svelte +19 -0
- package/dist/svelte/__tests__/fixtures/FormHelpFixture.svelte.d.ts +10 -0
- package/dist/svelte/__tests__/fixtures/FormHelpFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/GeneratedApiGearFixture.svelte +18 -0
- package/dist/svelte/__tests__/fixtures/GeneratedApiGearFixture.svelte.d.ts +11 -0
- package/dist/svelte/__tests__/fixtures/GeneratedApiGearFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/GracefulDegradeFixture.svelte +11 -0
- package/dist/svelte/__tests__/fixtures/GracefulDegradeFixture.svelte.d.ts +19 -0
- package/dist/svelte/__tests__/fixtures/GracefulDegradeFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/ModeSwitchFixture.svelte +18 -0
- package/dist/svelte/__tests__/fixtures/ModeSwitchFixture.svelte.d.ts +8 -0
- package/dist/svelte/__tests__/fixtures/ModeSwitchFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/NoIdFixture.svelte +24 -0
- package/dist/svelte/__tests__/fixtures/NoIdFixture.svelte.d.ts +13 -0
- package/dist/svelte/__tests__/fixtures/NoIdFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormActionsFixture.svelte +20 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormActionsFixture.svelte.d.ts +11 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormActionsFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormFixture.svelte +17 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormFixture.svelte.d.ts +10 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormRegistryFixture.svelte +25 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormRegistryFixture.svelte.d.ts +10 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormRegistryFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormSnippetFixture.svelte +34 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormSnippetFixture.svelte.d.ts +10 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormSnippetFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormSourceFixture.svelte +29 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormSourceFixture.svelte.d.ts +11 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormSourceFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormSourceSnippetFixture.svelte +33 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormSourceSnippetFixture.svelte.d.ts +9 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormSourceSnippetFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/PolicyDataTableFixture.svelte +40 -0
- package/dist/svelte/__tests__/fixtures/PolicyDataTableFixture.svelte.d.ts +8 -0
- package/dist/svelte/__tests__/fixtures/PolicyDataTableFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/PrefillFixture.svelte +26 -0
- package/dist/svelte/__tests__/fixtures/PrefillFixture.svelte.d.ts +13 -0
- package/dist/svelte/__tests__/fixtures/PrefillFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/ProviderFieldFixture.svelte +39 -0
- package/dist/svelte/__tests__/fixtures/ProviderFieldFixture.svelte.d.ts +17 -0
- package/dist/svelte/__tests__/fixtures/ProviderFieldFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/RegistryInput.svelte +15 -0
- package/dist/svelte/__tests__/fixtures/RegistryInput.svelte.d.ts +5 -0
- package/dist/svelte/__tests__/fixtures/RegistryInput.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/RevealPrefillFixture.svelte +29 -0
- package/dist/svelte/__tests__/fixtures/RevealPrefillFixture.svelte.d.ts +14 -0
- package/dist/svelte/__tests__/fixtures/RevealPrefillFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/SettingsCatalogStub.svelte +24 -0
- package/dist/svelte/__tests__/fixtures/SettingsCatalogStub.svelte.d.ts +14 -0
- package/dist/svelte/__tests__/fixtures/SettingsCatalogStub.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/SnippetFixture.svelte +28 -0
- package/dist/svelte/__tests__/fixtures/SnippetFixture.svelte.d.ts +13 -0
- package/dist/svelte/__tests__/fixtures/SnippetFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/ToggleModeButton.svelte +14 -0
- package/dist/svelte/__tests__/fixtures/ToggleModeButton.svelte.d.ts +19 -0
- package/dist/svelte/__tests__/fixtures/ToggleModeButton.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/TooltipFixture.svelte +25 -0
- package/dist/svelte/__tests__/fixtures/TooltipFixture.svelte.d.ts +14 -0
- package/dist/svelte/__tests__/fixtures/TooltipFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/settings-catalog.test.js +161 -0
- package/dist/svelte/components/AdvancedFields.svelte +49 -0
- package/dist/svelte/components/AdvancedFields.svelte.d.ts +17 -0
- package/dist/svelte/components/AdvancedFields.svelte.d.ts.map +1 -0
- package/dist/svelte/components/FieldInput.svelte +159 -0
- package/dist/svelte/components/FieldInput.svelte.d.ts +5 -0
- package/dist/svelte/components/FieldInput.svelte.d.ts.map +1 -0
- package/dist/svelte/components/FieldPolicyControlPanel.svelte +261 -0
- package/dist/svelte/components/FieldPolicyControlPanel.svelte.d.ts +26 -0
- package/dist/svelte/components/FieldPolicyControlPanel.svelte.d.ts.map +1 -0
- package/dist/svelte/components/FieldPolicyEditor.svelte +417 -0
- package/dist/svelte/components/FieldPolicyEditor.svelte.d.ts +18 -0
- package/dist/svelte/components/FieldPolicyEditor.svelte.d.ts.map +1 -0
- package/dist/svelte/components/FieldPolicyGearButton.svelte +17 -0
- package/dist/svelte/components/FieldPolicyGearButton.svelte.d.ts +8 -0
- package/dist/svelte/components/FieldPolicyGearButton.svelte.d.ts.map +1 -0
- package/dist/svelte/components/FieldPolicyGearProvider.svelte +135 -0
- package/dist/svelte/components/FieldPolicyGearProvider.svelte.d.ts +17 -0
- package/dist/svelte/components/FieldPolicyGearProvider.svelte.d.ts.map +1 -0
- package/dist/svelte/components/FieldPolicyProvider.svelte +89 -0
- package/dist/svelte/components/FieldPolicyProvider.svelte.d.ts +35 -0
- package/dist/svelte/components/FieldPolicyProvider.svelte.d.ts.map +1 -0
- package/dist/svelte/components/FormHelp.svelte +157 -0
- package/dist/svelte/components/FormHelp.svelte.d.ts +15 -0
- package/dist/svelte/components/FormHelp.svelte.d.ts.map +1 -0
- package/dist/svelte/components/ModeSwitch.svelte +47 -0
- package/dist/svelte/components/ModeSwitch.svelte.d.ts +10 -0
- package/dist/svelte/components/ModeSwitch.svelte.d.ts.map +1 -0
- package/dist/svelte/components/ObjectForm.svelte +351 -0
- package/dist/svelte/components/ObjectForm.svelte.d.ts +40 -0
- package/dist/svelte/components/ObjectForm.svelte.d.ts.map +1 -0
- package/dist/svelte/components/ObjectFormSourceProvider.svelte +18 -0
- package/dist/svelte/components/ObjectFormSourceProvider.svelte.d.ts +11 -0
- package/dist/svelte/components/ObjectFormSourceProvider.svelte.d.ts.map +1 -0
- package/dist/svelte/components/PolicyField.svelte +338 -0
- package/dist/svelte/components/PolicyField.svelte.d.ts +80 -0
- package/dist/svelte/components/PolicyField.svelte.d.ts.map +1 -0
- package/dist/svelte/context.svelte.d.ts +54 -0
- package/dist/svelte/context.svelte.d.ts.map +1 -0
- package/dist/svelte/context.svelte.js +55 -0
- package/dist/svelte/data-table.d.ts +17 -0
- package/dist/svelte/data-table.d.ts.map +1 -0
- package/dist/svelte/data-table.js +19 -0
- package/dist/svelte/field-policy-editor.d.ts +81 -0
- package/dist/svelte/field-policy-editor.d.ts.map +1 -0
- package/dist/svelte/field-policy-editor.js +120 -0
- package/dist/svelte/gear-context.svelte.d.ts +14 -0
- package/dist/svelte/gear-context.svelte.d.ts.map +1 -0
- package/dist/svelte/gear-context.svelte.js +15 -0
- package/dist/svelte/index.d.ts +58 -0
- package/dist/svelte/index.d.ts.map +1 -0
- package/dist/svelte/index.js +38 -0
- package/dist/svelte/input-registry.d.ts +21 -0
- package/dist/svelte/input-registry.d.ts.map +1 -0
- package/dist/svelte/input-registry.js +26 -0
- package/dist/svelte/object-form-source-context.svelte.d.ts +4 -0
- package/dist/svelte/object-form-source-context.svelte.d.ts.map +1 -0
- package/dist/svelte/object-form-source-context.svelte.js +8 -0
- package/dist/svelte/object-form-source.svelte.d.ts +18 -0
- package/dist/svelte/object-form-source.svelte.d.ts.map +1 -0
- package/dist/svelte/object-form-source.svelte.js +99 -0
- package/dist/svelte/object-form.d.ts +6 -0
- package/dist/svelte/object-form.d.ts.map +1 -0
- package/dist/svelte/object-form.js +38 -0
- package/dist/svelte/playground/FieldPolicyFormPreview.svelte +212 -0
- package/dist/svelte/playground/FieldPolicyFormPreview.svelte.d.ts +4 -0
- package/dist/svelte/playground/FieldPolicyFormPreview.svelte.d.ts.map +1 -0
- package/dist/svelte/playground/ObjectFormPreview.svelte +129 -0
- package/dist/svelte/playground/ObjectFormPreview.svelte.d.ts +4 -0
- package/dist/svelte/playground/ObjectFormPreview.svelte.d.ts.map +1 -0
- package/dist/svelte/playground.d.ts +20 -0
- package/dist/svelte/playground.d.ts.map +1 -0
- package/dist/svelte/playground.js +38 -0
- package/dist/svelte/settings-catalog.d.ts +54 -0
- package/dist/svelte/settings-catalog.d.ts.map +1 -0
- package/dist/svelte/settings-catalog.js +116 -0
- package/dist/svelte/types.d.ts +101 -0
- package/dist/svelte/types.d.ts.map +1 -0
- package/dist/svelte/types.js +4 -0
- package/dist/types.d.ts +276 -203
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js.map +1 -1
- package/package.json +33 -8
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* ObjectForm renders the safe intersection of a generated web definition and
|
|
4
|
+
* its resolved field policy. It remains transport-neutral: callers own the
|
|
5
|
+
* record and submission, while this component owns ordering, policy modes,
|
|
6
|
+
* built-in wire-type adapters, and accessible field layout.
|
|
7
|
+
*/
|
|
8
|
+
import { Form } from '@happyvertical/smrt-ui/forms';
|
|
9
|
+
import type { Component, Snippet } from 'svelte';
|
|
10
|
+
import type { ResolvedObjectFieldPolicy } from '../../types.js';
|
|
11
|
+
import { tryGetFieldPolicyGearContext } from '../gear-context.svelte.js';
|
|
12
|
+
import type {
|
|
13
|
+
FieldInputComponent,
|
|
14
|
+
FieldInputRegistry,
|
|
15
|
+
} from '../input-registry.js';
|
|
16
|
+
import { resolveObjectFormFields } from '../object-form.js';
|
|
17
|
+
import { tryGetObjectFormSource } from '../object-form-source-context.svelte.js';
|
|
18
|
+
import {
|
|
19
|
+
type FieldInputProps,
|
|
20
|
+
type ObjectFormField,
|
|
21
|
+
type ObjectFormFieldDefinition,
|
|
22
|
+
type ObjectFormFieldSnippetProps,
|
|
23
|
+
} from '../types.js';
|
|
24
|
+
import AdvancedFields from './AdvancedFields.svelte';
|
|
25
|
+
import FieldInput from './FieldInput.svelte';
|
|
26
|
+
import FieldPolicyGearButton from './FieldPolicyGearButton.svelte';
|
|
27
|
+
import FieldPolicyProvider from './FieldPolicyProvider.svelte';
|
|
28
|
+
import ModeSwitch from './ModeSwitch.svelte';
|
|
29
|
+
import PolicyField from './PolicyField.svelte';
|
|
30
|
+
|
|
31
|
+
export interface ObjectFormProps {
|
|
32
|
+
objectRef: string;
|
|
33
|
+
/**
|
|
34
|
+
* Generated browser-safe definitions — never pass raw server registry fields.
|
|
35
|
+
* Omit with `policy` to resolve both through ObjectFormSourceProvider.
|
|
36
|
+
*/
|
|
37
|
+
fields?: Readonly<Record<string, ObjectFormFieldDefinition>>;
|
|
38
|
+
/** Pre-resolved policy for the same objectRef. Omit with `fields` for a source. */
|
|
39
|
+
policy?: ResolvedObjectFieldPolicy;
|
|
40
|
+
/** Mutable create/edit record. Bind this prop to retain values in the host. */
|
|
41
|
+
value?: Record<string, unknown>;
|
|
42
|
+
/** True for creates (defaults may prefill), false for loaded edits. */
|
|
43
|
+
isNewRecord?: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Optional identity for a create session. Change this when reusing a mounted
|
|
46
|
+
* form for another new record; defaults then apply once to missing values.
|
|
47
|
+
* With no key, replacing the bound record with an empty object starts a new
|
|
48
|
+
* create session automatically.
|
|
49
|
+
*/
|
|
50
|
+
createSessionKey?: string | number;
|
|
51
|
+
disabled?: boolean;
|
|
52
|
+
showModeSwitch?: boolean;
|
|
53
|
+
/** Render a provider-registered policy gear next to the mode control. */
|
|
54
|
+
showPolicyGear?: boolean;
|
|
55
|
+
inputRegistry?: FieldInputRegistry;
|
|
56
|
+
/** Per-field custom widget snippets, keyed by field name. */
|
|
57
|
+
renderers?: Readonly<Record<string, Snippet<[ObjectFormFieldSnippetProps]>>>;
|
|
58
|
+
/** Optional controls rendered after the policy-ordered fields inside the form. */
|
|
59
|
+
actions?: Snippet;
|
|
60
|
+
class?: string;
|
|
61
|
+
onsubmit?: (event: SubmitEvent) => void;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let {
|
|
65
|
+
objectRef,
|
|
66
|
+
fields = undefined,
|
|
67
|
+
policy = undefined,
|
|
68
|
+
value = $bindable<Record<string, unknown>>({}),
|
|
69
|
+
isNewRecord = true,
|
|
70
|
+
createSessionKey,
|
|
71
|
+
disabled = false,
|
|
72
|
+
showModeSwitch = true,
|
|
73
|
+
showPolicyGear = false,
|
|
74
|
+
inputRegistry,
|
|
75
|
+
renderers = {},
|
|
76
|
+
actions,
|
|
77
|
+
class: className = '',
|
|
78
|
+
onsubmit,
|
|
79
|
+
}: ObjectFormProps = $props();
|
|
80
|
+
|
|
81
|
+
const fieldPolicyGear = tryGetFieldPolicyGearContext();
|
|
82
|
+
const objectFormSource = tryGetObjectFormSource();
|
|
83
|
+
const formInstanceId = $props.id();
|
|
84
|
+
let invalidFields = $state<Set<string>>(new Set());
|
|
85
|
+
|
|
86
|
+
let sourceResult = $state<{
|
|
87
|
+
fields: Readonly<Record<string, ObjectFormFieldDefinition>>;
|
|
88
|
+
policy: ResolvedObjectFieldPolicy;
|
|
89
|
+
} | null>(null);
|
|
90
|
+
let sourceError = $state<string | null>(null);
|
|
91
|
+
let sourceLoading = $state(false);
|
|
92
|
+
|
|
93
|
+
// Direct props preserve SSR and existing integrations. The provider path makes
|
|
94
|
+
// an app's generated definitions + resolveBatch transport reusable across all
|
|
95
|
+
// objects without per-form glue.
|
|
96
|
+
$effect(() => {
|
|
97
|
+
if (fields !== undefined || policy !== undefined) {
|
|
98
|
+
sourceLoading = false;
|
|
99
|
+
sourceResult = null;
|
|
100
|
+
if (fields === undefined || policy === undefined) {
|
|
101
|
+
sourceError =
|
|
102
|
+
'ObjectForm requires both fields and policy when direct props are used.';
|
|
103
|
+
} else if (policy.objectRef !== objectRef) {
|
|
104
|
+
sourceError = `ObjectForm policy does not match '${objectRef}'.`;
|
|
105
|
+
} else {
|
|
106
|
+
sourceError = null;
|
|
107
|
+
}
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (!objectFormSource) {
|
|
111
|
+
sourceLoading = false;
|
|
112
|
+
sourceResult = null;
|
|
113
|
+
sourceError = `No ObjectForm source is available for '${objectRef}'.`;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let active = true;
|
|
118
|
+
sourceLoading = true;
|
|
119
|
+
sourceResult = null;
|
|
120
|
+
sourceError = null;
|
|
121
|
+
void objectFormSource
|
|
122
|
+
.load(objectRef)
|
|
123
|
+
.then((resolved) => {
|
|
124
|
+
if (!active) return;
|
|
125
|
+
if (
|
|
126
|
+
resolved.definition.objectRef !== objectRef ||
|
|
127
|
+
resolved.policy.objectRef !== objectRef
|
|
128
|
+
) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
`ObjectForm source returned a mismatched objectRef for '${objectRef}'.`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
sourceResult = {
|
|
134
|
+
fields: resolved.definition.fields,
|
|
135
|
+
policy: resolved.policy,
|
|
136
|
+
};
|
|
137
|
+
})
|
|
138
|
+
.catch((error: unknown) => {
|
|
139
|
+
if (active) {
|
|
140
|
+
sourceError =
|
|
141
|
+
error instanceof Error ? error.message : 'Unable to load this form.';
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
.finally(() => {
|
|
145
|
+
if (active) sourceLoading = false;
|
|
146
|
+
});
|
|
147
|
+
return () => {
|
|
148
|
+
active = false;
|
|
149
|
+
};
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const resolvedFields = $derived(fields ?? sourceResult?.fields ?? {});
|
|
153
|
+
const resolvedPolicy = $derived(policy ?? sourceResult?.policy ?? null);
|
|
154
|
+
|
|
155
|
+
const formFields = $derived(
|
|
156
|
+
resolvedPolicy ? resolveObjectFormFields(resolvedFields, resolvedPolicy) : [],
|
|
157
|
+
);
|
|
158
|
+
const basicGroups = $derived(groupFields('basic'));
|
|
159
|
+
const advancedGroups = $derived(groupFields('advanced'));
|
|
160
|
+
|
|
161
|
+
function groupFields(
|
|
162
|
+
visibility: 'basic' | 'advanced',
|
|
163
|
+
): Array<[string | null, ObjectFormField[]]> {
|
|
164
|
+
const result = new Map<string | null, ObjectFormField[]>();
|
|
165
|
+
if (!resolvedPolicy) return [];
|
|
166
|
+
for (const field of formFields) {
|
|
167
|
+
if (resolvedPolicy.fields[field.name].visibility !== visibility) continue;
|
|
168
|
+
const group = result.get(field.group) ?? [];
|
|
169
|
+
group.push(field);
|
|
170
|
+
result.set(field.group, group);
|
|
171
|
+
}
|
|
172
|
+
return [...result.entries()];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function setValue(name: string, next: unknown): void {
|
|
176
|
+
const updated = { ...value, [name]: next };
|
|
177
|
+
lastInternalValue = updated;
|
|
178
|
+
lastObservedValue = updated;
|
|
179
|
+
value = updated;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Generated fields need a stable accessible label even when policy omits one. */
|
|
183
|
+
function fieldLabel(field: ObjectFormField): string {
|
|
184
|
+
return (
|
|
185
|
+
resolvedPolicy?.fields[field.name].label ??
|
|
186
|
+
field.name
|
|
187
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
188
|
+
.replace(/[-_]+/g, ' ')
|
|
189
|
+
.replace(/^./, (character) => character.toUpperCase())
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function setValidity(name: string, valid: boolean): void {
|
|
194
|
+
const next = new Set(invalidFields);
|
|
195
|
+
if (valid) next.delete(name);
|
|
196
|
+
else next.add(name);
|
|
197
|
+
invalidFields = next;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function fieldId(name: string): string {
|
|
201
|
+
return `${formInstanceId}-${name}`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Apply policy defaults to a create record once, before a user can interact
|
|
205
|
+
// with any control. This preserves JSON and boolean defaults (which cannot be
|
|
206
|
+
// faithfully inferred from generic DOM string assignment) and means callers
|
|
207
|
+
// receive a complete create payload. Loaded edit records never enter this path.
|
|
208
|
+
let initializedCreateSession = false;
|
|
209
|
+
let previousCreateSessionKey: string | number | undefined;
|
|
210
|
+
let previousIsNewRecord = false;
|
|
211
|
+
let lastObservedValue: Record<string, unknown> | undefined;
|
|
212
|
+
let lastInternalValue: Record<string, unknown> | undefined;
|
|
213
|
+
|
|
214
|
+
function isEmptyRecord(record: Record<string, unknown>): boolean {
|
|
215
|
+
return Object.keys(record).length === 0;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
$effect(() => {
|
|
219
|
+
const currentValue = value;
|
|
220
|
+
if (!resolvedPolicy || !isNewRecord) {
|
|
221
|
+
previousIsNewRecord = isNewRecord;
|
|
222
|
+
lastObservedValue = currentValue;
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const explicitNewSession =
|
|
227
|
+
createSessionKey !== undefined &&
|
|
228
|
+
createSessionKey !== previousCreateSessionKey;
|
|
229
|
+
const resetCreateSession =
|
|
230
|
+
initializedCreateSession &&
|
|
231
|
+
!previousIsNewRecord &&
|
|
232
|
+
isEmptyRecord(currentValue);
|
|
233
|
+
const externalEmptyReset =
|
|
234
|
+
initializedCreateSession &&
|
|
235
|
+
currentValue !== lastObservedValue &&
|
|
236
|
+
currentValue !== lastInternalValue &&
|
|
237
|
+
isEmptyRecord(currentValue);
|
|
238
|
+
const shouldApplyDefaults =
|
|
239
|
+
!initializedCreateSession ||
|
|
240
|
+
explicitNewSession ||
|
|
241
|
+
resetCreateSession ||
|
|
242
|
+
(createSessionKey === undefined && externalEmptyReset);
|
|
243
|
+
|
|
244
|
+
if (!shouldApplyDefaults) {
|
|
245
|
+
previousIsNewRecord = true;
|
|
246
|
+
previousCreateSessionKey = createSessionKey;
|
|
247
|
+
lastObservedValue = currentValue;
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const next = { ...currentValue };
|
|
252
|
+
let changed = false;
|
|
253
|
+
for (const field of formFields) {
|
|
254
|
+
const policyField = resolvedPolicy.fields[field.name];
|
|
255
|
+
if (policyField.hasDefault && next[field.name] === undefined) {
|
|
256
|
+
next[field.name] = policyField.defaultValue;
|
|
257
|
+
changed = true;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
initializedCreateSession = true;
|
|
261
|
+
previousIsNewRecord = true;
|
|
262
|
+
previousCreateSessionKey = createSessionKey;
|
|
263
|
+
if (changed) {
|
|
264
|
+
lastInternalValue = next;
|
|
265
|
+
lastObservedValue = next;
|
|
266
|
+
value = next;
|
|
267
|
+
} else {
|
|
268
|
+
lastObservedValue = currentValue;
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
function inputFor(field: ObjectFormField): FieldInputComponent {
|
|
273
|
+
return (
|
|
274
|
+
inputRegistry?.resolve(objectRef, field.name, field.definition.type) ??
|
|
275
|
+
(FieldInput as unknown as Component<FieldInputProps>)
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
</script>
|
|
279
|
+
|
|
280
|
+
{#snippet renderField(field: ObjectFormField)}
|
|
281
|
+
{@const renderer = renderers[field.name]}
|
|
282
|
+
<PolicyField name={field.name} label={fieldLabel(field)} id={fieldId(field.name)} helpId={`${fieldId(field.name)}-help`} {isNewRecord}>
|
|
283
|
+
{#if renderer}
|
|
284
|
+
{@render renderer({ field, id: fieldId(field.name), helpId: `${fieldId(field.name)}-help`, value: value[field.name], required: resolvedPolicy!.fields[field.name].required, disabled, setValue: (next) => setValue(field.name, next), setValidity: (valid) => setValidity(field.name, valid) })}
|
|
285
|
+
{:else}
|
|
286
|
+
{@const InputComponent = inputFor(field)}
|
|
287
|
+
<InputComponent
|
|
288
|
+
id={fieldId(field.name)}
|
|
289
|
+
name={field.name}
|
|
290
|
+
label={fieldLabel(field)}
|
|
291
|
+
{field}
|
|
292
|
+
value={value[field.name]}
|
|
293
|
+
required={resolvedPolicy!.fields[field.name].required}
|
|
294
|
+
{disabled}
|
|
295
|
+
onvaluechange={(next) => setValue(field.name, next)}
|
|
296
|
+
onvaliditychange={(valid) => setValidity(field.name, valid)}
|
|
297
|
+
/>
|
|
298
|
+
{/if}
|
|
299
|
+
</PolicyField>
|
|
300
|
+
{/snippet}
|
|
301
|
+
|
|
302
|
+
{#snippet renderGroups(groups: Array<[string | null, ObjectFormField[]]>)}
|
|
303
|
+
{#each groups as [groupName, groupFields] (groupName ?? '__ungrouped__')}
|
|
304
|
+
{#if groupName}
|
|
305
|
+
<fieldset class="object-form__group">
|
|
306
|
+
<legend>{groupName}</legend>
|
|
307
|
+
{#each groupFields as field (field.name)}
|
|
308
|
+
{@render renderField(field)}
|
|
309
|
+
{/each}
|
|
310
|
+
</fieldset>
|
|
311
|
+
{:else}
|
|
312
|
+
{#each groupFields as field (field.name)}
|
|
313
|
+
{@render renderField(field)}
|
|
314
|
+
{/each}
|
|
315
|
+
{/if}
|
|
316
|
+
{/each}
|
|
317
|
+
{/snippet}
|
|
318
|
+
|
|
319
|
+
{#if sourceLoading}
|
|
320
|
+
<div class="object-form__status" role="status" aria-live="polite">Loading form…</div>
|
|
321
|
+
{:else if sourceError}
|
|
322
|
+
<div class="object-form__status" role="alert">{sourceError}</div>
|
|
323
|
+
{:else if resolvedPolicy}
|
|
324
|
+
<FieldPolicyProvider policy={resolvedPolicy}>
|
|
325
|
+
{#if showModeSwitch || (showPolicyGear && fieldPolicyGear)}
|
|
326
|
+
{#if showModeSwitch}
|
|
327
|
+
<div class="object-form__mode"><ModeSwitch /></div>
|
|
328
|
+
{/if}
|
|
329
|
+
{#if showPolicyGear && fieldPolicyGear}
|
|
330
|
+
<div class="object-form__gear"><FieldPolicyGearButton /></div>
|
|
331
|
+
{/if}
|
|
332
|
+
{/if}
|
|
333
|
+
<Form class="object-form {className}" onsubmit={(event) => { if (invalidFields.size > 0) { event.preventDefault(); return; } onsubmit?.(event); }}>
|
|
334
|
+
{@render renderGroups(basicGroups)}
|
|
335
|
+
{#if advancedGroups.length > 0}
|
|
336
|
+
<AdvancedFields open>
|
|
337
|
+
{@render renderGroups(advancedGroups)}
|
|
338
|
+
</AdvancedFields>
|
|
339
|
+
{/if}
|
|
340
|
+
{@render actions?.()}
|
|
341
|
+
</Form>
|
|
342
|
+
</FieldPolicyProvider>
|
|
343
|
+
{/if}
|
|
344
|
+
|
|
345
|
+
<style>
|
|
346
|
+
:global(.object-form) { display: grid; gap: var(--smrt-spacing-4, 1rem); }
|
|
347
|
+
.object-form__mode, .object-form__gear { display: inline-flex; justify-content: flex-end; margin-bottom: var(--smrt-spacing-3, 0.75rem); }
|
|
348
|
+
.object-form__group { display: grid; gap: var(--smrt-spacing-3, 0.75rem); border: 0; margin: 0; min-inline-size: 0; padding: 0; }
|
|
349
|
+
.object-form__group legend { font: var(--smrt-typography-title-small-font, inherit); margin-bottom: var(--smrt-spacing-2, 0.5rem); }
|
|
350
|
+
.object-form__status { color: var(--smrt-color-on-surface-variant, currentColor); }
|
|
351
|
+
</style>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Component, Snippet } from 'svelte';
|
|
2
|
+
import type { ResolvedObjectFieldPolicy } from '../../types.js';
|
|
3
|
+
import type { FieldInputRegistry } from '../input-registry.js';
|
|
4
|
+
import { type ObjectFormFieldDefinition, type ObjectFormFieldSnippetProps } from '../types.js';
|
|
5
|
+
export interface ObjectFormProps {
|
|
6
|
+
objectRef: string;
|
|
7
|
+
/**
|
|
8
|
+
* Generated browser-safe definitions — never pass raw server registry fields.
|
|
9
|
+
* Omit with `policy` to resolve both through ObjectFormSourceProvider.
|
|
10
|
+
*/
|
|
11
|
+
fields?: Readonly<Record<string, ObjectFormFieldDefinition>>;
|
|
12
|
+
/** Pre-resolved policy for the same objectRef. Omit with `fields` for a source. */
|
|
13
|
+
policy?: ResolvedObjectFieldPolicy;
|
|
14
|
+
/** Mutable create/edit record. Bind this prop to retain values in the host. */
|
|
15
|
+
value?: Record<string, unknown>;
|
|
16
|
+
/** True for creates (defaults may prefill), false for loaded edits. */
|
|
17
|
+
isNewRecord?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Optional identity for a create session. Change this when reusing a mounted
|
|
20
|
+
* form for another new record; defaults then apply once to missing values.
|
|
21
|
+
* With no key, replacing the bound record with an empty object starts a new
|
|
22
|
+
* create session automatically.
|
|
23
|
+
*/
|
|
24
|
+
createSessionKey?: string | number;
|
|
25
|
+
disabled?: boolean;
|
|
26
|
+
showModeSwitch?: boolean;
|
|
27
|
+
/** Render a provider-registered policy gear next to the mode control. */
|
|
28
|
+
showPolicyGear?: boolean;
|
|
29
|
+
inputRegistry?: FieldInputRegistry;
|
|
30
|
+
/** Per-field custom widget snippets, keyed by field name. */
|
|
31
|
+
renderers?: Readonly<Record<string, Snippet<[ObjectFormFieldSnippetProps]>>>;
|
|
32
|
+
/** Optional controls rendered after the policy-ordered fields inside the form. */
|
|
33
|
+
actions?: Snippet;
|
|
34
|
+
class?: string;
|
|
35
|
+
onsubmit?: (event: SubmitEvent) => void;
|
|
36
|
+
}
|
|
37
|
+
declare const ObjectForm: Component<ObjectFormProps, {}, "value">;
|
|
38
|
+
type ObjectForm = ReturnType<typeof ObjectForm>;
|
|
39
|
+
export default ObjectForm;
|
|
40
|
+
//# sourceMappingURL=ObjectForm.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ObjectForm.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/ObjectForm.svelte.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACjD,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,gBAAgB,CAAC;AAEhE,OAAO,KAAK,EAEV,kBAAkB,EACnB,MAAM,sBAAsB,CAAC;AAG9B,OAAO,EAGL,KAAK,yBAAyB,EAC9B,KAAK,2BAA2B,EACjC,MAAM,aAAa,CAAC;AASrB,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,yBAAyB,CAAC,CAAC,CAAC;IAC7D,mFAAmF;IACnF,MAAM,CAAC,EAAE,yBAAyB,CAAC;IACnC,+EAA+E;IAC/E,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,uEAAuE;IACvE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACnC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,yEAAyE;IACzE,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,aAAa,CAAC,EAAE,kBAAkB,CAAC;IACnC,6DAA6D;IAC7D,SAAS,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,2BAA2B,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7E,kFAAkF;IAClF,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC;CACzC;AAsSD,QAAA,MAAM,UAAU,yCAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/** Provides one app-local generated ObjectForm source to descendant forms. */
|
|
3
|
+
import type { Snippet } from 'svelte';
|
|
4
|
+
import { setObjectFormSource } from '../object-form-source-context.svelte.js';
|
|
5
|
+
import type { ObjectFormSource } from '../types.js';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
source: ObjectFormSource;
|
|
9
|
+
children?: Snippet;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let { source, children }: Props = $props();
|
|
13
|
+
// Context is established once, but the wrapper reads the current app source
|
|
14
|
+
// when a descendant loads so a host may replace its transport configuration.
|
|
15
|
+
setObjectFormSource({ load: (objectRef) => source.load(objectRef) });
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
{@render children?.()}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Provides one app-local generated ObjectForm source to descendant forms. */
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { ObjectFormSource } from '../types.js';
|
|
4
|
+
interface Props {
|
|
5
|
+
source: ObjectFormSource;
|
|
6
|
+
children?: Snippet;
|
|
7
|
+
}
|
|
8
|
+
declare const ObjectFormSourceProvider: import("svelte").Component<Props, {}, "">;
|
|
9
|
+
type ObjectFormSourceProvider = ReturnType<typeof ObjectFormSourceProvider>;
|
|
10
|
+
export default ObjectFormSourceProvider;
|
|
11
|
+
//# sourceMappingURL=ObjectFormSourceProvider.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ObjectFormSourceProvider.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/ObjectFormSourceProvider.svelte.ts"],"names":[],"mappings":"AAGA,8EAA8E;AAC9E,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAGpD,UAAU,KAAK;IACb,MAAM,EAAE,gBAAgB,CAAC;IACzB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAkBD,QAAA,MAAM,wBAAwB,2CAAwC,CAAC;AACvE,KAAK,wBAAwB,GAAG,UAAU,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAC5E,eAAe,wBAAwB,CAAC"}
|