@xemahq/ui-kernel 0.11.1 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/errors/http-error-status.d.ts +17 -0
- package/dist/errors/http-error-status.d.ts.map +1 -0
- package/dist/errors/http-error-status.js +59 -0
- package/dist/errors/http-error-status.js.map +1 -0
- package/dist/errors/index.d.ts +2 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +2 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/biome-host/errors.d.ts.map +1 -1
- package/dist/lib/biome-host/errors.js +1 -19
- package/dist/lib/biome-host/errors.js.map +1 -1
- package/dist/offline/index.d.ts.map +1 -1
- package/dist/offline/index.js.map +1 -1
- package/dist/offline/online-controller.d.ts.map +1 -1
- package/dist/offline/online-controller.js +6 -1
- package/dist/offline/online-controller.js.map +1 -1
- package/dist/offline/outbox.d.ts.map +1 -1
- package/dist/offline/outbox.js +2 -24
- package/dist/offline/outbox.js.map +1 -1
- package/package.json +1 -2
- package/src/index.ts +0 -17
- package/src/lib/biome-host/agent-validation.ts +0 -239
- package/src/lib/biome-host/biome-builders.ts +0 -112
- package/src/lib/biome-host/biome-mode.ts +0 -6
- package/src/lib/biome-host/biome-navigation.ts +0 -37
- package/src/lib/biome-host/biome-registry.ts +0 -299
- package/src/lib/biome-host/biome-scope-hooks.ts +0 -41
- package/src/lib/biome-host/biome-scope.tsx +0 -52
- package/src/lib/biome-host/biome-scoped-query.ts +0 -134
- package/src/lib/biome-host/create-biome-orval-config.ts +0 -82
- package/src/lib/biome-host/define-web-biome.spec.ts +0 -221
- package/src/lib/biome-host/define-web-biome.ts +0 -222
- package/src/lib/biome-host/errors.ts +0 -220
- package/src/lib/biome-host/frontend-biome.ts +0 -256
- package/src/lib/biome-host/host-bridge.ts +0 -281
- package/src/lib/biome-host/host-sources.ts +0 -54
- package/src/lib/biome-host/i18n-hooks.ts +0 -51
- package/src/lib/biome-host/i18n-port.test.ts +0 -50
- package/src/lib/biome-host/i18n-port.ts +0 -96
- package/src/lib/biome-host/index.ts +0 -35
- package/src/lib/biome-host/realtime-hooks.ts +0 -75
- package/src/lib/biome-host/realtime-port.ts +0 -115
- package/src/lib/biome-host/session-contributions.ts +0 -296
- package/src/lib/biome-host/use-mutation-with-error-toast.ts +0 -90
- package/src/lib/biome-host/use-page-state.ts +0 -248
- package/src/lib/capabilities/capability-context.ts +0 -25
- package/src/lib/capabilities/capability-provider.tsx +0 -73
- package/src/lib/capabilities/index.ts +0 -17
- package/src/lib/capabilities/types.ts +0 -69
- package/src/lib/capabilities/use-capability.ts +0 -79
- package/src/lib/contribution-registry/contribution-registry.spec.ts +0 -141
- package/src/lib/contribution-registry/contribution-registry.ts +0 -204
- package/src/lib/contribution-registry/index.ts +0 -9
- package/src/lib/contribution-registry/renderable-registry.ts +0 -142
- package/src/lib/system-bus/capability-invoke.ts +0 -92
- package/src/lib/system-bus/deeplink.ts +0 -210
- package/src/lib/system-bus/enums.ts +0 -86
- package/src/lib/system-bus/host-ports.ts +0 -93
- package/src/lib/system-bus/index.ts +0 -16
- package/src/lib/system-bus/intent-registry.ts +0 -109
- package/src/lib/system-bus/intents.ts +0 -113
- package/src/lib/system-bus/palette.ts +0 -78
- package/src/lib/system-bus/system-bus-builder.ts +0 -159
- package/src/lib/system-bus/system-bus.ts +0 -37
- package/src/lib/system-bus/windows.ts +0 -51
- package/src/offline/id.ts +0 -16
- package/src/offline/index.ts +0 -54
- package/src/offline/mutation.ts +0 -75
- package/src/offline/offline-status-badge.tsx +0 -51
- package/src/offline/online-controller.ts +0 -105
- package/src/offline/outbox.test.ts +0 -172
- package/src/offline/outbox.ts +0 -348
- package/src/offline/react-hooks.ts +0 -40
- package/src/offline/react.tsx +0 -32
- package/src/offline/storage.ts +0 -112
- package/src/offline/types.ts +0 -77
- package/src/org-db/components/DbResultTable.tsx +0 -176
- package/src/org-db/index.ts +0 -1
- package/src/pdf/index.ts +0 -11
- package/src/registry/index.ts +0 -9
- package/src/registry/lib/agent-validation-host.ts +0 -36
- package/src/registry/lib/biome-slot.tsx +0 -50
- package/src/registry/lib/biomes-enabled-context.ts +0 -20
- package/src/registry/lib/extension-points.ts +0 -134
- package/src/registry/lib/primitives/SessionEventCard.tsx +0 -164
- package/src/registry/lib/primitives/SessionEventTimeline.tsx +0 -95
- package/src/registry/lib/primitives/SessionMutationBar.tsx +0 -76
- package/src/registry/lib/primitives/SessionTabbedDrawer.tsx +0 -165
- package/src/registry/lib/primitives/index.ts +0 -18
- package/src/registry/lib/session-context-builder.ts +0 -84
- package/src/registry/lib/session-context-provider.tsx +0 -33
- package/src/registry/lib/session-context.ts +0 -21
- package/src/registry/lib/session-selectors.ts +0 -272
- package/src/session/comments/CommentRail.tsx +0 -164
- package/src/session/index.ts +0 -3
- package/src/session/lib/cn.ts +0 -11
- package/src/session/shell/SessionWorkspaceShell.tsx +0 -147
- package/src/session-kit/approvals/ApprovalButton.tsx +0 -89
- package/src/session-kit/approvals/ApprovalCard.tsx +0 -341
- package/src/session-kit/approvals/ApprovalsCenter.tsx +0 -336
- package/src/session-kit/approvals/CapabilityApprovalStickyBar.tsx +0 -118
- package/src/session-kit/approvals/Obligation.tsx +0 -111
- package/src/session-kit/approvals/ScopedBody.tsx +0 -392
- package/src/session-kit/approvals/approval-icons.ts +0 -31
- package/src/session-kit/approvals/approval-model.ts +0 -204
- package/src/session-kit/approvals/index.ts +0 -22
- package/src/session-kit/approvals/obligation-display.ts +0 -100
- package/src/session-kit/approvals/risk-accent.ts +0 -47
- package/src/session-kit/approvals/scope-icons.ts +0 -19
- package/src/session-kit/combobox/Combobox.tsx +0 -358
- package/src/session-kit/combobox/use-click-outside.ts +0 -21
- package/src/session-kit/display/ContextHeader.tsx +0 -148
- package/src/session-kit/display/FileDiffCard.tsx +0 -140
- package/src/session-kit/display/MD.tsx +0 -162
- package/src/session-kit/display/MessageTurn.tsx +0 -157
- package/src/session-kit/display/ThinkingPanel.tsx +0 -81
- package/src/session-kit/display/TodoChecklist.tsx +0 -120
- package/src/session-kit/display/TokenMeter.tsx +0 -89
- package/src/session-kit/display/ToolStrip.tsx +0 -278
- package/src/session-kit/display/TypingDots.tsx +0 -24
- package/src/session-kit/index.ts +0 -44
- package/src/session-kit/lib/enums.ts +0 -66
- package/src/session-kit/lib/portal-accent.ts +0 -30
- package/src/session-kit/lib/status-dot.ts +0 -68
- package/src/session-kit/primitives/Avatar.tsx +0 -44
- package/src/session-kit/primitives/PortalGlyph.tsx +0 -51
- package/src/session-kit/primitives/RiskPill.tsx +0 -95
- package/src/session-kit/primitives/ScopeDot.tsx +0 -47
- package/src/session-kit/primitives/Segmented.tsx +0 -71
- package/src/ui/branding/app-branding-context.ts +0 -20
- package/src/ui/branding/app-branding-provider.tsx +0 -123
- package/src/ui/branding/sanitize.ts +0 -89
- package/src/ui/breakpoints.ts +0 -14
- package/src/ui/capture/document-preview.tsx +0 -166
- package/src/ui/capture/field-spec-to-zod.test.ts +0 -81
- package/src/ui/capture/field-spec-to-zod.ts +0 -174
- package/src/ui/capture/field-spec-validate.ts +0 -236
- package/src/ui/capture/field-spec.test.ts +0 -49
- package/src/ui/capture/field-spec.ts +0 -274
- package/src/ui/capture/index.ts +0 -51
- package/src/ui/capture/schema-field-input.tsx +0 -620
- package/src/ui/capture/schema-form.tsx +0 -149
- package/src/ui/capture/submit-values.test.ts +0 -70
- package/src/ui/capture/submit-values.ts +0 -63
- package/src/ui/capture/use-prefill.ts +0 -186
- package/src/ui/capture/use-schema-validation.ts +0 -145
- package/src/ui/capture/use-wizard.ts +0 -180
- package/src/ui/capture/value-format.ts +0 -32
- package/src/ui/capture/wizard.tsx +0 -105
- package/src/ui/charts/bar-chart.tsx +0 -139
- package/src/ui/charts/chart-frame.tsx +0 -125
- package/src/ui/charts/chart-types.ts +0 -52
- package/src/ui/charts/chart-utils.ts +0 -190
- package/src/ui/charts/charts.test.tsx +0 -204
- package/src/ui/charts/donut-chart.tsx +0 -129
- package/src/ui/charts/index.ts +0 -27
- package/src/ui/charts/line-chart.tsx +0 -202
- package/src/ui/charts/sparkline.tsx +0 -109
- package/src/ui/chrome/AsyncBoundary.tsx +0 -74
- package/src/ui/chrome/EmptyState.tsx +0 -257
- package/src/ui/chrome/ErrorCard.tsx +0 -63
- package/src/ui/chrome/LoadingState.tsx +0 -61
- package/src/ui/chrome/PageHeader.tsx +0 -137
- package/src/ui/chrome/RecordCardList.tsx +0 -98
- package/src/ui/chrome/StateCard.tsx +0 -150
- package/src/ui/chrome/confirm-delete-dialog.tsx +0 -59
- package/src/ui/chrome/page-shell.spec.tsx +0 -117
- package/src/ui/chrome/page-shell.tsx +0 -221
- package/src/ui/chrome/scope-badge.tsx +0 -40
- package/src/ui/chrome/status-badge.tsx +0 -79
- package/src/ui/cn.ts +0 -32
- package/src/ui/design-tokens.ts +0 -372
- package/src/ui/hooks/use-debounced-value.ts +0 -16
- package/src/ui/hooks/use-is-mobile.ts +0 -28
- package/src/ui/index.ts +0 -110
- package/src/ui/pdf/pdf-preview.tsx +0 -122
- package/src/ui/primitives/alert-dialog.tsx +0 -104
- package/src/ui/primitives/async-combobox.tsx +0 -182
- package/src/ui/primitives/badge-variants.ts +0 -21
- package/src/ui/primitives/badge.tsx +0 -25
- package/src/ui/primitives/button-variants.ts +0 -31
- package/src/ui/primitives/button.tsx +0 -29
- package/src/ui/primitives/card.tsx +0 -43
- package/src/ui/primitives/checkbox.tsx +0 -26
- package/src/ui/primitives/collapsible.tsx +0 -9
- package/src/ui/primitives/dialog.tsx +0 -103
- package/src/ui/primitives/dropdown-menu.tsx +0 -179
- package/src/ui/primitives/form-stepper.tsx +0 -94
- package/src/ui/primitives/form-stepper.types.ts +0 -15
- package/src/ui/primitives/input.tsx +0 -22
- package/src/ui/primitives/label.tsx +0 -17
- package/src/ui/primitives/overflow-tabs.tsx +0 -331
- package/src/ui/primitives/popover.tsx +0 -33
- package/src/ui/primitives/preview-device-frame.tsx +0 -213
- package/src/ui/primitives/radio-group.tsx +0 -36
- package/src/ui/primitives/resizable.tsx +0 -67
- package/src/ui/primitives/scroll-area.tsx +0 -38
- package/src/ui/primitives/select.tsx +0 -143
- package/src/ui/primitives/separator.tsx +0 -20
- package/src/ui/primitives/sheet.tsx +0 -107
- package/src/ui/primitives/skeleton.tsx +0 -99
- package/src/ui/primitives/switch.tsx +0 -27
- package/src/ui/primitives/table.tsx +0 -72
- package/src/ui/primitives/tabs.tsx +0 -53
- package/src/ui/primitives/tag-multi-select.tsx +0 -261
- package/src/ui/primitives/textarea.tsx +0 -21
- package/src/ui/primitives/tooltip.tsx +0 -30
- package/src/ui/primitives/url-preview-chrome.tsx +0 -103
- package/src/vista/GenericCatalogueSurface.tsx +0 -179
- package/src/vista/SurfaceFallbackCard.tsx +0 -109
- package/src/vista/VistaPanel.tsx +0 -163
- package/src/vista/form-payload.spec.ts +0 -139
- package/src/vista/html/CustomUiFrame.spec.tsx +0 -127
- package/src/vista/html/CustomUiFrame.tsx +0 -206
- package/src/vista/html/custom-ui-frame-bridge.spec.ts +0 -191
- package/src/vista/html/custom-ui-frame-bridge.ts +0 -98
- package/src/vista/html/custom-ui-frame-contract.ts +0 -88
- package/src/vista/html/custom-ui-intents.spec.ts +0 -135
- package/src/vista/html/custom-ui-intents.ts +0 -155
- package/src/vista/html/custom-ui-srcdoc.spec.ts +0 -151
- package/src/vista/html/custom-ui-srcdoc.ts +0 -193
- package/src/vista/html/index.ts +0 -13
- package/src/vista/index.ts +0 -52
- package/src/vista/surface-catalogue.spec.ts +0 -140
- package/src/vista/surface-catalogue.ts +0 -90
- package/src/vista/surface-icons.ts +0 -57
- package/src/vista/surface-registry.ts +0 -56
- package/src/vista/surfaces/ChartSurface.tsx +0 -34
- package/src/vista/surfaces/FilesSurface.tsx +0 -202
- package/src/vista/surfaces/FormSurface.tsx +0 -380
- package/src/vista/surfaces/HtmlSurface.tsx +0 -43
- package/src/vista/surfaces/TableSurface.tsx +0 -100
- package/src/vista/surfaces/UrlSurface.tsx +0 -40
- package/src/vista/surfaces/blob-surfaces.tsx +0 -139
- package/src/vista/surfaces/elicitation-transport.spec.ts +0 -369
- package/src/vista/surfaces/elicitation-transport.ts +0 -367
- package/src/vista/surfaces/file-surface-utils.ts +0 -69
- package/src/vista/surfaces/index.ts +0 -24
- package/src/vista/surfaces/register-surfaces.tsx +0 -139
- package/src/vista/surfaces/text-surfaces.tsx +0 -132
- package/src/vista/use-vista-tabs.ts +0 -196
- package/src/vista/vista-code-renderer-context.ts +0 -16
- package/src/vista/vista-code-renderer.tsx +0 -35
- package/src/vista/vista-file-context.ts +0 -14
- package/src/vista/vista-file-source.tsx +0 -141
- package/src/vista/vista-form-context.ts +0 -20
- package/src/vista/vista-form-host.tsx +0 -48
- package/src/vista/vista-model.ts +0 -137
- package/src/vista/vista-open-context.tsx +0 -27
- package/src/vista/vista-open-state.ts +0 -11
- package/src/vista/vista-payloads.ts +0 -635
- package/src/vista/vista-tabs-manifest.ts +0 -138
- package/src/vista/vista-tools.ts +0 -232
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* usePageState — page-level UI state synced to the URL query string.
|
|
3
|
-
*
|
|
4
|
-
* State survives refresh, back/forward, and is shareable via URL. Rebuilt on
|
|
5
|
-
* the {@link HostBridge} navigation port (`useSearchParams` to read,
|
|
6
|
-
* `useLocation` for the current path, `push`/`replace` to write) so biome code
|
|
7
|
-
* never imports a router primitive (`next/navigation`, `react-router-dom`)
|
|
8
|
-
* directly — the host owns history semantics.
|
|
9
|
-
*
|
|
10
|
-
* Usage:
|
|
11
|
-
* const [activeTab, setActiveTab] = usePageState<string>('tab', 'runs');
|
|
12
|
-
* const [selectedId, setSelectedId] = usePageState<string | null>('run', null);
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
16
|
-
|
|
17
|
-
import { useHostBridge } from './host-bridge';
|
|
18
|
-
|
|
19
|
-
/** Default debounce for the URL mirror of a free-text input, in ms. */
|
|
20
|
-
const INPUT_URL_MIRROR_DEBOUNCE_MS = 300;
|
|
21
|
-
|
|
22
|
-
function buildPath(pathname: string, params: URLSearchParams): string {
|
|
23
|
-
const queryString = params.toString();
|
|
24
|
-
return queryString ? `${pathname}?${queryString}` : pathname;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function serializeValue<T>(value: T): string {
|
|
28
|
-
if (typeof value === 'string') {
|
|
29
|
-
return value;
|
|
30
|
-
}
|
|
31
|
-
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
32
|
-
return String(value);
|
|
33
|
-
}
|
|
34
|
-
return JSON.stringify(value);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function tryParseJson(rawValue: string): unknown {
|
|
38
|
-
try {
|
|
39
|
-
return JSON.parse(rawValue);
|
|
40
|
-
} catch {
|
|
41
|
-
return undefined;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function deserializePrimitive<T>(
|
|
46
|
-
rawValue: string,
|
|
47
|
-
defaultValue: T,
|
|
48
|
-
): T | undefined {
|
|
49
|
-
if (typeof defaultValue === 'string') {
|
|
50
|
-
return rawValue as T;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (typeof defaultValue === 'number') {
|
|
54
|
-
const numericValue = Number(rawValue);
|
|
55
|
-
return (Number.isNaN(numericValue) ? defaultValue : numericValue) as T;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (typeof defaultValue === 'boolean') {
|
|
59
|
-
if (rawValue === 'true') {
|
|
60
|
-
return true as T;
|
|
61
|
-
}
|
|
62
|
-
if (rawValue === 'false') {
|
|
63
|
-
return false as T;
|
|
64
|
-
}
|
|
65
|
-
return defaultValue;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return undefined;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function deserializeValue<T>(rawValue: string | null, defaultValue: T): T {
|
|
72
|
-
if (rawValue === null) {
|
|
73
|
-
return defaultValue;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const primitive = deserializePrimitive(rawValue, defaultValue);
|
|
77
|
-
if (primitive !== undefined) {
|
|
78
|
-
return primitive;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Default is null/undefined or a complex type (array/object). The value was
|
|
82
|
-
// written via JSON.stringify in serializeValue, so attempt to parse it back.
|
|
83
|
-
// If parsing yields a structured value (array/object/null), return it;
|
|
84
|
-
// otherwise fall back to the raw string (covers `T = string | null`).
|
|
85
|
-
const parsedValue = tryParseJson(rawValue);
|
|
86
|
-
if (parsedValue !== undefined && typeof parsedValue === 'object') {
|
|
87
|
-
return parsedValue as T;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if (defaultValue === null || defaultValue === undefined) {
|
|
91
|
-
return rawValue as T;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (parsedValue !== undefined) {
|
|
95
|
-
return parsedValue as T;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (Array.isArray(defaultValue)) {
|
|
99
|
-
return rawValue ? ([rawValue] as unknown as T) : defaultValue;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return defaultValue;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export function usePageState<T>(
|
|
106
|
-
stateKey: string,
|
|
107
|
-
defaultValue: T,
|
|
108
|
-
): [T, (value: T) => void] {
|
|
109
|
-
const { navigation } = useHostBridge();
|
|
110
|
-
const searchParams = navigation.useSearchParams();
|
|
111
|
-
const { pathname } = navigation.useLocation();
|
|
112
|
-
|
|
113
|
-
const rawValue = searchParams.get(stateKey);
|
|
114
|
-
const value = deserializeValue(rawValue, defaultValue);
|
|
115
|
-
|
|
116
|
-
const setValue = useCallback(
|
|
117
|
-
(newValue: T) => {
|
|
118
|
-
const next = new URLSearchParams(searchParams);
|
|
119
|
-
if (
|
|
120
|
-
newValue === null ||
|
|
121
|
-
newValue === undefined ||
|
|
122
|
-
serializeValue(newValue) === serializeValue(defaultValue)
|
|
123
|
-
) {
|
|
124
|
-
next.delete(stateKey);
|
|
125
|
-
} else {
|
|
126
|
-
next.set(stateKey, serializeValue(newValue));
|
|
127
|
-
}
|
|
128
|
-
navigation.push(buildPath(pathname, next));
|
|
129
|
-
},
|
|
130
|
-
[defaultValue, stateKey, searchParams, pathname, navigation],
|
|
131
|
-
);
|
|
132
|
-
|
|
133
|
-
return [value, setValue];
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* usePageInputState — like {@link usePageState}, but tuned for FREE-TEXT
|
|
138
|
-
* `<Input>` values that are mirrored to the URL on every keystroke.
|
|
139
|
-
*
|
|
140
|
-
* The returned value lives in LOCAL React state, so typing is instant. The URL
|
|
141
|
-
* is mirrored — debounced — via the host's `replace` (an in-place history entry
|
|
142
|
-
* update), so the value stays shareable / refresh-survivable WITHOUT pushing a
|
|
143
|
-
* new history entry per keystroke.
|
|
144
|
-
*
|
|
145
|
-
* The local value re-syncs whenever the underlying search param changes from
|
|
146
|
-
* the outside (a real navigation, a "Clear" button writing the param, or
|
|
147
|
-
* back/forward), so external resets still flow through.
|
|
148
|
-
*
|
|
149
|
-
* Use this ONLY for free-text inputs. Tabs, selects, and row selection are real
|
|
150
|
-
* navigations — keep them on {@link usePageState}.
|
|
151
|
-
*
|
|
152
|
-
* Usage:
|
|
153
|
-
* const [filter, setFilter] = usePageInputState('runFilter', '');
|
|
154
|
-
* <Input value={filter} onChange={(e) => setFilter(e.target.value)} />
|
|
155
|
-
*/
|
|
156
|
-
export function usePageInputState(
|
|
157
|
-
stateKey: string,
|
|
158
|
-
defaultValue: string,
|
|
159
|
-
debounceMs: number = INPUT_URL_MIRROR_DEBOUNCE_MS,
|
|
160
|
-
): [string, (value: string) => void] {
|
|
161
|
-
const { navigation } = useHostBridge();
|
|
162
|
-
const searchParams = navigation.useSearchParams();
|
|
163
|
-
const { pathname } = navigation.useLocation();
|
|
164
|
-
|
|
165
|
-
const urlValue = searchParams.get(stateKey) ?? defaultValue;
|
|
166
|
-
|
|
167
|
-
const [localValue, setLocalValue] = useState(urlValue);
|
|
168
|
-
|
|
169
|
-
// Resync local state when the URL param changes from the outside (real
|
|
170
|
-
// navigation, Clear button, back/forward). We compare against the value we
|
|
171
|
-
// last mirrored so our own debounced writes don't bounce back and clobber
|
|
172
|
-
// freshly-typed characters.
|
|
173
|
-
const lastMirroredRef = useRef(urlValue);
|
|
174
|
-
useEffect(() => {
|
|
175
|
-
if (urlValue !== lastMirroredRef.current) {
|
|
176
|
-
lastMirroredRef.current = urlValue;
|
|
177
|
-
setLocalValue(urlValue);
|
|
178
|
-
}
|
|
179
|
-
}, [urlValue]);
|
|
180
|
-
|
|
181
|
-
const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
182
|
-
useEffect(
|
|
183
|
-
() => () => {
|
|
184
|
-
if (debounceTimerRef.current !== null) {
|
|
185
|
-
clearTimeout(debounceTimerRef.current);
|
|
186
|
-
}
|
|
187
|
-
},
|
|
188
|
-
[],
|
|
189
|
-
);
|
|
190
|
-
|
|
191
|
-
const setValue = useCallback(
|
|
192
|
-
(newValue: string) => {
|
|
193
|
-
// Instant: drive the input from local state synchronously.
|
|
194
|
-
setLocalValue(newValue);
|
|
195
|
-
|
|
196
|
-
if (debounceTimerRef.current !== null) {
|
|
197
|
-
clearTimeout(debounceTimerRef.current);
|
|
198
|
-
}
|
|
199
|
-
debounceTimerRef.current = setTimeout(() => {
|
|
200
|
-
debounceTimerRef.current = null;
|
|
201
|
-
lastMirroredRef.current =
|
|
202
|
-
newValue === defaultValue ? defaultValue : newValue;
|
|
203
|
-
const next = new URLSearchParams(searchParams);
|
|
204
|
-
if (newValue === defaultValue || newValue.length === 0) {
|
|
205
|
-
next.delete(stateKey);
|
|
206
|
-
} else {
|
|
207
|
-
next.set(stateKey, newValue);
|
|
208
|
-
}
|
|
209
|
-
navigation.replace(buildPath(pathname, next));
|
|
210
|
-
}, debounceMs);
|
|
211
|
-
},
|
|
212
|
-
[debounceMs, defaultValue, stateKey, searchParams, pathname, navigation],
|
|
213
|
-
);
|
|
214
|
-
|
|
215
|
-
return [localValue, setValue];
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Batch-update multiple page-state keys in a single navigation.
|
|
220
|
-
*
|
|
221
|
-
* Pass `null` for a value to delete the key (reset to default).
|
|
222
|
-
*
|
|
223
|
-
* Usage:
|
|
224
|
-
* const batch = useBatchPageState();
|
|
225
|
-
* batch({ selectedId: 'abc', mainTab: null, contextTab: null });
|
|
226
|
-
*/
|
|
227
|
-
export function useBatchPageState(): (
|
|
228
|
-
updates: Record<string, string | null>,
|
|
229
|
-
) => void {
|
|
230
|
-
const { navigation } = useHostBridge();
|
|
231
|
-
const searchParams = navigation.useSearchParams();
|
|
232
|
-
const { pathname } = navigation.useLocation();
|
|
233
|
-
|
|
234
|
-
return useCallback(
|
|
235
|
-
(updates: Record<string, string | null>) => {
|
|
236
|
-
const next = new URLSearchParams(searchParams);
|
|
237
|
-
for (const [key, value] of Object.entries(updates)) {
|
|
238
|
-
if (value === null) {
|
|
239
|
-
next.delete(key);
|
|
240
|
-
} else {
|
|
241
|
-
next.set(key, value);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
navigation.push(buildPath(pathname, next));
|
|
245
|
-
},
|
|
246
|
-
[searchParams, pathname, navigation],
|
|
247
|
-
);
|
|
248
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { createContext, useContext } from 'react';
|
|
2
|
-
|
|
3
|
-
import type { MeCapabilitiesSnapshot } from './types';
|
|
4
|
-
|
|
5
|
-
export interface CapabilityContextValue {
|
|
6
|
-
readonly snapshot: MeCapabilitiesSnapshot | undefined;
|
|
7
|
-
readonly isLoading: boolean;
|
|
8
|
-
readonly error: unknown;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export const CapabilityContext =
|
|
12
|
-
createContext<CapabilityContextValue | null>(null);
|
|
13
|
-
CapabilityContext.displayName = 'CapabilityContext';
|
|
14
|
-
|
|
15
|
-
export function useCapabilityContext(): CapabilityContextValue {
|
|
16
|
-
const ctx = useContext(CapabilityContext);
|
|
17
|
-
if (!ctx) {
|
|
18
|
-
throw new Error(
|
|
19
|
-
'[xema-ui-kernel] useCapability()/useCapabilities() called outside ' +
|
|
20
|
-
'<CapabilityProvider>. The host (or biome shell) must mount the ' +
|
|
21
|
-
'capability provider above any capability-gated subtree.',
|
|
22
|
-
);
|
|
23
|
-
}
|
|
24
|
-
return ctx;
|
|
25
|
-
}
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { useQuery } from '@tanstack/react-query';
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
CapabilityContext,
|
|
5
|
-
type CapabilityContextValue,
|
|
6
|
-
} from './capability-context';
|
|
7
|
-
import { useHostBridge } from '../biome-host/host-bridge';
|
|
8
|
-
|
|
9
|
-
import type { CapabilityPort, MeCapabilitiesSnapshot } from './types';
|
|
10
|
-
import type { ReactNode } from 'react';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Stable query-key root for the capability snapshot. Folded with the active
|
|
14
|
-
* org so each org caches independently and a re-fetch fires when the org
|
|
15
|
-
* changes — same tenant-isolation discipline as `useScopedQuery`.
|
|
16
|
-
*/
|
|
17
|
-
const CAPABILITIES_QUERY_ROOT = ['xema', 'capabilities', 'me'] as const;
|
|
18
|
-
|
|
19
|
-
export interface CapabilityProviderProps {
|
|
20
|
-
readonly children: ReactNode;
|
|
21
|
-
/**
|
|
22
|
-
* Override the host-provided port. Defaults to `bridge.capabilities`. Exists
|
|
23
|
-
* for tests and for hosts that compose the provider outside a bridge subtree;
|
|
24
|
-
* production wiring reads it OFF the bridge so biomes need no extra prop.
|
|
25
|
-
*/
|
|
26
|
-
readonly port?: CapabilityPort;
|
|
27
|
-
/**
|
|
28
|
-
* Override the active org. Defaults to `null` (the host's
|
|
29
|
-
* currently-selected org). When provided, the port is asked for that org and
|
|
30
|
-
* the cache key includes it.
|
|
31
|
-
*/
|
|
32
|
-
readonly orgId?: string | null;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Fetches the caller's server-authoritative capability set once per active org
|
|
37
|
-
* and exposes it (plus loading/error) to descendants via React context.
|
|
38
|
-
*
|
|
39
|
-
* Wiring: the {@link CapabilityPort} is read OFF `HostBridge.capabilities`
|
|
40
|
-
* (not prop-drilled) — the same decoupling the rest of the kernel uses to
|
|
41
|
-
* reach the host. The active org is read from the bridge's auth source. The
|
|
42
|
-
* bridge's `QueryClient` (TanStack Query) owns caching/refetch; the kernel
|
|
43
|
-
* rolls no bespoke state. The query re-runs when the org changes because the
|
|
44
|
-
* org is part of the query key.
|
|
45
|
-
*
|
|
46
|
-
* Rendering is fail-soft (callers can show a loading state), but failures are
|
|
47
|
-
* SURFACED via `error` rather than masquerading as an empty capability set.
|
|
48
|
-
*/
|
|
49
|
-
export function CapabilityProvider(
|
|
50
|
-
props: CapabilityProviderProps,
|
|
51
|
-
): JSX.Element {
|
|
52
|
-
const bridge = useHostBridge();
|
|
53
|
-
const port = props.port ?? bridge.capabilities;
|
|
54
|
-
const orgId =
|
|
55
|
-
props.orgId !== undefined ? props.orgId : bridge.auth.getOrgId();
|
|
56
|
-
|
|
57
|
-
const query = useQuery<MeCapabilitiesSnapshot>({
|
|
58
|
-
queryKey: [...CAPABILITIES_QUERY_ROOT, orgId],
|
|
59
|
-
queryFn: () => port.list(orgId ? { orgId } : undefined),
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
const value: CapabilityContextValue = {
|
|
63
|
-
snapshot: query.data,
|
|
64
|
-
isLoading: query.isLoading,
|
|
65
|
-
error: query.error,
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
return (
|
|
69
|
-
<CapabilityContext.Provider value={value}>
|
|
70
|
-
{props.children}
|
|
71
|
-
</CapabilityContext.Provider>
|
|
72
|
-
);
|
|
73
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Host-agnostic capabilities SDK for `@xemahq/ui-kernel`.
|
|
3
|
-
*
|
|
4
|
-
* Lets a biome UI gate on SERVER-AUTHORITATIVE capabilities (the caller's
|
|
5
|
-
* effective set from `GET /bff/me/capabilities`) instead of cosmetic role
|
|
6
|
-
* booleans. The kernel defines the contract and the React surface; it never
|
|
7
|
-
* fetches — the HOST implements {@link CapabilityPort} (hung off
|
|
8
|
-
* `HostBridge.capabilities`) and maps its Orval client response onto the
|
|
9
|
-
* local {@link MeCapabilitiesSnapshot} shape.
|
|
10
|
-
*
|
|
11
|
-
* Biome usage:
|
|
12
|
-
* import { CapabilityProvider, useCapability } from '@xemahq/ui-kernel';
|
|
13
|
-
*/
|
|
14
|
-
export * from './types';
|
|
15
|
-
export * from './capability-context';
|
|
16
|
-
export * from './capability-provider';
|
|
17
|
-
export * from './use-capability';
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import type { ExecutionEnvironmentKind } from '@xemahq/kernel-contracts/execution-environment';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* One server-authoritative capability the caller holds, mirroring the
|
|
5
|
-
* FROZEN `GET /bff/me/capabilities` wire entry.
|
|
6
|
-
*
|
|
7
|
-
* These types are defined LOCALLY in the kernel — deliberately NOT imported
|
|
8
|
-
* from a backend DTO or an Orval client — so `@xemahq/ui-kernel` stays
|
|
9
|
-
* host-agnostic (no fetch lib, no generated client). The concrete host
|
|
10
|
-
* (`submodules/xema-host-web`) maps its client response onto this shape when
|
|
11
|
-
* it implements {@link CapabilityPort}.
|
|
12
|
-
*/
|
|
13
|
-
export interface CapabilityEntry {
|
|
14
|
-
/**
|
|
15
|
-
* The capability reference (e.g. `capability:session.launch`). Matched
|
|
16
|
-
* verbatim by {@link useCapability}; the kernel does not parse it.
|
|
17
|
-
*/
|
|
18
|
-
readonly ref: string;
|
|
19
|
-
/**
|
|
20
|
-
* The execution environments in which the caller holds this capability.
|
|
21
|
-
* A gate that names an `environment` is allowed only if it appears here.
|
|
22
|
-
*/
|
|
23
|
-
readonly environments: ExecutionEnvironmentKind[];
|
|
24
|
-
/**
|
|
25
|
-
* When `true`, holding the capability still requires a human approval step
|
|
26
|
-
* before the side-effect runs — so a UI affordance should route through the
|
|
27
|
-
* approval flow rather than acting directly.
|
|
28
|
-
*/
|
|
29
|
-
readonly approvalRequired: boolean;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* The caller's effective capability set for one active org, mirroring the
|
|
34
|
-
* FROZEN `MeCapabilitiesResponse` wire shape.
|
|
35
|
-
*/
|
|
36
|
-
export interface MeCapabilitiesSnapshot {
|
|
37
|
-
/** The subject (user) the snapshot was resolved for. */
|
|
38
|
-
readonly subjectId: string;
|
|
39
|
-
/** The org the snapshot is scoped to. */
|
|
40
|
-
readonly orgId: string;
|
|
41
|
-
/** Every capability the subject effectively holds in this org. */
|
|
42
|
-
readonly capabilities: CapabilityEntry[];
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Host-implemented port that fetches the caller's effective capability set.
|
|
47
|
-
*
|
|
48
|
-
* The kernel never calls an API directly — the HOST implements this against
|
|
49
|
-
* its Orval client (`GET /bff/me/capabilities`) and maps the response onto
|
|
50
|
-
* {@link MeCapabilitiesSnapshot}. The implementation is hung off
|
|
51
|
-
* {@link HostBridge.capabilities}; {@link CapabilityProvider} reads it from
|
|
52
|
-
* the bridge and uses the bridge's `QueryClient` for caching/refetch.
|
|
53
|
-
*/
|
|
54
|
-
export interface CapabilityPort {
|
|
55
|
-
/**
|
|
56
|
-
* Return the caller's effective capability set for the active org.
|
|
57
|
-
*
|
|
58
|
-
* @param options.orgId The org to resolve for. When omitted the host
|
|
59
|
-
* resolves against its currently-selected org.
|
|
60
|
-
* @param options.environment Optional environment hint the host MAY pass to
|
|
61
|
-
* the backend to narrow the set. Callers gate by environment client-side
|
|
62
|
-
* via {@link useCapability}, so a host MAY ignore this and return the full
|
|
63
|
-
* set; it exists so a host can request a pre-filtered snapshot.
|
|
64
|
-
*/
|
|
65
|
-
list(options?: {
|
|
66
|
-
orgId?: string;
|
|
67
|
-
environment?: ExecutionEnvironmentKind;
|
|
68
|
-
}): Promise<MeCapabilitiesSnapshot>;
|
|
69
|
-
}
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
useCapabilityContext,
|
|
3
|
-
type CapabilityContextValue,
|
|
4
|
-
} from './capability-context';
|
|
5
|
-
|
|
6
|
-
import type { MeCapabilitiesSnapshot } from './types';
|
|
7
|
-
import type { ExecutionEnvironmentKind } from '@xemahq/kernel-contracts/execution-environment';
|
|
8
|
-
|
|
9
|
-
export interface UseCapabilityOptions {
|
|
10
|
-
/**
|
|
11
|
-
* Gate against a specific execution environment. When set, the capability
|
|
12
|
-
* counts only if the snapshot lists it for this environment.
|
|
13
|
-
*/
|
|
14
|
-
readonly environment?: ExecutionEnvironmentKind;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface CapabilityDecision {
|
|
18
|
-
/**
|
|
19
|
-
* `true` only when the caller holds the capability, holds it in the named
|
|
20
|
-
* environment (if one was given), AND it does NOT require approval. A gate
|
|
21
|
-
* that should let the user act directly checks this.
|
|
22
|
-
*/
|
|
23
|
-
readonly allowed: boolean;
|
|
24
|
-
/**
|
|
25
|
-
* `true` when the caller holds the capability (in the named environment, if
|
|
26
|
-
* given) but it requires a human approval step first — render the
|
|
27
|
-
* approval-routed affordance instead of acting directly.
|
|
28
|
-
*/
|
|
29
|
-
readonly approvalRequired: boolean;
|
|
30
|
-
/** The snapshot is still being fetched; treat as "unknown", not "denied". */
|
|
31
|
-
readonly isLoading: boolean;
|
|
32
|
-
/** The fetch failed; surfaced so callers never conflate failure with denial. */
|
|
33
|
-
readonly error: unknown;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Server-authoritative capability gate.
|
|
38
|
-
*
|
|
39
|
-
* Resolves `ref` against the cached {@link CapabilityProvider} snapshot:
|
|
40
|
-
* - `allowed` = entry exists AND (no environment asked OR listed for it) AND NOT approvalRequired.
|
|
41
|
-
* - `approvalRequired` = entry exists AND (no environment asked OR listed for it) AND approvalRequired.
|
|
42
|
-
*
|
|
43
|
-
* While the snapshot is loading, `allowed` is `false` and `isLoading` is
|
|
44
|
-
* `true` — callers gate on `isLoading` to distinguish "unknown" from "denied".
|
|
45
|
-
* On error, `allowed` is `false` and `error` is set; the hook never silently
|
|
46
|
-
* degrades a failed fetch into a confident denial.
|
|
47
|
-
*/
|
|
48
|
-
export function useCapability(
|
|
49
|
-
ref: string,
|
|
50
|
-
opts?: UseCapabilityOptions,
|
|
51
|
-
): CapabilityDecision {
|
|
52
|
-
const { snapshot, isLoading, error } = useCapabilityContext();
|
|
53
|
-
|
|
54
|
-
const entry = snapshot?.capabilities.find((c) => c.ref === ref);
|
|
55
|
-
const inEnvironment =
|
|
56
|
-
!!entry &&
|
|
57
|
-
(!opts?.environment || entry.environments.includes(opts.environment));
|
|
58
|
-
|
|
59
|
-
return {
|
|
60
|
-
allowed: inEnvironment && !entry.approvalRequired,
|
|
61
|
-
approvalRequired: inEnvironment && entry.approvalRequired,
|
|
62
|
-
isLoading,
|
|
63
|
-
error,
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Accessor for the raw capability snapshot plus its loading/error state.
|
|
69
|
-
* Use when a component needs to enumerate capabilities rather than gate on a
|
|
70
|
-
* single `ref`.
|
|
71
|
-
*/
|
|
72
|
-
export function useCapabilities(): {
|
|
73
|
-
snapshot: MeCapabilitiesSnapshot | undefined;
|
|
74
|
-
isLoading: boolean;
|
|
75
|
-
error: unknown;
|
|
76
|
-
} {
|
|
77
|
-
const ctx: CapabilityContextValue = useCapabilityContext();
|
|
78
|
-
return { snapshot: ctx.snapshot, isLoading: ctx.isLoading, error: ctx.error };
|
|
79
|
-
}
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Guards the shared closed+open contribution registry core — the ONE
|
|
3
|
-
* implementation behind the ChatWidget, biome output-renderer, reply-variant
|
|
4
|
-
* and Vista-surface planes. Every property the hand-written copies had must
|
|
5
|
-
* survive the extraction.
|
|
6
|
-
*
|
|
7
|
-
* Runs under the host-web vitest mirror (this package carries no vitest dep).
|
|
8
|
-
*/
|
|
9
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
10
|
-
|
|
11
|
-
import {
|
|
12
|
-
ContributionFallbackReason,
|
|
13
|
-
DuplicateContributionPolicy,
|
|
14
|
-
createContributionRegistry,
|
|
15
|
-
createRenderableContributionRegistry,
|
|
16
|
-
} from './index';
|
|
17
|
-
|
|
18
|
-
function makeRegistry(): ReturnType<typeof createContributionRegistry<string>> {
|
|
19
|
-
return createContributionRegistry<string>({
|
|
20
|
-
name: 'test',
|
|
21
|
-
reservedKinds: ['plan', 'diff'],
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
describe('createContributionRegistry', () => {
|
|
26
|
-
it('resolves first-party before open, and lists both', () => {
|
|
27
|
-
const registry = makeRegistry();
|
|
28
|
-
registry.registerFirstParty('plan', 'builtin-plan');
|
|
29
|
-
registry.register('acme:gantt', 'acme-gantt');
|
|
30
|
-
expect(registry.get('plan')).toBe('builtin-plan');
|
|
31
|
-
expect(registry.get('acme:gantt')).toBe('acme-gantt');
|
|
32
|
-
expect(registry.has('nope')).toBe(false);
|
|
33
|
-
expect(registry.list()).toEqual(['acme:gantt', 'plan']);
|
|
34
|
-
expect(registry.listOpen()).toEqual(['acme:gantt']);
|
|
35
|
-
expect([...registry.entries().keys()].sort()).toEqual(['acme:gantt', 'plan']);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('refuses to let the open set claim a reserved first-party kind', () => {
|
|
39
|
-
const registry = makeRegistry();
|
|
40
|
-
expect(registry.isReserved('diff')).toBe(true);
|
|
41
|
-
expect(() => registry.register('diff', 'hijack')).toThrow(/reserved/);
|
|
42
|
-
// The namespaced form is fine.
|
|
43
|
-
expect(() => registry.register('acme:diff', 'ok')).not.toThrow();
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('is idempotent for the identical entry and throws for a different one', () => {
|
|
47
|
-
const registry = makeRegistry();
|
|
48
|
-
registry.register('acme:gantt', 'entry');
|
|
49
|
-
expect(() => registry.register('acme:gantt', 'entry')).not.toThrow();
|
|
50
|
-
expect(() => registry.register('acme:gantt', 'other')).toThrow(
|
|
51
|
-
/already registered/,
|
|
52
|
-
);
|
|
53
|
-
registry.registerFirstParty('plan', 'builtin');
|
|
54
|
-
expect(() => registry.registerFirstParty('plan', 'builtin')).not.toThrow();
|
|
55
|
-
expect(() => registry.registerFirstParty('plan', 'other')).toThrow(
|
|
56
|
-
/already registered/,
|
|
57
|
-
);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('resets only the OPEN set', () => {
|
|
61
|
-
const registry = makeRegistry();
|
|
62
|
-
registry.registerFirstParty('plan', 'builtin');
|
|
63
|
-
registry.register('acme:gantt', 'entry');
|
|
64
|
-
registry.__resetOpenForTests();
|
|
65
|
-
expect(registry.list()).toEqual(['plan']);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('reports — never swallows — a duplicate under FirstWins', () => {
|
|
69
|
-
const onDuplicate = vi.fn();
|
|
70
|
-
const registry = createContributionRegistry<string>({
|
|
71
|
-
name: 'overlay',
|
|
72
|
-
duplicatePolicy: DuplicateContributionPolicy.FirstWins,
|
|
73
|
-
onDuplicate,
|
|
74
|
-
});
|
|
75
|
-
registry.register('k', 'first');
|
|
76
|
-
registry.register('k', 'second');
|
|
77
|
-
expect(registry.get('k')).toBe('first');
|
|
78
|
-
expect(onDuplicate).toHaveBeenCalledWith('k', 'first', 'second');
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('refuses FirstWins without a reporter', () => {
|
|
82
|
-
expect(() =>
|
|
83
|
-
createContributionRegistry<string>({
|
|
84
|
-
name: 'overlay',
|
|
85
|
-
duplicatePolicy: DuplicateContributionPolicy.FirstWins,
|
|
86
|
-
}),
|
|
87
|
-
).toThrow(/onDuplicate/);
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
describe('createRenderableContributionRegistry', () => {
|
|
92
|
-
const registry = createRenderableContributionRegistry<string>({
|
|
93
|
-
name: 'renderable',
|
|
94
|
-
reservedKinds: ['plan'],
|
|
95
|
-
});
|
|
96
|
-
registry.registerFirstParty<{ label: string }>('plan', {
|
|
97
|
-
version: 2,
|
|
98
|
-
validate: (payload) =>
|
|
99
|
-
typeof payload === 'object' &&
|
|
100
|
-
payload !== null &&
|
|
101
|
-
typeof (payload as { label?: unknown }).label === 'string'
|
|
102
|
-
? (payload as { label: string })
|
|
103
|
-
: null,
|
|
104
|
-
render: (payload) => `plan:${payload.label}`,
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('renders a matching envelope', () => {
|
|
108
|
-
const resolution = registry.resolve({
|
|
109
|
-
kind: 'plan',
|
|
110
|
-
version: 1,
|
|
111
|
-
payload: { label: 'ship it' },
|
|
112
|
-
});
|
|
113
|
-
expect(resolution).toEqual({
|
|
114
|
-
matched: true,
|
|
115
|
-
kind: 'plan',
|
|
116
|
-
version: 2,
|
|
117
|
-
rendered: 'plan:ship it',
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it('never throws — it returns a typed reason instead', () => {
|
|
122
|
-
expect(
|
|
123
|
-
registry.resolve({ kind: 'nope', version: 1, payload: {} }),
|
|
124
|
-
).toMatchObject({
|
|
125
|
-
matched: false,
|
|
126
|
-
reason: ContributionFallbackReason.UnknownKind,
|
|
127
|
-
});
|
|
128
|
-
expect(
|
|
129
|
-
registry.resolve({ kind: 'plan', version: 99, payload: {} }),
|
|
130
|
-
).toMatchObject({
|
|
131
|
-
matched: false,
|
|
132
|
-
reason: ContributionFallbackReason.UnsupportedVersion,
|
|
133
|
-
});
|
|
134
|
-
expect(
|
|
135
|
-
registry.resolve({ kind: 'plan', version: 1, payload: { label: 7 } }),
|
|
136
|
-
).toMatchObject({
|
|
137
|
-
matched: false,
|
|
138
|
-
reason: ContributionFallbackReason.InvalidPayload,
|
|
139
|
-
});
|
|
140
|
-
});
|
|
141
|
-
});
|