@assistant-ui/react-generative-ui 0.0.22 → 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/dist/a2ui/convert.d.ts.map +1 -1
- package/dist/a2ui/convert.js +21 -4
- package/dist/a2ui/convert.js.map +1 -1
- package/dist/constants.d.ts +1 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +2 -1
- package/dist/constants.js.map +1 -1
- package/dist/slack/decodeBlockAction.d.ts.map +1 -1
- package/dist/slack/decodeBlockAction.js +64 -26
- package/dist/slack/decodeBlockAction.js.map +1 -1
- package/dist/slack/fromSlackBlocks.d.ts.map +1 -1
- package/dist/slack/fromSlackBlocks.js +23 -0
- package/dist/slack/fromSlackBlocks.js.map +1 -1
- package/dist/slack/toSlackBlocks.d.ts.map +1 -1
- package/dist/slack/toSlackBlocks.js +74 -18
- package/dist/slack/toSlackBlocks.js.map +1 -1
- package/dist/slack/types.d.ts +20 -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/decodeSubmitData.d.ts.map +1 -1
- package/dist/teams/decodeSubmitData.js +24 -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.map +1 -1
- package/dist/teams/toAdaptiveCard.js +80 -1
- 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 +19 -1
- 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/collectFormValues.d.ts +1 -1
- package/dist/vocabulary/collectFormValues.d.ts.map +1 -1
- package/dist/vocabulary/collectFormValues.js +7 -3
- package/dist/vocabulary/collectFormValues.js.map +1 -1
- package/dist/vocabulary/data.d.ts +1 -1
- package/dist/vocabulary/interactive.d.ts +7 -1
- package/dist/vocabulary/interactive.d.ts.map +1 -1
- package/dist/vocabulary/interactive.js +62 -12
- package/dist/vocabulary/interactive.js.map +1 -1
- package/package.json +5 -5
- package/src/a2ui/convert.test.ts +223 -14
- package/src/a2ui/convert.ts +41 -7
- package/src/constants.ts +1 -0
- package/src/slack/datePicker.test.ts +612 -0
- package/src/slack/decodeBlockAction.ts +114 -41
- package/src/slack/fromSlackBlocks.ts +29 -0
- package/src/slack/toSlackBlocks.ts +123 -36
- package/src/slack/types.ts +24 -0
- package/src/slack.ts +2 -0
- package/src/teams/decodeSubmitData.test.ts +216 -0
- package/src/teams/decodeSubmitData.ts +49 -4
- package/src/teams/temporal.test.ts +766 -0
- package/src/teams/temporalId.ts +56 -0
- package/src/teams/toAdaptiveCard.ts +166 -2
- package/src/teams/toTeamsAttachments.ts +4 -1
- package/src/teams/types.ts +17 -0
- package/src/teams.ts +1 -0
- package/src/temporal.test.ts +319 -0
- package/src/temporal.ts +259 -0
- package/src/vocabulary/collectFormValues.test.ts +198 -1
- package/src/vocabulary/collectFormValues.ts +31 -2
- package/src/vocabulary/datepicker.dom.test.tsx +1148 -0
- package/src/vocabulary/interactive.dom.test.tsx +59 -11
- package/src/vocabulary/interactive.tsx +151 -15
|
@@ -1,7 +1,223 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import { decodeSubmitData } from "./decodeSubmitData";
|
|
3
|
+
import { toAdaptiveCard } from "./toAdaptiveCard";
|
|
3
4
|
|
|
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
|
+
|
|
5
221
|
it("decodes the aui envelope, spreading its payload into the result", () => {
|
|
6
222
|
const value = { aui: { type: "approve", payload: { requestId: "r1" } } };
|
|
7
223
|
expect(decodeSubmitData(value)).toEqual({
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { resolveFieldReferences } from "../fieldReferences";
|
|
2
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";
|
|
3
9
|
|
|
4
10
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
5
11
|
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -48,16 +54,55 @@ export function decodeSubmitData(value: unknown): Action | undefined {
|
|
|
48
54
|
([key]) => key !== "$input",
|
|
49
55
|
);
|
|
50
56
|
const inputEntries = Object.entries(value).filter(([key]) => key !== "aui");
|
|
51
|
-
const input =
|
|
52
|
-
|
|
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;
|
|
53
92
|
|
|
54
93
|
return {
|
|
55
94
|
...(resolveFieldReferences(
|
|
56
95
|
Object.fromEntries(payloadEntries),
|
|
57
|
-
|
|
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
|
+
),
|
|
58
103
|
) as Record<string, unknown>),
|
|
59
104
|
type,
|
|
60
|
-
...(
|
|
105
|
+
...(hasInput ? { $input: input } : {}),
|
|
61
106
|
};
|
|
62
107
|
} catch {
|
|
63
108
|
return undefined;
|