@redacto.io/consent-sdk-react 10.0.0-beta.11 → 10.0.0-beta.12

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 (259) hide show
  1. package/NOTICE.md +17 -0
  2. package/README.md +67 -3
  3. package/dist/chunk-FEGTW73Q.mjs +182 -0
  4. package/dist/fonts.d.mts +35 -0
  5. package/dist/fonts.d.ts +35 -0
  6. package/dist/fonts.js +183 -0
  7. package/dist/fonts.mjs +10 -0
  8. package/dist/index.d.mts +12 -1
  9. package/dist/index.d.ts +12 -1
  10. package/dist/index.js +613 -249
  11. package/dist/index.mjs +235 -85
  12. package/dist/montserrat.generated-NBTECYRW.mjs +22 -0
  13. package/dist/types-6Aj7faxL.d.mts +57 -0
  14. package/dist/types-6Aj7faxL.d.ts +57 -0
  15. package/package.json +10 -1
  16. package/.turbo/turbo-build.log +0 -22
  17. package/.turbo/turbo-test.log +0 -203
  18. package/.turbo/turbo-verify-pack.log +0 -6
  19. package/CHANGELOG.md +0 -383
  20. package/src/RedactoNoticeAssisted/RedactoNoticeAssisted.test.tsx +0 -934
  21. package/src/RedactoNoticeAssisted/RedactoNoticeAssisted.tsx +0 -2160
  22. package/src/RedactoNoticeAssisted/api/index.ts +0 -275
  23. package/src/RedactoNoticeAssisted/api/types.ts +0 -182
  24. package/src/RedactoNoticeAssisted/assets/redacto-logo.png +0 -0
  25. package/src/RedactoNoticeAssisted/constants.ts +0 -51
  26. package/src/RedactoNoticeAssisted/i18n.ts +0 -1408
  27. package/src/RedactoNoticeAssisted/index.ts +0 -1
  28. package/src/RedactoNoticeAssisted/injectStyles.ts +0 -251
  29. package/src/RedactoNoticeAssisted/styles.ts +0 -730
  30. package/src/RedactoNoticeAssisted/types.ts +0 -26
  31. package/src/RedactoNoticeAssisted/useMediaQuery.ts +0 -36
  32. package/src/RedactoNoticeConsent/RedactoNoticeConsent.test.tsx +0 -1186
  33. package/src/RedactoNoticeConsent/RedactoNoticeConsent.tsx +0 -4299
  34. package/src/RedactoNoticeConsent/api/index.ts +0 -486
  35. package/src/RedactoNoticeConsent/api/sandbox.ts +0 -150
  36. package/src/RedactoNoticeConsent/api/types.ts +0 -505
  37. package/src/RedactoNoticeConsent/assets/redacto-logo.png +0 -0
  38. package/src/RedactoNoticeConsent/components/DigiLockerCallback.tsx +0 -602
  39. package/src/RedactoNoticeConsent/components/index.ts +0 -7
  40. package/src/RedactoNoticeConsent/constants.ts +0 -20
  41. package/src/RedactoNoticeConsent/index.ts +0 -1
  42. package/src/RedactoNoticeConsent/injectStyles.ts +0 -151
  43. package/src/RedactoNoticeConsent/language-codes.ts +0 -95
  44. package/src/RedactoNoticeConsent/services/storage.ts +0 -122
  45. package/src/RedactoNoticeConsent/styles.ts +0 -409
  46. package/src/RedactoNoticeConsent/types.ts +0 -80
  47. package/src/RedactoNoticeConsent/useMediaQuery.ts +0 -36
  48. package/src/RedactoNoticeConsentInline/RedactoNoticeConsentInline.test.tsx +0 -369
  49. package/src/RedactoNoticeConsentInline/RedactoNoticeConsentInline.tsx +0 -709
  50. package/src/RedactoNoticeConsentInline/api/index.ts +0 -115
  51. package/src/RedactoNoticeConsentInline/api/notice-read-routing.test.ts +0 -72
  52. package/src/RedactoNoticeConsentInline/api/types.ts +0 -185
  53. package/src/RedactoNoticeConsentInline/assets/redacto-logo.png +0 -0
  54. package/src/RedactoNoticeConsentInline/index.ts +0 -1
  55. package/src/RedactoNoticeConsentInline/injectStyles.ts +0 -40
  56. package/src/RedactoNoticeConsentInline/styles.ts +0 -397
  57. package/src/RedactoNoticeConsentInline/types.ts +0 -51
  58. package/src/RedactoNoticeConsentInline/useMediaQuery.ts +0 -36
  59. package/src/RedactoPrivacyCenter/NotificationsPage.tsx +0 -182
  60. package/src/RedactoPrivacyCenter/PrivacyCenterActivityPage.tsx +0 -19
  61. package/src/RedactoPrivacyCenter/PrivacyCenterCaseDetailsPage.tsx +0 -156
  62. package/src/RedactoPrivacyCenter/PrivacyCenterConsentManagerPage.tsx +0 -17
  63. package/src/RedactoPrivacyCenter/PrivacyCenterFormPage.tsx +0 -79
  64. package/src/RedactoPrivacyCenter/PrivacyCenterHomePage.tsx +0 -36
  65. package/src/RedactoPrivacyCenter/PrivacyCenterLayout.tsx +0 -177
  66. package/src/RedactoPrivacyCenter/PrivacyCenterMyReceiptPage.tsx +0 -26
  67. package/src/RedactoPrivacyCenter/RedactoPrivacyCenter.tsx +0 -93
  68. package/src/RedactoPrivacyCenter/api/actions.ts +0 -682
  69. package/src/RedactoPrivacyCenter/api/client.ts +0 -1003
  70. package/src/RedactoPrivacyCenter/api/config.ts +0 -33
  71. package/src/RedactoPrivacyCenter/api/fetcher.ts +0 -275
  72. package/src/RedactoPrivacyCenter/api/orgBranding.ts +0 -16
  73. package/src/RedactoPrivacyCenter/api/upload.ts +0 -93
  74. package/src/RedactoPrivacyCenter/assets/character.svg +0 -31
  75. package/src/RedactoPrivacyCenter/assets/fileAttach.svg +0 -9
  76. package/src/RedactoPrivacyCenter/assets/greenTick.svg +0 -4
  77. package/src/RedactoPrivacyCenter/assets/mail.svg +0 -3
  78. package/src/RedactoPrivacyCenter/assets/poweredByRedacto.svg +0 -9
  79. package/src/RedactoPrivacyCenter/assets/sentImg.svg +0 -9
  80. package/src/RedactoPrivacyCenter/assets/uploadFileIcon.svg +0 -9
  81. package/src/RedactoPrivacyCenter/components/ActivitiesList.tsx +0 -252
  82. package/src/RedactoPrivacyCenter/components/ActivityTimeline.tsx +0 -239
  83. package/src/RedactoPrivacyCenter/components/AppShell/AppShell.tsx +0 -268
  84. package/src/RedactoPrivacyCenter/components/AppShell/MobileNavDrawer.tsx +0 -218
  85. package/src/RedactoPrivacyCenter/components/AppShell/NavItem.tsx +0 -205
  86. package/src/RedactoPrivacyCenter/components/AppShell/PageHeader.tsx +0 -107
  87. package/src/RedactoPrivacyCenter/components/AppShell/SidebarNav.tsx +0 -100
  88. package/src/RedactoPrivacyCenter/components/AppShell/TopAppBar.tsx +0 -339
  89. package/src/RedactoPrivacyCenter/components/AppShell/UserMenu.tsx +0 -245
  90. package/src/RedactoPrivacyCenter/components/AppShell/hooks/useIsTruncated.ts +0 -28
  91. package/src/RedactoPrivacyCenter/components/AppShell/hooks/useKeyboardShortcut.ts +0 -40
  92. package/src/RedactoPrivacyCenter/components/AppShell/hooks/useSidebarState.ts +0 -62
  93. package/src/RedactoPrivacyCenter/components/AppShell/usePrivacyCenterExitAction.ts +0 -36
  94. package/src/RedactoPrivacyCenter/components/CaseDetails/AttachmentCard.tsx +0 -38
  95. package/src/RedactoPrivacyCenter/components/CaseDetails/ChatBubble.tsx +0 -31
  96. package/src/RedactoPrivacyCenter/components/CaseDetails/DocumentRequestCard.tsx +0 -79
  97. package/src/RedactoPrivacyCenter/components/CaseDetails/FinalizedTooltipWrapper.tsx +0 -25
  98. package/src/RedactoPrivacyCenter/components/CaseDetails/MessageComposer.tsx +0 -96
  99. package/src/RedactoPrivacyCenter/components/CaseDetails/MessageItem.tsx +0 -97
  100. package/src/RedactoPrivacyCenter/components/CaseDetails/MessagesTab.tsx +0 -156
  101. package/src/RedactoPrivacyCenter/components/CaseDetails/RequestDetailsTab.tsx +0 -171
  102. package/src/RedactoPrivacyCenter/components/CaseDetails/StatusEvent.tsx +0 -23
  103. package/src/RedactoPrivacyCenter/components/CaseDetails/useFileUpload.ts +0 -116
  104. package/src/RedactoPrivacyCenter/components/CaseDetails/useMessagePolling.ts +0 -43
  105. package/src/RedactoPrivacyCenter/components/CaseDetails/utils.ts +0 -71
  106. package/src/RedactoPrivacyCenter/components/CaseDetailsModal.tsx +0 -469
  107. package/src/RedactoPrivacyCenter/components/CaseHistory.tsx +0 -426
  108. package/src/RedactoPrivacyCenter/components/ConsentManager.tsx +0 -1199
  109. package/src/RedactoPrivacyCenter/components/ConsentManagerCells.tsx +0 -201
  110. package/src/RedactoPrivacyCenter/components/ConsentManagerColumns.tsx +0 -138
  111. package/src/RedactoPrivacyCenter/components/ConsentManagerConstants.ts +0 -7
  112. package/src/RedactoPrivacyCenter/components/ConsentManagerExpandedRow.tsx +0 -179
  113. package/src/RedactoPrivacyCenter/components/ConsentManagerGroupedColumns.tsx +0 -318
  114. package/src/RedactoPrivacyCenter/components/ConsentManagerProductCards.tsx +0 -333
  115. package/src/RedactoPrivacyCenter/components/ConsentManagerPurposeCards.tsx +0 -102
  116. package/src/RedactoPrivacyCenter/components/ConsentManagerTableSkeleton.tsx +0 -159
  117. package/src/RedactoPrivacyCenter/components/DataRequest/CorrectionDataSection.tsx +0 -189
  118. package/src/RedactoPrivacyCenter/components/DataRequest/GrievanceDataSection.tsx +0 -118
  119. package/src/RedactoPrivacyCenter/components/DataRequest/NominationDataSection.tsx +0 -66
  120. package/src/RedactoPrivacyCenter/components/DataRequest/ProductSelectionSection.tsx +0 -222
  121. package/src/RedactoPrivacyCenter/components/DataRequest/PurposeSelectionSection.tsx +0 -331
  122. package/src/RedactoPrivacyCenter/components/Form.tsx +0 -1821
  123. package/src/RedactoPrivacyCenter/components/GrievanceTable.tsx +0 -174
  124. package/src/RedactoPrivacyCenter/components/GrievanceTablePagination.tsx +0 -337
  125. package/src/RedactoPrivacyCenter/components/ModifyConsentModal.tsx +0 -334
  126. package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationBell.tsx +0 -173
  127. package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationCTAButton.tsx +0 -18
  128. package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationDropdown.tsx +0 -54
  129. package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationDropdownItem.tsx +0 -57
  130. package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationIcon.tsx +0 -19
  131. package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationListPanel.tsx +0 -92
  132. package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationRefreshButton.tsx +0 -20
  133. package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationRow.tsx +0 -58
  134. package/src/RedactoPrivacyCenter/components/NotificationHub/StatCard.tsx +0 -10
  135. package/src/RedactoPrivacyCenter/components/NotificationHub/useNotificationActions.ts +0 -112
  136. package/src/RedactoPrivacyCenter/components/NotificationHub/utils.ts +0 -27
  137. package/src/RedactoPrivacyCenter/components/Pagination.tsx +0 -140
  138. package/src/RedactoPrivacyCenter/components/PrivacyCenterEmptyState.tsx +0 -76
  139. package/src/RedactoPrivacyCenter/components/PrivacyCenterSessionExpired.tsx +0 -97
  140. package/src/RedactoPrivacyCenter/components/ReceiptsList.tsx +0 -454
  141. package/src/RedactoPrivacyCenter/components/RequestSubmit.tsx +0 -270
  142. package/src/RedactoPrivacyCenter/components/SelectUser.tsx +0 -95
  143. package/src/RedactoPrivacyCenter/components/SelectUserGate.tsx +0 -195
  144. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/constants.tsx +0 -25
  145. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/types.ts +0 -21
  146. package/src/RedactoPrivacyCenter/components/caseHistoryColumns.tsx +0 -159
  147. package/src/RedactoPrivacyCenter/components/constant.ts +0 -52
  148. package/src/RedactoPrivacyCenter/components/groupHelpers.ts +0 -155
  149. package/src/RedactoPrivacyCenter/components/receiptColumns.tsx +0 -203
  150. package/src/RedactoPrivacyCenter/components/types.ts +0 -123
  151. package/src/RedactoPrivacyCenter/components/utils.tsx +0 -10
  152. package/src/RedactoPrivacyCenter/config/routes.ts +0 -9
  153. package/src/RedactoPrivacyCenter/config/table.ts +0 -25
  154. package/src/RedactoPrivacyCenter/context/AuthContext.tsx +0 -252
  155. package/src/RedactoPrivacyCenter/context/BaseUrlContext.tsx +0 -47
  156. package/src/RedactoPrivacyCenter/context/BrandingContext.tsx +0 -52
  157. package/src/RedactoPrivacyCenter/context/DataAvailabilityContext.session-expired.test.tsx +0 -92
  158. package/src/RedactoPrivacyCenter/context/DataAvailabilityContext.tsx +0 -181
  159. package/src/RedactoPrivacyCenter/context/LocaleContext.tsx +0 -11
  160. package/src/RedactoPrivacyCenter/context/NavigationContext.tsx +0 -96
  161. package/src/RedactoPrivacyCenter/context/OrganizationContext.tsx +0 -30
  162. package/src/RedactoPrivacyCenter/context/PageChromeContext.tsx +0 -80
  163. package/src/RedactoPrivacyCenter/context/ProfileSwitchContext.tsx +0 -15
  164. package/src/RedactoPrivacyCenter/context/types.ts +0 -57
  165. package/src/RedactoPrivacyCenter/lib/api.ts +0 -759
  166. package/src/RedactoPrivacyCenter/lib/consent-manager/utils.tsx +0 -67
  167. package/src/RedactoPrivacyCenter/lib/constants.ts +0 -283
  168. package/src/RedactoPrivacyCenter/lib/i18n.ts +0 -81
  169. package/src/RedactoPrivacyCenter/lib/locales.ts +0 -102
  170. package/src/RedactoPrivacyCenter/lib/queries.ts +0 -141
  171. package/src/RedactoPrivacyCenter/lib/sandbox.ts +0 -104
  172. package/src/RedactoPrivacyCenter/lib/types.ts +0 -892
  173. package/src/RedactoPrivacyCenter/lib/useCurrentLanguage.ts +0 -37
  174. package/src/RedactoPrivacyCenter/lib/utils.refresh-circuit.test.ts +0 -127
  175. package/src/RedactoPrivacyCenter/lib/utils.refresh-storage.test.ts +0 -85
  176. package/src/RedactoPrivacyCenter/lib/utils.session-expired.test.ts +0 -109
  177. package/src/RedactoPrivacyCenter/lib/utils.ts +0 -798
  178. package/src/RedactoPrivacyCenter/locales/as/translation.json +0 -340
  179. package/src/RedactoPrivacyCenter/locales/bn/translation.json +0 -340
  180. package/src/RedactoPrivacyCenter/locales/brx/translation.json +0 -340
  181. package/src/RedactoPrivacyCenter/locales/doi/translation.json +0 -340
  182. package/src/RedactoPrivacyCenter/locales/en/translation.json +0 -355
  183. package/src/RedactoPrivacyCenter/locales/gom/translation.json +0 -340
  184. package/src/RedactoPrivacyCenter/locales/gu/translation.json +0 -340
  185. package/src/RedactoPrivacyCenter/locales/hi/translation.json +0 -343
  186. package/src/RedactoPrivacyCenter/locales/kn/translation.json +0 -340
  187. package/src/RedactoPrivacyCenter/locales/ks/translation.json +0 -340
  188. package/src/RedactoPrivacyCenter/locales/mai/translation.json +0 -340
  189. package/src/RedactoPrivacyCenter/locales/ml/translation.json +0 -340
  190. package/src/RedactoPrivacyCenter/locales/mni-Mtei/translation.json +0 -340
  191. package/src/RedactoPrivacyCenter/locales/mr/translation.json +0 -340
  192. package/src/RedactoPrivacyCenter/locales/ne/translation.json +0 -340
  193. package/src/RedactoPrivacyCenter/locales/or/translation.json +0 -340
  194. package/src/RedactoPrivacyCenter/locales/pa/translation.json +0 -340
  195. package/src/RedactoPrivacyCenter/locales/sa/translation.json +0 -340
  196. package/src/RedactoPrivacyCenter/locales/sat/translation.json +0 -340
  197. package/src/RedactoPrivacyCenter/locales/sd/translation.json +0 -340
  198. package/src/RedactoPrivacyCenter/locales/ta/translation.json +0 -340
  199. package/src/RedactoPrivacyCenter/locales/te/translation.json +0 -340
  200. package/src/RedactoPrivacyCenter/locales/ur/translation.json +0 -340
  201. package/src/RedactoPrivacyCenter/styles/applyBrandingTheme.ts +0 -66
  202. package/src/RedactoPrivacyCenter/styles/base.css +0 -117
  203. package/src/RedactoPrivacyCenter/styles/injectStyles.ts +0 -3108
  204. package/src/RedactoPrivacyCenter/styles/pcStyles.ts +0 -831
  205. package/src/RedactoPrivacyCenter/styles/responsive.ts +0 -138
  206. package/src/RedactoPrivacyCenter/styles/variables.css +0 -96
  207. package/src/RedactoPrivacyCenter/table/AdvancedTable.tsx +0 -387
  208. package/src/RedactoPrivacyCenter/table/AdvancedTablePagination.tsx +0 -468
  209. package/src/RedactoPrivacyCenter/table/index.ts +0 -2
  210. package/src/RedactoPrivacyCenter/types/images.d.ts +0 -29
  211. package/src/RedactoPrivacyCenter/ui/Footer.tsx +0 -69
  212. package/src/RedactoPrivacyCenter/ui/FormBlockWrapper.tsx +0 -54
  213. package/src/RedactoPrivacyCenter/ui/PCAccordion.tsx +0 -93
  214. package/src/RedactoPrivacyCenter/ui/PCActionIcon.tsx +0 -47
  215. package/src/RedactoPrivacyCenter/ui/PCAvatar.test.tsx +0 -83
  216. package/src/RedactoPrivacyCenter/ui/PCAvatar.tsx +0 -93
  217. package/src/RedactoPrivacyCenter/ui/PCBadge.tsx +0 -48
  218. package/src/RedactoPrivacyCenter/ui/PCButton.tsx +0 -77
  219. package/src/RedactoPrivacyCenter/ui/PCCheckbox.tsx +0 -55
  220. package/src/RedactoPrivacyCenter/ui/PCCollapse.tsx +0 -107
  221. package/src/RedactoPrivacyCenter/ui/PCEmpty.tsx +0 -35
  222. package/src/RedactoPrivacyCenter/ui/PCInput.tsx +0 -132
  223. package/src/RedactoPrivacyCenter/ui/PCLoader.tsx +0 -31
  224. package/src/RedactoPrivacyCenter/ui/PCModal.tsx +0 -106
  225. package/src/RedactoPrivacyCenter/ui/PCPopover.tsx +0 -206
  226. package/src/RedactoPrivacyCenter/ui/PCSelect.tsx +0 -433
  227. package/src/RedactoPrivacyCenter/ui/PCTable.tsx +0 -149
  228. package/src/RedactoPrivacyCenter/ui/PCText.tsx +0 -41
  229. package/src/RedactoPrivacyCenter/ui/PCTextarea.tsx +0 -43
  230. package/src/RedactoPrivacyCenter/ui/PCTitle.tsx +0 -37
  231. package/src/RedactoPrivacyCenter/ui/PCTooltip.tsx +0 -121
  232. package/src/RedactoPrivacyCenter/ui/SimpleBar.tsx +0 -25
  233. package/src/RedactoPrivacyCenter/ui/StatusBadge.tsx +0 -53
  234. package/src/RedactoPrivacyCenter/ui/index.ts +0 -18
  235. package/src/RedactoPrivacyCenter/utils/cn.ts +0 -28
  236. package/src/RedactoPrivacyCenter/utils/date.ts +0 -48
  237. package/src/RedactoPrivacyCenter/utils/status.ts +0 -111
  238. package/src/RedactoPrivacyCenter/utils/toast.ts +0 -130
  239. package/src/index.ts +0 -19
  240. package/src/privacy-center.ts +0 -113
  241. package/src/shared/api-errors.ts +0 -180
  242. package/src/shared/notice-consent-status.ts +0 -190
  243. package/src/shared/sandbox.ts +0 -6
  244. package/src/types/css.d.ts +0 -4
  245. package/src/types/images.d.ts +0 -19
  246. package/src/types/react-jsx.d.ts +0 -26
  247. package/tests/DataAvailabilityContext.test.tsx +0 -123
  248. package/tests/Form.test.tsx +0 -1424
  249. package/tests/ProductSelectionSection.test.tsx +0 -328
  250. package/tests/RequestSubmit.test.tsx +0 -135
  251. package/tests/RevokeWarningMessage.test.tsx +0 -151
  252. package/tests/consentClient.test.ts +0 -217
  253. package/tests/manageConsentServer.test.ts +0 -38
  254. package/tests/mocks.ts +0 -257
  255. package/tests/resolveContactFromJwt.test.ts +0 -44
  256. package/tests/setup.ts +0 -16
  257. package/tsconfig.json +0 -16
  258. package/tsup.config.ts +0 -31
  259. package/vitest.config.ts +0 -16
