@redacto.io/consent-sdk-react 3.5.0 → 3.6.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 (152) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/LICENSE +176 -0
  3. package/dist/index.d.mts +51 -4
  4. package/dist/index.d.ts +51 -4
  5. package/dist/index.js +1891 -243
  6. package/dist/index.mjs +1890 -243
  7. package/dist/privacy-center.d.mts +566 -0
  8. package/dist/privacy-center.d.ts +566 -0
  9. package/dist/privacy-center.js +17174 -0
  10. package/dist/privacy-center.mjs +17136 -0
  11. package/package.json +47 -3
  12. package/src/RedactoNoticeConsent/RedactoNoticeConsent.test.tsx +201 -118
  13. package/src/RedactoNoticeConsent/RedactoNoticeConsent.tsx +1194 -93
  14. package/src/RedactoNoticeConsent/api/index.ts +335 -206
  15. package/src/RedactoNoticeConsent/api/types.ts +140 -16
  16. package/src/RedactoNoticeConsent/components/DigiLockerCallback.tsx +602 -0
  17. package/src/RedactoNoticeConsent/components/index.ts +7 -0
  18. package/src/RedactoNoticeConsent/injectStyles.ts +11 -0
  19. package/src/RedactoNoticeConsent/language-codes.ts +61 -0
  20. package/src/RedactoNoticeConsent/services/storage.ts +122 -0
  21. package/src/RedactoNoticeConsent/types.ts +2 -0
  22. package/src/RedactoPrivacyCenter/PrivacyCenterActivityPage.tsx +19 -0
  23. package/src/RedactoPrivacyCenter/PrivacyCenterConsentManagerPage.tsx +17 -0
  24. package/src/RedactoPrivacyCenter/PrivacyCenterFormPage.tsx +79 -0
  25. package/src/RedactoPrivacyCenter/PrivacyCenterHomePage.tsx +27 -0
  26. package/src/RedactoPrivacyCenter/PrivacyCenterLayout.tsx +142 -0
  27. package/src/RedactoPrivacyCenter/RedactoPrivacyCenter.tsx +51 -0
  28. package/src/RedactoPrivacyCenter/api/actions.ts +444 -0
  29. package/src/RedactoPrivacyCenter/api/client.ts +413 -0
  30. package/src/RedactoPrivacyCenter/api/config.ts +33 -0
  31. package/src/RedactoPrivacyCenter/api/fetcher.ts +181 -0
  32. package/src/RedactoPrivacyCenter/api/upload.ts +93 -0
  33. package/src/RedactoPrivacyCenter/assets/character.svg +31 -0
  34. package/src/RedactoPrivacyCenter/assets/fileAttach.svg +9 -0
  35. package/src/RedactoPrivacyCenter/assets/greenTick.svg +4 -0
  36. package/src/RedactoPrivacyCenter/assets/mail.svg +3 -0
  37. package/src/RedactoPrivacyCenter/assets/poweredByRedacto.svg +9 -0
  38. package/src/RedactoPrivacyCenter/assets/sentImg.svg +9 -0
  39. package/src/RedactoPrivacyCenter/assets/uploadFileIcon.svg +9 -0
  40. package/src/RedactoPrivacyCenter/components/ActivitiesList.tsx +272 -0
  41. package/src/RedactoPrivacyCenter/components/CaseDetailsModal.tsx +468 -0
  42. package/src/RedactoPrivacyCenter/components/CaseHistory.tsx +433 -0
  43. package/src/RedactoPrivacyCenter/components/ConsentManager.tsx +726 -0
  44. package/src/RedactoPrivacyCenter/components/ConsentManagerColumns.tsx +239 -0
  45. package/src/RedactoPrivacyCenter/components/ConsentManagerConstants.ts +7 -0
  46. package/src/RedactoPrivacyCenter/components/ConsentManagerTableSkeleton.tsx +155 -0
  47. package/src/RedactoPrivacyCenter/components/DataRequest/CorrectionDataSection.tsx +189 -0
  48. package/src/RedactoPrivacyCenter/components/DataRequest/GrievanceDataSection.tsx +118 -0
  49. package/src/RedactoPrivacyCenter/components/DataRequest/NominationDataSection.tsx +66 -0
  50. package/src/RedactoPrivacyCenter/components/DataRequest/PurposeSelectionSection.tsx +305 -0
  51. package/src/RedactoPrivacyCenter/components/Form.tsx +1455 -0
  52. package/src/RedactoPrivacyCenter/components/GrievanceTable.tsx +176 -0
  53. package/src/RedactoPrivacyCenter/components/GrievanceTablePagination.tsx +330 -0
  54. package/src/RedactoPrivacyCenter/components/ModifyConsentModal.tsx +329 -0
  55. package/src/RedactoPrivacyCenter/components/Pagination.tsx +140 -0
  56. package/src/RedactoPrivacyCenter/components/PrivacyCenterLayout.tsx +302 -0
  57. package/src/RedactoPrivacyCenter/components/RequestSubmit.tsx +218 -0
  58. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/SideBarPC.tsx +475 -0
  59. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/SideMenuPC.tsx +412 -0
  60. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/constants.tsx +42 -0
  61. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/menuItemsPC.tsx +1 -0
  62. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/types.ts +40 -0
  63. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/utils.ts +38 -0
  64. package/src/RedactoPrivacyCenter/components/ThemeTransitionOverlay.tsx +85 -0
  65. package/src/RedactoPrivacyCenter/components/activitiesColumns.tsx +141 -0
  66. package/src/RedactoPrivacyCenter/components/caseHistoryColumns.tsx +179 -0
  67. package/src/RedactoPrivacyCenter/components/constant.ts +20 -0
  68. package/src/RedactoPrivacyCenter/components/types.ts +68 -0
  69. package/src/RedactoPrivacyCenter/components/utils.tsx +10 -0
  70. package/src/RedactoPrivacyCenter/config/routes.ts +9 -0
  71. package/src/RedactoPrivacyCenter/config/table.ts +25 -0
  72. package/src/RedactoPrivacyCenter/context/AuthContext.tsx +123 -0
  73. package/src/RedactoPrivacyCenter/context/BaseUrlContext.tsx +44 -0
  74. package/src/RedactoPrivacyCenter/context/LocaleContext.tsx +11 -0
  75. package/src/RedactoPrivacyCenter/context/NavigationContext.tsx +64 -0
  76. package/src/RedactoPrivacyCenter/context/OrganizationContext.tsx +30 -0
  77. package/src/RedactoPrivacyCenter/context/PageChromeContext.tsx +60 -0
  78. package/src/RedactoPrivacyCenter/context/theme.tsx +127 -0
  79. package/src/RedactoPrivacyCenter/lib/api.ts +468 -0
  80. package/src/RedactoPrivacyCenter/lib/consent-manager/utils.tsx +63 -0
  81. package/src/RedactoPrivacyCenter/lib/constants.ts +116 -0
  82. package/src/RedactoPrivacyCenter/lib/i18n.ts +81 -0
  83. package/src/RedactoPrivacyCenter/lib/locales.ts +105 -0
  84. package/src/RedactoPrivacyCenter/lib/queries.ts +30 -0
  85. package/src/RedactoPrivacyCenter/lib/types.ts +346 -0
  86. package/src/RedactoPrivacyCenter/lib/useCurrentLanguage.ts +37 -0
  87. package/src/RedactoPrivacyCenter/lib/utils.ts +625 -0
  88. package/src/RedactoPrivacyCenter/locales/as/translation.json +188 -0
  89. package/src/RedactoPrivacyCenter/locales/bho/translation.json +188 -0
  90. package/src/RedactoPrivacyCenter/locales/bn/translation.json +188 -0
  91. package/src/RedactoPrivacyCenter/locales/brx/translation.json +188 -0
  92. package/src/RedactoPrivacyCenter/locales/doi/translation.json +188 -0
  93. package/src/RedactoPrivacyCenter/locales/en/translation.json +188 -0
  94. package/src/RedactoPrivacyCenter/locales/gom/translation.json +188 -0
  95. package/src/RedactoPrivacyCenter/locales/gu/translation.json +188 -0
  96. package/src/RedactoPrivacyCenter/locales/hi/translation.json +188 -0
  97. package/src/RedactoPrivacyCenter/locales/kn/translation.json +188 -0
  98. package/src/RedactoPrivacyCenter/locales/ks/translation.json +188 -0
  99. package/src/RedactoPrivacyCenter/locales/mai/translation.json +188 -0
  100. package/src/RedactoPrivacyCenter/locales/ml/translation.json +188 -0
  101. package/src/RedactoPrivacyCenter/locales/mni-Mtei/translation.json +188 -0
  102. package/src/RedactoPrivacyCenter/locales/mr/translation.json +188 -0
  103. package/src/RedactoPrivacyCenter/locales/ne/translation.json +188 -0
  104. package/src/RedactoPrivacyCenter/locales/or/translation.json +188 -0
  105. package/src/RedactoPrivacyCenter/locales/pa/translation.json +188 -0
  106. package/src/RedactoPrivacyCenter/locales/sa/translation.json +188 -0
  107. package/src/RedactoPrivacyCenter/locales/sat/translation.json +188 -0
  108. package/src/RedactoPrivacyCenter/locales/sd/translation.json +188 -0
  109. package/src/RedactoPrivacyCenter/locales/ta/translation.json +188 -0
  110. package/src/RedactoPrivacyCenter/locales/te/translation.json +188 -0
  111. package/src/RedactoPrivacyCenter/locales/ur/translation.json +188 -0
  112. package/src/RedactoPrivacyCenter/styles/base.css +117 -0
  113. package/src/RedactoPrivacyCenter/styles/injectStyles.ts +1000 -0
  114. package/src/RedactoPrivacyCenter/styles/pcStyles.ts +819 -0
  115. package/src/RedactoPrivacyCenter/styles/responsive.ts +138 -0
  116. package/src/RedactoPrivacyCenter/styles/variables.css +133 -0
  117. package/src/RedactoPrivacyCenter/table/AdvancedTable.tsx +365 -0
  118. package/src/RedactoPrivacyCenter/table/AdvancedTablePagination.tsx +479 -0
  119. package/src/RedactoPrivacyCenter/table/index.ts +2 -0
  120. package/src/RedactoPrivacyCenter/types/images.d.ts +29 -0
  121. package/src/RedactoPrivacyCenter/ui/Footer.tsx +73 -0
  122. package/src/RedactoPrivacyCenter/ui/FormBlockWrapper.tsx +54 -0
  123. package/src/RedactoPrivacyCenter/ui/PCAccordion.tsx +87 -0
  124. package/src/RedactoPrivacyCenter/ui/PCActionIcon.tsx +47 -0
  125. package/src/RedactoPrivacyCenter/ui/PCAvatar.tsx +69 -0
  126. package/src/RedactoPrivacyCenter/ui/PCBadge.tsx +48 -0
  127. package/src/RedactoPrivacyCenter/ui/PCButton.tsx +77 -0
  128. package/src/RedactoPrivacyCenter/ui/PCCheckbox.tsx +55 -0
  129. package/src/RedactoPrivacyCenter/ui/PCCollapse.tsx +97 -0
  130. package/src/RedactoPrivacyCenter/ui/PCEmpty.tsx +35 -0
  131. package/src/RedactoPrivacyCenter/ui/PCInput.tsx +128 -0
  132. package/src/RedactoPrivacyCenter/ui/PCLoader.tsx +30 -0
  133. package/src/RedactoPrivacyCenter/ui/PCModal.tsx +104 -0
  134. package/src/RedactoPrivacyCenter/ui/PCPopover.tsx +175 -0
  135. package/src/RedactoPrivacyCenter/ui/PCSelect.tsx +381 -0
  136. package/src/RedactoPrivacyCenter/ui/PCTable.tsx +137 -0
  137. package/src/RedactoPrivacyCenter/ui/PCText.tsx +39 -0
  138. package/src/RedactoPrivacyCenter/ui/PCTextarea.tsx +43 -0
  139. package/src/RedactoPrivacyCenter/ui/PCTitle.tsx +37 -0
  140. package/src/RedactoPrivacyCenter/ui/PCTooltip.tsx +119 -0
  141. package/src/RedactoPrivacyCenter/ui/SimpleBar.tsx +25 -0
  142. package/src/RedactoPrivacyCenter/ui/StatusBadge.tsx +55 -0
  143. package/src/RedactoPrivacyCenter/ui/ThemeToggle.tsx +70 -0
  144. package/src/RedactoPrivacyCenter/ui/index.ts +18 -0
  145. package/src/RedactoPrivacyCenter/utils/cn.ts +28 -0
  146. package/src/RedactoPrivacyCenter/utils/date.ts +48 -0
  147. package/src/RedactoPrivacyCenter/utils/status.ts +111 -0
  148. package/src/RedactoPrivacyCenter/utils/toast.ts +130 -0
  149. package/src/index.ts +10 -0
  150. package/src/privacy-center.ts +107 -0
  151. package/tsconfig.json +2 -0
  152. package/tsup.config.ts +16 -1
