@adecore/intelligent-ui 0.21.0 → 0.21.1
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 +29 -57
- package/dist/actions.d.ts +5 -0
- package/dist/actions.js +34 -0
- package/dist/catalog.d.ts +9 -1
- package/dist/catalog.js +12 -8
- package/dist/compiler.d.ts +9 -1
- package/dist/compiler.js +54 -44
- package/dist/expression.d.ts +1 -0
- package/dist/expression.js +16 -0
- package/dist/fallback.d.ts +2 -0
- package/dist/fallback.js +25 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/inputs.d.ts +5 -0
- package/dist/inputs.js +69 -0
- package/dist/links.d.ts +3 -1
- package/dist/links.js +6 -3
- package/dist/prop-list.d.ts +7 -0
- package/dist/prop-list.js +5 -0
- package/dist/query.d.ts +3 -2
- package/dist/query.js +14 -77
- package/dist/runtime.d.ts +4 -2
- package/dist/runtime.js +63 -54
- package/dist/text.d.ts +5 -2
- package/dist/text.js +21 -20
- package/package.json +1 -1
- package/src/actions.ts +39 -0
- package/src/catalog.ts +12 -8
- package/src/compiler.ts +59 -42
- package/src/expression.ts +17 -0
- package/src/fallback.ts +26 -0
- package/src/index.ts +5 -2
- package/src/inputs.ts +74 -0
- package/src/links.ts +8 -3
- package/src/prop-list.ts +7 -0
- package/src/query.ts +26 -80
- package/src/runtime.ts +64 -53
- package/src/text.ts +27 -20
package/src/runtime.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { copyUiValue, evaluateUiExpression, type UiExpression, type UiValue } from './expression.ts';
|
|
1
|
+
import { copyUiValue, evaluateUiExpression, sameUiValue, type UiExpression, type UiValue } from './expression.ts';
|
|
2
2
|
import { UiBudget, UiFailure, type UiLimits } from './budget.ts';
|
|
3
3
|
import { isUiComponent, UI_CATALOG, UI_CATALOG_VERSION } from './catalog.ts';
|
|
4
4
|
import { type UiBlock, type UiNode } from './compiler.ts';
|
|
5
|
+
import { uiActionProp, uiActionTarget, uiLocalState } from './actions.ts';
|
|
6
|
+
import { uiCompiledNodes, uiVisibleInputs } from './inputs.ts';
|
|
5
7
|
import { uiDiagnostic, type UiDiagnostic } from './syntax.ts';
|
|
6
8
|
|
|
7
9
|
export interface UiBinding<Value> {
|
|
@@ -19,6 +21,8 @@ export interface UiViewNode<Props = Record<string, unknown>> {
|
|
|
19
21
|
complete: boolean;
|
|
20
22
|
fallback: string;
|
|
21
23
|
error?: string;
|
|
24
|
+
// Runs the node's state action; present only on a component that has one.
|
|
25
|
+
onAction?(): void;
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
export interface UiEvaluation {
|
|
@@ -34,19 +38,21 @@ export class UiState {
|
|
|
34
38
|
private querySignatures: Record<string, string> = Object.create(null);
|
|
35
39
|
private readonly listeners = new Set<() => void>();
|
|
36
40
|
private revision = 0;
|
|
41
|
+
private readonly limits: Partial<UiLimits>;
|
|
37
42
|
|
|
38
|
-
constructor(block: UiBlock) {
|
|
43
|
+
constructor(block: UiBlock, limits: Partial<UiLimits> = {}) {
|
|
44
|
+
this.limits = limits;
|
|
39
45
|
this.sync(block);
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
sync(block: UiBlock): void {
|
|
43
|
-
const defaults = copyUiValue(block.defaults) as Record<string, UiValue>;
|
|
49
|
+
const defaults = copyUiValue(block.defaults, new UiBudget(this.limits)) as Record<string, UiValue>;
|
|
44
50
|
if (this.id !== block.id) {
|
|
45
51
|
this.values = Object.create(null);
|
|
46
52
|
this.queries = Object.create(null);
|
|
47
53
|
}
|
|
48
54
|
for (const [name, value] of Object.entries(defaults)) {
|
|
49
|
-
if (this.id !== block.id || !Object.hasOwn(this.defaults, name) ||
|
|
55
|
+
if (this.id !== block.id || !Object.hasOwn(this.defaults, name) || !sameUiValue(this.defaults[name], value)) {
|
|
50
56
|
this.values[name] = value;
|
|
51
57
|
}
|
|
52
58
|
}
|
|
@@ -78,14 +84,14 @@ export class UiState {
|
|
|
78
84
|
}
|
|
79
85
|
|
|
80
86
|
scope(): Readonly<Record<string, UiValue>> {
|
|
81
|
-
return copyUiValue({ ...this.values, ...this.queries }) as Record<string, UiValue>;
|
|
87
|
+
return copyUiValue({ ...this.values, ...this.queries }, new UiBudget(this.limits)) as Record<string, UiValue>;
|
|
82
88
|
}
|
|
83
89
|
|
|
84
90
|
set(name: string, value: unknown): void {
|
|
85
91
|
if (!Object.hasOwn(this.defaults, name)) {
|
|
86
92
|
throw new UiFailure('refused_binding', 'Only declared local state can be changed.');
|
|
87
93
|
}
|
|
88
|
-
const next = copyUiValue(value);
|
|
94
|
+
const next = copyUiValue(value, new UiBudget(this.limits));
|
|
89
95
|
const initial = this.defaults[name];
|
|
90
96
|
if (Array.isArray(next) !== Array.isArray(initial) || typeof next !== typeof initial || (next === null) !== (initial === null)) {
|
|
91
97
|
throw new UiFailure('invalid_value', 'The state type cannot change.');
|
|
@@ -98,29 +104,19 @@ export class UiState {
|
|
|
98
104
|
if (block.id !== this.id || !Object.hasOwn(block.queries, name) || this.querySignatures[name] !== JSON.stringify(block.queries[name])) {
|
|
99
105
|
throw new UiFailure('refused_query', 'The query must belong to this block.');
|
|
100
106
|
}
|
|
101
|
-
this.queries[name] = copyUiValue(value);
|
|
107
|
+
this.queries[name] = copyUiValue(value, new UiBudget(this.limits));
|
|
102
108
|
this.changed();
|
|
103
109
|
}
|
|
104
110
|
|
|
105
111
|
run(action: UiExpression): void {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (action.name === 'Reset' && action.args.length === 0) {
|
|
110
|
-
this.values = copyUiValue(this.defaults) as Record<string, UiValue>;
|
|
112
|
+
const target = uiActionTarget(action, (name) => Object.hasOwn(this.defaults, name));
|
|
113
|
+
if (target === null) {
|
|
114
|
+
this.values = copyUiValue(this.defaults, new UiBudget(this.limits)) as Record<string, UiValue>;
|
|
111
115
|
this.changed();
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const reference = action.args[0];
|
|
115
|
-
if (!reference || reference.kind !== 'reference' || !Object.hasOwn(this.defaults, reference.name)) {
|
|
116
|
-
throw new UiFailure('refused_binding', 'The action needs a local state variable.');
|
|
117
|
-
}
|
|
118
|
-
if (action.name === 'Set' && action.args.length === 2) {
|
|
119
|
-
this.set(reference.name, evaluateUiExpression(action.args[1], this.scope()));
|
|
120
|
-
} else if (action.name === 'Reset' && action.args.length === 1) {
|
|
121
|
-
this.set(reference.name, this.defaults[reference.name]);
|
|
116
|
+
} else if (action.kind === 'call' && action.name === 'Set') {
|
|
117
|
+
this.set(target, evaluateUiExpression(action.args[1], this.scope(), new UiBudget(this.limits)));
|
|
122
118
|
} else {
|
|
123
|
-
|
|
119
|
+
this.set(target, this.defaults[target]);
|
|
124
120
|
}
|
|
125
121
|
}
|
|
126
122
|
|
|
@@ -165,8 +161,11 @@ export function evaluateUiBlock(block: UiBlock, state = new UiState(block), limi
|
|
|
165
161
|
throw new UiFailure(node.error ?? 'unknown_catalog', 'This part uses an unsupported UI definition.');
|
|
166
162
|
}
|
|
167
163
|
const props: Record<string, UiValue> = copyUiValue(node.props, budget) as Record<string, UiValue>;
|
|
164
|
+
const actionProp = uiActionProp(node.type);
|
|
168
165
|
for (const [key, expression] of Object.entries(node.expressions)) {
|
|
169
|
-
|
|
166
|
+
if (key !== actionProp) {
|
|
167
|
+
props[key] = evaluateUiExpression(expression, variables, budget);
|
|
168
|
+
}
|
|
170
169
|
}
|
|
171
170
|
if (node.type === '$text') {
|
|
172
171
|
if (typeof props.text !== 'string' && typeof props.text !== 'number' && typeof props.text !== 'boolean' && props.text !== null) {
|
|
@@ -205,7 +204,7 @@ export function evaluateUiBlock(block: UiBlock, state = new UiState(block), limi
|
|
|
205
204
|
}
|
|
206
205
|
const bindings: Record<string, UiBinding<unknown>> = Object.create(null);
|
|
207
206
|
for (const [key, name] of Object.entries(node.bindings)) {
|
|
208
|
-
if (!('binding' in entry) || entry.binding !== key || !
|
|
207
|
+
if (!('binding' in entry) || entry.binding !== key || !uiLocalState(block)(name)) {
|
|
209
208
|
throw new UiFailure('refused_binding', 'The binding must target declared local input state.');
|
|
210
209
|
}
|
|
211
210
|
bindings[key] = {
|
|
@@ -222,6 +221,23 @@ export function evaluateUiBlock(block: UiBlock, state = new UiState(block), limi
|
|
|
222
221
|
};
|
|
223
222
|
}
|
|
224
223
|
view.bindings = bindings;
|
|
224
|
+
const action = actionProp === undefined ? undefined : node.expressions[actionProp];
|
|
225
|
+
if (action) {
|
|
226
|
+
uiActionTarget(action, uiLocalState(block));
|
|
227
|
+
view.onAction = () => {
|
|
228
|
+
if (!node.complete || props.disabled === true) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
try {
|
|
232
|
+
state.run(action);
|
|
233
|
+
} catch (error) {
|
|
234
|
+
// An action that cannot apply to the current state leaves it as it was.
|
|
235
|
+
if (!(error instanceof UiFailure)) {
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
}
|
|
225
241
|
view.children = children(node.children, variables, depth + 1, suffix);
|
|
226
242
|
return [view];
|
|
227
243
|
} catch (error) {
|
|
@@ -252,15 +268,28 @@ export interface UiChoiceSelection {
|
|
|
252
268
|
export function uiInputValues(block: UiBlock, state: UiState): Record<string, UiValue> {
|
|
253
269
|
const scope = state.scope();
|
|
254
270
|
const values: Record<string, UiValue> = Object.create(null);
|
|
271
|
+
const local = uiLocalState(block);
|
|
272
|
+
const actionTarget = (action: UiExpression): string | null => {
|
|
273
|
+
try {
|
|
274
|
+
return uiActionTarget(action, local);
|
|
275
|
+
} catch {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
};
|
|
255
279
|
const visit = (nodes: readonly UiNode[]) => {
|
|
256
280
|
for (const node of nodes) {
|
|
257
281
|
if (isUiComponent(node.type) && !node.error && node.complete) {
|
|
258
282
|
const entry = UI_CATALOG[node.type];
|
|
259
283
|
for (const [prop, name] of Object.entries(node.bindings)) {
|
|
260
|
-
if ('binding' in entry && entry.binding === prop &&
|
|
284
|
+
if ('binding' in entry && entry.binding === prop && local(name)) {
|
|
261
285
|
values[name] = scope[name];
|
|
262
286
|
}
|
|
263
287
|
}
|
|
288
|
+
const actionProp = uiActionProp(node.type);
|
|
289
|
+
const target = actionProp && node.expressions[actionProp] ? actionTarget(node.expressions[actionProp]) : null;
|
|
290
|
+
if (target !== null) {
|
|
291
|
+
values[target] = scope[target];
|
|
292
|
+
}
|
|
264
293
|
}
|
|
265
294
|
visit(node.children);
|
|
266
295
|
}
|
|
@@ -269,18 +298,19 @@ export function uiInputValues(block: UiBlock, state: UiState): Record<string, Ui
|
|
|
269
298
|
return values;
|
|
270
299
|
}
|
|
271
300
|
|
|
272
|
-
/* The host supplies its stored block; client data can change only declared
|
|
301
|
+
/* The host supplies its stored block; client data can change only declared inputs and Button targets. */
|
|
273
302
|
export function resolveUiChoice(
|
|
274
303
|
block: UiBlock,
|
|
275
304
|
choiceId: string,
|
|
276
305
|
input: Readonly<Record<string, unknown>> = {},
|
|
277
|
-
queries: Readonly<Record<string, unknown>> = {}
|
|
306
|
+
queries: Readonly<Record<string, unknown>> = {},
|
|
307
|
+
limits: Partial<UiLimits> = {}
|
|
278
308
|
): UiChoiceSelection {
|
|
279
309
|
if (!block.complete || block.catalogVersion !== UI_CATALOG_VERSION) {
|
|
280
310
|
throw new UiFailure('refused_choice', 'Only a completed supported block can send a choice.');
|
|
281
311
|
}
|
|
282
|
-
const values = copyUiValue(input, new UiBudget()) as Record<string, UiValue>;
|
|
283
|
-
const state = new UiState(block);
|
|
312
|
+
const values = copyUiValue(input, new UiBudget(limits)) as Record<string, UiValue>;
|
|
313
|
+
const state = new UiState(block, limits);
|
|
284
314
|
for (const [name, value] of Object.entries(queries)) {
|
|
285
315
|
state.setQuery(name, value, block);
|
|
286
316
|
}
|
|
@@ -290,25 +320,17 @@ export function resolveUiChoice(
|
|
|
290
320
|
if (!Object.hasOwn(allowed, name)) {
|
|
291
321
|
throw new UiFailure('refused_binding', 'Only declared input bindings can be submitted.');
|
|
292
322
|
}
|
|
293
|
-
if (
|
|
323
|
+
if (!sameUiValue(value, allowed[name])) {
|
|
294
324
|
state.set(name, value);
|
|
295
325
|
changed.add(name);
|
|
296
326
|
}
|
|
297
327
|
}
|
|
298
|
-
const evaluated = evaluateUiBlock(block, state);
|
|
328
|
+
const evaluated = evaluateUiBlock(block, state, limits);
|
|
299
329
|
if (evaluated.diagnostics.some((diagnostic) => diagnostic.code === 'budget_exceeded')) {
|
|
300
330
|
throw new UiFailure('budget_exceeded', 'The choice exceeded its evaluation budget.');
|
|
301
331
|
}
|
|
302
|
-
const validInputs = new Set<string>();
|
|
303
332
|
let choice: UiViewNode | undefined;
|
|
304
|
-
const written =
|
|
305
|
-
const remember = (nodes: readonly UiNode[]) => {
|
|
306
|
-
for (const node of nodes) {
|
|
307
|
-
written.set(node.id, node);
|
|
308
|
-
remember(node.children);
|
|
309
|
-
}
|
|
310
|
-
};
|
|
311
|
-
remember(block.nodes);
|
|
333
|
+
const written = uiCompiledNodes(block.nodes);
|
|
312
334
|
const visit = (nodes: readonly UiViewNode[], parent?: string) => {
|
|
313
335
|
for (const node of nodes) {
|
|
314
336
|
const source = written.get(node.sourceId ?? node.id);
|
|
@@ -318,18 +340,6 @@ export function resolveUiChoice(
|
|
|
318
340
|
if (node.error || !node.complete) {
|
|
319
341
|
continue;
|
|
320
342
|
}
|
|
321
|
-
let valid = true;
|
|
322
|
-
if (node.type === 'Segmented') {
|
|
323
|
-
valid = node.children.some((option) => option.type === 'Option' && !option.error && option.props.value === node.props.value);
|
|
324
|
-
} else if (node.type === 'Checklist') {
|
|
325
|
-
const options = node.children.filter((item) => item.type === 'Item' && !item.error).map((item) => item.props.value);
|
|
326
|
-
valid = (node.props.value as unknown[]).every((value) => options.includes(value));
|
|
327
|
-
}
|
|
328
|
-
if (valid && source) {
|
|
329
|
-
for (const prop of Object.keys(node.bindings)) {
|
|
330
|
-
validInputs.add(source.bindings[prop]);
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
343
|
if (node.id === choiceId && node.type === 'Choice' && parent === 'Choices' && node.props.disabled !== true) {
|
|
334
344
|
choice = node;
|
|
335
345
|
}
|
|
@@ -337,6 +347,7 @@ export function resolveUiChoice(
|
|
|
337
347
|
}
|
|
338
348
|
};
|
|
339
349
|
visit(evaluated.nodes);
|
|
350
|
+
const validInputs = uiVisibleInputs(block, evaluated.nodes, state.scope());
|
|
340
351
|
if ([...changed].some((name) => !validInputs.has(name))) {
|
|
341
352
|
throw new UiFailure('invalid_value', 'An input value is outside the visible control’s allowed values.');
|
|
342
353
|
}
|
package/src/text.ts
CHANGED
|
@@ -1,47 +1,54 @@
|
|
|
1
1
|
import { UI_CATALOG, UI_GROUPS } from './catalog.ts';
|
|
2
|
+
import { uiPropList } from './prop-list.ts';
|
|
2
3
|
import { UI_FENCE_LANGUAGE, type UiBlock } from './compiler.ts';
|
|
3
4
|
|
|
4
5
|
export function uiCompactCatalog(): string {
|
|
5
6
|
return Object.entries(UI_CATALOG)
|
|
6
7
|
.map(([name, entry]) => {
|
|
7
|
-
const props =
|
|
8
|
-
.map(([key, schema]) => `${key}${schema.isOptional() ? '?' : ''}`)
|
|
9
|
-
.join(',');
|
|
8
|
+
const props = uiPropList(entry).join(',');
|
|
10
9
|
return `${name}${props ? `: ${props}` : ''}`;
|
|
11
10
|
})
|
|
12
11
|
.join(' · ');
|
|
13
12
|
}
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
14
|
+
export interface UiTextOptions {
|
|
15
|
+
// The info string the host's compiler looks for; `UI_FENCE_LANGUAGE` without one.
|
|
16
|
+
fenceLanguage?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function uiSyntax(language: string): string {
|
|
20
|
+
return `Write XML-like component tags inside a fenced ${language} block. Put text between opening and closing tags; empty components end with />. Quote string props; put numbers, booleans and expressions in braces.`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function uiExample(language: string): string {
|
|
24
|
+
return [
|
|
25
|
+
'```' + language,
|
|
26
|
+
'$enabled = false',
|
|
27
|
+
'<Summary badge="Draft">Release overview</Summary>',
|
|
28
|
+
'<Stats><Stat label="Tests" value={42}/></Stats>',
|
|
29
|
+
'<Switch value={$enabled}>Notify me</Switch>',
|
|
30
|
+
'<Choices><Choice context="Review the release overview">Continue</Choice></Choices>',
|
|
31
|
+
'```'
|
|
32
|
+
].join('\n');
|
|
33
|
+
}
|
|
25
34
|
|
|
26
|
-
export function uiSessionNote(): string {
|
|
27
|
-
return `${
|
|
35
|
+
export function uiSessionNote({ fenceLanguage = UI_FENCE_LANGUAGE }: UiTextOptions = {}): string {
|
|
36
|
+
return `${uiSyntax(fenceLanguage)} Prose outside. Only named catalog props, no styling. $name = literal JSON; input value={$name}; Button action={@Set($name, literal)}, {@Reset($name)} or {@Reset()} changes local state only; braced expressions: @Count, @Filter(list,row,predicate), @Sum, @Join, @Round. Show/Each control children. Choice labels and precise context stay visible and send once. Image: attachment or generated="latest", never URL/path. No code execution. State resets on reload. Catalog (? means optional props): ${uiCompactCatalog()}\nExample:\n${uiExample(fenceLanguage)}\n`;
|
|
28
37
|
}
|
|
29
38
|
|
|
30
|
-
export function uiReferenceText(): string {
|
|
39
|
+
export function uiReferenceText({ fenceLanguage = UI_FENCE_LANGUAGE }: UiTextOptions = {}): string {
|
|
31
40
|
const groups = Object.entries(UI_GROUPS)
|
|
32
41
|
.map(([group, definition]) => {
|
|
33
42
|
const components = Object.entries(UI_CATALOG)
|
|
34
43
|
.filter(([, entry]) => entry.group === group)
|
|
35
44
|
.map(([name, entry]) => {
|
|
36
|
-
const props =
|
|
37
|
-
.map(([key, schema]) => `${key}${schema.isOptional() ? '?' : ''}`)
|
|
38
|
-
.join(', ');
|
|
45
|
+
const props = uiPropList(entry).join(', ');
|
|
39
46
|
return `${name}: ${entry.description}${props ? ` Props: ${props}.` : ''}`;
|
|
40
47
|
});
|
|
41
48
|
return [definition.description, definition.rules, ...components].join('\n');
|
|
42
49
|
})
|
|
43
50
|
.join('\n\n');
|
|
44
|
-
return `${
|
|
51
|
+
return `${uiSyntax(fenceLanguage)}\nExample:\n${uiExample(fenceLanguage)}\n\n${groups}`;
|
|
45
52
|
}
|
|
46
53
|
|
|
47
54
|
export function uiFallbackText(text: string, blocks: readonly UiBlock[]): string {
|