@happyvertical/smrt-svelte 0.51.4 → 0.51.6
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 +1 -0
- package/README.md +39 -1
- package/dist/components/workspace/README.md +25 -0
- package/dist/web/__tests__/assistant-dock.integration.svelte.test.js +204 -0
- package/dist/web/__tests__/list-data-surface.fixture.svelte +59 -0
- package/dist/web/__tests__/list-data-surface.fixture.svelte.d.ts +14 -0
- package/dist/web/__tests__/list-data-surface.fixture.svelte.d.ts.map +1 -0
- package/dist/web/__tests__/list-data-surface.svelte.test.js +106 -0
- package/dist/web/__tests__/list-data-surface.test.js +651 -0
- package/dist/web/index.d.ts +1 -0
- package/dist/web/index.d.ts.map +1 -1
- package/dist/web/index.js +1 -0
- package/dist/web/list-data-surface.svelte.d.ts +133 -0
- package/dist/web/list-data-surface.svelte.d.ts.map +1 -0
- package/dist/web/list-data-surface.svelte.js +308 -0
- package/package.json +13 -13
package/AGENTS.md
CHANGED
|
@@ -16,6 +16,7 @@ subpath you are editing. This file keeps what holds in every module.
|
|
|
16
16
|
| `src/components/settings/` (`./settings`) | `SettingsCatalog`, `paginateSettingsCatalog`, and the summary-vs-detail scalability contract | [agents/settings.md](agents/settings.md) |
|
|
17
17
|
| `src/components/workspace/` (`./workspace` + `./web`) | the AdminShell family and its principles, the legacy ToolsDock surface, the `./web` activity-feed and `updateAvailable` adapters, and server-side dock gates | [agents/workspace.md](agents/workspace.md) |
|
|
18
18
|
| `src/web/remote-query.svelte.ts` | Svelte 5 binding for query-shaped remote pages: rows, page, totals, loading/refreshing/stale/error, retry, last-updated, and query-scoped live subscriptions (#2445) | — |
|
|
19
|
+
| `src/web/list-data-surface.svelte.ts` (`mountListDataSurface`) | One-call registration of an existing, non-`DataTable` list (already mirroring a headless `DataTableController`) as a mounted `DataSurfaceDescriptor` — a same-behavior port of `registerContentListDataSurface`'s registration/translation logic (`@happyvertical/smrt-content/svelte`), generalized off ContentList's view-mode concept, so any custom list markup can adopt it directly instead of hand-mirroring the registry contract. Not yet unified behind a shared `@happyvertical/smrt-ui/data` implementation (#2917) — this copy is currently ahead; see its own module doc comment for the current, authoritative list of divergences #2917 must adopt rather than assume parity (#2906) | — |
|
|
19
20
|
| `src/web/webmcp-provider.ts` | Provider config for generated data/model WebMCP tools: definitions, effect policy, namespace, budget, legacy/canonical filters, and fetcher seams (#2520) | — |
|
|
20
21
|
| `src/web/webmcp-ui.ts` | Fixed, low-cardinality WebMCP adapter over the Provider's mounted form-control and data-surface registries (#2521) | — |
|
|
21
22
|
| `src/web/webmcp.svelte.ts` (`useWebMcpTool`) | Component-owned bespoke WebMCP tool. Routes through `@happyvertical/smrt-web`'s `registerWebMcpBespokeTool`, so it shares the fail-closed effect classification and `effects` exposure policy of generated tools — undeclared `annotations` classify destructive and are excluded by default. `namespace` and `maxTools` never apply to a bespoke tool (#2586) | — |
|
package/README.md
CHANGED
|
@@ -144,6 +144,44 @@ should be discoverable:
|
|
|
144
144
|
</Provider>
|
|
145
145
|
```
|
|
146
146
|
|
|
147
|
+
A page with its own hand-rolled list markup — not `DataTable` — that already
|
|
148
|
+
mirrors a headless `DataTableController`'s search/filters/sort/page/selection
|
|
149
|
+
can register that same registry in one call with `mountListDataSurface`
|
|
150
|
+
(`@happyvertical/smrt-svelte/web`) instead of hand-writing the registration:
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
import { createDataTableController } from '@happyvertical/smrt-ui/data';
|
|
154
|
+
import { mountListDataSurface } from '@happyvertical/smrt-svelte/web';
|
|
155
|
+
|
|
156
|
+
const controller = createDataTableController();
|
|
157
|
+
const handle = mountListDataSurface({
|
|
158
|
+
registry: surfaces,
|
|
159
|
+
descriptor: myListDescriptor,
|
|
160
|
+
controller,
|
|
161
|
+
context: { totalRows, queryFingerprint },
|
|
162
|
+
refresh: () => reload(),
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Only `controller` is observed automatically. App-owned `context` is a
|
|
166
|
+
// one-time snapshot at mount — publish a fresh one whenever totalRows,
|
|
167
|
+
// queryFingerprint, or freshness changes (an $effect keyed on those values
|
|
168
|
+
// is the usual place):
|
|
169
|
+
handle.update({ totalRows, queryFingerprint });
|
|
170
|
+
|
|
171
|
+
// on unmount:
|
|
172
|
+
handle.destroy();
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
It mirrors `controller` into the registry, translates visible table commands
|
|
176
|
+
back into `controller.dispatch()` calls, and routes the fixed
|
|
177
|
+
`refresh`/`retry`/`focus`/`reveal`/`highlight` controls to callbacks; any
|
|
178
|
+
other `controlId` goes through an `onControl` escape hatch (denied by
|
|
179
|
+
default) — except a canonical table-control id (`set-filters`, `reset`,
|
|
180
|
+
`set-page`, … the full `DATA_TABLE_SURFACE_CONTROL_IDS` list from
|
|
181
|
+
`@happyvertical/smrt-ui/data`), which is always intercepted first and never
|
|
182
|
+
reaches `onControl`, even under a custom label. See
|
|
183
|
+
`docs/data-surface-conformance.md` for the full contract.
|
|
184
|
+
|
|
147
185
|
The default prefix is `smrt_ui_`. Configure `ui.prefix` when multiple Providers
|
|
148
186
|
must coexist in one document; the same prefix cannot be registered twice. The
|
|
149
187
|
six derived names are reserved through the document-global tool-name lock, so
|
|
@@ -306,7 +344,7 @@ importable, even if it appears in `dist/`.
|
|
|
306
344
|
| `@happyvertical/smrt-svelte/workspace/live` | `systemFeed` — the AdminShell system scope (jobs/schedules/dispatch) polled from an app status endpoint; deliberately carries no `smrt-web` dependency |
|
|
307
345
|
| `@happyvertical/smrt-svelte/browser-ai` | Browser AI client (STT/TTS/LLM adapters, capability detection) |
|
|
308
346
|
| `@happyvertical/smrt-svelte/browser-ai/svelte` | Svelte AI components (VoiceInput, CapabilityGate, etc.) |
|
|
309
|
-
| `@happyvertical/smrt-svelte/web` | `smrt-web` live-query bindings (`liveCollection`, `activityFeed`, `useUpdateAvailable`) |
|
|
347
|
+
| `@happyvertical/smrt-svelte/web` | `smrt-web` live-query bindings (`liveCollection`, `activityFeed`, `useUpdateAvailable`) plus `mountListDataSurface` (custom-list data-surface registration) |
|
|
310
348
|
| `@happyvertical/smrt-svelte/i18n/server` | Server-side i18n resolver (Node only) |
|
|
311
349
|
|
|
312
350
|
Domain-agnostic UI lives in `@happyvertical/smrt-ui`. There is no `ui`,
|
|
@@ -35,6 +35,31 @@ identity and action content belongs in `topLeftCorner` / `topRightCorner`,
|
|
|
35
35
|
wrapped in `ShellCorner side="left"` or `ShellCorner side="right"` so it follows
|
|
36
36
|
the corresponding shell track as that edge expands and collapses.
|
|
37
37
|
|
|
38
|
+
## AssistantDock recipe (#2904)
|
|
39
|
+
|
|
40
|
+
`ShellDockTool` also hosts `@happyvertical/smrt-chat/svelte`'s `AssistantDock`
|
|
41
|
+
— a route-aware assistant surface. This package has no runtime dependency on
|
|
42
|
+
`@happyvertical/smrt-chat` (only a devDependency, for tests); the composition
|
|
43
|
+
below is intentionally application code, not a `workspace/` export:
|
|
44
|
+
|
|
45
|
+
```svelte
|
|
46
|
+
<script lang="ts">
|
|
47
|
+
import { ShellDockTool } from '@happyvertical/smrt-svelte/workspace';
|
|
48
|
+
import { AssistantDock } from '@happyvertical/smrt-chat/svelte';
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<ShellDockTool id="assistant" label="Assistant" icon="bot">
|
|
52
|
+
{#snippet render()}
|
|
53
|
+
<AssistantDock {transport} {registry} />
|
|
54
|
+
{/snippet}
|
|
55
|
+
</ShellDockTool>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`registry` is the same `DataSurfaceRegistry` instance mounted routes register
|
|
59
|
+
their descriptors on. See
|
|
60
|
+
[`docs/assistant-dock.md`](../../../../../docs/assistant-dock.md) for the full
|
|
61
|
+
design.
|
|
62
|
+
|
|
38
63
|
## Migration
|
|
39
64
|
|
|
40
65
|
See [MIGRATION.md](./MIGRATION.md) for the first-generation workspace migration
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
/**
|
|
3
|
+
* AssistantDock conformance-style integration test (#2904).
|
|
4
|
+
*
|
|
5
|
+
* Mirrors `data-surface-conformance.integration.svelte.test.ts` in this
|
|
6
|
+
* directory: a real `DataSurfaceRegistry` and a real
|
|
7
|
+
* `createAssistantDockController`/`AssistantDock` are composed together;
|
|
8
|
+
* only the chat transport and the action client are in-process test doubles
|
|
9
|
+
* (`createInMemoryAssistantTransport`, and a thin `actionClient` adapter over
|
|
10
|
+
* the registry's own `execute()`), matching that file's stated scope of
|
|
11
|
+
* faking only the transport boundary.
|
|
12
|
+
*
|
|
13
|
+
* Scope note (disclosed deviation from the original test plan): the action
|
|
14
|
+
* preview/apply path here is driven directly through
|
|
15
|
+
* `createAssistantDockController` rather than by having a fake model reply
|
|
16
|
+
* trigger it through the rendered `AssistantDock` DOM — `AssistantDock` has
|
|
17
|
+
* no built-in "parse this assistant message as an action proposal" step (no
|
|
18
|
+
* such parsing is specified anywhere in #2904's binding decisions), so the
|
|
19
|
+
* host application is expected to call `controller.previewAction(...)` from
|
|
20
|
+
* its own message-rendering logic. This test still exercises the full
|
|
21
|
+
* dock (registry discovery, rendering, send/poll) through the DOM, and
|
|
22
|
+
* separately proves the action pathway (preview → confirm → apply → registry
|
|
23
|
+
* "command" event) end-to-end against the real registry.
|
|
24
|
+
*/
|
|
25
|
+
import { AssistantDock, createAssistantDockController, createInMemoryAssistantTransport, } from '@happyvertical/smrt-chat/svelte';
|
|
26
|
+
import { createDataSurfaceRegistry, } from '@happyvertical/smrt-ui/data-surface';
|
|
27
|
+
import { render, screen, waitFor } from '@testing-library/svelte';
|
|
28
|
+
import { describe, expect, it } from 'vitest';
|
|
29
|
+
const identity = {
|
|
30
|
+
surfaceId: 'assistant-dock-orders',
|
|
31
|
+
kind: 'table',
|
|
32
|
+
subject: { type: 'tenant', id: 'tenant-a' },
|
|
33
|
+
};
|
|
34
|
+
const descriptor = {
|
|
35
|
+
version: 1,
|
|
36
|
+
identity,
|
|
37
|
+
schemaVersion: 1,
|
|
38
|
+
label: 'Orders',
|
|
39
|
+
rowKey: 'id',
|
|
40
|
+
columns: [
|
|
41
|
+
{
|
|
42
|
+
id: 'id',
|
|
43
|
+
label: 'ID',
|
|
44
|
+
capabilities: ['read', 'project'],
|
|
45
|
+
role: 'row-key',
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
query: {
|
|
49
|
+
modes: ['rows'],
|
|
50
|
+
projectableColumnIds: ['id'],
|
|
51
|
+
searchableColumnIds: [],
|
|
52
|
+
filterableColumnIds: [],
|
|
53
|
+
sortableColumnIds: [],
|
|
54
|
+
},
|
|
55
|
+
actions: [
|
|
56
|
+
{
|
|
57
|
+
id: 'archive',
|
|
58
|
+
label: 'Archive',
|
|
59
|
+
selectionScopes: ['explicit-ids'],
|
|
60
|
+
requiresConfirmation: true,
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
controls: [{ id: 'data-surface.action.archive', label: 'Archive' }],
|
|
64
|
+
limits: { maxQueryRows: 100, maxQueryBytes: 100_000, maxSelectionSize: 100 },
|
|
65
|
+
};
|
|
66
|
+
function mountRegistryWithOneSurface() {
|
|
67
|
+
const registry = createDataSurfaceRegistry();
|
|
68
|
+
let revision = 1;
|
|
69
|
+
const state = { archived: false };
|
|
70
|
+
const unregister = registry.register({
|
|
71
|
+
descriptor,
|
|
72
|
+
getSnapshot: () => ({
|
|
73
|
+
version: 1,
|
|
74
|
+
descriptor,
|
|
75
|
+
revision,
|
|
76
|
+
state: { archived: state.archived },
|
|
77
|
+
}),
|
|
78
|
+
execute: async (command) => {
|
|
79
|
+
// A minimal real mutation: the "archive" action flips `archived` and
|
|
80
|
+
// bumps the snapshot revision, which is what makes the registry emit a
|
|
81
|
+
// 'command' event other mounted UI can react to.
|
|
82
|
+
state.archived = true;
|
|
83
|
+
revision += 1;
|
|
84
|
+
return {
|
|
85
|
+
version: 1,
|
|
86
|
+
commandId: command.commandId,
|
|
87
|
+
identity: command.identity,
|
|
88
|
+
ok: true,
|
|
89
|
+
revision,
|
|
90
|
+
};
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
return { registry, unregister, state: () => state.archived };
|
|
94
|
+
}
|
|
95
|
+
describe('AssistantDock integration (#2904)', () => {
|
|
96
|
+
it('fails closed and shows the no-surfaces notice with an empty registry', async () => {
|
|
97
|
+
const registry = createDataSurfaceRegistry();
|
|
98
|
+
const transport = createInMemoryAssistantTransport();
|
|
99
|
+
render(AssistantDock, { props: { transport, registry } });
|
|
100
|
+
await waitFor(() => {
|
|
101
|
+
expect(screen.getByText(/No data surfaces are mounted on this route/i)).toBeInTheDocument();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
it('discovers a mounted surface via the registry and renders a sent conversation', async () => {
|
|
105
|
+
const { registry } = mountRegistryWithOneSurface();
|
|
106
|
+
const transport = createInMemoryAssistantTransport({
|
|
107
|
+
respond: (_threadId, userMessage) => ({
|
|
108
|
+
id: 'assistant-1',
|
|
109
|
+
threadId: userMessage.threadId,
|
|
110
|
+
content: 'Archived the order.',
|
|
111
|
+
role: 'assistant',
|
|
112
|
+
createdAt: new Date(),
|
|
113
|
+
}),
|
|
114
|
+
});
|
|
115
|
+
render(AssistantDock, { props: { transport, registry } });
|
|
116
|
+
await waitFor(() => {
|
|
117
|
+
expect(screen.queryByText(/No data surfaces are mounted on this route/i)).not.toBeInTheDocument();
|
|
118
|
+
});
|
|
119
|
+
const thread = await transport.createThread('Order question');
|
|
120
|
+
// Drive the same controller the component owns internally by exercising
|
|
121
|
+
// the transport directly (the composer's own send() path is covered at
|
|
122
|
+
// the unit level in packages/chat); here we assert the descriptor is
|
|
123
|
+
// visible to a controller built against this exact registry instance.
|
|
124
|
+
const controller = createAssistantDockController({ transport, registry });
|
|
125
|
+
await controller.openThread(thread.id);
|
|
126
|
+
// Cycle-3 second final finding 1: an attached file must round-trip onto
|
|
127
|
+
// the sent message, not just the message text — this is the surface
|
|
128
|
+
// that previously rendered nothing for message.attachments.
|
|
129
|
+
await controller.send('please archive this order', [
|
|
130
|
+
{ id: 'att-1', name: 'order-details.pdf' },
|
|
131
|
+
]);
|
|
132
|
+
await waitFor(() => {
|
|
133
|
+
expect(controller.messages.some((m) => m.content === 'Archived the order.')).toBe(true);
|
|
134
|
+
});
|
|
135
|
+
const sentMessage = controller.messages.find((m) => m.content === 'please archive this order');
|
|
136
|
+
expect(sentMessage?.attachments).toEqual([
|
|
137
|
+
{ id: 'att-1', name: 'order-details.pdf' },
|
|
138
|
+
]);
|
|
139
|
+
expect(controller.surfaces).toHaveLength(1);
|
|
140
|
+
expect(controller.surfaces[0].surfaceId).toBe('assistant-dock-orders');
|
|
141
|
+
controller.dispose();
|
|
142
|
+
});
|
|
143
|
+
it('drives preview → confirm → apply through the real registry and fires its "command" event', async () => {
|
|
144
|
+
const { registry, state } = mountRegistryWithOneSurface();
|
|
145
|
+
const events = [];
|
|
146
|
+
registry.subscribe((event) => events.push(event));
|
|
147
|
+
const actionClient = {
|
|
148
|
+
async preview(request) {
|
|
149
|
+
// A preview never mutates: assert the surface is untouched.
|
|
150
|
+
expect(state()).toBe(false);
|
|
151
|
+
return {
|
|
152
|
+
version: 1,
|
|
153
|
+
requestId: request.requestId,
|
|
154
|
+
identity: request.identity,
|
|
155
|
+
actionId: request.actionId,
|
|
156
|
+
phase: 'preview',
|
|
157
|
+
ok: true,
|
|
158
|
+
confirmationToken: 'token-1',
|
|
159
|
+
};
|
|
160
|
+
},
|
|
161
|
+
async apply(request) {
|
|
162
|
+
const result = await registry.execute({
|
|
163
|
+
version: 1,
|
|
164
|
+
commandId: request.requestId,
|
|
165
|
+
identity: request.identity,
|
|
166
|
+
expectedRevision: 1,
|
|
167
|
+
controlId: `data-surface.action.${request.actionId}`,
|
|
168
|
+
});
|
|
169
|
+
return {
|
|
170
|
+
version: 1,
|
|
171
|
+
requestId: request.requestId,
|
|
172
|
+
identity: request.identity,
|
|
173
|
+
actionId: request.actionId,
|
|
174
|
+
phase: 'apply',
|
|
175
|
+
ok: result.ok,
|
|
176
|
+
reason: result.ok ? undefined : result.reason,
|
|
177
|
+
};
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
const controller = createAssistantDockController({
|
|
181
|
+
transport: createInMemoryAssistantTransport(),
|
|
182
|
+
registry,
|
|
183
|
+
actionClient,
|
|
184
|
+
});
|
|
185
|
+
const requestId = 'archive-req-1';
|
|
186
|
+
await controller.previewAction({
|
|
187
|
+
version: 1,
|
|
188
|
+
requestId,
|
|
189
|
+
identity,
|
|
190
|
+
actionId: 'archive',
|
|
191
|
+
phase: 'preview',
|
|
192
|
+
selection: { scope: 'explicit-ids', rowIds: ['order-1'] },
|
|
193
|
+
});
|
|
194
|
+
expect(controller.actions.get(requestId)?.status).toBe('previewed');
|
|
195
|
+
expect(state()).toBe(false); // preview never mutated the surface
|
|
196
|
+
// applyAction now takes only requestId — it reuses the idempotencyKey
|
|
197
|
+
// minted once by previewAction (binding decision #2904, build phase 2).
|
|
198
|
+
await controller.applyAction(requestId);
|
|
199
|
+
expect(controller.actions.get(requestId)?.status).toBe('applied');
|
|
200
|
+
expect(state()).toBe(true);
|
|
201
|
+
expect(events.some((e) => e.type === 'command')).toBe(true);
|
|
202
|
+
controller.dispose();
|
|
203
|
+
});
|
|
204
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type {
|
|
3
|
+
DataSurfaceDescriptor,
|
|
4
|
+
DataSurfaceRegistry,
|
|
5
|
+
DataTableController,
|
|
6
|
+
} from '@happyvertical/smrt-ui/data';
|
|
7
|
+
import {
|
|
8
|
+
type ListDataSurfaceContext,
|
|
9
|
+
mountListDataSurface,
|
|
10
|
+
} from '../list-data-surface.svelte.js';
|
|
11
|
+
|
|
12
|
+
let {
|
|
13
|
+
registry,
|
|
14
|
+
descriptor,
|
|
15
|
+
controller,
|
|
16
|
+
context,
|
|
17
|
+
onRefresh,
|
|
18
|
+
onStar,
|
|
19
|
+
}: {
|
|
20
|
+
registry: DataSurfaceRegistry;
|
|
21
|
+
descriptor: DataSurfaceDescriptor;
|
|
22
|
+
controller: DataTableController;
|
|
23
|
+
context: ListDataSurfaceContext;
|
|
24
|
+
onRefresh?: () => boolean | Promise<boolean>;
|
|
25
|
+
onStar?: (payload: unknown) => boolean;
|
|
26
|
+
} = $props();
|
|
27
|
+
|
|
28
|
+
let starredCount = $state(0);
|
|
29
|
+
let filterText = $state('');
|
|
30
|
+
|
|
31
|
+
$effect(() => {
|
|
32
|
+
const handle = mountListDataSurface({
|
|
33
|
+
registry,
|
|
34
|
+
descriptor,
|
|
35
|
+
controller,
|
|
36
|
+
context,
|
|
37
|
+
refresh: onRefresh,
|
|
38
|
+
onControl: (controlId, payload) => {
|
|
39
|
+
if (controlId === 'star') {
|
|
40
|
+
starredCount += 1;
|
|
41
|
+
return onStar ? onStar(payload) : true;
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
const unsubscribe = controller.subscribe(() => {
|
|
47
|
+
filterText = String(controller.getState().filters[0]?.value ?? '');
|
|
48
|
+
});
|
|
49
|
+
return () => {
|
|
50
|
+
unsubscribe();
|
|
51
|
+
handle.destroy();
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<ul data-testid="custom-list">
|
|
57
|
+
<li data-testid="filter-text">{filterText}</li>
|
|
58
|
+
</ul>
|
|
59
|
+
<p data-testid="starred-count">{starredCount}</p>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { DataSurfaceDescriptor, DataSurfaceRegistry, DataTableController } from '@happyvertical/smrt-ui/data';
|
|
2
|
+
import { type ListDataSurfaceContext } from '../list-data-surface.svelte.js';
|
|
3
|
+
type $$ComponentProps = {
|
|
4
|
+
registry: DataSurfaceRegistry;
|
|
5
|
+
descriptor: DataSurfaceDescriptor;
|
|
6
|
+
controller: DataTableController;
|
|
7
|
+
context: ListDataSurfaceContext;
|
|
8
|
+
onRefresh?: () => boolean | Promise<boolean>;
|
|
9
|
+
onStar?: (payload: unknown) => boolean;
|
|
10
|
+
};
|
|
11
|
+
declare const ListDataSurface: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
12
|
+
type ListDataSurface = ReturnType<typeof ListDataSurface>;
|
|
13
|
+
export default ListDataSurface;
|
|
14
|
+
//# sourceMappingURL=list-data-surface.fixture.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"list-data-surface.fixture.svelte.d.ts","sourceRoot":"","sources":["../../../src/web/__tests__/list-data-surface.fixture.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,qBAAqB,EACrB,mBAAmB,EACnB,mBAAmB,EACpB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACL,KAAK,sBAAsB,EAE5B,MAAM,gCAAgC,CAAC;AAEvC,KAAK,gBAAgB,GAAI;IACxB,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,UAAU,EAAE,qBAAqB,CAAC;IAClC,UAAU,EAAE,mBAAmB,CAAC;IAChC,OAAO,EAAE,sBAAsB,CAAC;IAChC,SAAS,CAAC,EAAE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC7C,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC;CACxC,CAAC;AAiDF,QAAA,MAAM,eAAe,sDAAwC,CAAC;AAC9D,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;AAC1D,eAAe,eAAe,CAAC"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component-lifecycle proof for `mountListDataSurface` (#2906), in the same
|
|
3
|
+
* conformance style as `data-surface-conformance.integration.svelte.test.ts`:
|
|
4
|
+
* a real Svelte component mounts a custom (non-DataTable) list, the registry
|
|
5
|
+
* drives it with commands, and unmounting the component tears the
|
|
6
|
+
* registration down. The registration/translation logic itself is unit
|
|
7
|
+
* tested directly against the registry in `list-data-surface.test.ts`.
|
|
8
|
+
*/
|
|
9
|
+
import { createDataSurfaceRegistry, createDataTableController, } from '@happyvertical/smrt-ui/data';
|
|
10
|
+
import { render, screen } from '@testing-library/svelte';
|
|
11
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
12
|
+
import Fixture from './list-data-surface.fixture.svelte';
|
|
13
|
+
const identity = {
|
|
14
|
+
surfaceId: 'fixture-list',
|
|
15
|
+
kind: 'list',
|
|
16
|
+
};
|
|
17
|
+
function descriptor() {
|
|
18
|
+
return {
|
|
19
|
+
version: 1,
|
|
20
|
+
identity,
|
|
21
|
+
schemaVersion: 1,
|
|
22
|
+
label: 'Fixture list',
|
|
23
|
+
rowKey: 'id',
|
|
24
|
+
columns: [
|
|
25
|
+
{ id: 'id', label: 'ID', capabilities: ['read', 'project'] },
|
|
26
|
+
{
|
|
27
|
+
id: 'title',
|
|
28
|
+
label: 'Title',
|
|
29
|
+
capabilities: ['read', 'search', 'filter', 'sort', 'project'],
|
|
30
|
+
operators: { filter: ['contains'] },
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
query: {
|
|
34
|
+
modes: ['rows', 'count'],
|
|
35
|
+
projectableColumnIds: ['id', 'title'],
|
|
36
|
+
filterableColumnIds: ['title'],
|
|
37
|
+
sortableColumnIds: ['title'],
|
|
38
|
+
},
|
|
39
|
+
controls: [
|
|
40
|
+
{ id: 'set-filters', label: 'Filter' },
|
|
41
|
+
{ id: 'refresh', label: 'Refresh' },
|
|
42
|
+
{ id: 'star', label: 'Star selected' },
|
|
43
|
+
],
|
|
44
|
+
actions: [],
|
|
45
|
+
limits: { maxQueryRows: 50, maxQueryBytes: 10_000, maxSelectionSize: 5 },
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
describe('mountListDataSurface (component integration)', () => {
|
|
49
|
+
it('drives a mounted custom list from the registry and tears down on unmount', async () => {
|
|
50
|
+
const registry = createDataSurfaceRegistry();
|
|
51
|
+
const controller = createDataTableController();
|
|
52
|
+
const onRefresh = vi.fn().mockResolvedValue(true);
|
|
53
|
+
const { unmount } = render(Fixture, {
|
|
54
|
+
props: {
|
|
55
|
+
registry,
|
|
56
|
+
descriptor: descriptor(),
|
|
57
|
+
controller,
|
|
58
|
+
context: { totalRows: 3, queryFingerprint: 'fixture-1' },
|
|
59
|
+
onRefresh,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
expect(screen.getByTestId('filter-text').textContent).toBe('');
|
|
63
|
+
const before = registry.inspect(identity);
|
|
64
|
+
expect(before?.state.totalRows).toBe(3);
|
|
65
|
+
const filtered = await registry.execute({
|
|
66
|
+
version: 1,
|
|
67
|
+
commandId: 'filter-1',
|
|
68
|
+
identity,
|
|
69
|
+
expectedRevision: before?.revision ?? 0,
|
|
70
|
+
controlId: 'set-filters',
|
|
71
|
+
payload: {
|
|
72
|
+
filters: [{ columnId: 'title', operator: 'contains', value: 'draft' }],
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
expect(filtered.ok).toBe(true);
|
|
76
|
+
expect(screen.getByTestId('filter-text').textContent).toBe('draft');
|
|
77
|
+
const refreshed = await registry.execute({
|
|
78
|
+
version: 1,
|
|
79
|
+
commandId: 'refresh-1',
|
|
80
|
+
identity,
|
|
81
|
+
expectedRevision: registry.inspect(identity)?.revision ?? 0,
|
|
82
|
+
controlId: 'refresh',
|
|
83
|
+
});
|
|
84
|
+
expect(refreshed.ok).toBe(true);
|
|
85
|
+
expect(onRefresh).toHaveBeenCalledOnce();
|
|
86
|
+
const starred = await registry.execute({
|
|
87
|
+
version: 1,
|
|
88
|
+
commandId: 'star-1',
|
|
89
|
+
identity,
|
|
90
|
+
expectedRevision: registry.inspect(identity)?.revision ?? 0,
|
|
91
|
+
controlId: 'star',
|
|
92
|
+
});
|
|
93
|
+
expect(starred.ok).toBe(true);
|
|
94
|
+
expect(screen.getByTestId('starred-count').textContent).toBe('1');
|
|
95
|
+
unmount();
|
|
96
|
+
// Unmounting tore down the mounted surface — the identity is gone.
|
|
97
|
+
expect(registry.inspect(identity)).toBeUndefined();
|
|
98
|
+
await expect(registry.execute({
|
|
99
|
+
version: 1,
|
|
100
|
+
commandId: 'after-unmount',
|
|
101
|
+
identity,
|
|
102
|
+
expectedRevision: 0,
|
|
103
|
+
controlId: 'refresh',
|
|
104
|
+
})).resolves.toMatchObject({ ok: false, reason: 'not_found' });
|
|
105
|
+
});
|
|
106
|
+
});
|