@redacto.io/consent-sdk-react 9.2.0-beta.1 → 9.2.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/CHANGELOG.md +24 -0
- package/dist/privacy-center.d.mts +6 -10
- package/dist/privacy-center.d.ts +6 -10
- package/dist/privacy-center.js +208 -850
- package/dist/privacy-center.mjs +208 -850
- package/package.json +1 -1
- package/src/RedactoPrivacyCenter/components/DataRequest/ProductSelectionSection.tsx +4 -4
- package/src/RedactoPrivacyCenter/components/Form.tsx +214 -866
- package/src/RedactoPrivacyCenter/components/RequestSubmit.tsx +44 -219
- package/src/RedactoPrivacyCenter/components/types.ts +14 -20
- package/src/RedactoPrivacyCenter/lib/api.ts +23 -58
- package/src/RedactoPrivacyCenter/lib/types.ts +0 -31
- package/src/RedactoPrivacyCenter/locales/as/translation.json +2 -2
- package/src/RedactoPrivacyCenter/locales/bho/translation.json +2 -2
- package/src/RedactoPrivacyCenter/locales/bn/translation.json +2 -2
- package/src/RedactoPrivacyCenter/locales/brx/translation.json +2 -2
- package/src/RedactoPrivacyCenter/locales/doi/translation.json +2 -2
- package/src/RedactoPrivacyCenter/locales/en/translation.json +2 -2
- package/src/RedactoPrivacyCenter/locales/gom/translation.json +2 -2
- package/src/RedactoPrivacyCenter/locales/gu/translation.json +2 -2
- package/src/RedactoPrivacyCenter/locales/hi/translation.json +2 -2
- package/src/RedactoPrivacyCenter/locales/kn/translation.json +2 -2
- package/src/RedactoPrivacyCenter/locales/ks/translation.json +2 -2
- package/src/RedactoPrivacyCenter/locales/mai/translation.json +2 -2
- package/src/RedactoPrivacyCenter/locales/ml/translation.json +2 -2
- package/src/RedactoPrivacyCenter/locales/mni-Mtei/translation.json +2 -2
- package/src/RedactoPrivacyCenter/locales/mr/translation.json +2 -2
- package/src/RedactoPrivacyCenter/locales/ne/translation.json +2 -2
- package/src/RedactoPrivacyCenter/locales/or/translation.json +2 -2
- package/src/RedactoPrivacyCenter/locales/pa/translation.json +2 -2
- package/src/RedactoPrivacyCenter/locales/sa/translation.json +2 -2
- package/src/RedactoPrivacyCenter/locales/sat/translation.json +2 -2
- package/src/RedactoPrivacyCenter/locales/sd/translation.json +2 -2
- package/src/RedactoPrivacyCenter/locales/ta/translation.json +2 -2
- package/src/RedactoPrivacyCenter/locales/te/translation.json +2 -2
- package/src/RedactoPrivacyCenter/locales/ur/translation.json +2 -2
- package/tests/Form.test.tsx +123 -149
- package/tests/ProductSelectionSection.test.tsx +39 -4
- package/tests/RequestSubmit.test.tsx +27 -187
|
@@ -11,11 +11,7 @@ import { toast } from '../utils/toast';
|
|
|
11
11
|
import { PCButton, PCCheckbox, PCInput, PCLoader, PCSelect, PCText, PCTextarea } from '../ui';
|
|
12
12
|
import { buildGroupDataElements, buildGroupPurposes } from './groupHelpers';
|
|
13
13
|
import { pcStyles } from '../styles/pcStyles';
|
|
14
|
-
import {
|
|
15
|
-
createCaseRequest,
|
|
16
|
-
createCaseRequestUnsafe,
|
|
17
|
-
getFormData,
|
|
18
|
-
} from '../lib/api';
|
|
14
|
+
import { createCaseRequest, getFormData } from '../lib/api';
|
|
19
15
|
import { ensureValidToken } from '../lib/utils';
|
|
20
16
|
import { useAuth } from '../context/AuthContext';
|
|
21
17
|
import { useBaseUrl } from '../context/BaseUrlContext';
|
|
@@ -27,9 +23,7 @@ import { useOrganization } from '../context/OrganizationContext';
|
|
|
27
23
|
import i18n from '../lib/i18n';
|
|
28
24
|
import { getLangParam } from '../lib/useCurrentLanguage';
|
|
29
25
|
import type {
|
|
30
|
-
BatchCaseSubmitResult,
|
|
31
26
|
CreateCaseResponse,
|
|
32
|
-
DataElement,
|
|
33
27
|
FormData,
|
|
34
28
|
FormProductGroup,
|
|
35
29
|
GrievanceOptions,
|
|
@@ -72,22 +66,33 @@ type ExtractedCaseRef = { uuid: string; case_id: string };
|
|
|
72
66
|
type CaseIdContainer = { case_id?: string; uuid?: string };
|
|
73
67
|
|
|
74
68
|
/**
|
|
75
|
-
* Request types that engage the
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
* ACCESS/ERASURE drive a per-product `PurposeSelectionSection`; CORRECTION
|
|
79
|
-
* drives a per-product `CorrectionDataSection` whose `dataElements` are
|
|
80
|
-
* scoped to the selected group (dedup within product, not across products).
|
|
69
|
+
* Request types that engage the per-product picker and require a
|
|
70
|
+
* product anchor (`product_uuid`) on the submitted case. Nomination
|
|
71
|
+
* and grievance are org-level and skip the product step entirely.
|
|
81
72
|
*/
|
|
82
|
-
const
|
|
73
|
+
const PRODUCT_SCOPED_REQUEST_TYPES: ReadonlySet<RequestTypeEnum> = new Set([
|
|
83
74
|
RequestTypeEnum.ACCESS,
|
|
84
75
|
RequestTypeEnum.ERASURE,
|
|
85
76
|
RequestTypeEnum.CORRECTION,
|
|
86
77
|
]);
|
|
87
78
|
|
|
88
|
-
const
|
|
79
|
+
const isProductScopedRequestType = (
|
|
89
80
|
rt: RequestTypeEnum | null,
|
|
90
|
-
): rt is RequestTypeEnum =>
|
|
81
|
+
): rt is RequestTypeEnum =>
|
|
82
|
+
rt !== null && PRODUCT_SCOPED_REQUEST_TYPES.has(rt);
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Result shape rendered by RequestSubmit after a successful submission.
|
|
86
|
+
* Product fields are populated only for product-scoped request types
|
|
87
|
+
* (ACCESS / ERASURE / CORRECTION); nomination and grievance leave them
|
|
88
|
+
* as empty strings so the success card omits the product label.
|
|
89
|
+
*/
|
|
90
|
+
type SubmittedCase = {
|
|
91
|
+
case_id: string;
|
|
92
|
+
uuid: string;
|
|
93
|
+
product_name: string;
|
|
94
|
+
product_uuid: string;
|
|
95
|
+
};
|
|
91
96
|
|
|
92
97
|
/**
|
|
93
98
|
* Pull the `case_id` out of a `createCaseRequest` response, handling both
|
|
@@ -158,60 +163,39 @@ export default function Form({
|
|
|
158
163
|
additionalNote: '',
|
|
159
164
|
confirmCheckBox: false,
|
|
160
165
|
});
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
// product reached both directly and via a nominator stays distinct.
|
|
166
|
+
// Selected product-group key on the product-cards step. Keyed by
|
|
167
|
+
// `productGroupKey(group)` so the same product reached both directly
|
|
168
|
+
// AND via a nominator stays addressable as two distinct selections.
|
|
169
|
+
// null when no product is selected (or for org-level request types).
|
|
166
170
|
const [selectedProductKey, setSelectedProductKey] = useState<string | null>(
|
|
167
171
|
null,
|
|
168
172
|
);
|
|
169
|
-
//
|
|
170
|
-
// `
|
|
171
|
-
//
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
173
|
+
// Purpose state for the currently-selected product group. Seeded from
|
|
174
|
+
// `buildGroupPurposes(group)` whenever `selectedProductKey` changes, then
|
|
175
|
+
// mutated in place by `PurposeSelectionSection` as the user picks
|
|
176
|
+
// purposes / data elements. Single-select means we never need a Map.
|
|
177
|
+
const [selectedPurposes, setSelectedPurposes] = useState<Purpose[]>([]);
|
|
178
|
+
// CORRECTION selections for the currently-selected product group. These
|
|
179
|
+
// are flat (not keyed by product) because the picker is single-select —
|
|
180
|
+
// switching products re-seeds them via the same effect that re-seeds
|
|
181
|
+
// `selectedPurposes`. Names are the data-element names visible in the
|
|
182
|
+
// UI; they're reverse-translated to English at submit time.
|
|
175
183
|
const [selectedCorrectionElements, setSelectedCorrectionElements] = useState<
|
|
176
184
|
string[]
|
|
177
185
|
>([]);
|
|
178
|
-
const [uniqueDataElements, setUniqueDataElements] = useState<DataElement[]>(
|
|
179
|
-
[]
|
|
180
|
-
);
|
|
181
186
|
const [correctionValues, setCorrectionValues] = useState<
|
|
182
187
|
Record<string, { current: string; updated: string }>
|
|
183
188
|
>({});
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
const [
|
|
189
|
-
useState<Map<string, string[]>>(new Map());
|
|
190
|
-
const [correctionValuesByGroupKey, setCorrectionValuesByGroupKey] = useState<
|
|
191
|
-
Map<string, Record<string, { current: string; updated: string }>>
|
|
192
|
-
>(new Map());
|
|
193
|
-
// P2-SDK-4 of TECH-3712. Holds the aggregated batch result that
|
|
194
|
-
// `RequestSubmit` renders on the success step. Both paths populate this:
|
|
195
|
-
// - Grouped DSAR: directly from `submitBatchCases`.
|
|
196
|
-
// - Legacy single-case: a synthetic single-entry result.
|
|
197
|
-
const [batchResult, setBatchResult] = useState<BatchCaseSubmitResult | null>(
|
|
189
|
+
// Result rendered by `RequestSubmit` after a successful submission.
|
|
190
|
+
// Replaces the prior batch-shape now that submit is 1-form / 1-case.
|
|
191
|
+
// Product fields are empty strings for nomination / grievance (no
|
|
192
|
+
// product anchor) so the success card omits the product label.
|
|
193
|
+
const [submittedCase, setSubmittedCase] = useState<SubmittedCase | null>(
|
|
198
194
|
null,
|
|
199
195
|
);
|
|
200
|
-
// P2-SDK-4 of TECH-3712. Product-group keys currently mid-retry. Drives
|
|
201
|
-
// the per-row spinner state on the failure list. Keyed by
|
|
202
|
-
// `productGroupKey(group)` (not `product_uuid`) so a product picked both
|
|
203
|
-
// directly and via a nominator can be retried independently.
|
|
204
|
-
const [retryingGroupKeys, setRetryingGroupKeys] = useState<Set<string>>(
|
|
205
|
-
new Set(),
|
|
206
|
-
);
|
|
207
196
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
208
197
|
// Store the original English form data for submission — translations are display-only
|
|
209
198
|
const englishDataRef = useRef<FormData | null>(null);
|
|
210
|
-
// Store selections to restore after translation
|
|
211
|
-
const selectionsToRestoreRef = useRef<{
|
|
212
|
-
access: Map<string, { purposeSelected: boolean; dataElementSelections: Map<string, boolean> }>;
|
|
213
|
-
erasure: Map<string, { purposeSelected: boolean; dataElementSelections: Map<string, boolean> }>;
|
|
214
|
-
} | null>(null);
|
|
215
199
|
const [uploadedDocumentId, setUploadedDocumentId] = useState<string[]>([]);
|
|
216
200
|
const [uploadedDocuments, setUploadedDocuments] = useState<
|
|
217
201
|
{ uuid: string; name: string }[]
|
|
@@ -242,73 +226,41 @@ export default function Form({
|
|
|
242
226
|
}
|
|
243
227
|
};
|
|
244
228
|
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
// backends fall through to the legacy flat-purposes render below.
|
|
248
|
-
const useGroupedRender = useMemo<boolean>(
|
|
249
|
-
() => Array.isArray(data?.direct_groups),
|
|
250
|
-
[data],
|
|
251
|
-
);
|
|
252
|
-
|
|
253
|
-
// Phase 2 of TECH-3712. Surface a one-time warning per data load when we
|
|
254
|
-
// fall back to the legacy flat-purposes render. Helps customer developers
|
|
255
|
-
// notice their backend hasn't been upgraded to Phase 1 yet (the form still
|
|
256
|
-
// works, but the new product-scoped DSAR UX is unavailable).
|
|
257
|
-
useEffect(() => {
|
|
258
|
-
if (data && !useGroupedRender) {
|
|
259
|
-
console.warn(
|
|
260
|
-
'[consent-sdk] Backend response lacks direct_groups; ' +
|
|
261
|
-
'falling back to legacy flat-purposes render. ' +
|
|
262
|
-
'Update consent-server to Phase 1 to enable product-scoped DSAR.',
|
|
263
|
-
);
|
|
264
|
-
}
|
|
265
|
-
}, [data, useGroupedRender]);
|
|
266
|
-
|
|
267
|
-
// Phase 2 of TECH-3712. Union of direct + nominated product groups used to
|
|
268
|
-
// drive both the cards step and the per-product purpose-picker step.
|
|
229
|
+
// Union of direct + nominated product groups exposed by the backend.
|
|
230
|
+
// Drives both the product-cards step and the per-product picker beneath.
|
|
269
231
|
const allGroups = useMemo<FormProductGroup[]>(() => {
|
|
270
|
-
if (!
|
|
232
|
+
if (!data) {
|
|
271
233
|
return [];
|
|
272
234
|
}
|
|
273
235
|
return [...(data.direct_groups ?? []), ...(data.nominated_groups ?? [])];
|
|
274
|
-
}, [data
|
|
236
|
+
}, [data]);
|
|
275
237
|
|
|
276
238
|
// TECH-3712 UX: when the DP has exactly one consent group total
|
|
277
239
|
// (direct + nominated combined), the product-selection step is friction —
|
|
278
240
|
// there's nothing to choose. Skip rendering ProductSelectionSection
|
|
279
|
-
// entirely and auto-select the lone group below so
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
)
|
|
288
|
-
const skipProductStep = totalGroups === 1;
|
|
289
|
-
|
|
290
|
-
// Phase 2 of TECH-3712. Subset of groups currently checked on the cards
|
|
291
|
-
// step — drives which `PurposeSelectionSection`s render and which cases
|
|
292
|
-
// get POSTed on submit.
|
|
293
|
-
const selectedGroups = useMemo<FormProductGroup[]>(() => {
|
|
241
|
+
// entirely and auto-select the lone group below so the per-product
|
|
242
|
+
// picker renders without an intermediate card. With zero groups we
|
|
243
|
+
// still render the section so the empty-state message is visible.
|
|
244
|
+
const skipProductStep = allGroups.length === 1;
|
|
245
|
+
|
|
246
|
+
// The single product-group the user is currently working with. Null
|
|
247
|
+
// when no product is selected (the picker is shown empty / unselected)
|
|
248
|
+
// or when the request type is org-level (nomination / grievance).
|
|
249
|
+
const selectedGroup = useMemo<FormProductGroup | null>(() => {
|
|
294
250
|
if (selectedProductKey) {
|
|
295
|
-
|
|
296
|
-
(g) => productGroupKey(g) === selectedProductKey
|
|
251
|
+
return (
|
|
252
|
+
allGroups.find((g) => productGroupKey(g) === selectedProductKey) ??
|
|
253
|
+
null
|
|
297
254
|
);
|
|
298
|
-
if (group) {
|
|
299
|
-
return [group];
|
|
300
|
-
}
|
|
301
255
|
}
|
|
302
256
|
if (skipProductStep) {
|
|
303
|
-
|
|
304
|
-
data?.direct_groups?.[0] ?? data?.nominated_groups?.[0];
|
|
305
|
-
return onlyGroup ? [onlyGroup] : [];
|
|
257
|
+
return allGroups[0] ?? null;
|
|
306
258
|
}
|
|
307
|
-
return
|
|
308
|
-
}, [allGroups, selectedProductKey, skipProductStep
|
|
259
|
+
return null;
|
|
260
|
+
}, [allGroups, selectedProductKey, skipProductStep]);
|
|
309
261
|
|
|
310
|
-
//
|
|
311
|
-
//
|
|
262
|
+
// Stable select handler for ProductSelectionSection. Single-group
|
|
263
|
+
// auto-select fires this on mount via the child's effect.
|
|
312
264
|
const handleProductSelect = useCallback(
|
|
313
265
|
(key: string | null, _group: FormProductGroup | null) => {
|
|
314
266
|
setSelectedProductKey(key);
|
|
@@ -316,120 +268,6 @@ export default function Form({
|
|
|
316
268
|
[],
|
|
317
269
|
);
|
|
318
270
|
|
|
319
|
-
// Phase 2 of TECH-3712. Stable per-group setter passed to the wrapped
|
|
320
|
-
// PurposeSelectionSection so each card edits its own slice of state.
|
|
321
|
-
const setPurposesForGroup = useCallback(
|
|
322
|
-
(key: string, updated: Purpose[]) => {
|
|
323
|
-
setPurposesByGroupKey((prev) => {
|
|
324
|
-
const next = new Map(prev);
|
|
325
|
-
next.set(key, updated);
|
|
326
|
-
return next;
|
|
327
|
-
});
|
|
328
|
-
},
|
|
329
|
-
[],
|
|
330
|
-
);
|
|
331
|
-
|
|
332
|
-
// TECH-3712 (correction grouped path). Stable per-group setters for the
|
|
333
|
-
// CORRECTION maps. CorrectionDataSection's `setSelected` and
|
|
334
|
-
// `setCorrectionValues` props are value-setters (not state updaters), so
|
|
335
|
-
// these accept the full replacement value the child computed.
|
|
336
|
-
const setCorrectionElementsForGroup = useCallback(
|
|
337
|
-
(key: string, values: string[]) => {
|
|
338
|
-
setCorrectionElementsByGroupKey((prev) => {
|
|
339
|
-
const next = new Map(prev);
|
|
340
|
-
next.set(key, values);
|
|
341
|
-
return next;
|
|
342
|
-
});
|
|
343
|
-
},
|
|
344
|
-
[],
|
|
345
|
-
);
|
|
346
|
-
|
|
347
|
-
const setCorrectionValuesForGroup = useCallback(
|
|
348
|
-
(
|
|
349
|
-
key: string,
|
|
350
|
-
values: Record<string, { current: string; updated: string }>,
|
|
351
|
-
) => {
|
|
352
|
-
setCorrectionValuesByGroupKey((prev) => {
|
|
353
|
-
const next = new Map(prev);
|
|
354
|
-
next.set(key, values);
|
|
355
|
-
return next;
|
|
356
|
-
});
|
|
357
|
-
},
|
|
358
|
-
[],
|
|
359
|
-
);
|
|
360
|
-
|
|
361
|
-
useEffect(() => {
|
|
362
|
-
if (data?.purposes) {
|
|
363
|
-
// Validate purposes array structure
|
|
364
|
-
if (!Array.isArray(data.purposes)) {
|
|
365
|
-
console.warn('Form data purposes is not an array:', data.purposes);
|
|
366
|
-
return;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
const extractedPurposes = data.purposes
|
|
370
|
-
.map((p) => p?.purpose)
|
|
371
|
-
.filter((p) => p !== null && p !== undefined);
|
|
372
|
-
|
|
373
|
-
if (extractedPurposes.length === 0) {
|
|
374
|
-
console.warn('No valid purposes found in form data');
|
|
375
|
-
return;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
const uniqueElements = new Map();
|
|
379
|
-
extractedPurposes.forEach((purpose) => {
|
|
380
|
-
// Ensure data_elements is an array and filter out any null/undefined elements.
|
|
381
|
-
// Include every data element the org holds for this user, regardless of current
|
|
382
|
-
// consent state. Revoking consent does not delete data (TECH-4089), so purposes
|
|
383
|
-
// with revoked/withdrawn consent must remain raisable for access/erasure/correction.
|
|
384
|
-
const validDataElements = Array.isArray(purpose.data_elements)
|
|
385
|
-
? purpose.data_elements.filter(
|
|
386
|
-
(element) => element !== null && element !== undefined && element.uuid
|
|
387
|
-
)
|
|
388
|
-
: [];
|
|
389
|
-
|
|
390
|
-
// Data elements are validated and processed
|
|
391
|
-
|
|
392
|
-
validDataElements.forEach((element) => {
|
|
393
|
-
if (element.uuid && !uniqueElements.has(element.uuid)) {
|
|
394
|
-
uniqueElements.set(element.uuid, {
|
|
395
|
-
...element,
|
|
396
|
-
purposeName: purpose.name,
|
|
397
|
-
});
|
|
398
|
-
}
|
|
399
|
-
});
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
setUniqueDataElements(
|
|
403
|
-
Array.from(uniqueElements.values()) as DataElement[]
|
|
404
|
-
);
|
|
405
|
-
|
|
406
|
-
// Ensure data_elements are properly preserved with all properties.
|
|
407
|
-
// Include data elements regardless of consent state (revocation ≠ deletion,
|
|
408
|
-
// TECH-4089); only drop purposes that genuinely have no data elements.
|
|
409
|
-
const processedPurposes = extractedPurposes
|
|
410
|
-
.map((p) => {
|
|
411
|
-
const validDataElements = Array.isArray(p.data_elements)
|
|
412
|
-
? p.data_elements
|
|
413
|
-
.filter((de) => de !== null && de !== undefined && de.uuid)
|
|
414
|
-
.map((de) => ({
|
|
415
|
-
...de,
|
|
416
|
-
selected: false,
|
|
417
|
-
}))
|
|
418
|
-
: [];
|
|
419
|
-
|
|
420
|
-
return {
|
|
421
|
-
...p,
|
|
422
|
-
selected: false,
|
|
423
|
-
data_elements: validDataElements,
|
|
424
|
-
};
|
|
425
|
-
})
|
|
426
|
-
.filter((p) => p.data_elements.length > 0);
|
|
427
|
-
|
|
428
|
-
setAccessPurposes(processedPurposes);
|
|
429
|
-
setErasurePurposes(processedPurposes);
|
|
430
|
-
}
|
|
431
|
-
}, [data]);
|
|
432
|
-
|
|
433
271
|
const fetchAndApplyTranslations = useCallback(async (langValue: string) => {
|
|
434
272
|
if (!organizationId || !workspaceId || !data) {
|
|
435
273
|
return;
|
|
@@ -462,37 +300,6 @@ export default function Form({
|
|
|
462
300
|
return;
|
|
463
301
|
}
|
|
464
302
|
|
|
465
|
-
const currentAccessSelections = new Map<string, {
|
|
466
|
-
purposeSelected: boolean;
|
|
467
|
-
dataElementSelections: Map<string, boolean>;
|
|
468
|
-
}>();
|
|
469
|
-
const currentErasureSelections = new Map<string, {
|
|
470
|
-
purposeSelected: boolean;
|
|
471
|
-
dataElementSelections: Map<string, boolean>;
|
|
472
|
-
}>();
|
|
473
|
-
|
|
474
|
-
accessPurposes.forEach((purpose) => {
|
|
475
|
-
const dataElementSelections = new Map<string, boolean>();
|
|
476
|
-
purpose.data_elements.forEach((de) => {
|
|
477
|
-
dataElementSelections.set(de.uuid, de.selected);
|
|
478
|
-
});
|
|
479
|
-
currentAccessSelections.set(purpose.uuid, {
|
|
480
|
-
purposeSelected: purpose.selected,
|
|
481
|
-
dataElementSelections,
|
|
482
|
-
});
|
|
483
|
-
});
|
|
484
|
-
|
|
485
|
-
erasurePurposes.forEach((purpose) => {
|
|
486
|
-
const dataElementSelections = new Map<string, boolean>();
|
|
487
|
-
purpose.data_elements.forEach((de) => {
|
|
488
|
-
dataElementSelections.set(de.uuid, de.selected);
|
|
489
|
-
});
|
|
490
|
-
currentErasureSelections.set(purpose.uuid, {
|
|
491
|
-
purposeSelected: purpose.selected,
|
|
492
|
-
dataElementSelections,
|
|
493
|
-
});
|
|
494
|
-
});
|
|
495
|
-
|
|
496
303
|
if (!translatedFormData.purposes ||
|
|
497
304
|
!Array.isArray(translatedFormData.purposes) ||
|
|
498
305
|
translatedFormData.purposes.length === 0) {
|
|
@@ -513,11 +320,6 @@ export default function Form({
|
|
|
513
320
|
});
|
|
514
321
|
setSelectedGrievances(updatedGrievanceSelections);
|
|
515
322
|
}
|
|
516
|
-
|
|
517
|
-
selectionsToRestoreRef.current = {
|
|
518
|
-
access: currentAccessSelections,
|
|
519
|
-
erasure: currentErasureSelections,
|
|
520
|
-
};
|
|
521
323
|
} catch (error) {
|
|
522
324
|
console.error('Error fetching translation:', error);
|
|
523
325
|
// Fallback to English when translation API fails
|
|
@@ -525,155 +327,71 @@ export default function Form({
|
|
|
525
327
|
} finally {
|
|
526
328
|
setIsTranslating(false);
|
|
527
329
|
}
|
|
528
|
-
}, [organizationId, workspaceId, data, formData.contact, isPopup, isCaseManagementContext, authContext, consentClient,
|
|
529
|
-
|
|
530
|
-
// Clear
|
|
330
|
+
}, [organizationId, workspaceId, data, formData.contact, isPopup, isCaseManagementContext, authContext, consentClient, selectedGrievances, t]);
|
|
331
|
+
|
|
332
|
+
// Clear product / org-level selections when request type changes.
|
|
333
|
+
// Per-product state (selectedPurposes, selectedCorrectionElements,
|
|
334
|
+
// correctionValues) is owned by the seed effect below — clearing it
|
|
335
|
+
// here too would race with that effect's re-seed when skipProductStep
|
|
336
|
+
// is true (the sole group stays selected across request-type changes
|
|
337
|
+
// because the auto-select effect refires on requestType).
|
|
531
338
|
useEffect(() => {
|
|
532
|
-
|
|
533
|
-
|
|
339
|
+
if (!skipProductStep) {
|
|
340
|
+
setSelectedProductKey(null);
|
|
341
|
+
}
|
|
534
342
|
setNominationData({ nominee_email: '', nominee_mobile: '' });
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
// Ensure data_elements are properly preserved with all properties.
|
|
550
|
-
// Include data elements regardless of consent state (revocation ≠ deletion,
|
|
551
|
-
// TECH-4089); only drop purposes that genuinely have no data elements.
|
|
552
|
-
const processedPurposes = extractedPurposes
|
|
553
|
-
.map((p) => {
|
|
554
|
-
const validDataElements = Array.isArray(p.data_elements)
|
|
555
|
-
? p.data_elements
|
|
556
|
-
.filter((de) => de !== null && de !== undefined && de.uuid)
|
|
557
|
-
.map((de) => ({
|
|
558
|
-
...de,
|
|
559
|
-
selected: false,
|
|
560
|
-
}))
|
|
561
|
-
: [];
|
|
562
|
-
|
|
563
|
-
return {
|
|
564
|
-
...p,
|
|
565
|
-
selected: false,
|
|
566
|
-
data_elements: validDataElements,
|
|
567
|
-
};
|
|
568
|
-
})
|
|
569
|
-
.filter((p) => p.data_elements.length > 0);
|
|
570
|
-
|
|
571
|
-
setAccessPurposes(processedPurposes);
|
|
572
|
-
setErasurePurposes(processedPurposes);
|
|
573
|
-
|
|
574
|
-
if (
|
|
575
|
-
requestType === RequestTypeEnum.GRIEVANCE &&
|
|
576
|
-
grievanceOptions.length > 0
|
|
577
|
-
) {
|
|
578
|
-
const resetGrievances = grievanceOptions.reduce(
|
|
579
|
-
(acc, option) => {
|
|
580
|
-
acc[option.value] = false;
|
|
581
|
-
return acc;
|
|
582
|
-
},
|
|
583
|
-
{} as Record<string, boolean>
|
|
584
|
-
);
|
|
585
|
-
setSelectedGrievances(resetGrievances);
|
|
586
|
-
}
|
|
343
|
+
|
|
344
|
+
if (
|
|
345
|
+
requestType === RequestTypeEnum.GRIEVANCE &&
|
|
346
|
+
grievanceOptions.length > 0
|
|
347
|
+
) {
|
|
348
|
+
const resetGrievances = grievanceOptions.reduce(
|
|
349
|
+
(acc, option) => {
|
|
350
|
+
acc[option.value] = false;
|
|
351
|
+
return acc;
|
|
352
|
+
},
|
|
353
|
+
{} as Record<string, boolean>,
|
|
354
|
+
);
|
|
355
|
+
setSelectedGrievances(resetGrievances);
|
|
587
356
|
}
|
|
588
|
-
}, [requestType,
|
|
589
|
-
|
|
590
|
-
// TECH-3712 UX: when `skipProductStep` is true (DP has exactly one
|
|
591
|
-
// group total), auto-select that group so
|
|
592
|
-
// picker
|
|
593
|
-
// the request-type-change clearing effect above so a request-type
|
|
594
|
-
// re-establishes the selection on the next tick
|
|
595
|
-
//
|
|
596
|
-
//
|
|
597
|
-
// but that effect never fires here because the child is not rendered
|
|
598
|
-
// (see the JSX gate below).
|
|
357
|
+
}, [requestType, grievanceOptions, skipProductStep]);
|
|
358
|
+
|
|
359
|
+
// TECH-3712 UX: when `skipProductStep` is true (DP has exactly one
|
|
360
|
+
// consent group total), auto-select that group so the per-product
|
|
361
|
+
// picker renders directly without the user clicking a card. Placed
|
|
362
|
+
// AFTER the request-type-change clearing effect above so a request-type
|
|
363
|
+
// switch re-establishes the selection on the next tick. The child
|
|
364
|
+
// ProductSelectionSection has an equivalent mount-time effect, but
|
|
365
|
+
// that effect never fires here because the child is not rendered.
|
|
599
366
|
useEffect(() => {
|
|
600
367
|
if (!skipProductStep) {
|
|
601
368
|
return;
|
|
602
369
|
}
|
|
603
|
-
const onlyGroup =
|
|
604
|
-
data?.direct_groups?.[0] ?? data?.nominated_groups?.[0];
|
|
370
|
+
const onlyGroup = allGroups[0];
|
|
605
371
|
if (!onlyGroup) {
|
|
606
372
|
return;
|
|
607
373
|
}
|
|
608
374
|
const key = productGroupKey(onlyGroup);
|
|
609
375
|
setSelectedProductKey((prev) => (prev === key ? prev : key));
|
|
610
|
-
}, [skipProductStep,
|
|
611
|
-
|
|
612
|
-
//
|
|
613
|
-
//
|
|
614
|
-
//
|
|
615
|
-
//
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
const next = new Map(prev);
|
|
619
|
-
const selectedKeys = new Set<string>();
|
|
620
|
-
for (const group of selectedGroups) {
|
|
621
|
-
const key = productGroupKey(group);
|
|
622
|
-
selectedKeys.add(key);
|
|
623
|
-
if (!next.has(key)) {
|
|
624
|
-
next.set(key, buildGroupPurposes(group));
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
for (const existingKey of Array.from(next.keys())) {
|
|
628
|
-
if (!selectedKeys.has(existingKey)) {
|
|
629
|
-
next.delete(existingKey);
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
return next;
|
|
633
|
-
});
|
|
634
|
-
}, [selectedGroups]);
|
|
635
|
-
|
|
636
|
-
// TECH-3712 (correction grouped path). Mirror the purpose-state effect
|
|
637
|
-
// for the CORRECTION per-group maps: seed empty slices for newly-selected
|
|
638
|
-
// groups and prune slices for deselected ones. Empty defaults keep the
|
|
639
|
-
// CorrectionDataSection signature unchanged (it expects a `string[]` and
|
|
640
|
-
// a `Record<string, {current, updated}>`).
|
|
376
|
+
}, [skipProductStep, allGroups, requestType]);
|
|
377
|
+
|
|
378
|
+
// Seed (and re-seed on product switch or request-type switch) the
|
|
379
|
+
// purpose / correction state from the currently-selected group.
|
|
380
|
+
// Single-select means one re-seed per product change — no Map
|
|
381
|
+
// reconciliation needed. Clears selections entirely when the user
|
|
382
|
+
// de-selects the chip (selectedGroup -> null) or when the request
|
|
383
|
+
// type switches into NOMINATION / GRIEVANCE.
|
|
641
384
|
useEffect(() => {
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
for (const existingKey of Array.from(next.keys())) {
|
|
653
|
-
if (!selectedKeys.has(existingKey)) {
|
|
654
|
-
next.delete(existingKey);
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
return next;
|
|
658
|
-
});
|
|
659
|
-
setCorrectionValuesByGroupKey((prev) => {
|
|
660
|
-
const next = new Map(prev);
|
|
661
|
-
const selectedKeys = new Set<string>();
|
|
662
|
-
for (const group of selectedGroups) {
|
|
663
|
-
const key = productGroupKey(group);
|
|
664
|
-
selectedKeys.add(key);
|
|
665
|
-
if (!next.has(key)) {
|
|
666
|
-
next.set(key, {});
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
for (const existingKey of Array.from(next.keys())) {
|
|
670
|
-
if (!selectedKeys.has(existingKey)) {
|
|
671
|
-
next.delete(existingKey);
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
return next;
|
|
675
|
-
});
|
|
676
|
-
}, [selectedGroups]);
|
|
385
|
+
if (!selectedGroup || !isProductScopedRequestType(requestType)) {
|
|
386
|
+
setSelectedPurposes([]);
|
|
387
|
+
setSelectedCorrectionElements([]);
|
|
388
|
+
setCorrectionValues({});
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
setSelectedPurposes(buildGroupPurposes(selectedGroup));
|
|
392
|
+
setSelectedCorrectionElements([]);
|
|
393
|
+
setCorrectionValues({});
|
|
394
|
+
}, [selectedGroup, requestType]);
|
|
677
395
|
|
|
678
396
|
|
|
679
397
|
// Fetch API translations when language changes (purposes, data elements, grievance options)
|
|
@@ -823,56 +541,41 @@ export default function Form({
|
|
|
823
541
|
if (!isFormValid) {
|
|
824
542
|
return;
|
|
825
543
|
}
|
|
826
|
-
|
|
544
|
+
// For ERASURE, set revoke_consent_on_fulfilment=true so the
|
|
545
|
+
// backend's auto-revoke task fires when the case is fulfilled.
|
|
546
|
+
// Erasing data without revoking the consent that authorized its
|
|
547
|
+
// processing is incoherent per DPDPA Sec 12. Other request types
|
|
548
|
+
// (ACCESS / CORRECTION / NOMINATION / GRIEVANCE) do not engage
|
|
549
|
+
// the auto-revoke path on the backend and pass false here.
|
|
550
|
+
void handleSubmit(requestType === RequestTypeEnum.ERASURE);
|
|
827
551
|
};
|
|
828
552
|
|
|
553
|
+
/**
|
|
554
|
+
* Submit the form as a single case. Builds the payload from the
|
|
555
|
+
* currently-selected product group (for ACCESS/ERASURE/CORRECTION) or
|
|
556
|
+
* from the org-level state (for NOMINATION/GRIEVANCE), then POSTs once
|
|
557
|
+
* via `createCaseRequest`. The 1-form -> 1-case rule means no
|
|
558
|
+
* fan-out and no partial-failure UI to maintain.
|
|
559
|
+
*
|
|
560
|
+
* Anchors:
|
|
561
|
+
* - `product_uuid` is set ONLY for product-scoped request types and
|
|
562
|
+
* ONLY from `selectedGroup.product_uuid`. NOMINATION/GRIEVANCE
|
|
563
|
+
* never carry a product anchor.
|
|
564
|
+
* - `data_principal_uuid` is set ONLY for nominated cards (acting
|
|
565
|
+
* on someone else's behalf — the nominator's ConsentUser uuid).
|
|
566
|
+
* Direct cards omit the field so the backend defaults it from
|
|
567
|
+
* the JWT-derived requestor_uuid. `formData.uuid` is the
|
|
568
|
+
* FormDataOut session uuid, NOT the requestor's ConsentUser uuid,
|
|
569
|
+
* and would be rejected by the backend's ConsentUser lookup.
|
|
570
|
+
*/
|
|
829
571
|
const handleSubmit = async (revokeConsent: boolean = false) => {
|
|
830
572
|
if (!isFormValid) {
|
|
831
573
|
return;
|
|
832
574
|
}
|
|
833
575
|
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
// partial failures don't tank the whole submission.
|
|
837
|
-
if (useGroupedRender && isGroupedRequestType(requestType) && selectedGroups.length > 0) {
|
|
838
|
-
try {
|
|
839
|
-
setIsLoading(true);
|
|
840
|
-
const result = await submitBatchCases(revokeConsent);
|
|
841
|
-
|
|
842
|
-
if (result.succeeded.length > 0) {
|
|
843
|
-
queryClient.invalidateQueries({ queryKey: ['caseHistory'] });
|
|
844
|
-
// P2-SDK-4 of TECH-3712. Thread the full BatchCaseSubmitResult
|
|
845
|
-
// through to RequestSubmit so the success surface can list every
|
|
846
|
-
// case_id and render per-card retry buttons for any failures.
|
|
847
|
-
setBatchResult(result);
|
|
848
|
-
setIsSubmitted(true);
|
|
849
|
-
if (result.failed.length === 0) {
|
|
850
|
-
toast.success(t('requestSubmittedSuccess'));
|
|
851
|
-
} else {
|
|
852
|
-
toast.error(t('requestSubmissionFailed'));
|
|
853
|
-
}
|
|
854
|
-
if (isPopup) {
|
|
855
|
-
onCreateRequest?.();
|
|
856
|
-
}
|
|
857
|
-
} else {
|
|
858
|
-
toast.error(t('requestSubmissionFailed'));
|
|
859
|
-
}
|
|
860
|
-
} catch (error) {
|
|
861
|
-
console.error('Error submitting batch request:', error);
|
|
862
|
-
toast.error(t('submissionFailed'));
|
|
863
|
-
} finally {
|
|
864
|
-
setIsLoading(false);
|
|
865
|
-
if (isPopup) {
|
|
866
|
-
onClose?.();
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
return;
|
|
870
|
-
}
|
|
576
|
+
const isProductScoped = isProductScopedRequestType(requestType);
|
|
577
|
+
const anchorGroup = isProductScoped ? selectedGroup : null;
|
|
871
578
|
|
|
872
|
-
// Legacy / non-DSAR path: a single createCaseRequest with org-level
|
|
873
|
-
// request_details. Untouched by Phase 2 — nomination and grievance are
|
|
874
|
-
// always org-level, and DSAR falls here only when the backend hasn't
|
|
875
|
-
// shipped Phase 1 yet (useGroupedRender === false).
|
|
876
579
|
const requestDetails: {
|
|
877
580
|
purposes: UpdatePurpose[];
|
|
878
581
|
correction_data: UpdateCorrectionDataItem[];
|
|
@@ -884,19 +587,11 @@ export default function Form({
|
|
|
884
587
|
grievance_types: [],
|
|
885
588
|
};
|
|
886
589
|
|
|
887
|
-
if (
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
data_element_uuids: p.data_elements
|
|
893
|
-
.filter((de) => de.selected)
|
|
894
|
-
.map((de) => de.uuid),
|
|
895
|
-
}));
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
if (requestType === RequestTypeEnum.ERASURE) {
|
|
899
|
-
requestDetails.purposes = erasurePurposes
|
|
590
|
+
if (
|
|
591
|
+
requestType === RequestTypeEnum.ACCESS ||
|
|
592
|
+
requestType === RequestTypeEnum.ERASURE
|
|
593
|
+
) {
|
|
594
|
+
requestDetails.purposes = selectedPurposes
|
|
900
595
|
.filter((p) => p.selected || p.data_elements.some((de) => de.selected))
|
|
901
596
|
.map((p) => ({
|
|
902
597
|
purpose_uuid: p.uuid,
|
|
@@ -910,9 +605,9 @@ export default function Form({
|
|
|
910
605
|
requestDetails.correction_data = selectedCorrectionElements.map(
|
|
911
606
|
(name) => ({
|
|
912
607
|
name: getEnglishDataElementName(name),
|
|
913
|
-
curr_value: correctionValues[name]?.current
|
|
914
|
-
new_value: correctionValues[name]?.updated
|
|
915
|
-
})
|
|
608
|
+
curr_value: correctionValues[name]?.current ?? '',
|
|
609
|
+
new_value: correctionValues[name]?.updated ?? '',
|
|
610
|
+
}),
|
|
916
611
|
);
|
|
917
612
|
}
|
|
918
613
|
|
|
@@ -948,6 +643,10 @@ export default function Form({
|
|
|
948
643
|
...(requestType === RequestTypeEnum.ERASURE
|
|
949
644
|
? { revoke_consent_on_fulfilment: revokeConsent }
|
|
950
645
|
: {}),
|
|
646
|
+
...(anchorGroup ? { product_uuid: anchorGroup.product_uuid } : {}),
|
|
647
|
+
...(anchorGroup?.nominator
|
|
648
|
+
? { data_principal_uuid: anchorGroup.nominator.uuid }
|
|
649
|
+
: {}),
|
|
951
650
|
};
|
|
952
651
|
|
|
953
652
|
try {
|
|
@@ -959,44 +658,28 @@ export default function Form({
|
|
|
959
658
|
isPopup,
|
|
960
659
|
isCaseManagementContext, // Use session auth if in case management context
|
|
961
660
|
authContext,
|
|
962
|
-
consentClient
|
|
661
|
+
consentClient,
|
|
963
662
|
);
|
|
964
663
|
const extracted = extractCaseId(response);
|
|
965
664
|
const caseId = extracted?.case_id || extracted?.uuid || null;
|
|
966
665
|
|
|
967
666
|
if (caseId) {
|
|
968
667
|
queryClient.invalidateQueries({ queryKey: ['caseHistory'] });
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
// render a product label alongside the case ID; otherwise we fall
|
|
975
|
-
// back to empty strings (legacy nomination/grievance/non-grouped
|
|
976
|
-
// DSAR have no product context to render).
|
|
977
|
-
const productInfo =
|
|
978
|
-
useGroupedRender && selectedGroups.length === 1
|
|
979
|
-
? {
|
|
980
|
-
group_key: productGroupKey(selectedGroups[0]),
|
|
981
|
-
product_uuid: selectedGroups[0].product_uuid,
|
|
982
|
-
product_name: selectedGroups[0].product_name,
|
|
983
|
-
}
|
|
984
|
-
: { group_key: '', product_uuid: '', product_name: '' };
|
|
985
|
-
setBatchResult({
|
|
986
|
-
succeeded: [
|
|
987
|
-
{
|
|
988
|
-
...productInfo,
|
|
989
|
-
case_id: caseId,
|
|
990
|
-
uuid: extracted?.uuid ?? caseId,
|
|
991
|
-
},
|
|
992
|
-
],
|
|
993
|
-
failed: [],
|
|
668
|
+
setSubmittedCase({
|
|
669
|
+
case_id: caseId,
|
|
670
|
+
uuid: extracted?.uuid ?? caseId,
|
|
671
|
+
product_name: anchorGroup?.product_name ?? '',
|
|
672
|
+
product_uuid: anchorGroup?.product_uuid ?? '',
|
|
994
673
|
});
|
|
995
674
|
setIsSubmitted(true);
|
|
996
675
|
toast.success(t('requestSubmittedSuccess'));
|
|
997
676
|
|
|
998
677
|
if (isPopup) {
|
|
999
678
|
onCreateRequest?.();
|
|
679
|
+
// Only dismiss the popup on success. Closing in `finally`
|
|
680
|
+
// would discard the operator's filled form on a transient
|
|
681
|
+
// failure with no way to retry.
|
|
682
|
+
onClose?.();
|
|
1000
683
|
}
|
|
1001
684
|
} else {
|
|
1002
685
|
toast.error(t('requestSubmissionFailed'));
|
|
@@ -1005,8 +688,7 @@ export default function Form({
|
|
|
1005
688
|
// `createCaseRequest` returns null on non-auth errors (per its
|
|
1006
689
|
// documented contract) — those land in the `else` branch above
|
|
1007
690
|
// with a generic toast. This catch only fires on unexpected
|
|
1008
|
-
// exceptions
|
|
1009
|
-
// message the error carries.
|
|
691
|
+
// exceptions; surface whatever message the error carries.
|
|
1010
692
|
console.error('Error submitting request:', error);
|
|
1011
693
|
const message =
|
|
1012
694
|
error instanceof Error && error.message
|
|
@@ -1015,363 +697,68 @@ export default function Form({
|
|
|
1015
697
|
toast.error(message);
|
|
1016
698
|
} finally {
|
|
1017
699
|
setIsLoading(false);
|
|
1018
|
-
if (isPopup) {
|
|
1019
|
-
onClose?.();
|
|
1020
|
-
}
|
|
1021
700
|
}
|
|
1022
701
|
};
|
|
1023
702
|
|
|
1024
|
-
/**
|
|
1025
|
-
* Phase 2 of TECH-3712. Single-product POST shared by `submitBatchCases`
|
|
1026
|
-
* and `handleRetry` (P2-SDK-4). Builds the per-group payload (anchors,
|
|
1027
|
-
* purposes, principal) and invokes `createCaseRequest`. The caller is
|
|
1028
|
-
* responsible for catching/aggregating errors — this helper either
|
|
1029
|
-
* resolves with the response or throws.
|
|
1030
|
-
*
|
|
1031
|
-
* Anchors per group:
|
|
1032
|
-
* - `product_uuid` from the group.
|
|
1033
|
-
* - `data_principal_uuid` is set ONLY for nominated cards (the
|
|
1034
|
-
* requestor is acting on someone else's behalf — the nominator's
|
|
1035
|
-
* ConsentUser uuid). For direct cards we omit the field entirely
|
|
1036
|
-
* so the backend defaults it from `requestor_uuid` (derived from
|
|
1037
|
-
* the JWT). `formData.uuid` here is the FormDataOut session uuid,
|
|
1038
|
-
* NOT the requestor's ConsentUser uuid, and would be rejected by
|
|
1039
|
-
* the backend's ConsentUser lookup.
|
|
1040
|
-
*/
|
|
1041
|
-
const submitSingleProductCase = async (
|
|
1042
|
-
group: FormProductGroup,
|
|
1043
|
-
revokeConsent: boolean,
|
|
1044
|
-
): Promise<CreateCaseResponse | null> => {
|
|
1045
|
-
const groupKey = productGroupKey(group);
|
|
1046
|
-
|
|
1047
|
-
// ACCESS/ERASURE per-group purposes. Empty for CORRECTION — that path
|
|
1048
|
-
// populates `correction_data` below instead.
|
|
1049
|
-
const groupPurposes = purposesByGroupKey.get(groupKey) ?? [];
|
|
1050
|
-
const purposesPayload: UpdatePurpose[] =
|
|
1051
|
-
requestType === RequestTypeEnum.CORRECTION
|
|
1052
|
-
? []
|
|
1053
|
-
: groupPurposes
|
|
1054
|
-
.filter(
|
|
1055
|
-
(p) =>
|
|
1056
|
-
p.selected || p.data_elements.some((de) => de.selected),
|
|
1057
|
-
)
|
|
1058
|
-
.map((p) => ({
|
|
1059
|
-
purpose_uuid: p.uuid,
|
|
1060
|
-
data_element_uuids: p.data_elements
|
|
1061
|
-
.filter((de) => de.selected)
|
|
1062
|
-
.map((de) => de.uuid),
|
|
1063
|
-
}));
|
|
1064
|
-
|
|
1065
|
-
// TECH-3712 (correction grouped path). Per-group correction items: map
|
|
1066
|
-
// each selected element name to its {current, updated} pair. Names are
|
|
1067
|
-
// reverse-translated back to English via `getEnglishDataElementName`
|
|
1068
|
-
// so the backend always receives the canonical attribute name even
|
|
1069
|
-
// when the UI is showing a translated label.
|
|
1070
|
-
const groupSelectedNames =
|
|
1071
|
-
requestType === RequestTypeEnum.CORRECTION
|
|
1072
|
-
? correctionElementsByGroupKey.get(groupKey) ?? []
|
|
1073
|
-
: [];
|
|
1074
|
-
const groupValues =
|
|
1075
|
-
requestType === RequestTypeEnum.CORRECTION
|
|
1076
|
-
? correctionValuesByGroupKey.get(groupKey) ?? {}
|
|
1077
|
-
: {};
|
|
1078
|
-
const correctionPayload: UpdateCorrectionDataItem[] =
|
|
1079
|
-
requestType === RequestTypeEnum.CORRECTION
|
|
1080
|
-
? groupSelectedNames.map((name) => ({
|
|
1081
|
-
name: getEnglishDataElementName(name),
|
|
1082
|
-
curr_value: groupValues[name]?.current ?? '',
|
|
1083
|
-
new_value: groupValues[name]?.updated ?? '',
|
|
1084
|
-
}))
|
|
1085
|
-
: [];
|
|
1086
|
-
|
|
1087
|
-
const payload: UserDataRequest = {
|
|
1088
|
-
name: ' ',
|
|
1089
|
-
contact: formData.contact,
|
|
1090
|
-
request_type: requestType?.toLowerCase() as RequestTypeEnum,
|
|
1091
|
-
uuid: formData.uuid,
|
|
1092
|
-
supporting_docs_uuids: [...uploadedDocumentId],
|
|
1093
|
-
request_details: {
|
|
1094
|
-
purposes: purposesPayload,
|
|
1095
|
-
correction_data: correctionPayload,
|
|
1096
|
-
grievance_types: [],
|
|
1097
|
-
},
|
|
1098
|
-
time_period:
|
|
1099
|
-
requestType === RequestTypeEnum.ACCESS
|
|
1100
|
-
? parseInt(timePeriod || '30', 10)
|
|
1101
|
-
: 0,
|
|
1102
|
-
requestor_note: formData.additionalNote || '',
|
|
1103
|
-
...(requestType === RequestTypeEnum.ERASURE
|
|
1104
|
-
? { revoke_consent_on_fulfilment: revokeConsent }
|
|
1105
|
-
: {}),
|
|
1106
|
-
product_uuid: group.product_uuid,
|
|
1107
|
-
// Only set data_principal_uuid for nominee-driven cases. Direct
|
|
1108
|
-
// cards default server-side to the JWT-derived requestor_uuid.
|
|
1109
|
-
...(group.nominator
|
|
1110
|
-
? { data_principal_uuid: group.nominator.uuid }
|
|
1111
|
-
: {}),
|
|
1112
|
-
};
|
|
1113
|
-
|
|
1114
|
-
// Use the "unsafe" variant in the batch path so non-auth errors throw
|
|
1115
|
-
// instead of being swallowed to null — `Promise.allSettled` upstream
|
|
1116
|
-
// catches them and surfaces the backend message (e.g. `[400]
|
|
1117
|
-
// invalid_data_principal`) in `failed[].error` for that row.
|
|
1118
|
-
return await createCaseRequestUnsafe(
|
|
1119
|
-
organizationId || '',
|
|
1120
|
-
workspaceId || '',
|
|
1121
|
-
payload,
|
|
1122
|
-
isPopup,
|
|
1123
|
-
isCaseManagementContext,
|
|
1124
|
-
authContext,
|
|
1125
|
-
consentClient,
|
|
1126
|
-
);
|
|
1127
|
-
};
|
|
1128
|
-
|
|
1129
|
-
/**
|
|
1130
|
-
* Phase 2 of TECH-3712. Fan one `createCaseRequestUnsafe` per selected
|
|
1131
|
-
* product group via `Promise.allSettled`, aggregate into
|
|
1132
|
-
* `BatchCaseSubmitResult`.
|
|
1133
|
-
*/
|
|
1134
|
-
const submitBatchCases = async (
|
|
1135
|
-
revokeConsent: boolean,
|
|
1136
|
-
): Promise<BatchCaseSubmitResult> => {
|
|
1137
|
-
const submissions = selectedGroups.map((group) =>
|
|
1138
|
-
submitSingleProductCase(group, revokeConsent).then(
|
|
1139
|
-
(response) => ({ group, response }),
|
|
1140
|
-
),
|
|
1141
|
-
);
|
|
1142
|
-
|
|
1143
|
-
const results = await Promise.allSettled(submissions);
|
|
1144
|
-
const batchResult: BatchCaseSubmitResult = { succeeded: [], failed: [] };
|
|
1145
|
-
|
|
1146
|
-
results.forEach((result, idx) => {
|
|
1147
|
-
const group = selectedGroups[idx];
|
|
1148
|
-
const groupKey = productGroupKey(group);
|
|
1149
|
-
if (result.status === 'fulfilled') {
|
|
1150
|
-
const extracted = extractCaseId(result.value.response);
|
|
1151
|
-
if (extracted && (extracted.case_id || extracted.uuid)) {
|
|
1152
|
-
batchResult.succeeded.push({
|
|
1153
|
-
group_key: groupKey,
|
|
1154
|
-
product_uuid: group.product_uuid,
|
|
1155
|
-
product_name: group.product_name,
|
|
1156
|
-
case_id: extracted.case_id || extracted.uuid,
|
|
1157
|
-
uuid: extracted.uuid || extracted.case_id,
|
|
1158
|
-
});
|
|
1159
|
-
} else {
|
|
1160
|
-
batchResult.failed.push({
|
|
1161
|
-
group_key: groupKey,
|
|
1162
|
-
product_uuid: group.product_uuid,
|
|
1163
|
-
product_name: group.product_name,
|
|
1164
|
-
error: t('requestSubmissionFailed'),
|
|
1165
|
-
});
|
|
1166
|
-
}
|
|
1167
|
-
} else {
|
|
1168
|
-
const reason = result.reason;
|
|
1169
|
-
const message =
|
|
1170
|
-
reason instanceof Error
|
|
1171
|
-
? reason.message
|
|
1172
|
-
: typeof reason === 'string'
|
|
1173
|
-
? reason
|
|
1174
|
-
: t('submissionFailed');
|
|
1175
|
-
batchResult.failed.push({
|
|
1176
|
-
group_key: groupKey,
|
|
1177
|
-
product_uuid: group.product_uuid,
|
|
1178
|
-
product_name: group.product_name,
|
|
1179
|
-
error: message,
|
|
1180
|
-
});
|
|
1181
|
-
}
|
|
1182
|
-
});
|
|
1183
|
-
|
|
1184
|
-
return batchResult;
|
|
1185
|
-
};
|
|
1186
|
-
|
|
1187
|
-
/**
|
|
1188
|
-
* P2-SDK-4 of TECH-3712. Re-issue a single POST for one product-group
|
|
1189
|
-
* that previously failed in the batch. On success, promote the entry
|
|
1190
|
-
* from `batchResult.failed` to `batchResult.succeeded` so the UI can
|
|
1191
|
-
* update in place. On failure, refresh the recorded error message so
|
|
1192
|
-
* the user can try again.
|
|
1193
|
-
*
|
|
1194
|
-
* Keyed by `productGroupKey(group)` — NOT `product_uuid` — so that a
|
|
1195
|
-
* product picked both directly AND via a nominator (two distinct
|
|
1196
|
-
* groups with the same `product_uuid`) retries the right entry. No-op
|
|
1197
|
-
* for the legacy single-case path (which has no group context to
|
|
1198
|
-
* retry against — failures there don't land on this surface; they
|
|
1199
|
-
* short-circuit before `setIsSubmitted`).
|
|
1200
|
-
*/
|
|
1201
|
-
const handleRetry = useCallback(
|
|
1202
|
-
async (groupKey: string) => {
|
|
1203
|
-
if (groupKey === '') {
|
|
1204
|
-
return;
|
|
1205
|
-
}
|
|
1206
|
-
const group = selectedGroups.find(
|
|
1207
|
-
(g) => productGroupKey(g) === groupKey,
|
|
1208
|
-
);
|
|
1209
|
-
if (!group) {
|
|
1210
|
-
return;
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
setRetryingGroupKeys((prev) => {
|
|
1214
|
-
const next = new Set(prev);
|
|
1215
|
-
next.add(groupKey);
|
|
1216
|
-
return next;
|
|
1217
|
-
});
|
|
1218
|
-
|
|
1219
|
-
try {
|
|
1220
|
-
const response = await submitSingleProductCase(group, false);
|
|
1221
|
-
const extracted = extractCaseId(response);
|
|
1222
|
-
if (extracted && (extracted.case_id || extracted.uuid)) {
|
|
1223
|
-
queryClient.invalidateQueries({ queryKey: ['caseHistory'] });
|
|
1224
|
-
setBatchResult((prev) => {
|
|
1225
|
-
if (!prev) {
|
|
1226
|
-
return prev;
|
|
1227
|
-
}
|
|
1228
|
-
return {
|
|
1229
|
-
succeeded: [
|
|
1230
|
-
...prev.succeeded,
|
|
1231
|
-
{
|
|
1232
|
-
group_key: groupKey,
|
|
1233
|
-
product_uuid: group.product_uuid,
|
|
1234
|
-
product_name: group.product_name,
|
|
1235
|
-
case_id: extracted.case_id || extracted.uuid,
|
|
1236
|
-
uuid: extracted.uuid || extracted.case_id,
|
|
1237
|
-
},
|
|
1238
|
-
],
|
|
1239
|
-
failed: prev.failed.filter((f) => f.group_key !== groupKey),
|
|
1240
|
-
};
|
|
1241
|
-
});
|
|
1242
|
-
toast.success(t('requestSubmittedSuccess'));
|
|
1243
|
-
} else {
|
|
1244
|
-
setBatchResult((prev) => {
|
|
1245
|
-
if (!prev) {
|
|
1246
|
-
return prev;
|
|
1247
|
-
}
|
|
1248
|
-
return {
|
|
1249
|
-
succeeded: prev.succeeded,
|
|
1250
|
-
failed: prev.failed.map((f) =>
|
|
1251
|
-
f.group_key === groupKey
|
|
1252
|
-
? { ...f, error: t('requestSubmissionFailed') }
|
|
1253
|
-
: f,
|
|
1254
|
-
),
|
|
1255
|
-
};
|
|
1256
|
-
});
|
|
1257
|
-
}
|
|
1258
|
-
} catch (error) {
|
|
1259
|
-
const message =
|
|
1260
|
-
error instanceof Error ? error.message : t('submissionFailed');
|
|
1261
|
-
setBatchResult((prev) => {
|
|
1262
|
-
if (!prev) {
|
|
1263
|
-
return prev;
|
|
1264
|
-
}
|
|
1265
|
-
return {
|
|
1266
|
-
succeeded: prev.succeeded,
|
|
1267
|
-
failed: prev.failed.map((f) =>
|
|
1268
|
-
f.group_key === groupKey ? { ...f, error: message } : f,
|
|
1269
|
-
),
|
|
1270
|
-
};
|
|
1271
|
-
});
|
|
1272
|
-
} finally {
|
|
1273
|
-
setRetryingGroupKeys((prev) => {
|
|
1274
|
-
const next = new Set(prev);
|
|
1275
|
-
next.delete(groupKey);
|
|
1276
|
-
return next;
|
|
1277
|
-
});
|
|
1278
|
-
}
|
|
1279
|
-
},
|
|
1280
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1281
|
-
[
|
|
1282
|
-
selectedGroups,
|
|
1283
|
-
queryClient,
|
|
1284
|
-
t,
|
|
1285
|
-
// submitSingleProductCase closes over many form-state values; we
|
|
1286
|
-
// intentionally don't list it to avoid identity churn re-rendering
|
|
1287
|
-
// RequestSubmit while a retry is in flight.
|
|
1288
|
-
],
|
|
1289
|
-
);
|
|
1290
|
-
|
|
1291
703
|
const isRequestDetailsValid = useCallback(() => {
|
|
1292
704
|
if (!requestType) {
|
|
1293
705
|
return false;
|
|
1294
706
|
}
|
|
1295
707
|
|
|
1296
|
-
//
|
|
1297
|
-
// selected
|
|
1298
|
-
//
|
|
1299
|
-
//
|
|
1300
|
-
//
|
|
1301
|
-
//
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
708
|
+
// Product-anchor validity gate: ACCESS/ERASURE/CORRECTION require a
|
|
709
|
+
// selected product before submit is allowed. The backend leaves
|
|
710
|
+
// `product_uuid` Optional for back-compat, so we enforce the anchor
|
|
711
|
+
// on the client — a product-scoped case without an anchor would
|
|
712
|
+
// land as an org-level case with no scoping, which violates the
|
|
713
|
+
// 1-form -> 1-case -> 1-product contract.
|
|
714
|
+
if (isProductScopedRequestType(requestType)) {
|
|
715
|
+
if (allGroups.length === 0) {
|
|
716
|
+
// No consent groups for this DP at all — the picker shows its
|
|
717
|
+
// empty state and we block submit. The DP has nothing they can
|
|
718
|
+
// raise a product-scoped case against.
|
|
1305
719
|
return false;
|
|
1306
720
|
}
|
|
1307
|
-
if (
|
|
1308
|
-
//
|
|
1309
|
-
//
|
|
1310
|
-
//
|
|
1311
|
-
return
|
|
1312
|
-
const key = productGroupKey(group);
|
|
1313
|
-
const selected = correctionElementsByGroupKey.get(key) ?? [];
|
|
1314
|
-
const values = correctionValuesByGroupKey.get(key) ?? {};
|
|
1315
|
-
return (
|
|
1316
|
-
selected.length > 0 &&
|
|
1317
|
-
selected.every(
|
|
1318
|
-
(name) =>
|
|
1319
|
-
values[name]?.current?.trim() &&
|
|
1320
|
-
values[name]?.updated?.trim(),
|
|
1321
|
-
)
|
|
1322
|
-
);
|
|
1323
|
-
});
|
|
721
|
+
if (!selectedGroup) {
|
|
722
|
+
// Groups exist but the user hasn't picked one. With the
|
|
723
|
+
// single-group auto-select effect this should be rare in
|
|
724
|
+
// practice, but the gate must still reject.
|
|
725
|
+
return false;
|
|
1324
726
|
}
|
|
1325
|
-
|
|
1326
|
-
const purposes = purposesByGroupKey.get(productGroupKey(group)) ?? [];
|
|
1327
|
-
return purposes.some(
|
|
1328
|
-
(p) => p.selected || p.data_elements.some((de) => de.selected),
|
|
1329
|
-
);
|
|
1330
|
-
});
|
|
1331
|
-
}
|
|
1332
|
-
|
|
1333
|
-
switch (requestType) {
|
|
1334
|
-
case RequestTypeEnum.ACCESS:
|
|
1335
|
-
return accessPurposes.some(
|
|
1336
|
-
(p) => p.selected || p.data_elements.some((de) => de.selected)
|
|
1337
|
-
);
|
|
1338
|
-
|
|
1339
|
-
case RequestTypeEnum.ERASURE:
|
|
1340
|
-
return erasurePurposes.some(
|
|
1341
|
-
(p) => p.selected || p.data_elements.some((de) => de.selected)
|
|
1342
|
-
);
|
|
1343
|
-
|
|
1344
|
-
case RequestTypeEnum.GRIEVANCE:
|
|
1345
|
-
return Object.values(selectedGrievances).some(Boolean);
|
|
1346
|
-
|
|
1347
|
-
case RequestTypeEnum.CORRECTION:
|
|
727
|
+
if (requestType === RequestTypeEnum.CORRECTION) {
|
|
1348
728
|
return (
|
|
1349
729
|
selectedCorrectionElements.length > 0 &&
|
|
1350
730
|
selectedCorrectionElements.every(
|
|
1351
731
|
(name) =>
|
|
1352
732
|
correctionValues[name]?.current?.trim() &&
|
|
1353
|
-
correctionValues[name]?.updated?.trim()
|
|
733
|
+
correctionValues[name]?.updated?.trim(),
|
|
1354
734
|
)
|
|
1355
735
|
);
|
|
736
|
+
}
|
|
737
|
+
return selectedPurposes.some(
|
|
738
|
+
(p) => p.selected || p.data_elements.some((de) => de.selected),
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
switch (requestType) {
|
|
743
|
+
case RequestTypeEnum.GRIEVANCE:
|
|
744
|
+
return Object.values(selectedGrievances).some(Boolean);
|
|
1356
745
|
|
|
1357
746
|
case RequestTypeEnum.NOMINATION:
|
|
1358
|
-
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(
|
|
747
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(
|
|
748
|
+
nominationData.nominee_email.trim(),
|
|
749
|
+
);
|
|
1359
750
|
|
|
1360
751
|
default:
|
|
1361
752
|
return false;
|
|
1362
753
|
}
|
|
1363
754
|
}, [
|
|
1364
755
|
requestType,
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
correctionElementsByGroupKey,
|
|
1369
|
-
correctionValuesByGroupKey,
|
|
1370
|
-
accessPurposes,
|
|
1371
|
-
erasurePurposes,
|
|
1372
|
-
selectedGrievances,
|
|
756
|
+
allGroups,
|
|
757
|
+
selectedGroup,
|
|
758
|
+
selectedPurposes,
|
|
1373
759
|
selectedCorrectionElements,
|
|
1374
760
|
correctionValues,
|
|
761
|
+
selectedGrievances,
|
|
1375
762
|
nominationData,
|
|
1376
763
|
]);
|
|
1377
764
|
|
|
@@ -1567,13 +954,11 @@ export default function Form({
|
|
|
1567
954
|
|
|
1568
955
|
return (
|
|
1569
956
|
<>
|
|
1570
|
-
{isSubmitted &&
|
|
957
|
+
{isSubmitted && submittedCase && !isPopup ? (
|
|
1571
958
|
<RequestSubmit
|
|
1572
|
-
result={
|
|
959
|
+
result={submittedCase}
|
|
1573
960
|
contact={formData?.contact}
|
|
1574
961
|
onCreateRequest={onCreateRequest}
|
|
1575
|
-
onRetry={handleRetry}
|
|
1576
|
-
retryingGroupKeys={retryingGroupKeys}
|
|
1577
962
|
/>
|
|
1578
963
|
) : (
|
|
1579
964
|
<div>
|
|
@@ -1777,7 +1162,7 @@ export default function Form({
|
|
|
1777
1162
|
<div style={{
|
|
1778
1163
|
padding: getVar('--pc-spacing-md'),
|
|
1779
1164
|
}}>
|
|
1780
|
-
{
|
|
1165
|
+
{isProductScopedRequestType(requestType) ? (
|
|
1781
1166
|
<div
|
|
1782
1167
|
style={{
|
|
1783
1168
|
...pcStyles.flex,
|
|
@@ -1795,22 +1180,16 @@ export default function Form({
|
|
|
1795
1180
|
isTranslating={isTranslating}
|
|
1796
1181
|
/>
|
|
1797
1182
|
)}
|
|
1798
|
-
{
|
|
1799
|
-
const
|
|
1800
|
-
const key = productGroupKey(group);
|
|
1801
|
-
const groupPurposes = purposesByGroupKey.get(key) ?? [];
|
|
1183
|
+
{selectedGroup && (() => {
|
|
1184
|
+
const key = productGroupKey(selectedGroup);
|
|
1802
1185
|
const isCorrection =
|
|
1803
1186
|
requestType === RequestTypeEnum.CORRECTION;
|
|
1804
1187
|
const groupDataElements = isCorrection
|
|
1805
|
-
? buildGroupDataElements(
|
|
1188
|
+
? buildGroupDataElements(selectedGroup)
|
|
1806
1189
|
: [];
|
|
1807
|
-
const
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
correctionValuesByGroupKey.get(key) ?? {};
|
|
1811
|
-
const nominatorLabel = group.nominator
|
|
1812
|
-
? group.nominator.name ??
|
|
1813
|
-
group.nominator.email ??
|
|
1190
|
+
const nominatorLabel = selectedGroup.nominator
|
|
1191
|
+
? selectedGroup.nominator.name ??
|
|
1192
|
+
selectedGroup.nominator.email ??
|
|
1814
1193
|
''
|
|
1815
1194
|
: '';
|
|
1816
1195
|
|
|
@@ -1828,9 +1207,9 @@ export default function Form({
|
|
|
1828
1207
|
color="primary"
|
|
1829
1208
|
className="pc-product-purpose-panel-title"
|
|
1830
1209
|
>
|
|
1831
|
-
{
|
|
1210
|
+
{selectedGroup.product_name}
|
|
1832
1211
|
</PCText>
|
|
1833
|
-
{
|
|
1212
|
+
{selectedGroup.nominator && (
|
|
1834
1213
|
<PCText
|
|
1835
1214
|
as="span"
|
|
1836
1215
|
size="xs"
|
|
@@ -1860,22 +1239,16 @@ export default function Form({
|
|
|
1860
1239
|
{isCorrection ? (
|
|
1861
1240
|
<CorrectionDataSection
|
|
1862
1241
|
dataElements={groupDataElements}
|
|
1863
|
-
selected={
|
|
1864
|
-
setSelected={
|
|
1865
|
-
|
|
1866
|
-
}
|
|
1867
|
-
correctionValues={groupCorrectionValues}
|
|
1868
|
-
setCorrectionValues={(values) =>
|
|
1869
|
-
setCorrectionValuesForGroup(key, values)
|
|
1870
|
-
}
|
|
1242
|
+
selected={selectedCorrectionElements}
|
|
1243
|
+
setSelected={setSelectedCorrectionElements}
|
|
1244
|
+
correctionValues={correctionValues}
|
|
1245
|
+
setCorrectionValues={setCorrectionValues}
|
|
1871
1246
|
isTranslating={isTranslating}
|
|
1872
1247
|
/>
|
|
1873
1248
|
) : (
|
|
1874
1249
|
<PurposeSelectionSection
|
|
1875
|
-
purposes={
|
|
1876
|
-
setPurposes={
|
|
1877
|
-
setPurposesForGroup(key, updated)
|
|
1878
|
-
}
|
|
1250
|
+
purposes={selectedPurposes}
|
|
1251
|
+
setPurposes={setSelectedPurposes}
|
|
1879
1252
|
requestType={requestType}
|
|
1880
1253
|
isTranslating={isTranslating}
|
|
1881
1254
|
/>
|
|
@@ -1885,31 +1258,6 @@ export default function Form({
|
|
|
1885
1258
|
);
|
|
1886
1259
|
})()}
|
|
1887
1260
|
</div>
|
|
1888
|
-
) : requestType === RequestTypeEnum.ACCESS ||
|
|
1889
|
-
requestType === RequestTypeEnum.ERASURE ? (
|
|
1890
|
-
<PurposeSelectionSection
|
|
1891
|
-
purposes={
|
|
1892
|
-
requestType === RequestTypeEnum.ACCESS
|
|
1893
|
-
? accessPurposes
|
|
1894
|
-
: erasurePurposes
|
|
1895
|
-
}
|
|
1896
|
-
setPurposes={
|
|
1897
|
-
requestType === RequestTypeEnum.ACCESS
|
|
1898
|
-
? setAccessPurposes
|
|
1899
|
-
: setErasurePurposes
|
|
1900
|
-
}
|
|
1901
|
-
requestType={requestType}
|
|
1902
|
-
isTranslating={isTranslating}
|
|
1903
|
-
/>
|
|
1904
|
-
) : requestType === RequestTypeEnum.CORRECTION ? (
|
|
1905
|
-
<CorrectionDataSection
|
|
1906
|
-
dataElements={uniqueDataElements}
|
|
1907
|
-
selected={selectedCorrectionElements}
|
|
1908
|
-
setSelected={setSelectedCorrectionElements}
|
|
1909
|
-
correctionValues={correctionValues}
|
|
1910
|
-
setCorrectionValues={setCorrectionValues}
|
|
1911
|
-
isTranslating={isTranslating}
|
|
1912
|
-
/>
|
|
1913
1261
|
) : requestType === RequestTypeEnum.GRIEVANCE ? (
|
|
1914
1262
|
<GrievanceDataSection
|
|
1915
1263
|
grievanceOptions={grievanceOptions}
|