@truedat/dd 8.12.1 → 8.12.4

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 (82) hide show
  1. package/package.json +3 -3
  2. package/src/api/__tests__/queries.spec.js +55 -0
  3. package/src/api/queries.js +31 -0
  4. package/src/components/Grant.js +106 -16
  5. package/src/components/GrantRequest.js +2 -2
  6. package/src/components/GrantRequestBulkApprovalPopup.js +0 -1
  7. package/src/components/GrantRequestTemplateForm.js +76 -0
  8. package/src/components/GrantsDownloadButton.js +9 -1
  9. package/src/components/GrantsTable.js +4 -1
  10. package/src/components/StructureFields.js +7 -4
  11. package/src/components/StructureGrantCartCheckout.js +122 -59
  12. package/src/components/StructureGrantCartInformation.js +38 -9
  13. package/src/components/StructureGrantCartUserSelector.js +29 -4
  14. package/src/components/StructureGrantDropdown.js +7 -1
  15. package/src/components/StructureGrants.js +4 -1
  16. package/src/components/StructuresUpdateOption.js +2 -2
  17. package/src/components/SystemStructures.js +3 -3
  18. package/src/components/__tests__/CatalogViewConfigForm.spec.js +2 -0
  19. package/src/components/__tests__/EditDomainForm.spec.js +3 -1
  20. package/src/components/__tests__/Grant.spec.js +171 -0
  21. package/src/components/__tests__/GrantApprovalRuleForm.spec.js +2 -0
  22. package/src/components/__tests__/GrantApprovalRuleNew.spec.js +8 -0
  23. package/src/components/__tests__/GrantApprovalRules.spec.js +4 -0
  24. package/src/components/__tests__/GrantRequest.spec.js +4 -4
  25. package/src/components/__tests__/GrantRequestBulkActions.spec.js +1 -4
  26. package/src/components/__tests__/GrantRequestsSearchResults.spec.js +2 -0
  27. package/src/components/__tests__/GrantsDownloadButton.spec.js +58 -2
  28. package/src/components/__tests__/GrantsTable.spec.js +22 -2
  29. package/src/components/__tests__/LineageDefs.spec.js +6 -1
  30. package/src/components/__tests__/LineageForm.spec.js +1 -0
  31. package/src/components/__tests__/LineageGraphToolbar.spec.js +19 -17
  32. package/src/components/__tests__/LineageInspectorPanel.spec.js +17 -9
  33. package/src/components/__tests__/ProfileGroup.spec.js +7 -1
  34. package/src/components/__tests__/ReferenceDatasetForm.spec.js +2 -0
  35. package/src/components/__tests__/ReferenceDatasetRoutes.spec.js +9 -0
  36. package/src/components/__tests__/StructureGrantCartCheckout.grouping.spec.js +95 -0
  37. package/src/components/__tests__/StructureGrantCartCheckout.selection.spec.js +416 -0
  38. package/src/components/__tests__/StructureGrantCartCheckout.spec.js +216 -4
  39. package/src/components/__tests__/StructureGrantCartInformation.spec.js +5 -2
  40. package/src/components/__tests__/StructureGrantCartUserSelector.spec.js +128 -0
  41. package/src/components/__tests__/StructureGrantDropdown.spec.js +92 -0
  42. package/src/components/__tests__/StructureGrantListButton.spec.js +70 -0
  43. package/src/components/__tests__/StructureGrantSummaryButton.spec.js +36 -1
  44. package/src/components/__tests__/StructureGrants.spec.js +12 -0
  45. package/src/components/__tests__/StructureRelationRow.spec.js +72 -4
  46. package/src/components/__tests__/StructureRelations.spec.js +18 -0
  47. package/src/components/__tests__/StructureRoles.spec.js +9 -0
  48. package/src/components/__tests__/StructureRow.spec.js +13 -4
  49. package/src/components/__tests__/StructureSelector.spec.js +2 -0
  50. package/src/components/__tests__/StructureTag.spec.js +2 -0
  51. package/src/components/__tests__/StructureTagForm.spec.js +2 -0
  52. package/src/components/__tests__/StructureType.spec.js +3 -1
  53. package/src/components/__tests__/StructureTypeForm.spec.js +3 -1
  54. package/src/components/__tests__/StructureView.spec.js +17 -1
  55. package/src/components/__tests__/StructuresGrantRequestView.spec.js +2 -0
  56. package/src/components/__tests__/StructuresView.spec.js +2 -1
  57. package/src/components/__tests__/Tag.spec.js +4 -1
  58. package/src/components/__tests__/Tags.spec.js +7 -1
  59. package/src/components/__tests__/__snapshots__/GrantsTable.spec.js.snap +34 -2
  60. package/src/components/__tests__/__snapshots__/LineageDefs.spec.js.snap +32 -30
  61. package/src/components/__tests__/__snapshots__/StructureFields.spec.js.snap +1 -16
  62. package/src/components/__tests__/__snapshots__/StructureGrantCartCheckout.spec.js.snap +217 -1
  63. package/src/components/__tests__/__snapshots__/StructureGrantCartInformation.spec.js.snap +54 -0
  64. package/src/components/__tests__/__snapshots__/StructureRelationRow.spec.js.snap +23 -19
  65. package/src/components/__tests__/__snapshots__/StructureRow.spec.js.snap +28 -24
  66. package/src/components/__tests__/__snapshots__/StructuresUpdateOption.spec.js.snap +7 -3
  67. package/src/messages/en.js +2 -2
  68. package/src/messages/es.js +2 -2
  69. package/src/reducers/__tests__/grantRequestsCart.spec.js +86 -0
  70. package/src/reducers/__tests__/structureRelations.spec.js +28 -0
  71. package/src/reducers/grantRequestsCart.js +20 -2
  72. package/src/selectors/__tests__/__snapshots__/getGrantsColumns.spec.js.snap +1 -9
  73. package/src/selectors/__tests__/getGrantRequestsSearchColumns.spec.js +11 -4
  74. package/src/selectors/__tests__/getGrantsColumns.spec.js +295 -0
  75. package/src/selectors/getBusinessConceptsLinksToStructuresColumnsSelector.js +3 -5
  76. package/src/selectors/getGrantRequestsSearchColumns.js +3 -5
  77. package/src/selectors/getGrantsColumns.js +85 -11
  78. package/src/selectors/getLinkedImplementationsToStructuresColumns.js +3 -7
  79. package/src/selectors/getStructureFieldColumns.js +7 -6
  80. package/src/selectors/getStructureProperties.js +2 -5
  81. package/src/selectors/getStructuresUploadEventColumns.js +2 -7
  82. package/src/selectors/structureColumnsSelector.js +13 -16
