@assistant-ui/react-generative-ui 0.0.21 → 0.0.23
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 +363 -85
- 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 +3 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +4 -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 +96 -7
- package/dist/slack/decodeBlockAction.js.map +1 -1
- package/dist/slack/fromSlackBlocks.d.ts.map +1 -1
- package/dist/slack/fromSlackBlocks.js +43 -3
- package/dist/slack/fromSlackBlocks.js.map +1 -1
- package/dist/slack/toSlackBlocks.d.ts.map +1 -1
- package/dist/slack/toSlackBlocks.js +244 -20
- package/dist/slack/toSlackBlocks.js.map +1 -1
- package/dist/slack/types.d.ts +24 -1
- package/dist/slack/types.d.ts.map +1 -1
- package/dist/slack.d.ts +1 -1
- package/dist/slack.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 +29 -3
- package/dist/teams/decodeSubmitData.js.map +1 -1
- package/dist/teams/temporalId.d.ts +5 -0
- package/dist/teams/temporalId.d.ts.map +1 -0
- package/dist/teams/temporalId.js +28 -0
- package/dist/teams/temporalId.js.map +1 -0
- package/dist/teams/toAdaptiveCard.d.ts +5 -4
- package/dist/teams/toAdaptiveCard.d.ts.map +1 -1
- package/dist/teams/toAdaptiveCard.js +136 -18
- package/dist/teams/toAdaptiveCard.js.map +1 -1
- package/dist/teams/toTeamsAttachments.d.ts.map +1 -1
- package/dist/teams/toTeamsAttachments.js.map +1 -1
- package/dist/teams/types.d.ts +23 -2
- package/dist/teams/types.d.ts.map +1 -1
- package/dist/teams.d.ts +1 -1
- package/dist/teams.d.ts.map +1 -1
- package/dist/temporal.d.ts +33 -0
- package/dist/temporal.d.ts.map +1 -0
- package/dist/temporal.js +152 -0
- package/dist/temporal.js.map +1 -0
- 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 +15 -4
- 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 +143 -8
- package/dist/vocabulary/interactive.d.ts.map +1 -1
- package/dist/vocabulary/interactive.js +458 -107
- 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 +8 -5
- 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 +2054 -229
- package/src/a2ui/convert.ts +665 -134
- 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 +5 -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/datePicker.test.ts +612 -0
- package/src/slack/decodeBlockAction.test.ts +98 -0
- package/src/slack/decodeBlockAction.ts +182 -16
- package/src/slack/fromSlackBlocks.test.ts +222 -3
- package/src/slack/fromSlackBlocks.ts +82 -2
- package/src/slack/toSlackBlocks.test.ts +682 -3
- package/src/slack/toSlackBlocks.ts +463 -25
- package/src/slack/types.ts +28 -0
- package/src/slack.ts +2 -0
- package/src/teams/constants.ts +1 -1
- package/src/teams/decodeSubmitData.test.ts +249 -0
- package/src/teams/decodeSubmitData.ts +57 -4
- package/src/teams/temporal.test.ts +766 -0
- package/src/teams/temporalId.ts +56 -0
- package/src/teams/toAdaptiveCard.test.ts +219 -0
- package/src/teams/toAdaptiveCard.ts +259 -21
- package/src/teams/toTeamsAttachments.ts +4 -1
- package/src/teams/types.ts +21 -1
- package/src/teams.ts +1 -0
- package/src/temporal.test.ts +319 -0
- package/src/temporal.ts +259 -0
- package/src/vocabulary/alert.test.tsx +288 -7
- package/src/vocabulary/alert.tsx +168 -26
- package/src/vocabulary/collectFormValues.test.ts +225 -1
- package/src/vocabulary/collectFormValues.ts +54 -5
- package/src/vocabulary/data.test.tsx +248 -6
- package/src/vocabulary/data.tsx +398 -100
- package/src/vocabulary/datepicker.dom.test.tsx +1148 -0
- 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 +1747 -0
- package/src/vocabulary/interactive.test.tsx +60 -4
- package/src/vocabulary/interactive.tsx +910 -155
- package/src/vocabulary/layout.test.tsx +6 -4
- package/src/vocabulary/layout.tsx +13 -2
- package/src/vocabulary/list.tsx +2 -2
|
@@ -6,11 +6,17 @@ import {
|
|
|
6
6
|
import { copyBounded } from "../convert/copyBounded";
|
|
7
7
|
import { isElement } from "../convert/isElement";
|
|
8
8
|
import { takeRun } from "../convert/takeRun";
|
|
9
|
+
import { classifyTemporal, splitTemporalMinutes } from "../temporal";
|
|
9
10
|
import {
|
|
10
11
|
normalizeSpec,
|
|
11
12
|
type NormalizedUIElement,
|
|
12
13
|
type NormalizedUINode,
|
|
13
14
|
} from "../ir";
|
|
15
|
+
import {
|
|
16
|
+
factTrend,
|
|
17
|
+
formatFactDelta,
|
|
18
|
+
formatValue,
|
|
19
|
+
} from "../vocabulary/formatValue";
|
|
14
20
|
import {
|
|
15
21
|
ACTION_ID_CAP,
|
|
16
22
|
ACTIONS_ELEMENT_CAP,
|
|
@@ -22,6 +28,7 @@ import {
|
|
|
22
28
|
CARD_TITLE_CAP,
|
|
23
29
|
CAROUSEL_CARD_CAP,
|
|
24
30
|
CAROUSEL_CARD_MIN,
|
|
31
|
+
CHECKBOX_OPTION_CAP,
|
|
25
32
|
CONTEXT_ELEMENT_CAP,
|
|
26
33
|
CONTEXT_TEXT_CAP,
|
|
27
34
|
DATA_TABLE_CHAR_BUDGET,
|
|
@@ -47,6 +54,7 @@ import {
|
|
|
47
54
|
buildDataTableBlock,
|
|
48
55
|
} from "./constants";
|
|
49
56
|
import type {
|
|
57
|
+
FieldMapping,
|
|
50
58
|
SlackActionElement,
|
|
51
59
|
SlackBlock,
|
|
52
60
|
SlackButtonElement,
|
|
@@ -65,16 +73,22 @@ type ConversionContext = {
|
|
|
65
73
|
readonly blockCap: number;
|
|
66
74
|
readonly surface: "message" | "modal";
|
|
67
75
|
readonly warnings: SlackConversionWarning[];
|
|
76
|
+
readonly omittedPasswordNames: Set<string>;
|
|
77
|
+
fieldBlockIdSequence: number;
|
|
78
|
+
dateTimePairSequence: number;
|
|
68
79
|
markdownCharacters: number;
|
|
69
80
|
markdownExhausted: boolean;
|
|
70
81
|
dataTableCharacters: number;
|
|
71
82
|
};
|
|
72
83
|
|
|
84
|
+
const SLACK_BLOCK_ID_CAP = 255;
|
|
85
|
+
|
|
73
86
|
const INTERACTIVE_TYPES = new Set([
|
|
74
87
|
"Button",
|
|
75
88
|
"Select",
|
|
76
89
|
"DatePicker",
|
|
77
90
|
"Checkbox",
|
|
91
|
+
"CheckboxGroup",
|
|
78
92
|
"RadioGroup",
|
|
79
93
|
]);
|
|
80
94
|
|
|
@@ -84,7 +98,12 @@ const INTERACTIVE_TYPES = new Set([
|
|
|
84
98
|
* block rather than an actions element, but a reshape loses them the same way,
|
|
85
99
|
* and a `Form` gets a Submit button whether or not it carries an action.
|
|
86
100
|
*/
|
|
87
|
-
const CONTROL_TYPES = new Set([
|
|
101
|
+
const CONTROL_TYPES = new Set([
|
|
102
|
+
...INTERACTIVE_TYPES,
|
|
103
|
+
"Input",
|
|
104
|
+
"Slider",
|
|
105
|
+
"Form",
|
|
106
|
+
]);
|
|
88
107
|
|
|
89
108
|
const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
90
109
|
|
|
@@ -94,6 +113,9 @@ const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
|
94
113
|
const asString = (value: unknown): string =>
|
|
95
114
|
typeof value === "string" ? value : "";
|
|
96
115
|
|
|
116
|
+
const asFiniteNumber = (value: unknown): number | undefined =>
|
|
117
|
+
typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
118
|
+
|
|
97
119
|
const warn = (
|
|
98
120
|
context: ConversionContext,
|
|
99
121
|
code: SlackConversionWarning["code"],
|
|
@@ -140,16 +162,114 @@ const plainText = (text: string): SlackPlainText => ({
|
|
|
140
162
|
text,
|
|
141
163
|
});
|
|
142
164
|
|
|
143
|
-
const
|
|
165
|
+
const serializeFieldBlockId = (
|
|
166
|
+
sequence: number,
|
|
167
|
+
fields: readonly FieldMapping[],
|
|
168
|
+
): string =>
|
|
169
|
+
`aui:${sequence}:${JSON.stringify(
|
|
170
|
+
fields.map(
|
|
171
|
+
({ actionId, name, component, inputType, offset, part, pairId }) =>
|
|
172
|
+
inputType !== undefined
|
|
173
|
+
? [
|
|
174
|
+
actionId,
|
|
175
|
+
name,
|
|
176
|
+
component,
|
|
177
|
+
inputType,
|
|
178
|
+
offset ?? null,
|
|
179
|
+
part ?? null,
|
|
180
|
+
pairId ?? null,
|
|
181
|
+
]
|
|
182
|
+
: component === "Checkbox"
|
|
183
|
+
? [actionId, name, "Checkbox"]
|
|
184
|
+
: [actionId, name],
|
|
185
|
+
),
|
|
186
|
+
)}`;
|
|
187
|
+
|
|
188
|
+
const fieldBlockId = (
|
|
189
|
+
fields: readonly FieldMapping[],
|
|
190
|
+
context: ConversionContext,
|
|
191
|
+
): string | undefined => {
|
|
192
|
+
if (fields.length === 0) return undefined;
|
|
193
|
+
const sequence = context.fieldBlockIdSequence++;
|
|
194
|
+
const blockId = serializeFieldBlockId(sequence, fields);
|
|
195
|
+
if (blockId.length <= SLACK_BLOCK_ID_CAP) return blockId;
|
|
196
|
+
for (const field of fields) {
|
|
197
|
+
warn(
|
|
198
|
+
context,
|
|
199
|
+
"dropped",
|
|
200
|
+
field.component,
|
|
201
|
+
"name could not be mapped because its Slack block_id exceeded 255 characters.",
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
return undefined;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const collectOmittedPasswordNames = (
|
|
208
|
+
node: NormalizedUINode,
|
|
209
|
+
names: Set<string>,
|
|
210
|
+
depth = 0,
|
|
211
|
+
): void => {
|
|
212
|
+
if (depth > MAX_TRAVERSAL_DEPTH) return;
|
|
213
|
+
if (Array.isArray(node)) {
|
|
214
|
+
for (const child of node) {
|
|
215
|
+
collectOmittedPasswordNames(child, names, depth + 1);
|
|
216
|
+
}
|
|
217
|
+
} else if (isElement(node)) {
|
|
218
|
+
if (
|
|
219
|
+
node.type === "Input" &&
|
|
220
|
+
node.props["inputType"] === "password" &&
|
|
221
|
+
typeof node.props["name"] === "string"
|
|
222
|
+
) {
|
|
223
|
+
names.add(node.props["name"]);
|
|
224
|
+
}
|
|
225
|
+
if (node.children !== undefined) {
|
|
226
|
+
collectOmittedPasswordNames(node.children, names, depth + 1);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const withoutOmittedPasswordFallbacks = (
|
|
232
|
+
value: unknown,
|
|
233
|
+
names: ReadonlySet<string>,
|
|
234
|
+
depth = 0,
|
|
235
|
+
): unknown => {
|
|
236
|
+
if (depth > MAX_TRAVERSAL_DEPTH) {
|
|
237
|
+
throw new RangeError("Action payload nests too deeply to sanitize.");
|
|
238
|
+
}
|
|
239
|
+
if (Array.isArray(value)) {
|
|
240
|
+
return value.map((item) =>
|
|
241
|
+
withoutOmittedPasswordFallbacks(item, names, depth + 1),
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
if (!isRecord(value)) return value;
|
|
245
|
+
if (
|
|
246
|
+
typeof value["$field"] === "string" &&
|
|
247
|
+
names.has(value["$field"]) &&
|
|
248
|
+
Object.keys(value).every((key) => key === "$field" || key === "fallback")
|
|
249
|
+
) {
|
|
250
|
+
return { $field: value["$field"] };
|
|
251
|
+
}
|
|
252
|
+
return Object.fromEntries(
|
|
253
|
+
Object.entries(value).map(([key, item]) => [
|
|
254
|
+
key,
|
|
255
|
+
withoutOmittedPasswordFallbacks(item, names, depth + 1),
|
|
256
|
+
]),
|
|
257
|
+
);
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const actionValue = (
|
|
261
|
+
action: unknown,
|
|
262
|
+
context: ConversionContext,
|
|
263
|
+
): string | undefined => {
|
|
144
264
|
if (!isRecord(action)) return undefined;
|
|
145
265
|
const { type: _type, ...payload } = action;
|
|
146
266
|
if (Object.keys(payload).length === 0) return undefined;
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
267
|
+
const serialized = JSON.stringify(
|
|
268
|
+
context.omittedPasswordNames.size === 0
|
|
269
|
+
? payload
|
|
270
|
+
: withoutOmittedPasswordFallbacks(payload, context.omittedPasswordNames),
|
|
271
|
+
);
|
|
272
|
+
return serialized === "{}" ? undefined : serialized;
|
|
153
273
|
};
|
|
154
274
|
|
|
155
275
|
const buttonElement = (
|
|
@@ -159,8 +279,17 @@ const buttonElement = (
|
|
|
159
279
|
component: string,
|
|
160
280
|
context: ConversionContext,
|
|
161
281
|
): SlackButtonElement => {
|
|
162
|
-
|
|
163
|
-
|
|
282
|
+
let value: string | undefined;
|
|
283
|
+
try {
|
|
284
|
+
value = actionValue(action, context);
|
|
285
|
+
} catch {
|
|
286
|
+
warn(
|
|
287
|
+
context,
|
|
288
|
+
"dropped",
|
|
289
|
+
component,
|
|
290
|
+
"value was dropped because the action payload could not be serialized.",
|
|
291
|
+
);
|
|
292
|
+
}
|
|
164
293
|
if (value !== undefined && value.length > BUTTON_VALUE_CAP) {
|
|
165
294
|
warn(
|
|
166
295
|
context,
|
|
@@ -197,6 +326,7 @@ const optionFrom = (
|
|
|
197
326
|
) {
|
|
198
327
|
return undefined;
|
|
199
328
|
}
|
|
329
|
+
const description = asString(value["description"]);
|
|
200
330
|
return {
|
|
201
331
|
text: plainText(
|
|
202
332
|
clampText(
|
|
@@ -208,7 +338,20 @@ const optionFrom = (
|
|
|
208
338
|
),
|
|
209
339
|
),
|
|
210
340
|
value: value["value"],
|
|
211
|
-
|
|
341
|
+
...(description
|
|
342
|
+
? {
|
|
343
|
+
description: plainText(
|
|
344
|
+
clampText(
|
|
345
|
+
description,
|
|
346
|
+
INTERACTIVE_TEXT_CAP,
|
|
347
|
+
component,
|
|
348
|
+
"option description",
|
|
349
|
+
context,
|
|
350
|
+
),
|
|
351
|
+
),
|
|
352
|
+
}
|
|
353
|
+
: {}),
|
|
354
|
+
} as SlackOption;
|
|
212
355
|
};
|
|
213
356
|
|
|
214
357
|
const warnDroppedOptions = (
|
|
@@ -230,7 +373,7 @@ const warnDroppedOptions = (
|
|
|
230
373
|
const toActionElement = (
|
|
231
374
|
element: NormalizedUIElement,
|
|
232
375
|
context: ConversionContext,
|
|
233
|
-
): SlackActionElement | undefined => {
|
|
376
|
+
): SlackActionElement | SlackActionElement[] | undefined => {
|
|
234
377
|
const { props, action } = element;
|
|
235
378
|
switch (element.type) {
|
|
236
379
|
case "Button":
|
|
@@ -273,15 +416,86 @@ const toActionElement = (
|
|
|
273
416
|
"placeholder",
|
|
274
417
|
context,
|
|
275
418
|
);
|
|
419
|
+
const defaultValue = props["defaultValue"];
|
|
420
|
+
const initialOption =
|
|
421
|
+
typeof defaultValue === "string"
|
|
422
|
+
? options.find((option) => option.value === defaultValue)
|
|
423
|
+
: undefined;
|
|
276
424
|
return {
|
|
277
425
|
type: "static_select",
|
|
278
426
|
action_id: asActionId(action, "Select", context),
|
|
279
427
|
options,
|
|
428
|
+
...(initialOption !== undefined
|
|
429
|
+
? { initial_option: initialOption }
|
|
430
|
+
: {}),
|
|
280
431
|
...(placeholder ? { placeholder: plainText(placeholder) } : {}),
|
|
281
432
|
};
|
|
282
433
|
}
|
|
283
434
|
case "DatePicker": {
|
|
284
435
|
const rawDate = asString(props["value"]);
|
|
436
|
+
const inputType = props["inputType"];
|
|
437
|
+
if (inputType === "time" || inputType === "datetime") {
|
|
438
|
+
const actionId = asActionId(action, "DatePicker", context);
|
|
439
|
+
const temporal = classifyTemporal(rawDate);
|
|
440
|
+
const parts = splitTemporalMinutes(rawDate);
|
|
441
|
+
if (
|
|
442
|
+
(inputType === "time" && temporal.kind === "time") ||
|
|
443
|
+
(inputType === "datetime" && temporal.kind === "floating")
|
|
444
|
+
) {
|
|
445
|
+
if (parts?.droppedPrecision) {
|
|
446
|
+
warn(
|
|
447
|
+
context,
|
|
448
|
+
"dropped",
|
|
449
|
+
"DatePicker",
|
|
450
|
+
"Seconds were dropped because Slack timepickers only support minute precision.",
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
const timePicker: SlackActionElement = {
|
|
454
|
+
type: "timepicker",
|
|
455
|
+
action_id: actionId,
|
|
456
|
+
initial_time: parts!.time,
|
|
457
|
+
};
|
|
458
|
+
return inputType === "time"
|
|
459
|
+
? timePicker
|
|
460
|
+
: [
|
|
461
|
+
{
|
|
462
|
+
type: "datepicker",
|
|
463
|
+
action_id: actionId,
|
|
464
|
+
initial_date: parts!.date!,
|
|
465
|
+
},
|
|
466
|
+
timePicker,
|
|
467
|
+
];
|
|
468
|
+
}
|
|
469
|
+
if (inputType === "datetime" && temporal.kind === "instant") {
|
|
470
|
+
if (
|
|
471
|
+
temporal.epochMs % 1000 !== 0 ||
|
|
472
|
+
/[1-9]/.test(temporal.subMillisecondDigits ?? "")
|
|
473
|
+
) {
|
|
474
|
+
warn(
|
|
475
|
+
context,
|
|
476
|
+
"dropped",
|
|
477
|
+
"DatePicker",
|
|
478
|
+
"Fractional seconds were dropped because Slack datetimepickers only support second precision.",
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
return {
|
|
482
|
+
type: "datetimepicker",
|
|
483
|
+
action_id: actionId,
|
|
484
|
+
initial_date_time: Math.floor(temporal.epochMs / 1000),
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
if (rawDate)
|
|
488
|
+
warn(
|
|
489
|
+
context,
|
|
490
|
+
"dropped",
|
|
491
|
+
"DatePicker",
|
|
492
|
+
`value did not match the ${inputType} format and was dropped.`,
|
|
493
|
+
);
|
|
494
|
+
return {
|
|
495
|
+
type: inputType === "time" ? "timepicker" : "datetimepicker",
|
|
496
|
+
action_id: actionId,
|
|
497
|
+
};
|
|
498
|
+
}
|
|
285
499
|
const initialDate = DATE_PATTERN.test(rawDate) ? rawDate : undefined;
|
|
286
500
|
if (rawDate && initialDate === undefined) {
|
|
287
501
|
warn(
|
|
@@ -319,6 +533,46 @@ const toActionElement = (
|
|
|
319
533
|
: {}),
|
|
320
534
|
};
|
|
321
535
|
}
|
|
536
|
+
case "CheckboxGroup": {
|
|
537
|
+
const rawOptions = Array.isArray(props["options"])
|
|
538
|
+
? props["options"]
|
|
539
|
+
: [];
|
|
540
|
+
const { items: takenOptions, truncated } = copyBounded(
|
|
541
|
+
rawOptions,
|
|
542
|
+
CHECKBOX_OPTION_CAP,
|
|
543
|
+
);
|
|
544
|
+
if (truncated) {
|
|
545
|
+
warn(
|
|
546
|
+
context,
|
|
547
|
+
"clamped",
|
|
548
|
+
"CheckboxGroup",
|
|
549
|
+
`options were clamped to ${CHECKBOX_OPTION_CAP} entries.`,
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
const options = takenOptions
|
|
553
|
+
.map((option) => optionFrom(option, "CheckboxGroup", context))
|
|
554
|
+
.filter((option): option is SlackOption => option !== undefined);
|
|
555
|
+
warnDroppedOptions(
|
|
556
|
+
options.length,
|
|
557
|
+
takenOptions.length,
|
|
558
|
+
"CheckboxGroup",
|
|
559
|
+
context,
|
|
560
|
+
);
|
|
561
|
+
const defaultValue = Array.isArray(props["defaultValue"])
|
|
562
|
+
? props["defaultValue"]
|
|
563
|
+
: [];
|
|
564
|
+
const initialOptions = options.filter((option) =>
|
|
565
|
+
defaultValue.includes(option.value),
|
|
566
|
+
);
|
|
567
|
+
return {
|
|
568
|
+
type: "checkboxes",
|
|
569
|
+
action_id: asActionId(action, "CheckboxGroup", context),
|
|
570
|
+
options,
|
|
571
|
+
...(initialOptions.length > 0
|
|
572
|
+
? { initial_options: initialOptions }
|
|
573
|
+
: {}),
|
|
574
|
+
};
|
|
575
|
+
}
|
|
322
576
|
case "RadioGroup": {
|
|
323
577
|
const rawOptions = Array.isArray(props["options"])
|
|
324
578
|
? props["options"]
|
|
@@ -380,10 +634,15 @@ const convertFacts = (
|
|
|
380
634
|
const fields = facts.map((fact) => {
|
|
381
635
|
const label = asString(fact.props["label"]);
|
|
382
636
|
const value = asString(fact.props["value"]);
|
|
637
|
+
const delta = fact.props["delta"];
|
|
638
|
+
const deltaText =
|
|
639
|
+
typeof delta === "string"
|
|
640
|
+
? formatFactDelta(delta, factTrend(delta, fact.props["trend"]))
|
|
641
|
+
: undefined;
|
|
383
642
|
return {
|
|
384
643
|
type: "mrkdwn" as const,
|
|
385
644
|
text: clampText(
|
|
386
|
-
`*${label}*\n${value}`,
|
|
645
|
+
`*${label}*\n${deltaText === undefined ? value : `${value} (${deltaText})`}`,
|
|
387
646
|
FACT_FIELD_TEXT_CAP,
|
|
388
647
|
"Fact",
|
|
389
648
|
"field",
|
|
@@ -405,14 +664,103 @@ const convertActions = (
|
|
|
405
664
|
elements: readonly NormalizedUIElement[],
|
|
406
665
|
context: ConversionContext,
|
|
407
666
|
): SlackBlock[] => {
|
|
408
|
-
const converted = elements
|
|
409
|
-
|
|
410
|
-
|
|
667
|
+
const converted = elements.flatMap((source) => {
|
|
668
|
+
const result = toActionElement(source, context);
|
|
669
|
+
if (result === undefined) return [];
|
|
670
|
+
const controls = Array.isArray(result) ? result : [result];
|
|
671
|
+
const inputType =
|
|
672
|
+
source.type === "DatePicker" ? source.props["inputType"] : undefined;
|
|
673
|
+
const temporal =
|
|
674
|
+
inputType === "datetime"
|
|
675
|
+
? classifyTemporal(asString(source.props["value"]))
|
|
676
|
+
: undefined;
|
|
677
|
+
const pairId =
|
|
678
|
+
controls.length > 1 ? context.dateTimePairSequence++ : undefined;
|
|
679
|
+
return controls.map((element) => {
|
|
680
|
+
const name = asString(source.props["name"]);
|
|
681
|
+
const field: FieldMapping | undefined =
|
|
682
|
+
inputType === "time" || inputType === "datetime"
|
|
683
|
+
? {
|
|
684
|
+
actionId: element.action_id,
|
|
685
|
+
name,
|
|
686
|
+
component: source.type,
|
|
687
|
+
inputType,
|
|
688
|
+
...(temporal?.kind === "instant"
|
|
689
|
+
? { offset: temporal.offset }
|
|
690
|
+
: {}),
|
|
691
|
+
...(pairId !== undefined
|
|
692
|
+
? ({
|
|
693
|
+
part: element.type === "datepicker" ? "date" : "time",
|
|
694
|
+
pairId,
|
|
695
|
+
} as const)
|
|
696
|
+
: {}),
|
|
697
|
+
}
|
|
698
|
+
: name
|
|
699
|
+
? { actionId: element.action_id, name, component: source.type }
|
|
700
|
+
: undefined;
|
|
701
|
+
return { element, field };
|
|
702
|
+
});
|
|
703
|
+
});
|
|
411
704
|
const blocks: SlackBlock[] = [];
|
|
412
|
-
|
|
705
|
+
const groups: (typeof converted)[] = [];
|
|
706
|
+
let group: typeof converted = [];
|
|
707
|
+
let actionIds = new Set<string>();
|
|
708
|
+
const fieldsFor = (entries: typeof converted): FieldMapping[] =>
|
|
709
|
+
entries.flatMap(({ field }) => (field === undefined ? [] : [field]));
|
|
710
|
+
for (const entry of converted) {
|
|
711
|
+
const repeatedMappedActionId =
|
|
712
|
+
actionIds.has(entry.element.action_id) &&
|
|
713
|
+
(entry.field !== undefined ||
|
|
714
|
+
group.some(
|
|
715
|
+
(candidate) =>
|
|
716
|
+
candidate.element.action_id === entry.element.action_id &&
|
|
717
|
+
candidate.field !== undefined,
|
|
718
|
+
));
|
|
719
|
+
if (group.length === ACTIONS_ELEMENT_CAP || repeatedMappedActionId) {
|
|
720
|
+
groups.push(group);
|
|
721
|
+
group = [];
|
|
722
|
+
actionIds = new Set();
|
|
723
|
+
}
|
|
724
|
+
group.push(entry);
|
|
725
|
+
actionIds.add(entry.element.action_id);
|
|
726
|
+
}
|
|
727
|
+
if (group.length > 0) groups.push(group);
|
|
728
|
+
const mappedGroups: (typeof converted)[] = [];
|
|
729
|
+
let nextSequence = context.fieldBlockIdSequence;
|
|
730
|
+
for (const entries of groups) {
|
|
731
|
+
let current: typeof converted = [];
|
|
732
|
+
for (const entry of entries) {
|
|
733
|
+
const candidate = [...current, entry];
|
|
734
|
+
const fields = fieldsFor(candidate);
|
|
735
|
+
const blockId =
|
|
736
|
+
fields.length > 0
|
|
737
|
+
? serializeFieldBlockId(nextSequence, fields)
|
|
738
|
+
: undefined;
|
|
739
|
+
if (
|
|
740
|
+
current.length > 0 &&
|
|
741
|
+
fields.length > fieldsFor(current).length &&
|
|
742
|
+
blockId !== undefined &&
|
|
743
|
+
blockId.length > SLACK_BLOCK_ID_CAP
|
|
744
|
+
) {
|
|
745
|
+
mappedGroups.push(current);
|
|
746
|
+
if (fieldsFor(current).length > 0) nextSequence += 1;
|
|
747
|
+
current = [entry];
|
|
748
|
+
} else {
|
|
749
|
+
current = candidate;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
if (current.length > 0) {
|
|
753
|
+
mappedGroups.push(current);
|
|
754
|
+
if (fieldsFor(current).length > 0) nextSequence += 1;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
for (const group of mappedGroups) {
|
|
758
|
+
const fields = fieldsFor(group);
|
|
759
|
+
const blockId = fieldBlockId(fields, context);
|
|
413
760
|
blocks.push({
|
|
414
761
|
type: "actions",
|
|
415
|
-
|
|
762
|
+
...(blockId !== undefined ? { block_id: blockId } : {}),
|
|
763
|
+
elements: group.map(({ element }) => element),
|
|
416
764
|
});
|
|
417
765
|
}
|
|
418
766
|
return blocks;
|
|
@@ -914,12 +1262,18 @@ const convertCarousel = (
|
|
|
914
1262
|
return [buildCarouselBlock(cards)];
|
|
915
1263
|
};
|
|
916
1264
|
|
|
917
|
-
const toDataTableCell = (
|
|
918
|
-
|
|
1265
|
+
const toDataTableCell = (
|
|
1266
|
+
value: unknown,
|
|
1267
|
+
format: unknown,
|
|
1268
|
+
): SlackDataTableCell | undefined => {
|
|
1269
|
+
if (typeof value === "number" && format === undefined) {
|
|
919
1270
|
return { type: "raw_number", text: String(value) };
|
|
920
1271
|
}
|
|
921
1272
|
if (typeof value === "string" || typeof value === "boolean") {
|
|
922
|
-
return { type: "raw_text", text:
|
|
1273
|
+
return { type: "raw_text", text: formatValue(value, format) };
|
|
1274
|
+
}
|
|
1275
|
+
if (typeof value === "number") {
|
|
1276
|
+
return { type: "raw_text", text: formatValue(value, format) };
|
|
923
1277
|
}
|
|
924
1278
|
return undefined;
|
|
925
1279
|
};
|
|
@@ -982,8 +1336,13 @@ const convertTable = (
|
|
|
982
1336
|
? copyBounded(row, DATA_TABLE_COLUMN_CAP).items
|
|
983
1337
|
: []
|
|
984
1338
|
).map(
|
|
985
|
-
(value) =>
|
|
986
|
-
toDataTableCell(
|
|
1339
|
+
(value, index) =>
|
|
1340
|
+
toDataTableCell(
|
|
1341
|
+
value,
|
|
1342
|
+
isRecord(takenColumns[index])
|
|
1343
|
+
? takenColumns[index]["format"]
|
|
1344
|
+
: undefined,
|
|
1345
|
+
) ?? { type: "raw_text" as const, text: "" },
|
|
987
1346
|
),
|
|
988
1347
|
);
|
|
989
1348
|
const width = Math.max(
|
|
@@ -1115,9 +1474,68 @@ const convertElement = (
|
|
|
1115
1474
|
case "Select":
|
|
1116
1475
|
case "DatePicker":
|
|
1117
1476
|
case "Checkbox":
|
|
1477
|
+
case "CheckboxGroup":
|
|
1118
1478
|
case "RadioGroup":
|
|
1119
1479
|
return convertActions([element], context);
|
|
1480
|
+
case "Slider": {
|
|
1481
|
+
const label = clampText(
|
|
1482
|
+
asString(props["label"]),
|
|
1483
|
+
INPUT_LABEL_CAP,
|
|
1484
|
+
"Slider",
|
|
1485
|
+
"label",
|
|
1486
|
+
context,
|
|
1487
|
+
);
|
|
1488
|
+
const min = asFiniteNumber(props["min"]);
|
|
1489
|
+
const max = asFiniteNumber(props["max"]);
|
|
1490
|
+
const step = asFiniteNumber(props["step"]);
|
|
1491
|
+
const defaultValue = asFiniteNumber(props["defaultValue"]);
|
|
1492
|
+
const actionId = asActionId(element.action, "Slider", context);
|
|
1493
|
+
const name = props["name"];
|
|
1494
|
+
const blockId = fieldBlockId(
|
|
1495
|
+
typeof name === "string" && name.length > 0
|
|
1496
|
+
? [{ actionId, name, component: "Slider" }]
|
|
1497
|
+
: [],
|
|
1498
|
+
context,
|
|
1499
|
+
);
|
|
1500
|
+
return [
|
|
1501
|
+
{
|
|
1502
|
+
type: "input",
|
|
1503
|
+
...(blockId !== undefined ? { block_id: blockId } : {}),
|
|
1504
|
+
label: plainText(label),
|
|
1505
|
+
element: {
|
|
1506
|
+
type: "number_input",
|
|
1507
|
+
action_id: actionId,
|
|
1508
|
+
...(min !== undefined ? { min_value: min } : {}),
|
|
1509
|
+
...(max !== undefined ? { max_value: max } : {}),
|
|
1510
|
+
...(defaultValue !== undefined
|
|
1511
|
+
? { initial_value: defaultValue }
|
|
1512
|
+
: {}),
|
|
1513
|
+
...([step, min, max, defaultValue].some(
|
|
1514
|
+
(value) => value !== undefined && !Number.isInteger(value),
|
|
1515
|
+
)
|
|
1516
|
+
? { is_decimal_allowed: true }
|
|
1517
|
+
: {}),
|
|
1518
|
+
},
|
|
1519
|
+
} as unknown as SlackBlock,
|
|
1520
|
+
];
|
|
1521
|
+
}
|
|
1120
1522
|
case "Input": {
|
|
1523
|
+
if (props["inputType"] === "password") {
|
|
1524
|
+
warn(
|
|
1525
|
+
context,
|
|
1526
|
+
"dropped",
|
|
1527
|
+
"Input",
|
|
1528
|
+
"Password input was replaced by a Slack omission note.",
|
|
1529
|
+
);
|
|
1530
|
+
return [
|
|
1531
|
+
{
|
|
1532
|
+
type: "context",
|
|
1533
|
+
elements: [
|
|
1534
|
+
{ type: "mrkdwn", text: "Password input omitted on Slack." },
|
|
1535
|
+
],
|
|
1536
|
+
},
|
|
1537
|
+
];
|
|
1538
|
+
}
|
|
1121
1539
|
const label = clampText(
|
|
1122
1540
|
asString(props["label"]),
|
|
1123
1541
|
INPUT_LABEL_CAP,
|
|
@@ -1132,14 +1550,30 @@ const convertElement = (
|
|
|
1132
1550
|
"placeholder",
|
|
1133
1551
|
context,
|
|
1134
1552
|
);
|
|
1553
|
+
const actionId = asActionId(element.action, "Input", context);
|
|
1554
|
+
const name = props["name"];
|
|
1555
|
+
const blockId = fieldBlockId(
|
|
1556
|
+
typeof name === "string" && name.length > 0
|
|
1557
|
+
? [{ actionId, name, component: "Input" }]
|
|
1558
|
+
: [],
|
|
1559
|
+
context,
|
|
1560
|
+
);
|
|
1561
|
+
const defaultValue = props["defaultValue"];
|
|
1135
1562
|
return [
|
|
1136
1563
|
{
|
|
1137
1564
|
type: "input",
|
|
1565
|
+
...(blockId !== undefined ? { block_id: blockId } : {}),
|
|
1138
1566
|
label: plainText(label),
|
|
1139
1567
|
element: {
|
|
1140
1568
|
type: "plain_text_input",
|
|
1141
|
-
action_id:
|
|
1142
|
-
...(props["multiline"] === true
|
|
1569
|
+
action_id: actionId,
|
|
1570
|
+
...(props["multiline"] === true &&
|
|
1571
|
+
(props["inputType"] === undefined || props["inputType"] === "text")
|
|
1572
|
+
? { multiline: true }
|
|
1573
|
+
: {}),
|
|
1574
|
+
...(typeof defaultValue === "string" && defaultValue
|
|
1575
|
+
? { initial_value: defaultValue }
|
|
1576
|
+
: {}),
|
|
1143
1577
|
...(placeholder ? { placeholder: plainText(placeholder) } : {}),
|
|
1144
1578
|
},
|
|
1145
1579
|
},
|
|
@@ -1340,6 +1774,9 @@ export function toSlackBlocks(
|
|
|
1340
1774
|
blockCap: surface === "modal" ? MODAL_BLOCK_CAP : MESSAGE_BLOCK_CAP,
|
|
1341
1775
|
surface,
|
|
1342
1776
|
warnings: [],
|
|
1777
|
+
omittedPasswordNames: new Set(),
|
|
1778
|
+
fieldBlockIdSequence: 0,
|
|
1779
|
+
dateTimePairSequence: 0,
|
|
1343
1780
|
markdownCharacters: 0,
|
|
1344
1781
|
markdownExhausted: false,
|
|
1345
1782
|
dataTableCharacters: 0,
|
|
@@ -1349,6 +1786,7 @@ export function toSlackBlocks(
|
|
|
1349
1786
|
warn(context, "clamped", "Root", clampReasonDetail(reason)),
|
|
1350
1787
|
);
|
|
1351
1788
|
const { root } = normalizeSpec(bounded as never);
|
|
1789
|
+
collectOmittedPasswordNames(root, context.omittedPasswordNames);
|
|
1352
1790
|
const converted = convertSequence(root, context, 0);
|
|
1353
1791
|
if (converted.length <= context.blockCap) {
|
|
1354
1792
|
return { blocks: converted, warnings: context.warnings };
|