@@ -1,1821 +0,0 @@
1
- import fileAttachIcon from '../assets/fileAttach.svg';
2
- import uploadFileIcon from '../assets/uploadFileIcon.svg';
3
- import { uploadFileWithS2S } from '../api/upload';
4
- import { FormBlockWrapper } from '../ui/FormBlockWrapper';
5
- import { useQueryClient } from '@tanstack/react-query';
6
- import { useEffect, useRef, useState, useMemo, useCallback } from 'react';
7
- import { useTranslation } from 'react-i18next';
8
- import { AiOutlineClose } from 'react-icons/ai';
9
- import { PiUploadBold } from 'react-icons/pi';
10
- import { toast } from '../utils/toast';
11
- import { PCButton, PCCheckbox, PCInput, PCLoader, PCSelect, PCText, PCTextarea } from '../ui';
12
- import { PCModal } from '../ui/PCModal';
13
- import { buildGroupDataElements, buildGroupPurposes, consentGroupToFormProductGroup, legacyFlatGroup } from './groupHelpers';
14
- import { pcStyles } from '../styles/pcStyles';
15
- import { createCaseRequest, fetchUserConsents, getFormData } from '../lib/api';
16
- import { FORM_PICKER_GROUP_LIMIT, LEGACY_PRODUCT_GROUP_UUID } from '../lib/constants';
17
- import { ensureValidToken } from '../lib/utils';
18
- import { useAuth } from '../context/AuthContext';
19
- import { useBaseUrl } from '../context/BaseUrlContext';
20
- import {
21
- useRequestTypeLabels,
22
- useTimePeriodOptions,
23
- } from '../lib/constants';
24
- import { useOrganization } from '../context/OrganizationContext';
25
- import i18n from '../lib/i18n';
26
- import { getLangParam } from '../lib/useCurrentLanguage';
27
- import type {
28
- CreateCaseResponse,
29
- FormData,
30
- FormProductGroup,
31
- GrievanceOptions,
32
- GrievanceTypeElement,
33
- GrievanceValue,
34
- NominationData,
35
- Purpose,
36
- UpdateCorrectionDataItem,
37
- UpdatePurpose,
38
- UserDataRequest,
39
- } from '../lib/types';
40
- import { RequestTypeEnum } from '../lib/types';
41
- import CorrectionDataSection from './DataRequest/CorrectionDataSection';
42
- import GrievanceDataSection from './DataRequest/GrievanceDataSection';
43
- import NominationDataSection from './DataRequest/NominationDataSection';
44
- import {
45
- ProductSelectionSection,
46
- productGroupKey,
47
- } from './DataRequest/ProductSelectionSection';
48
- import PurposeSelectionSection from './DataRequest/PurposeSelectionSection';
49
- import RequestSubmit from './RequestSubmit';
50
- import type { CaseHistoryProps } from './types';
51
- import { FiAlertCircle, FiChevronDown, FiChevronUp, FiX } from 'react-icons/fi';
52
- import { MAX_DISPLAYED_PURPOSE_NAMES_IN_ERASURE_MODAL } from './constant';
53
-
54
- const getVar = (varName: string): string => `var(${varName})`;
55
-
56
- /**
57
- * Narrow shape of the `case_id`/`uuid` fields extracted from a
58
- * `CreateCaseResponse` (or its unwrapped privacy-center variant). Both
59
- * are stringly returned — empty strings mean "not present on the
60
- * response" rather than `null`, which keeps the call sites simple.
61
- */
62
- type ExtractedCaseRef = { uuid: string; case_id: string };
63
-
64
- /**
65
- * The two response shapes `extractCaseId` walks before resolving an
66
- * `ExtractedCaseRef`. Privacy-center returns the unwrapped detail;
67
- * case-management wraps it in `{ code, status, detail }`.
68
- */
69
- type CaseIdContainer = { case_id?: string; uuid?: string };
70
-
71
- /**
72
- * Request types that engage the per-product picker and require a
73
- * product anchor (`product_uuid`) on the submitted case. Nomination
74
- * and grievance are org-level and skip the product step entirely.
75
- */
76
- const PRODUCT_SCOPED_REQUEST_TYPES: ReadonlySet<RequestTypeEnum> = new Set([
77
- RequestTypeEnum.ACCESS,
78
- RequestTypeEnum.ERASURE,
79
- RequestTypeEnum.CORRECTION,
80
- ]);
81
-
82
- const isProductScopedRequestType = (
83
- rt: RequestTypeEnum | null,
84
- ): rt is RequestTypeEnum =>
85
- rt !== null && PRODUCT_SCOPED_REQUEST_TYPES.has(rt);
86
-
87
- /**
88
- * Result shape rendered by RequestSubmit after a successful submission.
89
- * Product fields are populated only for product-scoped request types
90
- * (ACCESS / ERASURE / CORRECTION); nomination and grievance leave them
91
- * as empty strings so the success card omits the product label.
92
- */
93
- type SubmittedCase = {
94
- case_id: string;
95
- uuid: string;
96
- product_name: string;
97
- product_uuid: string;
98
- };
99
-
100
- /**
101
- * Pull the `case_id` out of a `createCaseRequest` response, handling both
102
- * the privacy-center shape (unwrapped detail) and the case-management shape
103
- * (wrapped { code, status, detail }).
104
- */
105
- const extractCaseId = (
106
- response: CreateCaseResponse | null,
107
- ): ExtractedCaseRef | null => {
108
- if (!response) {
109
- return null;
110
- }
111
- if ('detail' in response && response.detail) {
112
- const detail = response.detail as CaseIdContainer;
113
- if (detail.case_id || detail.uuid) {
114
- return { case_id: detail.case_id ?? '', uuid: detail.uuid ?? '' };
115
- }
116
- }
117
- const direct = response as unknown as CaseIdContainer;
118
- if (direct.case_id || direct.uuid) {
119
- return { case_id: direct.case_id ?? '', uuid: direct.uuid ?? '' };
120
- }
121
- return null;
122
- };
123
-
124
- export default function Form({
125
- onCreateRequest,
126
- isPopup = false,
127
- onClose,
128
- }: CaseHistoryProps) {
129
- const queryClient = useQueryClient();
130
- const timePeriodOptions = useTimePeriodOptions();
131
- const { organization } = useOrganization();
132
- const orgSlug = organization?.slug || organization?.name || '';
133
-
134
- // Detect if we're in case management context (admin side)
135
- // Case management uses session auth, not JWT Bearer
136
- // We rely on an explicit pathname check rather than absence of access_token
137
- // to avoid incorrectly flagging unauthenticated privacy-center users as case-management
138
- const isCaseManagementContext =
139
- typeof window !== 'undefined' &&
140
- window.location.pathname.includes('/case-management');
141
- const [requestType, setRequestType] = useState<RequestTypeEnum | null>(null);
142
- const [timePeriod, setTimePeriod] = useState('');
143
- const [isSubmitted, setIsSubmitted] = useState(false);
144
- const { t } = useTranslation();
145
- const { accessToken, refreshToken, onError, updateTokens } = useAuth();
146
- const { consentClient, baseUrl } = useBaseUrl();
147
- const organizationId = organization?.uuid;
148
- const workspaceId = organization?.workspaces[0]?.uuid;
149
-
150
- // Create auth context object for API calls - memoized to prevent infinite loops
151
- const authContext = useMemo(() => ({
152
- accessToken,
153
- refreshToken,
154
- onError,
155
- updateTokens,
156
- }), [accessToken, refreshToken, onError, updateTokens]);
157
- const [data, setData] = useState<FormData | null>(null);
158
- const [formData, setFormData] = useState({
159
- uuid: '',
160
- case_id: '',
161
- name: '',
162
- contact: '',
163
- requestType: '',
164
- requestDetails: '',
165
- supportingDocumentation: '',
166
- additionalNote: '',
167
- confirmCheckBox: false,
168
- });
169
- // Selected product-group key on the product-cards step. Keyed by
170
- // `productGroupKey(group)` so the same product reached both directly
171
- // AND via a nominator stays addressable as two distinct selections.
172
- // null when no product is selected (or for org-level request types).
173
- const [selectedProductKey, setSelectedProductKey] = useState<string | null>(
174
- null,
175
- );
176
- // Purpose state for the currently-selected product group. Seeded from
177
- // `buildGroupPurposes(group)` whenever `selectedProductKey` changes, then
178
- // mutated in place by `PurposeSelectionSection` as the user picks
179
- // purposes / data elements. Single-select means we never need a Map.
180
- const [selectedPurposes, setSelectedPurposes] = useState<Purpose[]>([]);
181
- // CORRECTION selections for the currently-selected product group. These
182
- // are flat (not keyed by product) because the picker is single-select —
183
- // switching products re-seeds them via the same effect that re-seeds
184
- // `selectedPurposes`. Names are the data-element names visible in the
185
- // UI; they're reverse-translated to English at submit time.
186
- const [selectedCorrectionElements, setSelectedCorrectionElements] = useState<
187
- string[]
188
- >([]);
189
- const [correctionValues, setCorrectionValues] = useState<
190
- Record<string, { current: string; updated: string }>
191
- >({});
192
- // Result rendered by `RequestSubmit` after a successful submission.
193
- // Replaces the prior batch-shape now that submit is 1-form / 1-case.
194
- // Product fields are empty strings for nomination / grievance (no
195
- // product anchor) so the success card omits the product label.
196
- const [submittedCase, setSubmittedCase] = useState<SubmittedCase | null>(
197
- null,
198
- );
199
- const fileInputRef = useRef<HTMLInputElement>(null);
200
- // Store the original English form data for submission — translations are display-only
201
- const englishDataRef = useRef<FormData | null>(null);
202
- const [uploadedDocumentId, setUploadedDocumentId] = useState<string[]>([]);
203
- const [uploadedDocuments, setUploadedDocuments] = useState<
204
- { uuid: string; name: string }[]
205
- >([]);
206
- const [isDocumentUploading, setIsDocumentUploading] = useState(false);
207
- const [isDocument, setIsDocument] = useState(false);
208
- const [isPopupFormEnabled, setIsPopupFormEnabled] = useState(!isPopup);
209
- const [isVerifyingContact, setIsVerifyingContact] = useState(false);
210
- const requestTypeLabels = useRequestTypeLabels();
211
- const [grievanceOptions, setGrievanceOptions] = useState<GrievanceOptions[]>(
212
- []
213
- );
214
- const [selectedGrievances, setSelectedGrievances] = useState<
215
- Record<string, boolean>
216
- >({});
217
- const [nominationData, setNominationData] = useState<NominationData>({
218
- nominee_email: '',
219
- nominee_mobile: '',
220
- });
221
- const [isLoading, setIsLoading] = useState(false);
222
- const [isTranslating, setIsTranslating] = useState(false);
223
- const [isRequestDetailsExpanded, setIsRequestDetailsExpanded] =
224
- useState(true);
225
- const [isErasureConsentModalOpen, setIsErasureConsentModalOpen] =
226
- useState(false);
227
- const [revokeConsentOnFulfilment, setRevokeConsentOnFulfilment] =
228
- useState(false);
229
-
230
- const resetFileInput = () => {
231
- if (fileInputRef.current) {
232
- fileInputRef.current.value = '';
233
- }
234
- };
235
-
236
- // Union of direct + nominated product groups exposed by the backend.
237
- // Drives both the product-cards step and the per-product picker beneath.
238
- // When the backend exposes no groups at all but did return the legacy flat
239
- // `purposes` list (pre-grouping Python deployments, and the popup /
240
- // case-management contexts that cannot read user-consents), fall back to a
241
- // single synthetic group so the purposes stay reachable instead of the
242
- // picker claiming the principal has no consents.
243
- const allGroups = useMemo<FormProductGroup[]>(() => {
244
- if (!data) {
245
- return [];
246
- }
247
- const groups = [
248
- ...(data.direct_groups ?? []),
249
- ...(data.nominated_groups ?? []),
250
- ];
251
- if (groups.length > 0) {
252
- return groups;
253
- }
254
- const flatGroup = legacyFlatGroup(data, t('yourData'));
255
- // `buildGroupPurposes` is what the panel renders from — it drops purposes
256
- // with no data elements, so a flat list that survives nothing must keep
257
- // showing the empty state rather than an unusable panel.
258
- if (buildGroupPurposes(flatGroup).length === 0) {
259
- return [];
260
- }
261
- return [flatGroup];
262
- }, [data, t]);
263
-
264
- // TECH-3712 UX: when the DP has exactly one consent group total
265
- // (direct + nominated combined), the product-selection step is friction —
266
- // there's nothing to choose. Skip rendering ProductSelectionSection
267
- // entirely and auto-select the lone group below so the per-product
268
- // picker renders without an intermediate card. With zero groups we
269
- // still render the section so the empty-state message is visible.
270
- const skipProductStep = allGroups.length === 1;
271
-
272
- // The single product-group the user is currently working with. Null
273
- // when no product is selected (the picker is shown empty / unselected)
274
- // or when the request type is org-level (nomination / grievance).
275
- const selectedGroup = useMemo<FormProductGroup | null>(() => {
276
- if (selectedProductKey) {
277
- return (
278
- allGroups.find((g) => productGroupKey(g) === selectedProductKey) ??
279
- null
280
- );
281
- }
282
- if (skipProductStep) {
283
- return allGroups[0] ?? null;
284
- }
285
- return null;
286
- }, [allGroups, selectedProductKey, skipProductStep]);
287
-
288
- // Stable select handler for ProductSelectionSection. Single-group
289
- // auto-select fires this on mount via the child's effect.
290
- const handleProductSelect = useCallback(
291
- (key: string | null, _group: FormProductGroup | null) => {
292
- setSelectedProductKey(key);
293
- },
294
- [],
295
- );
296
-
297
- // Approach A: source the DSAR request-form product picker from user-consents
298
- // rather than from Python's form/data, which stopped emitting product groups
299
- // after the consent cutover. `consentClient.getUserConsents` serves the same
300
- // grouped payload from either backend — the Go ledger's
301
- // `/public/.../user-consents` when a ledgerBaseUrl is configured, Python's
302
- // `/dsar/privacy-center/user-consents` otherwise (the same source the
303
- // Manage Consent view reads in both modes) — so the overlay runs regardless
304
- // of which backend the SDK points at. Only the end-user privacy-center path
305
- // is eligible: popup / case-management resolve an arbitrary contact, which
306
- // the JWT-scoped user-consents endpoint cannot serve, so those keep the
307
- // form/data groups. Returns the form data unchanged when ineligible, when
308
- // the read fails, or when it comes back with no groups at all — an empty
309
- // result must never overwrite groups form/data did supply.
310
- const withUserConsentPickerGroups = useCallback(
311
- async (form: FormData, language?: string): Promise<FormData> => {
312
- if (isPopup || isCaseManagementContext || !organizationId || !workspaceId) {
313
- return form;
314
- }
315
- const consents = await fetchUserConsents({
316
- organizationId,
317
- workspaceId,
318
- authContext,
319
- consentClient,
320
- // No status filter: include every status (active / expired / withdrawn
321
- // / declined), matching Python form/data, so a DSAR can target products
322
- // the user no longer actively consents to.
323
- limit: FORM_PICKER_GROUP_LIMIT,
324
- nominatedLimit: FORM_PICKER_GROUP_LIMIT,
325
- language,
326
- });
327
- const direct = consents?.direct ?? [];
328
- const nominated = consents?.nominated ?? [];
329
- if (direct.length === 0 && nominated.length === 0) {
330
- return form;
331
- }
332
- return {
333
- ...form,
334
- direct_groups: direct.map(consentGroupToFormProductGroup),
335
- nominated_groups: nominated.map(consentGroupToFormProductGroup),
336
- };
337
- },
338
- [
339
- isPopup,
340
- isCaseManagementContext,
341
- organizationId,
342
- workspaceId,
343
- authContext,
344
- consentClient,
345
- ],
346
- );
347
-
348
- const fetchAndApplyTranslations = useCallback(async (langValue: string) => {
349
- if (!organizationId || !workspaceId || !data) {
350
- return;
351
- }
352
-
353
- const contact = formData.contact ||
354
- (typeof window !== 'undefined' ? sessionStorage.getItem('email') || '' : '');
355
-
356
- if (!contact) {
357
- return;
358
- }
359
-
360
- setIsTranslating(true);
361
-
362
- try {
363
- const translatedFormData = await getFormData(
364
- organizationId,
365
- workspaceId,
366
- contact,
367
- isPopup,
368
- isCaseManagementContext,
369
- authContext,
370
- consentClient,
371
- langValue
372
- );
373
-
374
- if (!translatedFormData) {
375
- // Fallback to English when translation API unavailable
376
- toast.info(t('translationNotAvailable'));
377
- return;
378
- }
379
-
380
- if (!translatedFormData.purposes ||
381
- !Array.isArray(translatedFormData.purposes) ||
382
- translatedFormData.purposes.length === 0) {
383
- // Fallback to English when no translations for this language
384
- toast.info(t('translationNotAvailable'));
385
- return;
386
- }
387
-
388
- setData(await withUserConsentPickerGroups(translatedFormData, langValue));
389
-
390
- if (translatedFormData.grievance_options) {
391
- const currentGrievanceSelections = { ...selectedGrievances };
392
- setGrievanceOptions(translatedFormData.grievance_options);
393
- const updatedGrievanceSelections: Record<string, boolean> = {};
394
- translatedFormData.grievance_options.forEach((option) => {
395
- updatedGrievanceSelections[option.value] =
396
- currentGrievanceSelections[option.value] || false;
397
- });
398
- setSelectedGrievances(updatedGrievanceSelections);
399
- }
400
- } catch (error) {
401
- console.error('Error fetching translation:', error);
402
- // Fallback to English when translation API fails
403
- toast.info(t('translationNotAvailable'));
404
- } finally {
405
- setIsTranslating(false);
406
- }
407
- }, [organizationId, workspaceId, data, formData.contact, isPopup, isCaseManagementContext, authContext, consentClient, selectedGrievances, t, withUserConsentPickerGroups]);
408
-
409
- // Clear product / org-level selections when request type changes.
410
- // Per-product state (selectedPurposes, selectedCorrectionElements,
411
- // correctionValues) is owned by the seed effect below — clearing it
412
- // here too would race with that effect's re-seed when skipProductStep
413
- // is true (the sole group stays selected across request-type changes
414
- // because the auto-select effect refires on requestType).
415
- useEffect(() => {
416
- if (!skipProductStep) {
417
- setSelectedProductKey(null);
418
- }
419
- setNominationData({ nominee_email: '', nominee_mobile: '' });
420
-
421
- if (
422
- requestType === RequestTypeEnum.GRIEVANCE &&
423
- grievanceOptions.length > 0
424
- ) {
425
- const resetGrievances = grievanceOptions.reduce(
426
- (acc, option) => {
427
- acc[option.value] = false;
428
- return acc;
429
- },
430
- {} as Record<string, boolean>,
431
- );
432
- setSelectedGrievances(resetGrievances);
433
- }
434
- }, [requestType, grievanceOptions, skipProductStep]);
435
-
436
- // TECH-3712 UX: when `skipProductStep` is true (DP has exactly one
437
- // consent group total), auto-select that group so the per-product
438
- // picker renders directly without the user clicking a card. Placed
439
- // AFTER the request-type-change clearing effect above so a request-type
440
- // switch re-establishes the selection on the next tick. The child
441
- // ProductSelectionSection has an equivalent mount-time effect, but
442
- // that effect never fires here because the child is not rendered.
443
- useEffect(() => {
444
- if (!skipProductStep) {
445
- return;
446
- }
447
- const onlyGroup = allGroups[0];
448
- if (!onlyGroup) {
449
- return;
450
- }
451
- const key = productGroupKey(onlyGroup);
452
- setSelectedProductKey((prev) => (prev === key ? prev : key));
453
- }, [skipProductStep, allGroups, requestType]);
454
-
455
- // Seed (and re-seed on product switch or request-type switch) the
456
- // purpose / correction state from the currently-selected group.
457
- // Single-select means one re-seed per product change — no Map
458
- // reconciliation needed. Clears selections entirely when the user
459
- // de-selects the chip (selectedGroup -> null) or when the request
460
- // type switches into NOMINATION / GRIEVANCE.
461
- useEffect(() => {
462
- if (!selectedGroup || !isProductScopedRequestType(requestType)) {
463
- setSelectedPurposes([]);
464
- setSelectedCorrectionElements([]);
465
- setCorrectionValues({});
466
- return;
467
- }
468
- setSelectedPurposes(buildGroupPurposes(selectedGroup));
469
- setSelectedCorrectionElements([]);
470
- setCorrectionValues({});
471
- }, [selectedGroup, requestType]);
472
-
473
-
474
- // Fetch API translations when language changes (purposes, data elements, grievance options)
475
- useEffect(() => {
476
- const handleLanguageChanged = () => {
477
- const lang = i18n.language;
478
- if (organizationId && workspaceId && data) {
479
- const contact = formData.contact ||
480
- (typeof window !== 'undefined' ? sessionStorage.getItem('email') || '' : '');
481
- if (contact) {
482
- fetchAndApplyTranslations(lang);
483
- }
484
- }
485
- };
486
-
487
- i18n.on('languageChanged', handleLanguageChanged);
488
-
489
- return () => {
490
- i18n.off('languageChanged', handleLanguageChanged);
491
- };
492
- }, [organizationId, workspaceId, data, formData.contact, fetchAndApplyTranslations]);
493
-
494
- useEffect(() => {
495
- const initializePrivacyCenterForm = async () => {
496
- if (isPopup) {
497
- return;
498
- }
499
-
500
- if (!organizationId || !workspaceId) {
501
- onError?.(new Error('Organization or workspace not found'));
502
- return;
503
- }
504
-
505
- const storedContact =
506
- typeof window !== 'undefined'
507
- ? sessionStorage.getItem('email') || ''
508
- : '';
509
-
510
- if (!storedContact) {
511
- onError?.(new Error('Contact email not found'));
512
- return;
513
- }
514
-
515
- try {
516
- // Always fetch English data first and store for submission
517
- const englishData = await getFormData(
518
- organizationId,
519
- workspaceId,
520
- storedContact,
521
- isPopup,
522
- isCaseManagementContext,
523
- authContext,
524
- consentClient
525
- );
526
-
527
- if (!englishData) {
528
- toast.error(t('failedToFetchFormData'));
529
- onError?.(new Error('Failed to fetch form data'));
530
- return;
531
- }
532
-
533
- englishDataRef.current = englishData;
534
-
535
- // If current language is non-English, fetch translated data for display
536
- const langParam = getLangParam(i18n.language);
537
- const displayData = langParam
538
- ? (await getFormData(
539
- organizationId,
540
- workspaceId,
541
- storedContact,
542
- isPopup,
543
- isCaseManagementContext,
544
- authContext,
545
- consentClient,
546
- langParam
547
- )) || englishData
548
- : englishData;
549
-
550
- if (displayData.purposes && Array.isArray(displayData.purposes)) {
551
- const purposesWithDataElements = displayData.purposes.filter(
552
- (p) => p.purpose?.data_elements && p.purpose.data_elements.length > 0
553
- );
554
-
555
- if (purposesWithDataElements.length === 0) {
556
- console.warn('No purposes with data elements found in API response');
557
- }
558
- }
559
-
560
- const pickerData = await withUserConsentPickerGroups(
561
- displayData,
562
- langParam || undefined,
563
- );
564
- setData(pickerData);
565
- setGrievanceOptions(displayData.grievance_options || []);
566
- const initialGrievances = (
567
- displayData.grievance_options || []
568
- ).reduce(
569
- (acc, option) => {
570
- acc[option.value] = false;
571
- return acc;
572
- },
573
- {} as Record<string, boolean>
574
- );
575
- setSelectedGrievances(initialGrievances);
576
- setFormData((prev) => ({
577
- ...prev,
578
- uuid: displayData.uuid || '',
579
- name: displayData.name || '',
580
- contact: displayData.contact || storedContact,
581
- }));
582
- } finally {
583
- }
584
- };
585
-
586
- void initializePrivacyCenterForm();
587
- }, [organizationId, workspaceId, isPopup, authContext, consentClient, withUserConsentPickerGroups]);
588
-
589
- /**
590
- * Maps translated correction element names back to English names using UUID lookup.
591
- */
592
- const getEnglishDataElementName = useCallback((translatedName: string): string => {
593
- if (!englishDataRef.current?.purposes){
594
- return translatedName
595
- }
596
-
597
- // Build a map from translated name -> english name via UUID
598
- const translatedToUuid = new Map<string, string>();
599
- if (data?.purposes) {
600
- data.purposes.forEach((pw) => {
601
- pw.purpose?.data_elements?.forEach((de) => {
602
- translatedToUuid.set(de.name, de.uuid);
603
- });
604
- });
605
- }
606
-
607
- const uuid = translatedToUuid.get(translatedName);
608
- if (!uuid){
609
- return translatedName
610
- }
611
-
612
- for (const pw of englishDataRef.current.purposes) {
613
- const englishDe = pw.purpose?.data_elements?.find((de) => de.uuid === uuid);
614
- if (englishDe){
615
- return englishDe.name
616
- }
617
- }
618
- return translatedName;
619
- }, [data]);
620
-
621
- // Purpose names selected in an ERASURE request — surfaced in the
622
- // consent-revoke modal and the post-submit notice. When the list is
623
- // empty or too long to enumerate, fall back to a generic phrase.
624
- const selectedErasurePurposeList = useMemo(
625
- () =>
626
- selectedPurposes
627
- .filter((p) => p.selected || p.data_elements.some((de) => de.selected))
628
- .map((p) => p.name)
629
- .filter(Boolean),
630
- [selectedPurposes]
631
- );
632
-
633
- const selectedErasurePurposeNames = useMemo(() => {
634
- if (
635
- selectedErasurePurposeList.length === 0 ||
636
- selectedErasurePurposeList.length > MAX_DISPLAYED_PURPOSE_NAMES_IN_ERASURE_MODAL
637
- ) {
638
- return t('revokeConsentModalYourPurposes');
639
- }
640
- return selectedErasurePurposeList.join(', ');
641
- }, [selectedErasurePurposeList, t]);
642
-
643
- const handleSubmitClick = () => {
644
- if (!isFormValid) {
645
- return;
646
- }
647
- // ERASURE asks the data principal whether to also revoke consent on
648
- // fulfilment — the choice is made in the modal and threaded into the
649
- // payload via handleErasureModalChoice. Other request types
650
- // (ACCESS / CORRECTION / NOMINATION / GRIEVANCE) submit directly.
651
- if (requestType === RequestTypeEnum.ERASURE) {
652
- setIsErasureConsentModalOpen(true);
653
- return;
654
- }
655
- setRevokeConsentOnFulfilment(false);
656
- void handleSubmit(false);
657
- };
658
-
659
- const handleErasureModalChoice = (revoke: boolean) => {
660
- setRevokeConsentOnFulfilment(revoke);
661
- setIsErasureConsentModalOpen(false);
662
- void handleSubmit(revoke);
663
- };
664
-
665
- const handleErasureModalClose = () => {
666
- setIsErasureConsentModalOpen(false);
667
- };
668
-
669
- /**
670
- * Submit the form as a single case. Builds the payload from the
671
- * currently-selected product group (for ACCESS/ERASURE/CORRECTION) or
672
- * from the org-level state (for NOMINATION/GRIEVANCE), then POSTs once
673
- * via `createCaseRequest`. The 1-form -> 1-case rule means no
674
- * fan-out and no partial-failure UI to maintain.
675
- *
676
- * Anchors:
677
- * - `product_uuid` is set ONLY for product-scoped request types and
678
- * ONLY from `selectedGroup.product_uuid`. NOMINATION/GRIEVANCE
679
- * never carry a product anchor.
680
- * - `data_principal_uuid` is set ONLY for nominated cards (acting
681
- * on someone else's behalf — the nominator's ConsentUser uuid).
682
- * Direct cards omit the field so the backend defaults it from
683
- * the JWT-derived requestor_uuid. `formData.uuid` is the
684
- * FormDataOut session uuid, NOT the requestor's ConsentUser uuid,
685
- * and would be rejected by the backend's ConsentUser lookup.
686
- */
687
- const handleSubmit = async (revokeConsent: boolean = false) => {
688
- if (!isFormValid) {
689
- return;
690
- }
691
-
692
- const isProductScoped = isProductScopedRequestType(requestType);
693
- // The synthetic legacy-flat group has no server-side product, so it must
694
- // not anchor the case — those submissions keep the pre-grouping
695
- // org-level shape (`product_uuid` is optional on the backend).
696
- const isLegacyFlatSelection =
697
- selectedGroup?.product_uuid === LEGACY_PRODUCT_GROUP_UUID;
698
- const anchorGroup =
699
- isProductScoped && !isLegacyFlatSelection ? selectedGroup : null;
700
-
701
- const requestDetails: {
702
- purposes: UpdatePurpose[];
703
- correction_data: UpdateCorrectionDataItem[];
704
- grievance_types: GrievanceTypeElement[];
705
- nomination_data?: NominationData;
706
- } = {
707
- purposes: [],
708
- correction_data: [],
709
- grievance_types: [],
710
- };
711
-
712
- if (
713
- requestType === RequestTypeEnum.ACCESS ||
714
- requestType === RequestTypeEnum.ERASURE
715
- ) {
716
- requestDetails.purposes = selectedPurposes
717
- .filter((p) => p.selected || p.data_elements.some((de) => de.selected))
718
- .map((p) => ({
719
- purpose_uuid: p.uuid,
720
- data_element_uuids: p.data_elements
721
- .filter((de) => de.selected)
722
- .map((de) => de.uuid),
723
- }));
724
- }
725
-
726
- if (requestType === RequestTypeEnum.CORRECTION) {
727
- requestDetails.correction_data = selectedCorrectionElements.map(
728
- (name) => ({
729
- name: getEnglishDataElementName(name),
730
- curr_value: correctionValues[name]?.current ?? '',
731
- new_value: correctionValues[name]?.updated ?? '',
732
- }),
733
- );
734
- }
735
-
736
- if (requestType === RequestTypeEnum.GRIEVANCE) {
737
- requestDetails.grievance_types = Object.entries(selectedGrievances)
738
- .filter(([, checked]) => checked)
739
- .map(([key]) => ({
740
- grievance: key as GrievanceValue,
741
- }));
742
- }
743
-
744
- if (requestType === RequestTypeEnum.NOMINATION) {
745
- requestDetails.nomination_data = {
746
- nominee_email: nominationData.nominee_email.trim(),
747
- ...(nominationData.nominee_mobile?.trim()
748
- ? { nominee_mobile: nominationData.nominee_mobile.trim() }
749
- : {}),
750
- };
751
- }
752
-
753
- const payload: UserDataRequest = {
754
- name: ' ',
755
- contact: formData.contact,
756
- request_type: requestType?.toLowerCase() as RequestTypeEnum,
757
- uuid: formData.uuid,
758
- supporting_docs_uuids: [...uploadedDocumentId],
759
- request_details: requestDetails,
760
- time_period:
761
- requestType === RequestTypeEnum.ACCESS
762
- ? parseInt(timePeriod || '30', 10)
763
- : 0,
764
- requestor_note: formData.additionalNote || '',
765
- ...(requestType === RequestTypeEnum.ERASURE
766
- ? { revoke_consent_on_fulfilment: revokeConsent }
767
- : {}),
768
- ...(anchorGroup ? { product_uuid: anchorGroup.product_uuid } : {}),
769
- ...(anchorGroup?.nominator
770
- ? { data_principal_uuid: anchorGroup.nominator.uuid }
771
- : {}),
772
- };
773
-
774
- try {
775
- setIsLoading(true);
776
- const response = await createCaseRequest(
777
- organizationId || '',
778
- workspaceId || '',
779
- payload,
780
- isPopup,
781
- isCaseManagementContext, // Use session auth if in case management context
782
- authContext,
783
- consentClient,
784
- );
785
- const extracted = extractCaseId(response);
786
- const caseId = extracted?.case_id || extracted?.uuid || null;
787
-
788
- if (caseId) {
789
- queryClient.invalidateQueries({ queryKey: ['caseHistory'] });
790
- setSubmittedCase({
791
- case_id: caseId,
792
- uuid: extracted?.uuid ?? caseId,
793
- product_name: anchorGroup?.product_name ?? '',
794
- product_uuid: anchorGroup?.product_uuid ?? '',
795
- });
796
- setIsSubmitted(true);
797
- toast.success(t('requestSubmittedSuccess'));
798
-
799
- if (isPopup) {
800
- onCreateRequest?.();
801
- // Only dismiss the popup on success. Closing in `finally`
802
- // would discard the operator's filled form on a transient
803
- // failure with no way to retry.
804
- onClose?.();
805
- }
806
- } else {
807
- toast.error(t('requestSubmissionFailed'));
808
- }
809
- } catch (error) {
810
- // `createCaseRequest` returns null on non-auth errors (per its
811
- // documented contract) — those land in the `else` branch above
812
- // with a generic toast. This catch only fires on unexpected
813
- // exceptions; surface whatever message the error carries.
814
- console.error('Error submitting request:', error);
815
- const message =
816
- error instanceof Error && error.message
817
- ? error.message
818
- : t('submissionFailed');
819
- toast.error(message);
820
- } finally {
821
- setIsLoading(false);
822
- }
823
- };
824
-
825
- const isRequestDetailsValid = useCallback(() => {
826
- if (!requestType) {
827
- return false;
828
- }
829
-
830
- // Product-anchor validity gate: ACCESS/ERASURE/CORRECTION require a
831
- // selected product before submit is allowed. The backend leaves
832
- // `product_uuid` Optional for back-compat, so we enforce the anchor
833
- // on the client — a product-scoped case without an anchor would
834
- // land as an org-level case with no scoping, which violates the
835
- // 1-form -> 1-case -> 1-product contract.
836
- if (isProductScopedRequestType(requestType)) {
837
- if (allGroups.length === 0) {
838
- // No consent groups for this DP at all — the picker shows its
839
- // empty state and we block submit. The DP has nothing they can
840
- // raise a product-scoped case against.
841
- return false;
842
- }
843
- if (!selectedGroup) {
844
- // Groups exist but the user hasn't picked one. With the
845
- // single-group auto-select effect this should be rare in
846
- // practice, but the gate must still reject.
847
- return false;
848
- }
849
- if (requestType === RequestTypeEnum.CORRECTION) {
850
- return (
851
- selectedCorrectionElements.length > 0 &&
852
- selectedCorrectionElements.every(
853
- (name) =>
854
- correctionValues[name]?.current?.trim() &&
855
- correctionValues[name]?.updated?.trim(),
856
- )
857
- );
858
- }
859
- return selectedPurposes.some(
860
- (p) => p.selected || p.data_elements.some((de) => de.selected),
861
- );
862
- }
863
-
864
- switch (requestType) {
865
- case RequestTypeEnum.GRIEVANCE:
866
- return Object.values(selectedGrievances).some(Boolean);
867
-
868
- case RequestTypeEnum.NOMINATION:
869
- return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(
870
- nominationData.nominee_email.trim(),
871
- );
872
-
873
- default:
874
- return false;
875
- }
876
- }, [
877
- requestType,
878
- allGroups,
879
- selectedGroup,
880
- selectedPurposes,
881
- selectedCorrectionElements,
882
- correctionValues,
883
- selectedGrievances,
884
- nominationData,
885
- ]);
886
-
887
- const isFormValid = useMemo(() => {
888
- return (
889
- (!isPopup || isPopupFormEnabled) &&
890
- !!formData.contact?.trim() &&
891
- !!requestType &&
892
- isRequestDetailsValid() &&
893
- formData.confirmCheckBox
894
- );
895
- }, [
896
- isPopup,
897
- isPopupFormEnabled,
898
- formData.contact,
899
- formData.confirmCheckBox,
900
- requestType,
901
- isRequestDetailsValid,
902
- ]);
903
-
904
- const handleFileUpload = async (
905
- file: File,
906
- isPublic: boolean,
907
- caseId: string
908
- ) => {
909
- setIsDocumentUploading(true);
910
- try {
911
- const metaData = {
912
- group_by: 'privacy',
913
- case_uuid: caseId,
914
- product: 'privacy_center',
915
- };
916
-
917
- const payload = {
918
- organization_uuid: organizationId || '',
919
- space: `privacy/${caseId}`,
920
- uuid: crypto.randomUUID(),
921
- is_public: isPublic,
922
- allow_ai_processing: true,
923
- ...(metaData ? { metadata: metaData } : {}),
924
- };
925
-
926
- // Create FormData on client side - this can be passed to server actions
927
- const formData = new FormData();
928
- formData.append('file', file);
929
- formData.append('payload', JSON.stringify(payload));
930
-
931
- // Ensure we have a valid token before uploading
932
- if (!organizationId || !workspaceId) {
933
- throw new Error('Organization ID and Workspace ID are required for file upload');
934
- }
935
-
936
- const authToken = await ensureValidToken(organizationId, workspaceId, orgSlug, authContext, consentClient);
937
- if (!authToken) {
938
- throw new Error('Authentication token is required for file upload');
939
- }
940
-
941
- // uploadFileWithS2S is a client-side function that uses JWT Bearer token authentication
942
- const uploadedDoc = await uploadFileWithS2S({
943
- formData,
944
- fileName: file.name,
945
- authToken,
946
- baseUrl,
947
- });
948
-
949
- if (!uploadedDoc.success) {
950
- toast.error(t('failedToUploadFile', { fileName: file.name }));
951
- throw new Error(uploadedDoc.error);
952
- }
953
-
954
- if (uploadedDoc.uuid) {
955
- setUploadedDocuments((prev) => [
956
- ...prev,
957
- { uuid: uploadedDoc.uuid!, name: file.name },
958
- ]);
959
- setUploadedDocumentId((prevIds) => [...prevIds, uploadedDoc.uuid!]);
960
- setIsDocument(true);
961
- toast.success(t('fileUploadedSuccess'));
962
- }
963
- } catch (error) {
964
- console.error('Upload error:', error);
965
- toast.error(t('uploadError', { error: String(error) }));
966
- return null;
967
- } finally {
968
- setIsDocumentUploading(false);
969
- }
970
- };
971
-
972
- const handleFileChange = async (
973
- event: React.ChangeEvent<HTMLInputElement>
974
- ) => {
975
- const files = Array.from(event.target.files || []);
976
- handleFileUpload(files[0], true, formData.uuid);
977
- resetFileInput();
978
- };
979
-
980
- const handleRemoveDocument = (uuidToRemove: string) => {
981
- setUploadedDocuments((prev) =>
982
- prev.filter((doc) => doc.uuid !== uuidToRemove)
983
- );
984
- setUploadedDocumentId((prev) => prev.filter((id) => id !== uuidToRemove));
985
- };
986
-
987
- const handleVerifyContact = async () => {
988
- if (!isPopup) {
989
- return
990
- };
991
- if (!formData.contact) {
992
- toast.error(t('pleaseEnterContactToVerify'));
993
- return;
994
- }
995
- if (!organizationId || !workspaceId) {
996
- toast.error(t('missingOrgOrWorkspace'));
997
- return;
998
- }
999
-
1000
- setIsVerifyingContact(true);
1001
- try {
1002
- // Always fetch English data first and store for submission
1003
- const englishData = await getFormData(
1004
- organizationId,
1005
- workspaceId,
1006
- formData.contact,
1007
- isPopup,
1008
- isCaseManagementContext,
1009
- authContext,
1010
- consentClient
1011
- );
1012
-
1013
- if (englishData) {
1014
- englishDataRef.current = englishData;
1015
-
1016
- // If current language is non-English, fetch translated data for display
1017
- const langParam = getLangParam(i18n.language);
1018
- const displayData = langParam
1019
- ? (await getFormData(
1020
- organizationId,
1021
- workspaceId,
1022
- formData.contact,
1023
- isPopup,
1024
- isCaseManagementContext,
1025
- authContext,
1026
- consentClient,
1027
- langParam
1028
- )) || englishData
1029
- : englishData;
1030
-
1031
- if (displayData.purposes && Array.isArray(displayData.purposes)) {
1032
- const purposesWithDataElements = displayData.purposes.filter(
1033
- (p) => p.purpose?.data_elements && p.purpose.data_elements.length > 0
1034
- );
1035
-
1036
- if (purposesWithDataElements.length === 0) {
1037
- console.warn('No purposes with data elements found in API response (popup)');
1038
- }
1039
- }
1040
-
1041
- setData(displayData);
1042
- setGrievanceOptions(displayData.grievance_options || []);
1043
- const initialGrievances = (displayData.grievance_options || []).reduce(
1044
- (acc, option) => {
1045
- acc[option.value] = false;
1046
- return acc;
1047
- },
1048
- {} as Record<string, boolean>
1049
- );
1050
- setSelectedGrievances(initialGrievances);
1051
- setFormData((prev) => ({
1052
- ...prev,
1053
- uuid: displayData.uuid || '',
1054
- name: displayData.name || '',
1055
- contact: displayData.contact || prev.contact,
1056
- }));
1057
- // Translations are handled locally via i18next, no API call needed
1058
- setIsPopupFormEnabled(true);
1059
- toast.success(t('contactVerifiedSuccess'));
1060
- } else {
1061
- setIsPopupFormEnabled(false);
1062
- toast.error(t('unableToVerifyContact'));
1063
- }
1064
- } catch (error: any) {
1065
- if (error.response?.status === 404) {
1066
- toast.info(t('noUserFoundForContact', { contact: formData.contact }));
1067
- onClose?.();
1068
- } else {
1069
- toast.error(t('failedToVerifyContact'));
1070
- setIsVerifyingContact(false);
1071
- }
1072
- } finally {
1073
- setIsVerifyingContact(false);
1074
- }
1075
- };
1076
-
1077
- return (
1078
- <>
1079
- {isSubmitted && submittedCase && !isPopup ? (
1080
- <RequestSubmit
1081
- result={submittedCase}
1082
- contact={formData?.contact}
1083
- onCreateRequest={onCreateRequest}
1084
- revokeConsentOnFulfilment={revokeConsentOnFulfilment}
1085
- revokedPurposeNames={selectedErasurePurposeNames}
1086
- />
1087
- ) : (
1088
- <div>
1089
- <div>
1090
- {!isPopup && (
1091
- <p style={{
1092
- marginTop: 0,
1093
- marginBottom: getVar('--pc-spacing-sm'),
1094
- lineHeight: 1.5,
1095
- fontSize: pcStyles.textSm.fontSize,
1096
- color: getVar('--pc-text-secondary'),
1097
- }}>{t('description')}</p>
1098
- )}
1099
- <div
1100
- style={{
1101
- backgroundColor: getVar('--pc-bg-primary'),
1102
- ...(isPopup ? {
1103
- marginTop: 0,
1104
- marginBottom: 0,
1105
- borderRadius: pcStyles.borderRadiusXl,
1106
- } : {
1107
- marginTop: pcStyles.mtSm.marginTop,
1108
- marginBottom: pcStyles.mbSm.marginBottom,
1109
- borderRadius: pcStyles.borderRadiusXl,
1110
- border: `1px solid ${getVar('--pc-border-color')}`,
1111
- boxShadow: getVar('--pc-shadow-sm'),
1112
- }),
1113
- }}
1114
- >
1115
- <FormBlockWrapper inputClassName="mx-auto">
1116
- <div
1117
- style={{
1118
- ...pcStyles.flex,
1119
- ...pcStyles.itemsCenter,
1120
- ...pcStyles.justifyBetween,
1121
- backgroundColor: `var(--pc-bg-secondary)`,
1122
- padding: getVar('--pc-spacing-md'),
1123
- width: '100%',
1124
- overflow: 'visible',
1125
- ...(isPopup ? {
1126
- position: 'sticky',
1127
- top: 0,
1128
- zIndex: 20,
1129
- borderBottom: `1px solid var(--pc-border-color)`,
1130
- } : {
1131
- position: 'relative',
1132
- borderRadius: `${getVar('--pc-border-radius-lg')} ${getVar('--pc-border-radius-lg')} 0 0`,
1133
- }),
1134
- }}
1135
- className="pc-padding-responsive"
1136
- >
1137
- <p style={{
1138
- fontSize: pcStyles.textSm.fontSize,
1139
- fontWeight: pcStyles.fontWeightSemibold.fontWeight,
1140
- color: `var(--pc-text-primary)`,
1141
- margin: 0,
1142
- whiteSpace: 'nowrap',
1143
- flexShrink: 0,
1144
- flexGrow: 0,
1145
- }} className="pc-text-responsive">
1146
- {t('raiseDataRequest')}
1147
- </p>
1148
- {isPopup && (
1149
- <PCButton
1150
- variant="text"
1151
- onClick={onClose}
1152
- style={{ padding: 0, flexShrink: 0, marginLeft: 'auto' }}
1153
- >
1154
- <FiX size={20} />
1155
- </PCButton>
1156
- )}
1157
- </div>
1158
- <div
1159
- style={{
1160
- ...pcStyles.flex,
1161
- ...pcStyles.flexCol,
1162
- gap: getVar('--pc-spacing-lg'),
1163
- padding: getVar('--pc-spacing-lg'),
1164
- }}
1165
- className="pc-padding-responsive pc-spacing-responsive"
1166
- >
1167
- <div style={{
1168
- ...pcStyles.flex,
1169
- width: '100%',
1170
- ...pcStyles.itemsEnd,
1171
- gap: getVar('--pc-spacing-sm'),
1172
- flexWrap: 'wrap',
1173
- }}>
1174
- <PCInput
1175
- style={{ flex: 1, minWidth: '200px' }}
1176
- label={
1177
- <span>
1178
- {t('contact')}
1179
- <span style={{ color: getVar('--pc-error') }}>*</span>
1180
- </span>
1181
- }
1182
- placeholder={t('contactPlaceholder')}
1183
- value={formData.contact}
1184
- readOnly={!isPopup}
1185
- onChange={(e) => {
1186
- if (!isPopup){
1187
- return
1188
- };
1189
- setFormData({
1190
- ...formData,
1191
- contact: e.target.value,
1192
- });
1193
- setIsPopupFormEnabled(false);
1194
- }}
1195
- />
1196
- {isPopup && !isPopupFormEnabled && (
1197
- <div style={{ ...pcStyles.flex, ...pcStyles.justifyEnd }}>
1198
- <PCButton
1199
- size="sm"
1200
- onClick={handleVerifyContact}
1201
- style={{ height: '2.25rem', flexShrink: 0 }}
1202
- disabled={!formData.contact || isVerifyingContact}
1203
- >
1204
- {isVerifyingContact ? t('verifying') : t('verify')}
1205
- </PCButton>
1206
- </div>
1207
- )}
1208
- </div>
1209
- <PCSelect
1210
- label={
1211
- <span>
1212
- {t('requestType')}
1213
- <span style={{ color: getVar('--pc-error') }}>*</span>
1214
- </span>
1215
- }
1216
- options={Object.values(RequestTypeEnum).map((type) => ({
1217
- value: type,
1218
- label: requestTypeLabels[type],
1219
- }))}
1220
- value={
1221
- Object.values(RequestTypeEnum)
1222
- .map((type) => ({
1223
- value: type,
1224
- label: requestTypeLabels[type],
1225
- }))
1226
- .find((opt) => opt.value === requestType) || null
1227
- }
1228
- onChange={(value) => {
1229
- if (value) {
1230
- setRequestType(value.value as RequestTypeEnum);
1231
- setTimePeriod('');
1232
- setIsRequestDetailsExpanded(true);
1233
- }
1234
- }}
1235
- placeholder={t('selectRequestType')}
1236
- disabled={isPopup && !isPopupFormEnabled}
1237
- />
1238
- <div
1239
- style={{
1240
- ...pcStyles.flex,
1241
- ...pcStyles.flexCol,
1242
- borderRadius: pcStyles.borderRadiusLg,
1243
- border: `1px solid ${getVar('--pc-border-color')}`,
1244
- backgroundColor: getVar('--pc-bg-secondary'),
1245
- overflow: 'hidden',
1246
- ...(isPopup && !isPopupFormEnabled ? {
1247
- pointerEvents: 'none',
1248
- opacity: 0.6,
1249
- } : {}),
1250
- }}
1251
- >
1252
- <div style={{
1253
- ...pcStyles.flex,
1254
- ...pcStyles.itemsCenter,
1255
- ...pcStyles.justifyBetween,
1256
- padding: getVar('--pc-spacing-md'),
1257
- borderBottom: isRequestDetailsExpanded ? `1px solid ${getVar('--pc-border-color')}` : 'none',
1258
- }}>
1259
- <p style={{
1260
- fontWeight: pcStyles.fontWeightSemibold.fontWeight,
1261
- color: `var(--pc-text-primary)`,
1262
- fontSize: pcStyles.textSm.fontSize,
1263
- margin: 0,
1264
- }}>
1265
- {t('requestDetails')}{' '}
1266
- <span style={{ color: getVar('--pc-error') }}>*</span>
1267
- </p>
1268
- <PCButton
1269
- variant="text"
1270
- size="sm"
1271
- style={{ height: 'auto', padding: 0, color: 'var(--pc-text-secondary)', flexShrink: 0 }}
1272
- onClick={() =>
1273
- setIsRequestDetailsExpanded(
1274
- !isRequestDetailsExpanded
1275
- )
1276
- }
1277
- >
1278
- {isRequestDetailsExpanded ? (
1279
- <FiChevronUp size={20} />
1280
- ) : (
1281
- <FiChevronDown size={20} />
1282
- )}
1283
- </PCButton>
1284
- </div>
1285
- {isRequestDetailsExpanded && (
1286
- <div style={{
1287
- padding: getVar('--pc-spacing-md'),
1288
- }}>
1289
- {isProductScopedRequestType(requestType) ? (
1290
- <div
1291
- style={{
1292
- ...pcStyles.flex,
1293
- ...pcStyles.flexCol,
1294
- gap: getVar('--pc-spacing-md'),
1295
- }}
1296
- >
1297
- {!skipProductStep && (
1298
- <ProductSelectionSection
1299
- directGroups={data?.direct_groups ?? []}
1300
- nominatedGroups={data?.nominated_groups ?? []}
1301
- selectedProductKey={selectedProductKey}
1302
- onSelect={handleProductSelect}
1303
- requestTypeLabel={requestTypeLabels[requestType]}
1304
- isTranslating={isTranslating}
1305
- />
1306
- )}
1307
- {selectedGroup && (() => {
1308
- const key = productGroupKey(selectedGroup);
1309
- const isCorrection =
1310
- requestType === RequestTypeEnum.CORRECTION;
1311
- const groupDataElements = isCorrection
1312
- ? buildGroupDataElements(selectedGroup)
1313
- : [];
1314
- const nominatorLabel = selectedGroup.nominator
1315
- ? selectedGroup.nominator.name ??
1316
- selectedGroup.nominator.email ??
1317
- ''
1318
- : '';
1319
-
1320
- return (
1321
- <div
1322
- className="pc-product-purpose-panel"
1323
- data-testid={`per-product-purposes-${key}`}
1324
- >
1325
- <div className="pc-product-purpose-panel-header">
1326
- <div className="pc-product-purpose-panel-title-row">
1327
- <PCText
1328
- as="span"
1329
- size="base"
1330
- weight="semibold"
1331
- color="primary"
1332
- className="pc-product-purpose-panel-title"
1333
- >
1334
- {selectedGroup.product_name}
1335
- </PCText>
1336
- {selectedGroup.nominator && (
1337
- <PCText
1338
- as="span"
1339
- size="xs"
1340
- weight="medium"
1341
- color="secondary"
1342
- className="pc-product-purpose-panel-nominator"
1343
- >
1344
- {t('actingOnBehalfOfShort', {
1345
- nominator: nominatorLabel,
1346
- })}
1347
- </PCText>
1348
- )}
1349
- </div>
1350
- <PCText
1351
- as="span"
1352
- size="xs"
1353
- color="secondary"
1354
- className="pc-product-purpose-panel-subtitle"
1355
- >
1356
- {t('selectPurposesForThisProduct')}
1357
- </PCText>
1358
- </div>
1359
- <div
1360
- className="pc-product-purpose-panel-body"
1361
- data-testid={`per-product-purposes-body-${key}`}
1362
- >
1363
- {isCorrection ? (
1364
- <CorrectionDataSection
1365
- dataElements={groupDataElements}
1366
- selected={selectedCorrectionElements}
1367
- setSelected={setSelectedCorrectionElements}
1368
- correctionValues={correctionValues}
1369
- setCorrectionValues={setCorrectionValues}
1370
- isTranslating={isTranslating}
1371
- />
1372
- ) : (
1373
- <PurposeSelectionSection
1374
- purposes={selectedPurposes}
1375
- setPurposes={setSelectedPurposes}
1376
- requestType={requestType}
1377
- isTranslating={isTranslating}
1378
- />
1379
- )}
1380
- </div>
1381
- </div>
1382
- );
1383
- })()}
1384
- </div>
1385
- ) : requestType === RequestTypeEnum.GRIEVANCE ? (
1386
- <GrievanceDataSection
1387
- grievanceOptions={grievanceOptions}
1388
- selectedGrievances={selectedGrievances}
1389
- setSelectedGrievances={setSelectedGrievances}
1390
- isTranslating={isTranslating}
1391
- />
1392
- ) : requestType === RequestTypeEnum.NOMINATION ? (
1393
- <NominationDataSection
1394
- nominationData={nominationData}
1395
- setNominationData={setNominationData}
1396
- isTranslating={isTranslating}
1397
- />
1398
- ) : (
1399
- <div
1400
- style={{
1401
- color: getVar('--pc-text-secondary'),
1402
- ...(isPopup ? {
1403
- padding: getVar('--pc-spacing-md'),
1404
- } : {
1405
- ...pcStyles.flex,
1406
- height: '8rem',
1407
- ...pcStyles.itemsCenter,
1408
- ...pcStyles.justifyCenter,
1409
- padding: getVar('--pc-spacing-lg'),
1410
- }),
1411
- textAlign: 'center',
1412
- }}
1413
- >
1414
- {t('selectRequestType')}
1415
- </div>
1416
- )}
1417
- </div>
1418
- )}
1419
- </div>
1420
- <div
1421
- style={{
1422
- ...pcStyles.flex,
1423
- ...pcStyles.flexCol,
1424
- gap: getVar('--pc-spacing-sm'),
1425
- ...(isPopup && !isPopupFormEnabled ? {
1426
- pointerEvents: 'none',
1427
- opacity: 0.6,
1428
- } : {}),
1429
- }}
1430
- >
1431
- <p style={{
1432
- fontWeight: pcStyles.fontWeightSemibold.fontWeight,
1433
- color: `var(--pc-text-primary)`,
1434
- fontSize: pcStyles.textSm.fontSize,
1435
- margin: 0,
1436
- }}>
1437
- {t('supportingDocumentation')}
1438
- </p>
1439
- <input
1440
- ref={fileInputRef}
1441
- id="privacy-file-upload"
1442
- type="file"
1443
- onChange={(e) => handleFileChange(e)}
1444
- style={{ display: 'none' }}
1445
- />
1446
- <div
1447
- style={{
1448
- ...pcStyles.flex,
1449
- flexWrap: 'wrap',
1450
- ...pcStyles.itemsStart,
1451
- borderRadius: pcStyles.borderRadiusMd,
1452
- border: `1px solid var(--pc-border-color)`,
1453
- backgroundColor: `var(--pc-bg-secondary)`,
1454
- padding: getVar('--pc-spacing-md'),
1455
- minHeight: '100px',
1456
- ...(isPopup ? {
1457
- gap: '0.5rem',
1458
- ...pcStyles.overflowYAuto,
1459
- } : {
1460
- gap: getVar('--pc-spacing-sm'),
1461
- }),
1462
- }}
1463
- >
1464
- {/* Always render documents list container to prevent layout shift */}
1465
- {(isDocument && uploadedDocuments.length > 0) && (
1466
- <div
1467
- style={{
1468
- ...pcStyles.flex,
1469
- ...pcStyles.gapSm,
1470
- ...(isPopup ? { flexWrap: 'wrap' } : {}),
1471
- width: '100%',
1472
- }}
1473
- >
1474
- {uploadedDocuments.map((doc) => (
1475
- <div
1476
- key={doc.uuid}
1477
- style={{
1478
- position: 'relative',
1479
- ...pcStyles.flex,
1480
- ...pcStyles.itemsCenter,
1481
- ...pcStyles.gapXs,
1482
- borderRadius: pcStyles.borderRadiusMd,
1483
- border: `1px solid var(--pc-border-color)`,
1484
- backgroundColor: `var(--pc-bg-primary)`,
1485
- padding: getVar('--pc-spacing-sm'),
1486
- }}
1487
- >
1488
- <img
1489
- src={fileAttachIcon}
1490
- alt="fileAttach"
1491
- width={24}
1492
- height={24}
1493
- />
1494
- <p
1495
- style={{
1496
- fontWeight: pcStyles.fontWeightMedium.fontWeight,
1497
- color: `var(--pc-text-primary)`,
1498
- ...(isPopup ? {
1499
- maxWidth: '100px',
1500
- overflow: 'hidden',
1501
- textOverflow: 'ellipsis',
1502
- whiteSpace: 'nowrap',
1503
- fontSize: pcStyles.textXs.fontSize,
1504
- } : {
1505
- fontSize: pcStyles.textSm.fontSize,
1506
- }),
1507
- }}
1508
- >
1509
- {doc.name}
1510
- </p>
1511
- <div
1512
- style={{
1513
- position: 'absolute',
1514
- right: '-0.25rem',
1515
- top: '-0.5rem',
1516
- cursor: 'pointer',
1517
- }}
1518
- onClick={(e) => {
1519
- e.stopPropagation();
1520
- handleRemoveDocument(doc.uuid);
1521
- }}
1522
- >
1523
- <div style={{
1524
- borderRadius: '50%',
1525
- border: `1px solid var(--pc-border-color)`,
1526
- backgroundColor: `var(--pc-bg-secondary)`,
1527
- padding: '0.125rem',
1528
- }}>
1529
- <AiOutlineClose size={12} style={{ color: 'var(--pc-error)' }} />
1530
- </div>
1531
- </div>
1532
- </div>
1533
- ))}
1534
- </div>
1535
- )}
1536
-
1537
- {/* Upload button or loading state - always in same position */}
1538
- <div style={{
1539
- width: uploadedDocuments.length > 0 ? 'auto' : '100%',
1540
- ...pcStyles.flex,
1541
- ...pcStyles.itemsCenter,
1542
- ...pcStyles.justifyCenter,
1543
- minHeight: uploadedDocuments.length > 0 ? 'auto' : '100px',
1544
- }}>
1545
- {isDocumentUploading ? (
1546
- <div style={{
1547
- padding: uploadedDocuments.length > 0 ? getVar('--pc-spacing-sm') : getVar('--pc-spacing-lg'),
1548
- borderRadius: pcStyles.borderRadiusMd,
1549
- border: uploadedDocuments.length > 0 ? `1px solid var(--pc-border-color)` : 'none',
1550
- backgroundColor: `var(--pc-bg-primary)`,
1551
- fontWeight: pcStyles.fontWeightSemibold.fontWeight,
1552
- color: `var(--pc-text-primary)`,
1553
- ...pcStyles.flex,
1554
- ...pcStyles.itemsCenter,
1555
- ...pcStyles.justifyCenter,
1556
- gap: uploadedDocuments.length > 0 ? getVar('--pc-spacing-sm') : getVar('--pc-spacing-md'),
1557
- width: 'auto',
1558
- }}>
1559
- <PCLoader variant="spinner" size="sm" />
1560
- <span>{t('uploading')}</span>
1561
- </div>
1562
- ) : (
1563
- <div
1564
- style={{
1565
- ...pcStyles.flex,
1566
- width: uploadedDocuments.length > 0 ? 'auto' : '100%',
1567
- cursor: 'pointer',
1568
- ...pcStyles.itemsCenter,
1569
- ...pcStyles.justifyCenter,
1570
- gap: uploadedDocuments.length > 0 ? getVar('--pc-spacing-sm') : getVar('--pc-spacing-md'),
1571
- borderRadius: pcStyles.borderRadiusMd,
1572
- padding: uploadedDocuments.length > 0 ? getVar('--pc-spacing-sm') : getVar('--pc-spacing-lg'),
1573
- border: uploadedDocuments.length > 0 ? `1px solid var(--pc-border-color)` : 'none',
1574
- backgroundColor: uploadedDocuments.length > 0 ? `var(--pc-bg-primary)` : 'transparent',
1575
- transition: 'all 0.2s ease',
1576
- }}
1577
- onMouseEnter={(e) => {
1578
- e.currentTarget.style.backgroundColor = uploadedDocuments.length > 0
1579
- ? `var(--pc-bg-hover)`
1580
- : `var(--pc-bg-hover)`;
1581
- if (uploadedDocuments.length > 0) {
1582
- e.currentTarget.style.borderColor = `var(--pc-primary)`;
1583
- }
1584
- }}
1585
- onMouseLeave={(e) => {
1586
- e.currentTarget.style.backgroundColor = uploadedDocuments.length > 0
1587
- ? `var(--pc-bg-primary)`
1588
- : 'transparent';
1589
- if (uploadedDocuments.length > 0) {
1590
- e.currentTarget.style.borderColor = `var(--pc-border-color)`;
1591
- }
1592
- }}
1593
- role="button"
1594
- tabIndex={0}
1595
- aria-label={t('uploadFile')}
1596
- onClick={() =>
1597
- document
1598
- .getElementById('privacy-file-upload')
1599
- ?.click()
1600
- }
1601
- onKeyDown={(e) => {
1602
- if (e.key === 'Enter' || e.key === ' ') {
1603
- e.preventDefault();
1604
- document.getElementById('privacy-file-upload')?.click();
1605
- }
1606
- }}
1607
- >
1608
- {uploadedDocuments.length === 0 ? (
1609
- <>
1610
- <div>
1611
- <img
1612
- src={uploadFileIcon}
1613
- width={44}
1614
- height={44}
1615
- alt="upload"
1616
- />
1617
- </div>
1618
- <span style={{
1619
- color: 'var(--pc-text-secondary)',
1620
- fontSize: pcStyles.textSm.fontSize,
1621
- }}>
1622
- {t('uploadFile')}
1623
- </span>
1624
- </>
1625
- ) : (
1626
- <div style={{ ...pcStyles.flex, ...pcStyles.itemsCenter, ...pcStyles.justifyCenter, ...pcStyles.gapSm }}>
1627
- <PiUploadBold
1628
- size={isPopup ? 20 : 16}
1629
- style={{ color: 'var(--pc-text-secondary)' }}
1630
- />
1631
- {!isPopup && (
1632
- <p style={{
1633
- fontWeight: pcStyles.fontWeightMedium.fontWeight,
1634
- color: `var(--pc-text-primary)`,
1635
- }}>
1636
- {t('uploadFileButton')}
1637
- </p>
1638
- )}
1639
- </div>
1640
- )}
1641
- </div>
1642
- )}
1643
- </div>
1644
- </div>
1645
- </div>
1646
- {requestType === RequestTypeEnum.ACCESS && (
1647
- <PCSelect
1648
- label={t('timePeriod')}
1649
- options={timePeriodOptions}
1650
- value={timePeriodOptions.find(
1651
- (opt) => opt.value === timePeriod
1652
- ) || null}
1653
- onChange={(value) => {
1654
- if (value) {
1655
- setTimePeriod(value.value as string);
1656
- }
1657
- }}
1658
- disabled={isPopup && !isPopupFormEnabled}
1659
- />
1660
- )}
1661
- <PCTextarea
1662
- label={t('additionalNote')}
1663
- placeholder={t('reasonOptional')}
1664
- value={formData.additionalNote}
1665
- onChange={(e) =>
1666
- setFormData({
1667
- ...formData,
1668
- additionalNote: e.target.value,
1669
- })
1670
- }
1671
- style={isPopup ? { minHeight: '60px' } : {}}
1672
- disabled={isPopup && !isPopupFormEnabled}
1673
- />
1674
- <PCCheckbox
1675
- size="sm"
1676
- label={
1677
- <span>
1678
- {t('confirmCheckBox')}{' '}
1679
- <span style={{ color: getVar('--pc-error') }}>*</span>
1680
- </span>
1681
- }
1682
- checked={formData.confirmCheckBox}
1683
- onChange={(e) =>
1684
- setFormData({
1685
- ...formData,
1686
- confirmCheckBox: e.target.checked,
1687
- })
1688
- }
1689
- disabled={isPopup && !isPopupFormEnabled}
1690
- />
1691
- <div
1692
- style={{
1693
- ...pcStyles.flex,
1694
- ...pcStyles.flexRow,
1695
- ...pcStyles.justifyEnd,
1696
- gap: getVar('--pc-spacing-sm'),
1697
- ...(isPopup ? {
1698
- position: 'sticky',
1699
- bottom: 0,
1700
- left: 0,
1701
- right: 0,
1702
- zIndex: 20,
1703
- borderTop: `1px solid ${getVar('--pc-border-color')}`,
1704
- backgroundColor: getVar('--pc-bg-primary'),
1705
- padding: '0.75rem',
1706
- marginLeft: '-0.75rem',
1707
- marginRight: '-0.75rem',
1708
- } : {
1709
- marginTop: pcStyles.mtMd.marginTop,
1710
- }),
1711
- }}
1712
- className="pc-flex-responsive pc-spacing-responsive"
1713
- >
1714
- {isPopup && (
1715
- <PCButton variant="secondary" onClick={onClose} style={{ flexShrink: 0 }}>
1716
- {t('cancel') ?? 'Cancel'}
1717
- </PCButton>
1718
- )}
1719
- <PCButton
1720
- variant="primary"
1721
- onClick={handleSubmitClick}
1722
- disabled={!isFormValid || isLoading}
1723
- isLoading={isLoading}
1724
- style={{
1725
- ...pcStyles.flex,
1726
- ...pcStyles.itemsCenter,
1727
- ...pcStyles.justifyCenter,
1728
- minWidth: '120px',
1729
- flexShrink: 0,
1730
- opacity: (!isFormValid || isLoading) ? 0.5 : 1,
1731
- cursor: (!isFormValid || isLoading) ? 'not-allowed' : (isLoading ? 'wait' : 'pointer'),
1732
- }}
1733
- >
1734
- {t('submit')}
1735
- </PCButton>
1736
- </div>
1737
- </div>
1738
- </FormBlockWrapper>
1739
- </div>
1740
- </div>
1741
- </div>
1742
- )}
1743
- <PCModal
1744
- isOpen={isErasureConsentModalOpen}
1745
- onClose={handleErasureModalClose}
1746
- size="md"
1747
- showCloseButton={false}
1748
- footer={
1749
- <div className="pc-erasure-modal-footer">
1750
- <PCButton
1751
- variant="secondary"
1752
- onClick={() => handleErasureModalChoice(false)}
1753
- disabled={isLoading}
1754
- >
1755
- {t('revokeConsentNo')}
1756
- </PCButton>
1757
- <PCButton
1758
- variant="primary"
1759
- onClick={() => handleErasureModalChoice(true)}
1760
- disabled={isLoading}
1761
- isLoading={isLoading}
1762
- >
1763
- {t('revokeConsentYes')}
1764
- </PCButton>
1765
- </div>
1766
- }
1767
- >
1768
- <div className="pc-erasure-modal-content">
1769
- <div className="pc-erasure-modal-header">
1770
- <div className="pc-erasure-modal-icon">
1771
- <FiAlertCircle size={24} />
1772
- </div>
1773
- <p className="pc-erasure-modal-title">
1774
- {t('revokeConsentModalTitle')}
1775
- </p>
1776
- <p className="pc-erasure-modal-question">
1777
- {t('revokeConsentModalQuestion')}
1778
- </p>
1779
- </div>
1780
-
1781
- <div className="pc-erasure-modal-purposes">
1782
- <p className="pc-erasure-modal-purposes-label">
1783
- {t('revokeConsentModalPurposesLabel')}
1784
- </p>
1785
- {selectedErasurePurposeList.length > 0 ? (
1786
- <>
1787
- {selectedErasurePurposeList
1788
- .slice(0, MAX_DISPLAYED_PURPOSE_NAMES_IN_ERASURE_MODAL)
1789
- .map((name) => (
1790
- <div key={name} className="pc-erasure-modal-purpose-item">
1791
- {name}
1792
- </div>
1793
- ))}
1794
- {selectedErasurePurposeList.length > MAX_DISPLAYED_PURPOSE_NAMES_IN_ERASURE_MODAL && (
1795
- <span className="pc-erasure-modal-purposes-placeholder">
1796
- {t('revokeConsentModalAndMore', {
1797
- count: selectedErasurePurposeList.length - MAX_DISPLAYED_PURPOSE_NAMES_IN_ERASURE_MODAL,
1798
- })}
1799
- </span>
1800
- )}
1801
- </>
1802
- ) : (
1803
- <span className="pc-erasure-modal-purposes-placeholder">
1804
- {t('revokeConsentModalYourPurposes')}
1805
- </span>
1806
- )}
1807
- </div>
1808
-
1809
- <div className="pc-erasure-modal-warning">
1810
- <p className="pc-erasure-modal-warning-text">
1811
- <span className="pc-erasure-modal-warning-label">
1812
- {t('revokeConsentModalIfYesLabel')}
1813
- </span>{' '}
1814
- {t('revokeConsentModalIfYesBody')}
1815
- </p>
1816
- </div>
1817
- </div>
1818
- </PCModal>
1819
- </>
1820
- );
1821
- }