@@ -0,0 +1,329 @@
1
+ import { useState, useMemo, useEffect } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { PCButton, PCText, PCModal, PCAccordion, PCBadge, PCCheckbox } from '../ui';
4
+ import { pcStyles } from '../styles/pcStyles';
5
+ import { ChevronDownIcon } from 'lucide-react';
6
+ import { PiWarningCircleBold } from 'react-icons/pi';
7
+ import type { ModifyConsentModalProps } from './types';
8
+ import { ConsentStatusEnum } from '../lib/types';
9
+ import type { ConsentAction } from '../lib/types';
10
+
11
+ export const ModifyConsentModal = ({
12
+ isOpen,
13
+ onClose,
14
+ selectedRecord,
15
+ isLoading,
16
+ onManageConsent,
17
+ }: ModifyConsentModalProps) => {
18
+ const { t } = useTranslation();
19
+ const [isAccordionOpen, setIsAccordionOpen] = useState(false);
20
+ const [selectedDataElementUuids, setSelectedDataElementUuids] = useState<string[]>([]);
21
+
22
+ const nominatorInfo = selectedRecord?.nominator_info;
23
+ const nominatorLabel = nominatorInfo?.name || nominatorInfo?.email || nominatorInfo?.org_user_id || null;
24
+
25
+ const actionConfig = useMemo(() => {
26
+ if (!selectedRecord) return null;
27
+
28
+ const status = selectedRecord.status;
29
+
30
+ if (status === ConsentStatusEnum.ACTIVE) {
31
+ return {
32
+ action: 'revoke' as ConsentAction,
33
+ buttonLabel: t('revokeConsent'),
34
+ buttonVariant: 'danger' as const,
35
+ warningMessage: t('revokeConsentWarning'),
36
+ description: nominatorLabel
37
+ ? t('aboutToModifyConsentOnBehalf', { nominator: nominatorLabel })
38
+ : t('aboutToModifyConsent'),
39
+ };
40
+ }
41
+
42
+ if (status === ConsentStatusEnum.WITHDRAWN) {
43
+ return {
44
+ action: 'regrant' as ConsentAction,
45
+ buttonLabel: t('regrantConsent'),
46
+ buttonVariant: 'primary' as const,
47
+ warningMessage: t('regrantConsentWarning'),
48
+ description: nominatorLabel
49
+ ? t('aboutToRegrantConsentOnBehalf', { nominator: nominatorLabel })
50
+ : t('aboutToRegrantConsent'),
51
+ };
52
+ }
53
+
54
+ if (status === ConsentStatusEnum.EXPIRED) {
55
+ return {
56
+ action: 'renew' as ConsentAction,
57
+ buttonLabel: t('renewConsent'),
58
+ buttonVariant: 'primary' as const,
59
+ warningMessage: t('renewConsentWarning'),
60
+ description: nominatorLabel
61
+ ? t('aboutToRenewConsentOnBehalf', { nominator: nominatorLabel })
62
+ : t('aboutToRenewConsent'),
63
+ };
64
+ }
65
+
66
+ // DECLINED or unknown statuses — no action available
67
+ return null;
68
+ }, [selectedRecord, t, nominatorLabel]);
69
+
70
+ const isRegrantAction = actionConfig?.action === 'regrant';
71
+
72
+ useEffect(() => {
73
+ setIsAccordionOpen(isRegrantAction);
74
+ }, [selectedRecord, isRegrantAction]);
75
+
76
+ const regrantEligibleDataElements = useMemo(
77
+ () =>
78
+ (selectedRecord?.data_elements || []).filter(
79
+ (element) => Boolean(element?.uuid) && element.enabled
80
+ ),
81
+ [selectedRecord]
82
+ );
83
+
84
+ const requiredDataElementUuids = useMemo(
85
+ () =>
86
+ regrantEligibleDataElements
87
+ .filter((element) => element.required)
88
+ .map((element) => element.uuid),
89
+ [regrantEligibleDataElements]
90
+ );
91
+
92
+ const requiredDataElementUuidSet = useMemo(
93
+ () => new Set(requiredDataElementUuids),
94
+ [requiredDataElementUuids]
95
+ );
96
+
97
+ useEffect(() => {
98
+ if (isRegrantAction) {
99
+ setSelectedDataElementUuids(regrantEligibleDataElements.map((element) => element.uuid));
100
+ return;
101
+ }
102
+ setSelectedDataElementUuids([]);
103
+ }, [isRegrantAction, regrantEligibleDataElements]);
104
+
105
+ const toggleDataElementSelection = (uuid: string, checked: boolean) => {
106
+ if (requiredDataElementUuidSet.has(uuid)) {
107
+ return;
108
+ }
109
+ setSelectedDataElementUuids((prev) => {
110
+ if (checked) {
111
+ return prev.includes(uuid) ? prev : [...prev, uuid];
112
+ }
113
+ return prev.filter((item) => item !== uuid);
114
+ });
115
+ };
116
+
117
+ return (
118
+ <PCModal
119
+ isOpen={isOpen}
120
+ onClose={onClose}
121
+ title={
122
+ <PCText size="base" weight="semibold">
123
+ {t('modifyConsent')}
124
+ </PCText>
125
+ }
126
+ >
127
+ <div style={{ padding: pcStyles.pMd.padding }}>
128
+ {selectedRecord && !actionConfig && (
129
+ <PCText size="sm" weight="medium" color="tertiary" style={{ textAlign: 'center', padding: pcStyles.pMd.padding }}>
130
+ {t('noActionsAvailable', 'No actions available for this record.')}
131
+ </PCText>
132
+ )}
133
+ {selectedRecord && actionConfig && (
134
+ <>
135
+ <PCText size="sm" weight="medium" color="secondary" style={{ marginBottom: pcStyles.mbMd.marginBottom }}>
136
+ {actionConfig.description}
137
+ </PCText>
138
+ <div style={{
139
+ marginTop: pcStyles.mtMd.marginTop,
140
+ borderRadius: pcStyles.borderRadiusMd,
141
+ ...pcStyles.bgSecondary,
142
+ border: `1px solid ${pcStyles.borderColor}`,
143
+ padding: pcStyles.pMd.padding,
144
+ ...pcStyles.overflowHidden,
145
+ }}>
146
+ <PCAccordion style={{ border: 'none', borderRadius: 0 }}>
147
+ <PCAccordion.Header
148
+ open={isAccordionOpen}
149
+ onToggle={() => setIsAccordionOpen(!isAccordionOpen)}
150
+ >
151
+ {({ open }) => (
152
+ <>
153
+ <div style={{ flex: 1 }}>
154
+ <p style={{
155
+ marginBottom: pcStyles.mbXs.marginBottom,
156
+ fontSize: pcStyles.textBase.fontSize,
157
+ fontWeight: pcStyles.fontWeightSemibold.fontWeight,
158
+ color: pcStyles.textPrimary.color,
159
+ }}>
160
+ {selectedRecord.purpose}
161
+ </p>
162
+ <p style={{
163
+ textAlign: 'left',
164
+ fontSize: pcStyles.textSm.fontSize,
165
+ fontWeight: pcStyles.fontWeightMedium.fontWeight,
166
+ color: pcStyles.textSecondary.color,
167
+ margin: 0,
168
+ }}>
169
+ {selectedRecord.purpose_description}
170
+ </p>
171
+ </div>
172
+ <ChevronDownIcon
173
+ size={20}
174
+ style={{
175
+ transform: open ? 'rotate(0deg)' : 'rotate(-90deg)',
176
+ transition: 'transform 0.3s',
177
+ color: pcStyles.textSecondary.color,
178
+ flexShrink: 0,
179
+ marginLeft: pcStyles.mlSm.marginLeft,
180
+ }}
181
+ />
182
+ </>
183
+ )}
184
+ </PCAccordion.Header>
185
+
186
+ <PCAccordion.Body open={isAccordionOpen}>
187
+ <div style={{
188
+ marginTop: pcStyles.mtSm.marginTop,
189
+ paddingTop: pcStyles.ptSm.paddingTop,
190
+ ...(!isRegrantAction && { borderTop: `1px solid ${pcStyles.borderColor}` }),
191
+ }}>
192
+ {isRegrantAction && regrantEligibleDataElements.length > 0 ? (
193
+ <div>
194
+ <PCText
195
+ size="sm"
196
+ color="secondary"
197
+ style={{ marginTop: 0, marginBottom: pcStyles.mbSm.marginBottom }}
198
+ >
199
+ {t(
200
+ 'selectDataElementsForRegrant',
201
+ 'Select the data elements you want to regrant consent for.'
202
+ )}
203
+ </PCText>
204
+ <div style={{ display: 'grid', gap: pcStyles.gapSm.gap }}>
205
+ {regrantEligibleDataElements.map((element) => {
206
+ const isRequired = requiredDataElementUuidSet.has(element.uuid);
207
+ const isChecked = isRequired || selectedDataElementUuids.includes(element.uuid);
208
+
209
+ return (
210
+ <PCCheckbox
211
+ key={element.uuid}
212
+ size="md"
213
+ checked={isChecked}
214
+ disabled={isRequired}
215
+ onChange={(event) =>
216
+ toggleDataElementSelection(element.uuid, event.target.checked)
217
+ }
218
+ label={
219
+ isRequired
220
+ ? `${element.name} (${t('mandatory', 'Mandatory')})`
221
+ : element.name
222
+ }
223
+ labelClassName="text-sm"
224
+ style={{ accentColor: pcStyles.primary }}
225
+ />
226
+ );
227
+ })}
228
+ </div>
229
+ </div>
230
+ ) : Array.isArray(selectedRecord.data_elements) &&
231
+ selectedRecord.data_elements.filter((el) => el.selected).length > 0 ? (
232
+ selectedRecord.data_elements
233
+ .filter((el) => el.selected)
234
+ .map((i, index, filteredArray) => (
235
+ <div
236
+ key={index}
237
+ style={{
238
+ ...pcStyles.flex,
239
+ ...pcStyles.itemsCenter,
240
+ paddingBottom: index < filteredArray.length - 1 ? pcStyles.pbXs.paddingBottom : 0,
241
+ marginBottom: index < filteredArray.length - 1 ? pcStyles.mbXs.marginBottom : 0,
242
+ }}
243
+ >
244
+ <PCBadge renderAsDot size="sm">{''}</PCBadge>
245
+ <span style={{
246
+ marginLeft: pcStyles.mlSm.marginLeft,
247
+ fontSize: pcStyles.textSm.fontSize,
248
+ color: pcStyles.textPrimary.color,
249
+ fontWeight: pcStyles.fontWeightMedium.fontWeight,
250
+ }}>
251
+ {i.name}
252
+ </span>
253
+ </div>
254
+ ))
255
+ ) : (
256
+ <PCText size="sm" color="tertiary">
257
+ {t('noDataElementsAvailable')}
258
+ </PCText>
259
+ )}
260
+ </div>
261
+ </PCAccordion.Body>
262
+ </PCAccordion>
263
+ </div>
264
+ <div style={{
265
+ marginTop: pcStyles.mtLg.marginTop,
266
+ ...pcStyles.flex,
267
+ ...pcStyles.itemsStart,
268
+ ...pcStyles.gapSm,
269
+ borderRadius: pcStyles.borderRadiusMd,
270
+ border: `1px solid ${pcStyles.warningBorder}`,
271
+ backgroundColor: pcStyles.warningBg,
272
+ padding: pcStyles.pMd.padding,
273
+ lineHeight: 1.5,
274
+ }}>
275
+ <PiWarningCircleBold
276
+ size={20}
277
+ style={{
278
+ flexShrink: 0,
279
+ color: pcStyles.warningText,
280
+ marginTop: '2px',
281
+ }}
282
+ />
283
+ <span style={{
284
+ fontWeight: pcStyles.fontWeightMedium.fontWeight,
285
+ fontSize: pcStyles.textSm.fontSize,
286
+ color: pcStyles.warningText,
287
+ }}>
288
+ {actionConfig.warningMessage}
289
+ </span>
290
+ </div>
291
+
292
+ <div style={{
293
+ marginTop: pcStyles.mtLg.marginTop,
294
+ ...pcStyles.flex,
295
+ ...pcStyles.flexRow,
296
+ ...pcStyles.gapSm,
297
+ ...pcStyles.justifyEnd,
298
+ }}>
299
+ <PCButton variant="outline" onClick={onClose}>
300
+ {t('cancel')}
301
+ </PCButton>
302
+ <PCButton
303
+ isLoading={isLoading}
304
+ disabled={
305
+ !selectedRecord.purpose_uuid ||
306
+ (isRegrantAction && selectedDataElementUuids.length === 0)
307
+ }
308
+ variant={actionConfig.buttonVariant}
309
+ onClick={() => {
310
+ if (selectedRecord.purpose_uuid) {
311
+ const nomContact = selectedRecord.nominator_info?.email || selectedRecord.nominator_info?.org_user_id || undefined;
312
+ onManageConsent(
313
+ selectedRecord.purpose_uuid,
314
+ actionConfig.action,
315
+ nomContact,
316
+ isRegrantAction ? selectedDataElementUuids : undefined
317
+ );
318
+ }
319
+ }}
320
+ >
321
+ {actionConfig.buttonLabel}
322
+ </PCButton>
323
+ </div>
324
+ </>
325
+ )}
326
+ </div>
327
+ </PCModal>
328
+ );
329
+ };
@@ -0,0 +1,140 @@
1
+ import { PCActionIcon, PCSelect, PCText, type SelectOption } from '../ui';
2
+ import {
3
+ ChevronLeft,
4
+ ChevronRight,
5
+ ChevronsLeft,
6
+ ChevronsRight,
7
+ } from 'lucide-react';
8
+ import { cn } from '../utils/cn';
9
+ import { pcStyles } from '../styles/pcStyles';
10
+ import { PAGE_SIZE_OPTIONS } from './constant';
11
+ import type { PaginationProps } from './types';
12
+
13
+ export default function Pagination({
14
+ currentPage,
15
+ totalPages,
16
+ totalRecords,
17
+ pageSize,
18
+ onPageChange,
19
+ onPageSizeChange,
20
+ className,
21
+ }: PaginationProps) {
22
+ const handlePageChange = (newPageIndex: number) => {
23
+ onPageChange(newPageIndex + 1);
24
+ };
25
+
26
+ const currentPageSizeOption = PAGE_SIZE_OPTIONS.find(opt => opt.value === pageSize) || PAGE_SIZE_OPTIONS[1];
27
+
28
+ return (
29
+ <div className={cn("pc-flex-responsive", className)} style={{
30
+ ...pcStyles.flex,
31
+ ...pcStyles.gapMd,
32
+ ...pcStyles.itemsCenter,
33
+ ...pcStyles.justifyBetween,
34
+ }}>
35
+ <div style={{
36
+ ...pcStyles.flex,
37
+ ...pcStyles.itemsCenter,
38
+ ...pcStyles.gapMd,
39
+ width: '100%',
40
+ flexShrink: 0,
41
+ }} className="pc-flex-responsive">
42
+ <div style={{ ...pcStyles.flex, ...pcStyles.itemsCenter, ...pcStyles.gapSm }}>
43
+ <PCText size="xs" color="secondary" className="pc-hidden-xs pc-block-sm">
44
+ Rows per page
45
+ </PCText>
46
+ <PCSelect
47
+ size="sm"
48
+ options={PAGE_SIZE_OPTIONS as SelectOption[]}
49
+ value={currentPageSizeOption}
50
+ onChange={(v) => {
51
+ if (v) {
52
+ const newSize = Number(v.value);
53
+ onPageSizeChange(newSize);
54
+ }
55
+ }}
56
+ style={{ width: '48px' }}
57
+ />
58
+ </div>
59
+ <div style={{ ...pcStyles.flex, ...pcStyles.itemsCenter, ...pcStyles.gapSm }}>
60
+ <PCText size="xs" color="secondary">
61
+ Total: {totalRecords} records
62
+ </PCText>
63
+ </div>
64
+ </div>
65
+ <div style={{
66
+ ...pcStyles.flex,
67
+ ...pcStyles.justifyEnd,
68
+ ...pcStyles.itemsCenter,
69
+ ...pcStyles.gapSm,
70
+ width: '100%',
71
+ }} className="pc-flex-responsive">
72
+ <div style={{ ...pcStyles.flex, ...pcStyles.itemsCenter, ...pcStyles.gapSm }}>
73
+ <PCText size="xs" color="secondary" className="pc-hidden-xs pc-block-sm">Page</PCText>
74
+ <PCText size="xs" weight="semibold" color="primary">
75
+ {totalPages === 0 ? 0 : currentPage}
76
+ </PCText>
77
+ <PCText size="xs" color="secondary">of {totalPages}</PCText>
78
+ </div>
79
+ <div style={{
80
+ display: 'grid',
81
+ gridTemplateColumns: 'repeat(4, 1fr)',
82
+ gap: '0.375rem',
83
+ }}>
84
+ <PCActionIcon
85
+ size="sm"
86
+ variant="outline"
87
+ aria-label="Go to first page"
88
+ onClick={() => handlePageChange(0)}
89
+ disabled={currentPage === 1}
90
+ style={{
91
+ borderRadius: '0.5rem',
92
+ boxShadow: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
93
+ }}
94
+ >
95
+ <ChevronsLeft size={14} />
96
+ </PCActionIcon>
97
+ <PCActionIcon
98
+ size="sm"
99
+ variant="outline"
100
+ aria-label="Go to previous page"
101
+ onClick={() => handlePageChange(currentPage - 2)}
102
+ disabled={currentPage === 1}
103
+ style={{
104
+ borderRadius: '0.5rem',
105
+ boxShadow: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
106
+ }}
107
+ >
108
+ <ChevronLeft size={14} />
109
+ </PCActionIcon>
110
+ <PCActionIcon
111
+ size="sm"
112
+ variant="outline"
113
+ aria-label="Go to next page"
114
+ onClick={() => handlePageChange(currentPage)}
115
+ disabled={totalRecords === 0 || currentPage === totalPages}
116
+ style={{
117
+ borderRadius: '0.5rem',
118
+ boxShadow: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
119
+ }}
120
+ >
121
+ <ChevronRight size={14} />
122
+ </PCActionIcon>
123
+ <PCActionIcon
124
+ size="sm"
125
+ variant="outline"
126
+ aria-label="Go to last page"
127
+ onClick={() => handlePageChange(totalPages - 1)}
128
+ disabled={totalRecords === 0 || currentPage === totalPages}
129
+ style={{
130
+ borderRadius: '0.5rem',
131
+ boxShadow: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
132
+ }}
133
+ >
134
+ <ChevronsRight size={14} />
135
+ </PCActionIcon>
136
+ </div>
137
+ </div>
138
+ </div>
139
+ );
140
+ }