@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
|
@@ -2,6 +2,30 @@ import { describe, expect, it } from "vitest";
|
|
|
2
2
|
import { decodeBlockAction } from "./decodeBlockAction";
|
|
3
3
|
|
|
4
4
|
describe("decodeBlockAction", () => {
|
|
5
|
+
it.each([
|
|
6
|
+
"{}",
|
|
7
|
+
'{"sku":"typed text","qty":2}',
|
|
8
|
+
'{"type":"other","$input":"typed text"}',
|
|
9
|
+
])("preserves plain-text input containing JSON verbatim: %s", (value) => {
|
|
10
|
+
expect(
|
|
11
|
+
decodeBlockAction({
|
|
12
|
+
type: "plain_text_input",
|
|
13
|
+
action_id: "leave_note",
|
|
14
|
+
value,
|
|
15
|
+
}),
|
|
16
|
+
).toEqual({ type: "leave_note", $input: value });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("keeps typed text distinct from a button's serialized payload", () => {
|
|
20
|
+
const value = '{"sku":"abc123","$input":"ignored"}';
|
|
21
|
+
expect(
|
|
22
|
+
decodeBlockAction({ type: "button", action_id: "buy_item", value }),
|
|
23
|
+
).toEqual({
|
|
24
|
+
type: "buy_item",
|
|
25
|
+
sku: "abc123",
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
5
29
|
it("decodes a button action, spreading its JSON value into the payload", () => {
|
|
6
30
|
const action = {
|
|
7
31
|
action_id: "buy_item",
|
|
@@ -14,6 +38,70 @@ describe("decodeBlockAction", () => {
|
|
|
14
38
|
});
|
|
15
39
|
});
|
|
16
40
|
|
|
41
|
+
it("resolves nested field references from named plain-text input state", () => {
|
|
42
|
+
const blockId = `aui:0:${JSON.stringify([["note", "note"]])}`;
|
|
43
|
+
expect(
|
|
44
|
+
decodeBlockAction(
|
|
45
|
+
{
|
|
46
|
+
action_id: "save",
|
|
47
|
+
value: JSON.stringify({ context: { note: { $field: "note" } } }),
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
[blockId]: {
|
|
51
|
+
note: { type: "plain_text_input", value: "edited" },
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
),
|
|
55
|
+
).toEqual({ type: "save", context: { note: "edited" } });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("maps each action in a shared block and preserves empty checkbox selections", () => {
|
|
59
|
+
const blockId = `aui:1:${JSON.stringify([
|
|
60
|
+
["choose_plan", "plan"],
|
|
61
|
+
["choose_tags", "tags"],
|
|
62
|
+
["choose_date", "date"],
|
|
63
|
+
["choose_size", "size"],
|
|
64
|
+
])}`;
|
|
65
|
+
expect(
|
|
66
|
+
decodeBlockAction(
|
|
67
|
+
{
|
|
68
|
+
action_id: "submit",
|
|
69
|
+
value: JSON.stringify({
|
|
70
|
+
context: {
|
|
71
|
+
plan: { $field: "plan" },
|
|
72
|
+
tags: { $field: "tags" },
|
|
73
|
+
date: { $field: "date" },
|
|
74
|
+
size: { $field: "size" },
|
|
75
|
+
missing: { $field: "missing" },
|
|
76
|
+
},
|
|
77
|
+
}),
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
[blockId]: {
|
|
81
|
+
choose_plan: {
|
|
82
|
+
type: "static_select",
|
|
83
|
+
selected_option: { value: "pro" },
|
|
84
|
+
},
|
|
85
|
+
choose_tags: { type: "checkboxes", selected_options: [] },
|
|
86
|
+
choose_date: { type: "datepicker", selected_date: "2026-09-25" },
|
|
87
|
+
choose_size: {
|
|
88
|
+
type: "radio_buttons",
|
|
89
|
+
selected_option: { value: "small" },
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
),
|
|
94
|
+
).toEqual({
|
|
95
|
+
type: "submit",
|
|
96
|
+
context: {
|
|
97
|
+
plan: "pro",
|
|
98
|
+
tags: [],
|
|
99
|
+
date: "2026-09-25",
|
|
100
|
+
size: "small",
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
17
105
|
it("decodes a static_select action, reading $input from selected_option.value", () => {
|
|
18
106
|
const action = {
|
|
19
107
|
action_id: "choose_color",
|
|
@@ -36,6 +124,16 @@ describe("decodeBlockAction", () => {
|
|
|
36
124
|
});
|
|
37
125
|
});
|
|
38
126
|
|
|
127
|
+
it("decodes a number input action as a number", () => {
|
|
128
|
+
expect(
|
|
129
|
+
decodeBlockAction({
|
|
130
|
+
type: "number_input",
|
|
131
|
+
action_id: "set_quantity",
|
|
132
|
+
value: "3.5",
|
|
133
|
+
}),
|
|
134
|
+
).toEqual({ type: "set_quantity", $input: 3.5 });
|
|
135
|
+
});
|
|
136
|
+
|
|
39
137
|
it("decodes a checkboxes action, reading $input from selected_options[].value", () => {
|
|
40
138
|
const action = {
|
|
41
139
|
action_id: "toggle_opts",
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import type { Action } from "../ir";
|
|
2
|
+
import { resolveFieldReferences } from "../fieldReferences";
|
|
3
|
+
import {
|
|
4
|
+
formatTemporalUnixSeconds,
|
|
5
|
+
mergeTemporalMinutes,
|
|
6
|
+
splitTemporalMinutes,
|
|
7
|
+
} from "../temporal";
|
|
8
|
+
import type { FieldMapping } from "./types";
|
|
2
9
|
|
|
3
10
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
4
11
|
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -8,12 +15,158 @@ const optionValue = (value: unknown): string | undefined =>
|
|
|
8
15
|
? value["value"]
|
|
9
16
|
: undefined;
|
|
10
17
|
|
|
18
|
+
const selectedValue = (
|
|
19
|
+
action: Record<string, unknown>,
|
|
20
|
+
field?: FieldMapping,
|
|
21
|
+
): unknown => {
|
|
22
|
+
if (action["type"] === "datetimepicker" || "selected_date_time" in action) {
|
|
23
|
+
const seconds = action["selected_date_time"];
|
|
24
|
+
return seconds === null
|
|
25
|
+
? ""
|
|
26
|
+
: typeof seconds === "number"
|
|
27
|
+
? formatTemporalUnixSeconds(seconds, field?.offset)
|
|
28
|
+
: undefined;
|
|
29
|
+
}
|
|
30
|
+
if (action["type"] === "timepicker" || "selected_time" in action) {
|
|
31
|
+
const time = action["selected_time"];
|
|
32
|
+
if (time === null) return "";
|
|
33
|
+
const parts =
|
|
34
|
+
typeof time === "string" ? splitTemporalMinutes(time) : undefined;
|
|
35
|
+
return parts?.date === undefined ? parts?.time : undefined;
|
|
36
|
+
}
|
|
37
|
+
if (field?.part === "date" && action["selected_date"] === null) return "";
|
|
38
|
+
const selectedOption = optionValue(action["selected_option"]);
|
|
39
|
+
const selectedDate =
|
|
40
|
+
typeof action["selected_date"] === "string"
|
|
41
|
+
? action["selected_date"]
|
|
42
|
+
: undefined;
|
|
43
|
+
const selectedOptions = Array.isArray(action["selected_options"])
|
|
44
|
+
? action["selected_options"]
|
|
45
|
+
.map(optionValue)
|
|
46
|
+
.filter((value): value is string => value !== undefined)
|
|
47
|
+
: undefined;
|
|
48
|
+
const numberInput =
|
|
49
|
+
action["type"] === "number_input" && typeof action["value"] === "string"
|
|
50
|
+
? Number(action["value"])
|
|
51
|
+
: undefined;
|
|
52
|
+
return (
|
|
53
|
+
selectedOption ??
|
|
54
|
+
selectedDate ??
|
|
55
|
+
(selectedOptions !== undefined
|
|
56
|
+
? selectedOptions
|
|
57
|
+
: Number.isFinite(numberInput)
|
|
58
|
+
? numberInput
|
|
59
|
+
: undefined)
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const fieldMappingsFromBlockId = (blockId: string): readonly FieldMapping[] => {
|
|
64
|
+
if (!/^aui:\d+:/.test(blockId)) return [];
|
|
65
|
+
const mappingStart = blockId.indexOf(":", 4);
|
|
66
|
+
if (mappingStart === -1) return [];
|
|
67
|
+
try {
|
|
68
|
+
const parsed: unknown = JSON.parse(blockId.slice(mappingStart + 1));
|
|
69
|
+
if (!Array.isArray(parsed)) return [];
|
|
70
|
+
return parsed.flatMap((entry): FieldMapping[] => {
|
|
71
|
+
if (
|
|
72
|
+
!Array.isArray(entry) ||
|
|
73
|
+
typeof entry[0] !== "string" ||
|
|
74
|
+
typeof entry[1] !== "string"
|
|
75
|
+
)
|
|
76
|
+
return [];
|
|
77
|
+
return [
|
|
78
|
+
{
|
|
79
|
+
actionId: entry[0],
|
|
80
|
+
name: entry[1],
|
|
81
|
+
component: typeof entry[2] === "string" ? entry[2] : "",
|
|
82
|
+
...(entry[2] === "DatePicker" &&
|
|
83
|
+
(entry[3] === "time" || entry[3] === "datetime")
|
|
84
|
+
? {
|
|
85
|
+
inputType: entry[3],
|
|
86
|
+
...(typeof entry[4] === "string" ? { offset: entry[4] } : {}),
|
|
87
|
+
...((entry[5] === "date" || entry[5] === "time") &&
|
|
88
|
+
Number.isSafeInteger(entry[6]) &&
|
|
89
|
+
entry[6] >= 0
|
|
90
|
+
? { part: entry[5], pairId: entry[6] }
|
|
91
|
+
: {}),
|
|
92
|
+
}
|
|
93
|
+
: {}),
|
|
94
|
+
},
|
|
95
|
+
];
|
|
96
|
+
});
|
|
97
|
+
} catch {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const checkboxValue = (
|
|
103
|
+
value: Record<string, unknown>,
|
|
104
|
+
name: string,
|
|
105
|
+
): boolean | undefined =>
|
|
106
|
+
Array.isArray(value["selected_options"])
|
|
107
|
+
? value["selected_options"].some((option) => optionValue(option) === name)
|
|
108
|
+
: undefined;
|
|
109
|
+
|
|
110
|
+
const stateFieldValue = (value: unknown, field: FieldMapping): unknown => {
|
|
111
|
+
if (!isRecord(value)) return undefined;
|
|
112
|
+
if (field.component === "Checkbox" && value["type"] === "checkboxes") {
|
|
113
|
+
return checkboxValue(value, field.name);
|
|
114
|
+
}
|
|
115
|
+
if (value["type"] === "plain_text_input") {
|
|
116
|
+
return typeof value["value"] === "string" ? value["value"] : undefined;
|
|
117
|
+
}
|
|
118
|
+
return selectedValue(value, field);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const fieldValuesFromState = (
|
|
122
|
+
stateValues: unknown,
|
|
123
|
+
action: Record<string, unknown>,
|
|
124
|
+
actionField: FieldMapping | undefined,
|
|
125
|
+
) => {
|
|
126
|
+
const fields: Record<string, unknown> = Object.create(null);
|
|
127
|
+
const pairs = new Map<number, { name: string; date: string; time: string }>();
|
|
128
|
+
const collect = (raw: unknown, field: FieldMapping) => {
|
|
129
|
+
const value = stateFieldValue(raw, field);
|
|
130
|
+
if (field.part !== undefined && field.pairId !== undefined) {
|
|
131
|
+
const pair = pairs.get(field.pairId) ?? {
|
|
132
|
+
name: field.name,
|
|
133
|
+
date: "",
|
|
134
|
+
time: "",
|
|
135
|
+
};
|
|
136
|
+
pair[field.part] = typeof value === "string" ? value : "";
|
|
137
|
+
pairs.set(field.pairId, pair);
|
|
138
|
+
} else if (field.name && value !== undefined) {
|
|
139
|
+
fields[field.name] = value;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
if (isRecord(stateValues)) {
|
|
143
|
+
for (const [blockId, rawBlockValues] of Object.entries(stateValues)) {
|
|
144
|
+
if (!isRecord(rawBlockValues)) continue;
|
|
145
|
+
for (const field of fieldMappingsFromBlockId(blockId)) {
|
|
146
|
+
if (Object.hasOwn(rawBlockValues, field.actionId))
|
|
147
|
+
collect(rawBlockValues[field.actionId], field);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (actionField?.inputType !== undefined) collect(action, actionField);
|
|
152
|
+
for (const pair of pairs.values()) {
|
|
153
|
+
if (pair.name)
|
|
154
|
+
fields[pair.name] = mergeTemporalMinutes(pair.date, pair.time);
|
|
155
|
+
}
|
|
156
|
+
return { fields, pairs };
|
|
157
|
+
};
|
|
158
|
+
|
|
11
159
|
/**
|
|
12
160
|
* Decodes one structural entry from a Slack `block_actions` payload.
|
|
13
161
|
* `$input` is reserved for the runtime selection: a `$input` key inside the
|
|
14
162
|
* button's JSON `value` payload is dropped rather than spread into the result.
|
|
163
|
+
* `stateValues` is the companion `state.values` object used to resolve named
|
|
164
|
+
* control values referenced by `{ "$field": name }` in the action payload.
|
|
15
165
|
*/
|
|
16
|
-
export function decodeBlockAction(
|
|
166
|
+
export function decodeBlockAction(
|
|
167
|
+
action: unknown,
|
|
168
|
+
stateValues?: unknown,
|
|
169
|
+
): Action | undefined {
|
|
17
170
|
try {
|
|
18
171
|
if (!isRecord(action) || typeof action["action_id"] !== "string") {
|
|
19
172
|
return undefined;
|
|
@@ -25,7 +178,9 @@ export function decodeBlockAction(action: unknown): Action | undefined {
|
|
|
25
178
|
const rawValue = action["value"];
|
|
26
179
|
let payload: Record<string, unknown> = {};
|
|
27
180
|
let plainValue: string | undefined;
|
|
28
|
-
if (typeof rawValue === "string") {
|
|
181
|
+
if (action["type"] === "plain_text_input" && typeof rawValue === "string") {
|
|
182
|
+
plainValue = rawValue;
|
|
183
|
+
} else if (typeof rawValue === "string") {
|
|
29
184
|
try {
|
|
30
185
|
const parsed: unknown = JSON.parse(rawValue);
|
|
31
186
|
if (isRecord(parsed)) {
|
|
@@ -38,28 +193,39 @@ export function decodeBlockAction(action: unknown): Action | undefined {
|
|
|
38
193
|
}
|
|
39
194
|
}
|
|
40
195
|
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
196
|
+
const actionField =
|
|
197
|
+
typeof action["block_id"] === "string"
|
|
198
|
+
? fieldMappingsFromBlockId(action["block_id"]).find(
|
|
199
|
+
(field) => field.actionId === actionId,
|
|
200
|
+
)
|
|
45
201
|
: undefined;
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
202
|
+
const { fields, pairs } = fieldValuesFromState(
|
|
203
|
+
stateValues,
|
|
204
|
+
action,
|
|
205
|
+
actionField,
|
|
206
|
+
);
|
|
207
|
+
let input = selectedValue(action, actionField) ?? plainValue;
|
|
208
|
+
if (actionField?.pairId !== undefined) {
|
|
209
|
+
const pair = pairs.get(actionField.pairId);
|
|
210
|
+
input =
|
|
211
|
+
pair === undefined ? "" : mergeTemporalMinutes(pair.date, pair.time);
|
|
212
|
+
if (!input) return undefined;
|
|
213
|
+
}
|
|
214
|
+
if (
|
|
215
|
+
(action["type"] === "timepicker" ||
|
|
216
|
+
action["type"] === "datetimepicker") &&
|
|
217
|
+
input === undefined
|
|
218
|
+
)
|
|
219
|
+
return undefined;
|
|
55
220
|
|
|
56
|
-
|
|
221
|
+
const decoded = {
|
|
57
222
|
...Object.fromEntries(
|
|
58
223
|
Object.entries(payload).filter(([key]) => key !== "$input"),
|
|
59
224
|
),
|
|
60
225
|
type: actionId,
|
|
61
226
|
...(input !== undefined ? { $input: input } : {}),
|
|
62
227
|
};
|
|
228
|
+
return resolveFieldReferences(decoded, fields) as Action;
|
|
63
229
|
} catch {
|
|
64
230
|
return undefined;
|
|
65
231
|
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import type { UIElement } from "../ir";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
CHECKBOX_OPTION_CAP,
|
|
5
|
+
INBOUND_BLOCK_CAP,
|
|
6
|
+
SELECT_OPTION_CAP,
|
|
7
|
+
} from "./constants";
|
|
4
8
|
import { fromSlackBlocks } from "./fromSlackBlocks";
|
|
5
9
|
import { toSlackBlocks } from "./toSlackBlocks";
|
|
6
10
|
import type { ToSlackBlocksOptions } from "./types";
|
|
@@ -319,6 +323,54 @@ describe("fromSlackBlocks", () => {
|
|
|
319
323
|
]);
|
|
320
324
|
});
|
|
321
325
|
|
|
326
|
+
it("reads defaultValue from a matching static_select initial_option", () => {
|
|
327
|
+
const { nodes } = fromSlackBlocks([
|
|
328
|
+
{
|
|
329
|
+
type: "actions",
|
|
330
|
+
elements: [
|
|
331
|
+
{
|
|
332
|
+
type: "static_select",
|
|
333
|
+
action_id: "pick",
|
|
334
|
+
options: [
|
|
335
|
+
{ text: { type: "plain_text", text: "Alpha" }, value: "a" },
|
|
336
|
+
{ text: { type: "plain_text", text: "Beta" }, value: "b" },
|
|
337
|
+
],
|
|
338
|
+
initial_option: {
|
|
339
|
+
text: { type: "plain_text", text: "Beta" },
|
|
340
|
+
value: "b",
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
],
|
|
344
|
+
},
|
|
345
|
+
]);
|
|
346
|
+
expect(nodes[0]).toMatchObject({
|
|
347
|
+
$type: "Select",
|
|
348
|
+
defaultValue: "b",
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it("omits defaultValue when static_select initial_option is not an option", () => {
|
|
353
|
+
const { nodes } = fromSlackBlocks([
|
|
354
|
+
{
|
|
355
|
+
type: "actions",
|
|
356
|
+
elements: [
|
|
357
|
+
{
|
|
358
|
+
type: "static_select",
|
|
359
|
+
action_id: "pick",
|
|
360
|
+
options: [
|
|
361
|
+
{ text: { type: "plain_text", text: "Alpha" }, value: "a" },
|
|
362
|
+
],
|
|
363
|
+
initial_option: {
|
|
364
|
+
text: { type: "plain_text", text: "Missing" },
|
|
365
|
+
value: "missing",
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
],
|
|
369
|
+
},
|
|
370
|
+
]);
|
|
371
|
+
expect(nodes[0]).not.toHaveProperty("defaultValue");
|
|
372
|
+
});
|
|
373
|
+
|
|
322
374
|
it("inverts a datepicker, reading value from initial_date and omitting it when absent", () => {
|
|
323
375
|
const { nodes: withDate } = fromSlackBlocks([
|
|
324
376
|
{
|
|
@@ -394,6 +446,68 @@ describe("fromSlackBlocks", () => {
|
|
|
394
446
|
expect(unchecked[0]).not.toHaveProperty("defaultChecked");
|
|
395
447
|
});
|
|
396
448
|
|
|
449
|
+
it("inverts multi-option checkboxes into CheckboxGroup with matching initial options", () => {
|
|
450
|
+
const { nodes } = fromSlackBlocks([
|
|
451
|
+
{
|
|
452
|
+
type: "actions",
|
|
453
|
+
elements: [
|
|
454
|
+
{
|
|
455
|
+
type: "checkboxes",
|
|
456
|
+
action_id: "sizes",
|
|
457
|
+
value: '{"source":"picker"}',
|
|
458
|
+
options: [
|
|
459
|
+
{ text: { type: "plain_text", text: "Small" }, value: "s" },
|
|
460
|
+
{ text: { type: "plain_text", text: "Large" }, value: "l" },
|
|
461
|
+
],
|
|
462
|
+
initial_options: [
|
|
463
|
+
{ text: { type: "plain_text", text: "Large" }, value: "l" },
|
|
464
|
+
{
|
|
465
|
+
text: { type: "plain_text", text: "Missing" },
|
|
466
|
+
value: "missing",
|
|
467
|
+
},
|
|
468
|
+
],
|
|
469
|
+
},
|
|
470
|
+
],
|
|
471
|
+
},
|
|
472
|
+
]);
|
|
473
|
+
expect(nodes).toEqual([
|
|
474
|
+
{
|
|
475
|
+
$type: "CheckboxGroup",
|
|
476
|
+
options: [
|
|
477
|
+
{ label: "Small", value: "s" },
|
|
478
|
+
{ label: "Large", value: "l" },
|
|
479
|
+
],
|
|
480
|
+
defaultValue: ["l"],
|
|
481
|
+
$action: { type: "sizes", source: "picker" },
|
|
482
|
+
},
|
|
483
|
+
]);
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it("drops malformed CheckboxGroup options", () => {
|
|
487
|
+
const { nodes } = fromSlackBlocks([
|
|
488
|
+
{
|
|
489
|
+
type: "actions",
|
|
490
|
+
elements: [
|
|
491
|
+
{
|
|
492
|
+
type: "checkboxes",
|
|
493
|
+
action_id: "sizes",
|
|
494
|
+
options: [
|
|
495
|
+
{ text: { type: "plain_text", text: "Small" }, value: "s" },
|
|
496
|
+
{ text: { type: "plain_text", text: "Missing value" } },
|
|
497
|
+
],
|
|
498
|
+
},
|
|
499
|
+
],
|
|
500
|
+
},
|
|
501
|
+
]);
|
|
502
|
+
expect(nodes).toEqual([
|
|
503
|
+
{
|
|
504
|
+
$type: "CheckboxGroup",
|
|
505
|
+
options: [{ label: "Small", value: "s" }],
|
|
506
|
+
$action: { type: "sizes" },
|
|
507
|
+
},
|
|
508
|
+
]);
|
|
509
|
+
});
|
|
510
|
+
|
|
397
511
|
it("inverts radio_buttons, reading defaultValue from initial_option and omitting it when absent", () => {
|
|
398
512
|
const { nodes: withValue } = fromSlackBlocks([
|
|
399
513
|
{
|
|
@@ -504,6 +618,33 @@ describe("fromSlackBlocks", () => {
|
|
|
504
618
|
expect(nodes[0]).not.toHaveProperty("multiline");
|
|
505
619
|
});
|
|
506
620
|
|
|
621
|
+
it("reads defaultValue from a string initial_value, including empty text", () => {
|
|
622
|
+
const withText = fromSlackBlocks([
|
|
623
|
+
{
|
|
624
|
+
type: "input",
|
|
625
|
+
label: { type: "plain_text", text: "Notes" },
|
|
626
|
+
element: {
|
|
627
|
+
type: "plain_text_input",
|
|
628
|
+
action_id: "notes",
|
|
629
|
+
initial_value: "Draft reply",
|
|
630
|
+
},
|
|
631
|
+
},
|
|
632
|
+
]);
|
|
633
|
+
const withEmptyText = fromSlackBlocks([
|
|
634
|
+
{
|
|
635
|
+
type: "input",
|
|
636
|
+
label: { type: "plain_text", text: "Notes" },
|
|
637
|
+
element: {
|
|
638
|
+
type: "plain_text_input",
|
|
639
|
+
action_id: "notes",
|
|
640
|
+
initial_value: "",
|
|
641
|
+
},
|
|
642
|
+
},
|
|
643
|
+
]);
|
|
644
|
+
expect(withText.nodes[0]).toMatchObject({ defaultValue: "Draft reply" });
|
|
645
|
+
expect(withEmptyText.nodes[0]).toMatchObject({ defaultValue: "" });
|
|
646
|
+
});
|
|
647
|
+
|
|
507
648
|
it("drops an input block wrapping an unrecognized element", () => {
|
|
508
649
|
const { nodes, warnings } = fromSlackBlocks([
|
|
509
650
|
{
|
|
@@ -765,6 +906,27 @@ describe("fromSlackBlocks", () => {
|
|
|
765
906
|
});
|
|
766
907
|
|
|
767
908
|
describe("round trip", () => {
|
|
909
|
+
it("keeps Input and Select defaultValue props", () => {
|
|
910
|
+
const tree = [
|
|
911
|
+
{
|
|
912
|
+
$type: "Input",
|
|
913
|
+
label: "Notes",
|
|
914
|
+
defaultValue: "Draft reply",
|
|
915
|
+
$action: { type: "notes" },
|
|
916
|
+
},
|
|
917
|
+
{
|
|
918
|
+
$type: "Select",
|
|
919
|
+
options: [
|
|
920
|
+
{ label: "Alpha", value: "a" },
|
|
921
|
+
{ label: "Beta", value: "b" },
|
|
922
|
+
],
|
|
923
|
+
defaultValue: "b",
|
|
924
|
+
$action: { type: "pick" },
|
|
925
|
+
},
|
|
926
|
+
];
|
|
927
|
+
expect(fromSlackBlocks(toSlackBlocks(tree).blocks).nodes).toEqual(tree);
|
|
928
|
+
});
|
|
929
|
+
|
|
768
930
|
const fixtures: readonly {
|
|
769
931
|
readonly name: string;
|
|
770
932
|
readonly tree: unknown;
|
|
@@ -898,6 +1060,29 @@ describe("fromSlackBlocks", () => {
|
|
|
898
1060
|
},
|
|
899
1061
|
],
|
|
900
1062
|
},
|
|
1063
|
+
{
|
|
1064
|
+
name: "CheckboxGroup with defaultValue",
|
|
1065
|
+
tree: {
|
|
1066
|
+
$type: "CheckboxGroup",
|
|
1067
|
+
options: [
|
|
1068
|
+
{ label: "Small", value: "s" },
|
|
1069
|
+
{ label: "Large", value: "l" },
|
|
1070
|
+
],
|
|
1071
|
+
defaultValue: ["s", "l"],
|
|
1072
|
+
$action: { type: "sizes" },
|
|
1073
|
+
},
|
|
1074
|
+
expected: [
|
|
1075
|
+
{
|
|
1076
|
+
$type: "CheckboxGroup",
|
|
1077
|
+
options: [
|
|
1078
|
+
{ label: "Small", value: "s" },
|
|
1079
|
+
{ label: "Large", value: "l" },
|
|
1080
|
+
],
|
|
1081
|
+
defaultValue: ["s", "l"],
|
|
1082
|
+
$action: { type: "sizes" },
|
|
1083
|
+
},
|
|
1084
|
+
],
|
|
1085
|
+
},
|
|
901
1086
|
{
|
|
902
1087
|
name: "Input",
|
|
903
1088
|
tree: {
|
|
@@ -1063,7 +1248,7 @@ describe("fromSlackBlocks", () => {
|
|
|
1063
1248
|
});
|
|
1064
1249
|
|
|
1065
1250
|
describe("fromSlackBlocks checkbox and radio caps", () => {
|
|
1066
|
-
it("marks defaultChecked only when
|
|
1251
|
+
it("marks defaultChecked only when its single option is initially selected", () => {
|
|
1067
1252
|
const base = {
|
|
1068
1253
|
type: "actions",
|
|
1069
1254
|
elements: [
|
|
@@ -1072,7 +1257,6 @@ describe("fromSlackBlocks checkbox and radio caps", () => {
|
|
|
1072
1257
|
action_id: "toggle",
|
|
1073
1258
|
options: [
|
|
1074
1259
|
{ text: { type: "plain_text", text: "First" }, value: "first" },
|
|
1075
|
-
{ text: { type: "plain_text", text: "Second" }, value: "second" },
|
|
1076
1260
|
],
|
|
1077
1261
|
initial_options: [
|
|
1078
1262
|
{ text: { type: "plain_text", text: "Second" }, value: "second" },
|
|
@@ -1119,6 +1303,41 @@ describe("fromSlackBlocks checkbox and radio caps", () => {
|
|
|
1119
1303
|
),
|
|
1120
1304
|
).toBe(true);
|
|
1121
1305
|
});
|
|
1306
|
+
|
|
1307
|
+
it("clamps inbound CheckboxGroup options to the checkbox cap", () => {
|
|
1308
|
+
const { nodes, warnings } = fromSlackBlocks([
|
|
1309
|
+
{
|
|
1310
|
+
type: "actions",
|
|
1311
|
+
elements: [
|
|
1312
|
+
{
|
|
1313
|
+
type: "checkboxes",
|
|
1314
|
+
action_id: "sizes",
|
|
1315
|
+
options: Array.from(
|
|
1316
|
+
{ length: CHECKBOX_OPTION_CAP + 3 },
|
|
1317
|
+
(_, i) => ({
|
|
1318
|
+
text: { type: "plain_text", text: `O${i}` },
|
|
1319
|
+
value: `v${i}`,
|
|
1320
|
+
}),
|
|
1321
|
+
),
|
|
1322
|
+
},
|
|
1323
|
+
],
|
|
1324
|
+
},
|
|
1325
|
+
]);
|
|
1326
|
+
const checkboxGroup = nodes[0];
|
|
1327
|
+
if (
|
|
1328
|
+
checkboxGroup?.$type !== "CheckboxGroup" ||
|
|
1329
|
+
!Array.isArray(checkboxGroup.options)
|
|
1330
|
+
) {
|
|
1331
|
+
throw new Error("Expected a CheckboxGroup with options.");
|
|
1332
|
+
}
|
|
1333
|
+
expect(checkboxGroup.options).toHaveLength(CHECKBOX_OPTION_CAP);
|
|
1334
|
+
expect(
|
|
1335
|
+
warnings.some(
|
|
1336
|
+
(warning) =>
|
|
1337
|
+
warning.component === "CheckboxGroup" && warning.code === "clamped",
|
|
1338
|
+
),
|
|
1339
|
+
).toBe(true);
|
|
1340
|
+
});
|
|
1122
1341
|
});
|
|
1123
1342
|
|
|
1124
1343
|
it("bounds static-select options when an array replaces its slice", () => {
|