@assistant-ui/react-generative-ui 0.0.21 → 0.0.22
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/LICENSE +1 -1
- package/dist/JSONGenerativeUI.client.d.ts.map +1 -1
- package/dist/JSONGenerativeUI.client.js +30 -13
- package/dist/JSONGenerativeUI.client.js.map +1 -1
- package/dist/RadioGroupScope.d.ts +6 -0
- package/dist/RadioGroupScope.d.ts.map +1 -0
- package/dist/RadioGroupScope.js +25 -0
- package/dist/RadioGroupScope.js.map +1 -0
- package/dist/a2ui/BindingContext.d.ts +11 -0
- package/dist/a2ui/BindingContext.d.ts.map +1 -0
- package/dist/a2ui/BindingContext.js +12 -0
- package/dist/a2ui/BindingContext.js.map +1 -0
- package/dist/a2ui/PresentRenderer.d.ts +11 -0
- package/dist/a2ui/PresentRenderer.d.ts.map +1 -0
- package/dist/a2ui/PresentRenderer.js +39 -0
- package/dist/a2ui/PresentRenderer.js.map +1 -0
- package/dist/a2ui/convert.d.ts +6 -0
- package/dist/a2ui/convert.d.ts.map +1 -1
- package/dist/a2ui/convert.js +344 -83
- package/dist/a2ui/convert.js.map +1 -1
- package/dist/a2ui/dataModel.d.ts +9 -0
- package/dist/a2ui/dataModel.d.ts.map +1 -0
- package/dist/a2ui/dataModel.js +44 -0
- package/dist/a2ui/dataModel.js.map +1 -0
- package/dist/a2ui/reducer.d.ts +5 -0
- package/dist/a2ui/reducer.d.ts.map +1 -1
- package/dist/a2ui/reducer.js +3 -3
- package/dist/a2ui/reducer.js.map +1 -1
- package/dist/a2ui/types.d.ts +8 -2
- package/dist/a2ui/types.d.ts.map +1 -1
- package/dist/a2ui/types.js.map +1 -1
- package/dist/a2ui/valueFunctions.d.ts +16 -0
- package/dist/a2ui/valueFunctions.d.ts.map +1 -0
- package/dist/a2ui/valueFunctions.js +332 -0
- package/dist/a2ui/valueFunctions.js.map +1 -0
- package/dist/actionRegistry.d.ts +1 -1
- package/dist/actionRegistry.js.map +1 -1
- package/dist/answeredValues.d.ts +16 -0
- package/dist/answeredValues.d.ts.map +1 -0
- package/dist/answeredValues.js +37 -0
- package/dist/answeredValues.js.map +1 -0
- package/dist/constants.d.ts +2 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +3 -1
- package/dist/constants.js.map +1 -1
- package/dist/fieldReferences.d.ts +9 -0
- package/dist/fieldReferences.d.ts.map +1 -0
- package/dist/fieldReferences.js +53 -0
- package/dist/fieldReferences.js.map +1 -0
- package/dist/generativeUIToJSX.js +3 -3
- package/dist/generativeUIToJSX.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.server.js +2 -2
- package/dist/index.shared.js +2 -2
- package/dist/ir.d.ts +1 -1
- package/dist/ir.js.map +1 -1
- package/dist/renderGenerativeUI.d.ts.map +1 -1
- package/dist/renderGenerativeUI.js +12 -1
- package/dist/renderGenerativeUI.js.map +1 -1
- package/dist/slack/constants.d.ts +2 -0
- package/dist/slack/constants.d.ts.map +1 -1
- package/dist/slack/constants.js +3 -1
- package/dist/slack/constants.js.map +1 -1
- package/dist/slack/decodeBlockAction.d.ts +3 -1
- package/dist/slack/decodeBlockAction.d.ts.map +1 -1
- package/dist/slack/decodeBlockAction.js +58 -7
- package/dist/slack/decodeBlockAction.js.map +1 -1
- package/dist/slack/fromSlackBlocks.d.ts.map +1 -1
- package/dist/slack/fromSlackBlocks.js +20 -3
- package/dist/slack/fromSlackBlocks.js.map +1 -1
- package/dist/slack/toSlackBlocks.d.ts.map +1 -1
- package/dist/slack/toSlackBlocks.js +187 -19
- package/dist/slack/toSlackBlocks.js.map +1 -1
- package/dist/slack/types.d.ts +4 -0
- package/dist/slack/types.d.ts.map +1 -1
- package/dist/teams/constants.d.ts +1 -1
- package/dist/teams/constants.d.ts.map +1 -1
- package/dist/teams/constants.js +1 -1
- package/dist/teams/constants.js.map +1 -1
- package/dist/teams/decodeSubmitData.d.ts +4 -0
- package/dist/teams/decodeSubmitData.d.ts.map +1 -1
- package/dist/teams/decodeSubmitData.js +6 -1
- package/dist/teams/decodeSubmitData.js.map +1 -1
- package/dist/teams/toAdaptiveCard.d.ts +5 -4
- package/dist/teams/toAdaptiveCard.d.ts.map +1 -1
- package/dist/teams/toAdaptiveCard.js +56 -17
- package/dist/teams/toAdaptiveCard.js.map +1 -1
- package/dist/teams/types.d.ts +4 -1
- package/dist/teams/types.d.ts.map +1 -1
- package/dist/vocabulary/alert.d.ts +9 -2
- package/dist/vocabulary/alert.d.ts.map +1 -1
- package/dist/vocabulary/alert.js +116 -19
- package/dist/vocabulary/alert.js.map +1 -1
- package/dist/vocabulary/collectFormValues.d.ts +2 -1
- package/dist/vocabulary/collectFormValues.d.ts.map +1 -1
- package/dist/vocabulary/collectFormValues.js +10 -3
- package/dist/vocabulary/collectFormValues.js.map +1 -1
- package/dist/vocabulary/data.d.ts +22 -5
- package/dist/vocabulary/data.d.ts.map +1 -1
- package/dist/vocabulary/data.js +278 -83
- package/dist/vocabulary/data.js.map +1 -1
- package/dist/vocabulary/dispatch.d.ts +3 -2
- package/dist/vocabulary/dispatch.d.ts.map +1 -1
- package/dist/vocabulary/dispatch.js +33 -4
- package/dist/vocabulary/dispatch.js.map +1 -1
- package/dist/vocabulary/fact.d.ts +8 -1
- package/dist/vocabulary/fact.d.ts.map +1 -1
- package/dist/vocabulary/fact.js +37 -11
- package/dist/vocabulary/fact.js.map +1 -1
- package/dist/vocabulary/form.d.ts.map +1 -1
- package/dist/vocabulary/form.js +3 -3
- package/dist/vocabulary/form.js.map +1 -1
- package/dist/vocabulary/formatValue.d.ts +7 -0
- package/dist/vocabulary/formatValue.d.ts.map +1 -0
- package/dist/vocabulary/formatValue.js +76 -0
- package/dist/vocabulary/formatValue.js.map +1 -0
- package/dist/vocabulary/icon.d.ts +1 -1
- package/dist/vocabulary/interactive.d.ts +137 -8
- package/dist/vocabulary/interactive.d.ts.map +1 -1
- package/dist/vocabulary/interactive.js +404 -103
- package/dist/vocabulary/interactive.js.map +1 -1
- package/dist/vocabulary/layout.d.ts.map +1 -1
- package/dist/vocabulary/layout.js +4 -4
- package/dist/vocabulary/layout.js.map +1 -1
- package/dist/vocabulary/list.js +2 -2
- package/dist/vocabulary/list.js.map +1 -1
- package/dist/vocabulary/text.d.ts +1 -1
- package/package.json +6 -3
- package/src/JSONGenerativeUI.client.tsx +84 -40
- package/src/JSONGenerativeUI.test.tsx +964 -68
- package/src/RadioGroupScope.server.test.tsx +24 -0
- package/src/RadioGroupScope.tsx +34 -0
- package/src/a2ui/BindingContext.ts +26 -0
- package/src/a2ui/PresentRenderer.tsx +83 -0
- package/src/a2ui/convert.test.ts +1795 -179
- package/src/a2ui/convert.ts +629 -132
- package/src/a2ui/dataModel.ts +69 -0
- package/src/a2ui/reducer.test.ts +35 -0
- package/src/a2ui/reducer.ts +5 -4
- package/src/a2ui/types.ts +9 -2
- package/src/a2ui/valueFunctions.test.ts +271 -0
- package/src/a2ui/valueFunctions.ts +523 -0
- package/src/actionRegistry.ts +1 -1
- package/src/answeredValues.test.ts +33 -0
- package/src/answeredValues.tsx +70 -0
- package/src/constants.ts +4 -0
- package/src/fieldReferences.test.ts +66 -0
- package/src/fieldReferences.ts +83 -0
- package/src/generativeUIToJSX.test.ts +18 -0
- package/src/generativeUIToJSX.ts +4 -6
- package/src/index.server.test.ts +106 -0
- package/src/ir.ts +1 -1
- package/src/renderGenerativeUI.test.tsx +14 -5
- package/src/renderGenerativeUI.tsx +30 -1
- package/src/slack/a2uiSlack.test.ts +198 -0
- package/src/slack/constants.ts +3 -0
- package/src/slack/decodeBlockAction.test.ts +98 -0
- package/src/slack/decodeBlockAction.ts +110 -17
- package/src/slack/fromSlackBlocks.test.ts +222 -3
- package/src/slack/fromSlackBlocks.ts +53 -2
- package/src/slack/toSlackBlocks.test.ts +682 -3
- package/src/slack/toSlackBlocks.ts +374 -23
- package/src/slack/types.ts +4 -0
- package/src/teams/constants.ts +1 -1
- package/src/teams/decodeSubmitData.test.ts +33 -0
- package/src/teams/decodeSubmitData.ts +9 -1
- package/src/teams/toAdaptiveCard.test.ts +219 -0
- package/src/teams/toAdaptiveCard.ts +93 -19
- package/src/teams/types.ts +4 -1
- package/src/vocabulary/alert.test.tsx +288 -7
- package/src/vocabulary/alert.tsx +168 -26
- package/src/vocabulary/collectFormValues.test.ts +28 -1
- package/src/vocabulary/collectFormValues.ts +25 -5
- package/src/vocabulary/data.test.tsx +248 -6
- package/src/vocabulary/data.tsx +398 -100
- package/src/vocabulary/dispatch.ts +63 -2
- package/src/vocabulary/fact.test.tsx +67 -0
- package/src/vocabulary/fact.tsx +40 -9
- package/src/vocabulary/form.tsx +7 -2
- package/src/vocabulary/formatValue.test.ts +72 -0
- package/src/vocabulary/formatValue.ts +127 -0
- package/src/vocabulary/index.test.ts +2 -0
- package/src/vocabulary/interactive.dispatch.test.tsx +167 -79
- package/src/vocabulary/interactive.dom.test.tsx +1699 -0
- package/src/vocabulary/interactive.test.tsx +60 -4
- package/src/vocabulary/interactive.tsx +770 -151
- package/src/vocabulary/layout.test.tsx +6 -4
- package/src/vocabulary/layout.tsx +13 -2
- package/src/vocabulary/list.tsx +2 -2
|
@@ -11,6 +11,11 @@ import {
|
|
|
11
11
|
type NormalizedUIElement,
|
|
12
12
|
type NormalizedUINode,
|
|
13
13
|
} from "../ir";
|
|
14
|
+
import {
|
|
15
|
+
factTrend,
|
|
16
|
+
formatFactDelta,
|
|
17
|
+
formatValue,
|
|
18
|
+
} from "../vocabulary/formatValue";
|
|
14
19
|
import {
|
|
15
20
|
ACTION_ID_CAP,
|
|
16
21
|
ACTIONS_ELEMENT_CAP,
|
|
@@ -22,6 +27,7 @@ import {
|
|
|
22
27
|
CARD_TITLE_CAP,
|
|
23
28
|
CAROUSEL_CARD_CAP,
|
|
24
29
|
CAROUSEL_CARD_MIN,
|
|
30
|
+
CHECKBOX_OPTION_CAP,
|
|
25
31
|
CONTEXT_ELEMENT_CAP,
|
|
26
32
|
CONTEXT_TEXT_CAP,
|
|
27
33
|
DATA_TABLE_CHAR_BUDGET,
|
|
@@ -65,16 +71,27 @@ type ConversionContext = {
|
|
|
65
71
|
readonly blockCap: number;
|
|
66
72
|
readonly surface: "message" | "modal";
|
|
67
73
|
readonly warnings: SlackConversionWarning[];
|
|
74
|
+
readonly omittedPasswordNames: Set<string>;
|
|
75
|
+
fieldBlockIdSequence: number;
|
|
68
76
|
markdownCharacters: number;
|
|
69
77
|
markdownExhausted: boolean;
|
|
70
78
|
dataTableCharacters: number;
|
|
71
79
|
};
|
|
72
80
|
|
|
81
|
+
type FieldMapping = {
|
|
82
|
+
readonly actionId: string;
|
|
83
|
+
readonly name: string;
|
|
84
|
+
readonly component: string;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const SLACK_BLOCK_ID_CAP = 255;
|
|
88
|
+
|
|
73
89
|
const INTERACTIVE_TYPES = new Set([
|
|
74
90
|
"Button",
|
|
75
91
|
"Select",
|
|
76
92
|
"DatePicker",
|
|
77
93
|
"Checkbox",
|
|
94
|
+
"CheckboxGroup",
|
|
78
95
|
"RadioGroup",
|
|
79
96
|
]);
|
|
80
97
|
|
|
@@ -84,7 +101,12 @@ const INTERACTIVE_TYPES = new Set([
|
|
|
84
101
|
* block rather than an actions element, but a reshape loses them the same way,
|
|
85
102
|
* and a `Form` gets a Submit button whether or not it carries an action.
|
|
86
103
|
*/
|
|
87
|
-
const CONTROL_TYPES = new Set([
|
|
104
|
+
const CONTROL_TYPES = new Set([
|
|
105
|
+
...INTERACTIVE_TYPES,
|
|
106
|
+
"Input",
|
|
107
|
+
"Slider",
|
|
108
|
+
"Form",
|
|
109
|
+
]);
|
|
88
110
|
|
|
89
111
|
const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
90
112
|
|
|
@@ -94,6 +116,9 @@ const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
|
94
116
|
const asString = (value: unknown): string =>
|
|
95
117
|
typeof value === "string" ? value : "";
|
|
96
118
|
|
|
119
|
+
const asFiniteNumber = (value: unknown): number | undefined =>
|
|
120
|
+
typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
121
|
+
|
|
97
122
|
const warn = (
|
|
98
123
|
context: ConversionContext,
|
|
99
124
|
code: SlackConversionWarning["code"],
|
|
@@ -140,16 +165,103 @@ const plainText = (text: string): SlackPlainText => ({
|
|
|
140
165
|
text,
|
|
141
166
|
});
|
|
142
167
|
|
|
143
|
-
const
|
|
168
|
+
const serializeFieldBlockId = (
|
|
169
|
+
sequence: number,
|
|
170
|
+
fields: readonly FieldMapping[],
|
|
171
|
+
): string =>
|
|
172
|
+
`aui:${sequence}:${JSON.stringify(
|
|
173
|
+
fields.map(({ actionId, name, component }) =>
|
|
174
|
+
component === "Checkbox"
|
|
175
|
+
? [actionId, name, "Checkbox"]
|
|
176
|
+
: [actionId, name],
|
|
177
|
+
),
|
|
178
|
+
)}`;
|
|
179
|
+
|
|
180
|
+
const fieldBlockId = (
|
|
181
|
+
fields: readonly FieldMapping[],
|
|
182
|
+
context: ConversionContext,
|
|
183
|
+
): string | undefined => {
|
|
184
|
+
if (fields.length === 0) return undefined;
|
|
185
|
+
const sequence = context.fieldBlockIdSequence++;
|
|
186
|
+
const blockId = serializeFieldBlockId(sequence, fields);
|
|
187
|
+
if (blockId.length <= SLACK_BLOCK_ID_CAP) return blockId;
|
|
188
|
+
for (const field of fields) {
|
|
189
|
+
warn(
|
|
190
|
+
context,
|
|
191
|
+
"dropped",
|
|
192
|
+
field.component,
|
|
193
|
+
"name could not be mapped because its Slack block_id exceeded 255 characters.",
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
return undefined;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const collectOmittedPasswordNames = (
|
|
200
|
+
node: NormalizedUINode,
|
|
201
|
+
names: Set<string>,
|
|
202
|
+
depth = 0,
|
|
203
|
+
): void => {
|
|
204
|
+
if (depth > MAX_TRAVERSAL_DEPTH) return;
|
|
205
|
+
if (Array.isArray(node)) {
|
|
206
|
+
for (const child of node) {
|
|
207
|
+
collectOmittedPasswordNames(child, names, depth + 1);
|
|
208
|
+
}
|
|
209
|
+
} else if (isElement(node)) {
|
|
210
|
+
if (
|
|
211
|
+
node.type === "Input" &&
|
|
212
|
+
node.props["inputType"] === "password" &&
|
|
213
|
+
typeof node.props["name"] === "string"
|
|
214
|
+
) {
|
|
215
|
+
names.add(node.props["name"]);
|
|
216
|
+
}
|
|
217
|
+
if (node.children !== undefined) {
|
|
218
|
+
collectOmittedPasswordNames(node.children, names, depth + 1);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const withoutOmittedPasswordFallbacks = (
|
|
224
|
+
value: unknown,
|
|
225
|
+
names: ReadonlySet<string>,
|
|
226
|
+
depth = 0,
|
|
227
|
+
): unknown => {
|
|
228
|
+
if (depth > MAX_TRAVERSAL_DEPTH) {
|
|
229
|
+
throw new RangeError("Action payload nests too deeply to sanitize.");
|
|
230
|
+
}
|
|
231
|
+
if (Array.isArray(value)) {
|
|
232
|
+
return value.map((item) =>
|
|
233
|
+
withoutOmittedPasswordFallbacks(item, names, depth + 1),
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
if (!isRecord(value)) return value;
|
|
237
|
+
if (
|
|
238
|
+
typeof value["$field"] === "string" &&
|
|
239
|
+
names.has(value["$field"]) &&
|
|
240
|
+
Object.keys(value).every((key) => key === "$field" || key === "fallback")
|
|
241
|
+
) {
|
|
242
|
+
return { $field: value["$field"] };
|
|
243
|
+
}
|
|
244
|
+
return Object.fromEntries(
|
|
245
|
+
Object.entries(value).map(([key, item]) => [
|
|
246
|
+
key,
|
|
247
|
+
withoutOmittedPasswordFallbacks(item, names, depth + 1),
|
|
248
|
+
]),
|
|
249
|
+
);
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const actionValue = (
|
|
253
|
+
action: unknown,
|
|
254
|
+
context: ConversionContext,
|
|
255
|
+
): string | undefined => {
|
|
144
256
|
if (!isRecord(action)) return undefined;
|
|
145
257
|
const { type: _type, ...payload } = action;
|
|
146
258
|
if (Object.keys(payload).length === 0) return undefined;
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
259
|
+
const serialized = JSON.stringify(
|
|
260
|
+
context.omittedPasswordNames.size === 0
|
|
261
|
+
? payload
|
|
262
|
+
: withoutOmittedPasswordFallbacks(payload, context.omittedPasswordNames),
|
|
263
|
+
);
|
|
264
|
+
return serialized === "{}" ? undefined : serialized;
|
|
153
265
|
};
|
|
154
266
|
|
|
155
267
|
const buttonElement = (
|
|
@@ -159,8 +271,17 @@ const buttonElement = (
|
|
|
159
271
|
component: string,
|
|
160
272
|
context: ConversionContext,
|
|
161
273
|
): SlackButtonElement => {
|
|
162
|
-
|
|
163
|
-
|
|
274
|
+
let value: string | undefined;
|
|
275
|
+
try {
|
|
276
|
+
value = actionValue(action, context);
|
|
277
|
+
} catch {
|
|
278
|
+
warn(
|
|
279
|
+
context,
|
|
280
|
+
"dropped",
|
|
281
|
+
component,
|
|
282
|
+
"value was dropped because the action payload could not be serialized.",
|
|
283
|
+
);
|
|
284
|
+
}
|
|
164
285
|
if (value !== undefined && value.length > BUTTON_VALUE_CAP) {
|
|
165
286
|
warn(
|
|
166
287
|
context,
|
|
@@ -197,6 +318,7 @@ const optionFrom = (
|
|
|
197
318
|
) {
|
|
198
319
|
return undefined;
|
|
199
320
|
}
|
|
321
|
+
const description = asString(value["description"]);
|
|
200
322
|
return {
|
|
201
323
|
text: plainText(
|
|
202
324
|
clampText(
|
|
@@ -208,7 +330,20 @@ const optionFrom = (
|
|
|
208
330
|
),
|
|
209
331
|
),
|
|
210
332
|
value: value["value"],
|
|
211
|
-
|
|
333
|
+
...(description
|
|
334
|
+
? {
|
|
335
|
+
description: plainText(
|
|
336
|
+
clampText(
|
|
337
|
+
description,
|
|
338
|
+
INTERACTIVE_TEXT_CAP,
|
|
339
|
+
component,
|
|
340
|
+
"option description",
|
|
341
|
+
context,
|
|
342
|
+
),
|
|
343
|
+
),
|
|
344
|
+
}
|
|
345
|
+
: {}),
|
|
346
|
+
} as SlackOption;
|
|
212
347
|
};
|
|
213
348
|
|
|
214
349
|
const warnDroppedOptions = (
|
|
@@ -273,10 +408,18 @@ const toActionElement = (
|
|
|
273
408
|
"placeholder",
|
|
274
409
|
context,
|
|
275
410
|
);
|
|
411
|
+
const defaultValue = props["defaultValue"];
|
|
412
|
+
const initialOption =
|
|
413
|
+
typeof defaultValue === "string"
|
|
414
|
+
? options.find((option) => option.value === defaultValue)
|
|
415
|
+
: undefined;
|
|
276
416
|
return {
|
|
277
417
|
type: "static_select",
|
|
278
418
|
action_id: asActionId(action, "Select", context),
|
|
279
419
|
options,
|
|
420
|
+
...(initialOption !== undefined
|
|
421
|
+
? { initial_option: initialOption }
|
|
422
|
+
: {}),
|
|
280
423
|
...(placeholder ? { placeholder: plainText(placeholder) } : {}),
|
|
281
424
|
};
|
|
282
425
|
}
|
|
@@ -319,6 +462,46 @@ const toActionElement = (
|
|
|
319
462
|
: {}),
|
|
320
463
|
};
|
|
321
464
|
}
|
|
465
|
+
case "CheckboxGroup": {
|
|
466
|
+
const rawOptions = Array.isArray(props["options"])
|
|
467
|
+
? props["options"]
|
|
468
|
+
: [];
|
|
469
|
+
const { items: takenOptions, truncated } = copyBounded(
|
|
470
|
+
rawOptions,
|
|
471
|
+
CHECKBOX_OPTION_CAP,
|
|
472
|
+
);
|
|
473
|
+
if (truncated) {
|
|
474
|
+
warn(
|
|
475
|
+
context,
|
|
476
|
+
"clamped",
|
|
477
|
+
"CheckboxGroup",
|
|
478
|
+
`options were clamped to ${CHECKBOX_OPTION_CAP} entries.`,
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
const options = takenOptions
|
|
482
|
+
.map((option) => optionFrom(option, "CheckboxGroup", context))
|
|
483
|
+
.filter((option): option is SlackOption => option !== undefined);
|
|
484
|
+
warnDroppedOptions(
|
|
485
|
+
options.length,
|
|
486
|
+
takenOptions.length,
|
|
487
|
+
"CheckboxGroup",
|
|
488
|
+
context,
|
|
489
|
+
);
|
|
490
|
+
const defaultValue = Array.isArray(props["defaultValue"])
|
|
491
|
+
? props["defaultValue"]
|
|
492
|
+
: [];
|
|
493
|
+
const initialOptions = options.filter((option) =>
|
|
494
|
+
defaultValue.includes(option.value),
|
|
495
|
+
);
|
|
496
|
+
return {
|
|
497
|
+
type: "checkboxes",
|
|
498
|
+
action_id: asActionId(action, "CheckboxGroup", context),
|
|
499
|
+
options,
|
|
500
|
+
...(initialOptions.length > 0
|
|
501
|
+
? { initial_options: initialOptions }
|
|
502
|
+
: {}),
|
|
503
|
+
};
|
|
504
|
+
}
|
|
322
505
|
case "RadioGroup": {
|
|
323
506
|
const rawOptions = Array.isArray(props["options"])
|
|
324
507
|
? props["options"]
|
|
@@ -380,10 +563,15 @@ const convertFacts = (
|
|
|
380
563
|
const fields = facts.map((fact) => {
|
|
381
564
|
const label = asString(fact.props["label"]);
|
|
382
565
|
const value = asString(fact.props["value"]);
|
|
566
|
+
const delta = fact.props["delta"];
|
|
567
|
+
const deltaText =
|
|
568
|
+
typeof delta === "string"
|
|
569
|
+
? formatFactDelta(delta, factTrend(delta, fact.props["trend"]))
|
|
570
|
+
: undefined;
|
|
383
571
|
return {
|
|
384
572
|
type: "mrkdwn" as const,
|
|
385
573
|
text: clampText(
|
|
386
|
-
`*${label}*\n${value}`,
|
|
574
|
+
`*${label}*\n${deltaText === undefined ? value : `${value} (${deltaText})`}`,
|
|
387
575
|
FACT_FIELD_TEXT_CAP,
|
|
388
576
|
"Fact",
|
|
389
577
|
"field",
|
|
@@ -406,13 +594,87 @@ const convertActions = (
|
|
|
406
594
|
context: ConversionContext,
|
|
407
595
|
): SlackBlock[] => {
|
|
408
596
|
const converted = elements
|
|
409
|
-
.map((
|
|
410
|
-
|
|
597
|
+
.map((source) => ({
|
|
598
|
+
source,
|
|
599
|
+
element: toActionElement(source, context),
|
|
600
|
+
}))
|
|
601
|
+
.filter(
|
|
602
|
+
(
|
|
603
|
+
entry,
|
|
604
|
+
): entry is {
|
|
605
|
+
readonly source: NormalizedUIElement;
|
|
606
|
+
readonly element: SlackActionElement;
|
|
607
|
+
} => entry.element !== undefined,
|
|
608
|
+
);
|
|
411
609
|
const blocks: SlackBlock[] = [];
|
|
412
|
-
|
|
610
|
+
const groups: (typeof converted)[] = [];
|
|
611
|
+
let group: typeof converted = [];
|
|
612
|
+
let actionIds = new Set<string>();
|
|
613
|
+
const hasFieldName = (entry: (typeof converted)[number]): boolean => {
|
|
614
|
+
const name = entry.source.props["name"];
|
|
615
|
+
return typeof name === "string" && name.length > 0;
|
|
616
|
+
};
|
|
617
|
+
const fieldsFor = (entries: typeof converted): FieldMapping[] =>
|
|
618
|
+
entries.flatMap(({ source, element }) => {
|
|
619
|
+
const name = source.props["name"];
|
|
620
|
+
return typeof name === "string" && name.length > 0
|
|
621
|
+
? [{ actionId: element.action_id, name, component: source.type }]
|
|
622
|
+
: [];
|
|
623
|
+
});
|
|
624
|
+
for (const entry of converted) {
|
|
625
|
+
const repeatedMappedActionId =
|
|
626
|
+
actionIds.has(entry.element.action_id) &&
|
|
627
|
+
(hasFieldName(entry) ||
|
|
628
|
+
group.some(
|
|
629
|
+
(candidate) =>
|
|
630
|
+
candidate.element.action_id === entry.element.action_id &&
|
|
631
|
+
hasFieldName(candidate),
|
|
632
|
+
));
|
|
633
|
+
if (group.length === ACTIONS_ELEMENT_CAP || repeatedMappedActionId) {
|
|
634
|
+
groups.push(group);
|
|
635
|
+
group = [];
|
|
636
|
+
actionIds = new Set();
|
|
637
|
+
}
|
|
638
|
+
group.push(entry);
|
|
639
|
+
actionIds.add(entry.element.action_id);
|
|
640
|
+
}
|
|
641
|
+
if (group.length > 0) groups.push(group);
|
|
642
|
+
const mappedGroups: (typeof converted)[] = [];
|
|
643
|
+
let nextSequence = context.fieldBlockIdSequence;
|
|
644
|
+
for (const entries of groups) {
|
|
645
|
+
let current: typeof converted = [];
|
|
646
|
+
for (const entry of entries) {
|
|
647
|
+
const candidate = [...current, entry];
|
|
648
|
+
const fields = fieldsFor(candidate);
|
|
649
|
+
const blockId =
|
|
650
|
+
fields.length > 0
|
|
651
|
+
? serializeFieldBlockId(nextSequence, fields)
|
|
652
|
+
: undefined;
|
|
653
|
+
if (
|
|
654
|
+
current.length > 0 &&
|
|
655
|
+
fields.length > fieldsFor(current).length &&
|
|
656
|
+
blockId !== undefined &&
|
|
657
|
+
blockId.length > SLACK_BLOCK_ID_CAP
|
|
658
|
+
) {
|
|
659
|
+
mappedGroups.push(current);
|
|
660
|
+
if (fieldsFor(current).length > 0) nextSequence += 1;
|
|
661
|
+
current = [entry];
|
|
662
|
+
} else {
|
|
663
|
+
current = candidate;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
if (current.length > 0) {
|
|
667
|
+
mappedGroups.push(current);
|
|
668
|
+
if (fieldsFor(current).length > 0) nextSequence += 1;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
for (const group of mappedGroups) {
|
|
672
|
+
const fields = fieldsFor(group);
|
|
673
|
+
const blockId = fieldBlockId(fields, context);
|
|
413
674
|
blocks.push({
|
|
414
675
|
type: "actions",
|
|
415
|
-
|
|
676
|
+
...(blockId !== undefined ? { block_id: blockId } : {}),
|
|
677
|
+
elements: group.map(({ element }) => element),
|
|
416
678
|
});
|
|
417
679
|
}
|
|
418
680
|
return blocks;
|
|
@@ -914,12 +1176,18 @@ const convertCarousel = (
|
|
|
914
1176
|
return [buildCarouselBlock(cards)];
|
|
915
1177
|
};
|
|
916
1178
|
|
|
917
|
-
const toDataTableCell = (
|
|
918
|
-
|
|
1179
|
+
const toDataTableCell = (
|
|
1180
|
+
value: unknown,
|
|
1181
|
+
format: unknown,
|
|
1182
|
+
): SlackDataTableCell | undefined => {
|
|
1183
|
+
if (typeof value === "number" && format === undefined) {
|
|
919
1184
|
return { type: "raw_number", text: String(value) };
|
|
920
1185
|
}
|
|
921
1186
|
if (typeof value === "string" || typeof value === "boolean") {
|
|
922
|
-
return { type: "raw_text", text:
|
|
1187
|
+
return { type: "raw_text", text: formatValue(value, format) };
|
|
1188
|
+
}
|
|
1189
|
+
if (typeof value === "number") {
|
|
1190
|
+
return { type: "raw_text", text: formatValue(value, format) };
|
|
923
1191
|
}
|
|
924
1192
|
return undefined;
|
|
925
1193
|
};
|
|
@@ -982,8 +1250,13 @@ const convertTable = (
|
|
|
982
1250
|
? copyBounded(row, DATA_TABLE_COLUMN_CAP).items
|
|
983
1251
|
: []
|
|
984
1252
|
).map(
|
|
985
|
-
(value) =>
|
|
986
|
-
toDataTableCell(
|
|
1253
|
+
(value, index) =>
|
|
1254
|
+
toDataTableCell(
|
|
1255
|
+
value,
|
|
1256
|
+
isRecord(takenColumns[index])
|
|
1257
|
+
? takenColumns[index]["format"]
|
|
1258
|
+
: undefined,
|
|
1259
|
+
) ?? { type: "raw_text" as const, text: "" },
|
|
987
1260
|
),
|
|
988
1261
|
);
|
|
989
1262
|
const width = Math.max(
|
|
@@ -1115,9 +1388,68 @@ const convertElement = (
|
|
|
1115
1388
|
case "Select":
|
|
1116
1389
|
case "DatePicker":
|
|
1117
1390
|
case "Checkbox":
|
|
1391
|
+
case "CheckboxGroup":
|
|
1118
1392
|
case "RadioGroup":
|
|
1119
1393
|
return convertActions([element], context);
|
|
1394
|
+
case "Slider": {
|
|
1395
|
+
const label = clampText(
|
|
1396
|
+
asString(props["label"]),
|
|
1397
|
+
INPUT_LABEL_CAP,
|
|
1398
|
+
"Slider",
|
|
1399
|
+
"label",
|
|
1400
|
+
context,
|
|
1401
|
+
);
|
|
1402
|
+
const min = asFiniteNumber(props["min"]);
|
|
1403
|
+
const max = asFiniteNumber(props["max"]);
|
|
1404
|
+
const step = asFiniteNumber(props["step"]);
|
|
1405
|
+
const defaultValue = asFiniteNumber(props["defaultValue"]);
|
|
1406
|
+
const actionId = asActionId(element.action, "Slider", context);
|
|
1407
|
+
const name = props["name"];
|
|
1408
|
+
const blockId = fieldBlockId(
|
|
1409
|
+
typeof name === "string" && name.length > 0
|
|
1410
|
+
? [{ actionId, name, component: "Slider" }]
|
|
1411
|
+
: [],
|
|
1412
|
+
context,
|
|
1413
|
+
);
|
|
1414
|
+
return [
|
|
1415
|
+
{
|
|
1416
|
+
type: "input",
|
|
1417
|
+
...(blockId !== undefined ? { block_id: blockId } : {}),
|
|
1418
|
+
label: plainText(label),
|
|
1419
|
+
element: {
|
|
1420
|
+
type: "number_input",
|
|
1421
|
+
action_id: actionId,
|
|
1422
|
+
...(min !== undefined ? { min_value: min } : {}),
|
|
1423
|
+
...(max !== undefined ? { max_value: max } : {}),
|
|
1424
|
+
...(defaultValue !== undefined
|
|
1425
|
+
? { initial_value: defaultValue }
|
|
1426
|
+
: {}),
|
|
1427
|
+
...([step, min, max, defaultValue].some(
|
|
1428
|
+
(value) => value !== undefined && !Number.isInteger(value),
|
|
1429
|
+
)
|
|
1430
|
+
? { is_decimal_allowed: true }
|
|
1431
|
+
: {}),
|
|
1432
|
+
},
|
|
1433
|
+
} as unknown as SlackBlock,
|
|
1434
|
+
];
|
|
1435
|
+
}
|
|
1120
1436
|
case "Input": {
|
|
1437
|
+
if (props["inputType"] === "password") {
|
|
1438
|
+
warn(
|
|
1439
|
+
context,
|
|
1440
|
+
"dropped",
|
|
1441
|
+
"Input",
|
|
1442
|
+
"Password input was replaced by a Slack omission note.",
|
|
1443
|
+
);
|
|
1444
|
+
return [
|
|
1445
|
+
{
|
|
1446
|
+
type: "context",
|
|
1447
|
+
elements: [
|
|
1448
|
+
{ type: "mrkdwn", text: "Password input omitted on Slack." },
|
|
1449
|
+
],
|
|
1450
|
+
},
|
|
1451
|
+
];
|
|
1452
|
+
}
|
|
1121
1453
|
const label = clampText(
|
|
1122
1454
|
asString(props["label"]),
|
|
1123
1455
|
INPUT_LABEL_CAP,
|
|
@@ -1132,14 +1464,30 @@ const convertElement = (
|
|
|
1132
1464
|
"placeholder",
|
|
1133
1465
|
context,
|
|
1134
1466
|
);
|
|
1467
|
+
const actionId = asActionId(element.action, "Input", context);
|
|
1468
|
+
const name = props["name"];
|
|
1469
|
+
const blockId = fieldBlockId(
|
|
1470
|
+
typeof name === "string" && name.length > 0
|
|
1471
|
+
? [{ actionId, name, component: "Input" }]
|
|
1472
|
+
: [],
|
|
1473
|
+
context,
|
|
1474
|
+
);
|
|
1475
|
+
const defaultValue = props["defaultValue"];
|
|
1135
1476
|
return [
|
|
1136
1477
|
{
|
|
1137
1478
|
type: "input",
|
|
1479
|
+
...(blockId !== undefined ? { block_id: blockId } : {}),
|
|
1138
1480
|
label: plainText(label),
|
|
1139
1481
|
element: {
|
|
1140
1482
|
type: "plain_text_input",
|
|
1141
|
-
action_id:
|
|
1142
|
-
...(props["multiline"] === true
|
|
1483
|
+
action_id: actionId,
|
|
1484
|
+
...(props["multiline"] === true &&
|
|
1485
|
+
(props["inputType"] === undefined || props["inputType"] === "text")
|
|
1486
|
+
? { multiline: true }
|
|
1487
|
+
: {}),
|
|
1488
|
+
...(typeof defaultValue === "string" && defaultValue
|
|
1489
|
+
? { initial_value: defaultValue }
|
|
1490
|
+
: {}),
|
|
1143
1491
|
...(placeholder ? { placeholder: plainText(placeholder) } : {}),
|
|
1144
1492
|
},
|
|
1145
1493
|
},
|
|
@@ -1340,6 +1688,8 @@ export function toSlackBlocks(
|
|
|
1340
1688
|
blockCap: surface === "modal" ? MODAL_BLOCK_CAP : MESSAGE_BLOCK_CAP,
|
|
1341
1689
|
surface,
|
|
1342
1690
|
warnings: [],
|
|
1691
|
+
omittedPasswordNames: new Set(),
|
|
1692
|
+
fieldBlockIdSequence: 0,
|
|
1343
1693
|
markdownCharacters: 0,
|
|
1344
1694
|
markdownExhausted: false,
|
|
1345
1695
|
dataTableCharacters: 0,
|
|
@@ -1349,6 +1699,7 @@ export function toSlackBlocks(
|
|
|
1349
1699
|
warn(context, "clamped", "Root", clampReasonDetail(reason)),
|
|
1350
1700
|
);
|
|
1351
1701
|
const { root } = normalizeSpec(bounded as never);
|
|
1702
|
+
collectOmittedPasswordNames(root, context.omittedPasswordNames);
|
|
1352
1703
|
const converted = convertSequence(root, context, 0);
|
|
1353
1704
|
if (converted.length <= context.blockCap) {
|
|
1354
1705
|
return { blocks: converted, warnings: context.warnings };
|
package/src/slack/types.ts
CHANGED
|
@@ -35,6 +35,7 @@ export interface SlackStaticSelectElement {
|
|
|
35
35
|
readonly type: "static_select";
|
|
36
36
|
readonly action_id: string;
|
|
37
37
|
readonly options: readonly SlackOption[];
|
|
38
|
+
readonly initial_option?: SlackOption;
|
|
38
39
|
readonly placeholder?: SlackPlainText;
|
|
39
40
|
}
|
|
40
41
|
|
|
@@ -66,6 +67,7 @@ export interface SlackPlainTextInputElement {
|
|
|
66
67
|
readonly type: "plain_text_input";
|
|
67
68
|
readonly action_id: string;
|
|
68
69
|
readonly multiline?: boolean;
|
|
70
|
+
readonly initial_value?: string;
|
|
69
71
|
readonly placeholder?: SlackPlainText;
|
|
70
72
|
}
|
|
71
73
|
|
|
@@ -112,12 +114,14 @@ export interface SlackDividerBlock {
|
|
|
112
114
|
/** A Slack actions block. */
|
|
113
115
|
export interface SlackActionsBlock {
|
|
114
116
|
readonly type: "actions";
|
|
117
|
+
readonly block_id?: string;
|
|
115
118
|
readonly elements: readonly SlackActionElement[];
|
|
116
119
|
}
|
|
117
120
|
|
|
118
121
|
/** A Slack input block. */
|
|
119
122
|
export interface SlackInputBlock {
|
|
120
123
|
readonly type: "input";
|
|
124
|
+
readonly block_id?: string;
|
|
121
125
|
readonly label: SlackPlainText;
|
|
122
126
|
readonly element: SlackPlainTextInputElement;
|
|
123
127
|
}
|
package/src/teams/constants.ts
CHANGED
|
@@ -29,7 +29,7 @@ export const TABLE_ROW_CAP = 100;
|
|
|
29
29
|
/** The maximum number of columns in a Table. */
|
|
30
30
|
export const TABLE_COLUMN_CAP = 20;
|
|
31
31
|
|
|
32
|
-
/** The maximum number of choices in a Select or
|
|
32
|
+
/** The maximum number of choices in a Select, RadioGroup, or CheckboxGroup. */
|
|
33
33
|
export const CHOICE_OPTION_CAP = 100;
|
|
34
34
|
|
|
35
35
|
/** The soft byte-count budget for a serialized card, past which a warning notes Teams' 100 KB bot message limit (the card is never truncated to fit it). */
|
|
@@ -23,6 +23,39 @@ describe("decodeSubmitData", () => {
|
|
|
23
23
|
});
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
+
it("keeps a number input as a number", () => {
|
|
27
|
+
const value = {
|
|
28
|
+
aui: { type: "set_quantity" },
|
|
29
|
+
quantity: 3.5,
|
|
30
|
+
};
|
|
31
|
+
expect(decodeSubmitData(value)).toEqual({
|
|
32
|
+
type: "set_quantity",
|
|
33
|
+
$input: { quantity: 3.5 },
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("resolves $field references in the payload from same-card inputs, else their fallback", () => {
|
|
38
|
+
const value = {
|
|
39
|
+
aui: {
|
|
40
|
+
type: "save",
|
|
41
|
+
payload: {
|
|
42
|
+
note: { $field: "note" },
|
|
43
|
+
form: { plan: [{ $field: "plan" }], missing: { $field: "gone" } },
|
|
44
|
+
renamed: { $field: "aui", fallback: "kept" },
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
note: "Ship it",
|
|
48
|
+
plan: "pro",
|
|
49
|
+
};
|
|
50
|
+
expect(decodeSubmitData(value)).toEqual({
|
|
51
|
+
type: "save",
|
|
52
|
+
note: "Ship it",
|
|
53
|
+
form: { plan: ["pro"] },
|
|
54
|
+
renamed: "kept",
|
|
55
|
+
$input: { note: "Ship it", plan: "pro" },
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
26
59
|
it("omits $input when there are no other top-level keys", () => {
|
|
27
60
|
expect(decodeSubmitData({ aui: { type: "approve" } })).toEqual({
|
|
28
61
|
type: "approve",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveFieldReferences } from "../fieldReferences";
|
|
1
2
|
import type { Action } from "../ir";
|
|
2
3
|
|
|
3
4
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
@@ -9,6 +10,10 @@ const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
|
9
10
|
* the resume action under. Adaptive Cards fold every other same-card input's
|
|
10
11
|
* value into the same submit object keyed by its `id`, so every top-level key
|
|
11
12
|
* besides `aui` is collected into `$input` (omitted when there are none).
|
|
13
|
+
* Each `{ "$field": name }` reference inside the payload resolves to the
|
|
14
|
+
* same-card input value with that id, as the string Adaptive Cards submitted,
|
|
15
|
+
* and a reference with no such input resolves to its `fallback`, or is dropped
|
|
16
|
+
* without one.
|
|
12
17
|
* `aui` is reserved for the envelope: `toAdaptiveCard` renames any input
|
|
13
18
|
* whose id would collide to an unused id derived from it before encoding, so
|
|
14
19
|
* a same-card input value can never land on this key. `$input` and `type`
|
|
@@ -47,7 +52,10 @@ export function decodeSubmitData(value: unknown): Action | undefined {
|
|
|
47
52
|
inputEntries.length > 0 ? Object.fromEntries(inputEntries) : undefined;
|
|
48
53
|
|
|
49
54
|
return {
|
|
50
|
-
...
|
|
55
|
+
...(resolveFieldReferences(
|
|
56
|
+
Object.fromEntries(payloadEntries),
|
|
57
|
+
input ?? {},
|
|
58
|
+
) as Record<string, unknown>),
|
|
51
59
|
type,
|
|
52
60
|
...(input !== undefined ? { $input: input } : {}),
|
|
53
61
|
};
|