@assistant-ui/react-generative-ui 0.0.20 → 0.0.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/dist/JSONGenerativeUI.client.d.ts.map +1 -1
- package/dist/JSONGenerativeUI.client.js +30 -13
- package/dist/JSONGenerativeUI.client.js.map +1 -1
- package/dist/RadioGroupScope.d.ts +6 -0
- package/dist/RadioGroupScope.d.ts.map +1 -0
- package/dist/RadioGroupScope.js +25 -0
- package/dist/RadioGroupScope.js.map +1 -0
- package/dist/a2ui/BindingContext.d.ts +11 -0
- package/dist/a2ui/BindingContext.d.ts.map +1 -0
- package/dist/a2ui/BindingContext.js +12 -0
- package/dist/a2ui/BindingContext.js.map +1 -0
- package/dist/a2ui/PresentRenderer.d.ts +11 -0
- package/dist/a2ui/PresentRenderer.d.ts.map +1 -0
- package/dist/a2ui/PresentRenderer.js +39 -0
- package/dist/a2ui/PresentRenderer.js.map +1 -0
- package/dist/a2ui/convert.d.ts +6 -0
- package/dist/a2ui/convert.d.ts.map +1 -1
- package/dist/a2ui/convert.js +369 -87
- package/dist/a2ui/convert.js.map +1 -1
- package/dist/a2ui/dataModel.d.ts +9 -0
- package/dist/a2ui/dataModel.d.ts.map +1 -0
- package/dist/a2ui/dataModel.js +44 -0
- package/dist/a2ui/dataModel.js.map +1 -0
- package/dist/a2ui/reducer.d.ts +5 -0
- package/dist/a2ui/reducer.d.ts.map +1 -1
- package/dist/a2ui/reducer.js +3 -3
- package/dist/a2ui/reducer.js.map +1 -1
- package/dist/a2ui/types.d.ts +8 -2
- package/dist/a2ui/types.d.ts.map +1 -1
- package/dist/a2ui/types.js.map +1 -1
- package/dist/a2ui/valueFunctions.d.ts +16 -0
- package/dist/a2ui/valueFunctions.d.ts.map +1 -0
- package/dist/a2ui/valueFunctions.js +332 -0
- package/dist/a2ui/valueFunctions.js.map +1 -0
- package/dist/actionRegistry.d.ts +1 -1
- package/dist/actionRegistry.js.map +1 -1
- package/dist/answeredValues.d.ts +16 -0
- package/dist/answeredValues.d.ts.map +1 -0
- package/dist/answeredValues.js +37 -0
- package/dist/answeredValues.js.map +1 -0
- package/dist/constants.d.ts +2 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +3 -1
- package/dist/constants.js.map +1 -1
- package/dist/fieldReferences.d.ts +9 -0
- package/dist/fieldReferences.d.ts.map +1 -0
- package/dist/fieldReferences.js +53 -0
- package/dist/fieldReferences.js.map +1 -0
- package/dist/generativeUIToJSX.js +3 -3
- package/dist/generativeUIToJSX.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.server.js +2 -2
- package/dist/index.shared.js +2 -2
- package/dist/ir.d.ts +1 -1
- package/dist/ir.js.map +1 -1
- package/dist/renderGenerativeUI.d.ts.map +1 -1
- package/dist/renderGenerativeUI.js +12 -1
- package/dist/renderGenerativeUI.js.map +1 -1
- package/dist/slack/constants.d.ts +2 -0
- package/dist/slack/constants.d.ts.map +1 -1
- package/dist/slack/constants.js +3 -1
- package/dist/slack/constants.js.map +1 -1
- package/dist/slack/decodeBlockAction.d.ts +3 -1
- package/dist/slack/decodeBlockAction.d.ts.map +1 -1
- package/dist/slack/decodeBlockAction.js +58 -7
- package/dist/slack/decodeBlockAction.js.map +1 -1
- package/dist/slack/fromSlackBlocks.d.ts.map +1 -1
- package/dist/slack/fromSlackBlocks.js +20 -3
- package/dist/slack/fromSlackBlocks.js.map +1 -1
- package/dist/slack/toSlackBlocks.d.ts.map +1 -1
- package/dist/slack/toSlackBlocks.js +187 -19
- package/dist/slack/toSlackBlocks.js.map +1 -1
- package/dist/slack/types.d.ts +4 -0
- package/dist/slack/types.d.ts.map +1 -1
- package/dist/teams/constants.d.ts +1 -1
- package/dist/teams/constants.d.ts.map +1 -1
- package/dist/teams/constants.js +1 -1
- package/dist/teams/constants.js.map +1 -1
- package/dist/teams/decodeSubmitData.d.ts +4 -0
- package/dist/teams/decodeSubmitData.d.ts.map +1 -1
- package/dist/teams/decodeSubmitData.js +6 -1
- package/dist/teams/decodeSubmitData.js.map +1 -1
- package/dist/teams/toAdaptiveCard.d.ts +5 -4
- package/dist/teams/toAdaptiveCard.d.ts.map +1 -1
- package/dist/teams/toAdaptiveCard.js +56 -17
- package/dist/teams/toAdaptiveCard.js.map +1 -1
- package/dist/teams/types.d.ts +4 -1
- package/dist/teams/types.d.ts.map +1 -1
- package/dist/vocabulary/alert.d.ts +9 -2
- package/dist/vocabulary/alert.d.ts.map +1 -1
- package/dist/vocabulary/alert.js +116 -19
- package/dist/vocabulary/alert.js.map +1 -1
- package/dist/vocabulary/collectFormValues.d.ts +2 -1
- package/dist/vocabulary/collectFormValues.d.ts.map +1 -1
- package/dist/vocabulary/collectFormValues.js +10 -3
- package/dist/vocabulary/collectFormValues.js.map +1 -1
- package/dist/vocabulary/data.d.ts +22 -5
- package/dist/vocabulary/data.d.ts.map +1 -1
- package/dist/vocabulary/data.js +278 -83
- package/dist/vocabulary/data.js.map +1 -1
- package/dist/vocabulary/dispatch.d.ts +3 -2
- package/dist/vocabulary/dispatch.d.ts.map +1 -1
- package/dist/vocabulary/dispatch.js +33 -4
- package/dist/vocabulary/dispatch.js.map +1 -1
- package/dist/vocabulary/fact.d.ts +8 -1
- package/dist/vocabulary/fact.d.ts.map +1 -1
- package/dist/vocabulary/fact.js +37 -11
- package/dist/vocabulary/fact.js.map +1 -1
- package/dist/vocabulary/form.d.ts.map +1 -1
- package/dist/vocabulary/form.js +3 -3
- package/dist/vocabulary/form.js.map +1 -1
- package/dist/vocabulary/formatValue.d.ts +7 -0
- package/dist/vocabulary/formatValue.d.ts.map +1 -0
- package/dist/vocabulary/formatValue.js +76 -0
- package/dist/vocabulary/formatValue.js.map +1 -0
- package/dist/vocabulary/icon.d.ts +1 -1
- package/dist/vocabulary/interactive.d.ts +137 -8
- package/dist/vocabulary/interactive.d.ts.map +1 -1
- package/dist/vocabulary/interactive.js +404 -103
- package/dist/vocabulary/interactive.js.map +1 -1
- package/dist/vocabulary/layout.d.ts.map +1 -1
- package/dist/vocabulary/layout.js +4 -4
- package/dist/vocabulary/layout.js.map +1 -1
- package/dist/vocabulary/list.js +2 -2
- package/dist/vocabulary/list.js.map +1 -1
- package/dist/vocabulary/text.d.ts +1 -1
- package/package.json +6 -3
- package/src/JSONGenerativeUI.client.tsx +84 -40
- package/src/JSONGenerativeUI.test.tsx +964 -68
- package/src/RadioGroupScope.server.test.tsx +24 -0
- package/src/RadioGroupScope.tsx +34 -0
- package/src/a2ui/BindingContext.ts +26 -0
- package/src/a2ui/PresentRenderer.tsx +83 -0
- package/src/a2ui/convert.test.ts +1982 -145
- package/src/a2ui/convert.ts +677 -137
- package/src/a2ui/dataModel.ts +69 -0
- package/src/a2ui/reducer.test.ts +35 -0
- package/src/a2ui/reducer.ts +5 -4
- package/src/a2ui/types.ts +9 -2
- package/src/a2ui/valueFunctions.test.ts +271 -0
- package/src/a2ui/valueFunctions.ts +523 -0
- package/src/actionRegistry.ts +1 -1
- package/src/answeredValues.test.ts +33 -0
- package/src/answeredValues.tsx +70 -0
- package/src/constants.ts +4 -0
- package/src/fieldReferences.test.ts +66 -0
- package/src/fieldReferences.ts +83 -0
- package/src/generativeUIToJSX.test.ts +18 -0
- package/src/generativeUIToJSX.ts +4 -6
- package/src/index.server.test.ts +106 -0
- package/src/ir.ts +1 -1
- package/src/renderGenerativeUI.test.tsx +14 -5
- package/src/renderGenerativeUI.tsx +30 -1
- package/src/slack/a2uiSlack.test.ts +198 -0
- package/src/slack/constants.ts +3 -0
- package/src/slack/decodeBlockAction.test.ts +98 -0
- package/src/slack/decodeBlockAction.ts +110 -17
- package/src/slack/fromSlackBlocks.test.ts +222 -3
- package/src/slack/fromSlackBlocks.ts +53 -2
- package/src/slack/toSlackBlocks.test.ts +682 -3
- package/src/slack/toSlackBlocks.ts +374 -23
- package/src/slack/types.ts +4 -0
- package/src/teams/constants.ts +1 -1
- package/src/teams/decodeSubmitData.test.ts +33 -0
- package/src/teams/decodeSubmitData.ts +9 -1
- package/src/teams/toAdaptiveCard.test.ts +219 -0
- package/src/teams/toAdaptiveCard.ts +93 -19
- package/src/teams/types.ts +4 -1
- package/src/vocabulary/alert.test.tsx +288 -7
- package/src/vocabulary/alert.tsx +168 -26
- package/src/vocabulary/collectFormValues.test.ts +28 -1
- package/src/vocabulary/collectFormValues.ts +25 -5
- package/src/vocabulary/data.test.tsx +248 -6
- package/src/vocabulary/data.tsx +398 -100
- package/src/vocabulary/dispatch.ts +63 -2
- package/src/vocabulary/fact.test.tsx +67 -0
- package/src/vocabulary/fact.tsx +40 -9
- package/src/vocabulary/form.tsx +7 -2
- package/src/vocabulary/formatValue.test.ts +72 -0
- package/src/vocabulary/formatValue.ts +127 -0
- package/src/vocabulary/index.test.ts +2 -0
- package/src/vocabulary/interactive.dispatch.test.tsx +167 -79
- package/src/vocabulary/interactive.dom.test.tsx +1699 -0
- package/src/vocabulary/interactive.test.tsx +60 -4
- package/src/vocabulary/interactive.tsx +770 -151
- package/src/vocabulary/layout.test.tsx +6 -4
- package/src/vocabulary/layout.tsx +13 -2
- package/src/vocabulary/list.tsx +2 -2
|
@@ -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,5 @@
|
|
|
1
1
|
import type { Action } from "../ir";
|
|
2
|
+
import { resolveFieldReferences } from "../fieldReferences";
|
|
2
3
|
|
|
3
4
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
4
5
|
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -8,12 +9,111 @@ const optionValue = (value: unknown): string | undefined =>
|
|
|
8
9
|
? value["value"]
|
|
9
10
|
: undefined;
|
|
10
11
|
|
|
12
|
+
const selectedValue = (action: Record<string, unknown>): unknown => {
|
|
13
|
+
const selectedOption = optionValue(action["selected_option"]);
|
|
14
|
+
const selectedDate =
|
|
15
|
+
typeof action["selected_date"] === "string"
|
|
16
|
+
? action["selected_date"]
|
|
17
|
+
: undefined;
|
|
18
|
+
const selectedOptions = Array.isArray(action["selected_options"])
|
|
19
|
+
? action["selected_options"]
|
|
20
|
+
.map(optionValue)
|
|
21
|
+
.filter((value): value is string => value !== undefined)
|
|
22
|
+
: undefined;
|
|
23
|
+
const numberInput =
|
|
24
|
+
action["type"] === "number_input" && typeof action["value"] === "string"
|
|
25
|
+
? Number(action["value"])
|
|
26
|
+
: undefined;
|
|
27
|
+
return (
|
|
28
|
+
selectedOption ??
|
|
29
|
+
selectedDate ??
|
|
30
|
+
(selectedOptions !== undefined
|
|
31
|
+
? selectedOptions
|
|
32
|
+
: Number.isFinite(numberInput)
|
|
33
|
+
? numberInput
|
|
34
|
+
: undefined)
|
|
35
|
+
);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const fieldMappingsFromBlockId = (
|
|
39
|
+
blockId: string,
|
|
40
|
+
): readonly (readonly [string, string, boolean])[] => {
|
|
41
|
+
if (!/^aui:\d+:/.test(blockId)) return [];
|
|
42
|
+
const mappingStart = blockId.indexOf(":", 4);
|
|
43
|
+
if (mappingStart === -1) return [];
|
|
44
|
+
try {
|
|
45
|
+
const parsed: unknown = JSON.parse(blockId.slice(mappingStart + 1));
|
|
46
|
+
if (!Array.isArray(parsed)) return [];
|
|
47
|
+
return parsed.flatMap((entry): (readonly [string, string, boolean])[] =>
|
|
48
|
+
Array.isArray(entry) &&
|
|
49
|
+
typeof entry[0] === "string" &&
|
|
50
|
+
typeof entry[1] === "string"
|
|
51
|
+
? [[entry[0], entry[1], entry[2] === "Checkbox"]]
|
|
52
|
+
: [],
|
|
53
|
+
);
|
|
54
|
+
} catch {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const checkboxValue = (
|
|
60
|
+
value: Record<string, unknown>,
|
|
61
|
+
name: string,
|
|
62
|
+
): boolean | undefined =>
|
|
63
|
+
Array.isArray(value["selected_options"])
|
|
64
|
+
? value["selected_options"].some((option) => optionValue(option) === name)
|
|
65
|
+
: undefined;
|
|
66
|
+
|
|
67
|
+
const stateFieldValue = (
|
|
68
|
+
value: unknown,
|
|
69
|
+
name: string,
|
|
70
|
+
isCheckbox: boolean,
|
|
71
|
+
): unknown => {
|
|
72
|
+
if (!isRecord(value)) return undefined;
|
|
73
|
+
if (isCheckbox && value["type"] === "checkboxes") {
|
|
74
|
+
return checkboxValue(value, name);
|
|
75
|
+
}
|
|
76
|
+
if (value["type"] === "plain_text_input") {
|
|
77
|
+
return typeof value["value"] === "string" ? value["value"] : undefined;
|
|
78
|
+
}
|
|
79
|
+
return selectedValue(value);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const fieldValuesFromState = (
|
|
83
|
+
stateValues: unknown,
|
|
84
|
+
): Record<string, unknown> => {
|
|
85
|
+
const fields: Record<string, unknown> = Object.create(null);
|
|
86
|
+
if (!isRecord(stateValues)) return fields;
|
|
87
|
+
for (const [blockId, rawBlockValues] of Object.entries(stateValues)) {
|
|
88
|
+
if (!isRecord(rawBlockValues)) continue;
|
|
89
|
+
for (const [actionId, name, isCheckbox] of fieldMappingsFromBlockId(
|
|
90
|
+
blockId,
|
|
91
|
+
)) {
|
|
92
|
+
if (!Object.hasOwn(rawBlockValues, actionId)) continue;
|
|
93
|
+
const value = stateFieldValue(rawBlockValues[actionId], name, isCheckbox);
|
|
94
|
+
if (value === undefined) continue;
|
|
95
|
+
Object.defineProperty(fields, name, {
|
|
96
|
+
value,
|
|
97
|
+
enumerable: true,
|
|
98
|
+
configurable: true,
|
|
99
|
+
writable: true,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return fields;
|
|
104
|
+
};
|
|
105
|
+
|
|
11
106
|
/**
|
|
12
107
|
* Decodes one structural entry from a Slack `block_actions` payload.
|
|
13
108
|
* `$input` is reserved for the runtime selection: a `$input` key inside the
|
|
14
109
|
* button's JSON `value` payload is dropped rather than spread into the result.
|
|
110
|
+
* `stateValues` is the companion `state.values` object used to resolve named
|
|
111
|
+
* control values referenced by `{ "$field": name }` in the action payload.
|
|
15
112
|
*/
|
|
16
|
-
export function decodeBlockAction(
|
|
113
|
+
export function decodeBlockAction(
|
|
114
|
+
action: unknown,
|
|
115
|
+
stateValues?: unknown,
|
|
116
|
+
): Action | undefined {
|
|
17
117
|
try {
|
|
18
118
|
if (!isRecord(action) || typeof action["action_id"] !== "string") {
|
|
19
119
|
return undefined;
|
|
@@ -25,7 +125,9 @@ export function decodeBlockAction(action: unknown): Action | undefined {
|
|
|
25
125
|
const rawValue = action["value"];
|
|
26
126
|
let payload: Record<string, unknown> = {};
|
|
27
127
|
let plainValue: string | undefined;
|
|
28
|
-
if (typeof rawValue === "string") {
|
|
128
|
+
if (action["type"] === "plain_text_input" && typeof rawValue === "string") {
|
|
129
|
+
plainValue = rawValue;
|
|
130
|
+
} else if (typeof rawValue === "string") {
|
|
29
131
|
try {
|
|
30
132
|
const parsed: unknown = JSON.parse(rawValue);
|
|
31
133
|
if (isRecord(parsed)) {
|
|
@@ -38,28 +140,19 @@ export function decodeBlockAction(action: unknown): Action | undefined {
|
|
|
38
140
|
}
|
|
39
141
|
}
|
|
40
142
|
|
|
41
|
-
const
|
|
42
|
-
const selectedDate =
|
|
43
|
-
typeof action["selected_date"] === "string"
|
|
44
|
-
? action["selected_date"]
|
|
45
|
-
: undefined;
|
|
46
|
-
const selectedOptions = Array.isArray(action["selected_options"])
|
|
47
|
-
? action["selected_options"]
|
|
48
|
-
.map(optionValue)
|
|
49
|
-
.filter((value): value is string => value !== undefined)
|
|
50
|
-
: undefined;
|
|
51
|
-
const input =
|
|
52
|
-
selectedOption ??
|
|
53
|
-
selectedDate ??
|
|
54
|
-
(selectedOptions !== undefined ? selectedOptions : plainValue);
|
|
143
|
+
const input = selectedValue(action) ?? plainValue;
|
|
55
144
|
|
|
56
|
-
|
|
145
|
+
const decoded = {
|
|
57
146
|
...Object.fromEntries(
|
|
58
147
|
Object.entries(payload).filter(([key]) => key !== "$input"),
|
|
59
148
|
),
|
|
60
149
|
type: actionId,
|
|
61
150
|
...(input !== undefined ? { $input: input } : {}),
|
|
62
151
|
};
|
|
152
|
+
return resolveFieldReferences(
|
|
153
|
+
decoded,
|
|
154
|
+
fieldValuesFromState(stateValues),
|
|
155
|
+
) as Action;
|
|
63
156
|
} catch {
|
|
64
157
|
return undefined;
|
|
65
158
|
}
|
|
@@ -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", () => {
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
ACTIONS_ELEMENT_CAP,
|
|
5
5
|
CARD_ACTIONS_CAP,
|
|
6
6
|
CAROUSEL_CARD_CAP,
|
|
7
|
+
CHECKBOX_OPTION_CAP,
|
|
7
8
|
CONTEXT_ELEMENT_CAP,
|
|
8
9
|
DATA_TABLE_COLUMN_CAP,
|
|
9
10
|
DATA_TABLE_ROW_CAP,
|
|
@@ -212,10 +213,20 @@ const selectFrom = (
|
|
|
212
213
|
"Select",
|
|
213
214
|
`options were clamped to ${SELECT_OPTION_CAP} entries.`,
|
|
214
215
|
);
|
|
216
|
+
const convertedOptions = options.map(optionFrom).filter(isDefined);
|
|
217
|
+
const initialOption = element["initial_option"];
|
|
218
|
+
const defaultValue =
|
|
219
|
+
isRecord(initialOption) && typeof initialOption["value"] === "string"
|
|
220
|
+
? initialOption["value"]
|
|
221
|
+
: undefined;
|
|
215
222
|
const placeholder = element["placeholder"];
|
|
216
223
|
return {
|
|
217
224
|
$type: "Select",
|
|
218
|
-
options:
|
|
225
|
+
options: convertedOptions,
|
|
226
|
+
...(defaultValue !== undefined &&
|
|
227
|
+
convertedOptions.some((option) => option.value === defaultValue)
|
|
228
|
+
? { defaultValue }
|
|
229
|
+
: {}),
|
|
219
230
|
...(isRecord(placeholder) ? { placeholder: textOf(placeholder) } : {}),
|
|
220
231
|
$action: decodeAction(element["action_id"], element["value"]),
|
|
221
232
|
};
|
|
@@ -250,6 +261,40 @@ const checkboxFrom = (element: Record<string, unknown>): UIElement => {
|
|
|
250
261
|
};
|
|
251
262
|
};
|
|
252
263
|
|
|
264
|
+
const checkboxGroupFrom = (
|
|
265
|
+
element: Record<string, unknown>,
|
|
266
|
+
warnings: SlackConversionWarning[],
|
|
267
|
+
): UIElement => {
|
|
268
|
+
const options = boundedArray(
|
|
269
|
+
element["options"],
|
|
270
|
+
CHECKBOX_OPTION_CAP,
|
|
271
|
+
warnings,
|
|
272
|
+
"CheckboxGroup",
|
|
273
|
+
`options were clamped to ${CHECKBOX_OPTION_CAP} entries.`,
|
|
274
|
+
)
|
|
275
|
+
.map(optionFrom)
|
|
276
|
+
.filter(isDefined);
|
|
277
|
+
const defaultValue = clampArray(
|
|
278
|
+
element["initial_options"],
|
|
279
|
+
CHECKBOX_OPTION_CAP,
|
|
280
|
+
).items.flatMap((initialOption) => {
|
|
281
|
+
const value =
|
|
282
|
+
isRecord(initialOption) && typeof initialOption["value"] === "string"
|
|
283
|
+
? initialOption["value"]
|
|
284
|
+
: undefined;
|
|
285
|
+
return value !== undefined &&
|
|
286
|
+
options.some((option) => option.value === value)
|
|
287
|
+
? [value]
|
|
288
|
+
: [];
|
|
289
|
+
});
|
|
290
|
+
return {
|
|
291
|
+
$type: "CheckboxGroup",
|
|
292
|
+
options,
|
|
293
|
+
...(defaultValue.length > 0 ? { defaultValue } : {}),
|
|
294
|
+
$action: decodeAction(element["action_id"], element["value"]),
|
|
295
|
+
};
|
|
296
|
+
};
|
|
297
|
+
|
|
253
298
|
const radioGroupFrom = (
|
|
254
299
|
element: Record<string, unknown>,
|
|
255
300
|
warnings: SlackConversionWarning[],
|
|
@@ -287,7 +332,10 @@ const actionElementFrom = (
|
|
|
287
332
|
case "datepicker":
|
|
288
333
|
return datePickerFrom(element);
|
|
289
334
|
case "checkboxes":
|
|
290
|
-
return
|
|
335
|
+
return Array.isArray(element["options"]) &&
|
|
336
|
+
element["options"].length > 1
|
|
337
|
+
? checkboxGroupFrom(element, warnings)
|
|
338
|
+
: checkboxFrom(element);
|
|
291
339
|
case "radio_buttons":
|
|
292
340
|
return radioGroupFrom(element, warnings);
|
|
293
341
|
}
|
|
@@ -340,6 +388,9 @@ const inputFrom = (
|
|
|
340
388
|
label: textOf(block["label"]),
|
|
341
389
|
...(isRecord(placeholder) ? { placeholder: textOf(placeholder) } : {}),
|
|
342
390
|
...(element["multiline"] === true ? { multiline: true } : {}),
|
|
391
|
+
...(typeof element["initial_value"] === "string"
|
|
392
|
+
? { defaultValue: element["initial_value"] }
|
|
393
|
+
: {}),
|
|
343
394
|
$action: decodeAction(element["action_id"], undefined),
|
|
344
395
|
},
|
|
345
396
|
];
|