@svadmin/surface 0.8.19 → 0.10.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/README.md +76 -38
- package/STYLING.md +33 -0
- package/compatibility.json +40 -8
- package/dist/agent-contract.d.ts +37 -0
- package/dist/agent-contract.js +206 -0
- package/dist/agent.d.ts +38 -7
- package/dist/agent.js +98 -59
- package/dist/binding.d.ts +2 -0
- package/dist/binding.js +7 -1
- package/dist/builtin-definitions.d.ts +6 -0
- package/dist/builtin-definitions.js +40 -0
- package/dist/builtin-schemas.d.ts +25 -0
- package/dist/builtin-schemas.js +13 -0
- package/dist/business-data.d.ts +16 -0
- package/dist/business-data.js +42 -0
- package/dist/business-definitions.d.ts +28 -0
- package/dist/business-definitions.js +53 -0
- package/dist/business.d.ts +3 -0
- package/dist/business.js +17 -0
- package/dist/catalog.d.ts +4 -61
- package/dist/catalog.js +20 -44
- package/dist/components/ActivityFeedWidget.svelte +34 -0
- package/dist/components/ActivityFeedWidget.svelte.d.ts +4 -0
- package/dist/components/MetricWidget.svelte +13 -8
- package/dist/components/MetricWidget.svelte.d.ts +1 -0
- package/dist/components/ResourceDetailWidget.svelte +56 -0
- package/dist/components/ResourceDetailWidget.svelte.d.ts +4 -0
- package/dist/components/ResourceFormBody.svelte +97 -0
- package/dist/components/ResourceFormBody.svelte.d.ts +13 -0
- package/dist/components/ResourceFormWidget.svelte +19 -0
- package/dist/components/ResourceFormWidget.svelte.d.ts +4 -0
- package/dist/components/ResourceTableWidget.svelte +15 -12
- package/dist/components/ResourceTableWidget.svelte.d.ts +1 -0
- package/dist/components/SurfaceEditPreview.svelte +107 -0
- package/dist/components/SurfaceEditPreview.svelte.d.ts +20 -0
- package/dist/components/SurfaceRenderer.svelte +121 -163
- package/dist/components/SurfaceRenderer.svelte.d.ts +5 -0
- package/dist/components/SurfaceWorkflowProvider.svelte +22 -0
- package/dist/components/SurfaceWorkflowProvider.svelte.d.ts +17 -0
- package/dist/components/SvarGridWidget.svelte +47 -0
- package/dist/components/SvarGridWidget.svelte.d.ts +4 -0
- package/dist/components/SvarSurfaceProvider.svelte +12 -0
- package/dist/components/SvarSurfaceProvider.svelte.d.ts +11 -0
- package/dist/design-contract.d.ts +2 -0
- package/dist/design-contract.js +2 -0
- package/dist/edits.d.ts +57 -0
- package/dist/edits.js +166 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.js +5 -1
- package/dist/interactive.d.ts +6 -0
- package/dist/interactive.js +15 -0
- package/dist/localization.d.ts +11 -1
- package/dist/localization.js +20 -0
- package/dist/openui.d.ts +64 -0
- package/dist/openui.js +168 -0
- package/dist/recipes.d.ts +20 -0
- package/dist/recipes.js +20 -0
- package/dist/schema.d.ts +108 -0
- package/dist/schema.js +73 -0
- package/dist/server-sqlite.d.ts +1 -0
- package/dist/server-sqlite.js +2 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.js +4 -0
- package/dist/source-cache.d.ts +30 -0
- package/dist/source-cache.js +135 -0
- package/dist/styles/editor.css +124 -0
- package/dist/styles/editor.d.ts +8 -0
- package/dist/styles/editor.js +9 -0
- package/dist/styles.css +43 -0
- package/dist/svar-catalog.d.ts +3 -0
- package/dist/svar-catalog.js +7 -0
- package/dist/svar-context.d.ts +9 -0
- package/dist/svar-context.js +1 -0
- package/dist/svar-schema.d.ts +21 -0
- package/dist/svar-schema.js +36 -0
- package/dist/svar.d.ts +4 -0
- package/dist/svar.js +3 -0
- package/dist/svelte.d.ts +4 -2
- package/dist/svelte.js +2 -1
- package/dist/types.d.ts +5 -1
- package/dist/validation.js +14 -108
- package/dist/wire.d.ts +13 -0
- package/dist/wire.js +30 -0
- package/dist/workflows/action-contracts.d.ts +7 -0
- package/dist/workflows/action-contracts.js +54 -0
- package/dist/workflows/catalog.d.ts +20 -0
- package/dist/workflows/catalog.js +50 -0
- package/dist/workflows/client.d.ts +45 -0
- package/dist/workflows/client.js +125 -0
- package/dist/workflows/context.d.ts +9 -0
- package/dist/workflows/context.js +5 -0
- package/dist/workflows/openui-guard.d.ts +19 -0
- package/dist/workflows/openui-guard.js +209 -0
- package/dist/workflows/service.d.ts +58 -0
- package/dist/workflows/service.js +296 -0
- package/dist/workflows/sqlite-store.d.ts +20 -0
- package/dist/workflows/sqlite-store.js +91 -0
- package/dist/workflows/types.d.ts +103 -0
- package/dist/workflows/types.js +8 -0
- package/dist/workflows.d.ts +5 -0
- package/dist/workflows.js +4 -0
- package/package.json +59 -8
|
@@ -1,22 +1,31 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import { captureAdminContext, getLogoutVersion } from '@svadmin/core';
|
|
3
3
|
import { useTranslation } from '@svadmin/core/i18n';
|
|
4
|
-
import { untrack } from 'svelte';
|
|
5
|
-
import {
|
|
4
|
+
import { onDestroy, untrack } from 'svelte';
|
|
5
|
+
import { SvelteMap } from 'svelte/reactivity';
|
|
6
|
+
import { resolveSurfaceSourceData } from '../binding.js';
|
|
6
7
|
import { defaultSurfaceCatalog } from '../catalog.js';
|
|
7
8
|
import type { SurfaceRenderCatalog } from '../catalog.js';
|
|
8
9
|
import { loadSurfaceSource } from '../runtime.js';
|
|
9
10
|
import { resolveSurfaceMessages } from '../localization.js';
|
|
10
11
|
import type { SurfaceMessages } from '../localization.js';
|
|
11
12
|
import type {
|
|
12
|
-
SurfaceDataError,
|
|
13
|
-
|
|
14
|
-
SurfaceDataSource,
|
|
15
|
-
SurfacePolicy,
|
|
16
|
-
SurfaceSourceDataState,
|
|
17
|
-
SurfaceValidationIssue,
|
|
13
|
+
SurfaceDataError, SurfaceDataProvider, SurfacePolicy,
|
|
14
|
+
SurfaceSourceDataState, SurfaceValidationIssue, JsonObject,
|
|
18
15
|
} from '../types.js';
|
|
19
16
|
import { validateSurfaceSpec } from '../validation.js';
|
|
17
|
+
import { createSurfaceSourceCache, sameSourceIdentity, snapshotSurfaceSource } from '../source-cache.js';
|
|
18
|
+
import type { SurfaceSourceRequest } from '../source-cache.js';
|
|
19
|
+
import { surfaceMetric } from '../recipes.js';
|
|
20
|
+
import '../styles.css';
|
|
21
|
+
import { withoutSurfaceAppearance } from '../workflows/catalog.js';
|
|
22
|
+
|
|
23
|
+
function appearanceClasses(props: JsonObject): string {
|
|
24
|
+
const appearance = props['appearance'] as { tone?: 'neutral' | 'success' | 'warning' | 'danger' | 'info'; density?: 'compact' | 'comfortable' } | undefined;
|
|
25
|
+
if (!appearance) return '';
|
|
26
|
+
const classes = surfaceMetric(appearance);
|
|
27
|
+
return `surface-appearance ${classes.root} ${classes.card}`;
|
|
28
|
+
}
|
|
20
29
|
|
|
21
30
|
export type SurfaceRendererError =
|
|
22
31
|
| { readonly type: 'validation'; readonly issues: readonly SurfaceValidationIssue[] }
|
|
@@ -27,6 +36,10 @@
|
|
|
27
36
|
readonly policy: SurfacePolicy;
|
|
28
37
|
readonly catalog?: SurfaceRenderCatalog;
|
|
29
38
|
readonly dataProvider?: SurfaceDataProvider;
|
|
39
|
+
/** 可信宿主的用户/租户/授权会话标识;禁止使用凭证或模型提供的值。 */
|
|
40
|
+
readonly scopeKey?: string;
|
|
41
|
+
/** 与独立增量加载接口保持兼容;任一作用域标识变化都会清空状态。 */
|
|
42
|
+
readonly dataScopeKey?: string | number;
|
|
30
43
|
readonly locale?: string;
|
|
31
44
|
readonly messages?: Partial<SurfaceMessages>;
|
|
32
45
|
readonly class?: string;
|
|
@@ -34,14 +47,8 @@
|
|
|
34
47
|
}
|
|
35
48
|
|
|
36
49
|
let {
|
|
37
|
-
spec,
|
|
38
|
-
|
|
39
|
-
catalog = defaultSurfaceCatalog,
|
|
40
|
-
dataProvider,
|
|
41
|
-
locale,
|
|
42
|
-
messages,
|
|
43
|
-
class: className = '',
|
|
44
|
-
onError,
|
|
50
|
+
spec, policy, catalog = defaultSurfaceCatalog, dataProvider, scopeKey = '', dataScopeKey,
|
|
51
|
+
locale, messages, class: className = '', onError,
|
|
45
52
|
}: SurfaceRendererProps = $props();
|
|
46
53
|
|
|
47
54
|
const adminContext = captureAdminContext();
|
|
@@ -50,182 +57,133 @@
|
|
|
50
57
|
const activeMessages = $derived(resolveSurfaceMessages(activeLocale, messages));
|
|
51
58
|
const validation = $derived(validateSurfaceSpec(spec, catalog, policy));
|
|
52
59
|
const widgetRegistrations = $derived(new Map(catalog.widgets.map((widget) => [widget.type, widget])));
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
let
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
async function authorize(resource: string, action: 'list' | 'show') {
|
|
72
|
-
return canAccessAsync(resource, action);
|
|
73
|
-
}
|
|
60
|
+
const sourceStates = new SvelteMap<string, SurfaceSourceDataState>();
|
|
61
|
+
let destroyed = false;
|
|
62
|
+
let previousScope: readonly unknown[] = [];
|
|
63
|
+
let widgetScopeRevision = $state(0);
|
|
64
|
+
const sessionIdentity = $derived([
|
|
65
|
+
validation.ok ? validation.value.surfaceId : undefined,
|
|
66
|
+
catalog.version, scopeKey, dataScopeKey, getLogoutVersion(),
|
|
67
|
+
adminContext.tenantCacheKey?.__svadminTenant,
|
|
68
|
+
adminContext.authProvider, adminContext.accessControlProvider,
|
|
69
|
+
]);
|
|
70
|
+
// 复用 PR #428 的请求所有权与会话隔离实现,不维护第二套缓存。
|
|
71
|
+
const sourceCache = createSurfaceSourceCache({
|
|
72
|
+
set: (id, state) => { sourceStates.set(id, state); },
|
|
73
|
+
remove: (id) => { sourceStates.delete(id); },
|
|
74
|
+
onError: (state) => { onError?.({ type: 'data', error: state.error }); },
|
|
75
|
+
});
|
|
74
76
|
|
|
75
77
|
function providerError(sourceId: string, failure: unknown): SurfaceSourceDataState {
|
|
76
78
|
const message = failure instanceof Error ? failure.message : activeMessages.providerUnavailable;
|
|
77
|
-
return {
|
|
78
|
-
status: 'error',
|
|
79
|
-
sourceId,
|
|
80
|
-
error: { code: 'provider_failed', sourceId, message },
|
|
81
|
-
};
|
|
79
|
+
return { status: 'error', sourceId, error: { code: 'provider_failed', sourceId, message } };
|
|
82
80
|
}
|
|
83
81
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const resourcePolicy = Object.hasOwn(policy.resources, source.resource)
|
|
91
|
-
? policy.resources[source.resource]
|
|
92
|
-
: undefined;
|
|
82
|
+
const sourceRequests: SurfaceSourceRequest[] = $derived.by(() => {
|
|
83
|
+
if (!validation.ok) return [];
|
|
84
|
+
const accessControl = adminContext.accessControlProvider;
|
|
85
|
+
const scope = sessionIdentity;
|
|
86
|
+
return validation.value.dataSources.map((source): SurfaceSourceRequest => {
|
|
87
|
+
const resourcePolicy = Object.hasOwn(policy.resources, source.resource) ? policy.resources[source.resource] : undefined;
|
|
93
88
|
if (!resourcePolicy) throw new Error(`Resource "${source.resource}" is not allowed`);
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
89
|
+
const snapshot = snapshotSurfaceSource(source, resourcePolicy);
|
|
90
|
+
let provider: SurfaceDataProvider | undefined;
|
|
91
|
+
let providerFailure: unknown;
|
|
92
|
+
try {
|
|
93
|
+
provider = dataProvider ?? adminContext.getDataProviderForResource(source.resource);
|
|
94
|
+
} catch (failure) { providerFailure = failure; }
|
|
95
|
+
const identity = [snapshot.key, provider, provider?.getList, provider?.getOne, ...scope];
|
|
96
|
+
const isCurrent = () => !destroyed && sourceRequests.some((request) =>
|
|
97
|
+
request.id === snapshot.source.id && sameSourceIdentity(request.identity, identity));
|
|
98
|
+
return {
|
|
99
|
+
id: snapshot.source.id, identity, isCurrent,
|
|
100
|
+
async load(isRequestCurrent) {
|
|
101
|
+
if (!provider) return providerError(snapshot.source.id, providerFailure);
|
|
102
|
+
try {
|
|
103
|
+
return await loadSurfaceSource({
|
|
104
|
+
source: snapshot.source, resourcePolicy: snapshot.resourcePolicy, provider,
|
|
105
|
+
async authorize(resource, action) {
|
|
106
|
+
if (!isRequestCurrent()) return { can: false };
|
|
107
|
+
// 授权检查与缓存身份必须使用同一个所属上下文,不能回退到无关全局授权。
|
|
108
|
+
const meta = adminContext.getProviderMeta(resource);
|
|
109
|
+
const decision = accessControl
|
|
110
|
+
? await accessControl.can({ resource, action, ...(meta === undefined ? {} : { meta }) })
|
|
111
|
+
: { can: true };
|
|
112
|
+
return isRequestCurrent() ? decision : { can: false };
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
} catch (failure) { return providerError(snapshot.source.id, failure); }
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
});
|
|
117
120
|
|
|
118
121
|
export async function refresh(sourceId?: string): Promise<void> {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const sources = sourceId === undefined
|
|
122
|
-
? activeSpec.dataSources
|
|
123
|
-
: activeSpec.dataSources.filter((source) => source.id === sourceId);
|
|
124
|
-
await loadSources(sources);
|
|
122
|
+
if (!validation.ok) { sourceCache.clear(); return; }
|
|
123
|
+
await untrack(() => sourceCache.reconcile(sourceRequests, sourceId ?? true));
|
|
125
124
|
}
|
|
126
125
|
|
|
127
|
-
$effect(() => {
|
|
126
|
+
$effect.pre(() => {
|
|
128
127
|
const validatedSpec = validation;
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (!validatedSpec.ok) {
|
|
132
|
-
onError?.({ type: 'validation', issues: validatedSpec.issues });
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
|
|
128
|
+
const requests = sourceRequests;
|
|
129
|
+
const scope = sessionIdentity;
|
|
136
130
|
untrack(() => {
|
|
137
|
-
|
|
131
|
+
if (!sameSourceIdentity(previousScope, scope)) {
|
|
132
|
+
previousScope = [...scope];
|
|
133
|
+
widgetScopeRevision += 1;
|
|
134
|
+
}
|
|
135
|
+
if (!validatedSpec.ok) {
|
|
136
|
+
sourceCache.clear();
|
|
137
|
+
onError?.({ type: 'validation', issues: validatedSpec.issues });
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
void sourceCache.reconcile(requests);
|
|
138
141
|
});
|
|
139
142
|
});
|
|
143
|
+
onDestroy(() => { destroyed = true; sourceCache.dispose(); });
|
|
140
144
|
</script>
|
|
141
145
|
|
|
142
146
|
{#if validation.ok}
|
|
143
|
-
<section
|
|
144
|
-
class="surface {
|
|
145
|
-
aria-labelledby="surface-{validation.value.surfaceId}-title"
|
|
146
|
-
data-surface-id={validation.value.surfaceId}
|
|
147
|
-
>
|
|
148
|
-
<header class="surface-header">
|
|
149
|
-
<h2 id="surface-{validation.value.surfaceId}-title">{validation.value.title}</h2>
|
|
150
|
-
</header>
|
|
147
|
+
<section class="surface {className}" aria-labelledby="surface-{validation.value.surfaceId}-title" data-surface-id={validation.value.surfaceId}>
|
|
148
|
+
<header class="surface-header"><h2 id="surface-{validation.value.surfaceId}-title">{validation.value.title}</h2></header>
|
|
151
149
|
<div class="surface-grid surface-gap-{validation.value.layout.gap ?? 'md'}">
|
|
152
|
-
{#
|
|
153
|
-
{
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
data-testid="surface-widget-{widget.id}"
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
/>
|
|
167
|
-
{/if}
|
|
168
|
-
</div>
|
|
169
|
-
{/each}
|
|
150
|
+
{#key widgetScopeRevision}
|
|
151
|
+
{#each validation.value.widgets as widget (widget.id)}
|
|
152
|
+
{@const registration = widgetRegistrations.get(widget.type)}
|
|
153
|
+
{@const WidgetComponent = registration?.component}
|
|
154
|
+
{@const semantic = registration?.presentation === 'surface-appearance/v1'}
|
|
155
|
+
<div class="surface-widget surface-span-{widget.placement?.columnSpan ?? 12} {semantic ? appearanceClasses(widget.props) : ''}" data-testid="surface-widget-{widget.id}">
|
|
156
|
+
{#if WidgetComponent}
|
|
157
|
+
<WidgetComponent widgetId={widget.id} props={semantic ? withoutSurfaceAppearance(widget.props) : widget.props}
|
|
158
|
+
data={resolveSurfaceSourceData(widget, widget.binding ? sourceStates.get(widget.binding.sourceId) : undefined)}
|
|
159
|
+
locale={activeLocale} messages={activeMessages} />
|
|
160
|
+
{/if}
|
|
161
|
+
</div>
|
|
162
|
+
{/each}
|
|
163
|
+
{/key}
|
|
170
164
|
</div>
|
|
171
165
|
</section>
|
|
172
166
|
{:else}
|
|
173
167
|
<section class="surface-error {className}" role="alert" data-surface-error>
|
|
174
168
|
<h2>{activeMessages.renderErrorTitle}</h2>
|
|
175
|
-
<ul>
|
|
176
|
-
{
|
|
177
|
-
|
|
178
|
-
{/each}
|
|
179
|
-
</ul>
|
|
169
|
+
<ul>{#each validation.issues as issue, issueIndex (`${issue.code}:${issue.path}:${issueIndex}`)}
|
|
170
|
+
<li><code>{issue.code}</code>: {issue.message}</li>
|
|
171
|
+
{/each}</ul>
|
|
180
172
|
</section>
|
|
181
173
|
{/if}
|
|
182
174
|
|
|
183
175
|
<style>
|
|
184
|
-
.surface,
|
|
185
|
-
.surface-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
.surface-header {
|
|
191
|
-
margin-bottom: 1rem;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
.surface-header h2,
|
|
195
|
-
.surface-error h2 {
|
|
196
|
-
margin: 0;
|
|
197
|
-
color: var(--foreground);
|
|
198
|
-
font-size: clamp(1.25rem, 2vw, 1.75rem);
|
|
199
|
-
line-height: 1.2;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
.surface-grid {
|
|
203
|
-
display: grid;
|
|
204
|
-
grid-template-columns: repeat(12, minmax(0, 1fr));
|
|
205
|
-
}
|
|
206
|
-
|
|
176
|
+
.surface, .surface-error { width: 100%; min-width: 0; }
|
|
177
|
+
.surface-header { margin-bottom: 1rem; }
|
|
178
|
+
.surface-header h2, .surface-error h2 { margin: 0; color: var(--foreground); font-size: clamp(1.25rem, 2vw, 1.75rem); line-height: 1.2; }
|
|
179
|
+
.surface-grid { display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); }
|
|
207
180
|
.surface-gap-sm { gap: 0.5rem; }
|
|
208
181
|
.surface-gap-md { gap: 1rem; }
|
|
209
182
|
.surface-gap-lg { gap: 1.5rem; }
|
|
210
|
-
|
|
211
|
-
.surface-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
.surface-error {
|
|
217
|
-
padding: 1rem;
|
|
218
|
-
border: 1px solid var(--destructive);
|
|
219
|
-
border-radius: 0.75rem;
|
|
220
|
-
background: var(--card);
|
|
221
|
-
color: var(--destructive);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
.surface-error ul {
|
|
225
|
-
margin: 0.75rem 0 0;
|
|
226
|
-
padding-left: 1.25rem;
|
|
227
|
-
}
|
|
228
|
-
|
|
183
|
+
.surface-widget { grid-column: 1 / -1; min-width: 0; }
|
|
184
|
+
.surface-appearance { border-block: 1px solid var(--border); border-inline-end: 1px solid var(--border); border-inline-start-style: solid; border-radius: 0.75rem; background: var(--card); }
|
|
185
|
+
.surface-error { padding: 1rem; border: 1px solid var(--destructive); border-radius: 0.75rem; background: var(--card); color: var(--destructive); }
|
|
186
|
+
.surface-error ul { margin: 0.75rem 0 0; padding-left: 1.25rem; }
|
|
229
187
|
@media (min-width: 48rem) {
|
|
230
188
|
.surface-span-1 { grid-column: span 1; }
|
|
231
189
|
.surface-span-2 { grid-column: span 2; }
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { SurfaceRenderCatalog } from '../catalog.js';
|
|
2
2
|
import type { SurfaceMessages } from '../localization.js';
|
|
3
3
|
import type { SurfaceDataError, SurfaceDataProvider, SurfacePolicy, SurfaceValidationIssue } from '../types.js';
|
|
4
|
+
import '../styles.css';
|
|
4
5
|
export type SurfaceRendererError = {
|
|
5
6
|
readonly type: 'validation';
|
|
6
7
|
readonly issues: readonly SurfaceValidationIssue[];
|
|
@@ -13,6 +14,10 @@ export interface SurfaceRendererProps {
|
|
|
13
14
|
readonly policy: SurfacePolicy;
|
|
14
15
|
readonly catalog?: SurfaceRenderCatalog;
|
|
15
16
|
readonly dataProvider?: SurfaceDataProvider;
|
|
17
|
+
/** 可信宿主的用户/租户/授权会话标识;禁止使用凭证或模型提供的值。 */
|
|
18
|
+
readonly scopeKey?: string;
|
|
19
|
+
/** 与独立增量加载接口保持兼容;任一作用域标识变化都会清空状态。 */
|
|
20
|
+
readonly dataScopeKey?: string | number;
|
|
16
21
|
readonly locale?: string;
|
|
17
22
|
readonly messages?: Partial<SurfaceMessages>;
|
|
18
23
|
readonly class?: string;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { setContext, type Snippet } from 'svelte';
|
|
3
|
+
import { SURFACE_WORKFLOW_CONTEXT, type SurfaceWorkflowHost } from '../workflows/context.js';
|
|
4
|
+
import type { SurfaceActionDescriptor, SurfaceActionProposal } from '../workflows/types.js';
|
|
5
|
+
import type { SurfaceWorkflowTransport } from '../workflows/client.js';
|
|
6
|
+
let { actions, transport, scopeKey, surfaceId, revision, enabled = false, onCommitted, children }: {
|
|
7
|
+
actions: readonly SurfaceActionDescriptor[]; transport: SurfaceWorkflowTransport;
|
|
8
|
+
scopeKey: string; surfaceId: string; revision: number;
|
|
9
|
+
/** Opt-in only after a complete validated surface is explicitly accepted. */
|
|
10
|
+
enabled?: boolean; onCommitted?: (proposal: SurfaceActionProposal) => void; children: Snippet;
|
|
11
|
+
} = $props();
|
|
12
|
+
const host: SurfaceWorkflowHost = {
|
|
13
|
+
getScope: () => ({ transport, scopeKey, surfaceId, revision, enabled }),
|
|
14
|
+
getAction: (id) => actions.find((action) => action.id === id),
|
|
15
|
+
committed: (proposal) => onCommitted?.(proposal),
|
|
16
|
+
};
|
|
17
|
+
setContext(SURFACE_WORKFLOW_CONTEXT, host);
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
{#key scopeKey}
|
|
21
|
+
{@render children()}
|
|
22
|
+
{/key}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type Snippet } from 'svelte';
|
|
2
|
+
import type { SurfaceActionDescriptor, SurfaceActionProposal } from '../workflows/types.js';
|
|
3
|
+
import type { SurfaceWorkflowTransport } from '../workflows/client.js';
|
|
4
|
+
type $$ComponentProps = {
|
|
5
|
+
actions: readonly SurfaceActionDescriptor[];
|
|
6
|
+
transport: SurfaceWorkflowTransport;
|
|
7
|
+
scopeKey: string;
|
|
8
|
+
surfaceId: string;
|
|
9
|
+
revision: number;
|
|
10
|
+
/** Opt-in only after a complete validated surface is explicitly accepted. */
|
|
11
|
+
enabled?: boolean;
|
|
12
|
+
onCommitted?: (proposal: SurfaceActionProposal) => void;
|
|
13
|
+
children: Snippet;
|
|
14
|
+
};
|
|
15
|
+
declare const SurfaceWorkflowProvider: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
16
|
+
type SurfaceWorkflowProvider = ReturnType<typeof SurfaceWorkflowProvider>;
|
|
17
|
+
export default SurfaceWorkflowProvider;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getContext } from 'svelte';
|
|
3
|
+
import SvarDataGrid from '@svadmin/ui/components/SvarDataGrid.svelte';
|
|
4
|
+
import type { SurfaceWidgetRendererProps } from '../catalog.js';
|
|
5
|
+
import { SVAR_SURFACE_CONTEXT, type SvarSurfaceContext } from '../svar-context.js';
|
|
6
|
+
import { decodeSvarGridProps } from '../svar-schema.js';
|
|
7
|
+
import { asRecordArray } from '../widget-data.js';
|
|
8
|
+
import { resolveSurfaceMessages } from '../localization.js';
|
|
9
|
+
|
|
10
|
+
let { widgetId, props, data, locale = 'en-US', messages }: SurfaceWidgetRendererProps = $props();
|
|
11
|
+
const host = getContext<SvarSurfaceContext | undefined>(SVAR_SURFACE_CONTEXT);
|
|
12
|
+
const labels = $derived(resolveSurfaceMessages(locale, messages));
|
|
13
|
+
const config = $derived(decodeSvarGridProps(props));
|
|
14
|
+
const records = $derived(data.status === 'ready' ? asRecordArray(data.value) : undefined);
|
|
15
|
+
const columns = $derived(config.columns.map(column => ({
|
|
16
|
+
key: column.field, label: column.label,
|
|
17
|
+
...(column.width === undefined ? {} : { width: column.width }),
|
|
18
|
+
...(column.format === undefined ? {} : { format: column.format }),
|
|
19
|
+
})));
|
|
20
|
+
const Grid = $derived(host?.Grid);
|
|
21
|
+
const Theme = $derived(host?.Theme);
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<section aria-labelledby="svar-surface-{widgetId}" class="surface-data-grid">
|
|
25
|
+
<h3 id="svar-surface-{widgetId}">{config.title}</h3>
|
|
26
|
+
{#if !Grid || !Theme}
|
|
27
|
+
<p role="alert">{locale.startsWith('zh') ? '宿主尚未配置 SVAR 引擎' : 'The host has not configured the SVAR engine'}</p>
|
|
28
|
+
{:else if data.status === 'error'}
|
|
29
|
+
<p role="alert">{data.error.message}</p>
|
|
30
|
+
{:else if data.status === 'unbound' || (records && !records.ok)}
|
|
31
|
+
<p role="alert">{labels.tableInvalidData}</p>
|
|
32
|
+
{:else}
|
|
33
|
+
<SvarDataGrid {Grid} {Theme} {columns} items={records?.ok ? records.value : []}
|
|
34
|
+
primaryKey={config.rowKey ?? 'id'} height={config.height ?? 360} density={config.density ?? 'comfortable'}
|
|
35
|
+
freezeLeft={config.freezeLeft ?? 0} freezeRight={config.freezeRight ?? 0}
|
|
36
|
+
loading={data.status === 'loading'} label={config.title} {locale}
|
|
37
|
+
scopeKey={`${host?.scopeKey ?? 0}:${widgetId}:${'sourceId' in data ? data.sourceId : ''}`}
|
|
38
|
+
loadingLabel={labels.tableLoading} emptyLabel={config.emptyLabel ?? labels.tableNoRecords}
|
|
39
|
+
selectable={false} />
|
|
40
|
+
{/if}
|
|
41
|
+
</section>
|
|
42
|
+
|
|
43
|
+
<style>
|
|
44
|
+
.surface-data-grid { min-width: 0; padding: 1rem; background: var(--card, Canvas); color: var(--foreground, CanvasText); border: 1px solid var(--border, GrayText); border-radius: var(--radius, 8px); }
|
|
45
|
+
h3 { margin: 0 0 0.75rem; font-size: 1rem; }
|
|
46
|
+
[role='alert'] { color: var(--destructive, CanvasText); }
|
|
47
|
+
</style>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { setContext, type Snippet } from 'svelte';
|
|
3
|
+
import { SVAR_SURFACE_CONTEXT, type SvarSurfaceContext } from '../svar-context.js';
|
|
4
|
+
let { Grid, Theme, scopeKey = 0, children }: {
|
|
5
|
+
Grid: SvarSurfaceContext['Grid']; Theme: SvarSurfaceContext['Theme']; scopeKey?: string | number; children: Snippet;
|
|
6
|
+
} = $props();
|
|
7
|
+
setContext<SvarSurfaceContext>(SVAR_SURFACE_CONTEXT, {
|
|
8
|
+
get Grid() { return Grid; }, get Theme() { return Theme; }, get scopeKey() { return scopeKey; },
|
|
9
|
+
});
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
{@render children()}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type Snippet } from 'svelte';
|
|
2
|
+
import { type SvarSurfaceContext } from '../svar-context.js';
|
|
3
|
+
type $$ComponentProps = {
|
|
4
|
+
Grid: SvarSurfaceContext['Grid'];
|
|
5
|
+
Theme: SvarSurfaceContext['Theme'];
|
|
6
|
+
scopeKey?: string | number;
|
|
7
|
+
children: Snippet;
|
|
8
|
+
};
|
|
9
|
+
declare const SvarSurfaceProvider: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
10
|
+
type SvarSurfaceProvider = ReturnType<typeof SvarSurfaceProvider>;
|
|
11
|
+
export default SvarSurfaceProvider;
|
package/dist/edits.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { SurfaceAgentMessage } from './agent.js';
|
|
2
|
+
import type { JsonObject, SurfaceCatalog, SurfaceDataSource, SurfaceGridLayout, SurfacePolicy, SurfaceSpec, SurfaceValidationIssue, SurfaceWidget } from './types.js';
|
|
3
|
+
export declare const SURFACE_EDIT_SCHEMA_VERSION: "surface-edit/v1";
|
|
4
|
+
export declare const SURFACE_EDIT_LIMITS: {
|
|
5
|
+
readonly maxOperations: 64;
|
|
6
|
+
};
|
|
7
|
+
export type SurfaceEditOperation = {
|
|
8
|
+
readonly op: 'set-title';
|
|
9
|
+
readonly value: string;
|
|
10
|
+
} | {
|
|
11
|
+
readonly op: 'set-layout';
|
|
12
|
+
readonly value: SurfaceGridLayout;
|
|
13
|
+
} | {
|
|
14
|
+
readonly op: 'upsert-widget';
|
|
15
|
+
readonly widget: SurfaceWidget;
|
|
16
|
+
} | {
|
|
17
|
+
readonly op: 'remove-widget';
|
|
18
|
+
readonly id: string;
|
|
19
|
+
} | {
|
|
20
|
+
readonly op: 'upsert-source';
|
|
21
|
+
readonly source: SurfaceDataSource;
|
|
22
|
+
} | {
|
|
23
|
+
readonly op: 'remove-source';
|
|
24
|
+
readonly id: string;
|
|
25
|
+
} | {
|
|
26
|
+
readonly op: 'reorder-widgets';
|
|
27
|
+
readonly ids: readonly string[];
|
|
28
|
+
};
|
|
29
|
+
export interface SurfaceEditProposal {
|
|
30
|
+
readonly schemaVersion: typeof SURFACE_EDIT_SCHEMA_VERSION;
|
|
31
|
+
readonly catalogVersion: string;
|
|
32
|
+
readonly surfaceId: string;
|
|
33
|
+
readonly baseRevision: number;
|
|
34
|
+
readonly operations: readonly SurfaceEditOperation[];
|
|
35
|
+
}
|
|
36
|
+
export interface SurfaceRevision {
|
|
37
|
+
readonly revision: number;
|
|
38
|
+
readonly spec: SurfaceSpec;
|
|
39
|
+
}
|
|
40
|
+
export interface SurfaceEditIssue extends Omit<SurfaceValidationIssue, 'code'> {
|
|
41
|
+
readonly code: SurfaceValidationIssue['code'] | 'revision_conflict' | 'invalid_edit';
|
|
42
|
+
}
|
|
43
|
+
export type SurfaceRevisionResult = {
|
|
44
|
+
readonly ok: true;
|
|
45
|
+
readonly value: SurfaceRevision;
|
|
46
|
+
} | {
|
|
47
|
+
readonly ok: false;
|
|
48
|
+
readonly issues: readonly SurfaceEditIssue[];
|
|
49
|
+
};
|
|
50
|
+
export declare function createSurfaceRevision(spec: unknown, catalog: SurfaceCatalog, policy: SurfacePolicy, revision?: number): SurfaceRevisionResult;
|
|
51
|
+
/**
|
|
52
|
+
* 只生成下一版候选快照,不保存、不渲染、不访问 DataProvider。
|
|
53
|
+
* 服务端保存时仍必须对 revision 做原子 compare-and-swap,并重新授权。
|
|
54
|
+
*/
|
|
55
|
+
export declare function applySurfaceEditProposal(current: SurfaceRevision, input: unknown, catalog: SurfaceCatalog, policy: SurfacePolicy): SurfaceRevisionResult;
|
|
56
|
+
export declare function createSurfaceEditSchema(): JsonObject;
|
|
57
|
+
export declare function buildSurfaceEditMessages(request: string, current: SurfaceRevision, catalog: SurfaceCatalog, policy: SurfacePolicy): readonly SurfaceAgentMessage[];
|