@happyvertical/smrt-svelte 0.51.39 → 0.52.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.
- package/AGENTS.md +3 -0
- package/README.md +167 -1
- package/agents/workspace.md +24 -0
- package/dist/browser-ai/adapters/stt/browser-speech.d.ts +11 -0
- package/dist/browser-ai/adapters/stt/browser-speech.d.ts.map +1 -1
- package/dist/browser-ai/adapters/stt/browser-speech.js +20 -4
- package/dist/browser-ai/adapters/stt/browser-speech.test.js +113 -0
- package/dist/browser-ai/adapters/stt/dictation-source.d.ts +3 -0
- package/dist/browser-ai/adapters/stt/dictation-source.d.ts.map +1 -0
- package/dist/browser-ai/adapters/stt/dictation-source.js +38 -0
- package/dist/browser-ai/adapters/stt/dictation-source.test.js +52 -0
- package/dist/browser-ai/adapters/stt/index.d.ts +2 -1
- package/dist/browser-ai/adapters/stt/index.d.ts.map +1 -1
- package/dist/browser-ai/adapters/stt/index.js +2 -1
- package/dist/components/forms/Form.svelte +118 -8
- package/dist/components/forms/Form.svelte.d.ts +18 -2
- package/dist/components/forms/Form.svelte.d.ts.map +1 -1
- package/dist/components/forms/FormScope.svelte +202 -0
- package/dist/components/forms/FormScope.svelte.d.ts +22 -0
- package/dist/components/forms/FormScope.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/Form.ui-controls.test.js +119 -0
- package/dist/components/forms/__tests__/Form.webmcp.test.js +5 -3
- package/dist/components/forms/__tests__/FormScope.test.js +39 -0
- package/dist/components/forms/__tests__/form-scope.fixture.svelte +30 -0
- package/dist/components/forms/__tests__/form-scope.fixture.svelte.d.ts +10 -0
- package/dist/components/forms/__tests__/form-scope.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/form-with-ui-controls.fixture.svelte +84 -0
- package/dist/components/forms/__tests__/form-with-ui-controls.fixture.svelte.d.ts +14 -0
- package/dist/components/forms/__tests__/form-with-ui-controls.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/index.d.ts +1 -0
- package/dist/components/forms/index.d.ts.map +1 -1
- package/dist/components/forms/index.js +1 -0
- package/dist/components/workspace/README.md +11 -0
- package/dist/components/workspace/__tests__/AdminShell.responsive.test.js +586 -0
- package/dist/components/workspace/__tests__/AdminShell.test.js +85 -0
- package/dist/components/workspace/__tests__/TenantNav.test.js +49 -0
- package/dist/components/workspace/__tests__/admin-shell-header-overflow.test.js +25 -0
- package/dist/components/workspace/__tests__/admin-shell-mobile.test.js +217 -0
- package/dist/components/workspace/__tests__/admin-shell-page-contracts.test.js +30 -0
- package/dist/components/workspace/__tests__/admin-shell-responsive-state.test.js +184 -0
- package/dist/components/workspace/__tests__/admin-shell-theme-hooks.test.js +36 -0
- package/dist/components/workspace/__tests__/shell-chrome-components.test.js +108 -0
- package/dist/components/workspace/admin-shell/AdminShell.svelte +998 -40
- package/dist/components/workspace/admin-shell/AdminShell.svelte.d.ts +42 -1
- package/dist/components/workspace/admin-shell/AdminShell.svelte.d.ts.map +1 -1
- package/dist/components/workspace/admin-shell/PhoneBottomBar.svelte +170 -0
- package/dist/components/workspace/admin-shell/PhoneBottomBar.svelte.d.ts +11 -0
- package/dist/components/workspace/admin-shell/PhoneBottomBar.svelte.d.ts.map +1 -0
- package/dist/components/workspace/admin-shell/PhoneTopBar.svelte +93 -0
- package/dist/components/workspace/admin-shell/PhoneTopBar.svelte.d.ts +11 -0
- package/dist/components/workspace/admin-shell/PhoneTopBar.svelte.d.ts.map +1 -0
- package/dist/components/workspace/admin-shell/ShellNavToggle.svelte +88 -0
- package/dist/components/workspace/admin-shell/ShellNavToggle.svelte.d.ts +14 -0
- package/dist/components/workspace/admin-shell/ShellNavToggle.svelte.d.ts.map +1 -0
- package/dist/components/workspace/admin-shell/ShellTitle.svelte +42 -0
- package/dist/components/workspace/admin-shell/ShellTitle.svelte.d.ts +11 -0
- package/dist/components/workspace/admin-shell/ShellTitle.svelte.d.ts.map +1 -0
- package/dist/components/workspace/admin-shell/TenantNav.svelte +46 -1
- package/dist/components/workspace/admin-shell/TenantNav.svelte.d.ts.map +1 -1
- package/dist/components/workspace/admin-shell/mobile-shell-dom.d.ts +37 -0
- package/dist/components/workspace/admin-shell/mobile-shell-dom.d.ts.map +1 -0
- package/dist/components/workspace/admin-shell/mobile-shell-dom.js +150 -0
- package/dist/components/workspace/admin-shell/mobile-shell.d.ts +191 -0
- package/dist/components/workspace/admin-shell/mobile-shell.d.ts.map +1 -0
- package/dist/components/workspace/admin-shell/mobile-shell.js +165 -0
- package/dist/components/workspace/admin-shell/settings.d.ts +27 -2
- package/dist/components/workspace/admin-shell/settings.d.ts.map +1 -1
- package/dist/components/workspace/admin-shell/settings.js +84 -2
- package/dist/components/workspace/admin-shell/state.svelte.d.ts +55 -1
- package/dist/components/workspace/admin-shell/state.svelte.d.ts.map +1 -1
- package/dist/components/workspace/admin-shell/state.svelte.js +149 -6
- package/dist/components/workspace/admin-shell/types.d.ts +97 -0
- package/dist/components/workspace/admin-shell/types.d.ts.map +1 -1
- package/dist/components/workspace/admin-shell/types.js +13 -0
- package/dist/components/workspace/index.d.ts +11 -4
- package/dist/components/workspace/index.d.ts.map +1 -1
- package/dist/components/workspace/index.js +12 -3
- package/dist/i18n/strings.workspace.d.ts +10 -0
- package/dist/i18n/strings.workspace.d.ts.map +1 -1
- package/dist/i18n/strings.workspace.js +10 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/web/__tests__/assistant-dock.integration.svelte.test.js +2 -2
- package/dist/web/__tests__/list-sort-surface.fixture.svelte +41 -0
- package/dist/web/__tests__/list-sort-surface.fixture.svelte.d.ts +15 -0
- package/dist/web/__tests__/list-sort-surface.fixture.svelte.d.ts.map +1 -0
- package/dist/web/__tests__/list-sort-surface.svelte.test.js +59 -0
- package/dist/web/__tests__/surfaces.child.fixture.svelte +69 -0
- package/dist/web/__tests__/surfaces.child.fixture.svelte.d.ts +16 -0
- package/dist/web/__tests__/surfaces.child.fixture.svelte.d.ts.map +1 -0
- package/dist/web/__tests__/surfaces.fixture.svelte +26 -0
- package/dist/web/__tests__/surfaces.fixture.svelte.d.ts +18 -0
- package/dist/web/__tests__/surfaces.fixture.svelte.d.ts.map +1 -0
- package/dist/web/__tests__/surfaces.svelte.test.js +140 -0
- package/dist/web/__tests__/webmcp-ui.test.js +18 -0
- package/dist/web/index.d.ts +4 -1
- package/dist/web/index.d.ts.map +1 -1
- package/dist/web/index.js +4 -1
- package/dist/web/link-surface.svelte.d.ts +14 -0
- package/dist/web/link-surface.svelte.d.ts.map +1 -0
- package/dist/web/link-surface.svelte.js +49 -0
- package/dist/web/list-surface.svelte.d.ts +80 -0
- package/dist/web/list-surface.svelte.d.ts.map +1 -0
- package/dist/web/list-surface.svelte.js +309 -0
- package/dist/web/step-surface.svelte.d.ts +17 -0
- package/dist/web/step-surface.svelte.d.ts.map +1 -0
- package/dist/web/step-surface.svelte.js +56 -0
- package/dist/web/webmcp-ui-context.d.ts +7 -0
- package/dist/web/webmcp-ui-context.d.ts.map +1 -1
- package/dist/web/webmcp-ui-context.js +10 -0
- package/dist/web/webmcp-ui.d.ts.map +1 -1
- package/dist/web/webmcp-ui.js +16 -3
- package/package.json +16 -16
|
@@ -6,13 +6,16 @@ import {
|
|
|
6
6
|
type ControlKind,
|
|
7
7
|
type ControlRuntimeState,
|
|
8
8
|
type ControlSubject,
|
|
9
|
+
controlProposalProperties,
|
|
9
10
|
createControlInteractionRegistry,
|
|
10
11
|
StagedControlReview,
|
|
11
12
|
setControlInteractionContext,
|
|
13
|
+
stageControlProposals,
|
|
12
14
|
} from '@happyvertical/smrt-ui/forms';
|
|
13
15
|
import { useI18n } from '@happyvertical/smrt-ui/i18n';
|
|
14
16
|
import type { Snippet } from 'svelte';
|
|
15
17
|
import { onDestroy, untrack } from 'svelte';
|
|
18
|
+
import type { HTMLFormAttributes } from 'svelte/elements';
|
|
16
19
|
import { useAppState } from '../../hooks/useAppState.svelte.js';
|
|
17
20
|
import { useSTT } from '../../hooks/useSTT.svelte.js';
|
|
18
21
|
import { M } from '../../i18n/strings.forms.js';
|
|
@@ -28,7 +31,20 @@ import type { LLMModelId, STTAdapterType } from './types.js';
|
|
|
28
31
|
|
|
29
32
|
const { t } = useI18n();
|
|
30
33
|
|
|
31
|
-
|
|
34
|
+
/**
|
|
35
|
+
* A SvelteKit-style form action: `enhance` from `$app/forms` fits as-is, or
|
|
36
|
+
* wrap it to pass a submit function (`(form) => enhance(form, submit)`).
|
|
37
|
+
*/
|
|
38
|
+
export type FormEnhance = (
|
|
39
|
+
form: HTMLFormElement,
|
|
40
|
+
// biome-ignore lint/suspicious/noConfusingVoidType: an action may return nothing, like Svelte's own Action type.
|
|
41
|
+
) => void | { destroy?: () => void };
|
|
42
|
+
|
|
43
|
+
export interface Props
|
|
44
|
+
extends Omit<
|
|
45
|
+
HTMLFormAttributes,
|
|
46
|
+
'children' | 'class' | 'id' | 'name' | 'method' | 'action' | 'onsubmit'
|
|
47
|
+
> {
|
|
32
48
|
/** Form children */
|
|
33
49
|
children: Snippet;
|
|
34
50
|
/** Show mode toggle button */
|
|
@@ -48,7 +64,7 @@ export interface Props {
|
|
|
48
64
|
/** Collection/model identity used when naming the generated intent. */
|
|
49
65
|
collection?: string;
|
|
50
66
|
/** HTTP method for native form submission (default: GET) */
|
|
51
|
-
method?: 'GET' | 'POST';
|
|
67
|
+
method?: 'GET' | 'POST' | 'get' | 'post' | 'dialog';
|
|
52
68
|
/** Form action URL for native form submission */
|
|
53
69
|
action?: string;
|
|
54
70
|
/** Stable identity used by control/agent interaction adapters. */
|
|
@@ -67,6 +83,14 @@ export interface Props {
|
|
|
67
83
|
name?: string;
|
|
68
84
|
/** CSS classes to apply to the form element. */
|
|
69
85
|
class?: string;
|
|
86
|
+
/**
|
|
87
|
+
* Progressive-enhancement action applied to the rendered `<form>` — pass
|
|
88
|
+
* SvelteKit's `enhance` (`<Form enhance={enhance}>`). A component cannot
|
|
89
|
+
* take `use:`, so this is the `use:enhance` equivalent; attachments
|
|
90
|
+
* (`{@attach …}`) and every other native form attribute (`enctype`,
|
|
91
|
+
* `novalidate`, `aria-*`, `data-*`, `autocomplete`, …) pass straight through.
|
|
92
|
+
*/
|
|
93
|
+
enhance?: FormEnhance;
|
|
70
94
|
}
|
|
71
95
|
|
|
72
96
|
const {
|
|
@@ -89,8 +113,27 @@ const {
|
|
|
89
113
|
id,
|
|
90
114
|
name,
|
|
91
115
|
class: className = '',
|
|
116
|
+
enhance,
|
|
117
|
+
...formAttributes
|
|
92
118
|
}: Props = $props();
|
|
93
119
|
|
|
120
|
+
/** Apply the consumer's `enhance` action; re-run it when the action changes. */
|
|
121
|
+
function applyEnhance(form: HTMLFormElement, initial: FormEnhance | undefined) {
|
|
122
|
+
let current = initial;
|
|
123
|
+
let handle = current?.(form);
|
|
124
|
+
return {
|
|
125
|
+
update(next: FormEnhance | undefined) {
|
|
126
|
+
if (next === current) return;
|
|
127
|
+
if (handle) handle.destroy?.();
|
|
128
|
+
current = next;
|
|
129
|
+
handle = current?.(form);
|
|
130
|
+
},
|
|
131
|
+
destroy() {
|
|
132
|
+
if (handle) handle.destroy?.();
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
94
137
|
const app = useAppState();
|
|
95
138
|
const stt = useSTT();
|
|
96
139
|
const instanceId = $props.id();
|
|
@@ -291,7 +334,25 @@ function recordDirectUserEdit(event: Event) {
|
|
|
291
334
|
target as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement,
|
|
292
335
|
),
|
|
293
336
|
);
|
|
294
|
-
if (!field)
|
|
337
|
+
if (!field) {
|
|
338
|
+
// A smrt-ui primitive (Input, Select, Combobox, …) registered itself
|
|
339
|
+
// through the control context; its element carries the address.
|
|
340
|
+
const control = target.closest<HTMLElement>('[data-smrt-control]');
|
|
341
|
+
const controlId = control?.dataset.smrtControl;
|
|
342
|
+
if (!control || !controlId) return;
|
|
343
|
+
resolvedInteractionRegistry.recordUserEdit?.({
|
|
344
|
+
formId: resolvedFormId,
|
|
345
|
+
controlId,
|
|
346
|
+
subject:
|
|
347
|
+
control.dataset.smrtSubjectType && control.dataset.smrtSubjectId
|
|
348
|
+
? {
|
|
349
|
+
type: control.dataset.smrtSubjectType,
|
|
350
|
+
id: control.dataset.smrtSubjectId,
|
|
351
|
+
}
|
|
352
|
+
: undefined,
|
|
353
|
+
});
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
295
356
|
resolvedInteractionRegistry.recordUserEdit?.({
|
|
296
357
|
formId: resolvedFormId,
|
|
297
358
|
controlId: field.controlId ?? field.name,
|
|
@@ -812,6 +873,16 @@ function prepareInteractionValue(field: FieldDefinition, value: unknown) {
|
|
|
812
873
|
return field.prepareValue ? field.prepareValue(preparedValue) : preparedValue;
|
|
813
874
|
}
|
|
814
875
|
|
|
876
|
+
/** Registry control ids owned by fields registered through this Form. */
|
|
877
|
+
function richFieldControlIds(): Set<string> {
|
|
878
|
+
const ids = new Set<string>();
|
|
879
|
+
for (const field of fields.values()) {
|
|
880
|
+
ids.add(field.controlId ?? field.name);
|
|
881
|
+
ids.add(field.name);
|
|
882
|
+
}
|
|
883
|
+
return ids;
|
|
884
|
+
}
|
|
885
|
+
|
|
815
886
|
function formInputSchema(): Record<string, unknown> {
|
|
816
887
|
void interactionRegistryRevision;
|
|
817
888
|
const properties: Record<string, unknown> = {};
|
|
@@ -830,6 +901,17 @@ function formInputSchema(): Record<string, unknown> {
|
|
|
830
901
|
if (field.constraints?.required) required.push(field.name);
|
|
831
902
|
}
|
|
832
903
|
|
|
904
|
+
// Every other control mounted in this form — smrt-ui primitives
|
|
905
|
+
// (FormGroup + Input/Select/Textarea/Combobox/…) and composites registered
|
|
906
|
+
// through useControlRegistration — proposes through the same tool.
|
|
907
|
+
for (const [controlId, schema] of Object.entries(
|
|
908
|
+
controlProposalProperties(resolvedInteractionRegistry, resolvedFormId, {
|
|
909
|
+
exclude: richFieldControlIds(),
|
|
910
|
+
}),
|
|
911
|
+
)) {
|
|
912
|
+
if (!Object.hasOwn(properties, controlId)) properties[controlId] = schema;
|
|
913
|
+
}
|
|
914
|
+
|
|
833
915
|
return {
|
|
834
916
|
type: 'object',
|
|
835
917
|
additionalProperties: false,
|
|
@@ -879,7 +961,23 @@ async function submitCurrentForm(): Promise<string> {
|
|
|
879
961
|
}
|
|
880
962
|
|
|
881
963
|
async function stageForWebMcp(args: Record<string, unknown>): Promise<string> {
|
|
882
|
-
const
|
|
964
|
+
const richArgs: Record<string, unknown> = {};
|
|
965
|
+
const controlArgs: Record<string, unknown> = {};
|
|
966
|
+
for (const [key, value] of Object.entries(args ?? {})) {
|
|
967
|
+
if (fields.has(key)) richArgs[key] = value;
|
|
968
|
+
else controlArgs[key] = value;
|
|
969
|
+
}
|
|
970
|
+
const controlResult =
|
|
971
|
+
Object.keys(controlArgs).length > 0
|
|
972
|
+
? await stageControlProposals(
|
|
973
|
+
resolvedInteractionRegistry,
|
|
974
|
+
resolvedFormId,
|
|
975
|
+
controlArgs,
|
|
976
|
+
{ source: 'agent', actorId: 'webmcp' },
|
|
977
|
+
{ exclude: richFieldControlIds() },
|
|
978
|
+
)
|
|
979
|
+
: undefined;
|
|
980
|
+
const commands = Object.entries(richArgs).flatMap(([name, value]) => {
|
|
883
981
|
const field = fields.get(name);
|
|
884
982
|
const state = field ? fieldRuntimeState(field) : undefined;
|
|
885
983
|
if (
|
|
@@ -914,11 +1012,17 @@ async function stageForWebMcp(args: Record<string, unknown>): Promise<string> {
|
|
|
914
1012
|
},
|
|
915
1013
|
];
|
|
916
1014
|
});
|
|
917
|
-
|
|
1015
|
+
const controlStaged = controlResult?.staged ?? 0;
|
|
1016
|
+
const controlRejected = controlResult?.rejected.length ?? 0;
|
|
1017
|
+
if (commands.length === 0 && controlStaged + controlRejected === 0) {
|
|
1018
|
+
return 'No reviewable changes provided';
|
|
1019
|
+
}
|
|
918
1020
|
|
|
919
1021
|
const context = { source: 'agent' as const, actorId: 'webmcp' };
|
|
920
1022
|
let batch;
|
|
921
|
-
if (
|
|
1023
|
+
if (commands.length === 0) {
|
|
1024
|
+
batch = { ok: true, results: [] };
|
|
1025
|
+
} else if (resolvedInteractionRegistry.executeBatch) {
|
|
922
1026
|
batch = await resolvedInteractionRegistry.executeBatch(commands, context);
|
|
923
1027
|
} else {
|
|
924
1028
|
const results = [];
|
|
@@ -927,8 +1031,12 @@ async function stageForWebMcp(args: Record<string, unknown>): Promise<string> {
|
|
|
927
1031
|
}
|
|
928
1032
|
batch = { ok: results.every((result) => result.ok), results };
|
|
929
1033
|
}
|
|
930
|
-
const completed =
|
|
931
|
-
|
|
1034
|
+
const completed =
|
|
1035
|
+
batch.results.filter((result) => result.ok).length + controlStaged;
|
|
1036
|
+
const rejected =
|
|
1037
|
+
batch.results.length -
|
|
1038
|
+
batch.results.filter((result) => result.ok).length +
|
|
1039
|
+
controlRejected;
|
|
932
1040
|
return rejected === 0
|
|
933
1041
|
? `Staged ${completed} change${completed === 1 ? '' : 's'} for review`
|
|
934
1042
|
: `Staged ${completed} changes for review; ${rejected} rejected`;
|
|
@@ -1412,6 +1520,7 @@ export function getInteractionRegistry(): ControlInteractionRegistry {
|
|
|
1412
1520
|
</script>
|
|
1413
1521
|
|
|
1414
1522
|
<form
|
|
1523
|
+
{...formAttributes}
|
|
1415
1524
|
bind:this={formElement}
|
|
1416
1525
|
{id}
|
|
1417
1526
|
{name}
|
|
@@ -1420,6 +1529,7 @@ export function getInteractionRegistry(): ControlInteractionRegistry {
|
|
|
1420
1529
|
onsubmit={handleSubmit}
|
|
1421
1530
|
{method}
|
|
1422
1531
|
{action}
|
|
1532
|
+
use:applyEnhance={enhance}
|
|
1423
1533
|
>
|
|
1424
1534
|
<!--
|
|
1425
1535
|
Screen-reader status region (L1 #1420): announces async STT / field-
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import { type ControlInteractionEvent, type ControlInteractionRegistry, type ControlSubject } from '@happyvertical/smrt-ui/forms';
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { HTMLFormAttributes } from 'svelte/elements';
|
|
3
4
|
import type { LLMModelId, STTAdapterType } from './types.js';
|
|
4
|
-
|
|
5
|
+
/**
|
|
6
|
+
* A SvelteKit-style form action: `enhance` from `$app/forms` fits as-is, or
|
|
7
|
+
* wrap it to pass a submit function (`(form) => enhance(form, submit)`).
|
|
8
|
+
*/
|
|
9
|
+
export type FormEnhance = (form: HTMLFormElement) => void | {
|
|
10
|
+
destroy?: () => void;
|
|
11
|
+
};
|
|
12
|
+
export interface Props extends Omit<HTMLFormAttributes, 'children' | 'class' | 'id' | 'name' | 'method' | 'action' | 'onsubmit'> {
|
|
5
13
|
/** Form children */
|
|
6
14
|
children: Snippet;
|
|
7
15
|
/** Show mode toggle button */
|
|
@@ -24,7 +32,7 @@ export interface Props {
|
|
|
24
32
|
/** Collection/model identity used when naming the generated intent. */
|
|
25
33
|
collection?: string;
|
|
26
34
|
/** HTTP method for native form submission (default: GET) */
|
|
27
|
-
method?: 'GET' | 'POST';
|
|
35
|
+
method?: 'GET' | 'POST' | 'get' | 'post' | 'dialog';
|
|
28
36
|
/** Form action URL for native form submission */
|
|
29
37
|
action?: string;
|
|
30
38
|
/** Stable identity used by control/agent interaction adapters. */
|
|
@@ -43,6 +51,14 @@ export interface Props {
|
|
|
43
51
|
name?: string;
|
|
44
52
|
/** CSS classes to apply to the form element. */
|
|
45
53
|
class?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Progressive-enhancement action applied to the rendered `<form>` — pass
|
|
56
|
+
* SvelteKit's `enhance` (`<Form enhance={enhance}>`). A component cannot
|
|
57
|
+
* take `use:`, so this is the `use:enhance` equivalent; attachments
|
|
58
|
+
* (`{@attach …}`) and every other native form attribute (`enctype`,
|
|
59
|
+
* `novalidate`, `aria-*`, `data-*`, `autocomplete`, …) pass straight through.
|
|
60
|
+
*/
|
|
61
|
+
enhance?: FormEnhance;
|
|
46
62
|
}
|
|
47
63
|
declare const Form: import("svelte").Component<Props, {
|
|
48
64
|
getFormData: () => Record<string, unknown>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Form.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/Form.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,uBAAuB,EAC5B,KAAK,0BAA0B,EAG/B,KAAK,cAAc,
|
|
1
|
+
{"version":3,"file":"Form.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/Form.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,uBAAuB,EAC5B,KAAK,0BAA0B,EAG/B,KAAK,cAAc,EAMpB,MAAM,8BAA8B,CAAC;AAEtC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAY1D,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAG7D;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG,CACxB,IAAI,EAAE,eAAe,KAElB,IAAI,GAAG;IAAE,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;CAAE,CAAC;AAErC,MAAM,WAAW,KACf,SAAQ,IAAI,CACV,kBAAkB,EAClB,UAAU,GAAG,OAAO,GAAG,IAAI,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,GAAG,UAAU,CACxE;IACD,oBAAoB;IACpB,QAAQ,EAAE,OAAO,CAAC;IAClB,8BAA8B;IAC9B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,oCAAoC;IACpC,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,iDAAiD;IACjD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,uBAAuB;IACvB,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,UAAU,CAAC;IACtB,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnE,+DAA+D;IAC/D,MAAM,CAAC,EAAE,OAAO,GAAG;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3D,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4DAA4D;IAC5D,MAAM,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,QAAQ,CAAC;IACpD,iDAAiD;IACjD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kEAAkE;IAClE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,+DAA+D;IAC/D,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,+DAA+D;IAC/D,mBAAmB,CAAC,EAAE,0BAA0B,CAAC;IACjD,2DAA2D;IAC3D,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,uBAAuB,KAAK,IAAI,CAAC;IACzD,mEAAmE;IACnE,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,8CAA8C;IAC9C,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,gDAAgD;IAChD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,gDAAgD;IAChD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,WAAW,CAAC;CACvB;AA2+CD,QAAA,MAAM,IAAI;uBAjFe,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;kCAQZ,0BAA0B;MAyEZ,CAAC;AACnD,KAAK,IAAI,GAAG,UAAU,CAAC,OAAO,IAAI,CAAC,CAAC;AACpC,eAAe,IAAI,CAAC"}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
FormScope — makes the fields inside it readable and proposable by agents
|
|
3
|
+
without rendering a `<form>`.
|
|
4
|
+
|
|
5
|
+
Use the rich `<Form webmcp>` when the page has a form: it now passes every
|
|
6
|
+
native attribute and SvelteKit's `enhance` through. FormScope is for the
|
|
7
|
+
cases a `<form>` element does not fit — a fetch-driven wizard whose buttons
|
|
8
|
+
are `type="button"`, an editor made of several independently saved parts,
|
|
9
|
+
or a page that must keep its own `<form>` markup (scoped `form.x` styles).
|
|
10
|
+
|
|
11
|
+
<FormScope formId="new-site" purpose="Start a new town site">
|
|
12
|
+
<FormGroup label="Site name" id="site-name"><Input name="name" bind:value={name} /></FormGroup>
|
|
13
|
+
</FormScope>
|
|
14
|
+
|
|
15
|
+
Inside, smrt-ui primitives (FormGroup + Input/Select/Textarea/Combobox/…)
|
|
16
|
+
and composites registered through `useControlRegistration` join the nearest
|
|
17
|
+
`<Provider webmcp>` control registry (or a local one), and — with `webmcp`
|
|
18
|
+
(default on) and at least one proposable control — one
|
|
19
|
+
`<formId>_stage_changes` tool lets an agent PROPOSE values. Proposals show
|
|
20
|
+
in the staged-review panel; a person applies them. Nothing is saved or
|
|
21
|
+
submitted by an agent.
|
|
22
|
+
-->
|
|
23
|
+
<script lang="ts">
|
|
24
|
+
import {
|
|
25
|
+
type ControlInteractionRegistry,
|
|
26
|
+
controlProposalInputSchema,
|
|
27
|
+
controlProposalToolName,
|
|
28
|
+
createControlInteractionRegistry,
|
|
29
|
+
StagedControlReview,
|
|
30
|
+
setControlInteractionContext,
|
|
31
|
+
stageControlProposals,
|
|
32
|
+
} from '@happyvertical/smrt-ui/forms';
|
|
33
|
+
import type { Snippet } from 'svelte';
|
|
34
|
+
import { untrack } from 'svelte';
|
|
35
|
+
import { useWebMcpTool } from '../../web/webmcp.svelte.js';
|
|
36
|
+
import { tryGetWebMcpUiContext } from '../../web/webmcp-ui-context.js';
|
|
37
|
+
|
|
38
|
+
export interface Props {
|
|
39
|
+
/** Stable, page-unique form identity (also names the WebMCP tool). */
|
|
40
|
+
formId: string;
|
|
41
|
+
/** Plain-language name of what the form does, for the tool description. */
|
|
42
|
+
purpose?: string;
|
|
43
|
+
/** Register the `<formId>_stage_changes` proposal tool (default true). */
|
|
44
|
+
webmcp?: boolean | { name?: string; description?: string };
|
|
45
|
+
/** Override the registry (tests, nested hosts). */
|
|
46
|
+
interactionRegistry?: ControlInteractionRegistry;
|
|
47
|
+
/** Show the staged-review summary for agent proposals (default true). */
|
|
48
|
+
stagedReview?: boolean;
|
|
49
|
+
children: Snippet;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const {
|
|
53
|
+
formId,
|
|
54
|
+
purpose,
|
|
55
|
+
webmcp = true,
|
|
56
|
+
interactionRegistry,
|
|
57
|
+
stagedReview = true,
|
|
58
|
+
children,
|
|
59
|
+
}: Props = $props();
|
|
60
|
+
|
|
61
|
+
const providerUi = tryGetWebMcpUiContext();
|
|
62
|
+
const localRegistry = createControlInteractionRegistry();
|
|
63
|
+
const registry = $derived(
|
|
64
|
+
interactionRegistry ??
|
|
65
|
+
(providerUi?.enabled ? providerUi.controlRegistry : undefined) ??
|
|
66
|
+
localRegistry,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
setControlInteractionContext({
|
|
70
|
+
get formId() {
|
|
71
|
+
return formId;
|
|
72
|
+
},
|
|
73
|
+
get registry() {
|
|
74
|
+
return registry;
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
let scope = $state<HTMLDivElement | null>(null);
|
|
79
|
+
let formElement = $state<HTMLFormElement | null>(null);
|
|
80
|
+
$effect(() => {
|
|
81
|
+
if (!scope) return;
|
|
82
|
+
const element = scope;
|
|
83
|
+
const find = () => {
|
|
84
|
+
formElement = element.querySelector('form');
|
|
85
|
+
};
|
|
86
|
+
find();
|
|
87
|
+
// A form can appear after mount ({#if} around it); keep the reference current.
|
|
88
|
+
const observer = new MutationObserver(find);
|
|
89
|
+
observer.observe(element, { childList: true, subtree: true });
|
|
90
|
+
return () => observer.disconnect();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Record trusted human edits so a staged proposal goes stale when the person
|
|
94
|
+
// types over it (the same contract as the base and rich Form).
|
|
95
|
+
function recordDirectUserEdit(event: Event) {
|
|
96
|
+
if (!event.isTrusted) return;
|
|
97
|
+
const target = event.target;
|
|
98
|
+
if (!(target instanceof Element)) return;
|
|
99
|
+
const control = target.closest<HTMLElement>('[data-smrt-control]');
|
|
100
|
+
const controlId = control?.dataset.smrtControl;
|
|
101
|
+
if (!control || !controlId) return;
|
|
102
|
+
registry.recordUserEdit?.({
|
|
103
|
+
formId,
|
|
104
|
+
controlId,
|
|
105
|
+
subject:
|
|
106
|
+
control.dataset.smrtSubjectType && control.dataset.smrtSubjectId
|
|
107
|
+
? {
|
|
108
|
+
type: control.dataset.smrtSubjectType,
|
|
109
|
+
id: control.dataset.smrtSubjectId,
|
|
110
|
+
}
|
|
111
|
+
: undefined,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
$effect(() => {
|
|
116
|
+
const element = scope;
|
|
117
|
+
if (!element) return;
|
|
118
|
+
element.addEventListener('input', recordDirectUserEdit);
|
|
119
|
+
element.addEventListener('change', recordDirectUserEdit);
|
|
120
|
+
return () => {
|
|
121
|
+
element.removeEventListener('input', recordDirectUserEdit);
|
|
122
|
+
element.removeEventListener('change', recordDirectUserEdit);
|
|
123
|
+
};
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// The registry is not reactive; bump a counter on registration changes so
|
|
127
|
+
// the tool's schema follows controls as they mount, unmount, or disable.
|
|
128
|
+
let registryRevision = $state(0);
|
|
129
|
+
$effect(() => {
|
|
130
|
+
const currentFormId = formId;
|
|
131
|
+
return registry.subscribe((event) => {
|
|
132
|
+
if (event.identity.formId !== currentFormId) return;
|
|
133
|
+
if (
|
|
134
|
+
event.type === 'registered' ||
|
|
135
|
+
event.type === 'unregistered' ||
|
|
136
|
+
event.type === 'refreshed'
|
|
137
|
+
) {
|
|
138
|
+
registryRevision = untrack(() => registryRevision) + 1;
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
useWebMcpTool(
|
|
144
|
+
() => {
|
|
145
|
+
if (!webmcp) return null;
|
|
146
|
+
void registryRevision;
|
|
147
|
+
const options = typeof webmcp === 'object' ? webmcp : {};
|
|
148
|
+
const currentRegistry = registry;
|
|
149
|
+
const currentFormId = formId;
|
|
150
|
+
const inputSchema = controlProposalInputSchema(
|
|
151
|
+
currentRegistry,
|
|
152
|
+
currentFormId,
|
|
153
|
+
);
|
|
154
|
+
// Nothing an agent could fill (yet): no tool, rather than an empty one.
|
|
155
|
+
if (Object.keys(inputSchema.properties as object).length === 0) return null;
|
|
156
|
+
return {
|
|
157
|
+
name: options.name ?? controlProposalToolName(currentFormId),
|
|
158
|
+
description:
|
|
159
|
+
options.description ??
|
|
160
|
+
`Propose values for the "${purpose ?? currentFormId}" form. The person reviews and applies them; nothing is saved or submitted.`,
|
|
161
|
+
inputSchema,
|
|
162
|
+
// Stages proposals only: write-class, never destructive.
|
|
163
|
+
annotations: {
|
|
164
|
+
readOnlyHint: false,
|
|
165
|
+
destructiveHint: false,
|
|
166
|
+
idempotentHint: true,
|
|
167
|
+
openWorldHint: false,
|
|
168
|
+
},
|
|
169
|
+
execute: async (args) => {
|
|
170
|
+
const result = await stageControlProposals(
|
|
171
|
+
currentRegistry,
|
|
172
|
+
currentFormId,
|
|
173
|
+
(args ?? {}) as Record<string, unknown>,
|
|
174
|
+
{ source: 'agent', actorId: 'assistant' },
|
|
175
|
+
);
|
|
176
|
+
return JSON.stringify({
|
|
177
|
+
...result,
|
|
178
|
+
message:
|
|
179
|
+
result.staged > 0
|
|
180
|
+
? 'Proposed changes are waiting for the person to review and apply them. Nothing was saved or submitted.'
|
|
181
|
+
: 'No reviewable changes provided',
|
|
182
|
+
});
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
},
|
|
186
|
+
// Same fallback as `<Form webmcp>`: used only when no Provider declares an
|
|
187
|
+
// explicit `webmcp.effects` policy.
|
|
188
|
+
{ effects: ['read', 'write'] },
|
|
189
|
+
);
|
|
190
|
+
</script>
|
|
191
|
+
|
|
192
|
+
<div class="smrt-form-scope" data-smrt-form-scope={formId} bind:this={scope}>
|
|
193
|
+
{@render children()}
|
|
194
|
+
<StagedControlReview {registry} {formId} {formElement} summary={stagedReview} />
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
<style>
|
|
198
|
+
/* Layout-neutral: the wrapped content lays out exactly as before. */
|
|
199
|
+
.smrt-form-scope {
|
|
200
|
+
display: contents;
|
|
201
|
+
}
|
|
202
|
+
</style>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type ControlInteractionRegistry } from '@happyvertical/smrt-ui/forms';
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
export interface Props {
|
|
4
|
+
/** Stable, page-unique form identity (also names the WebMCP tool). */
|
|
5
|
+
formId: string;
|
|
6
|
+
/** Plain-language name of what the form does, for the tool description. */
|
|
7
|
+
purpose?: string;
|
|
8
|
+
/** Register the `<formId>_stage_changes` proposal tool (default true). */
|
|
9
|
+
webmcp?: boolean | {
|
|
10
|
+
name?: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
};
|
|
13
|
+
/** Override the registry (tests, nested hosts). */
|
|
14
|
+
interactionRegistry?: ControlInteractionRegistry;
|
|
15
|
+
/** Show the staged-review summary for agent proposals (default true). */
|
|
16
|
+
stagedReview?: boolean;
|
|
17
|
+
children: Snippet;
|
|
18
|
+
}
|
|
19
|
+
declare const FormScope: import("svelte").Component<Props, {}, "">;
|
|
20
|
+
type FormScope = ReturnType<typeof FormScope>;
|
|
21
|
+
export default FormScope;
|
|
22
|
+
//# sourceMappingURL=FormScope.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FormScope.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/FormScope.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,KAAK,0BAA0B,EAOhC,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAMtC,MAAM,WAAW,KAAK;IACpB,sEAAsE;IACtE,MAAM,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,0EAA0E;IAC1E,MAAM,CAAC,EAAE,OAAO,GAAG;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3D,mDAAmD;IACnD,mBAAmB,CAAC,EAAE,0BAA0B,CAAC;IACjD,yEAAyE;IACzE,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,QAAQ,EAAE,OAAO,CAAC;CACnB;AA+JD,QAAA,MAAM,SAAS,2CAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { createControlInteractionRegistry } from '@happyvertical/smrt-ui/forms';
|
|
2
|
+
import { render, screen, waitFor } from '@testing-library/svelte';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import { tick } from 'svelte';
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
+
vi.mock('../../../hooks/useAppState.svelte.js', () => ({
|
|
7
|
+
useAppState: () => ({ state: { mode: 'default' }, setMode: vi.fn() }),
|
|
8
|
+
}));
|
|
9
|
+
vi.mock('../../../hooks/useSTT.svelte.js', () => ({
|
|
10
|
+
useSTT: () => ({
|
|
11
|
+
isListening: false,
|
|
12
|
+
lastResult: '',
|
|
13
|
+
isReady: false,
|
|
14
|
+
adapterType: null,
|
|
15
|
+
initialize: vi.fn(),
|
|
16
|
+
start: vi.fn(),
|
|
17
|
+
stop: vi.fn(),
|
|
18
|
+
}),
|
|
19
|
+
}));
|
|
20
|
+
import Fixture from './form-with-ui-controls.fixture.svelte';
|
|
21
|
+
function captureTools() {
|
|
22
|
+
const registered = [];
|
|
23
|
+
document.modelContext = {
|
|
24
|
+
async registerTool(tool) {
|
|
25
|
+
registered.push(tool);
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
return registered;
|
|
29
|
+
}
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
delete document.modelContext;
|
|
32
|
+
});
|
|
33
|
+
describe('rich Form: native attributes, enhance, and smrt-ui controls', () => {
|
|
34
|
+
it('passes native form attributes and attachments through to <form>', async () => {
|
|
35
|
+
const attach = vi.fn();
|
|
36
|
+
render(Fixture, { props: { attach } });
|
|
37
|
+
const form = screen.getByTestId('network-form');
|
|
38
|
+
expect(form.tagName).toBe('FORM');
|
|
39
|
+
expect(form).toHaveAttribute('enctype', 'multipart/form-data');
|
|
40
|
+
expect(form).toHaveAttribute('novalidate');
|
|
41
|
+
expect(form).toHaveAttribute('method', 'POST');
|
|
42
|
+
expect(form).toHaveAttribute('action', '?/create');
|
|
43
|
+
expect(form).toHaveAccessibleName('Set up your network');
|
|
44
|
+
expect(attach).toHaveBeenCalledWith(form);
|
|
45
|
+
});
|
|
46
|
+
it('applies an enhance action to the form and tears it down on unmount', () => {
|
|
47
|
+
const destroy = vi.fn();
|
|
48
|
+
const enhance = vi.fn(() => ({ destroy }));
|
|
49
|
+
const { unmount } = render(Fixture, { props: { enhance } });
|
|
50
|
+
expect(enhance).toHaveBeenCalledWith(screen.getByTestId('network-form'));
|
|
51
|
+
unmount();
|
|
52
|
+
expect(destroy).toHaveBeenCalledTimes(1);
|
|
53
|
+
});
|
|
54
|
+
it('lets an enhanced submit through without preventing it', async () => {
|
|
55
|
+
let submitted;
|
|
56
|
+
const enhance = (form) => {
|
|
57
|
+
const listener = (event) => {
|
|
58
|
+
submitted = event;
|
|
59
|
+
event.preventDefault();
|
|
60
|
+
};
|
|
61
|
+
form.addEventListener('submit', listener);
|
|
62
|
+
return { destroy: () => form.removeEventListener('submit', listener) };
|
|
63
|
+
};
|
|
64
|
+
render(Fixture, { props: { enhance } });
|
|
65
|
+
await userEvent.click(screen.getByRole('button', { name: 'Create' }));
|
|
66
|
+
expect(submitted).toBeDefined();
|
|
67
|
+
});
|
|
68
|
+
it('describes smrt-ui controls and rich fields in one stage-only tool', async () => {
|
|
69
|
+
const registered = captureTools();
|
|
70
|
+
render(Fixture);
|
|
71
|
+
await tick();
|
|
72
|
+
await vi.waitFor(() => expect(registered.length).toBeGreaterThan(0));
|
|
73
|
+
const tool = registered.at(-1);
|
|
74
|
+
expect(tool.name).toBe('setup-network_stage_changes');
|
|
75
|
+
expect(tool.inputSchema).toMatchObject({
|
|
76
|
+
type: 'object',
|
|
77
|
+
properties: {
|
|
78
|
+
tagline: { type: 'string', title: 'Tagline' },
|
|
79
|
+
name: { type: 'string', title: 'Network name' },
|
|
80
|
+
kind: { type: 'string', title: 'Kind of network' },
|
|
81
|
+
town: {
|
|
82
|
+
type: 'string',
|
|
83
|
+
title: 'Home town',
|
|
84
|
+
enum: ['lacombe', 'blackfalds'],
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
it('stages smrt-ui control proposals for review and never writes them', async () => {
|
|
90
|
+
const registered = captureTools();
|
|
91
|
+
const registry = createControlInteractionRegistry();
|
|
92
|
+
render(Fixture, { props: { interactionRegistry: registry } });
|
|
93
|
+
await tick();
|
|
94
|
+
await vi.waitFor(() => expect(registered.length).toBeGreaterThan(0));
|
|
95
|
+
const tool = registered.at(-1);
|
|
96
|
+
const result = await tool.execute({
|
|
97
|
+
name: 'Central Alberta News',
|
|
98
|
+
town: 'blackfalds',
|
|
99
|
+
tagline: 'Local first',
|
|
100
|
+
});
|
|
101
|
+
expect(result).toBe('Staged 3 changes for review');
|
|
102
|
+
expect(screen.getByRole('textbox', { name: 'Network name' })).toHaveValue('');
|
|
103
|
+
await waitFor(() => expect(screen.getByRole('region', { name: 'Review proposed changes' })).toBeInTheDocument());
|
|
104
|
+
expect(registry.get({ formId: 'setup-network', controlId: 'name' })?.state.staged
|
|
105
|
+
?.value).toBe('Central Alberta News');
|
|
106
|
+
expect(screen.queryByText(/setup-network\//)).not.toBeInTheDocument();
|
|
107
|
+
});
|
|
108
|
+
it('marks a staged smrt-ui proposal stale when the person types over it', async () => {
|
|
109
|
+
const registered = captureTools();
|
|
110
|
+
const registry = createControlInteractionRegistry();
|
|
111
|
+
render(Fixture, { props: { interactionRegistry: registry } });
|
|
112
|
+
await tick();
|
|
113
|
+
await vi.waitFor(() => expect(registered.length).toBeGreaterThan(0));
|
|
114
|
+
await registered.at(-1).execute({ name: 'Proposed' });
|
|
115
|
+
await userEvent.type(screen.getByRole('textbox', { name: 'Network name' }), 'Mine');
|
|
116
|
+
await waitFor(() => expect(registry.get({ formId: 'setup-network', controlId: 'name' })?.state
|
|
117
|
+
.staged?.stale).toBe(true));
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -1251,7 +1251,9 @@ describe('Form WebMCP staged-edit intent', () => {
|
|
|
1251
1251
|
const fullname = screen.getByRole('textbox', { name: 'Full name' });
|
|
1252
1252
|
expect(fullname).toHaveAttribute('data-smrt-subject-type', 'person');
|
|
1253
1253
|
expect(fullname).toHaveAttribute('data-smrt-subject-id', 'person-1');
|
|
1254
|
-
|
|
1254
|
+
// The review names the field, not the registry subject (plain labels).
|
|
1255
|
+
expect(await screen.findByRole('button', { name: 'Apply Full name' })).toBeInTheDocument();
|
|
1256
|
+
expect(screen.queryByText(/person:person-1/)).not.toBeInTheDocument();
|
|
1255
1257
|
await view.rerender({
|
|
1256
1258
|
webmcp: true,
|
|
1257
1259
|
showAge: false,
|
|
@@ -1263,12 +1265,12 @@ describe('Form WebMCP staged-edit intent', () => {
|
|
|
1263
1265
|
await vi.waitFor(() => expect(registered.length).toBeGreaterThan(_regLenBefore));
|
|
1264
1266
|
expect(await registered.at(-1)?.execute({ fullname: 'Grace' })).toBe('Staged 1 change for review');
|
|
1265
1267
|
expect(fullname).toHaveAttribute('data-smrt-subject-id', 'person-2');
|
|
1266
|
-
expect(await screen.
|
|
1268
|
+
expect(await screen.findByRole('button', { name: 'Apply Full name' })).toBeInTheDocument();
|
|
1267
1269
|
await userEvent.click(screen.getByRole('button', { name: 'Mutate subject' }));
|
|
1268
1270
|
await tick();
|
|
1269
1271
|
expect(await registered.at(-1)?.execute({ fullname: 'Katherine' })).toBe('Staged 1 change for review');
|
|
1270
1272
|
expect(fullname).toHaveAttribute('data-smrt-subject-id', 'person-mutated');
|
|
1271
|
-
expect(await screen.
|
|
1273
|
+
expect(await screen.findByRole('button', { name: 'Apply Full name' })).toBeInTheDocument();
|
|
1272
1274
|
});
|
|
1273
1275
|
it('publishes money in integer minor units and applies cents without rescaling', async () => {
|
|
1274
1276
|
const registered = [];
|