@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.
- package/NOTICE.md +17 -0
- package/README.md +141 -3
- package/dist/chunk-FEGTW73Q.mjs +182 -0
- package/dist/{chunk-6A665OPR.mjs → chunk-OGL6OC3A.mjs} +4 -0
- package/dist/fonts.d.mts +35 -0
- package/dist/fonts.d.ts +35 -0
- package/dist/fonts.js +183 -0
- package/dist/fonts.mjs +10 -0
- package/dist/index.d.mts +41 -4
- package/dist/index.d.ts +41 -4
- package/dist/index.js +881 -416
- package/dist/index.mjs +502 -253
- package/dist/montserrat.generated-NBTECYRW.mjs +22 -0
- package/dist/privacy-center.d.mts +51 -11
- package/dist/privacy-center.d.ts +51 -11
- package/dist/privacy-center.js +252 -136
- package/dist/privacy-center.mjs +251 -137
- package/dist/types-6Aj7faxL.d.mts +57 -0
- package/dist/types-6Aj7faxL.d.ts +57 -0
- package/package.json +10 -1
- package/.turbo/turbo-build.log +0 -22
- package/.turbo/turbo-test.log +0 -203
- package/.turbo/turbo-verify-pack.log +0 -6
- package/CHANGELOG.md +0 -343
- package/src/RedactoNoticeAssisted/RedactoNoticeAssisted.test.tsx +0 -934
- package/src/RedactoNoticeAssisted/RedactoNoticeAssisted.tsx +0 -2160
- package/src/RedactoNoticeAssisted/api/index.ts +0 -275
- package/src/RedactoNoticeAssisted/api/types.ts +0 -182
- package/src/RedactoNoticeAssisted/assets/redacto-logo.png +0 -0
- package/src/RedactoNoticeAssisted/constants.ts +0 -51
- package/src/RedactoNoticeAssisted/i18n.ts +0 -1408
- package/src/RedactoNoticeAssisted/index.ts +0 -1
- package/src/RedactoNoticeAssisted/injectStyles.ts +0 -251
- package/src/RedactoNoticeAssisted/styles.ts +0 -730
- package/src/RedactoNoticeAssisted/types.ts +0 -26
- package/src/RedactoNoticeAssisted/useMediaQuery.ts +0 -36
- package/src/RedactoNoticeConsent/RedactoNoticeConsent.test.tsx +0 -1152
- package/src/RedactoNoticeConsent/RedactoNoticeConsent.tsx +0 -4258
- package/src/RedactoNoticeConsent/api/index.ts +0 -512
- package/src/RedactoNoticeConsent/api/types.ts +0 -469
- package/src/RedactoNoticeConsent/assets/redacto-logo.png +0 -0
- package/src/RedactoNoticeConsent/components/DigiLockerCallback.tsx +0 -602
- package/src/RedactoNoticeConsent/components/index.ts +0 -7
- package/src/RedactoNoticeConsent/constants.ts +0 -20
- package/src/RedactoNoticeConsent/index.ts +0 -1
- package/src/RedactoNoticeConsent/injectStyles.ts +0 -151
- package/src/RedactoNoticeConsent/language-codes.ts +0 -95
- package/src/RedactoNoticeConsent/services/storage.ts +0 -122
- package/src/RedactoNoticeConsent/styles.ts +0 -409
- package/src/RedactoNoticeConsent/types.ts +0 -54
- package/src/RedactoNoticeConsent/useMediaQuery.ts +0 -36
- package/src/RedactoNoticeConsentInline/RedactoNoticeConsentInline.test.tsx +0 -369
- package/src/RedactoNoticeConsentInline/RedactoNoticeConsentInline.tsx +0 -687
- package/src/RedactoNoticeConsentInline/api/index.ts +0 -106
- package/src/RedactoNoticeConsentInline/api/types.ts +0 -183
- package/src/RedactoNoticeConsentInline/assets/redacto-logo.png +0 -0
- package/src/RedactoNoticeConsentInline/index.ts +0 -1
- package/src/RedactoNoticeConsentInline/injectStyles.ts +0 -40
- package/src/RedactoNoticeConsentInline/styles.ts +0 -397
- package/src/RedactoNoticeConsentInline/types.ts +0 -51
- package/src/RedactoNoticeConsentInline/useMediaQuery.ts +0 -36
- package/src/RedactoPrivacyCenter/NotificationsPage.tsx +0 -182
- package/src/RedactoPrivacyCenter/PrivacyCenterActivityPage.tsx +0 -19
- package/src/RedactoPrivacyCenter/PrivacyCenterCaseDetailsPage.tsx +0 -156
- package/src/RedactoPrivacyCenter/PrivacyCenterConsentManagerPage.tsx +0 -17
- package/src/RedactoPrivacyCenter/PrivacyCenterFormPage.tsx +0 -79
- package/src/RedactoPrivacyCenter/PrivacyCenterHomePage.tsx +0 -36
- package/src/RedactoPrivacyCenter/PrivacyCenterLayout.tsx +0 -177
- package/src/RedactoPrivacyCenter/PrivacyCenterMyReceiptPage.tsx +0 -26
- package/src/RedactoPrivacyCenter/RedactoPrivacyCenter.tsx +0 -58
- package/src/RedactoPrivacyCenter/api/actions.ts +0 -695
- package/src/RedactoPrivacyCenter/api/client.ts +0 -911
- package/src/RedactoPrivacyCenter/api/config.ts +0 -33
- package/src/RedactoPrivacyCenter/api/fetcher.ts +0 -260
- package/src/RedactoPrivacyCenter/api/orgBranding.ts +0 -16
- package/src/RedactoPrivacyCenter/api/upload.ts +0 -93
- package/src/RedactoPrivacyCenter/assets/character.svg +0 -31
- package/src/RedactoPrivacyCenter/assets/fileAttach.svg +0 -9
- package/src/RedactoPrivacyCenter/assets/greenTick.svg +0 -4
- package/src/RedactoPrivacyCenter/assets/mail.svg +0 -3
- package/src/RedactoPrivacyCenter/assets/poweredByRedacto.svg +0 -9
- package/src/RedactoPrivacyCenter/assets/sentImg.svg +0 -9
- package/src/RedactoPrivacyCenter/assets/uploadFileIcon.svg +0 -9
- package/src/RedactoPrivacyCenter/components/ActivitiesList.tsx +0 -252
- package/src/RedactoPrivacyCenter/components/ActivityTimeline.tsx +0 -239
- package/src/RedactoPrivacyCenter/components/AppShell/AppShell.tsx +0 -268
- package/src/RedactoPrivacyCenter/components/AppShell/MobileNavDrawer.tsx +0 -218
- package/src/RedactoPrivacyCenter/components/AppShell/NavItem.tsx +0 -205
- package/src/RedactoPrivacyCenter/components/AppShell/PageHeader.tsx +0 -107
- package/src/RedactoPrivacyCenter/components/AppShell/SidebarNav.tsx +0 -100
- package/src/RedactoPrivacyCenter/components/AppShell/TopAppBar.tsx +0 -339
- package/src/RedactoPrivacyCenter/components/AppShell/UserMenu.tsx +0 -245
- package/src/RedactoPrivacyCenter/components/AppShell/hooks/useIsTruncated.ts +0 -28
- package/src/RedactoPrivacyCenter/components/AppShell/hooks/useKeyboardShortcut.ts +0 -40
- package/src/RedactoPrivacyCenter/components/AppShell/hooks/useSidebarState.ts +0 -62
- package/src/RedactoPrivacyCenter/components/AppShell/usePrivacyCenterExitAction.ts +0 -36
- package/src/RedactoPrivacyCenter/components/CaseDetails/AttachmentCard.tsx +0 -38
- package/src/RedactoPrivacyCenter/components/CaseDetails/ChatBubble.tsx +0 -31
- package/src/RedactoPrivacyCenter/components/CaseDetails/DocumentRequestCard.tsx +0 -79
- package/src/RedactoPrivacyCenter/components/CaseDetails/FinalizedTooltipWrapper.tsx +0 -25
- package/src/RedactoPrivacyCenter/components/CaseDetails/MessageComposer.tsx +0 -96
- package/src/RedactoPrivacyCenter/components/CaseDetails/MessageItem.tsx +0 -97
- package/src/RedactoPrivacyCenter/components/CaseDetails/MessagesTab.tsx +0 -156
- package/src/RedactoPrivacyCenter/components/CaseDetails/RequestDetailsTab.tsx +0 -171
- package/src/RedactoPrivacyCenter/components/CaseDetails/StatusEvent.tsx +0 -23
- package/src/RedactoPrivacyCenter/components/CaseDetails/useFileUpload.ts +0 -116
- package/src/RedactoPrivacyCenter/components/CaseDetails/useMessagePolling.ts +0 -43
- package/src/RedactoPrivacyCenter/components/CaseDetails/utils.ts +0 -71
- package/src/RedactoPrivacyCenter/components/CaseDetailsModal.tsx +0 -469
- package/src/RedactoPrivacyCenter/components/CaseHistory.tsx +0 -426
- package/src/RedactoPrivacyCenter/components/ConsentManager.tsx +0 -1206
- package/src/RedactoPrivacyCenter/components/ConsentManagerCells.tsx +0 -218
- package/src/RedactoPrivacyCenter/components/ConsentManagerColumns.tsx +0 -138
- package/src/RedactoPrivacyCenter/components/ConsentManagerConstants.ts +0 -7
- package/src/RedactoPrivacyCenter/components/ConsentManagerExpandedRow.tsx +0 -179
- package/src/RedactoPrivacyCenter/components/ConsentManagerGroupedColumns.tsx +0 -318
- package/src/RedactoPrivacyCenter/components/ConsentManagerProductCards.tsx +0 -333
- package/src/RedactoPrivacyCenter/components/ConsentManagerPurposeCards.tsx +0 -102
- package/src/RedactoPrivacyCenter/components/ConsentManagerTableSkeleton.tsx +0 -159
- package/src/RedactoPrivacyCenter/components/DataRequest/CorrectionDataSection.tsx +0 -189
- package/src/RedactoPrivacyCenter/components/DataRequest/GrievanceDataSection.tsx +0 -118
- package/src/RedactoPrivacyCenter/components/DataRequest/NominationDataSection.tsx +0 -66
- package/src/RedactoPrivacyCenter/components/DataRequest/ProductSelectionSection.tsx +0 -222
- package/src/RedactoPrivacyCenter/components/DataRequest/PurposeSelectionSection.tsx +0 -331
- package/src/RedactoPrivacyCenter/components/Form.tsx +0 -1796
- package/src/RedactoPrivacyCenter/components/GrievanceTable.tsx +0 -174
- package/src/RedactoPrivacyCenter/components/GrievanceTablePagination.tsx +0 -337
- package/src/RedactoPrivacyCenter/components/ModifyConsentModal.tsx +0 -334
- package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationBell.tsx +0 -173
- package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationCTAButton.tsx +0 -18
- package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationDropdown.tsx +0 -54
- package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationDropdownItem.tsx +0 -57
- package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationIcon.tsx +0 -19
- package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationListPanel.tsx +0 -92
- package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationRefreshButton.tsx +0 -20
- package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationRow.tsx +0 -58
- package/src/RedactoPrivacyCenter/components/NotificationHub/StatCard.tsx +0 -10
- package/src/RedactoPrivacyCenter/components/NotificationHub/useNotificationActions.ts +0 -112
- package/src/RedactoPrivacyCenter/components/NotificationHub/utils.ts +0 -27
- package/src/RedactoPrivacyCenter/components/Pagination.tsx +0 -140
- package/src/RedactoPrivacyCenter/components/PrivacyCenterEmptyState.tsx +0 -76
- package/src/RedactoPrivacyCenter/components/PrivacyCenterSessionExpired.tsx +0 -97
- package/src/RedactoPrivacyCenter/components/ReceiptsList.tsx +0 -454
- package/src/RedactoPrivacyCenter/components/RequestSubmit.tsx +0 -270
- package/src/RedactoPrivacyCenter/components/SelectUser.tsx +0 -95
- package/src/RedactoPrivacyCenter/components/SelectUserGate.tsx +0 -187
- package/src/RedactoPrivacyCenter/components/SideMenuBarPC/constants.tsx +0 -25
- package/src/RedactoPrivacyCenter/components/SideMenuBarPC/types.ts +0 -21
- package/src/RedactoPrivacyCenter/components/caseHistoryColumns.tsx +0 -159
- package/src/RedactoPrivacyCenter/components/constant.ts +0 -52
- package/src/RedactoPrivacyCenter/components/groupHelpers.ts +0 -119
- package/src/RedactoPrivacyCenter/components/receiptColumns.tsx +0 -203
- package/src/RedactoPrivacyCenter/components/types.ts +0 -123
- package/src/RedactoPrivacyCenter/components/utils.tsx +0 -10
- package/src/RedactoPrivacyCenter/config/routes.ts +0 -9
- package/src/RedactoPrivacyCenter/config/table.ts +0 -25
- package/src/RedactoPrivacyCenter/context/AuthContext.tsx +0 -160
- package/src/RedactoPrivacyCenter/context/BaseUrlContext.tsx +0 -47
- package/src/RedactoPrivacyCenter/context/BrandingContext.tsx +0 -52
- package/src/RedactoPrivacyCenter/context/DataAvailabilityContext.session-expired.test.tsx +0 -92
- package/src/RedactoPrivacyCenter/context/DataAvailabilityContext.tsx +0 -181
- package/src/RedactoPrivacyCenter/context/LocaleContext.tsx +0 -11
- package/src/RedactoPrivacyCenter/context/NavigationContext.tsx +0 -96
- package/src/RedactoPrivacyCenter/context/OrganizationContext.tsx +0 -30
- package/src/RedactoPrivacyCenter/context/PageChromeContext.tsx +0 -80
- package/src/RedactoPrivacyCenter/context/ProfileSwitchContext.tsx +0 -15
- package/src/RedactoPrivacyCenter/context/types.ts +0 -19
- package/src/RedactoPrivacyCenter/lib/api.ts +0 -759
- package/src/RedactoPrivacyCenter/lib/consent-manager/utils.tsx +0 -67
- package/src/RedactoPrivacyCenter/lib/constants.ts +0 -276
- package/src/RedactoPrivacyCenter/lib/i18n.ts +0 -81
- package/src/RedactoPrivacyCenter/lib/locales.ts +0 -102
- package/src/RedactoPrivacyCenter/lib/queries.ts +0 -141
- package/src/RedactoPrivacyCenter/lib/types.ts +0 -876
- package/src/RedactoPrivacyCenter/lib/useCurrentLanguage.ts +0 -37
- package/src/RedactoPrivacyCenter/lib/utils.refresh-circuit.test.ts +0 -127
- package/src/RedactoPrivacyCenter/lib/utils.refresh-storage.test.ts +0 -85
- package/src/RedactoPrivacyCenter/lib/utils.session-expired.test.ts +0 -109
- package/src/RedactoPrivacyCenter/lib/utils.ts +0 -783
- package/src/RedactoPrivacyCenter/locales/as/translation.json +0 -342
- package/src/RedactoPrivacyCenter/locales/bn/translation.json +0 -342
- package/src/RedactoPrivacyCenter/locales/brx/translation.json +0 -342
- package/src/RedactoPrivacyCenter/locales/doi/translation.json +0 -342
- package/src/RedactoPrivacyCenter/locales/en/translation.json +0 -357
- package/src/RedactoPrivacyCenter/locales/gom/translation.json +0 -342
- package/src/RedactoPrivacyCenter/locales/gu/translation.json +0 -342
- package/src/RedactoPrivacyCenter/locales/hi/translation.json +0 -345
- package/src/RedactoPrivacyCenter/locales/kn/translation.json +0 -342
- package/src/RedactoPrivacyCenter/locales/ks/translation.json +0 -342
- package/src/RedactoPrivacyCenter/locales/mai/translation.json +0 -342
- package/src/RedactoPrivacyCenter/locales/ml/translation.json +0 -342
- package/src/RedactoPrivacyCenter/locales/mni-Mtei/translation.json +0 -342
- package/src/RedactoPrivacyCenter/locales/mr/translation.json +0 -342
- package/src/RedactoPrivacyCenter/locales/ne/translation.json +0 -342
- package/src/RedactoPrivacyCenter/locales/or/translation.json +0 -342
- package/src/RedactoPrivacyCenter/locales/pa/translation.json +0 -342
- package/src/RedactoPrivacyCenter/locales/sa/translation.json +0 -342
- package/src/RedactoPrivacyCenter/locales/sat/translation.json +0 -342
- package/src/RedactoPrivacyCenter/locales/sd/translation.json +0 -342
- package/src/RedactoPrivacyCenter/locales/ta/translation.json +0 -342
- package/src/RedactoPrivacyCenter/locales/te/translation.json +0 -342
- package/src/RedactoPrivacyCenter/locales/ur/translation.json +0 -342
- package/src/RedactoPrivacyCenter/styles/applyBrandingTheme.ts +0 -66
- package/src/RedactoPrivacyCenter/styles/base.css +0 -117
- package/src/RedactoPrivacyCenter/styles/injectStyles.ts +0 -3108
- package/src/RedactoPrivacyCenter/styles/pcStyles.ts +0 -831
- package/src/RedactoPrivacyCenter/styles/responsive.ts +0 -138
- package/src/RedactoPrivacyCenter/styles/variables.css +0 -96
- package/src/RedactoPrivacyCenter/table/AdvancedTable.tsx +0 -387
- package/src/RedactoPrivacyCenter/table/AdvancedTablePagination.tsx +0 -468
- package/src/RedactoPrivacyCenter/table/index.ts +0 -2
- package/src/RedactoPrivacyCenter/types/images.d.ts +0 -29
- package/src/RedactoPrivacyCenter/ui/Footer.tsx +0 -69
- package/src/RedactoPrivacyCenter/ui/FormBlockWrapper.tsx +0 -54
- package/src/RedactoPrivacyCenter/ui/PCAccordion.tsx +0 -93
- package/src/RedactoPrivacyCenter/ui/PCActionIcon.tsx +0 -47
- package/src/RedactoPrivacyCenter/ui/PCAvatar.test.tsx +0 -83
- package/src/RedactoPrivacyCenter/ui/PCAvatar.tsx +0 -93
- package/src/RedactoPrivacyCenter/ui/PCBadge.tsx +0 -48
- package/src/RedactoPrivacyCenter/ui/PCButton.tsx +0 -77
- package/src/RedactoPrivacyCenter/ui/PCCheckbox.tsx +0 -55
- package/src/RedactoPrivacyCenter/ui/PCCollapse.tsx +0 -107
- package/src/RedactoPrivacyCenter/ui/PCEmpty.tsx +0 -35
- package/src/RedactoPrivacyCenter/ui/PCInput.tsx +0 -132
- package/src/RedactoPrivacyCenter/ui/PCLoader.tsx +0 -31
- package/src/RedactoPrivacyCenter/ui/PCModal.tsx +0 -106
- package/src/RedactoPrivacyCenter/ui/PCPopover.tsx +0 -206
- package/src/RedactoPrivacyCenter/ui/PCSelect.tsx +0 -433
- package/src/RedactoPrivacyCenter/ui/PCTable.tsx +0 -149
- package/src/RedactoPrivacyCenter/ui/PCText.tsx +0 -41
- package/src/RedactoPrivacyCenter/ui/PCTextarea.tsx +0 -43
- package/src/RedactoPrivacyCenter/ui/PCTitle.tsx +0 -37
- package/src/RedactoPrivacyCenter/ui/PCTooltip.tsx +0 -121
- package/src/RedactoPrivacyCenter/ui/SimpleBar.tsx +0 -25
- package/src/RedactoPrivacyCenter/ui/StatusBadge.tsx +0 -53
- package/src/RedactoPrivacyCenter/ui/index.ts +0 -18
- package/src/RedactoPrivacyCenter/utils/cn.ts +0 -28
- package/src/RedactoPrivacyCenter/utils/date.ts +0 -48
- package/src/RedactoPrivacyCenter/utils/status.ts +0 -111
- package/src/RedactoPrivacyCenter/utils/toast.ts +0 -130
- package/src/index.ts +0 -19
- package/src/privacy-center.ts +0 -113
- package/src/shared/api-errors.ts +0 -180
- package/src/shared/notice-consent-status.ts +0 -190
- package/src/types/css.d.ts +0 -4
- package/src/types/images.d.ts +0 -19
- package/src/types/react-jsx.d.ts +0 -26
- package/tests/DataAvailabilityContext.test.tsx +0 -123
- package/tests/Form.test.tsx +0 -1287
- package/tests/ManageConsentErrorMapping.test.tsx +0 -85
- package/tests/ProductSelectionSection.test.tsx +0 -328
- package/tests/RequestSubmit.test.tsx +0 -135
- package/tests/RevocablePurpose.test.tsx +0 -146
- package/tests/RevokeWarningMessage.test.tsx +0 -151
- package/tests/consentClient.test.ts +0 -217
- package/tests/manageConsentServer.test.ts +0 -38
- package/tests/mocks.ts +0 -257
- package/tests/resolveContactFromJwt.test.ts +0 -44
- package/tests/setup.ts +0 -16
- package/tsconfig.json +0 -16
- package/tsup.config.ts +0 -31
- 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
|
-
}
|