@assistant-ui/react-generative-ui 0.0.23 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/a2ui.d.ts +1 -4
- package/dist/a2ui.d.ts.map +1 -1
- package/dist/a2ui.js +1 -4
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -10
- package/dist/index.server.d.ts +1 -2
- package/dist/index.server.d.ts.map +1 -1
- package/dist/index.server.js +1 -10
- package/dist/ir.d.ts +1 -111
- package/dist/ir.d.ts.map +1 -1
- package/dist/ir.js +1 -175
- package/dist/slack.d.ts +1 -4
- package/dist/slack.d.ts.map +1 -1
- package/dist/slack.js +1 -4
- package/dist/teams.d.ts +1 -4
- package/dist/teams.d.ts.map +1 -1
- package/dist/teams.js +1 -4
- package/package.json +5 -5
- package/src/a2ui.ts +1 -22
- package/src/index.server.ts +1 -2
- package/src/index.test.ts +75 -0
- package/src/index.ts +1 -2
- package/src/ir.ts +1 -279
- package/src/slack.ts +1 -39
- package/src/teams.ts +1 -35
- package/dist/JSONGenerativeUI.client.d.ts +0 -28
- package/dist/JSONGenerativeUI.client.d.ts.map +0 -1
- package/dist/JSONGenerativeUI.client.js +0 -101
- package/dist/JSONGenerativeUI.client.js.map +0 -1
- package/dist/JSONGenerativeUI.server.d.ts +0 -21
- package/dist/JSONGenerativeUI.server.d.ts.map +0 -1
- package/dist/JSONGenerativeUI.server.js +0 -32
- package/dist/JSONGenerativeUI.server.js.map +0 -1
- package/dist/JSONGenerativeUI.shared.d.ts +0 -63
- package/dist/JSONGenerativeUI.shared.d.ts.map +0 -1
- package/dist/JSONGenerativeUI.shared.js +0 -31
- package/dist/JSONGenerativeUI.shared.js.map +0 -1
- package/dist/RadioGroupScope.d.ts +0 -6
- package/dist/RadioGroupScope.d.ts.map +0 -1
- package/dist/RadioGroupScope.js +0 -25
- package/dist/RadioGroupScope.js.map +0 -1
- package/dist/a2ui/BindingContext.d.ts +0 -11
- package/dist/a2ui/BindingContext.d.ts.map +0 -1
- package/dist/a2ui/BindingContext.js +0 -12
- package/dist/a2ui/BindingContext.js.map +0 -1
- package/dist/a2ui/PresentRenderer.d.ts +0 -11
- package/dist/a2ui/PresentRenderer.d.ts.map +0 -1
- package/dist/a2ui/PresentRenderer.js +0 -39
- package/dist/a2ui/PresentRenderer.js.map +0 -1
- package/dist/a2ui/convert.d.ts +0 -15
- package/dist/a2ui/convert.d.ts.map +0 -1
- package/dist/a2ui/convert.js +0 -728
- package/dist/a2ui/convert.js.map +0 -1
- package/dist/a2ui/dataModel.d.ts +0 -9
- package/dist/a2ui/dataModel.d.ts.map +0 -1
- package/dist/a2ui/dataModel.js +0 -44
- package/dist/a2ui/dataModel.js.map +0 -1
- package/dist/a2ui/reducer.d.ts +0 -8
- package/dist/a2ui/reducer.d.ts.map +0 -1
- package/dist/a2ui/reducer.js +0 -242
- package/dist/a2ui/reducer.js.map +0 -1
- package/dist/a2ui/snapshot.d.ts +0 -3
- package/dist/a2ui/snapshot.d.ts.map +0 -1
- package/dist/a2ui/snapshot.js +0 -33
- package/dist/a2ui/snapshot.js.map +0 -1
- package/dist/a2ui/types.d.ts +0 -88
- package/dist/a2ui/types.d.ts.map +0 -1
- package/dist/a2ui/types.js +0 -6
- package/dist/a2ui/types.js.map +0 -1
- package/dist/a2ui/valueFunctions.d.ts +0 -16
- package/dist/a2ui/valueFunctions.d.ts.map +0 -1
- package/dist/a2ui/valueFunctions.js +0 -332
- package/dist/a2ui/valueFunctions.js.map +0 -1
- package/dist/actionRegistry.d.ts +0 -29
- package/dist/actionRegistry.d.ts.map +0 -1
- package/dist/actionRegistry.js +0 -41
- package/dist/actionRegistry.js.map +0 -1
- package/dist/answeredValues.d.ts +0 -16
- package/dist/answeredValues.d.ts.map +0 -1
- package/dist/answeredValues.js +0 -37
- package/dist/answeredValues.js.map +0 -1
- package/dist/buildPresentParameters.d.ts +0 -21
- package/dist/buildPresentParameters.d.ts.map +0 -1
- package/dist/buildPresentParameters.js +0 -95
- package/dist/buildPresentParameters.js.map +0 -1
- package/dist/constants.d.ts +0 -15
- package/dist/constants.d.ts.map +0 -1
- package/dist/constants.js +0 -19
- package/dist/constants.js.map +0 -1
- package/dist/convert/boundSpec.d.ts +0 -48
- package/dist/convert/boundSpec.d.ts.map +0 -1
- package/dist/convert/boundSpec.js +0 -108
- package/dist/convert/boundSpec.js.map +0 -1
- package/dist/convert/copyBounded.d.ts +0 -25
- package/dist/convert/copyBounded.d.ts.map +0 -1
- package/dist/convert/copyBounded.js +0 -38
- package/dist/convert/copyBounded.js.map +0 -1
- package/dist/convert/isElement.d.ts +0 -3
- package/dist/convert/isElement.d.ts.map +0 -1
- package/dist/convert/isElement.js +0 -6
- package/dist/convert/isElement.js.map +0 -1
- package/dist/convert/takeRun.d.ts +0 -6
- package/dist/convert/takeRun.d.ts.map +0 -1
- package/dist/convert/takeRun.js +0 -20
- package/dist/convert/takeRun.js.map +0 -1
- package/dist/defineGenerativeComponents.d.ts +0 -29
- package/dist/defineGenerativeComponents.d.ts.map +0 -1
- package/dist/defineGenerativeComponents.js +0 -34
- package/dist/defineGenerativeComponents.js.map +0 -1
- package/dist/fieldReferences.d.ts +0 -9
- package/dist/fieldReferences.d.ts.map +0 -1
- package/dist/fieldReferences.js +0 -53
- package/dist/fieldReferences.js.map +0 -1
- package/dist/generativeUIToJSX.d.ts +0 -18
- package/dist/generativeUIToJSX.d.ts.map +0 -1
- package/dist/generativeUIToJSX.js +0 -76
- package/dist/generativeUIToJSX.js.map +0 -1
- package/dist/index.shared.d.ts +0 -15
- package/dist/index.shared.d.ts.map +0 -1
- package/dist/index.shared.js +0 -9
- package/dist/ir.js.map +0 -1
- package/dist/renderGenerativeUI.d.ts +0 -14
- package/dist/renderGenerativeUI.d.ts.map +0 -1
- package/dist/renderGenerativeUI.js +0 -83
- package/dist/renderGenerativeUI.js.map +0 -1
- package/dist/scopeSchema.d.ts +0 -6
- package/dist/scopeSchema.d.ts.map +0 -1
- package/dist/scopeSchema.js +0 -56
- package/dist/scopeSchema.js.map +0 -1
- package/dist/slack/constants.d.ts +0 -93
- package/dist/slack/constants.d.ts.map +0 -1
- package/dist/slack/constants.js +0 -146
- package/dist/slack/constants.js.map +0 -1
- package/dist/slack/decodeBlockAction.d.ts +0 -10
- package/dist/slack/decodeBlockAction.d.ts.map +0 -1
- package/dist/slack/decodeBlockAction.js +0 -129
- package/dist/slack/decodeBlockAction.js.map +0 -1
- package/dist/slack/fromSlackBlocks.d.ts +0 -27
- package/dist/slack/fromSlackBlocks.d.ts.map +0 -1
- package/dist/slack/fromSlackBlocks.js +0 -483
- package/dist/slack/fromSlackBlocks.js.map +0 -1
- package/dist/slack/toSlackBlocks.d.ts +0 -4
- package/dist/slack/toSlackBlocks.d.ts.map +0 -1
- package/dist/slack/toSlackBlocks.js +0 -901
- package/dist/slack/toSlackBlocks.js.map +0 -1
- package/dist/slack/types.d.ts +0 -204
- package/dist/slack/types.d.ts.map +0 -1
- package/dist/slack/types.js +0 -0
- package/dist/teams/constants.d.ts +0 -34
- package/dist/teams/constants.d.ts.map +0 -1
- package/dist/teams/constants.js +0 -68
- package/dist/teams/constants.js.map +0 -1
- package/dist/teams/decodeSubmitData.d.ts +0 -26
- package/dist/teams/decodeSubmitData.d.ts.map +0 -1
- package/dist/teams/decodeSubmitData.js +0 -70
- package/dist/teams/decodeSubmitData.js.map +0 -1
- package/dist/teams/temporalId.d.ts +0 -5
- package/dist/teams/temporalId.d.ts.map +0 -1
- package/dist/teams/temporalId.js +0 -28
- package/dist/teams/temporalId.js.map +0 -1
- package/dist/teams/toAdaptiveCard.d.ts +0 -45
- package/dist/teams/toAdaptiveCard.d.ts.map +0 -1
- package/dist/teams/toAdaptiveCard.js +0 -642
- package/dist/teams/toAdaptiveCard.js.map +0 -1
- package/dist/teams/toTeamsAttachments.d.ts +0 -10
- package/dist/teams/toTeamsAttachments.d.ts.map +0 -1
- package/dist/teams/toTeamsAttachments.js +0 -99
- package/dist/teams/toTeamsAttachments.js.map +0 -1
- package/dist/teams/types.d.ts +0 -224
- package/dist/teams/types.d.ts.map +0 -1
- package/dist/teams/types.js +0 -0
- package/dist/temporal.d.ts +0 -33
- package/dist/temporal.d.ts.map +0 -1
- package/dist/temporal.js +0 -152
- package/dist/temporal.js.map +0 -1
- package/dist/types.d.ts +0 -99
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -0
- package/dist/vocabulary/alert.d.ts +0 -32
- package/dist/vocabulary/alert.d.ts.map +0 -1
- package/dist/vocabulary/alert.js +0 -160
- package/dist/vocabulary/alert.js.map +0 -1
- package/dist/vocabulary/collectFormValues.d.ts +0 -23
- package/dist/vocabulary/collectFormValues.d.ts.map +0 -1
- package/dist/vocabulary/collectFormValues.js +0 -44
- package/dist/vocabulary/collectFormValues.js.map +0 -1
- package/dist/vocabulary/data.d.ts +0 -66
- package/dist/vocabulary/data.d.ts.map +0 -1
- package/dist/vocabulary/data.js +0 -503
- package/dist/vocabulary/data.js.map +0 -1
- package/dist/vocabulary/dispatch.d.ts +0 -9
- package/dist/vocabulary/dispatch.d.ts.map +0 -1
- package/dist/vocabulary/dispatch.js +0 -50
- package/dist/vocabulary/dispatch.js.map +0 -1
- package/dist/vocabulary/fact.d.ts +0 -19
- package/dist/vocabulary/fact.d.ts.map +0 -1
- package/dist/vocabulary/fact.js +0 -51
- package/dist/vocabulary/fact.js.map +0 -1
- package/dist/vocabulary/form.d.ts +0 -11
- package/dist/vocabulary/form.d.ts.map +0 -1
- package/dist/vocabulary/form.js +0 -23
- package/dist/vocabulary/form.js.map +0 -1
- package/dist/vocabulary/formatValue.d.ts +0 -7
- package/dist/vocabulary/formatValue.d.ts.map +0 -1
- package/dist/vocabulary/formatValue.js +0 -76
- package/dist/vocabulary/formatValue.js.map +0 -1
- package/dist/vocabulary/icon.d.ts +0 -41
- package/dist/vocabulary/icon.d.ts.map +0 -1
- package/dist/vocabulary/icon.js +0 -272
- package/dist/vocabulary/icon.js.map +0 -1
- package/dist/vocabulary/index.d.ts +0 -12
- package/dist/vocabulary/index.d.ts.map +0 -1
- package/dist/vocabulary/index.js +0 -36
- package/dist/vocabulary/index.js.map +0 -1
- package/dist/vocabulary/interactive.d.ts +0 -230
- package/dist/vocabulary/interactive.d.ts.map +0 -1
- package/dist/vocabulary/interactive.js +0 -530
- package/dist/vocabulary/interactive.js.map +0 -1
- package/dist/vocabulary/layout.d.ts +0 -79
- package/dist/vocabulary/layout.d.ts.map +0 -1
- package/dist/vocabulary/layout.js +0 -139
- package/dist/vocabulary/layout.js.map +0 -1
- package/dist/vocabulary/list.d.ts +0 -14
- package/dist/vocabulary/list.d.ts.map +0 -1
- package/dist/vocabulary/list.js +0 -50
- package/dist/vocabulary/list.js.map +0 -1
- package/dist/vocabulary/media.d.ts +0 -25
- package/dist/vocabulary/media.d.ts.map +0 -1
- package/dist/vocabulary/media.js +0 -42
- package/dist/vocabulary/media.js.map +0 -1
- package/dist/vocabulary/text.d.ts +0 -57
- package/dist/vocabulary/text.d.ts.map +0 -1
- package/dist/vocabulary/text.js +0 -49
- package/dist/vocabulary/text.js.map +0 -1
- package/dist/vocabulary/toTextContent.d.ts +0 -2
- package/dist/vocabulary/toTextContent.d.ts.map +0 -1
- package/dist/vocabulary/toTextContent.js +0 -6
- package/dist/vocabulary/toTextContent.js.map +0 -1
- package/src/JSONGenerativeUI.client.tsx +0 -168
- package/src/JSONGenerativeUI.server.tsx +0 -39
- package/src/JSONGenerativeUI.shared.ts +0 -90
- package/src/JSONGenerativeUI.test.tsx +0 -1298
- package/src/RadioGroupScope.server.test.tsx +0 -24
- package/src/RadioGroupScope.tsx +0 -34
- package/src/a2ui/BindingContext.ts +0 -26
- package/src/a2ui/PresentRenderer.tsx +0 -83
- package/src/a2ui/convert.test.ts +0 -2923
- package/src/a2ui/convert.ts +0 -1207
- package/src/a2ui/dataModel.ts +0 -69
- package/src/a2ui/reducer.test.ts +0 -422
- package/src/a2ui/reducer.ts +0 -363
- package/src/a2ui/snapshot.test.ts +0 -52
- package/src/a2ui/snapshot.ts +0 -52
- package/src/a2ui/types.ts +0 -114
- package/src/a2ui/valueFunctions.test.ts +0 -271
- package/src/a2ui/valueFunctions.ts +0 -523
- package/src/actionRegistry.test.ts +0 -90
- package/src/actionRegistry.ts +0 -88
- package/src/answeredValues.test.ts +0 -33
- package/src/answeredValues.tsx +0 -70
- package/src/buildPresentParameters.test.ts +0 -246
- package/src/buildPresentParameters.ts +0 -130
- package/src/constants.ts +0 -17
- package/src/convert/boundSpec.test.ts +0 -192
- package/src/convert/boundSpec.ts +0 -134
- package/src/convert/copyBounded.test.ts +0 -131
- package/src/convert/copyBounded.ts +0 -38
- package/src/convert/isElement.ts +0 -6
- package/src/convert/takeRun.test.ts +0 -64
- package/src/convert/takeRun.ts +0 -19
- package/src/defineGenerativeComponents.ts +0 -38
- package/src/fieldReferences.test.ts +0 -66
- package/src/fieldReferences.ts +0 -83
- package/src/generativeUIToJSX.test.ts +0 -268
- package/src/generativeUIToJSX.ts +0 -152
- package/src/index.server.test.ts +0 -106
- package/src/index.shared.ts +0 -61
- package/src/ir.test.ts +0 -203
- package/src/renderGenerativeUI.test.tsx +0 -373
- package/src/renderGenerativeUI.tsx +0 -165
- package/src/scopeSchema.test.ts +0 -72
- package/src/scopeSchema.ts +0 -71
- package/src/slack/a2uiSlack.test.ts +0 -198
- package/src/slack/constants.ts +0 -194
- package/src/slack/datePicker.test.ts +0 -612
- package/src/slack/decodeBlockAction.test.ts +0 -233
- package/src/slack/decodeBlockAction.ts +0 -232
- package/src/slack/fromSlackBlocks.test.ts +0 -1465
- package/src/slack/fromSlackBlocks.ts +0 -789
- package/src/slack/toSlackBlocks.test.ts +0 -3065
- package/src/slack/toSlackBlocks.ts +0 -1818
- package/src/slack/types.ts +0 -264
- package/src/teams/constants.ts +0 -93
- package/src/teams/decodeSubmitData.test.ts +0 -397
- package/src/teams/decodeSubmitData.ts +0 -110
- package/src/teams/temporal.test.ts +0 -766
- package/src/teams/temporalId.ts +0 -56
- package/src/teams/toAdaptiveCard.test.ts +0 -1897
- package/src/teams/toAdaptiveCard.ts +0 -1057
- package/src/teams/toTeamsAttachments.test.ts +0 -74
- package/src/teams/toTeamsAttachments.ts +0 -133
- package/src/teams/types.ts +0 -266
- package/src/temporal.test.ts +0 -319
- package/src/temporal.ts +0 -259
- package/src/types.ts +0 -119
- package/src/vocabulary/alert.test.tsx +0 -427
- package/src/vocabulary/alert.tsx +0 -217
- package/src/vocabulary/collectFormValues.test.ts +0 -389
- package/src/vocabulary/collectFormValues.ts +0 -116
- package/src/vocabulary/data.test.tsx +0 -858
- package/src/vocabulary/data.tsx +0 -819
- package/src/vocabulary/datepicker.dom.test.tsx +0 -1148
- package/src/vocabulary/dispatch.ts +0 -83
- package/src/vocabulary/fact.test.tsx +0 -94
- package/src/vocabulary/fact.tsx +0 -54
- package/src/vocabulary/form.test.tsx +0 -113
- package/src/vocabulary/form.tsx +0 -39
- package/src/vocabulary/formatValue.test.ts +0 -72
- package/src/vocabulary/formatValue.ts +0 -127
- package/src/vocabulary/icon.test.tsx +0 -64
- package/src/vocabulary/icon.tsx +0 -174
- package/src/vocabulary/index.test.ts +0 -65
- package/src/vocabulary/index.ts +0 -33
- package/src/vocabulary/interactive.dispatch.test.tsx +0 -454
- package/src/vocabulary/interactive.dom.test.tsx +0 -1747
- package/src/vocabulary/interactive.test.tsx +0 -377
- package/src/vocabulary/interactive.tsx +0 -1048
- package/src/vocabulary/layout.test.tsx +0 -323
- package/src/vocabulary/layout.tsx +0 -247
- package/src/vocabulary/list.test.tsx +0 -222
- package/src/vocabulary/list.tsx +0 -50
- package/src/vocabulary/media.test.tsx +0 -85
- package/src/vocabulary/media.tsx +0 -56
- package/src/vocabulary/text.test.tsx +0 -102
- package/src/vocabulary/text.tsx +0 -60
- package/src/vocabulary/toTextContent.ts +0 -2
|
@@ -1,397 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { decodeSubmitData } from "./decodeSubmitData";
|
|
3
|
-
import { toAdaptiveCard } from "./toAdaptiveCard";
|
|
4
|
-
|
|
5
|
-
describe("decodeSubmitData", () => {
|
|
6
|
-
describe.each(["2025-12-15T17:00:30.123456+02:00", "2025-12-15T17:00", ""])(
|
|
7
|
-
"partial datetime submission for %j",
|
|
8
|
-
(value) => {
|
|
9
|
-
it.each(["Input.Date", "Input.Time"])(
|
|
10
|
-
"clears the field when only %s is submitted",
|
|
11
|
-
(submittedType) => {
|
|
12
|
-
const { card } = toAdaptiveCard({
|
|
13
|
-
$type: "DatePicker",
|
|
14
|
-
name: "when",
|
|
15
|
-
inputType: "datetime",
|
|
16
|
-
value,
|
|
17
|
-
$action: {
|
|
18
|
-
type: "save",
|
|
19
|
-
selected: { $field: "when", fallback: "missing" },
|
|
20
|
-
},
|
|
21
|
-
});
|
|
22
|
-
const input = card.body.find(
|
|
23
|
-
(element) => element.type === submittedType,
|
|
24
|
-
);
|
|
25
|
-
const action = card.body.find(
|
|
26
|
-
(element) => element.type === "ActionSet",
|
|
27
|
-
);
|
|
28
|
-
if (!input || !("id" in input) || action?.type !== "ActionSet")
|
|
29
|
-
throw new Error("Missing picker input or action");
|
|
30
|
-
const submitted = {
|
|
31
|
-
...action.actions[0]!.data,
|
|
32
|
-
[input.id]: submittedType === "Input.Date" ? "2025-12-16" : "18:20",
|
|
33
|
-
};
|
|
34
|
-
expect(Object.keys(submitted)).toHaveLength(2);
|
|
35
|
-
expect(decodeSubmitData(submitted)).toEqual({
|
|
36
|
-
type: "save",
|
|
37
|
-
selected: "",
|
|
38
|
-
$input: { when: "" },
|
|
39
|
-
});
|
|
40
|
-
},
|
|
41
|
-
);
|
|
42
|
-
},
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
it.each([
|
|
46
|
-
"aui:datetime:",
|
|
47
|
-
"aui:datetime:when",
|
|
48
|
-
"aui:datetime:time:when::extra",
|
|
49
|
-
"aui:datetime:time:when:%",
|
|
50
|
-
"aui:datetime:time:%E0%A4%A:",
|
|
51
|
-
"aui:datetime:time:%77hen:",
|
|
52
|
-
"aui:datetime:time:when:not-an-instant",
|
|
53
|
-
"aui:datetime:time:when:2025-02-30T17%3A00%3A00Z",
|
|
54
|
-
"aui:datetime:time:when:2025-12-15T17%3A00",
|
|
55
|
-
"aui:datetime:time:when:%7B%22dateId%22%3A%22other%22%7D",
|
|
56
|
-
"aui:datetime:time::",
|
|
57
|
-
"aui:datetime:time:aui:",
|
|
58
|
-
"aui:datetime:time:aui%3Adatetime%3Awhen%3A:",
|
|
59
|
-
"aui:datetime:missing:",
|
|
60
|
-
"aui:datetime:time:other:",
|
|
61
|
-
])("rejects forged or malformed time input id %j", (id) => {
|
|
62
|
-
const value = {
|
|
63
|
-
aui: { type: "save", payload: { selected: { $field: "when" } } },
|
|
64
|
-
when: "2025-12-15",
|
|
65
|
-
other: "untouched",
|
|
66
|
-
[id]: "17:00",
|
|
67
|
-
};
|
|
68
|
-
expect(decodeSubmitData(value)).toBeUndefined();
|
|
69
|
-
expect(value.when).toBe("2025-12-15");
|
|
70
|
-
expect(value.other).toBe("untouched");
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it.each([
|
|
74
|
-
"aui:datetime:date:",
|
|
75
|
-
"aui:datetime:date:when:",
|
|
76
|
-
"aui:datetime:date:when:2025-12-15T17%3A00Z",
|
|
77
|
-
"aui:datetime:date:%",
|
|
78
|
-
"aui:datetime:date:%E0%A4%A",
|
|
79
|
-
"aui:datetime:date:%77hen",
|
|
80
|
-
"aui:datetime:date:aui",
|
|
81
|
-
"aui:datetime:date:aui%3Adatetime%3Awhen",
|
|
82
|
-
"aui:datetime:time:when",
|
|
83
|
-
"aui:datetime:time:%77hen:",
|
|
84
|
-
"aui:datetime:time:when:not-an-instant",
|
|
85
|
-
"aui:datetime:time:when:2025-02-30T17%3A00%3A00Z",
|
|
86
|
-
"aui:datetime:time:when:2025-12-15T17%3a00%3a00Z",
|
|
87
|
-
"aui:datetime:other:when",
|
|
88
|
-
"aui:datetime:when:",
|
|
89
|
-
])("rejects malformed temporal id %j even without another field", (id) => {
|
|
90
|
-
expect(
|
|
91
|
-
decodeSubmitData({ aui: { type: "save" }, [id]: "" }),
|
|
92
|
-
).toBeUndefined();
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it.each(["date", "time"])(
|
|
96
|
-
"rejects a forged %s id targeting an ordinary field",
|
|
97
|
-
(role) => {
|
|
98
|
-
const id = `aui:datetime:${role}:other${role === "time" ? ":" : ""}`;
|
|
99
|
-
const submitted = role === "date" ? "2025-12-16" : "18:20";
|
|
100
|
-
for (const other of ["2025-12-15", "17:00", "", "untouched"]) {
|
|
101
|
-
for (const entries of [
|
|
102
|
-
[
|
|
103
|
-
["other", other],
|
|
104
|
-
[id, submitted],
|
|
105
|
-
],
|
|
106
|
-
[
|
|
107
|
-
[id, submitted],
|
|
108
|
-
["other", other],
|
|
109
|
-
],
|
|
110
|
-
]) {
|
|
111
|
-
const value = Object.freeze({
|
|
112
|
-
aui: { type: "save" },
|
|
113
|
-
...Object.fromEntries(entries),
|
|
114
|
-
});
|
|
115
|
-
expect(decodeSubmitData(value)).toBeUndefined();
|
|
116
|
-
expect(value["other"]).toBe(other);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
},
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
it.each([
|
|
123
|
-
["date", "2025-02-30"],
|
|
124
|
-
["date", "18:20"],
|
|
125
|
-
["date", null],
|
|
126
|
-
["date", undefined],
|
|
127
|
-
["date", 20251215],
|
|
128
|
-
["time", "24:00"],
|
|
129
|
-
["time", "2025-12-15"],
|
|
130
|
-
["time", {}],
|
|
131
|
-
["time", []],
|
|
132
|
-
["time", false],
|
|
133
|
-
])("rejects invalid %s half %j", (role, submitted) => {
|
|
134
|
-
const id = `aui:datetime:${role}:when${role === "time" ? ":" : ""}`;
|
|
135
|
-
expect(
|
|
136
|
-
decodeSubmitData({ aui: { type: "save" }, [id]: submitted }),
|
|
137
|
-
).toBeUndefined();
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it.each([false, true])(
|
|
141
|
-
"rejects conflicting metadata regardless of submission order (%s)",
|
|
142
|
-
(reverse) => {
|
|
143
|
-
const entries = [
|
|
144
|
-
["aui:datetime:time:when:", "17:00"],
|
|
145
|
-
["aui:datetime:time:when:2025-12-15T17%3A00%3A00Z", "18:00"],
|
|
146
|
-
];
|
|
147
|
-
expect(
|
|
148
|
-
decodeSubmitData({
|
|
149
|
-
aui: { type: "save" },
|
|
150
|
-
"aui:datetime:date:when": "2025-12-15",
|
|
151
|
-
...Object.fromEntries(reverse ? entries.reverse() : entries),
|
|
152
|
-
}),
|
|
153
|
-
).toBeUndefined();
|
|
154
|
-
},
|
|
155
|
-
);
|
|
156
|
-
|
|
157
|
-
it("never reads temporal metadata from the action envelope or its prototype", () => {
|
|
158
|
-
const temporal = {
|
|
159
|
-
get() {
|
|
160
|
-
throw new Error("Unexpected temporal read");
|
|
161
|
-
},
|
|
162
|
-
};
|
|
163
|
-
for (const aui of [
|
|
164
|
-
Object.defineProperty({ type: "save" }, "temporal", temporal),
|
|
165
|
-
Object.assign(
|
|
166
|
-
Object.create(Object.defineProperty({}, "temporal", temporal)),
|
|
167
|
-
{ type: "save" },
|
|
168
|
-
),
|
|
169
|
-
]) {
|
|
170
|
-
expect(
|
|
171
|
-
decodeSubmitData({
|
|
172
|
-
aui,
|
|
173
|
-
"aui:datetime:date:when": "2025-12-15",
|
|
174
|
-
"aui:datetime:time:when:": "17:00",
|
|
175
|
-
}),
|
|
176
|
-
).toEqual({
|
|
177
|
-
type: "save",
|
|
178
|
-
$input: { when: "2025-12-15T17:00" },
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it("rejects a legacy time id with an inherited date half", () => {
|
|
184
|
-
const value = Object.assign(Object.create({ when: "2025-12-15" }), {
|
|
185
|
-
aui: { type: "save" },
|
|
186
|
-
"aui:datetime:when:": "17:00",
|
|
187
|
-
});
|
|
188
|
-
expect(decodeSubmitData(value)).toBeUndefined();
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
it.each([
|
|
192
|
-
[
|
|
193
|
-
"aui:datetime:date:when",
|
|
194
|
-
"2025-12-15",
|
|
195
|
-
"aui:datetime:time:when:",
|
|
196
|
-
"17:00",
|
|
197
|
-
],
|
|
198
|
-
[
|
|
199
|
-
"aui:datetime:time:when:",
|
|
200
|
-
"17:00",
|
|
201
|
-
"aui:datetime:date:when",
|
|
202
|
-
"2025-12-15",
|
|
203
|
-
],
|
|
204
|
-
])(
|
|
205
|
-
"ignores an inherited half %s",
|
|
206
|
-
(inheritedId, inheritedValue, id, submitted) => {
|
|
207
|
-
const value = Object.assign(
|
|
208
|
-
Object.create({ [inheritedId]: inheritedValue }),
|
|
209
|
-
{
|
|
210
|
-
aui: { type: "save" },
|
|
211
|
-
[id]: submitted,
|
|
212
|
-
},
|
|
213
|
-
);
|
|
214
|
-
expect(decodeSubmitData(value)).toEqual({
|
|
215
|
-
type: "save",
|
|
216
|
-
$input: { when: "" },
|
|
217
|
-
});
|
|
218
|
-
},
|
|
219
|
-
);
|
|
220
|
-
|
|
221
|
-
it("decodes the aui envelope, spreading its payload into the result", () => {
|
|
222
|
-
const value = { aui: { type: "approve", payload: { requestId: "r1" } } };
|
|
223
|
-
expect(decodeSubmitData(value)).toEqual({
|
|
224
|
-
type: "approve",
|
|
225
|
-
requestId: "r1",
|
|
226
|
-
});
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
it("collects every other top-level key into $input", () => {
|
|
230
|
-
const value = {
|
|
231
|
-
aui: { type: "approve", payload: { requestId: "r1" } },
|
|
232
|
-
email: "a@b.com",
|
|
233
|
-
subscribe: "true",
|
|
234
|
-
};
|
|
235
|
-
expect(decodeSubmitData(value)).toEqual({
|
|
236
|
-
type: "approve",
|
|
237
|
-
requestId: "r1",
|
|
238
|
-
$input: { email: "a@b.com", subscribe: "true" },
|
|
239
|
-
});
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
it("keeps a number input as a number", () => {
|
|
243
|
-
const value = {
|
|
244
|
-
aui: { type: "set_quantity" },
|
|
245
|
-
quantity: 3.5,
|
|
246
|
-
};
|
|
247
|
-
expect(decodeSubmitData(value)).toEqual({
|
|
248
|
-
type: "set_quantity",
|
|
249
|
-
$input: { quantity: 3.5 },
|
|
250
|
-
});
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
it("resolves $field references in the payload from same-card inputs, else their fallback", () => {
|
|
254
|
-
const value = {
|
|
255
|
-
aui: {
|
|
256
|
-
type: "save",
|
|
257
|
-
payload: {
|
|
258
|
-
note: { $field: "note" },
|
|
259
|
-
form: { plan: [{ $field: "plan" }], missing: { $field: "gone" } },
|
|
260
|
-
renamed: { $field: "aui", fallback: "kept" },
|
|
261
|
-
},
|
|
262
|
-
},
|
|
263
|
-
note: "Ship it",
|
|
264
|
-
plan: "pro",
|
|
265
|
-
};
|
|
266
|
-
expect(decodeSubmitData(value)).toEqual({
|
|
267
|
-
type: "save",
|
|
268
|
-
note: "Ship it",
|
|
269
|
-
form: { plan: ["pro"] },
|
|
270
|
-
renamed: "kept",
|
|
271
|
-
$input: { note: "Ship it", plan: "pro" },
|
|
272
|
-
});
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
it("omits $input when there are no other top-level keys", () => {
|
|
276
|
-
expect(decodeSubmitData({ aui: { type: "approve" } })).toEqual({
|
|
277
|
-
type: "approve",
|
|
278
|
-
});
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
it("omits payload when the envelope carries none", () => {
|
|
282
|
-
const value = { aui: { type: "approve" }, note: "hi" };
|
|
283
|
-
expect(decodeSubmitData(value)).toEqual({
|
|
284
|
-
type: "approve",
|
|
285
|
-
$input: { note: "hi" },
|
|
286
|
-
});
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
it("drops a $input key inside the envelope payload, keeping the slot for collected inputs", () => {
|
|
290
|
-
const value = {
|
|
291
|
-
aui: { type: "approve", payload: { requestId: "r1", $input: "spoofed" } },
|
|
292
|
-
email: "a@b.com",
|
|
293
|
-
};
|
|
294
|
-
expect(decodeSubmitData(value)).toEqual({
|
|
295
|
-
type: "approve",
|
|
296
|
-
requestId: "r1",
|
|
297
|
-
$input: { email: "a@b.com" },
|
|
298
|
-
});
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
it("drops a payload $input even when no input values were collected", () => {
|
|
302
|
-
const value = {
|
|
303
|
-
aui: { type: "approve", payload: { $input: "spoofed" } },
|
|
304
|
-
};
|
|
305
|
-
const decoded = decodeSubmitData(value);
|
|
306
|
-
expect(decoded).toEqual({ type: "approve" });
|
|
307
|
-
expect(decoded !== undefined && "$input" in decoded).toBe(false);
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
it("keeps the envelope type over a payload key of the same name", () => {
|
|
311
|
-
const value = {
|
|
312
|
-
aui: { type: "approve", payload: { type: "spoofed" } },
|
|
313
|
-
};
|
|
314
|
-
expect(decodeSubmitData(value)).toEqual({ type: "approve" });
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
it("returns undefined when aui is missing", () => {
|
|
318
|
-
expect(decodeSubmitData({ email: "a@b.com" })).toBeUndefined();
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
it("returns undefined for malformed or null input, never throwing", () => {
|
|
322
|
-
const malformed: unknown[] = [
|
|
323
|
-
null,
|
|
324
|
-
undefined,
|
|
325
|
-
42,
|
|
326
|
-
"just a string",
|
|
327
|
-
true,
|
|
328
|
-
[],
|
|
329
|
-
{},
|
|
330
|
-
{ aui: "nope" },
|
|
331
|
-
{ aui: {} },
|
|
332
|
-
{ aui: { type: 123 } },
|
|
333
|
-
];
|
|
334
|
-
for (const input of malformed) {
|
|
335
|
-
expect(() => decodeSubmitData(input)).not.toThrow();
|
|
336
|
-
expect(decodeSubmitData(input)).toBeUndefined();
|
|
337
|
-
}
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
it("never throws even when reading the input itself throws", () => {
|
|
341
|
-
const hostile = new Proxy(
|
|
342
|
-
{},
|
|
343
|
-
{
|
|
344
|
-
get() {
|
|
345
|
-
throw new Error("boom");
|
|
346
|
-
},
|
|
347
|
-
},
|
|
348
|
-
);
|
|
349
|
-
expect(() => decodeSubmitData(hostile)).not.toThrow();
|
|
350
|
-
expect(decodeSubmitData(hostile)).toBeUndefined();
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
it("returns undefined for a non-empty array activity.value", () => {
|
|
354
|
-
expect(decodeSubmitData([1, 2, 3])).toBeUndefined();
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
it("returns undefined when aui is inherited rather than owned", () => {
|
|
358
|
-
const value = Object.create({ aui: { type: "x" } });
|
|
359
|
-
expect(decodeSubmitData(value)).toBeUndefined();
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
it("returns undefined when the envelope's type is inherited rather than owned", () => {
|
|
363
|
-
const value = { aui: Object.create({ type: "x" }) };
|
|
364
|
-
expect(decodeSubmitData(value)).toBeUndefined();
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
it("keeps __proto__ and constructor keys from the payload as own data properties without polluting Object.prototype", () => {
|
|
368
|
-
const payload = JSON.parse(
|
|
369
|
-
'{"__proto__": {"evil": true}, "constructor": {"evil": true}}',
|
|
370
|
-
);
|
|
371
|
-
const value = { aui: { type: "approve", payload } };
|
|
372
|
-
const result = decodeSubmitData(value) as Record<string, unknown>;
|
|
373
|
-
expect(Object.prototype.hasOwnProperty.call(result, "__proto__")).toBe(
|
|
374
|
-
true,
|
|
375
|
-
);
|
|
376
|
-
expect(result["__proto__"]).toEqual({ evil: true });
|
|
377
|
-
expect(result["constructor"]).toEqual({ evil: true });
|
|
378
|
-
expect(({} as Record<string, unknown>)["evil"]).toBeUndefined();
|
|
379
|
-
expect(Object.getPrototypeOf({})).toBe(Object.prototype);
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
it("keeps __proto__ and constructor keys from $input as own data properties without polluting Object.prototype", () => {
|
|
383
|
-
const value = JSON.parse(
|
|
384
|
-
'{"aui": {"type": "approve"}, "__proto__": "hostile", "constructor": "hostile"}',
|
|
385
|
-
);
|
|
386
|
-
const result = decodeSubmitData(value);
|
|
387
|
-
const input = result?.["$input"] as Record<string, unknown>;
|
|
388
|
-
expect(Object.prototype.hasOwnProperty.call(input, "__proto__")).toBe(true);
|
|
389
|
-
expect(Object.prototype.hasOwnProperty.call(input, "constructor")).toBe(
|
|
390
|
-
true,
|
|
391
|
-
);
|
|
392
|
-
expect(input["__proto__"]).toBe("hostile");
|
|
393
|
-
expect(input["constructor"]).toBe("hostile");
|
|
394
|
-
expect(typeof ({} as object).constructor).toBe("function");
|
|
395
|
-
expect(Object.getPrototypeOf({})).toBe(Object.prototype);
|
|
396
|
-
});
|
|
397
|
-
});
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import { resolveFieldReferences } from "../fieldReferences";
|
|
2
|
-
import type { Action } from "../ir";
|
|
3
|
-
import {
|
|
4
|
-
classifyTemporal,
|
|
5
|
-
fromOffsetDateTime,
|
|
6
|
-
mergeTemporalMinutes,
|
|
7
|
-
} from "../temporal";
|
|
8
|
-
import { decodeTemporalInputId, TEMPORAL_INPUT_PREFIX } from "./temporalId";
|
|
9
|
-
|
|
10
|
-
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
11
|
-
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Decodes the merged `activity.value` object a Teams bot receives on
|
|
15
|
-
* Action.Submit, inverting the `aui` envelope {@link buildSubmitAction} nests
|
|
16
|
-
* the resume action under. Adaptive Cards fold every other same-card input's
|
|
17
|
-
* value into the same submit object keyed by its `id`, so every top-level key
|
|
18
|
-
* besides `aui` is collected into `$input` (omitted when there are none).
|
|
19
|
-
* Each `{ "$field": name }` reference inside the payload resolves to the
|
|
20
|
-
* same-card input value with that id, as the string Adaptive Cards submitted,
|
|
21
|
-
* and a reference with no such input resolves to its `fallback`, or is dropped
|
|
22
|
-
* without one.
|
|
23
|
-
* `aui` is reserved for the envelope: `toAdaptiveCard` renames any input
|
|
24
|
-
* whose id would collide to an unused id derived from it before encoding, so
|
|
25
|
-
* a same-card input value can never land on this key. `$input` and `type`
|
|
26
|
-
* are reserved on the way out: a `$input` key inside the envelope payload is
|
|
27
|
-
* dropped so the slot only ever reflects same-card input values, and the
|
|
28
|
-
* envelope's own `type` wins over a payload key of the same name. Every
|
|
29
|
-
* collected key (in `payload` or `$input`) is kept as an own data property of
|
|
30
|
-
* the returned object, even a `__proto__` or `constructor` key, since the
|
|
31
|
-
* object is always built by spreading and `Object.fromEntries` rather than by
|
|
32
|
-
* keyed assignment. Envelope keys are read as own properties only, so
|
|
33
|
-
* prototype-inherited `aui`, `type`, or `payload` values never dispatch.
|
|
34
|
-
* Returns `undefined` for a missing or malformed `aui` envelope; never throws.
|
|
35
|
-
*/
|
|
36
|
-
export function decodeSubmitData(value: unknown): Action | undefined {
|
|
37
|
-
try {
|
|
38
|
-
if (!isRecord(value) || !Object.hasOwn(value, "aui")) return undefined;
|
|
39
|
-
const aui = value["aui"];
|
|
40
|
-
if (
|
|
41
|
-
!isRecord(aui) ||
|
|
42
|
-
!Object.hasOwn(aui, "type") ||
|
|
43
|
-
typeof aui["type"] !== "string"
|
|
44
|
-
) {
|
|
45
|
-
return undefined;
|
|
46
|
-
}
|
|
47
|
-
const type = aui["type"];
|
|
48
|
-
|
|
49
|
-
const payload =
|
|
50
|
-
Object.hasOwn(aui, "payload") && isRecord(aui["payload"])
|
|
51
|
-
? aui["payload"]
|
|
52
|
-
: {};
|
|
53
|
-
const payloadEntries = Object.entries(payload).filter(
|
|
54
|
-
([key]) => key !== "$input",
|
|
55
|
-
);
|
|
56
|
-
const inputEntries = Object.entries(value).filter(([key]) => key !== "aui");
|
|
57
|
-
const input = Object.fromEntries(inputEntries);
|
|
58
|
-
const temporalFields = new Map<
|
|
59
|
-
string,
|
|
60
|
-
{ date?: string; time?: string; previousValue?: string }
|
|
61
|
-
>();
|
|
62
|
-
for (const [id, submitted] of inputEntries) {
|
|
63
|
-
if (!id.startsWith(TEMPORAL_INPUT_PREFIX)) continue;
|
|
64
|
-
const metadata = decodeTemporalInputId(id);
|
|
65
|
-
if (
|
|
66
|
-
!metadata ||
|
|
67
|
-
Object.hasOwn(input, metadata.fieldId) ||
|
|
68
|
-
typeof submitted !== "string" ||
|
|
69
|
-
(submitted !== "" && classifyTemporal(submitted).kind !== metadata.role)
|
|
70
|
-
)
|
|
71
|
-
return undefined;
|
|
72
|
-
const field = temporalFields.get(metadata.fieldId) ?? {};
|
|
73
|
-
if (field[metadata.role] !== undefined) return undefined;
|
|
74
|
-
field[metadata.role] = submitted;
|
|
75
|
-
if (metadata.role === "time" && metadata.previousValue !== undefined)
|
|
76
|
-
field.previousValue = metadata.previousValue;
|
|
77
|
-
temporalFields.set(metadata.fieldId, field);
|
|
78
|
-
delete input[id];
|
|
79
|
-
}
|
|
80
|
-
for (const [fieldId, field] of temporalFields) {
|
|
81
|
-
Object.defineProperty(input, fieldId, {
|
|
82
|
-
value: fromOffsetDateTime(
|
|
83
|
-
mergeTemporalMinutes(field.date ?? "", field.time ?? ""),
|
|
84
|
-
field.previousValue,
|
|
85
|
-
),
|
|
86
|
-
enumerable: true,
|
|
87
|
-
configurable: true,
|
|
88
|
-
writable: true,
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
const hasInput = inputEntries.length > 0;
|
|
92
|
-
|
|
93
|
-
return {
|
|
94
|
-
...(resolveFieldReferences(
|
|
95
|
-
Object.fromEntries(payloadEntries),
|
|
96
|
-
Object.fromEntries(
|
|
97
|
-
Object.entries(input).flatMap(([id, submitted]) =>
|
|
98
|
-
/^_+aui:datetime:/.test(id)
|
|
99
|
-
? [[id.slice(1), submitted]]
|
|
100
|
-
: [[id, submitted]],
|
|
101
|
-
),
|
|
102
|
-
),
|
|
103
|
-
) as Record<string, unknown>),
|
|
104
|
-
type,
|
|
105
|
-
...(hasInput ? { $input: input } : {}),
|
|
106
|
-
};
|
|
107
|
-
} catch {
|
|
108
|
-
return undefined;
|
|
109
|
-
}
|
|
110
|
-
}
|