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