@assistant-ui/react-generative-ui 0.0.24 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/a2ui.d.ts +1 -4
- package/dist/a2ui.d.ts.map +1 -1
- package/dist/a2ui.js +1 -4
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -10
- package/dist/index.server.d.ts +1 -2
- package/dist/index.server.d.ts.map +1 -1
- package/dist/index.server.js +1 -10
- package/dist/ir.d.ts +1 -111
- package/dist/ir.d.ts.map +1 -1
- package/dist/ir.js +1 -175
- package/dist/slack.d.ts +1 -4
- package/dist/slack.d.ts.map +1 -1
- package/dist/slack.js +1 -4
- package/dist/teams.d.ts +1 -4
- package/dist/teams.d.ts.map +1 -1
- package/dist/teams.js +1 -4
- package/package.json +4 -5
- package/src/a2ui.ts +1 -22
- package/src/index.server.ts +1 -2
- package/src/index.test.ts +75 -0
- package/src/index.ts +1 -2
- package/src/ir.ts +1 -279
- package/src/slack.ts +1 -39
- package/src/teams.ts +1 -35
- package/dist/JSONGenerativeUI.client.d.ts +0 -28
- package/dist/JSONGenerativeUI.client.d.ts.map +0 -1
- package/dist/JSONGenerativeUI.client.js +0 -101
- package/dist/JSONGenerativeUI.client.js.map +0 -1
- package/dist/JSONGenerativeUI.server.d.ts +0 -21
- package/dist/JSONGenerativeUI.server.d.ts.map +0 -1
- package/dist/JSONGenerativeUI.server.js +0 -32
- package/dist/JSONGenerativeUI.server.js.map +0 -1
- package/dist/JSONGenerativeUI.shared.d.ts +0 -63
- package/dist/JSONGenerativeUI.shared.d.ts.map +0 -1
- package/dist/JSONGenerativeUI.shared.js +0 -31
- package/dist/JSONGenerativeUI.shared.js.map +0 -1
- package/dist/RadioGroupScope.d.ts +0 -6
- package/dist/RadioGroupScope.d.ts.map +0 -1
- package/dist/RadioGroupScope.js +0 -25
- package/dist/RadioGroupScope.js.map +0 -1
- package/dist/a2ui/BindingContext.d.ts +0 -11
- package/dist/a2ui/BindingContext.d.ts.map +0 -1
- package/dist/a2ui/BindingContext.js +0 -12
- package/dist/a2ui/BindingContext.js.map +0 -1
- package/dist/a2ui/PresentRenderer.d.ts +0 -11
- package/dist/a2ui/PresentRenderer.d.ts.map +0 -1
- package/dist/a2ui/PresentRenderer.js +0 -39
- package/dist/a2ui/PresentRenderer.js.map +0 -1
- package/dist/a2ui/convert.d.ts +0 -15
- package/dist/a2ui/convert.d.ts.map +0 -1
- package/dist/a2ui/convert.js +0 -728
- package/dist/a2ui/convert.js.map +0 -1
- package/dist/a2ui/dataModel.d.ts +0 -9
- package/dist/a2ui/dataModel.d.ts.map +0 -1
- package/dist/a2ui/dataModel.js +0 -44
- package/dist/a2ui/dataModel.js.map +0 -1
- package/dist/a2ui/reducer.d.ts +0 -8
- package/dist/a2ui/reducer.d.ts.map +0 -1
- package/dist/a2ui/reducer.js +0 -242
- package/dist/a2ui/reducer.js.map +0 -1
- package/dist/a2ui/snapshot.d.ts +0 -3
- package/dist/a2ui/snapshot.d.ts.map +0 -1
- package/dist/a2ui/snapshot.js +0 -33
- package/dist/a2ui/snapshot.js.map +0 -1
- package/dist/a2ui/types.d.ts +0 -88
- package/dist/a2ui/types.d.ts.map +0 -1
- package/dist/a2ui/types.js +0 -6
- package/dist/a2ui/types.js.map +0 -1
- package/dist/a2ui/valueFunctions.d.ts +0 -16
- package/dist/a2ui/valueFunctions.d.ts.map +0 -1
- package/dist/a2ui/valueFunctions.js +0 -332
- package/dist/a2ui/valueFunctions.js.map +0 -1
- package/dist/actionRegistry.d.ts +0 -29
- package/dist/actionRegistry.d.ts.map +0 -1
- package/dist/actionRegistry.js +0 -41
- package/dist/actionRegistry.js.map +0 -1
- package/dist/answeredValues.d.ts +0 -16
- package/dist/answeredValues.d.ts.map +0 -1
- package/dist/answeredValues.js +0 -37
- package/dist/answeredValues.js.map +0 -1
- package/dist/buildPresentParameters.d.ts +0 -21
- package/dist/buildPresentParameters.d.ts.map +0 -1
- package/dist/buildPresentParameters.js +0 -95
- package/dist/buildPresentParameters.js.map +0 -1
- package/dist/constants.d.ts +0 -15
- package/dist/constants.d.ts.map +0 -1
- package/dist/constants.js +0 -19
- package/dist/constants.js.map +0 -1
- package/dist/convert/boundSpec.d.ts +0 -48
- package/dist/convert/boundSpec.d.ts.map +0 -1
- package/dist/convert/boundSpec.js +0 -108
- package/dist/convert/boundSpec.js.map +0 -1
- package/dist/convert/copyBounded.d.ts +0 -25
- package/dist/convert/copyBounded.d.ts.map +0 -1
- package/dist/convert/copyBounded.js +0 -38
- package/dist/convert/copyBounded.js.map +0 -1
- package/dist/convert/isElement.d.ts +0 -3
- package/dist/convert/isElement.d.ts.map +0 -1
- package/dist/convert/isElement.js +0 -6
- package/dist/convert/isElement.js.map +0 -1
- package/dist/convert/takeRun.d.ts +0 -6
- package/dist/convert/takeRun.d.ts.map +0 -1
- package/dist/convert/takeRun.js +0 -20
- package/dist/convert/takeRun.js.map +0 -1
- package/dist/defineGenerativeComponents.d.ts +0 -29
- package/dist/defineGenerativeComponents.d.ts.map +0 -1
- package/dist/defineGenerativeComponents.js +0 -34
- package/dist/defineGenerativeComponents.js.map +0 -1
- package/dist/fieldReferences.d.ts +0 -9
- package/dist/fieldReferences.d.ts.map +0 -1
- package/dist/fieldReferences.js +0 -53
- package/dist/fieldReferences.js.map +0 -1
- package/dist/generativeUIToJSX.d.ts +0 -18
- package/dist/generativeUIToJSX.d.ts.map +0 -1
- package/dist/generativeUIToJSX.js +0 -76
- package/dist/generativeUIToJSX.js.map +0 -1
- package/dist/index.shared.d.ts +0 -15
- package/dist/index.shared.d.ts.map +0 -1
- package/dist/index.shared.js +0 -9
- package/dist/ir.js.map +0 -1
- package/dist/renderGenerativeUI.d.ts +0 -14
- package/dist/renderGenerativeUI.d.ts.map +0 -1
- package/dist/renderGenerativeUI.js +0 -83
- package/dist/renderGenerativeUI.js.map +0 -1
- package/dist/scopeSchema.d.ts +0 -6
- package/dist/scopeSchema.d.ts.map +0 -1
- package/dist/scopeSchema.js +0 -56
- package/dist/scopeSchema.js.map +0 -1
- package/dist/slack/constants.d.ts +0 -93
- package/dist/slack/constants.d.ts.map +0 -1
- package/dist/slack/constants.js +0 -146
- package/dist/slack/constants.js.map +0 -1
- package/dist/slack/decodeBlockAction.d.ts +0 -10
- package/dist/slack/decodeBlockAction.d.ts.map +0 -1
- package/dist/slack/decodeBlockAction.js +0 -129
- package/dist/slack/decodeBlockAction.js.map +0 -1
- package/dist/slack/fromSlackBlocks.d.ts +0 -27
- package/dist/slack/fromSlackBlocks.d.ts.map +0 -1
- package/dist/slack/fromSlackBlocks.js +0 -483
- package/dist/slack/fromSlackBlocks.js.map +0 -1
- package/dist/slack/toSlackBlocks.d.ts +0 -4
- package/dist/slack/toSlackBlocks.d.ts.map +0 -1
- package/dist/slack/toSlackBlocks.js +0 -901
- package/dist/slack/toSlackBlocks.js.map +0 -1
- package/dist/slack/types.d.ts +0 -204
- package/dist/slack/types.d.ts.map +0 -1
- package/dist/slack/types.js +0 -0
- package/dist/teams/constants.d.ts +0 -34
- package/dist/teams/constants.d.ts.map +0 -1
- package/dist/teams/constants.js +0 -68
- package/dist/teams/constants.js.map +0 -1
- package/dist/teams/decodeSubmitData.d.ts +0 -26
- package/dist/teams/decodeSubmitData.d.ts.map +0 -1
- package/dist/teams/decodeSubmitData.js +0 -70
- package/dist/teams/decodeSubmitData.js.map +0 -1
- package/dist/teams/temporalId.d.ts +0 -5
- package/dist/teams/temporalId.d.ts.map +0 -1
- package/dist/teams/temporalId.js +0 -28
- package/dist/teams/temporalId.js.map +0 -1
- package/dist/teams/toAdaptiveCard.d.ts +0 -45
- package/dist/teams/toAdaptiveCard.d.ts.map +0 -1
- package/dist/teams/toAdaptiveCard.js +0 -642
- package/dist/teams/toAdaptiveCard.js.map +0 -1
- package/dist/teams/toTeamsAttachments.d.ts +0 -10
- package/dist/teams/toTeamsAttachments.d.ts.map +0 -1
- package/dist/teams/toTeamsAttachments.js +0 -99
- package/dist/teams/toTeamsAttachments.js.map +0 -1
- package/dist/teams/types.d.ts +0 -224
- package/dist/teams/types.d.ts.map +0 -1
- package/dist/teams/types.js +0 -0
- package/dist/temporal.d.ts +0 -33
- package/dist/temporal.d.ts.map +0 -1
- package/dist/temporal.js +0 -153
- package/dist/temporal.js.map +0 -1
- package/dist/types.d.ts +0 -99
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -0
- package/dist/vocabulary/alert.d.ts +0 -32
- package/dist/vocabulary/alert.d.ts.map +0 -1
- package/dist/vocabulary/alert.js +0 -160
- package/dist/vocabulary/alert.js.map +0 -1
- package/dist/vocabulary/collectFormValues.d.ts +0 -23
- package/dist/vocabulary/collectFormValues.d.ts.map +0 -1
- package/dist/vocabulary/collectFormValues.js +0 -44
- package/dist/vocabulary/collectFormValues.js.map +0 -1
- package/dist/vocabulary/data.d.ts +0 -66
- package/dist/vocabulary/data.d.ts.map +0 -1
- package/dist/vocabulary/data.js +0 -503
- package/dist/vocabulary/data.js.map +0 -1
- package/dist/vocabulary/dispatch.d.ts +0 -9
- package/dist/vocabulary/dispatch.d.ts.map +0 -1
- package/dist/vocabulary/dispatch.js +0 -50
- package/dist/vocabulary/dispatch.js.map +0 -1
- package/dist/vocabulary/fact.d.ts +0 -19
- package/dist/vocabulary/fact.d.ts.map +0 -1
- package/dist/vocabulary/fact.js +0 -51
- package/dist/vocabulary/fact.js.map +0 -1
- package/dist/vocabulary/form.d.ts +0 -11
- package/dist/vocabulary/form.d.ts.map +0 -1
- package/dist/vocabulary/form.js +0 -23
- package/dist/vocabulary/form.js.map +0 -1
- package/dist/vocabulary/formatValue.d.ts +0 -7
- package/dist/vocabulary/formatValue.d.ts.map +0 -1
- package/dist/vocabulary/formatValue.js +0 -76
- package/dist/vocabulary/formatValue.js.map +0 -1
- package/dist/vocabulary/icon.d.ts +0 -41
- package/dist/vocabulary/icon.d.ts.map +0 -1
- package/dist/vocabulary/icon.js +0 -272
- package/dist/vocabulary/icon.js.map +0 -1
- package/dist/vocabulary/index.d.ts +0 -12
- package/dist/vocabulary/index.d.ts.map +0 -1
- package/dist/vocabulary/index.js +0 -36
- package/dist/vocabulary/index.js.map +0 -1
- package/dist/vocabulary/interactive.d.ts +0 -230
- package/dist/vocabulary/interactive.d.ts.map +0 -1
- package/dist/vocabulary/interactive.js +0 -584
- package/dist/vocabulary/interactive.js.map +0 -1
- package/dist/vocabulary/layout.d.ts +0 -79
- package/dist/vocabulary/layout.d.ts.map +0 -1
- package/dist/vocabulary/layout.js +0 -139
- package/dist/vocabulary/layout.js.map +0 -1
- package/dist/vocabulary/list.d.ts +0 -14
- package/dist/vocabulary/list.d.ts.map +0 -1
- package/dist/vocabulary/list.js +0 -50
- package/dist/vocabulary/list.js.map +0 -1
- package/dist/vocabulary/media.d.ts +0 -25
- package/dist/vocabulary/media.d.ts.map +0 -1
- package/dist/vocabulary/media.js +0 -42
- package/dist/vocabulary/media.js.map +0 -1
- package/dist/vocabulary/text.d.ts +0 -57
- package/dist/vocabulary/text.d.ts.map +0 -1
- package/dist/vocabulary/text.js +0 -49
- package/dist/vocabulary/text.js.map +0 -1
- package/dist/vocabulary/toTextContent.d.ts +0 -2
- package/dist/vocabulary/toTextContent.d.ts.map +0 -1
- package/dist/vocabulary/toTextContent.js +0 -6
- package/dist/vocabulary/toTextContent.js.map +0 -1
- package/src/JSONGenerativeUI.client.tsx +0 -168
- package/src/JSONGenerativeUI.server.tsx +0 -39
- package/src/JSONGenerativeUI.shared.ts +0 -90
- package/src/JSONGenerativeUI.test.tsx +0 -1298
- package/src/RadioGroupScope.server.test.tsx +0 -24
- package/src/RadioGroupScope.tsx +0 -34
- package/src/a2ui/BindingContext.ts +0 -26
- package/src/a2ui/PresentRenderer.tsx +0 -83
- package/src/a2ui/convert.test.ts +0 -2923
- package/src/a2ui/convert.ts +0 -1207
- package/src/a2ui/dataModel.ts +0 -69
- package/src/a2ui/reducer.test.ts +0 -422
- package/src/a2ui/reducer.ts +0 -363
- package/src/a2ui/snapshot.test.ts +0 -52
- package/src/a2ui/snapshot.ts +0 -52
- package/src/a2ui/types.ts +0 -114
- package/src/a2ui/valueFunctions.test.ts +0 -271
- package/src/a2ui/valueFunctions.ts +0 -523
- package/src/actionRegistry.test.ts +0 -90
- package/src/actionRegistry.ts +0 -88
- package/src/answeredValues.test.ts +0 -33
- package/src/answeredValues.tsx +0 -70
- package/src/buildPresentParameters.test.ts +0 -246
- package/src/buildPresentParameters.ts +0 -130
- package/src/constants.ts +0 -17
- package/src/convert/boundSpec.test.ts +0 -192
- package/src/convert/boundSpec.ts +0 -134
- package/src/convert/copyBounded.test.ts +0 -131
- package/src/convert/copyBounded.ts +0 -38
- package/src/convert/isElement.ts +0 -6
- package/src/convert/takeRun.test.ts +0 -64
- package/src/convert/takeRun.ts +0 -19
- package/src/defineGenerativeComponents.ts +0 -38
- package/src/fieldReferences.test.ts +0 -66
- package/src/fieldReferences.ts +0 -83
- package/src/generativeUIToJSX.test.ts +0 -268
- package/src/generativeUIToJSX.ts +0 -152
- package/src/index.server.test.ts +0 -106
- package/src/index.shared.ts +0 -61
- package/src/ir.test.ts +0 -203
- package/src/renderGenerativeUI.test.tsx +0 -373
- package/src/renderGenerativeUI.tsx +0 -165
- package/src/scopeSchema.test.ts +0 -72
- package/src/scopeSchema.ts +0 -71
- package/src/slack/a2uiSlack.test.ts +0 -198
- package/src/slack/constants.ts +0 -194
- package/src/slack/datePicker.test.ts +0 -612
- package/src/slack/decodeBlockAction.test.ts +0 -233
- package/src/slack/decodeBlockAction.ts +0 -232
- package/src/slack/fromSlackBlocks.test.ts +0 -1465
- package/src/slack/fromSlackBlocks.ts +0 -789
- package/src/slack/toSlackBlocks.test.ts +0 -3065
- package/src/slack/toSlackBlocks.ts +0 -1818
- package/src/slack/types.ts +0 -264
- package/src/teams/constants.ts +0 -93
- package/src/teams/decodeSubmitData.test.ts +0 -397
- package/src/teams/decodeSubmitData.ts +0 -110
- package/src/teams/temporal.test.ts +0 -766
- package/src/teams/temporalId.ts +0 -56
- package/src/teams/toAdaptiveCard.test.ts +0 -1897
- package/src/teams/toAdaptiveCard.ts +0 -1057
- package/src/teams/toTeamsAttachments.test.ts +0 -74
- package/src/teams/toTeamsAttachments.ts +0 -133
- package/src/teams/types.ts +0 -266
- package/src/temporal.test.ts +0 -337
- package/src/temporal.ts +0 -263
- package/src/types.ts +0 -119
- package/src/vocabulary/alert.test.tsx +0 -427
- package/src/vocabulary/alert.tsx +0 -217
- package/src/vocabulary/collectFormValues.test.ts +0 -389
- package/src/vocabulary/collectFormValues.ts +0 -116
- package/src/vocabulary/data.test.tsx +0 -858
- package/src/vocabulary/data.tsx +0 -819
- package/src/vocabulary/datepicker.dom.test.tsx +0 -1457
- package/src/vocabulary/dispatch.ts +0 -83
- package/src/vocabulary/fact.test.tsx +0 -94
- package/src/vocabulary/fact.tsx +0 -54
- package/src/vocabulary/form.test.tsx +0 -113
- package/src/vocabulary/form.tsx +0 -39
- package/src/vocabulary/formatValue.test.ts +0 -72
- package/src/vocabulary/formatValue.ts +0 -127
- package/src/vocabulary/icon.test.tsx +0 -64
- package/src/vocabulary/icon.tsx +0 -174
- package/src/vocabulary/index.test.ts +0 -65
- package/src/vocabulary/index.ts +0 -33
- package/src/vocabulary/interactive.dispatch.test.tsx +0 -454
- package/src/vocabulary/interactive.dom.test.tsx +0 -1747
- package/src/vocabulary/interactive.test.tsx +0 -377
- package/src/vocabulary/interactive.tsx +0 -1109
- package/src/vocabulary/layout.test.tsx +0 -323
- package/src/vocabulary/layout.tsx +0 -247
- package/src/vocabulary/list.test.tsx +0 -222
- package/src/vocabulary/list.tsx +0 -50
- package/src/vocabulary/media.test.tsx +0 -85
- package/src/vocabulary/media.tsx +0 -56
- package/src/vocabulary/text.test.tsx +0 -102
- package/src/vocabulary/text.tsx +0 -60
- package/src/vocabulary/toTextContent.ts +0 -2
|
@@ -1,901 +0,0 @@
|
|
|
1
|
-
import { normalizeSpec } from "../ir.js";
|
|
2
|
-
import { classifyTemporal, splitTemporalMinutes } from "../temporal.js";
|
|
3
|
-
import { copyBounded } from "../convert/copyBounded.js";
|
|
4
|
-
import { boundSpec, clampReasonDetail } from "../convert/boundSpec.js";
|
|
5
|
-
import { isElement } from "../convert/isElement.js";
|
|
6
|
-
import { takeRun } from "../convert/takeRun.js";
|
|
7
|
-
import { factTrend, formatFactDelta, formatValue } from "../vocabulary/formatValue.js";
|
|
8
|
-
import { CONTEXT_TEXT_CAP, DATA_TABLE_CHAR_BUDGET, FACT_FIELD_TEXT_CAP, INPUT_LABEL_CAP, MARKDOWN_TEXT_BUDGET, SECTION_TEXT_CAP, buildAlertBlock, buildAlertFallback, buildBlockBudgetNote, buildCardBlock, buildCarouselBlock, buildDataTableBlock } from "./constants.js";
|
|
9
|
-
//#region src/slack/toSlackBlocks.ts
|
|
10
|
-
const SLACK_BLOCK_ID_CAP = 255;
|
|
11
|
-
const INTERACTIVE_TYPES = /* @__PURE__ */ new Set([
|
|
12
|
-
"Button",
|
|
13
|
-
"Select",
|
|
14
|
-
"DatePicker",
|
|
15
|
-
"Checkbox",
|
|
16
|
-
"CheckboxGroup",
|
|
17
|
-
"RadioGroup"
|
|
18
|
-
]);
|
|
19
|
-
/**
|
|
20
|
-
* Types whose control disappears when a card is reshaped to text. `Input` and
|
|
21
|
-
* `Form` are not in {@link INTERACTIVE_TYPES} because they emit their own
|
|
22
|
-
* block rather than an actions element, but a reshape loses them the same way,
|
|
23
|
-
* and a `Form` gets a Submit button whether or not it carries an action.
|
|
24
|
-
*/
|
|
25
|
-
const CONTROL_TYPES = /* @__PURE__ */ new Set([
|
|
26
|
-
...INTERACTIVE_TYPES,
|
|
27
|
-
"Input",
|
|
28
|
-
"Slider",
|
|
29
|
-
"Form"
|
|
30
|
-
]);
|
|
31
|
-
const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
32
|
-
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
33
|
-
const asString = (value) => typeof value === "string" ? value : "";
|
|
34
|
-
const asFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
35
|
-
const warn = (context, code, component, detail) => {
|
|
36
|
-
context.warnings.push({
|
|
37
|
-
code,
|
|
38
|
-
component,
|
|
39
|
-
detail
|
|
40
|
-
});
|
|
41
|
-
};
|
|
42
|
-
const clampText = (value, cap, component, field, context) => {
|
|
43
|
-
if (value.length <= cap) return value;
|
|
44
|
-
warn(context, "clamped", component, `${field} was clamped to ${cap} characters.`);
|
|
45
|
-
return value.slice(0, cap);
|
|
46
|
-
};
|
|
47
|
-
const asActionId = (action, component, context) => clampText(isRecord(action) && typeof action["type"] === "string" ? action["type"] : "action", 255, component, "action_id", context);
|
|
48
|
-
const plainText = (text) => ({
|
|
49
|
-
type: "plain_text",
|
|
50
|
-
text
|
|
51
|
-
});
|
|
52
|
-
const serializeFieldBlockId = (sequence, fields) => `aui:${sequence}:${JSON.stringify(fields.map(({ actionId, name, component, inputType, offset, part, pairId }) => inputType !== void 0 ? [
|
|
53
|
-
actionId,
|
|
54
|
-
name,
|
|
55
|
-
component,
|
|
56
|
-
inputType,
|
|
57
|
-
offset ?? null,
|
|
58
|
-
part ?? null,
|
|
59
|
-
pairId ?? null
|
|
60
|
-
] : component === "Checkbox" ? [
|
|
61
|
-
actionId,
|
|
62
|
-
name,
|
|
63
|
-
"Checkbox"
|
|
64
|
-
] : [actionId, name]))}`;
|
|
65
|
-
const fieldBlockId = (fields, context) => {
|
|
66
|
-
if (fields.length === 0) return void 0;
|
|
67
|
-
const sequence = context.fieldBlockIdSequence++;
|
|
68
|
-
const blockId = serializeFieldBlockId(sequence, fields);
|
|
69
|
-
if (blockId.length <= SLACK_BLOCK_ID_CAP) return blockId;
|
|
70
|
-
for (const field of fields) warn(context, "dropped", field.component, "name could not be mapped because its Slack block_id exceeded 255 characters.");
|
|
71
|
-
};
|
|
72
|
-
const collectOmittedPasswordNames = (node, names, depth = 0) => {
|
|
73
|
-
if (depth > 64) return;
|
|
74
|
-
if (Array.isArray(node)) for (const child of node) collectOmittedPasswordNames(child, names, depth + 1);
|
|
75
|
-
else if (isElement(node)) {
|
|
76
|
-
if (node.type === "Input" && node.props["inputType"] === "password" && typeof node.props["name"] === "string") names.add(node.props["name"]);
|
|
77
|
-
if (node.children !== void 0) collectOmittedPasswordNames(node.children, names, depth + 1);
|
|
78
|
-
}
|
|
79
|
-
};
|
|
80
|
-
const withoutOmittedPasswordFallbacks = (value, names, depth = 0) => {
|
|
81
|
-
if (depth > 64) throw new RangeError("Action payload nests too deeply to sanitize.");
|
|
82
|
-
if (Array.isArray(value)) return value.map((item) => withoutOmittedPasswordFallbacks(item, names, depth + 1));
|
|
83
|
-
if (!isRecord(value)) return value;
|
|
84
|
-
if (typeof value["$field"] === "string" && names.has(value["$field"]) && Object.keys(value).every((key) => key === "$field" || key === "fallback")) return { $field: value["$field"] };
|
|
85
|
-
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, withoutOmittedPasswordFallbacks(item, names, depth + 1)]));
|
|
86
|
-
};
|
|
87
|
-
const actionValue = (action, context) => {
|
|
88
|
-
if (!isRecord(action)) return void 0;
|
|
89
|
-
const { type: _type, ...payload } = action;
|
|
90
|
-
if (Object.keys(payload).length === 0) return void 0;
|
|
91
|
-
const serialized = JSON.stringify(context.omittedPasswordNames.size === 0 ? payload : withoutOmittedPasswordFallbacks(payload, context.omittedPasswordNames));
|
|
92
|
-
return serialized === "{}" ? void 0 : serialized;
|
|
93
|
-
};
|
|
94
|
-
const buttonElement = (label, buttonStyle, action, component, context) => {
|
|
95
|
-
let value;
|
|
96
|
-
try {
|
|
97
|
-
value = actionValue(action, context);
|
|
98
|
-
} catch {
|
|
99
|
-
warn(context, "dropped", component, "value was dropped because the action payload could not be serialized.");
|
|
100
|
-
}
|
|
101
|
-
if (value !== void 0 && value.length > 2e3) {
|
|
102
|
-
warn(context, "dropped", component, "value was dropped because the action payload was too large to round-trip.");
|
|
103
|
-
value = void 0;
|
|
104
|
-
}
|
|
105
|
-
const style = buttonStyle === "primary" || buttonStyle === "danger" ? buttonStyle : void 0;
|
|
106
|
-
return {
|
|
107
|
-
type: "button",
|
|
108
|
-
text: plainText(clampText(label, 75, component, "label", context)),
|
|
109
|
-
action_id: asActionId(action, component, context),
|
|
110
|
-
...value !== void 0 ? { value } : {},
|
|
111
|
-
...style !== void 0 ? { style } : {}
|
|
112
|
-
};
|
|
113
|
-
};
|
|
114
|
-
const optionFrom = (value, component, context) => {
|
|
115
|
-
if (!isRecord(value) || typeof value["label"] !== "string" || typeof value["value"] !== "string") return;
|
|
116
|
-
const description = asString(value["description"]);
|
|
117
|
-
return {
|
|
118
|
-
text: plainText(clampText(value["label"], 75, component, "option label", context)),
|
|
119
|
-
value: value["value"],
|
|
120
|
-
...description ? { description: plainText(clampText(description, 75, component, "option description", context)) } : {}
|
|
121
|
-
};
|
|
122
|
-
};
|
|
123
|
-
const warnDroppedOptions = (kept, taken, component, context) => {
|
|
124
|
-
const dropped = taken - kept;
|
|
125
|
-
if (dropped === 0) return;
|
|
126
|
-
warn(context, "dropped", component, `${dropped} ${dropped === 1 ? "option was" : "options were"} dropped for want of a string label and value.`);
|
|
127
|
-
};
|
|
128
|
-
const toActionElement = (element, context) => {
|
|
129
|
-
const { props, action } = element;
|
|
130
|
-
switch (element.type) {
|
|
131
|
-
case "Button": return buttonElement(asString(props["label"]), props["buttonStyle"], action, "Button", context);
|
|
132
|
-
case "Select": {
|
|
133
|
-
const rawOptions = Array.isArray(props["options"]) ? props["options"] : [];
|
|
134
|
-
const { items: takenOptions, truncated } = copyBounded(rawOptions, 100);
|
|
135
|
-
if (truncated) warn(context, "clamped", "Select", `options were clamped to 100 entries.`);
|
|
136
|
-
const options = takenOptions.map((option) => optionFrom(option, "Select", context)).filter((option) => option !== void 0);
|
|
137
|
-
warnDroppedOptions(options.length, takenOptions.length, "Select", context);
|
|
138
|
-
const placeholder = clampText(asString(props["placeholder"]), 150, "Select", "placeholder", context);
|
|
139
|
-
const defaultValue = props["defaultValue"];
|
|
140
|
-
const initialOption = typeof defaultValue === "string" ? options.find((option) => option.value === defaultValue) : void 0;
|
|
141
|
-
return {
|
|
142
|
-
type: "static_select",
|
|
143
|
-
action_id: asActionId(action, "Select", context),
|
|
144
|
-
options,
|
|
145
|
-
...initialOption !== void 0 ? { initial_option: initialOption } : {},
|
|
146
|
-
...placeholder ? { placeholder: plainText(placeholder) } : {}
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
case "DatePicker": {
|
|
150
|
-
const rawDate = asString(props["value"]);
|
|
151
|
-
const inputType = props["inputType"];
|
|
152
|
-
if (inputType === "time" || inputType === "datetime") {
|
|
153
|
-
const actionId = asActionId(action, "DatePicker", context);
|
|
154
|
-
const temporal = classifyTemporal(rawDate);
|
|
155
|
-
const parts = splitTemporalMinutes(rawDate);
|
|
156
|
-
if (inputType === "time" && temporal.kind === "time" || inputType === "datetime" && temporal.kind === "floating") {
|
|
157
|
-
if (parts?.droppedPrecision) warn(context, "dropped", "DatePicker", "Seconds were dropped because Slack timepickers only support minute precision.");
|
|
158
|
-
const timePicker = {
|
|
159
|
-
type: "timepicker",
|
|
160
|
-
action_id: actionId,
|
|
161
|
-
initial_time: parts.time
|
|
162
|
-
};
|
|
163
|
-
return inputType === "time" ? timePicker : [{
|
|
164
|
-
type: "datepicker",
|
|
165
|
-
action_id: actionId,
|
|
166
|
-
initial_date: parts.date
|
|
167
|
-
}, timePicker];
|
|
168
|
-
}
|
|
169
|
-
if (inputType === "datetime" && temporal.kind === "instant") {
|
|
170
|
-
if (temporal.epochMs % 1e3 !== 0 || /[1-9]/.test(temporal.subMillisecondDigits ?? "")) warn(context, "dropped", "DatePicker", "Fractional seconds were dropped because Slack datetimepickers only support second precision.");
|
|
171
|
-
return {
|
|
172
|
-
type: "datetimepicker",
|
|
173
|
-
action_id: actionId,
|
|
174
|
-
initial_date_time: Math.floor(temporal.epochMs / 1e3)
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
if (rawDate) warn(context, "dropped", "DatePicker", `value did not match the ${inputType} format and was dropped.`);
|
|
178
|
-
return {
|
|
179
|
-
type: inputType === "time" ? "timepicker" : "datetimepicker",
|
|
180
|
-
action_id: actionId
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
const initialDate = DATE_PATTERN.test(rawDate) ? rawDate : void 0;
|
|
184
|
-
if (rawDate && initialDate === void 0) warn(context, "dropped", "DatePicker", "value did not match the YYYY-MM-DD format and was dropped.");
|
|
185
|
-
return {
|
|
186
|
-
type: "datepicker",
|
|
187
|
-
action_id: asActionId(action, "DatePicker", context),
|
|
188
|
-
...initialDate !== void 0 ? { initial_date: initialDate } : {}
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
case "Checkbox": {
|
|
192
|
-
const label = clampText(asString(props["label"]), 75, "Checkbox", "label", context);
|
|
193
|
-
const name = asString(props["name"]);
|
|
194
|
-
const option = {
|
|
195
|
-
text: plainText(label),
|
|
196
|
-
value: name || label
|
|
197
|
-
};
|
|
198
|
-
return {
|
|
199
|
-
type: "checkboxes",
|
|
200
|
-
action_id: asActionId(action, "Checkbox", context),
|
|
201
|
-
options: [option],
|
|
202
|
-
...props["defaultChecked"] === true ? { initial_options: [option] } : {}
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
case "CheckboxGroup": {
|
|
206
|
-
const rawOptions = Array.isArray(props["options"]) ? props["options"] : [];
|
|
207
|
-
const { items: takenOptions, truncated } = copyBounded(rawOptions, 10);
|
|
208
|
-
if (truncated) warn(context, "clamped", "CheckboxGroup", `options were clamped to 10 entries.`);
|
|
209
|
-
const options = takenOptions.map((option) => optionFrom(option, "CheckboxGroup", context)).filter((option) => option !== void 0);
|
|
210
|
-
warnDroppedOptions(options.length, takenOptions.length, "CheckboxGroup", context);
|
|
211
|
-
const defaultValue = Array.isArray(props["defaultValue"]) ? props["defaultValue"] : [];
|
|
212
|
-
const initialOptions = options.filter((option) => defaultValue.includes(option.value));
|
|
213
|
-
return {
|
|
214
|
-
type: "checkboxes",
|
|
215
|
-
action_id: asActionId(action, "CheckboxGroup", context),
|
|
216
|
-
options,
|
|
217
|
-
...initialOptions.length > 0 ? { initial_options: initialOptions } : {}
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
case "RadioGroup": {
|
|
221
|
-
const rawOptions = Array.isArray(props["options"]) ? props["options"] : [];
|
|
222
|
-
const { items: takenOptions, truncated } = copyBounded(rawOptions, 10);
|
|
223
|
-
if (truncated) warn(context, "clamped", "RadioGroup", `options were clamped to 10 entries.`);
|
|
224
|
-
const options = takenOptions.map((option) => optionFrom(option, "RadioGroup", context)).filter((option) => option !== void 0);
|
|
225
|
-
warnDroppedOptions(options.length, takenOptions.length, "RadioGroup", context);
|
|
226
|
-
const selectedValue = typeof props["value"] === "string" ? props["value"] : asString(props["defaultValue"]);
|
|
227
|
-
const initialOption = options.find((option) => option.value === selectedValue);
|
|
228
|
-
return {
|
|
229
|
-
type: "radio_buttons",
|
|
230
|
-
action_id: asActionId(action, "RadioGroup", context),
|
|
231
|
-
options,
|
|
232
|
-
...initialOption !== void 0 ? { initial_option: initialOption } : {}
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
default: return;
|
|
236
|
-
}
|
|
237
|
-
};
|
|
238
|
-
const normalizedList = (node) => {
|
|
239
|
-
if (node === void 0 || node === null) return [];
|
|
240
|
-
if (!Array.isArray(node)) return [node];
|
|
241
|
-
return node.flatMap((child) => normalizedList(child));
|
|
242
|
-
};
|
|
243
|
-
const convertFacts = (facts, context) => {
|
|
244
|
-
const fields = facts.map((fact) => {
|
|
245
|
-
const label = asString(fact.props["label"]);
|
|
246
|
-
const value = asString(fact.props["value"]);
|
|
247
|
-
const delta = fact.props["delta"];
|
|
248
|
-
const deltaText = typeof delta === "string" ? formatFactDelta(delta, factTrend(delta, fact.props["trend"])) : void 0;
|
|
249
|
-
return {
|
|
250
|
-
type: "mrkdwn",
|
|
251
|
-
text: clampText(`*${label}*\n${deltaText === void 0 ? value : `${value} (${deltaText})`}`, FACT_FIELD_TEXT_CAP, "Fact", "field", context)
|
|
252
|
-
};
|
|
253
|
-
});
|
|
254
|
-
const blocks = [];
|
|
255
|
-
for (let index = 0; index < fields.length; index += 10) blocks.push({
|
|
256
|
-
type: "section",
|
|
257
|
-
fields: fields.slice(index, index + 10)
|
|
258
|
-
});
|
|
259
|
-
return blocks;
|
|
260
|
-
};
|
|
261
|
-
const convertActions = (elements, context) => {
|
|
262
|
-
const converted = elements.flatMap((source) => {
|
|
263
|
-
const result = toActionElement(source, context);
|
|
264
|
-
if (result === void 0) return [];
|
|
265
|
-
const controls = Array.isArray(result) ? result : [result];
|
|
266
|
-
const inputType = source.type === "DatePicker" ? source.props["inputType"] : void 0;
|
|
267
|
-
const temporal = inputType === "datetime" ? classifyTemporal(asString(source.props["value"])) : void 0;
|
|
268
|
-
const pairId = controls.length > 1 ? context.dateTimePairSequence++ : void 0;
|
|
269
|
-
return controls.map((element) => {
|
|
270
|
-
const name = asString(source.props["name"]);
|
|
271
|
-
return {
|
|
272
|
-
element,
|
|
273
|
-
field: inputType === "time" || inputType === "datetime" ? {
|
|
274
|
-
actionId: element.action_id,
|
|
275
|
-
name,
|
|
276
|
-
component: source.type,
|
|
277
|
-
inputType,
|
|
278
|
-
...temporal?.kind === "instant" ? { offset: temporal.offset } : {},
|
|
279
|
-
...pairId !== void 0 ? {
|
|
280
|
-
part: element.type === "datepicker" ? "date" : "time",
|
|
281
|
-
pairId
|
|
282
|
-
} : {}
|
|
283
|
-
} : name ? {
|
|
284
|
-
actionId: element.action_id,
|
|
285
|
-
name,
|
|
286
|
-
component: source.type
|
|
287
|
-
} : void 0
|
|
288
|
-
};
|
|
289
|
-
});
|
|
290
|
-
});
|
|
291
|
-
const blocks = [];
|
|
292
|
-
const groups = [];
|
|
293
|
-
let group = [];
|
|
294
|
-
let actionIds = /* @__PURE__ */ new Set();
|
|
295
|
-
const fieldsFor = (entries) => entries.flatMap(({ field }) => field === void 0 ? [] : [field]);
|
|
296
|
-
for (const entry of converted) {
|
|
297
|
-
const repeatedMappedActionId = actionIds.has(entry.element.action_id) && (entry.field !== void 0 || group.some((candidate) => candidate.element.action_id === entry.element.action_id && candidate.field !== void 0));
|
|
298
|
-
if (group.length === 25 || repeatedMappedActionId) {
|
|
299
|
-
groups.push(group);
|
|
300
|
-
group = [];
|
|
301
|
-
actionIds = /* @__PURE__ */ new Set();
|
|
302
|
-
}
|
|
303
|
-
group.push(entry);
|
|
304
|
-
actionIds.add(entry.element.action_id);
|
|
305
|
-
}
|
|
306
|
-
if (group.length > 0) groups.push(group);
|
|
307
|
-
const mappedGroups = [];
|
|
308
|
-
let nextSequence = context.fieldBlockIdSequence;
|
|
309
|
-
for (const entries of groups) {
|
|
310
|
-
let current = [];
|
|
311
|
-
for (const entry of entries) {
|
|
312
|
-
const candidate = [...current, entry];
|
|
313
|
-
const fields = fieldsFor(candidate);
|
|
314
|
-
const blockId = fields.length > 0 ? serializeFieldBlockId(nextSequence, fields) : void 0;
|
|
315
|
-
if (current.length > 0 && fields.length > fieldsFor(current).length && blockId !== void 0 && blockId.length > SLACK_BLOCK_ID_CAP) {
|
|
316
|
-
mappedGroups.push(current);
|
|
317
|
-
if (fieldsFor(current).length > 0) nextSequence += 1;
|
|
318
|
-
current = [entry];
|
|
319
|
-
} else current = candidate;
|
|
320
|
-
}
|
|
321
|
-
if (current.length > 0) {
|
|
322
|
-
mappedGroups.push(current);
|
|
323
|
-
if (fieldsFor(current).length > 0) nextSequence += 1;
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
for (const group of mappedGroups) {
|
|
327
|
-
const fields = fieldsFor(group);
|
|
328
|
-
const blockId = fieldBlockId(fields, context);
|
|
329
|
-
blocks.push({
|
|
330
|
-
type: "actions",
|
|
331
|
-
...blockId !== void 0 ? { block_id: blockId } : {},
|
|
332
|
-
elements: group.map(({ element }) => element)
|
|
333
|
-
});
|
|
334
|
-
}
|
|
335
|
-
return blocks;
|
|
336
|
-
};
|
|
337
|
-
const cardActionButtons = (props, context) => {
|
|
338
|
-
const buttons = [];
|
|
339
|
-
for (const key of ["confirm", "cancel"]) {
|
|
340
|
-
const footer = props[key];
|
|
341
|
-
if (!isRecord(footer)) continue;
|
|
342
|
-
buttons.push(buttonElement(asString(footer["label"]), key === "confirm" ? "primary" : void 0, footer["$action"], "Card", context));
|
|
343
|
-
}
|
|
344
|
-
return buttons;
|
|
345
|
-
};
|
|
346
|
-
const cardTitleText = (props, context) => {
|
|
347
|
-
const rawTitle = asString(props["title"]);
|
|
348
|
-
return rawTitle ? clampText(rawTitle, 150, "Card", "title", context) : void 0;
|
|
349
|
-
};
|
|
350
|
-
const isCardNoOp = (node) => isElement(node) && (node.type === "Spacer" || node.type === "Icon");
|
|
351
|
-
const collectText = (node, depth) => {
|
|
352
|
-
if (node === void 0 || node === null || depth > 64) return [];
|
|
353
|
-
if (typeof node === "string" || typeof node === "number") return [String(node)];
|
|
354
|
-
if (Array.isArray(node)) return node.flatMap((child) => collectText(child, depth + 1));
|
|
355
|
-
const element = node;
|
|
356
|
-
return [...[
|
|
357
|
-
"text",
|
|
358
|
-
"value",
|
|
359
|
-
"label",
|
|
360
|
-
"title",
|
|
361
|
-
"description"
|
|
362
|
-
].map((key) => element.props[key]).filter((value) => typeof value === "string"), ...collectText(element.children, depth + 1)];
|
|
363
|
-
};
|
|
364
|
-
/**
|
|
365
|
-
* Scans a card's children for the pieces the native card block shape can
|
|
366
|
-
* carry (a hero image, body text, and subtext), each taken from the first
|
|
367
|
-
* matching child. Anything left over means the card cannot map cleanly onto
|
|
368
|
-
* the native shape.
|
|
369
|
-
*/
|
|
370
|
-
const extractCardFields = (element) => {
|
|
371
|
-
const children = normalizedList(element.children);
|
|
372
|
-
let heroImage;
|
|
373
|
-
let bodyRaw;
|
|
374
|
-
let subtextRaw;
|
|
375
|
-
const leftover = [];
|
|
376
|
-
for (const child of children) {
|
|
377
|
-
if (heroImage === void 0 && isElement(child) && child.type === "Image") {
|
|
378
|
-
heroImage = {
|
|
379
|
-
type: "image",
|
|
380
|
-
image_url: asString(child.props["src"]),
|
|
381
|
-
alt_text: asString(child.props["alt"])
|
|
382
|
-
};
|
|
383
|
-
continue;
|
|
384
|
-
}
|
|
385
|
-
if (bodyRaw === void 0 && isElement(child) && (child.type === "Text" || child.type === "Markdown")) {
|
|
386
|
-
bodyRaw = asString(child.props["value"]);
|
|
387
|
-
continue;
|
|
388
|
-
}
|
|
389
|
-
if (subtextRaw === void 0 && isElement(child) && child.type === "Caption") {
|
|
390
|
-
subtextRaw = asString(child.props["value"]);
|
|
391
|
-
continue;
|
|
392
|
-
}
|
|
393
|
-
if (isCardNoOp(child)) continue;
|
|
394
|
-
leftover.push(child);
|
|
395
|
-
}
|
|
396
|
-
return {
|
|
397
|
-
heroImage,
|
|
398
|
-
bodyRaw,
|
|
399
|
-
subtextRaw,
|
|
400
|
-
leftover
|
|
401
|
-
};
|
|
402
|
-
};
|
|
403
|
-
/** Assembles a native card block, clamping its body and subtext text. */
|
|
404
|
-
const assembleCleanCard = (titleText, fields, buttons, context) => {
|
|
405
|
-
const body = fields.bodyRaw !== void 0 ? {
|
|
406
|
-
type: "mrkdwn",
|
|
407
|
-
text: clampText(fields.bodyRaw, 200, "Card", "body", context)
|
|
408
|
-
} : void 0;
|
|
409
|
-
const subtext = fields.subtextRaw !== void 0 ? {
|
|
410
|
-
type: "mrkdwn",
|
|
411
|
-
text: clampText(fields.subtextRaw, 200, "Card", "subtext", context)
|
|
412
|
-
} : void 0;
|
|
413
|
-
const { items: boundedButtons, truncated } = copyBounded(buttons, 3);
|
|
414
|
-
if (truncated) warn(context, "clamped", "Card", `actions were clamped to 3 entries.`);
|
|
415
|
-
return buildCardBlock({
|
|
416
|
-
...fields.heroImage !== void 0 ? { heroImage: fields.heroImage } : {},
|
|
417
|
-
...titleText !== void 0 ? { title: {
|
|
418
|
-
type: "mrkdwn",
|
|
419
|
-
text: titleText
|
|
420
|
-
} } : {},
|
|
421
|
-
...body !== void 0 ? { body } : {},
|
|
422
|
-
...subtext !== void 0 ? { subtext } : {},
|
|
423
|
-
...boundedButtons.length > 0 ? { actions: boundedButtons } : {}
|
|
424
|
-
});
|
|
425
|
-
};
|
|
426
|
-
/**
|
|
427
|
-
* A card requires at least one of `hero_image`, `title`, `body`, or `actions`
|
|
428
|
-
* (see {@link SlackCardBlock}); a card with none of them is not a valid
|
|
429
|
-
* native block and must be dropped instead of emitted.
|
|
430
|
-
*/
|
|
431
|
-
const isEmptyCard = (card) => card.hero_image === void 0 && card.title === void 0 && card.body === void 0 && (card.actions === void 0 || card.actions.length === 0);
|
|
432
|
-
const convertCard = (element, context, depth) => {
|
|
433
|
-
const titleText = cardTitleText(element.props, context);
|
|
434
|
-
const fields = extractCardFields(element);
|
|
435
|
-
const buttons = cardActionButtons(element.props, context);
|
|
436
|
-
if (fields.leftover.length > 0) {
|
|
437
|
-
warn(context, "fallback", "Card", "A card with content beyond hero_image, body, subtext, and actions was rendered inline.");
|
|
438
|
-
return [
|
|
439
|
-
...titleText ? [{
|
|
440
|
-
type: "header",
|
|
441
|
-
text: plainText(titleText)
|
|
442
|
-
}] : [],
|
|
443
|
-
...convertSequence(element.children, context, depth + 1),
|
|
444
|
-
...buttons.length > 0 ? [{
|
|
445
|
-
type: "actions",
|
|
446
|
-
elements: buttons
|
|
447
|
-
}] : []
|
|
448
|
-
];
|
|
449
|
-
}
|
|
450
|
-
const card = assembleCleanCard(titleText, fields, buttons, context);
|
|
451
|
-
if (isEmptyCard(card)) {
|
|
452
|
-
warn(context, "dropped", "Card", "A card with none of hero_image, title, body, or actions was dropped.");
|
|
453
|
-
return [];
|
|
454
|
-
}
|
|
455
|
-
return [card];
|
|
456
|
-
};
|
|
457
|
-
/** The content kinds a reshaped carousel card cannot carry, in report order. */
|
|
458
|
-
const LOST_CONTENT_KINDS = [
|
|
459
|
-
"images",
|
|
460
|
-
"tables",
|
|
461
|
-
"charts",
|
|
462
|
-
"controls"
|
|
463
|
-
];
|
|
464
|
-
const listPhrase = (items) => items.length <= 1 ? items[0] ?? "" : `${items.slice(0, -1).join(", ")} and ${items[items.length - 1]}`;
|
|
465
|
-
/**
|
|
466
|
-
* Collects the kinds of content a reshape drops, walking the same tree
|
|
467
|
-
* {@link collectText} does so a node nested below the top level counts too.
|
|
468
|
-
* These are exactly the pieces text cannot represent: an image has no text, a
|
|
469
|
-
* table and a chart carry theirs in array props, and a control is behavior,
|
|
470
|
-
* whether or not it carries an action of its own. An `$action` counts only on
|
|
471
|
-
* a node that renders a control from it, since a `Box` or a `Row` carrying one
|
|
472
|
-
* renders no control on the clean path either.
|
|
473
|
-
*/
|
|
474
|
-
const scanLostContent = (node, into, depth) => {
|
|
475
|
-
if (depth > 64) return;
|
|
476
|
-
if (Array.isArray(node)) {
|
|
477
|
-
for (const child of node) scanLostContent(child, into, depth + 1);
|
|
478
|
-
return;
|
|
479
|
-
}
|
|
480
|
-
if (node === void 0 || !isElement(node)) return;
|
|
481
|
-
if (CONTROL_TYPES.has(node.type) || node.type === "ListViewItem" && isRecord(node.action) || node.type === "Card" && (isRecord(node.props["confirm"]) || isRecord(node.props["cancel"]))) into.add("controls");
|
|
482
|
-
if (node.type === "Image") into.add("images");
|
|
483
|
-
if (node.type === "Table") into.add("tables");
|
|
484
|
-
if (node.type === "Chart") into.add("charts");
|
|
485
|
-
scanLostContent(node.children, into, depth + 1);
|
|
486
|
-
};
|
|
487
|
-
/**
|
|
488
|
-
* Degrades a card that cannot map cleanly into a title-and-body card block.
|
|
489
|
-
* A carousel cannot fall back to a block sequence like a standalone card can,
|
|
490
|
-
* so the card is reshaped to the two text fields the block has. Text reachable
|
|
491
|
-
* by {@link collectText} survives at any depth; an image, a table, a chart,
|
|
492
|
-
* and a control do not, and are reported separately from the reshape itself.
|
|
493
|
-
*/
|
|
494
|
-
const degradeCard = (element, context, depth) => {
|
|
495
|
-
const rawTitle = asString(element.props["title"]);
|
|
496
|
-
const textChunks = collectText(element.children, depth + 1);
|
|
497
|
-
const titleSource = rawTitle || textChunks[0] || "";
|
|
498
|
-
const bodyChunks = rawTitle ? textChunks : textChunks.slice(1);
|
|
499
|
-
const titleText = clampText(titleSource, 150, "Card", "title", context);
|
|
500
|
-
const bodyText = clampText(bodyChunks.join("\n"), 200, "Card", "body", context);
|
|
501
|
-
warn(context, "fallback", "Card", "A card inside a carousel was reshaped to title and body.");
|
|
502
|
-
const lostKinds = /* @__PURE__ */ new Set();
|
|
503
|
-
if (isRecord(element.props["confirm"]) || isRecord(element.props["cancel"])) lostKinds.add("controls");
|
|
504
|
-
scanLostContent(element.children, lostKinds, depth + 1);
|
|
505
|
-
const lost = LOST_CONTENT_KINDS.filter((kind) => lostKinds.has(kind));
|
|
506
|
-
if (lost.length > 0) warn(context, "dropped", "Card", `A reshaped carousel card's ${listPhrase(lost)} were dropped.`);
|
|
507
|
-
return buildCardBlock({
|
|
508
|
-
...titleText ? { title: {
|
|
509
|
-
type: "mrkdwn",
|
|
510
|
-
text: titleText
|
|
511
|
-
} } : {},
|
|
512
|
-
...bodyText ? { body: {
|
|
513
|
-
type: "mrkdwn",
|
|
514
|
-
text: bodyText
|
|
515
|
-
} } : {}
|
|
516
|
-
});
|
|
517
|
-
};
|
|
518
|
-
const convertCarouselCard = (element, context, depth) => {
|
|
519
|
-
const fields = extractCardFields(element);
|
|
520
|
-
const card = fields.leftover.length === 0 ? assembleCleanCard(cardTitleText(element.props, context), fields, cardActionButtons(element.props, context), context) : degradeCard(element, context, depth);
|
|
521
|
-
if (isEmptyCard(card)) {
|
|
522
|
-
warn(context, "dropped", "Card", "A card with none of hero_image, title, body, or actions was dropped.");
|
|
523
|
-
return;
|
|
524
|
-
}
|
|
525
|
-
return card;
|
|
526
|
-
};
|
|
527
|
-
const isBadgeOrCaption = (node) => isElement(node) && (node.type === "Badge" || node.type === "Caption");
|
|
528
|
-
const contextRow = (element, context) => {
|
|
529
|
-
const allChildren = normalizedList(element.children);
|
|
530
|
-
const children = allChildren.filter(isBadgeOrCaption);
|
|
531
|
-
if (allChildren.length === 0 || children.length !== allChildren.length) return;
|
|
532
|
-
const { items: boundedChildren, truncated } = copyBounded(children, 10);
|
|
533
|
-
if (truncated) warn(context, "clamped", "Row", `context elements were clamped to 10 entries.`);
|
|
534
|
-
return [{
|
|
535
|
-
type: "context",
|
|
536
|
-
elements: boundedChildren.map((child) => ({
|
|
537
|
-
type: "mrkdwn",
|
|
538
|
-
text: clampText(asString(child.props["value"]), CONTEXT_TEXT_CAP, child.type, "value", context)
|
|
539
|
-
}))
|
|
540
|
-
}];
|
|
541
|
-
};
|
|
542
|
-
const convertListItem = (element, context, depth) => {
|
|
543
|
-
const text = clampText(collectText(element.children, depth + 1).join("\n"), SECTION_TEXT_CAP, "ListViewItem", "text", context);
|
|
544
|
-
const accessory = isRecord(element.action) ? buttonElement("Open", void 0, element.action, "ListViewItem", context) : void 0;
|
|
545
|
-
return {
|
|
546
|
-
type: "section",
|
|
547
|
-
text: {
|
|
548
|
-
type: "mrkdwn",
|
|
549
|
-
text
|
|
550
|
-
},
|
|
551
|
-
...accessory !== void 0 ? { accessory } : {}
|
|
552
|
-
};
|
|
553
|
-
};
|
|
554
|
-
/**
|
|
555
|
-
* Converts a child whose output is thrown away, and reports whether anything
|
|
556
|
-
* was lost with it. A scratch context keeps the throwaway out of the shared
|
|
557
|
-
* markdown and data-table budgets that surviving blocks still need; only its
|
|
558
|
-
* `dropped` warnings are forwarded, because those describe the tree the caller
|
|
559
|
-
* wrote, while a clamp or a fallback would describe content never delivered.
|
|
560
|
-
*/
|
|
561
|
-
const discardedChild = (child, context, depth) => {
|
|
562
|
-
const scratch = {
|
|
563
|
-
...context,
|
|
564
|
-
warnings: []
|
|
565
|
-
};
|
|
566
|
-
const produced = convertSequence(child, scratch, depth).length > 0;
|
|
567
|
-
const lost = scratch.warnings.filter((warning) => warning.code === "dropped");
|
|
568
|
-
context.warnings.push(...lost);
|
|
569
|
-
return produced || lost.length > 0;
|
|
570
|
-
};
|
|
571
|
-
const convertListView = (element, context, depth) => {
|
|
572
|
-
const items = [];
|
|
573
|
-
let discarded = 0;
|
|
574
|
-
for (const child of normalizedList(element.children)) {
|
|
575
|
-
if (isElement(child) && child.type === "ListViewItem") {
|
|
576
|
-
items.push(child);
|
|
577
|
-
continue;
|
|
578
|
-
}
|
|
579
|
-
if (discardedChild(child, context, depth + 1)) discarded += 1;
|
|
580
|
-
}
|
|
581
|
-
if (discarded > 0) warn(context, "dropped", "ListView", `${discarded} non-item ${discarded === 1 ? "child was" : "children were"} dropped.`);
|
|
582
|
-
return items.flatMap((item, index) => [...index > 0 ? [{ type: "divider" }] : [], convertListItem(item, context, depth + 1)]);
|
|
583
|
-
};
|
|
584
|
-
const convertCarousel = (element, context, depth) => {
|
|
585
|
-
const cardChildren = [];
|
|
586
|
-
let droppedCards = 0;
|
|
587
|
-
for (const child of normalizedList(element.children)) {
|
|
588
|
-
if (isElement(child) && child.type === "Card") {
|
|
589
|
-
cardChildren.push(child);
|
|
590
|
-
continue;
|
|
591
|
-
}
|
|
592
|
-
if (discardedChild(child, context, depth + 1)) droppedCards += 1;
|
|
593
|
-
}
|
|
594
|
-
if (droppedCards > 0) warn(context, "dropped", "Carousel", `${droppedCards} non-card ${droppedCards === 1 ? "child was" : "children were"} dropped.`);
|
|
595
|
-
const { items: boundedCards, truncated } = copyBounded(cardChildren, 10);
|
|
596
|
-
if (truncated) warn(context, "clamped", "Carousel", `cards were clamped to 10 entries.`);
|
|
597
|
-
const cards = boundedCards.map((card) => convertCarouselCard(card, context, depth + 1)).filter((card) => card !== void 0);
|
|
598
|
-
if (cards.length < 1) {
|
|
599
|
-
warn(context, "dropped", "Carousel", "A carousel without renderable cards was dropped.");
|
|
600
|
-
return [];
|
|
601
|
-
}
|
|
602
|
-
return [buildCarouselBlock(cards)];
|
|
603
|
-
};
|
|
604
|
-
const toDataTableCell = (value, format) => {
|
|
605
|
-
if (typeof value === "number" && format === void 0) return {
|
|
606
|
-
type: "raw_number",
|
|
607
|
-
text: String(value)
|
|
608
|
-
};
|
|
609
|
-
if (typeof value === "string" || typeof value === "boolean") return {
|
|
610
|
-
type: "raw_text",
|
|
611
|
-
text: formatValue(value, format)
|
|
612
|
-
};
|
|
613
|
-
if (typeof value === "number") return {
|
|
614
|
-
type: "raw_text",
|
|
615
|
-
text: formatValue(value, format)
|
|
616
|
-
};
|
|
617
|
-
};
|
|
618
|
-
const rowCharacters = (row) => row.reduce((sum, cell) => sum + cell.text.length, 0);
|
|
619
|
-
const convertTable = (element, context) => {
|
|
620
|
-
const rawColumns = Array.isArray(element.props["columns"]) ? element.props["columns"] : [];
|
|
621
|
-
const rawRows = Array.isArray(element.props["rows"]) ? element.props["rows"] : [];
|
|
622
|
-
const { items: takenColumns, truncated: columnsTruncated } = copyBounded(rawColumns, 20);
|
|
623
|
-
const { items: takenRows, truncated: rowsTruncated } = copyBounded(rawRows, 200);
|
|
624
|
-
if (columnsTruncated) warn(context, "clamped", "Table", `columns were clamped to 20 entries.`);
|
|
625
|
-
if (rowsTruncated) warn(context, "clamped", "Table", `rows were clamped to 200 entries.`);
|
|
626
|
-
const unlabeled = takenColumns.filter((column) => !isRecord(column) || typeof column["label"] !== "string").length;
|
|
627
|
-
if (unlabeled > 0) warn(context, "dropped", "Table", `${unlabeled} column ${unlabeled === 1 ? "header was" : "headers were"} left blank for want of a string label.`);
|
|
628
|
-
const columnHeaderRow = takenColumns.map((column) => ({
|
|
629
|
-
type: "raw_text",
|
|
630
|
-
text: isRecord(column) ? asString(column["label"]) : ""
|
|
631
|
-
}));
|
|
632
|
-
const dataRows = takenRows.map((row) => (Array.isArray(row) ? copyBounded(row, 20).items : []).map((value, index) => toDataTableCell(value, isRecord(takenColumns[index]) ? takenColumns[index]["format"] : void 0) ?? {
|
|
633
|
-
type: "raw_text",
|
|
634
|
-
text: ""
|
|
635
|
-
}));
|
|
636
|
-
const width = Math.max(columnHeaderRow.length, 0, ...dataRows.map((row) => row.length));
|
|
637
|
-
const padRow = (row) => row.length >= width ? row : [...row, ...Array.from({ length: width - row.length }, () => ({
|
|
638
|
-
type: "raw_text",
|
|
639
|
-
text: ""
|
|
640
|
-
}))];
|
|
641
|
-
const headerRow = padRow(columnHeaderRow);
|
|
642
|
-
const headerCharacters = rowCharacters(headerRow);
|
|
643
|
-
if (context.dataTableCharacters + headerCharacters > 2e4) {
|
|
644
|
-
warn(context, "dropped", "Table", `the table was dropped because its header row exceeds the ${DATA_TABLE_CHAR_BUDGET}-character table budget.`);
|
|
645
|
-
return [];
|
|
646
|
-
}
|
|
647
|
-
const kept = [headerRow];
|
|
648
|
-
let tableCharacters = headerCharacters;
|
|
649
|
-
let budgetClamped = false;
|
|
650
|
-
for (const row of dataRows.map(padRow)) {
|
|
651
|
-
const chars = rowCharacters(row);
|
|
652
|
-
if (context.dataTableCharacters + tableCharacters + chars > 2e4) {
|
|
653
|
-
budgetClamped = true;
|
|
654
|
-
break;
|
|
655
|
-
}
|
|
656
|
-
kept.push(row);
|
|
657
|
-
tableCharacters += chars;
|
|
658
|
-
}
|
|
659
|
-
context.dataTableCharacters += tableCharacters;
|
|
660
|
-
if (budgetClamped) warn(context, "clamped", "Table", `rows were clamped to fit the ${DATA_TABLE_CHAR_BUDGET}-character table budget.`);
|
|
661
|
-
return [buildDataTableBlock(kept)];
|
|
662
|
-
};
|
|
663
|
-
const convertElement = (element, context, depth) => {
|
|
664
|
-
const { props } = element;
|
|
665
|
-
switch (element.type) {
|
|
666
|
-
case "Header": return [{
|
|
667
|
-
type: "header",
|
|
668
|
-
text: plainText(clampText(asString(props["text"]), 150, "Header", "text", context))
|
|
669
|
-
}];
|
|
670
|
-
case "Text": return [{
|
|
671
|
-
type: "section",
|
|
672
|
-
text: {
|
|
673
|
-
type: "mrkdwn",
|
|
674
|
-
text: clampText(asString(props["value"]), SECTION_TEXT_CAP, "Text", "value", context)
|
|
675
|
-
}
|
|
676
|
-
}];
|
|
677
|
-
case "Caption":
|
|
678
|
-
case "Badge": return [{
|
|
679
|
-
type: "context",
|
|
680
|
-
elements: [{
|
|
681
|
-
type: "mrkdwn",
|
|
682
|
-
text: clampText(asString(props["value"]), CONTEXT_TEXT_CAP, element.type, "value", context)
|
|
683
|
-
}]
|
|
684
|
-
}];
|
|
685
|
-
case "Fact": return convertFacts([element], context);
|
|
686
|
-
case "Image": return [{
|
|
687
|
-
type: "image",
|
|
688
|
-
image_url: asString(props["src"]),
|
|
689
|
-
alt_text: asString(props["alt"])
|
|
690
|
-
}];
|
|
691
|
-
case "Divider": return [{ type: "divider" }];
|
|
692
|
-
case "Button":
|
|
693
|
-
case "Select":
|
|
694
|
-
case "DatePicker":
|
|
695
|
-
case "Checkbox":
|
|
696
|
-
case "CheckboxGroup":
|
|
697
|
-
case "RadioGroup": return convertActions([element], context);
|
|
698
|
-
case "Slider": {
|
|
699
|
-
const label = clampText(asString(props["label"]), INPUT_LABEL_CAP, "Slider", "label", context);
|
|
700
|
-
const min = asFiniteNumber(props["min"]);
|
|
701
|
-
const max = asFiniteNumber(props["max"]);
|
|
702
|
-
const step = asFiniteNumber(props["step"]);
|
|
703
|
-
const defaultValue = asFiniteNumber(props["defaultValue"]);
|
|
704
|
-
const actionId = asActionId(element.action, "Slider", context);
|
|
705
|
-
const name = props["name"];
|
|
706
|
-
const blockId = fieldBlockId(typeof name === "string" && name.length > 0 ? [{
|
|
707
|
-
actionId,
|
|
708
|
-
name,
|
|
709
|
-
component: "Slider"
|
|
710
|
-
}] : [], context);
|
|
711
|
-
return [{
|
|
712
|
-
type: "input",
|
|
713
|
-
...blockId !== void 0 ? { block_id: blockId } : {},
|
|
714
|
-
label: plainText(label),
|
|
715
|
-
element: {
|
|
716
|
-
type: "number_input",
|
|
717
|
-
action_id: actionId,
|
|
718
|
-
...min !== void 0 ? { min_value: min } : {},
|
|
719
|
-
...max !== void 0 ? { max_value: max } : {},
|
|
720
|
-
...defaultValue !== void 0 ? { initial_value: defaultValue } : {},
|
|
721
|
-
...[
|
|
722
|
-
step,
|
|
723
|
-
min,
|
|
724
|
-
max,
|
|
725
|
-
defaultValue
|
|
726
|
-
].some((value) => value !== void 0 && !Number.isInteger(value)) ? { is_decimal_allowed: true } : {}
|
|
727
|
-
}
|
|
728
|
-
}];
|
|
729
|
-
}
|
|
730
|
-
case "Input": {
|
|
731
|
-
if (props["inputType"] === "password") {
|
|
732
|
-
warn(context, "dropped", "Input", "Password input was replaced by a Slack omission note.");
|
|
733
|
-
return [{
|
|
734
|
-
type: "context",
|
|
735
|
-
elements: [{
|
|
736
|
-
type: "mrkdwn",
|
|
737
|
-
text: "Password input omitted on Slack."
|
|
738
|
-
}]
|
|
739
|
-
}];
|
|
740
|
-
}
|
|
741
|
-
const label = clampText(asString(props["label"]), INPUT_LABEL_CAP, "Input", "label", context);
|
|
742
|
-
const placeholder = clampText(asString(props["placeholder"]), 150, "Input", "placeholder", context);
|
|
743
|
-
const actionId = asActionId(element.action, "Input", context);
|
|
744
|
-
const name = props["name"];
|
|
745
|
-
const blockId = fieldBlockId(typeof name === "string" && name.length > 0 ? [{
|
|
746
|
-
actionId,
|
|
747
|
-
name,
|
|
748
|
-
component: "Input"
|
|
749
|
-
}] : [], context);
|
|
750
|
-
const defaultValue = props["defaultValue"];
|
|
751
|
-
return [{
|
|
752
|
-
type: "input",
|
|
753
|
-
...blockId !== void 0 ? { block_id: blockId } : {},
|
|
754
|
-
label: plainText(label),
|
|
755
|
-
element: {
|
|
756
|
-
type: "plain_text_input",
|
|
757
|
-
action_id: actionId,
|
|
758
|
-
...props["multiline"] === true && (props["inputType"] === void 0 || props["inputType"] === "text") ? { multiline: true } : {},
|
|
759
|
-
...typeof defaultValue === "string" && defaultValue ? { initial_value: defaultValue } : {},
|
|
760
|
-
...placeholder ? { placeholder: plainText(placeholder) } : {}
|
|
761
|
-
}
|
|
762
|
-
}];
|
|
763
|
-
}
|
|
764
|
-
case "Alert": {
|
|
765
|
-
const tone = props["tone"] === "success" || props["tone"] === "warning" || props["tone"] === "danger" ? props["tone"] : "info";
|
|
766
|
-
const titleText = asString(props["title"]);
|
|
767
|
-
const description = asString(props["description"]);
|
|
768
|
-
if (context.surface === "modal") {
|
|
769
|
-
const combined = [titleText, description].filter(Boolean).join("\n");
|
|
770
|
-
return [buildAlertBlock(clampText(combined, 200, "Alert", "text", context), tone)];
|
|
771
|
-
}
|
|
772
|
-
warn(context, "fallback", "Alert", "Alert was rendered with message-surface fallback blocks.");
|
|
773
|
-
return [...buildAlertFallback(titleText, clampText(description, SECTION_TEXT_CAP, "Alert", "description", context), tone)];
|
|
774
|
-
}
|
|
775
|
-
case "Card": return convertCard(element, context, depth);
|
|
776
|
-
case "Carousel": return convertCarousel(element, context, depth);
|
|
777
|
-
case "Col":
|
|
778
|
-
case "Box": return convertSequence(element.children, context, depth + 1);
|
|
779
|
-
case "Row": return contextRow(element, context) ?? convertSequence(element.children, context, depth + 1);
|
|
780
|
-
case "Spacer":
|
|
781
|
-
case "Icon": return [];
|
|
782
|
-
case "Table": return convertTable(element, context);
|
|
783
|
-
case "Markdown": {
|
|
784
|
-
const value = asString(props["value"]);
|
|
785
|
-
if (!context.markdownExhausted && context.markdownCharacters + value.length <= 12e3) {
|
|
786
|
-
context.markdownCharacters += value.length;
|
|
787
|
-
return [{
|
|
788
|
-
type: "markdown",
|
|
789
|
-
text: value
|
|
790
|
-
}];
|
|
791
|
-
}
|
|
792
|
-
context.markdownExhausted = true;
|
|
793
|
-
warn(context, "fallback", "Markdown", `The ${MARKDOWN_TEXT_BUDGET}-character markdown budget was exceeded.`);
|
|
794
|
-
return [{
|
|
795
|
-
type: "section",
|
|
796
|
-
text: {
|
|
797
|
-
type: "mrkdwn",
|
|
798
|
-
text: clampText(value, SECTION_TEXT_CAP, "Markdown", "fallback text", context)
|
|
799
|
-
}
|
|
800
|
-
}];
|
|
801
|
-
}
|
|
802
|
-
case "Chart":
|
|
803
|
-
warn(context, "dropped", "Chart", "Chart was replaced by a Slack omission note.");
|
|
804
|
-
return [{
|
|
805
|
-
type: "context",
|
|
806
|
-
elements: [{
|
|
807
|
-
type: "mrkdwn",
|
|
808
|
-
text: "Chart omitted on Slack."
|
|
809
|
-
}]
|
|
810
|
-
}];
|
|
811
|
-
case "ListView": return convertListView(element, context, depth);
|
|
812
|
-
case "ListViewItem": return [convertListItem(element, context, depth)];
|
|
813
|
-
case "Form": return [...convertSequence(element.children, context, depth + 1), {
|
|
814
|
-
type: "actions",
|
|
815
|
-
elements: [buttonElement("Submit", "primary", element.action, "Form", context)]
|
|
816
|
-
}];
|
|
817
|
-
default:
|
|
818
|
-
warn(context, "dropped", element.type, "Unknown component type was dropped.");
|
|
819
|
-
return [];
|
|
820
|
-
}
|
|
821
|
-
};
|
|
822
|
-
function convertSequence(node, context, depth) {
|
|
823
|
-
if (node === void 0 || node === null || depth > 64) return [];
|
|
824
|
-
const nodes = Array.isArray(node) ? node : [node];
|
|
825
|
-
const blocks = [];
|
|
826
|
-
for (let index = 0; index < nodes.length;) {
|
|
827
|
-
const current = nodes[index];
|
|
828
|
-
if (current === void 0 || current === null) {
|
|
829
|
-
index += 1;
|
|
830
|
-
continue;
|
|
831
|
-
}
|
|
832
|
-
if (isElement(current) && current.type === "Fact") {
|
|
833
|
-
const { run: facts, next } = takeRun(nodes, index, (candidate) => candidate.type === "Fact");
|
|
834
|
-
index = next;
|
|
835
|
-
blocks.push(...convertFacts(facts, context));
|
|
836
|
-
continue;
|
|
837
|
-
}
|
|
838
|
-
if (isElement(current) && INTERACTIVE_TYPES.has(current.type)) {
|
|
839
|
-
const { run: controls, next } = takeRun(nodes, index, (candidate) => INTERACTIVE_TYPES.has(candidate.type));
|
|
840
|
-
index = next;
|
|
841
|
-
blocks.push(...convertActions(controls, context));
|
|
842
|
-
continue;
|
|
843
|
-
}
|
|
844
|
-
if (typeof current === "string" || typeof current === "number") blocks.push({
|
|
845
|
-
type: "section",
|
|
846
|
-
text: {
|
|
847
|
-
type: "mrkdwn",
|
|
848
|
-
text: clampText(String(current), SECTION_TEXT_CAP, "Text", "value", context)
|
|
849
|
-
}
|
|
850
|
-
});
|
|
851
|
-
else if (Array.isArray(current)) blocks.push(...convertSequence(current, context, depth + 1));
|
|
852
|
-
else blocks.push(...convertElement(current, context, depth + 1));
|
|
853
|
-
index += 1;
|
|
854
|
-
}
|
|
855
|
-
return blocks;
|
|
856
|
-
}
|
|
857
|
-
/** Converts a generative-UI tree into Slack Block Kit JSON and downgrade warnings. */
|
|
858
|
-
function toSlackBlocks(node, options) {
|
|
859
|
-
const surface = options?.surface === "modal" ? "modal" : "message";
|
|
860
|
-
const context = {
|
|
861
|
-
blockCap: surface === "modal" ? 100 : 50,
|
|
862
|
-
surface,
|
|
863
|
-
warnings: [],
|
|
864
|
-
omittedPasswordNames: /* @__PURE__ */ new Set(),
|
|
865
|
-
fieldBlockIdSequence: 0,
|
|
866
|
-
dateTimePairSequence: 0,
|
|
867
|
-
markdownCharacters: 0,
|
|
868
|
-
markdownExhausted: false,
|
|
869
|
-
dataTableCharacters: 0
|
|
870
|
-
};
|
|
871
|
-
try {
|
|
872
|
-
const bounded = boundSpec(node, (reason) => warn(context, "clamped", "Root", clampReasonDetail(reason)));
|
|
873
|
-
const { root } = normalizeSpec(bounded);
|
|
874
|
-
collectOmittedPasswordNames(root, context.omittedPasswordNames);
|
|
875
|
-
const converted = convertSequence(root, context, 0);
|
|
876
|
-
if (converted.length <= context.blockCap) return {
|
|
877
|
-
blocks: converted,
|
|
878
|
-
warnings: context.warnings
|
|
879
|
-
};
|
|
880
|
-
const kept = converted.slice(0, context.blockCap - 1);
|
|
881
|
-
const omitted = converted.length - kept.length;
|
|
882
|
-
warn(context, "clamped", "Root", `${omitted} ${omitted === 1 ? "block was" : "blocks were"} omitted.`);
|
|
883
|
-
return {
|
|
884
|
-
blocks: [...kept, buildBlockBudgetNote(omitted)],
|
|
885
|
-
warnings: context.warnings
|
|
886
|
-
};
|
|
887
|
-
} catch {
|
|
888
|
-
return {
|
|
889
|
-
blocks: [],
|
|
890
|
-
warnings: [{
|
|
891
|
-
code: "dropped",
|
|
892
|
-
component: "Root",
|
|
893
|
-
detail: "Input could not be converted."
|
|
894
|
-
}]
|
|
895
|
-
};
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
|
-
//#endregion
|
|
899
|
-
export { toSlackBlocks };
|
|
900
|
-
|
|
901
|
-
//# sourceMappingURL=toSlackBlocks.js.map
|