@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.
Files changed (74) hide show
  1. package/dist/a2ui/convert.d.ts.map +1 -1
  2. package/dist/a2ui/convert.js +21 -4
  3. package/dist/a2ui/convert.js.map +1 -1
  4. package/dist/constants.d.ts +1 -0
  5. package/dist/constants.d.ts.map +1 -1
  6. package/dist/constants.js +2 -1
  7. package/dist/constants.js.map +1 -1
  8. package/dist/slack/decodeBlockAction.d.ts.map +1 -1
  9. package/dist/slack/decodeBlockAction.js +64 -26
  10. package/dist/slack/decodeBlockAction.js.map +1 -1
  11. package/dist/slack/fromSlackBlocks.d.ts.map +1 -1
  12. package/dist/slack/fromSlackBlocks.js +23 -0
  13. package/dist/slack/fromSlackBlocks.js.map +1 -1
  14. package/dist/slack/toSlackBlocks.d.ts.map +1 -1
  15. package/dist/slack/toSlackBlocks.js +74 -18
  16. package/dist/slack/toSlackBlocks.js.map +1 -1
  17. package/dist/slack/types.d.ts +20 -1
  18. package/dist/slack/types.d.ts.map +1 -1
  19. package/dist/slack.d.ts +1 -1
  20. package/dist/slack.d.ts.map +1 -1
  21. package/dist/teams/decodeSubmitData.d.ts.map +1 -1
  22. package/dist/teams/decodeSubmitData.js +24 -3
  23. package/dist/teams/decodeSubmitData.js.map +1 -1
  24. package/dist/teams/temporalId.d.ts +5 -0
  25. package/dist/teams/temporalId.d.ts.map +1 -0
  26. package/dist/teams/temporalId.js +28 -0
  27. package/dist/teams/temporalId.js.map +1 -0
  28. package/dist/teams/toAdaptiveCard.d.ts.map +1 -1
  29. package/dist/teams/toAdaptiveCard.js +80 -1
  30. package/dist/teams/toAdaptiveCard.js.map +1 -1
  31. package/dist/teams/toTeamsAttachments.d.ts.map +1 -1
  32. package/dist/teams/toTeamsAttachments.js.map +1 -1
  33. package/dist/teams/types.d.ts +19 -1
  34. package/dist/teams/types.d.ts.map +1 -1
  35. package/dist/teams.d.ts +1 -1
  36. package/dist/teams.d.ts.map +1 -1
  37. package/dist/temporal.d.ts +33 -0
  38. package/dist/temporal.d.ts.map +1 -0
  39. package/dist/temporal.js +152 -0
  40. package/dist/temporal.js.map +1 -0
  41. package/dist/vocabulary/collectFormValues.d.ts +1 -1
  42. package/dist/vocabulary/collectFormValues.d.ts.map +1 -1
  43. package/dist/vocabulary/collectFormValues.js +7 -3
  44. package/dist/vocabulary/collectFormValues.js.map +1 -1
  45. package/dist/vocabulary/data.d.ts +1 -1
  46. package/dist/vocabulary/interactive.d.ts +7 -1
  47. package/dist/vocabulary/interactive.d.ts.map +1 -1
  48. package/dist/vocabulary/interactive.js +62 -12
  49. package/dist/vocabulary/interactive.js.map +1 -1
  50. package/package.json +5 -5
  51. package/src/a2ui/convert.test.ts +223 -14
  52. package/src/a2ui/convert.ts +41 -7
  53. package/src/constants.ts +1 -0
  54. package/src/slack/datePicker.test.ts +612 -0
  55. package/src/slack/decodeBlockAction.ts +114 -41
  56. package/src/slack/fromSlackBlocks.ts +29 -0
  57. package/src/slack/toSlackBlocks.ts +123 -36
  58. package/src/slack/types.ts +24 -0
  59. package/src/slack.ts +2 -0
  60. package/src/teams/decodeSubmitData.test.ts +216 -0
  61. package/src/teams/decodeSubmitData.ts +49 -4
  62. package/src/teams/temporal.test.ts +766 -0
  63. package/src/teams/temporalId.ts +56 -0
  64. package/src/teams/toAdaptiveCard.ts +166 -2
  65. package/src/teams/toTeamsAttachments.ts +4 -1
  66. package/src/teams/types.ts +17 -0
  67. package/src/teams.ts +1 -0
  68. package/src/temporal.test.ts +319 -0
  69. package/src/temporal.ts +259 -0
  70. package/src/vocabulary/collectFormValues.test.ts +198 -1
  71. package/src/vocabulary/collectFormValues.ts +31 -2
  72. package/src/vocabulary/datepicker.dom.test.tsx +1148 -0
  73. package/src/vocabulary/interactive.dom.test.tsx +59 -11
  74. 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
- inputEntries.length > 0 ? Object.fromEntries(inputEntries) : undefined;
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
- input ?? {},
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
- ...(input !== undefined ? { $input: input } : {}),
105
+ ...(hasInput ? { $input: input } : {}),
61
106
  };
62
107
  } catch {
63
108
  return undefined;