@@ -0,0 +1,95 @@
1
+ import { getGroupedStructuresByDomainAndTemplate as groupStructures } from "../StructureGrantCartCheckout";
2
+
3
+ const structures = [
4
+ { id: 1, system_id: 10, domain_ids: [1] },
5
+ { id: 2, system_id: 20, domain_ids: [1] },
6
+ { id: 3, system_id: 20, domain_ids: [2] },
7
+ ];
8
+
9
+ // These cases capture the grouping policy on main before TD-8477.
10
+ it.each([
11
+ ["no policy", [], null, false, true, { nodomain_others_notype: [1, 2, 3] }],
12
+ [
13
+ "default template only",
14
+ [],
15
+ 7,
16
+ false,
17
+ true,
18
+ { nodomain_others_notype: [1, 2, 3] },
19
+ ],
20
+ [
21
+ "default domain policy",
22
+ [],
23
+ 7,
24
+ true,
25
+ true,
26
+ { "1_others_notype": [1, 2], "2_others_notype": [3] },
27
+ ],
28
+ [
29
+ "explicit system policy",
30
+ [{ resource_id: 10, template_id: 5 }],
31
+ null,
32
+ false,
33
+ true,
34
+ { nodomain_10_5: [1], nodomain_others_notype: [2, 3] },
35
+ ],
36
+ [
37
+ "unmatched system policy",
38
+ [{ resource_id: 30, template_id: 5 }],
39
+ null,
40
+ false,
41
+ true,
42
+ { nodomain_others_notype: [1, 2, 3] },
43
+ ],
44
+ [
45
+ "explicit system domain policy",
46
+ [{ resource_id: 20, template_id: 5, group_by_domain: true }],
47
+ null,
48
+ false,
49
+ true,
50
+ { nodomain_others_notype: [1], "1_20_5": [2], "2_20_5": [3] },
51
+ ],
52
+ [
53
+ "explicit false overrides default domain policy",
54
+ [{ resource_id: 20, template_id: 5, group_by_domain: false }],
55
+ 7,
56
+ true,
57
+ true,
58
+ { "1_others_notype": [1], nodomain_20_5: [2, 3] },
59
+ ],
60
+ [
61
+ "explicit system falls back to default template",
62
+ [{ resource_id: 20, template_id: null }],
63
+ 7,
64
+ false,
65
+ true,
66
+ { nodomain_others_notype: [1], nodomain_20_7: [2, 3] },
67
+ ],
68
+ [
69
+ "disabled grouping",
70
+ [{ resource_id: 20, template_id: 5, group_by_domain: true }],
71
+ 7,
72
+ true,
73
+ false,
74
+ { all: [1, 2, 3] },
75
+ ],
76
+ ])(
77
+ "preserves the original grouping for %s",
78
+ (_label, relations, defaultId, byDomain, enabled, expected) => {
79
+ const { grouped } = groupStructures(
80
+ structures,
81
+ relations,
82
+ defaultId,
83
+ byDomain,
84
+ enabled,
85
+ );
86
+ expect(
87
+ Object.fromEntries(
88
+ Object.entries(grouped).map(([key, entries]) => [
89
+ key,
90
+ entries.map(({ id }) => id),
91
+ ]),
92
+ ),
93
+ ).toEqual(expected);
94
+ },
95
+ );
@@ -0,0 +1,416 @@
1
+ import { waitFor, within } from "@testing-library/react";
2
+ import PropTypes from "prop-types";
3
+ import userEvent from "@testing-library/user-event";
4
+ import { useSelector, useDispatch } from "react-redux";
5
+ import { render } from "@truedat/test/render";
6
+ import { TEMPLATES_QUERY, DOMAINS_QUERY } from "@truedat/core/api/queries";
7
+ import { useTemplateRelations } from "@truedat/df/hooks/useTemplateRelations";
8
+ import {
9
+ addGrantRequestToCart,
10
+ removeGrantRequestFromCart,
11
+ } from "../../routines";
12
+ import {
13
+ grantRequestsCart,
14
+ initialState,
15
+ } from "../../reducers/grantRequestsCart";
16
+ import StructureGrantCartCheckout from "../StructureGrantCartCheckout";
17
+ import { useTemplatesByIds } from "../../hooks/useTemplatesByIds";
18
+ import { useCheckoutGrantRequest } from "../../hooks/useCheckoutGrantRequest";
19
+
20
+ jest.mock("@truedat/core/webContext", () => ({
21
+ useWebContext: () => ({ scopesWithRelations: ["gr"] }),
22
+ }));
23
+ jest.mock("@truedat/df/hooks/useTemplateRelations", () => ({
24
+ useTemplateRelations: jest.fn(),
25
+ }));
26
+ jest.mock("../../hooks/useTemplatesByIds", () => ({
27
+ useTemplatesByIds: jest.fn(),
28
+ }));
29
+ jest.mock("../../hooks/useCheckoutGrantRequest", () => ({
30
+ useCheckoutGrantRequest: jest.fn(),
31
+ }));
32
+ jest.mock("../StructureGrantCart", () => () => <div>Structures</div>);
33
+ jest.mock("../StructureGrantCartUserSelector", () => () => (
34
+ <div data-testid="user-selector" />
35
+ ));
36
+
37
+ const makeTemplate = (id, name) => ({
38
+ id,
39
+ name,
40
+ label: name,
41
+ scope: "gr",
42
+ content: [
43
+ {
44
+ name: "details",
45
+ fields: [
46
+ {
47
+ name: "reason",
48
+ label: "Reason",
49
+ type: "string",
50
+ placeholder: "Reason",
51
+ },
52
+ ],
53
+ },
54
+ ],
55
+ });
56
+ const assigned = makeTemplate("1", "Assigned");
57
+ const fallback = makeTemplate("2", "Default");
58
+ const alternative = makeTemplate("3", "Alternative");
59
+ const templates = [assigned, fallback, alternative];
60
+ const structure = (id, systemId) => ({
61
+ id,
62
+ system_id: systemId,
63
+ system: { id: systemId, name: `System ${systemId}` },
64
+ domain_ids: [1],
65
+ });
66
+ const trigger = jest.fn().mockResolvedValue({});
67
+
68
+ function CheckoutView({ visible = true }) {
69
+ const cart = useSelector((state) => state.grantRequestsCart);
70
+ const dispatch = useDispatch();
71
+ return (
72
+ <>
73
+ <button
74
+ onClick={() => dispatch(addGrantRequestToCart(structure(201, 20)))}
75
+ >
76
+ Add system
77
+ </button>
78
+ <button onClick={() => dispatch(removeGrantRequestFromCart({ id: 201 }))}>
79
+ Remove system
80
+ </button>
81
+ <output data-testid="cart">{JSON.stringify(cart)}</output>
82
+ {visible && <StructureGrantCartCheckout />}
83
+ </>
84
+ );
85
+ }
86
+
87
+ CheckoutView.propTypes = { visible: PropTypes.bool };
88
+
89
+ const openCheckout = (structures, extra = {}) =>
90
+ render(<CheckoutView />, {
91
+ reducer: { grantRequestsCart },
92
+ state: { grantRequestsCart: { ...initialState, structures, ...extra } },
93
+ mocks: [
94
+ {
95
+ request: { query: TEMPLATES_QUERY, variables: { scope: "gr" } },
96
+ result: { data: { templates } },
97
+ maxUsageCount: 20,
98
+ },
99
+ {
100
+ request: {
101
+ query: DOMAINS_QUERY,
102
+ variables: { action: "createForeignGrantRequest" },
103
+ },
104
+ result: {
105
+ data: {
106
+ domains: [
107
+ {
108
+ id: "1",
109
+ name: "Domain",
110
+ externalId: "domain",
111
+ parentId: null,
112
+ actions: [],
113
+ },
114
+ ],
115
+ },
116
+ },
117
+ maxUsageCount: 20,
118
+ },
119
+ ],
120
+ });
121
+
122
+ beforeEach(() => {
123
+ jest.clearAllMocks();
124
+ useTemplateRelations.mockReturnValue({
125
+ resource: [],
126
+ default: { template_id: 2 },
127
+ loading: false,
128
+ });
129
+ useTemplatesByIds.mockReturnValue({ 1: assigned, 2: fallback });
130
+ useCheckoutGrantRequest.mockReturnValue({ trigger, isMutating: false });
131
+ });
132
+
133
+ it("restores a selected template and edited content after leaving and reentering single checkout", async () => {
134
+ const view = openCheckout([structure(101, 10)]);
135
+ const user = userEvent.setup({ delay: null });
136
+ await view.findByPlaceholderText("Reason");
137
+ const userSelector = await view.findByTestId("user-selector");
138
+ expect(userSelector.closest("form")).toContainElement(
139
+ view.getByPlaceholderText("Reason"),
140
+ );
141
+ expect(
142
+ view.container.querySelectorAll(".grant-cart-group-form"),
143
+ ).toHaveLength(0);
144
+ await user.click(await view.findByRole("option", { name: "Alternative" }));
145
+ await user.type(view.getByPlaceholderText("Reason"), "My draft");
146
+ view.rerender(<CheckoutView visible={false} />);
147
+ view.rerender(<CheckoutView />);
148
+ await waitFor(() =>
149
+ expect(view.getByPlaceholderText("Reason")).toHaveValue("My draft"),
150
+ );
151
+ expect(
152
+ JSON.parse(view.getByTestId("cart").textContent).groupTemplates
153
+ .nodomain_others_notype.template,
154
+ ).toBe("Alternative");
155
+ await user.click(view.getByRole("button", { name: "actions.save" }));
156
+ expect(trigger).toHaveBeenCalledWith(
157
+ expect.objectContaining({
158
+ grant_request_group: expect.objectContaining({
159
+ type: "Alternative",
160
+ requests: [
161
+ expect.objectContaining({
162
+ metadata: { reason: { value: "My draft", origin: "user" } },
163
+ }),
164
+ ],
165
+ }),
166
+ }),
167
+ );
168
+ });
169
+
170
+ it("keeps assigned and selectable system drafts separate across a remount", async () => {
171
+ useTemplateRelations.mockReturnValue({
172
+ resource: [{ resource_id: 10, template_id: 1 }],
173
+ default: { template_id: 2 },
174
+ loading: false,
175
+ });
176
+ const view = openCheckout([structure(101, 10), structure(201, 20)]);
177
+ const user = userEvent.setup({ delay: null });
178
+ await waitFor(() =>
179
+ expect(view.getAllByPlaceholderText("Reason")).toHaveLength(2),
180
+ );
181
+ const groups = view.container.querySelectorAll(".grant-cart-group-form");
182
+ expect(within(groups[0]).queryByRole("listbox")).not.toBeInTheDocument();
183
+ await user.click(
184
+ await within(groups[1]).findByRole("option", { name: "Alternative" }),
185
+ );
186
+ await user.type(
187
+ within(groups[1]).getByPlaceholderText("Reason"),
188
+ "Group draft",
189
+ );
190
+ view.rerender(<CheckoutView visible={false} />);
191
+ view.rerender(<CheckoutView />);
192
+ await waitFor(() =>
193
+ expect(view.getAllByPlaceholderText("Reason")[1]).toHaveValue(
194
+ "Group draft",
195
+ ),
196
+ );
197
+ await user.click(view.getByRole("button", { name: "actions.save" }));
198
+ expect(trigger).toHaveBeenCalledWith({
199
+ grant_request_groups: [
200
+ expect.objectContaining({ type: "Assigned" }),
201
+ expect.objectContaining({
202
+ type: "Alternative",
203
+ requests: [
204
+ expect.objectContaining({
205
+ data_structure_id: 201,
206
+ metadata: { reason: { value: "Group draft", origin: "user" } },
207
+ }),
208
+ ],
209
+ }),
210
+ ],
211
+ });
212
+ });
213
+
214
+ it("waits for relations and restores a saved selection when definitions arrive later", async () => {
215
+ useTemplateRelations.mockReturnValue({ resource: [], loading: true });
216
+ useTemplatesByIds.mockReturnValue({});
217
+ const view = openCheckout([structure(101, 10)], {
218
+ groupTemplates: {
219
+ nodomain_others_notype: {
220
+ template: "Alternative",
221
+ templateContent: { reason: { value: "Saved", origin: "user" } },
222
+ },
223
+ },
224
+ });
225
+ expect(view.queryByPlaceholderText("Reason")).not.toBeInTheDocument();
226
+ expect(view.getByRole("button", { name: "actions.save" })).toBeDisabled();
227
+ useTemplateRelations.mockReturnValue({
228
+ resource: [],
229
+ default: { template_id: 2 },
230
+ loading: false,
231
+ });
232
+ view.rerender(<CheckoutView />);
233
+ await waitFor(() =>
234
+ expect(view.getByPlaceholderText("Reason")).toHaveValue("Saved"),
235
+ );
236
+ useTemplatesByIds.mockReturnValue({ 2: { ...fallback } });
237
+ view.rerender(<CheckoutView />);
238
+ expect(view.getByPlaceholderText("Reason")).toHaveValue("Saved");
239
+ expect(
240
+ JSON.parse(view.getByTestId("cart").textContent).groupTemplates
241
+ .nodomain_others_notype.template,
242
+ ).toBe("Alternative");
243
+ });
244
+
245
+ it("shares all grant templates across systems without a matching relation", async () => {
246
+ useTemplateRelations.mockReturnValue({
247
+ resource: [{ resource_id: 30, template_id: 1 }],
248
+ loading: false,
249
+ });
250
+ const view = openCheckout([structure(101, 10), structure(201, 20)]);
251
+ await waitFor(() =>
252
+ expect(view.getAllByRole("option", { name: "Assigned" })).toHaveLength(1),
253
+ );
254
+ expect(
255
+ view.queryByText("grantRequest.cart.group.others"),
256
+ ).not.toBeInTheDocument();
257
+ expect(view.getByText("structure.grant.cart.template")).toBeInTheDocument();
258
+ expect(view.queryByPlaceholderText("Reason")).not.toBeInTheDocument();
259
+ expect(view.getByRole("button", { name: "actions.save" })).toBeDisabled();
260
+ });
261
+
262
+ it("preserves the draft when adding and removing another system", async () => {
263
+ useTemplateRelations.mockReturnValue({
264
+ resource: [{ resource_id: 20, template_id: 1 }],
265
+ default: { template_id: 2 },
266
+ loading: false,
267
+ });
268
+ const view = openCheckout([structure(101, 10)]);
269
+ const user = userEvent.setup({ delay: null });
270
+ await view.findByPlaceholderText("Reason");
271
+ await user.click(await view.findByRole("option", { name: "Alternative" }));
272
+ await user.type(view.getByPlaceholderText("Reason"), "First system");
273
+ expect(view.getByText("structure.grant.cart.template")).toBeInTheDocument();
274
+ expect(
275
+ view.container.querySelectorAll(".grant-cart-group-form"),
276
+ ).toHaveLength(0);
277
+ await user.click(view.getByText("Add system"));
278
+ await waitFor(() =>
279
+ expect(view.getAllByPlaceholderText("Reason")).toHaveLength(2),
280
+ );
281
+ expect(view.getAllByPlaceholderText("Reason")[0]).toHaveValue("First system");
282
+ expect(
283
+ view.container.querySelectorAll(".grant-cart-group-form"),
284
+ ).toHaveLength(2);
285
+ expect(
286
+ view.queryByText("structure.grant.cart.template"),
287
+ ).not.toBeInTheDocument();
288
+ await user.click(view.getByText("Remove system"));
289
+ expect(view.getByPlaceholderText("Reason")).toHaveValue("First system");
290
+ expect(view.getByText("structure.grant.cart.template")).toBeInTheDocument();
291
+ expect(
292
+ view.container.querySelectorAll(".grant-cart-group-form"),
293
+ ).toHaveLength(0);
294
+ await user.click(view.getByRole("button", { name: "actions.save" }));
295
+ expect(trigger).toHaveBeenCalledWith(
296
+ expect.objectContaining({
297
+ grant_request_group: expect.objectContaining({ type: "Alternative" }),
298
+ }),
299
+ );
300
+ });
301
+
302
+ it("restores a grant modification draft without reapplying its default", async () => {
303
+ const view = openCheckout([structure(101, 10)], {
304
+ modificationGrant: { id: 7, detail: {} },
305
+ });
306
+ const user = userEvent.setup({ delay: null });
307
+ await view.findByPlaceholderText("Reason");
308
+ await user.click(await view.findByRole("option", { name: "Alternative" }));
309
+ await user.type(view.getByPlaceholderText("Reason"), "Updated draft");
310
+ view.rerender(<CheckoutView visible={false} />);
311
+ view.rerender(<CheckoutView />);
312
+ await waitFor(() =>
313
+ expect(view.getByPlaceholderText("Reason")).toHaveValue("Updated draft"),
314
+ );
315
+ await user.click(view.getByRole("button", { name: "actions.save" }));
316
+ expect(trigger).toHaveBeenCalledWith(
317
+ expect.objectContaining({
318
+ grant_request_group: expect.objectContaining({
319
+ type: "Alternative",
320
+ modification_grant_id: 7,
321
+ }),
322
+ }),
323
+ );
324
+ });
325
+
326
+ it("retains the default assignment for an explicit system relation with no template", async () => {
327
+ useTemplateRelations.mockReturnValue({
328
+ resource: [{ resource_id: 10, template_id: null }],
329
+ default: { template_id: 2 },
330
+ loading: false,
331
+ });
332
+ const view = openCheckout([structure(101, 10)]);
333
+ await view.findByPlaceholderText("Reason");
334
+ expect(view.queryByRole("combobox")).not.toBeInTheDocument();
335
+ expect(
336
+ JSON.parse(view.getByTestId("cart").textContent).groupTemplates
337
+ .nodomain_10_2.template,
338
+ ).toBe("Default");
339
+ });
340
+
341
+ it("preserves domain grouping for assigned templates", async () => {
342
+ useTemplateRelations.mockReturnValue({
343
+ resource: [{ resource_id: 10, template_id: 1, group_by_domain: true }],
344
+ loading: false,
345
+ });
346
+ const view = openCheckout([
347
+ structure(101, 10),
348
+ { ...structure(102, 10), domain_ids: [2] },
349
+ ]);
350
+ await waitFor(() =>
351
+ expect(view.getAllByPlaceholderText("Reason")).toHaveLength(2),
352
+ );
353
+ expect(view.queryByRole("combobox")).not.toBeInTheDocument();
354
+ const cart = JSON.parse(view.getByTestId("cart").textContent);
355
+ expect(Object.keys(cart.groupTemplates)).toEqual(["1_10_1", "2_10_1"]);
356
+ });
357
+
358
+ it("renders a late default after the template list loads and preserves edits on refresh", async () => {
359
+ useTemplatesByIds.mockReturnValue({});
360
+ const view = openCheckout([structure(101, 10)]);
361
+ await view.findByRole("option", { name: "Alternative" });
362
+ expect(view.queryByPlaceholderText("Reason")).not.toBeInTheDocument();
363
+ useTemplatesByIds.mockReturnValue({ 2: fallback });
364
+ view.rerender(<CheckoutView />);
365
+ const user = userEvent.setup({ delay: null });
366
+ await user.type(await view.findByPlaceholderText("Reason"), "Default draft");
367
+ useTemplatesByIds.mockReturnValue({ 2: { ...fallback } });
368
+ view.rerender(<CheckoutView />);
369
+ expect(view.getByPlaceholderText("Reason")).toHaveValue("Default draft");
370
+ });
371
+
372
+ it("retains a single draft created before per-group storage", async () => {
373
+ const view = openCheckout([structure(101, 10)], {
374
+ template: "Alternative",
375
+ templateContent: { reason: { value: "Existing draft", origin: "user" } },
376
+ validCart: true,
377
+ });
378
+ await waitFor(() =>
379
+ expect(view.getByPlaceholderText("Reason")).toHaveValue("Existing draft"),
380
+ );
381
+ expect(
382
+ JSON.parse(view.getByTestId("cart").textContent).groupTemplates
383
+ .nodomain_others_notype.template,
384
+ ).toBe("Alternative");
385
+ });
386
+
387
+ it("submits all systems and domains in one group when no grouping policy exists", async () => {
388
+ useTemplateRelations.mockReturnValue({
389
+ resource: [],
390
+ default: null,
391
+ loading: false,
392
+ });
393
+ const view = openCheckout([
394
+ structure(101, 10),
395
+ { ...structure(201, 20), domain_ids: [2] },
396
+ ]);
397
+ const user = userEvent.setup({ delay: null });
398
+ expect(await view.findAllByRole("combobox")).toHaveLength(1);
399
+ await user.click(await view.findByRole("option", { name: "Alternative" }));
400
+ await user.type(view.getByPlaceholderText("Reason"), "Shared draft");
401
+ view.rerender(<CheckoutView visible={false} />);
402
+ view.rerender(<CheckoutView />);
403
+ await waitFor(() =>
404
+ expect(view.getByPlaceholderText("Reason")).toHaveValue("Shared draft"),
405
+ );
406
+ await user.click(view.getByRole("button", { name: "actions.save" }));
407
+ expect(trigger).toHaveBeenCalledWith({
408
+ grant_request_group: expect.objectContaining({
409
+ type: "Alternative",
410
+ requests: [
411
+ expect.objectContaining({ data_structure_id: 101 }),
412
+ expect.objectContaining({ data_structure_id: 201 }),
413
+ ],
414
+ }),
415
+ });
416
+ });