@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
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
import { decodeBlockAction } from "./decodeBlockAction";
|
|
1
2
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
CHILDREN_CAP,
|
|
5
|
+
MAX_TRAVERSAL_DEPTH,
|
|
6
|
+
NODE_BUDGET,
|
|
7
|
+
} from "../convert/boundSpec";
|
|
3
8
|
import { toSlackBlocks } from "./toSlackBlocks";
|
|
4
9
|
import {
|
|
5
10
|
ACTION_ID_CAP,
|
|
@@ -10,6 +15,7 @@ import {
|
|
|
10
15
|
CARD_SUBTEXT_CAP,
|
|
11
16
|
CARD_TITLE_CAP,
|
|
12
17
|
CAROUSEL_CARD_CAP,
|
|
18
|
+
CHECKBOX_OPTION_CAP,
|
|
13
19
|
CONTEXT_ELEMENT_CAP,
|
|
14
20
|
CONTEXT_TEXT_CAP,
|
|
15
21
|
DATA_TABLE_CHAR_BUDGET,
|
|
@@ -172,6 +178,19 @@ describe("toSlackBlocks", () => {
|
|
|
172
178
|
expect(warnings).toEqual([]);
|
|
173
179
|
});
|
|
174
180
|
|
|
181
|
+
it("appends a directional fact delta to the value text", () => {
|
|
182
|
+
const { blocks } = toSlackBlocks({
|
|
183
|
+
$type: "Fact",
|
|
184
|
+
label: "Revenue",
|
|
185
|
+
value: "$12.4k",
|
|
186
|
+
delta: "8%",
|
|
187
|
+
trend: "down",
|
|
188
|
+
});
|
|
189
|
+
expect((blocks[0] as SlackSectionBlock).fields?.[0]?.text).toBe(
|
|
190
|
+
"*Revenue*\n$12.4k (↓ 8%)",
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
|
|
175
194
|
it("breaks Fact merging on a non-Fact sibling", () => {
|
|
176
195
|
const root = [
|
|
177
196
|
{ $type: "Fact", label: "A", value: "1" },
|
|
@@ -334,6 +353,37 @@ describe("toSlackBlocks", () => {
|
|
|
334
353
|
});
|
|
335
354
|
});
|
|
336
355
|
|
|
356
|
+
it("preserves field references until state is available to the decoder", () => {
|
|
357
|
+
const { blocks, warnings } = toSlackBlocks({
|
|
358
|
+
$type: "Button",
|
|
359
|
+
label: "Save",
|
|
360
|
+
$action: {
|
|
361
|
+
type: "save",
|
|
362
|
+
note: { $field: "note" },
|
|
363
|
+
form: {
|
|
364
|
+
id: 7,
|
|
365
|
+
name: { $field: "name", fallback: "Ada" },
|
|
366
|
+
plan: [{ $field: "plan" }],
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
});
|
|
370
|
+
expect((blocks[0] as SlackActionsBlock).elements[0]).toMatchObject({
|
|
371
|
+
action_id: "save",
|
|
372
|
+
value: JSON.stringify({
|
|
373
|
+
note: { $field: "note" },
|
|
374
|
+
form: {
|
|
375
|
+
id: 7,
|
|
376
|
+
name: { $field: "name", fallback: "Ada" },
|
|
377
|
+
plan: [{ $field: "plan" }],
|
|
378
|
+
},
|
|
379
|
+
}),
|
|
380
|
+
});
|
|
381
|
+
expect(warnings).toEqual([]);
|
|
382
|
+
expect(
|
|
383
|
+
decodeBlockAction((blocks[0] as SlackActionsBlock).elements[0]),
|
|
384
|
+
).toEqual({ type: "save", form: { id: 7, name: "Ada", plan: [] } });
|
|
385
|
+
});
|
|
386
|
+
|
|
337
387
|
it(`omits value instead of truncating and warns when the serialized action payload exceeds ${BUTTON_VALUE_CAP} characters`, () => {
|
|
338
388
|
const { blocks, warnings } = toSlackBlocks({
|
|
339
389
|
$type: "Button",
|
|
@@ -394,6 +444,51 @@ describe("toSlackBlocks", () => {
|
|
|
394
444
|
});
|
|
395
445
|
});
|
|
396
446
|
|
|
447
|
+
it("sets initial_option from a matching defaultValue", () => {
|
|
448
|
+
const { blocks } = toSlackBlocks({
|
|
449
|
+
$type: "Select",
|
|
450
|
+
defaultValue: "b",
|
|
451
|
+
options: [
|
|
452
|
+
{ label: "Alpha", value: "a" },
|
|
453
|
+
{ label: "Beta", value: "b" },
|
|
454
|
+
],
|
|
455
|
+
});
|
|
456
|
+
expect(
|
|
457
|
+
(
|
|
458
|
+
(blocks[0] as SlackActionsBlock)
|
|
459
|
+
.elements[0] as SlackStaticSelectElement
|
|
460
|
+
).initial_option,
|
|
461
|
+
).toEqual({ text: { type: "plain_text", text: "Beta" }, value: "b" });
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it("sets initial_option from a matching empty defaultValue", () => {
|
|
465
|
+
const { blocks } = toSlackBlocks({
|
|
466
|
+
$type: "Select",
|
|
467
|
+
defaultValue: "",
|
|
468
|
+
options: [{ label: "No preference", value: "" }],
|
|
469
|
+
});
|
|
470
|
+
expect(
|
|
471
|
+
(
|
|
472
|
+
(blocks[0] as SlackActionsBlock)
|
|
473
|
+
.elements[0] as SlackStaticSelectElement
|
|
474
|
+
).initial_option,
|
|
475
|
+
).toEqual({
|
|
476
|
+
text: { type: "plain_text", text: "No preference" },
|
|
477
|
+
value: "",
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
it("omits initial_option when defaultValue has no matching option", () => {
|
|
482
|
+
const { blocks } = toSlackBlocks({
|
|
483
|
+
$type: "Select",
|
|
484
|
+
defaultValue: "missing",
|
|
485
|
+
options: [{ label: "Alpha", value: "a" }],
|
|
486
|
+
});
|
|
487
|
+
expect((blocks[0] as SlackActionsBlock).elements[0]).not.toHaveProperty(
|
|
488
|
+
"initial_option",
|
|
489
|
+
);
|
|
490
|
+
});
|
|
491
|
+
|
|
397
492
|
it(`clamps options past ${SELECT_OPTION_CAP} entries and warns`, () => {
|
|
398
493
|
const options = Array.from({ length: SELECT_OPTION_CAP + 5 }, (_, i) => ({
|
|
399
494
|
label: `L${i}`,
|
|
@@ -479,6 +574,153 @@ describe("toSlackBlocks", () => {
|
|
|
479
574
|
});
|
|
480
575
|
|
|
481
576
|
describe("Input", () => {
|
|
577
|
+
it("keeps number text and drops the omitted password's field fallback", () => {
|
|
578
|
+
const { blocks, warnings } = toSlackBlocks([
|
|
579
|
+
{
|
|
580
|
+
$type: "Input",
|
|
581
|
+
name: "quantity",
|
|
582
|
+
inputType: "number",
|
|
583
|
+
defaultValue: "2.5",
|
|
584
|
+
$action: { type: "edit_quantity" },
|
|
585
|
+
},
|
|
586
|
+
{
|
|
587
|
+
$type: "Input",
|
|
588
|
+
name: "password",
|
|
589
|
+
label: "Password",
|
|
590
|
+
inputType: "password",
|
|
591
|
+
defaultValue: "secret",
|
|
592
|
+
},
|
|
593
|
+
{
|
|
594
|
+
$type: "Button",
|
|
595
|
+
label: "Submit",
|
|
596
|
+
$action: {
|
|
597
|
+
type: "submit",
|
|
598
|
+
quantity: { $field: "quantity" },
|
|
599
|
+
password: { $field: "password", fallback: "secret" },
|
|
600
|
+
},
|
|
601
|
+
},
|
|
602
|
+
]);
|
|
603
|
+
expect(blocks[0]).toMatchObject({
|
|
604
|
+
type: "input",
|
|
605
|
+
element: { type: "plain_text_input", initial_value: "2.5" },
|
|
606
|
+
});
|
|
607
|
+
expect(blocks[1]).toEqual({
|
|
608
|
+
type: "context",
|
|
609
|
+
elements: [
|
|
610
|
+
{ type: "mrkdwn", text: "Password input omitted on Slack." },
|
|
611
|
+
],
|
|
612
|
+
});
|
|
613
|
+
expect(blocks.filter((block) => block.type === "input")).toHaveLength(1);
|
|
614
|
+
expect(JSON.stringify(blocks)).not.toContain("secret");
|
|
615
|
+
expect(warnings).toContainEqual({
|
|
616
|
+
code: "dropped",
|
|
617
|
+
component: "Input",
|
|
618
|
+
detail: "Password input was replaced by a Slack omission note.",
|
|
619
|
+
});
|
|
620
|
+
const button = blocks[2];
|
|
621
|
+
const numberInput = blocks[0];
|
|
622
|
+
if (
|
|
623
|
+
button?.type !== "actions" ||
|
|
624
|
+
button.elements[0]?.type !== "button" ||
|
|
625
|
+
numberInput?.type !== "input" ||
|
|
626
|
+
!numberInput.block_id
|
|
627
|
+
) {
|
|
628
|
+
throw new Error("Expected a named number input and submit button");
|
|
629
|
+
}
|
|
630
|
+
expect(
|
|
631
|
+
decodeBlockAction(button.elements[0], {
|
|
632
|
+
[numberInput.block_id]: {
|
|
633
|
+
edit_quantity: { type: "plain_text_input", value: "3.5" },
|
|
634
|
+
},
|
|
635
|
+
}),
|
|
636
|
+
).toEqual({
|
|
637
|
+
type: "submit",
|
|
638
|
+
quantity: "3.5",
|
|
639
|
+
});
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
it("drops nested password fallbacks from button values", () => {
|
|
643
|
+
const { blocks } = toSlackBlocks({
|
|
644
|
+
$type: "Col",
|
|
645
|
+
children: [
|
|
646
|
+
{
|
|
647
|
+
$type: "Input",
|
|
648
|
+
name: "password",
|
|
649
|
+
inputType: "password",
|
|
650
|
+
},
|
|
651
|
+
{
|
|
652
|
+
$type: "Button",
|
|
653
|
+
label: "Submit",
|
|
654
|
+
$action: {
|
|
655
|
+
type: "submit",
|
|
656
|
+
fields: [
|
|
657
|
+
{ password: { $field: "password", fallback: "secret" } },
|
|
658
|
+
],
|
|
659
|
+
},
|
|
660
|
+
},
|
|
661
|
+
],
|
|
662
|
+
});
|
|
663
|
+
expect(JSON.stringify(blocks)).not.toContain("secret");
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
it("drops a button value that nests a password reference past the traversal limit", () => {
|
|
667
|
+
let nested: unknown = { $field: "password", fallback: "secret" };
|
|
668
|
+
for (let depth = 0; depth <= MAX_TRAVERSAL_DEPTH; depth++) {
|
|
669
|
+
nested = { nested };
|
|
670
|
+
}
|
|
671
|
+
const { blocks, warnings } = toSlackBlocks({
|
|
672
|
+
$type: "Col",
|
|
673
|
+
children: [
|
|
674
|
+
{ $type: "Input", name: "password", inputType: "password" },
|
|
675
|
+
{
|
|
676
|
+
$type: "Button",
|
|
677
|
+
label: "Submit",
|
|
678
|
+
$action: { type: "submit", nested },
|
|
679
|
+
},
|
|
680
|
+
],
|
|
681
|
+
});
|
|
682
|
+
expect(JSON.stringify(blocks)).not.toContain("secret");
|
|
683
|
+
expect(warnings).toContainEqual({
|
|
684
|
+
code: "dropped",
|
|
685
|
+
component: "Button",
|
|
686
|
+
detail:
|
|
687
|
+
"value was dropped because the action payload could not be serialized.",
|
|
688
|
+
});
|
|
689
|
+
const button = blocks[1];
|
|
690
|
+
if (button?.type !== "actions" || button.elements[0]?.type !== "button") {
|
|
691
|
+
throw new Error("Expected a submit button");
|
|
692
|
+
}
|
|
693
|
+
expect(button.elements[0].value).toBeUndefined();
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
it("carries a named control in its block_id", () => {
|
|
697
|
+
const { blocks, warnings } = toSlackBlocks({
|
|
698
|
+
$type: "Input",
|
|
699
|
+
name: "email",
|
|
700
|
+
label: "Email",
|
|
701
|
+
});
|
|
702
|
+
expect(blocks[0]).toMatchObject({
|
|
703
|
+
type: "input",
|
|
704
|
+
block_id: `aui:0:${JSON.stringify([["action", "email"]])}`,
|
|
705
|
+
});
|
|
706
|
+
expect(warnings).toEqual([]);
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
it("warns when a name cannot fit in Slack's block_id limit", () => {
|
|
710
|
+
const { blocks, warnings } = toSlackBlocks({
|
|
711
|
+
$type: "Input",
|
|
712
|
+
name: "n".repeat(250),
|
|
713
|
+
label: "Name",
|
|
714
|
+
});
|
|
715
|
+
expect(blocks[0]).not.toHaveProperty("block_id");
|
|
716
|
+
expect(warnings).toContainEqual({
|
|
717
|
+
code: "dropped",
|
|
718
|
+
component: "Input",
|
|
719
|
+
detail:
|
|
720
|
+
"name could not be mapped because its Slack block_id exceeded 255 characters.",
|
|
721
|
+
});
|
|
722
|
+
});
|
|
723
|
+
|
|
482
724
|
it("wraps a plain_text_input element, passing multiline through", () => {
|
|
483
725
|
const { blocks } = toSlackBlocks({
|
|
484
726
|
$type: "Input",
|
|
@@ -510,6 +752,39 @@ describe("toSlackBlocks", () => {
|
|
|
510
752
|
);
|
|
511
753
|
});
|
|
512
754
|
|
|
755
|
+
it("omits multiline for a number input", () => {
|
|
756
|
+
const { blocks } = toSlackBlocks({
|
|
757
|
+
$type: "Input",
|
|
758
|
+
inputType: "number",
|
|
759
|
+
multiline: true,
|
|
760
|
+
});
|
|
761
|
+
expect((blocks[0] as SlackInputBlock).element).not.toHaveProperty(
|
|
762
|
+
"multiline",
|
|
763
|
+
);
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
it("sets initial_value from a non-empty defaultValue", () => {
|
|
767
|
+
const { blocks } = toSlackBlocks({
|
|
768
|
+
$type: "Input",
|
|
769
|
+
label: "Notes",
|
|
770
|
+
defaultValue: "Draft reply",
|
|
771
|
+
});
|
|
772
|
+
expect((blocks[0] as SlackInputBlock).element.initial_value).toBe(
|
|
773
|
+
"Draft reply",
|
|
774
|
+
);
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
it("omits initial_value when defaultValue is empty", () => {
|
|
778
|
+
const { blocks } = toSlackBlocks({
|
|
779
|
+
$type: "Input",
|
|
780
|
+
label: "Notes",
|
|
781
|
+
defaultValue: "",
|
|
782
|
+
});
|
|
783
|
+
expect((blocks[0] as SlackInputBlock).element).not.toHaveProperty(
|
|
784
|
+
"initial_value",
|
|
785
|
+
);
|
|
786
|
+
});
|
|
787
|
+
|
|
513
788
|
it(`clamps label to ${INPUT_LABEL_CAP} and placeholder to ${PLACEHOLDER_TEXT_CAP} characters, warning on each`, () => {
|
|
514
789
|
const label = "l".repeat(INPUT_LABEL_CAP + 10);
|
|
515
790
|
const placeholder = "p".repeat(PLACEHOLDER_TEXT_CAP + 10);
|
|
@@ -584,6 +859,63 @@ describe("toSlackBlocks", () => {
|
|
|
584
859
|
});
|
|
585
860
|
});
|
|
586
861
|
|
|
862
|
+
describe("Slider", () => {
|
|
863
|
+
it("renders a number input block with its numeric bounds", () => {
|
|
864
|
+
const { blocks } = toSlackBlocks({
|
|
865
|
+
$type: "Slider",
|
|
866
|
+
label: "Quantity",
|
|
867
|
+
min: 1,
|
|
868
|
+
max: 12,
|
|
869
|
+
step: 0.5,
|
|
870
|
+
defaultValue: 3.5,
|
|
871
|
+
$action: { type: "set_quantity" },
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
expect(blocks).toEqual([
|
|
875
|
+
{
|
|
876
|
+
type: "input",
|
|
877
|
+
label: { type: "plain_text", text: "Quantity" },
|
|
878
|
+
element: {
|
|
879
|
+
type: "number_input",
|
|
880
|
+
action_id: "set_quantity",
|
|
881
|
+
min_value: 1,
|
|
882
|
+
max_value: 12,
|
|
883
|
+
initial_value: 3.5,
|
|
884
|
+
is_decimal_allowed: true,
|
|
885
|
+
},
|
|
886
|
+
},
|
|
887
|
+
]);
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
it.each([
|
|
891
|
+
["minimum", { min: 0.5, max: 2, step: 1 }],
|
|
892
|
+
["maximum", { min: 0, max: 2.5 }],
|
|
893
|
+
["default value", { min: 0, max: 2, step: 1, defaultValue: 0.5 }],
|
|
894
|
+
])("allows decimals when the %s is fractional", (_property, values) => {
|
|
895
|
+
const { blocks } = toSlackBlocks({
|
|
896
|
+
$type: "Slider",
|
|
897
|
+
label: "Quantity",
|
|
898
|
+
...values,
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
expect(blocks[0]).toMatchObject({
|
|
902
|
+
element: { type: "number_input", is_decimal_allowed: true },
|
|
903
|
+
});
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
it("uses the same input block when nested in a Form", () => {
|
|
907
|
+
const { blocks } = toSlackBlocks({
|
|
908
|
+
$type: "Form",
|
|
909
|
+
children: { $type: "Slider", label: "Quantity", min: 1, max: 12 },
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
expect(blocks[0]).toMatchObject({
|
|
913
|
+
type: "input",
|
|
914
|
+
element: { type: "number_input", min_value: 1, max_value: 12 },
|
|
915
|
+
});
|
|
916
|
+
});
|
|
917
|
+
});
|
|
918
|
+
|
|
587
919
|
describe("Checkbox", () => {
|
|
588
920
|
it("emits a single-option checkboxes element", () => {
|
|
589
921
|
const { blocks } = toSlackBlocks({
|
|
@@ -594,6 +926,7 @@ describe("toSlackBlocks", () => {
|
|
|
594
926
|
});
|
|
595
927
|
expect(blocks[0]).toEqual({
|
|
596
928
|
type: "actions",
|
|
929
|
+
block_id: `aui:0:${JSON.stringify([["toggle", "agree", "Checkbox"]])}`,
|
|
597
930
|
elements: [
|
|
598
931
|
{
|
|
599
932
|
type: "checkboxes",
|
|
@@ -604,6 +937,27 @@ describe("toSlackBlocks", () => {
|
|
|
604
937
|
},
|
|
605
938
|
],
|
|
606
939
|
});
|
|
940
|
+
const block = blocks[0];
|
|
941
|
+
if (block?.type !== "actions" || !block.block_id) {
|
|
942
|
+
throw new Error("Expected a named checkbox block");
|
|
943
|
+
}
|
|
944
|
+
const checkbox = block.elements[0];
|
|
945
|
+
if (checkbox?.type !== "checkboxes") {
|
|
946
|
+
throw new Error("Expected a checkbox");
|
|
947
|
+
}
|
|
948
|
+
for (const [selectedOptions, expected] of [
|
|
949
|
+
[[], []],
|
|
950
|
+
[checkbox.options, ["agree"]],
|
|
951
|
+
] as const) {
|
|
952
|
+
expect(
|
|
953
|
+
decodeBlockAction({
|
|
954
|
+
type: "checkboxes",
|
|
955
|
+
block_id: block.block_id,
|
|
956
|
+
action_id: checkbox.action_id,
|
|
957
|
+
selected_options: selectedOptions,
|
|
958
|
+
}),
|
|
959
|
+
).toEqual({ type: "toggle", $input: expected });
|
|
960
|
+
}
|
|
607
961
|
});
|
|
608
962
|
|
|
609
963
|
it("falls back to the label as the option value when name is absent", () => {
|
|
@@ -642,9 +996,42 @@ describe("toSlackBlocks", () => {
|
|
|
642
996
|
"initial_options",
|
|
643
997
|
);
|
|
644
998
|
});
|
|
999
|
+
|
|
1000
|
+
it("maps a switch like a checkbox", () => {
|
|
1001
|
+
const { blocks } = toSlackBlocks({
|
|
1002
|
+
$type: "Checkbox",
|
|
1003
|
+
label: "Agree",
|
|
1004
|
+
variant: "switch",
|
|
1005
|
+
});
|
|
1006
|
+
expect((blocks[0] as SlackActionsBlock).elements[0]).toMatchObject({
|
|
1007
|
+
type: "checkboxes",
|
|
1008
|
+
options: [{ value: "Agree" }],
|
|
1009
|
+
});
|
|
1010
|
+
});
|
|
645
1011
|
});
|
|
646
1012
|
|
|
647
1013
|
describe("RadioGroup", () => {
|
|
1014
|
+
it("maps option descriptions to Slack option descriptions", () => {
|
|
1015
|
+
const { blocks } = toSlackBlocks({
|
|
1016
|
+
$type: "RadioGroup",
|
|
1017
|
+
options: [
|
|
1018
|
+
{
|
|
1019
|
+
label: "Free",
|
|
1020
|
+
description: "For personal projects",
|
|
1021
|
+
value: "free",
|
|
1022
|
+
},
|
|
1023
|
+
],
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
expect((blocks[0] as SlackActionsBlock).elements[0]).toMatchObject({
|
|
1027
|
+
options: [
|
|
1028
|
+
{
|
|
1029
|
+
description: { type: "plain_text", text: "For personal projects" },
|
|
1030
|
+
},
|
|
1031
|
+
],
|
|
1032
|
+
});
|
|
1033
|
+
});
|
|
1034
|
+
|
|
648
1035
|
it("names RadioGroup when the same option loss happens there", () => {
|
|
649
1036
|
const { warnings } = toSlackBlocks({
|
|
650
1037
|
$type: "RadioGroup",
|
|
@@ -718,6 +1105,89 @@ describe("toSlackBlocks", () => {
|
|
|
718
1105
|
});
|
|
719
1106
|
});
|
|
720
1107
|
|
|
1108
|
+
describe("CheckboxGroup", () => {
|
|
1109
|
+
const options = [
|
|
1110
|
+
{ label: "Small", value: "s" },
|
|
1111
|
+
{ label: "Large", value: "l" },
|
|
1112
|
+
];
|
|
1113
|
+
|
|
1114
|
+
it("emits a multi-option checkboxes element", () => {
|
|
1115
|
+
const { blocks } = toSlackBlocks({
|
|
1116
|
+
$type: "CheckboxGroup",
|
|
1117
|
+
options,
|
|
1118
|
+
$action: { type: "sizes" },
|
|
1119
|
+
});
|
|
1120
|
+
expect(blocks[0]).toEqual({
|
|
1121
|
+
type: "actions",
|
|
1122
|
+
elements: [
|
|
1123
|
+
{
|
|
1124
|
+
type: "checkboxes",
|
|
1125
|
+
action_id: "sizes",
|
|
1126
|
+
options: [
|
|
1127
|
+
{ text: { type: "plain_text", text: "Small" }, value: "s" },
|
|
1128
|
+
{ text: { type: "plain_text", text: "Large" }, value: "l" },
|
|
1129
|
+
],
|
|
1130
|
+
},
|
|
1131
|
+
],
|
|
1132
|
+
});
|
|
1133
|
+
});
|
|
1134
|
+
|
|
1135
|
+
it("sets initial_options from matching defaultValue entries", () => {
|
|
1136
|
+
const { blocks } = toSlackBlocks({
|
|
1137
|
+
$type: "CheckboxGroup",
|
|
1138
|
+
options,
|
|
1139
|
+
defaultValue: ["l", "missing"],
|
|
1140
|
+
$action: { type: "sizes" },
|
|
1141
|
+
});
|
|
1142
|
+
expect(
|
|
1143
|
+
((blocks[0] as SlackActionsBlock).elements[0] as SlackCheckboxesElement)
|
|
1144
|
+
.initial_options,
|
|
1145
|
+
).toEqual([{ text: { type: "plain_text", text: "Large" }, value: "l" }]);
|
|
1146
|
+
|
|
1147
|
+
const { blocks: withoutInitialOptions } = toSlackBlocks({
|
|
1148
|
+
$type: "CheckboxGroup",
|
|
1149
|
+
options,
|
|
1150
|
+
defaultValue: ["missing"],
|
|
1151
|
+
$action: { type: "sizes" },
|
|
1152
|
+
});
|
|
1153
|
+
expect(
|
|
1154
|
+
(withoutInitialOptions[0] as SlackActionsBlock).elements[0],
|
|
1155
|
+
).not.toHaveProperty("initial_options");
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
it("names CheckboxGroup when malformed options are dropped", () => {
|
|
1159
|
+
const { warnings } = toSlackBlocks({
|
|
1160
|
+
$type: "CheckboxGroup",
|
|
1161
|
+
options: [{ label: "ok", value: "a" }, { label: "bad" }],
|
|
1162
|
+
});
|
|
1163
|
+
expect(warnings).toContainEqual({
|
|
1164
|
+
code: "dropped",
|
|
1165
|
+
component: "CheckboxGroup",
|
|
1166
|
+
detail: "1 option was dropped for want of a string label and value.",
|
|
1167
|
+
});
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1170
|
+
it(`clamps options past ${CHECKBOX_OPTION_CAP} entries and warns`, () => {
|
|
1171
|
+
const manyOptions = Array.from(
|
|
1172
|
+
{ length: CHECKBOX_OPTION_CAP + 3 },
|
|
1173
|
+
(_, i) => ({ label: `L${i}`, value: `v${i}` }),
|
|
1174
|
+
);
|
|
1175
|
+
const { blocks, warnings } = toSlackBlocks({
|
|
1176
|
+
$type: "CheckboxGroup",
|
|
1177
|
+
options: manyOptions,
|
|
1178
|
+
$action: { type: "sizes" },
|
|
1179
|
+
});
|
|
1180
|
+
const element = (blocks[0] as SlackActionsBlock)
|
|
1181
|
+
.elements[0] as SlackCheckboxesElement;
|
|
1182
|
+
expect(element.options).toHaveLength(CHECKBOX_OPTION_CAP);
|
|
1183
|
+
expect(warnings).toContainEqual({
|
|
1184
|
+
code: "clamped",
|
|
1185
|
+
component: "CheckboxGroup",
|
|
1186
|
+
detail: `options were clamped to ${CHECKBOX_OPTION_CAP} entries.`,
|
|
1187
|
+
});
|
|
1188
|
+
});
|
|
1189
|
+
});
|
|
1190
|
+
|
|
721
1191
|
describe("interactive grouping", () => {
|
|
722
1192
|
it("groups consecutive interactive siblings into one actions block, split by a non-interactive sibling", () => {
|
|
723
1193
|
const root = [
|
|
@@ -727,6 +1197,11 @@ describe("toSlackBlocks", () => {
|
|
|
727
1197
|
options: [{ label: "X", value: "x" }],
|
|
728
1198
|
$action: { type: "b" },
|
|
729
1199
|
},
|
|
1200
|
+
{
|
|
1201
|
+
$type: "CheckboxGroup",
|
|
1202
|
+
options: [{ label: "Y", value: "y" }],
|
|
1203
|
+
$action: { type: "checkbox" },
|
|
1204
|
+
},
|
|
730
1205
|
{ $type: "Text", value: "gap" },
|
|
731
1206
|
{ $type: "Button", label: "C", $action: { type: "c" } },
|
|
732
1207
|
];
|
|
@@ -734,7 +1209,11 @@ describe("toSlackBlocks", () => {
|
|
|
734
1209
|
expect(blocks).toHaveLength(3);
|
|
735
1210
|
expect(blocks[0]).toMatchObject({
|
|
736
1211
|
type: "actions",
|
|
737
|
-
elements: [
|
|
1212
|
+
elements: [
|
|
1213
|
+
{ type: "button" },
|
|
1214
|
+
{ type: "static_select" },
|
|
1215
|
+
{ type: "checkboxes" },
|
|
1216
|
+
],
|
|
738
1217
|
});
|
|
739
1218
|
expect(blocks[1]).toMatchObject({ type: "section" });
|
|
740
1219
|
expect(blocks[2]).toMatchObject({
|
|
@@ -1695,6 +2174,29 @@ describe("toSlackBlocks", () => {
|
|
|
1695
2174
|
]);
|
|
1696
2175
|
});
|
|
1697
2176
|
|
|
2177
|
+
it("formats table cells from their columns", () => {
|
|
2178
|
+
const { blocks } = toSlackBlocks({
|
|
2179
|
+
$type: "Table",
|
|
2180
|
+
columns: [
|
|
2181
|
+
{ label: "Number", format: { kind: "number", decimals: 1 } },
|
|
2182
|
+
{
|
|
2183
|
+
label: "Revenue",
|
|
2184
|
+
format: { kind: "currency", currency: "USD", decimals: 2 },
|
|
2185
|
+
},
|
|
2186
|
+
{ label: "Share", format: { kind: "percent", decimals: 0 } },
|
|
2187
|
+
{ label: "Date", format: { kind: "date" } },
|
|
2188
|
+
],
|
|
2189
|
+
rows: [[1234.5, 12.4, 0.08, "2024-01-02"]],
|
|
2190
|
+
});
|
|
2191
|
+
const table = blocks[0] as SlackDataTableBlock;
|
|
2192
|
+
expect(table.rows[1]?.map((cell) => cell.text)).toEqual([
|
|
2193
|
+
"1,234.5",
|
|
2194
|
+
"$12.40",
|
|
2195
|
+
"8%",
|
|
2196
|
+
"Jan 2, 2024",
|
|
2197
|
+
]);
|
|
2198
|
+
});
|
|
2199
|
+
|
|
1698
2200
|
it("requires a caption on every emitted table", () => {
|
|
1699
2201
|
const { blocks } = toSlackBlocks({
|
|
1700
2202
|
$type: "Table",
|
|
@@ -2059,6 +2561,7 @@ describe("toSlackBlocks", () => {
|
|
|
2059
2561
|
expect(blocks).toEqual([
|
|
2060
2562
|
{
|
|
2061
2563
|
type: "input",
|
|
2564
|
+
block_id: `aui:0:${JSON.stringify([["action", "name"]])}`,
|
|
2062
2565
|
label: { type: "plain_text", text: "Name" },
|
|
2063
2566
|
element: { type: "plain_text_input", action_id: "action" },
|
|
2064
2567
|
},
|
|
@@ -2075,6 +2578,136 @@ describe("toSlackBlocks", () => {
|
|
|
2075
2578
|
},
|
|
2076
2579
|
]);
|
|
2077
2580
|
});
|
|
2581
|
+
|
|
2582
|
+
it("maps named controls per element when they share an actions block", () => {
|
|
2583
|
+
const { blocks, warnings } = toSlackBlocks([
|
|
2584
|
+
{
|
|
2585
|
+
$type: "Select",
|
|
2586
|
+
name: "plan",
|
|
2587
|
+
options: [{ label: "Pro", value: "pro" }],
|
|
2588
|
+
$action: { type: "choose_plan" },
|
|
2589
|
+
},
|
|
2590
|
+
{
|
|
2591
|
+
$type: "CheckboxGroup",
|
|
2592
|
+
name: "tags",
|
|
2593
|
+
options: [{ label: "News", value: "news" }],
|
|
2594
|
+
$action: { type: "choose_tags" },
|
|
2595
|
+
},
|
|
2596
|
+
{
|
|
2597
|
+
$type: "Button",
|
|
2598
|
+
label: "Save",
|
|
2599
|
+
$action: {
|
|
2600
|
+
type: "save",
|
|
2601
|
+
plan: { $field: "plan" },
|
|
2602
|
+
tags: { $field: "tags" },
|
|
2603
|
+
},
|
|
2604
|
+
},
|
|
2605
|
+
]);
|
|
2606
|
+
expect(blocks[0]).toMatchObject({
|
|
2607
|
+
type: "actions",
|
|
2608
|
+
block_id: `aui:0:${JSON.stringify([
|
|
2609
|
+
["choose_plan", "plan"],
|
|
2610
|
+
["choose_tags", "tags"],
|
|
2611
|
+
])}`,
|
|
2612
|
+
});
|
|
2613
|
+
expect(warnings).toEqual([]);
|
|
2614
|
+
const block = blocks[0];
|
|
2615
|
+
if (block?.type !== "actions" || !block.block_id) {
|
|
2616
|
+
throw new Error("Expected a named actions block");
|
|
2617
|
+
}
|
|
2618
|
+
const button = block.elements.find(
|
|
2619
|
+
(element) => element.type === "button",
|
|
2620
|
+
);
|
|
2621
|
+
if (!button) throw new Error("Expected a button");
|
|
2622
|
+
for (const [selectedOptions, tags] of [
|
|
2623
|
+
[[], []],
|
|
2624
|
+
[[{ value: "news" }], ["news"]],
|
|
2625
|
+
] as const) {
|
|
2626
|
+
expect(
|
|
2627
|
+
decodeBlockAction({
|
|
2628
|
+
type: "checkboxes",
|
|
2629
|
+
block_id: block.block_id,
|
|
2630
|
+
action_id: "choose_tags",
|
|
2631
|
+
selected_options: selectedOptions,
|
|
2632
|
+
}),
|
|
2633
|
+
).toEqual({ type: "choose_tags", $input: tags });
|
|
2634
|
+
expect(
|
|
2635
|
+
decodeBlockAction(button, {
|
|
2636
|
+
[block.block_id]: {
|
|
2637
|
+
choose_tags: {
|
|
2638
|
+
type: "checkboxes",
|
|
2639
|
+
selected_options: selectedOptions,
|
|
2640
|
+
},
|
|
2641
|
+
},
|
|
2642
|
+
}),
|
|
2643
|
+
).toEqual({ type: "save", tags });
|
|
2644
|
+
}
|
|
2645
|
+
});
|
|
2646
|
+
|
|
2647
|
+
it("starts a new actions block when repeated action ids need distinct field mappings", () => {
|
|
2648
|
+
const { blocks } = toSlackBlocks([
|
|
2649
|
+
{
|
|
2650
|
+
$type: "Select",
|
|
2651
|
+
name: "first",
|
|
2652
|
+
options: [{ label: "One", value: "one" }],
|
|
2653
|
+
$action: { type: "choose" },
|
|
2654
|
+
},
|
|
2655
|
+
{
|
|
2656
|
+
$type: "Select",
|
|
2657
|
+
name: "second",
|
|
2658
|
+
options: [{ label: "Two", value: "two" }],
|
|
2659
|
+
$action: { type: "choose" },
|
|
2660
|
+
},
|
|
2661
|
+
]);
|
|
2662
|
+
expect(blocks).toHaveLength(2);
|
|
2663
|
+
expect(blocks[0]).toMatchObject({
|
|
2664
|
+
type: "actions",
|
|
2665
|
+
block_id: `aui:0:${JSON.stringify([["choose", "first"]])}`,
|
|
2666
|
+
});
|
|
2667
|
+
expect(blocks[1]).toMatchObject({
|
|
2668
|
+
type: "actions",
|
|
2669
|
+
block_id: `aui:1:${JSON.stringify([["choose", "second"]])}`,
|
|
2670
|
+
});
|
|
2671
|
+
});
|
|
2672
|
+
|
|
2673
|
+
it("keeps unnamed controls with repeated action ids in their existing block", () => {
|
|
2674
|
+
const { blocks } = toSlackBlocks([
|
|
2675
|
+
{
|
|
2676
|
+
$type: "Button",
|
|
2677
|
+
label: "First",
|
|
2678
|
+
$action: { type: "choose" },
|
|
2679
|
+
},
|
|
2680
|
+
{
|
|
2681
|
+
$type: "Button",
|
|
2682
|
+
label: "Second",
|
|
2683
|
+
$action: { type: "choose" },
|
|
2684
|
+
},
|
|
2685
|
+
]);
|
|
2686
|
+
expect(blocks).toHaveLength(1);
|
|
2687
|
+
expect(blocks[0]).not.toHaveProperty("block_id");
|
|
2688
|
+
expect((blocks[0] as SlackActionsBlock).elements).toHaveLength(2);
|
|
2689
|
+
});
|
|
2690
|
+
|
|
2691
|
+
it("splits grouped fields when their mapping would exceed Slack's block_id limit", () => {
|
|
2692
|
+
const { blocks, warnings } = toSlackBlocks([
|
|
2693
|
+
{
|
|
2694
|
+
$type: "Select",
|
|
2695
|
+
name: "a".repeat(120),
|
|
2696
|
+
options: [{ label: "A", value: "a" }],
|
|
2697
|
+
$action: { type: "choose_a" },
|
|
2698
|
+
},
|
|
2699
|
+
{
|
|
2700
|
+
$type: "Select",
|
|
2701
|
+
name: "b".repeat(120),
|
|
2702
|
+
options: [{ label: "B", value: "b" }],
|
|
2703
|
+
$action: { type: "choose_b" },
|
|
2704
|
+
},
|
|
2705
|
+
]);
|
|
2706
|
+
expect(blocks).toHaveLength(2);
|
|
2707
|
+
expect(blocks[0]).toHaveProperty("block_id");
|
|
2708
|
+
expect(blocks[1]).toHaveProperty("block_id");
|
|
2709
|
+
expect(warnings).toEqual([]);
|
|
2710
|
+
});
|
|
2078
2711
|
});
|
|
2079
2712
|
|
|
2080
2713
|
describe("Root", () => {
|
|
@@ -2221,11 +2854,19 @@ describe("toSlackBlocks", () => {
|
|
|
2221
2854
|
expect(() =>
|
|
2222
2855
|
toSlackBlocks({ $type: "Button", label: "Go", $action: circular }),
|
|
2223
2856
|
).not.toThrow();
|
|
2224
|
-
const { blocks } = toSlackBlocks({
|
|
2857
|
+
const { blocks, warnings } = toSlackBlocks({
|
|
2225
2858
|
$type: "Button",
|
|
2226
2859
|
label: "Go",
|
|
2227
2860
|
$action: circular,
|
|
2228
2861
|
});
|
|
2862
|
+
expect(warnings).toEqual([
|
|
2863
|
+
{
|
|
2864
|
+
code: "dropped",
|
|
2865
|
+
component: "Button",
|
|
2866
|
+
detail:
|
|
2867
|
+
"value was dropped because the action payload could not be serialized.",
|
|
2868
|
+
},
|
|
2869
|
+
]);
|
|
2229
2870
|
expect(blocks[0]).toEqual({
|
|
2230
2871
|
type: "actions",
|
|
2231
2872
|
elements: [
|
|
@@ -2384,3 +3025,41 @@ describe("toSlackBlocks data_table integrity", () => {
|
|
|
2384
3025
|
).toBe(true);
|
|
2385
3026
|
});
|
|
2386
3027
|
});
|
|
3028
|
+
|
|
3029
|
+
it("resolves a Slider field reference from numeric Slack state", () => {
|
|
3030
|
+
const { blocks } = toSlackBlocks([
|
|
3031
|
+
{
|
|
3032
|
+
$type: "Slider",
|
|
3033
|
+
name: "count",
|
|
3034
|
+
label: "Count",
|
|
3035
|
+
min: 0,
|
|
3036
|
+
max: 10,
|
|
3037
|
+
$action: { type: "change_count" },
|
|
3038
|
+
},
|
|
3039
|
+
{
|
|
3040
|
+
$type: "Button",
|
|
3041
|
+
label: "Submit",
|
|
3042
|
+
$action: { type: "submit", count: { $field: "count" } },
|
|
3043
|
+
},
|
|
3044
|
+
]);
|
|
3045
|
+
const slider = blocks.find((block) => block.type === "input");
|
|
3046
|
+
const actions = blocks.find((block) => block.type === "actions");
|
|
3047
|
+
if (
|
|
3048
|
+
!slider ||
|
|
3049
|
+
slider.type !== "input" ||
|
|
3050
|
+
!slider.block_id ||
|
|
3051
|
+
!actions ||
|
|
3052
|
+
actions.type !== "actions"
|
|
3053
|
+
)
|
|
3054
|
+
throw new Error("Expected slider and action blocks");
|
|
3055
|
+
const button = actions.elements[0];
|
|
3056
|
+
if (!button || button.type !== "button")
|
|
3057
|
+
throw new Error("Expected submit button");
|
|
3058
|
+
expect(
|
|
3059
|
+
decodeBlockAction(button, {
|
|
3060
|
+
[slider.block_id]: {
|
|
3061
|
+
change_count: { type: "number_input", value: "3" },
|
|
3062
|
+
},
|
|
3063
|
+
}),
|
|
3064
|
+
).toEqual({ type: "submit", count: 3 });
|
|
3065
|
+
});
|