@redacto.io/consent-sdk-react 5.3.0 → 7.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/.turbo/turbo-build.log +24 -0
- package/CHANGELOG.md +23 -0
- package/dist/privacy-center.d.mts +100 -42
- package/dist/privacy-center.d.ts +100 -42
- package/dist/privacy-center.js +4232 -2247
- package/dist/privacy-center.mjs +4266 -2284
- package/package.json +1 -1
- package/src/RedactoPrivacyCenter/PrivacyCenterLayout.tsx +6 -10
- package/src/RedactoPrivacyCenter/RedactoPrivacyCenter.tsx +18 -17
- package/src/RedactoPrivacyCenter/api/actions.ts +52 -8
- package/src/RedactoPrivacyCenter/api/client.ts +58 -15
- package/src/RedactoPrivacyCenter/api/orgBranding.ts +9 -0
- package/src/RedactoPrivacyCenter/components/ConsentManager.tsx +729 -278
- package/src/RedactoPrivacyCenter/components/ConsentManagerCells.tsx +187 -0
- package/src/RedactoPrivacyCenter/components/ConsentManagerColumns.tsx +65 -165
- package/src/RedactoPrivacyCenter/components/ConsentManagerExpandedRow.tsx +179 -0
- package/src/RedactoPrivacyCenter/components/ConsentManagerGroupedColumns.tsx +318 -0
- package/src/RedactoPrivacyCenter/components/ConsentManagerProductCards.tsx +333 -0
- package/src/RedactoPrivacyCenter/components/ConsentManagerPurposeCards.tsx +102 -0
- package/src/RedactoPrivacyCenter/components/ConsentManagerTableSkeleton.tsx +6 -2
- package/src/RedactoPrivacyCenter/components/Form.tsx +3 -3
- package/src/RedactoPrivacyCenter/components/ModifyConsentModal.tsx +9 -6
- package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationBell.tsx +1 -1
- package/src/RedactoPrivacyCenter/components/PrivacyCenterLayout.tsx +101 -56
- package/src/RedactoPrivacyCenter/components/SideMenuBarPC/SideBarPC.tsx +16 -2
- package/src/RedactoPrivacyCenter/components/types.ts +2 -5
- package/src/RedactoPrivacyCenter/context/BrandingContext.tsx +44 -0
- package/src/RedactoPrivacyCenter/context/PageChromeContext.tsx +21 -1
- package/src/RedactoPrivacyCenter/lib/api.ts +70 -28
- package/src/RedactoPrivacyCenter/lib/consent-manager/utils.tsx +4 -2
- package/src/RedactoPrivacyCenter/lib/types.ts +79 -36
- package/src/RedactoPrivacyCenter/locales/as/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/bho/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/bn/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/brx/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/doi/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/en/translation.json +16 -1
- package/src/RedactoPrivacyCenter/locales/gom/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/gu/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/hi/translation.json +15 -1
- package/src/RedactoPrivacyCenter/locales/kn/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/ks/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/mai/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/ml/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/mni-Mtei/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/mr/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/ne/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/or/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/pa/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/sa/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/sat/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/sd/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/ta/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/te/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/ur/translation.json +12 -1
- package/src/RedactoPrivacyCenter/styles/applyBrandingTheme.ts +66 -0
- package/src/RedactoPrivacyCenter/styles/injectStyles.ts +528 -114
- package/src/RedactoPrivacyCenter/styles/pcStyles.ts +3 -2
- package/src/RedactoPrivacyCenter/styles/variables.css +5 -42
- package/src/RedactoPrivacyCenter/table/AdvancedTable.tsx +36 -16
- package/src/RedactoPrivacyCenter/table/AdvancedTablePagination.tsx +4 -12
- package/src/RedactoPrivacyCenter/ui/Footer.tsx +4 -10
- package/src/RedactoPrivacyCenter/ui/PCInput.tsx +1 -1
- package/src/RedactoPrivacyCenter/ui/PCSelect.tsx +51 -5
- package/src/privacy-center.ts +7 -3
- package/src/RedactoPrivacyCenter/components/ThemeTransitionOverlay.tsx +0 -85
- package/src/RedactoPrivacyCenter/context/theme.tsx +0 -127
- package/src/RedactoPrivacyCenter/ui/ThemeToggle.tsx +0 -72
|
@@ -1,24 +1,47 @@
|
|
|
1
1
|
import { useState, useEffect, useCallback, useMemo } from 'react';
|
|
2
2
|
import { useTranslation } from 'react-i18next';
|
|
3
3
|
import { PCInput, PCSelect, PCButton, PCText, PCModal, type SelectOption } from '../ui';
|
|
4
|
-
import { Search, ArrowUpToLine } from 'lucide-react';
|
|
4
|
+
import { Search, ArrowUpToLine, Filter, Circle } from 'lucide-react';
|
|
5
5
|
import { pcStyles } from '../styles/pcStyles';
|
|
6
6
|
import { AdvancedTable, AdvancedTablePagination } from '../table';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
getCoreRowModel,
|
|
9
|
+
getExpandedRowModel,
|
|
10
|
+
useReactTable,
|
|
11
|
+
type ExpandedState,
|
|
12
|
+
type PaginationState,
|
|
13
|
+
type Row,
|
|
14
|
+
type Updater,
|
|
15
|
+
} from '@tanstack/react-table';
|
|
8
16
|
import {
|
|
9
17
|
fetchUserConsents,
|
|
18
|
+
fetchGroupPurposes,
|
|
10
19
|
manageConsent,
|
|
11
20
|
} from '../lib/api';
|
|
12
21
|
import { useOrganization } from '../context/OrganizationContext';
|
|
13
22
|
import { useAuth } from '../context/AuthContext';
|
|
14
23
|
import { useBaseUrl } from '../context/BaseUrlContext';
|
|
15
|
-
import
|
|
24
|
+
import { usePageChrome } from '../context/PageChromeContext';
|
|
25
|
+
import { ConsentStatusEnum } from '../lib/types';
|
|
26
|
+
import type {
|
|
27
|
+
ConsentAction,
|
|
28
|
+
ConsentGroup,
|
|
29
|
+
ConsentModalRecord,
|
|
30
|
+
DisplayProductGroup,
|
|
31
|
+
DisplayPurposeRow,
|
|
32
|
+
PurposeItem,
|
|
33
|
+
UserConsentDetail,
|
|
34
|
+
} from '../lib/types';
|
|
16
35
|
import { useCurrentLanguage } from '../lib/useCurrentLanguage';
|
|
17
36
|
import { exportTableToCSV, useStatusOptions } from '../lib/consent-manager/utils';
|
|
18
37
|
import { toast } from '../utils/toast';
|
|
19
38
|
import { ConsentManagerTableSkeleton } from './ConsentManagerTableSkeleton';
|
|
20
39
|
import { ModifyConsentModal } from './ModifyConsentModal';
|
|
40
|
+
import { useConsentManagerGroupedColumns } from './ConsentManagerGroupedColumns';
|
|
21
41
|
import { useConsentManagerColumns } from './ConsentManagerColumns';
|
|
42
|
+
import { ConsentManagerExpandedRow } from './ConsentManagerExpandedRow';
|
|
43
|
+
import { ConsentManagerProductCards } from './ConsentManagerProductCards';
|
|
44
|
+
import { ConsentManagerPurposeCards } from './ConsentManagerPurposeCards';
|
|
22
45
|
import type {
|
|
23
46
|
ConsentManagerProps,
|
|
24
47
|
StatusFilterOption,
|
|
@@ -30,12 +53,15 @@ import {
|
|
|
30
53
|
DEFAULT_PAGE_SIZE,
|
|
31
54
|
} from './ConsentManagerConstants';
|
|
32
55
|
|
|
56
|
+
type ConsentViewMode = 'product' | 'purpose';
|
|
57
|
+
|
|
33
58
|
export default function ConsentManager({ variant }: ConsentManagerProps) {
|
|
34
59
|
const { t } = useTranslation();
|
|
35
60
|
const statusOptions = useStatusOptions();
|
|
36
61
|
const { organization } = useOrganization();
|
|
37
62
|
const { accessToken, refreshToken, onError, updateTokens } = useAuth();
|
|
38
63
|
const { consentClient } = useBaseUrl();
|
|
64
|
+
const { setHeaderStatus } = usePageChrome();
|
|
39
65
|
const organizationId = organization?.uuid;
|
|
40
66
|
const workspaceId = organization?.workspaces[0]?.uuid;
|
|
41
67
|
const contact = sessionStorage.getItem('email') || '';
|
|
@@ -53,9 +79,10 @@ export default function ConsentManager({ variant }: ConsentManagerProps) {
|
|
|
53
79
|
|
|
54
80
|
// State management
|
|
55
81
|
const [activeTab, setActiveTab] = useState<'direct' | 'nominated'>('direct');
|
|
82
|
+
const [viewMode, setViewMode] = useState<ConsentViewMode>('product');
|
|
56
83
|
const [consentDetail, setConsentDetail] = useState<UserConsentDetail | null>(null);
|
|
57
|
-
const [
|
|
58
|
-
const [
|
|
84
|
+
const [consentGroups, setConsentGroups] = useState<DisplayProductGroup[]>([]);
|
|
85
|
+
const [consentRows, setConsentRows] = useState<DisplayPurposeRow[]>([]);
|
|
59
86
|
const [isLoading, setIsLoading] = useState(true);
|
|
60
87
|
const [searchQuery, setSearchQuery] = useState('');
|
|
61
88
|
const [debouncedSearch, setDebouncedSearch] = useState('');
|
|
@@ -68,13 +95,45 @@ export default function ConsentManager({ variant }: ConsentManagerProps) {
|
|
|
68
95
|
[]
|
|
69
96
|
);
|
|
70
97
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
71
|
-
const [selectedRecord, setSelectedRecord] = useState<
|
|
98
|
+
const [selectedRecord, setSelectedRecord] = useState<ConsentModalRecord | null>(
|
|
72
99
|
null
|
|
73
100
|
);
|
|
74
101
|
const [exportModal, setExportModal] = useState(false);
|
|
75
102
|
const [isManageLoading, setIsManageLoading] = useState(false);
|
|
76
103
|
const [currentPage, setCurrentPage] = useState(DEFAULT_PAGE);
|
|
77
104
|
const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE);
|
|
105
|
+
const [expanded, setExpanded] = useState<ExpandedState>({});
|
|
106
|
+
const [extraPurposesByKey, setExtraPurposesByKey] = useState<Record<string, PurposeItem[]>>({});
|
|
107
|
+
const [hasMoreByKey, setHasMoreByKey] = useState<Record<string, boolean>>({});
|
|
108
|
+
const [loadingMoreByKey, setLoadingMoreByKey] = useState<Record<string, boolean>>({});
|
|
109
|
+
const composedSearchPlaceholder = useMemo(
|
|
110
|
+
() => `${t('search')} ${t('consent')}, ${t('product')}, ${t('purpose')}...`,
|
|
111
|
+
[t]
|
|
112
|
+
);
|
|
113
|
+
const allStatusOption = useMemo<StatusFilterOption>(
|
|
114
|
+
() => ({
|
|
115
|
+
label: `${t('all')} ${t('status')}`,
|
|
116
|
+
value: '__all__',
|
|
117
|
+
}),
|
|
118
|
+
[t]
|
|
119
|
+
);
|
|
120
|
+
const allProductOption = useMemo<ProductFilterOption>(
|
|
121
|
+
() => ({
|
|
122
|
+
label: `${t('all')} ${t('product')}`,
|
|
123
|
+
value: '__all__',
|
|
124
|
+
}),
|
|
125
|
+
[t]
|
|
126
|
+
);
|
|
127
|
+
const statusFilterOptions = useMemo<StatusFilterOption[]>(
|
|
128
|
+
() => [allStatusOption, ...statusOptions],
|
|
129
|
+
[allStatusOption, statusOptions]
|
|
130
|
+
);
|
|
131
|
+
const productFilterOptions = useMemo<ProductFilterOption[]>(
|
|
132
|
+
() => [allProductOption, ...productOptions],
|
|
133
|
+
[allProductOption, productOptions]
|
|
134
|
+
);
|
|
135
|
+
const statusSelectValue = statusFilter ?? allStatusOption;
|
|
136
|
+
const productSelectValue = productFilter ?? allProductOption;
|
|
78
137
|
|
|
79
138
|
// Sync selected status filter label when language changes
|
|
80
139
|
useEffect(() => {
|
|
@@ -96,6 +155,19 @@ export default function ConsentManager({ variant }: ConsentManagerProps) {
|
|
|
96
155
|
}
|
|
97
156
|
}, [productOptions, productFilter]);
|
|
98
157
|
|
|
158
|
+
// Push user_status into the page header so it renders inline with the page title
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
const status = consentDetail?.user_status;
|
|
161
|
+
if (status === 'TRANSFERRED') {
|
|
162
|
+
setHeaderStatus({ label: t('statusTransferred'), variant: 'warning' });
|
|
163
|
+
} else if (status === 'ACTIVE') {
|
|
164
|
+
setHeaderStatus({ label: t('statusActive'), variant: 'success' });
|
|
165
|
+
} else {
|
|
166
|
+
setHeaderStatus(null);
|
|
167
|
+
}
|
|
168
|
+
return () => setHeaderStatus(null);
|
|
169
|
+
}, [consentDetail?.user_status, setHeaderStatus, t]);
|
|
170
|
+
|
|
99
171
|
// Debounce search query
|
|
100
172
|
useEffect(() => {
|
|
101
173
|
const timer = setTimeout(() => {
|
|
@@ -104,25 +176,132 @@ export default function ConsentManager({ variant }: ConsentManagerProps) {
|
|
|
104
176
|
return () => clearTimeout(timer);
|
|
105
177
|
}, [searchQuery]);
|
|
106
178
|
|
|
107
|
-
const handleOpenModal = useCallback((record:
|
|
179
|
+
const handleOpenModal = useCallback((record: ConsentModalRecord) => {
|
|
108
180
|
setSelectedRecord(record);
|
|
109
181
|
setIsModalOpen(true);
|
|
110
182
|
}, []);
|
|
111
183
|
|
|
184
|
+
const getCardKey = useCallback(
|
|
185
|
+
(group: ConsentGroup): string =>
|
|
186
|
+
group.nominator
|
|
187
|
+
? `nominated:${group.nominator.uuid}:${group.product_uuid}`
|
|
188
|
+
: `direct:${group.product_uuid}`,
|
|
189
|
+
[]
|
|
190
|
+
);
|
|
191
|
+
|
|
112
192
|
const handleCloseModal = useCallback(() => {
|
|
113
193
|
setIsModalOpen(false);
|
|
114
194
|
setSelectedRecord(null);
|
|
115
195
|
}, []);
|
|
116
196
|
|
|
197
|
+
const matchesPurpose = useCallback(
|
|
198
|
+
(
|
|
199
|
+
purpose: PurposeItem,
|
|
200
|
+
group: ConsentGroup,
|
|
201
|
+
normalizedSearch: string,
|
|
202
|
+
statusValue?: string
|
|
203
|
+
) => {
|
|
204
|
+
const matchesSearch =
|
|
205
|
+
!normalizedSearch ||
|
|
206
|
+
[
|
|
207
|
+
purpose.name,
|
|
208
|
+
purpose.description,
|
|
209
|
+
group.product_name,
|
|
210
|
+
group.product_description,
|
|
211
|
+
purpose.method,
|
|
212
|
+
purpose.status,
|
|
213
|
+
]
|
|
214
|
+
.filter(Boolean)
|
|
215
|
+
.some((value) => String(value).toLowerCase().includes(normalizedSearch));
|
|
216
|
+
|
|
217
|
+
const matchesStatus =
|
|
218
|
+
!statusValue || purpose.status?.toLowerCase() === statusValue.toLowerCase();
|
|
219
|
+
|
|
220
|
+
return matchesSearch && matchesStatus;
|
|
221
|
+
},
|
|
222
|
+
[]
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
const buildDisplayGroups = useCallback(
|
|
226
|
+
(
|
|
227
|
+
groups: ConsentGroup[] | undefined,
|
|
228
|
+
search: string,
|
|
229
|
+
statusValue?: string,
|
|
230
|
+
productUuid?: string
|
|
231
|
+
): DisplayProductGroup[] => {
|
|
232
|
+
if (!groups || groups.length === 0) return [];
|
|
233
|
+
const normalizedSearch = search.trim().toLowerCase();
|
|
234
|
+
|
|
235
|
+
return groups.reduce<DisplayProductGroup[]>((acc, group) => {
|
|
236
|
+
if (productUuid && group.product_uuid !== productUuid) {
|
|
237
|
+
return acc;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const filteredPurposes = (group.purposes || []).filter((purpose) =>
|
|
241
|
+
matchesPurpose(purpose, group, normalizedSearch, statusValue)
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
if (filteredPurposes.length === 0) {
|
|
245
|
+
return acc;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const counts = filteredPurposes.reduce(
|
|
249
|
+
(totals, purpose) => {
|
|
250
|
+
const normalized = (typeof purpose.status === 'string' ? purpose.status : '').toUpperCase();
|
|
251
|
+
switch (normalized) {
|
|
252
|
+
case ConsentStatusEnum.ACTIVE:
|
|
253
|
+
totals.active += 1;
|
|
254
|
+
break;
|
|
255
|
+
case ConsentStatusEnum.WITHDRAWN:
|
|
256
|
+
case ConsentStatusEnum.REVOKED:
|
|
257
|
+
totals.withdrawn += 1;
|
|
258
|
+
break;
|
|
259
|
+
case ConsentStatusEnum.EXPIRED:
|
|
260
|
+
totals.expired += 1;
|
|
261
|
+
break;
|
|
262
|
+
case ConsentStatusEnum.DECLINED:
|
|
263
|
+
totals.declined += 1;
|
|
264
|
+
break;
|
|
265
|
+
default:
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
return totals;
|
|
269
|
+
},
|
|
270
|
+
{ active: 0, withdrawn: 0, expired: 0, declined: 0 }
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
const latestGivenDate = filteredPurposes.reduce<string | null>((latest, purpose) => {
|
|
274
|
+
if (!purpose.given_date) return latest;
|
|
275
|
+
if (!latest) return purpose.given_date;
|
|
276
|
+
return new Date(purpose.given_date).getTime() > new Date(latest).getTime()
|
|
277
|
+
? purpose.given_date
|
|
278
|
+
: latest;
|
|
279
|
+
}, null);
|
|
280
|
+
|
|
281
|
+
acc.push({
|
|
282
|
+
...group,
|
|
283
|
+
purposes: filteredPurposes,
|
|
284
|
+
withdrawn_purposes: counts.withdrawn,
|
|
285
|
+
expired_purposes: counts.expired,
|
|
286
|
+
declined_purposes: counts.declined,
|
|
287
|
+
latest_given_date: latestGivenDate,
|
|
288
|
+
});
|
|
289
|
+
return acc;
|
|
290
|
+
}, []);
|
|
291
|
+
},
|
|
292
|
+
[matchesPurpose]
|
|
293
|
+
);
|
|
294
|
+
|
|
117
295
|
const flattenProductGroups = useCallback(
|
|
118
|
-
(groups:
|
|
296
|
+
(groups: ConsentGroup[] | undefined): DisplayPurposeRow[] => {
|
|
119
297
|
if (!groups || groups.length === 0) return [];
|
|
120
298
|
return groups.flatMap((group) =>
|
|
121
299
|
(group.purposes || []).map((purpose) => ({
|
|
122
300
|
...purpose,
|
|
123
|
-
product_uuid:
|
|
124
|
-
product_name:
|
|
125
|
-
product_description:
|
|
301
|
+
product_uuid: group.product_uuid,
|
|
302
|
+
product_name: group.product_name,
|
|
303
|
+
product_description: group.product_description,
|
|
304
|
+
nominator: group.nominator,
|
|
126
305
|
}))
|
|
127
306
|
);
|
|
128
307
|
},
|
|
@@ -131,19 +310,18 @@ export default function ConsentManager({ variant }: ConsentManagerProps) {
|
|
|
131
310
|
|
|
132
311
|
const filterConsentRows = useCallback(
|
|
133
312
|
(
|
|
134
|
-
rows:
|
|
313
|
+
rows: DisplayPurposeRow[],
|
|
135
314
|
search: string,
|
|
136
315
|
statusValue?: string,
|
|
137
316
|
productUuid?: string
|
|
138
|
-
) => {
|
|
317
|
+
): DisplayPurposeRow[] => {
|
|
139
318
|
const normalizedSearch = search.trim().toLowerCase();
|
|
140
|
-
|
|
141
319
|
return rows.filter((row) => {
|
|
142
320
|
const matchesSearch =
|
|
143
321
|
!normalizedSearch ||
|
|
144
322
|
[
|
|
145
|
-
row.
|
|
146
|
-
row.
|
|
323
|
+
row.name,
|
|
324
|
+
row.description,
|
|
147
325
|
row.product_name,
|
|
148
326
|
row.product_description,
|
|
149
327
|
row.method,
|
|
@@ -156,9 +334,7 @@ export default function ConsentManager({ variant }: ConsentManagerProps) {
|
|
|
156
334
|
|
|
157
335
|
const matchesStatus =
|
|
158
336
|
!statusValue || row.status?.toLowerCase() === statusValue.toLowerCase();
|
|
159
|
-
|
|
160
|
-
const matchesProduct =
|
|
161
|
-
!productUuid || row.product_uuid === productUuid;
|
|
337
|
+
const matchesProduct = !productUuid || row.product_uuid === productUuid;
|
|
162
338
|
|
|
163
339
|
return matchesSearch && matchesStatus && matchesProduct;
|
|
164
340
|
});
|
|
@@ -167,7 +343,7 @@ export default function ConsentManager({ variant }: ConsentManagerProps) {
|
|
|
167
343
|
);
|
|
168
344
|
|
|
169
345
|
const buildProductOptionsFromGroups = useCallback(
|
|
170
|
-
(groups:
|
|
346
|
+
(groups: ConsentGroup[] | undefined) => {
|
|
171
347
|
if (!groups || groups.length === 0) return [];
|
|
172
348
|
const productMap = new Map<string, { label: string; value: string }>();
|
|
173
349
|
groups.forEach((group) => {
|
|
@@ -183,31 +359,42 @@ export default function ConsentManager({ variant }: ConsentManagerProps) {
|
|
|
183
359
|
[]
|
|
184
360
|
);
|
|
185
361
|
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
362
|
+
const groupedColumns = useConsentManagerGroupedColumns();
|
|
363
|
+
const purposeColumns = useConsentManagerColumns({
|
|
364
|
+
onOpenModal: handleOpenModal,
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
const groupedTableClassNames = useMemo(
|
|
368
|
+
() => ({
|
|
369
|
+
container: 'pc-consent-table-container',
|
|
370
|
+
tableClassName: 'pc-consent-table',
|
|
371
|
+
headerClassName: 'pc-consent-table-header',
|
|
372
|
+
bodyClassName: 'pc-consent-table-body',
|
|
373
|
+
rowClassName: 'pc-consent-table-row',
|
|
374
|
+
cellClassName: 'pc-consent-table-cell',
|
|
375
|
+
headerCellClassName: 'pc-consent-table-head-cell',
|
|
376
|
+
expandedRowClassName: 'pc-consent-table-expanded-row',
|
|
377
|
+
expandedCellClassName: 'pc-consent-table-expanded-cell',
|
|
378
|
+
}),
|
|
201
379
|
[]
|
|
202
380
|
);
|
|
203
381
|
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
382
|
+
const purposeTableClassNames = useMemo(
|
|
383
|
+
() => ({
|
|
384
|
+
container: 'pc-consent-table-container',
|
|
385
|
+
tableClassName: 'pc-consent-table pc-consent-table-purpose',
|
|
386
|
+
headerClassName: 'pc-consent-table-header',
|
|
387
|
+
bodyClassName: 'pc-consent-table-body',
|
|
388
|
+
rowClassName: 'pc-consent-table-row',
|
|
389
|
+
cellClassName: 'pc-consent-table-cell',
|
|
390
|
+
headerCellClassName: 'pc-consent-table-head-cell',
|
|
391
|
+
}),
|
|
392
|
+
[]
|
|
393
|
+
);
|
|
207
394
|
|
|
208
395
|
// Single data fetch function
|
|
209
396
|
const fetchData = useCallback(async () => {
|
|
210
|
-
if (!organizationId || !workspaceId
|
|
397
|
+
if (!organizationId || !workspaceId) {
|
|
211
398
|
return;
|
|
212
399
|
}
|
|
213
400
|
|
|
@@ -218,31 +405,39 @@ export default function ConsentManager({ variant }: ConsentManagerProps) {
|
|
|
218
405
|
const statusParam = statusFilter?.value;
|
|
219
406
|
const productUuidParam = productFilter?.value;
|
|
220
407
|
|
|
221
|
-
const response = await fetchUserConsents(
|
|
408
|
+
const response = await fetchUserConsents({
|
|
222
409
|
organizationId,
|
|
223
410
|
workspaceId,
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
411
|
+
offset: activeTab === 'direct' ? offset : undefined,
|
|
412
|
+
limit: activeTab === 'direct' ? pageSize : undefined,
|
|
413
|
+
nominatedOffset: activeTab === 'nominated' ? offset : undefined,
|
|
414
|
+
nominatedLimit: activeTab === 'nominated' ? pageSize : undefined,
|
|
415
|
+
search: searchParam,
|
|
416
|
+
status: statusParam,
|
|
417
|
+
product_uuid: productUuidParam,
|
|
418
|
+
language: langParam,
|
|
230
419
|
authContext,
|
|
231
420
|
consentClient,
|
|
232
|
-
|
|
233
|
-
);
|
|
421
|
+
});
|
|
234
422
|
|
|
235
423
|
if (!response) {
|
|
236
424
|
throw new Error('Failed to fetch consent data');
|
|
237
425
|
}
|
|
238
426
|
|
|
239
427
|
setConsentDetail(response);
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
428
|
+
setExtraPurposesByKey({});
|
|
429
|
+
setHasMoreByKey({});
|
|
430
|
+
setLoadingMoreByKey({});
|
|
243
431
|
|
|
244
432
|
const selectedGroups =
|
|
245
|
-
activeTab === 'direct' ?
|
|
433
|
+
activeTab === 'direct' ? response.direct : response.nominated;
|
|
434
|
+
const displayGroups = buildDisplayGroups(
|
|
435
|
+
selectedGroups,
|
|
436
|
+
debouncedSearch,
|
|
437
|
+
statusParam,
|
|
438
|
+
productUuidParam
|
|
439
|
+
);
|
|
440
|
+
setConsentGroups(displayGroups);
|
|
246
441
|
const flattenedRows = flattenProductGroups(selectedGroups);
|
|
247
442
|
const filteredRows = filterConsentRows(
|
|
248
443
|
flattenedRows,
|
|
@@ -250,32 +445,28 @@ export default function ConsentManager({ variant }: ConsentManagerProps) {
|
|
|
250
445
|
statusParam,
|
|
251
446
|
productUuidParam
|
|
252
447
|
);
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
const directCount = response?.status_summary?.direct_purposes;
|
|
256
|
-
const nominatedCount = response?.status_summary?.nominated_purposes;
|
|
257
|
-
const fallbackTotal = response?.pagination?.total_count ?? 0;
|
|
258
|
-
const hasActiveFilters = Boolean(searchParam || statusParam || productUuidParam);
|
|
259
|
-
setTotalCount(
|
|
260
|
-
hasActiveFilters
|
|
261
|
-
? filteredRows.length
|
|
262
|
-
: activeTab === 'direct'
|
|
263
|
-
? (directCount ?? fallbackTotal)
|
|
264
|
-
: (nominatedCount ?? fallbackTotal)
|
|
265
|
-
);
|
|
448
|
+
setConsentRows(filteredRows);
|
|
266
449
|
|
|
267
|
-
//
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
450
|
+
// Only refresh the product filter options from an unfiltered fetch.
|
|
451
|
+
// When a product filter is active the API returns rows for that one
|
|
452
|
+
// product, so re-deriving options here would shrink the dropdown to
|
|
453
|
+
// just the selected product (forcing the user to refresh to recover).
|
|
454
|
+
if (!productFilter) {
|
|
455
|
+
const combinedGroups = [
|
|
456
|
+
...(response.direct || []),
|
|
457
|
+
...(response.nominated || []),
|
|
458
|
+
];
|
|
459
|
+
setProductOptions(buildProductOptionsFromGroups(combinedGroups));
|
|
460
|
+
}
|
|
273
461
|
} catch (error) {
|
|
274
462
|
console.error(error);
|
|
275
463
|
setConsentDetail(null);
|
|
276
|
-
|
|
277
|
-
|
|
464
|
+
setConsentGroups([]);
|
|
465
|
+
setConsentRows([]);
|
|
278
466
|
setProductOptions([]);
|
|
467
|
+
setExtraPurposesByKey({});
|
|
468
|
+
setHasMoreByKey({});
|
|
469
|
+
setLoadingMoreByKey({});
|
|
279
470
|
toast.error(t('failedToFetchConsentData'));
|
|
280
471
|
} finally {
|
|
281
472
|
setIsLoading(false);
|
|
@@ -283,7 +474,6 @@ export default function ConsentManager({ variant }: ConsentManagerProps) {
|
|
|
283
474
|
}, [
|
|
284
475
|
organizationId,
|
|
285
476
|
workspaceId,
|
|
286
|
-
contact,
|
|
287
477
|
currentPage,
|
|
288
478
|
pageSize,
|
|
289
479
|
debouncedSearch,
|
|
@@ -293,10 +483,10 @@ export default function ConsentManager({ variant }: ConsentManagerProps) {
|
|
|
293
483
|
authContext,
|
|
294
484
|
consentClient,
|
|
295
485
|
langParam,
|
|
486
|
+
buildDisplayGroups,
|
|
296
487
|
flattenProductGroups,
|
|
297
488
|
filterConsentRows,
|
|
298
489
|
buildProductOptionsFromGroups,
|
|
299
|
-
getNominatedProductGroups,
|
|
300
490
|
t,
|
|
301
491
|
]);
|
|
302
492
|
|
|
@@ -305,55 +495,234 @@ export default function ConsentManager({ variant }: ConsentManagerProps) {
|
|
|
305
495
|
fetchData();
|
|
306
496
|
}, [fetchData]);
|
|
307
497
|
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
498
|
+
const hasActiveFilters = Boolean(
|
|
499
|
+
debouncedSearch.trim() || statusFilter?.value || productFilter?.value
|
|
500
|
+
);
|
|
501
|
+
|
|
502
|
+
const groupedPurposeCount = useMemo(
|
|
503
|
+
() => consentGroups.reduce((sum, group) => sum + group.purposes.length, 0),
|
|
504
|
+
[consentGroups]
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
const visibleConsentCount =
|
|
508
|
+
viewMode === 'product' ? groupedPurposeCount : consentRows.length;
|
|
509
|
+
|
|
510
|
+
const visibleProductCount = useMemo(() => {
|
|
511
|
+
if (viewMode === 'product') {
|
|
512
|
+
return consentGroups.length;
|
|
513
|
+
}
|
|
514
|
+
const productIds = new Set(
|
|
515
|
+
consentRows.map((row) => row.product_uuid || row.product_name || '')
|
|
516
|
+
);
|
|
517
|
+
productIds.delete('');
|
|
518
|
+
return productIds.size;
|
|
519
|
+
}, [viewMode, consentGroups, consentRows]);
|
|
520
|
+
|
|
521
|
+
const tableTotalCount = useMemo(() => {
|
|
522
|
+
const pagination =
|
|
523
|
+
activeTab === 'direct'
|
|
524
|
+
? consentDetail?.direct_pagination
|
|
525
|
+
: consentDetail?.nominated_pagination;
|
|
526
|
+
return pagination?.total_count ?? 0;
|
|
527
|
+
}, [activeTab, consentDetail]);
|
|
528
|
+
|
|
529
|
+
const tablePageCount = Math.ceil(tableTotalCount / pageSize) || 0;
|
|
530
|
+
|
|
531
|
+
const handlePaginationChange = useCallback(
|
|
532
|
+
(updater: Updater<PaginationState>) => {
|
|
533
|
+
const next =
|
|
534
|
+
typeof updater === 'function'
|
|
535
|
+
? updater({ pageIndex: currentPage - 1, pageSize })
|
|
536
|
+
: updater;
|
|
537
|
+
const nextPage = next.pageIndex + 1;
|
|
538
|
+
const nextSize = next.pageSize;
|
|
539
|
+
if (nextPage === currentPage && nextSize === pageSize) return;
|
|
540
|
+
setCurrentPage(nextPage);
|
|
541
|
+
setPageSize(nextSize);
|
|
542
|
+
},
|
|
543
|
+
[currentPage, pageSize]
|
|
544
|
+
);
|
|
545
|
+
|
|
546
|
+
const productTable = useReactTable<DisplayProductGroup>({
|
|
547
|
+
data: consentGroups,
|
|
548
|
+
columns: groupedColumns,
|
|
311
549
|
getCoreRowModel: getCoreRowModel(),
|
|
550
|
+
getExpandedRowModel: getExpandedRowModel(),
|
|
551
|
+
getRowCanExpand: () => true,
|
|
312
552
|
manualPagination: true,
|
|
313
|
-
pageCount:
|
|
553
|
+
pageCount: tablePageCount,
|
|
314
554
|
state: {
|
|
315
555
|
pagination: {
|
|
316
556
|
pageIndex: currentPage - 1,
|
|
317
557
|
pageSize: pageSize,
|
|
318
558
|
},
|
|
559
|
+
expanded,
|
|
560
|
+
},
|
|
561
|
+
onExpandedChange: (updater) => {
|
|
562
|
+
const next = typeof updater === 'function' ? updater(expanded) : updater;
|
|
563
|
+
if (next === true) {
|
|
564
|
+
setExpanded(next);
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
if (!next) {
|
|
568
|
+
setExpanded({});
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const nextMap = next as Record<string, boolean>;
|
|
573
|
+
const nextOpenIds = Object.keys(nextMap).filter((rowId) => nextMap[rowId]);
|
|
574
|
+
if (nextOpenIds.length <= 1) {
|
|
575
|
+
setExpanded(nextOpenIds.length === 1 ? { [nextOpenIds[0]]: true } : {});
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const prevMap =
|
|
580
|
+
expanded && expanded !== true ? (expanded as Record<string, boolean>) : {};
|
|
581
|
+
const newlyOpenedId =
|
|
582
|
+
nextOpenIds.find((rowId) => !prevMap[rowId]) ??
|
|
583
|
+
nextOpenIds[nextOpenIds.length - 1];
|
|
584
|
+
|
|
585
|
+
setExpanded(newlyOpenedId ? { [newlyOpenedId]: true } : {});
|
|
319
586
|
},
|
|
320
587
|
onPaginationChange: (updater) => {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
588
|
+
handlePaginationChange(updater);
|
|
589
|
+
setExpanded({});
|
|
590
|
+
},
|
|
591
|
+
meta: {
|
|
592
|
+
onRowClick: (_row: DisplayProductGroup, tableRow?: Row<DisplayProductGroup>) => {
|
|
593
|
+
tableRow?.toggleExpanded();
|
|
594
|
+
},
|
|
595
|
+
},
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
const purposeTable = useReactTable<DisplayPurposeRow>({
|
|
599
|
+
data: consentRows,
|
|
600
|
+
columns: purposeColumns,
|
|
601
|
+
getCoreRowModel: getCoreRowModel(),
|
|
602
|
+
manualPagination: true,
|
|
603
|
+
pageCount: tablePageCount,
|
|
604
|
+
state: {
|
|
605
|
+
pagination: {
|
|
606
|
+
pageIndex: currentPage - 1,
|
|
607
|
+
pageSize: pageSize,
|
|
608
|
+
},
|
|
330
609
|
},
|
|
610
|
+
onPaginationChange: handlePaginationChange,
|
|
331
611
|
});
|
|
332
612
|
|
|
333
613
|
const handleStatusFilterChange = (data: SelectOption | null) => {
|
|
334
|
-
|
|
614
|
+
if (!data || String(data.value) === allStatusOption.value) {
|
|
615
|
+
setStatusFilter(null);
|
|
616
|
+
} else {
|
|
617
|
+
setStatusFilter(data as StatusFilterOption);
|
|
618
|
+
}
|
|
335
619
|
setCurrentPage(DEFAULT_PAGE);
|
|
620
|
+
setExpanded({});
|
|
336
621
|
};
|
|
337
622
|
|
|
338
623
|
const handleProductFilterChange = (data: SelectOption | null) => {
|
|
339
|
-
|
|
624
|
+
if (!data || String(data.value) === allProductOption.value) {
|
|
625
|
+
setProductFilter(null);
|
|
626
|
+
} else {
|
|
627
|
+
setProductFilter(data as ProductFilterOption);
|
|
628
|
+
}
|
|
340
629
|
setCurrentPage(DEFAULT_PAGE);
|
|
630
|
+
setExpanded({});
|
|
341
631
|
};
|
|
342
632
|
|
|
343
633
|
const handleTabChange = (tab: 'direct' | 'nominated') => {
|
|
344
634
|
setActiveTab(tab);
|
|
345
635
|
setCurrentPage(DEFAULT_PAGE);
|
|
636
|
+
setExpanded({});
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
const handleViewModeChange = (mode: ConsentViewMode) => {
|
|
640
|
+
if (mode === viewMode) return;
|
|
641
|
+
setViewMode(mode);
|
|
642
|
+
setCurrentPage(DEFAULT_PAGE);
|
|
643
|
+
setExpanded({});
|
|
346
644
|
};
|
|
347
645
|
|
|
348
646
|
const handleExport = useCallback(() => {
|
|
349
|
-
const value =
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
647
|
+
const value = viewMode === 'product'
|
|
648
|
+
? productTable.getRowModel().rows.flatMap((row) =>
|
|
649
|
+
row.original.purposes.map((purpose) => {
|
|
650
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
651
|
+
const { purpose_uuid, link_reason, data_elements, ...rest } = purpose;
|
|
652
|
+
return {
|
|
653
|
+
...rest,
|
|
654
|
+
product_name: row.original.product_name,
|
|
655
|
+
product_description: row.original.product_description,
|
|
656
|
+
};
|
|
657
|
+
})
|
|
658
|
+
)
|
|
659
|
+
: purposeTable.getRowModel().rows.map((row) => {
|
|
660
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
661
|
+
const { purpose_uuid, link_reason, nominator, data_elements, ...rest } = row.original;
|
|
662
|
+
return rest;
|
|
663
|
+
});
|
|
354
664
|
exportTableToCSV(value);
|
|
355
665
|
setExportModal(false);
|
|
356
|
-
}, [
|
|
666
|
+
}, [viewMode, productTable, purposeTable]);
|
|
667
|
+
|
|
668
|
+
const handleLoadMorePurposes = useCallback(
|
|
669
|
+
async (group: DisplayProductGroup) => {
|
|
670
|
+
if (!organizationId || !workspaceId) return;
|
|
671
|
+
const cardKey = getCardKey(group);
|
|
672
|
+
if (loadingMoreByKey[cardKey]) return;
|
|
673
|
+
|
|
674
|
+
const alreadyLoaded = extraPurposesByKey[cardKey]?.length ?? 0;
|
|
675
|
+
const inlineCount = group.purposes.length;
|
|
676
|
+
const offset = inlineCount + alreadyLoaded;
|
|
677
|
+
const statusParam = statusFilter?.value;
|
|
678
|
+
const searchParam = debouncedSearch.trim() || undefined;
|
|
679
|
+
|
|
680
|
+
setLoadingMoreByKey((prev) => ({ ...prev, [cardKey]: true }));
|
|
681
|
+
try {
|
|
682
|
+
const result = await fetchGroupPurposes({
|
|
683
|
+
organizationId,
|
|
684
|
+
workspaceId,
|
|
685
|
+
productUuid: group.product_uuid,
|
|
686
|
+
offset,
|
|
687
|
+
limit: 50,
|
|
688
|
+
status: statusParam,
|
|
689
|
+
search: searchParam,
|
|
690
|
+
language: langParam,
|
|
691
|
+
nominatorUuid: group.nominator?.uuid,
|
|
692
|
+
authContext,
|
|
693
|
+
consentClient,
|
|
694
|
+
});
|
|
695
|
+
if (!result) {
|
|
696
|
+
throw new Error('Failed to load more purposes');
|
|
697
|
+
}
|
|
698
|
+
setExtraPurposesByKey((prev) => ({
|
|
699
|
+
...prev,
|
|
700
|
+
[cardKey]: [...(prev[cardKey] ?? []), ...result.purposes],
|
|
701
|
+
}));
|
|
702
|
+
const fetchedTotal = offset + result.purposes.length;
|
|
703
|
+
const hasMore = fetchedTotal < result.pagination.total_count;
|
|
704
|
+
setHasMoreByKey((prev) => ({ ...prev, [cardKey]: hasMore }));
|
|
705
|
+
} catch (error) {
|
|
706
|
+
console.error('Failed to load more purposes:', error);
|
|
707
|
+
toast.error(t('failedToFetchConsentData'));
|
|
708
|
+
} finally {
|
|
709
|
+
setLoadingMoreByKey((prev) => ({ ...prev, [cardKey]: false }));
|
|
710
|
+
}
|
|
711
|
+
},
|
|
712
|
+
[
|
|
713
|
+
organizationId,
|
|
714
|
+
workspaceId,
|
|
715
|
+
authContext,
|
|
716
|
+
consentClient,
|
|
717
|
+
langParam,
|
|
718
|
+
statusFilter?.value,
|
|
719
|
+
debouncedSearch,
|
|
720
|
+
extraPurposesByKey,
|
|
721
|
+
loadingMoreByKey,
|
|
722
|
+
getCardKey,
|
|
723
|
+
t,
|
|
724
|
+
]
|
|
725
|
+
);
|
|
357
726
|
|
|
358
727
|
const handleManageConsent = useCallback(
|
|
359
728
|
async (
|
|
@@ -419,187 +788,219 @@ export default function ConsentManager({ variant }: ConsentManagerProps) {
|
|
|
419
788
|
|
|
420
789
|
const getVar = (varName: string): string => `var(${varName})`;
|
|
421
790
|
|
|
422
|
-
const directBadge =
|
|
423
|
-
consentDetail?.status_summary?.direct_purposes ??
|
|
424
|
-
(activeTab === 'direct' ? totalCount : '');
|
|
791
|
+
const directBadge: number | '' =
|
|
792
|
+
consentDetail?.status_summary?.direct_purposes ?? '';
|
|
425
793
|
|
|
426
794
|
const nominatedBadge: number | '' =
|
|
427
|
-
consentDetail?.status_summary?.nominated_purposes ??
|
|
428
|
-
(activeTab === 'nominated' ? totalCount : '');
|
|
429
|
-
|
|
430
|
-
const renderUserRelationshipStrip = () => {
|
|
431
|
-
if (!consentDetail) return null;
|
|
432
|
-
|
|
433
|
-
const status = consentDetail.user_status;
|
|
434
|
-
let statusLabel: string | null = null;
|
|
435
|
-
let statusBgColor: string | null = null;
|
|
436
|
-
let statusTextColor: string | null = null;
|
|
437
|
-
if (status === 'ACTIVE') {
|
|
438
|
-
statusLabel = t('statusActive');
|
|
439
|
-
statusBgColor = 'rgba(16, 185, 129, 0.1)';
|
|
440
|
-
statusTextColor = getVar('--pc-success');
|
|
441
|
-
} else if (status === 'TRANSFERRED') {
|
|
442
|
-
statusLabel = t('statusTransferred');
|
|
443
|
-
statusBgColor = 'rgba(245, 158, 11, 0.1)';
|
|
444
|
-
statusTextColor = getVar('--pc-warning');
|
|
445
|
-
}
|
|
795
|
+
consentDetail?.status_summary?.nominated_purposes ?? '';
|
|
446
796
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
return (
|
|
456
|
-
<div style={{
|
|
457
|
-
marginBottom: getVar('--pc-spacing-md'),
|
|
458
|
-
borderRadius: getVar('--pc-border-radius-lg'),
|
|
459
|
-
border: `1px solid ${getVar('--pc-border-color')}`,
|
|
460
|
-
backgroundColor: getVar('--pc-bg-secondary'),
|
|
461
|
-
padding: getVar('--pc-spacing-md'),
|
|
462
|
-
}}>
|
|
463
|
-
<div style={{
|
|
464
|
-
display: 'flex',
|
|
465
|
-
flexDirection: 'column',
|
|
466
|
-
gap: getVar('--pc-spacing-sm'),
|
|
467
|
-
}} className="pc-flex-responsive">
|
|
468
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
|
469
|
-
<PCText size="sm" weight="semibold" style={{ margin: 0, color: getVar('--pc-text-primary') }}>
|
|
470
|
-
{t('consentVisibility')}
|
|
471
|
-
</PCText>
|
|
472
|
-
{nominatorLabels.length > 0 && (
|
|
473
|
-
<PCText size="sm" style={{ margin: 0, color: getVar('--pc-text-secondary') }}>
|
|
474
|
-
{t('managingConsentsFor')}{' '}
|
|
475
|
-
<span style={{ fontWeight: 600, color: getVar('--pc-text-primary') }}>
|
|
476
|
-
{nominatorLabels.join(', ')}
|
|
477
|
-
</span>
|
|
478
|
-
</PCText>
|
|
479
|
-
)}
|
|
480
|
-
</div>
|
|
481
|
-
{statusLabel && (
|
|
482
|
-
<div style={{ flexShrink: 0 }}>
|
|
483
|
-
<span style={{
|
|
484
|
-
display: 'inline-flex',
|
|
485
|
-
alignItems: 'center',
|
|
486
|
-
borderRadius: '9999px',
|
|
487
|
-
padding: '0.25rem 0.75rem',
|
|
488
|
-
fontSize: getVar('--pc-font-size-xs'),
|
|
489
|
-
fontWeight: 600,
|
|
490
|
-
backgroundColor: statusBgColor!,
|
|
491
|
-
color: statusTextColor!,
|
|
492
|
-
}}>
|
|
493
|
-
{statusLabel}
|
|
494
|
-
</span>
|
|
495
|
-
</div>
|
|
496
|
-
)}
|
|
497
|
-
</div>
|
|
498
|
-
</div>
|
|
499
|
-
);
|
|
500
|
-
};
|
|
797
|
+
const nominatorLabels = (consentDetail?.nominators || [])
|
|
798
|
+
.map((n) => n.email?.trim() || n.org_user_id?.trim() || n.uuid?.trim() || '')
|
|
799
|
+
.filter(Boolean);
|
|
800
|
+
const summaryText = t('productsAndConsentsSummary', {
|
|
801
|
+
products: visibleProductCount,
|
|
802
|
+
consents: visibleConsentCount,
|
|
803
|
+
});
|
|
501
804
|
|
|
502
805
|
return (
|
|
503
|
-
<div style={{ width: '100%' }}>
|
|
504
|
-
{/* User Relationship Strip */}
|
|
505
|
-
{renderUserRelationshipStrip()}
|
|
506
|
-
|
|
806
|
+
<div className="pc-consent-manager-surface" style={{ width: '100%' }}>
|
|
507
807
|
<div
|
|
808
|
+
className="pc-consent-shell-card"
|
|
508
809
|
style={{
|
|
509
|
-
|
|
510
|
-
border: `1px solid ${getVar('--pc-border-color')}`,
|
|
511
|
-
backgroundColor: getVar('--pc-bg-primary'),
|
|
512
|
-
boxShadow: getVar('--pc-shadow-sm'),
|
|
513
|
-
padding: getVar('--pc-spacing-md'),
|
|
514
|
-
marginBottom: getVar('--pc-spacing-lg'),
|
|
810
|
+
backgroundColor: 'transparent',
|
|
515
811
|
}}
|
|
516
812
|
>
|
|
517
|
-
{/* Tabs: My Consents / Managing */}
|
|
518
|
-
<div
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
813
|
+
{/* Tabs: My Consents / Managing + Product/Purpose toggle */}
|
|
814
|
+
<div
|
|
815
|
+
className="pc-consent-tabs-row"
|
|
816
|
+
style={{
|
|
817
|
+
display: 'flex',
|
|
818
|
+
alignItems: 'center',
|
|
819
|
+
justifyContent: 'space-between',
|
|
820
|
+
gap: getVar('--pc-spacing-sm'),
|
|
821
|
+
borderBottom: `1px solid ${getVar('--pc-border-color')}`,
|
|
822
|
+
padding: '0.125rem 0 0.375rem',
|
|
823
|
+
}}
|
|
824
|
+
>
|
|
825
|
+
<div className="pc-consent-tabs-left" style={{ display: 'flex', minWidth: 0 }}>
|
|
826
|
+
<button
|
|
827
|
+
type="button"
|
|
828
|
+
onClick={() => handleTabChange('direct')}
|
|
829
|
+
style={{
|
|
830
|
+
display: 'flex',
|
|
831
|
+
alignItems: 'center',
|
|
832
|
+
gap: getVar('--pc-spacing-sm'),
|
|
833
|
+
padding: `0.5rem ${getVar('--pc-spacing-sm')}`,
|
|
834
|
+
fontSize: '0.75rem',
|
|
835
|
+
fontWeight: 600,
|
|
836
|
+
background: 'none',
|
|
837
|
+
border: 'none',
|
|
838
|
+
cursor: 'pointer',
|
|
839
|
+
borderBottom: activeTab === 'direct' ? `2px solid ${getVar('--pc-primary')}` : '2px solid transparent',
|
|
840
|
+
color: activeTab === 'direct' ? getVar('--pc-primary') : getVar('--pc-text-secondary'),
|
|
841
|
+
transition: 'color 0.2s, border-color 0.2s',
|
|
842
|
+
}}
|
|
843
|
+
>
|
|
844
|
+
{t('myConsents')}
|
|
845
|
+
<span style={{
|
|
846
|
+
borderRadius: '9999px',
|
|
847
|
+
backgroundColor: getVar('--pc-bg-tertiary'),
|
|
848
|
+
padding: '0.0625rem 0.375rem',
|
|
849
|
+
fontSize: '0.6875rem',
|
|
850
|
+
color: getVar('--pc-text-secondary'),
|
|
851
|
+
}}>
|
|
852
|
+
{directBadge}
|
|
853
|
+
</span>
|
|
854
|
+
</button>
|
|
855
|
+
<button
|
|
856
|
+
type="button"
|
|
857
|
+
onClick={() => handleTabChange('nominated')}
|
|
858
|
+
style={{
|
|
859
|
+
display: 'flex',
|
|
860
|
+
alignItems: 'center',
|
|
861
|
+
gap: getVar('--pc-spacing-sm'),
|
|
862
|
+
padding: `0.5rem ${getVar('--pc-spacing-sm')}`,
|
|
863
|
+
fontSize: '0.75rem',
|
|
864
|
+
fontWeight: 600,
|
|
865
|
+
background: 'none',
|
|
866
|
+
border: 'none',
|
|
867
|
+
cursor: 'pointer',
|
|
868
|
+
borderBottom: activeTab === 'nominated' ? `2px solid ${getVar('--pc-primary')}` : '2px solid transparent',
|
|
869
|
+
color: activeTab === 'nominated' ? getVar('--pc-primary') : getVar('--pc-text-secondary'),
|
|
870
|
+
transition: 'color 0.2s, border-color 0.2s',
|
|
871
|
+
}}
|
|
872
|
+
>
|
|
873
|
+
{t('managing')}
|
|
874
|
+
<span style={{
|
|
875
|
+
borderRadius: '9999px',
|
|
876
|
+
backgroundColor: getVar('--pc-bg-tertiary'),
|
|
877
|
+
padding: '0.0625rem 0.375rem',
|
|
878
|
+
fontSize: '0.6875rem',
|
|
879
|
+
color: getVar('--pc-text-secondary'),
|
|
880
|
+
}}>
|
|
881
|
+
{nominatedBadge}
|
|
882
|
+
</span>
|
|
883
|
+
</button>
|
|
884
|
+
</div>
|
|
885
|
+
|
|
886
|
+
<div
|
|
887
|
+
className="pc-consent-view-toggle"
|
|
527
888
|
style={{
|
|
889
|
+
marginLeft: 'auto',
|
|
528
890
|
display: 'flex',
|
|
529
891
|
alignItems: 'center',
|
|
530
892
|
gap: getVar('--pc-spacing-sm'),
|
|
531
|
-
|
|
532
|
-
fontSize: getVar('--pc-font-size-sm'),
|
|
533
|
-
fontWeight: 600,
|
|
534
|
-
background: 'none',
|
|
535
|
-
border: 'none',
|
|
536
|
-
cursor: 'pointer',
|
|
537
|
-
borderBottom: activeTab === 'direct' ? `2px solid ${getVar('--pc-primary')}` : '2px solid transparent',
|
|
538
|
-
color: activeTab === 'direct' ? getVar('--pc-primary') : getVar('--pc-text-secondary'),
|
|
539
|
-
transition: 'color 0.2s, border-color 0.2s',
|
|
893
|
+
paddingRight: getVar('--pc-spacing-sm'),
|
|
540
894
|
}}
|
|
541
895
|
>
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
896
|
+
<PCText
|
|
897
|
+
size="sm"
|
|
898
|
+
color="secondary"
|
|
899
|
+
className="pc-consent-view-toggle-label"
|
|
900
|
+
style={{ margin: 0, fontSize: '0.75rem', fontWeight: 500 }}
|
|
901
|
+
>
|
|
902
|
+
{t('consentVisibility')}
|
|
903
|
+
</PCText>
|
|
904
|
+
<div
|
|
905
|
+
className="pc-consent-view-toggle-group"
|
|
906
|
+
style={{
|
|
907
|
+
display: 'inline-flex',
|
|
908
|
+
borderRadius: '9999px',
|
|
909
|
+
backgroundColor: getVar('--pc-bg-secondary'),
|
|
910
|
+
padding: '0.125rem',
|
|
911
|
+
}}
|
|
912
|
+
>
|
|
913
|
+
<button
|
|
914
|
+
type="button"
|
|
915
|
+
className={`pc-consent-view-toggle-btn ${viewMode === 'product' ? 'is-active' : ''}`}
|
|
916
|
+
onClick={() => handleViewModeChange('product')}
|
|
917
|
+
style={{
|
|
918
|
+
minWidth: '5.25rem',
|
|
919
|
+
minHeight: '1.75rem',
|
|
920
|
+
padding: '0.3125rem 0.75rem',
|
|
921
|
+
border: 'none',
|
|
922
|
+
backgroundColor:
|
|
923
|
+
viewMode === 'product' ? getVar('--pc-primary') : 'transparent',
|
|
924
|
+
color:
|
|
925
|
+
viewMode === 'product'
|
|
926
|
+
? getVar('--pc-text-inverse')
|
|
927
|
+
: getVar('--pc-text-secondary'),
|
|
928
|
+
fontSize: '0.6875rem',
|
|
929
|
+
fontWeight: 600,
|
|
930
|
+
cursor: 'pointer',
|
|
931
|
+
borderRadius: '9999px',
|
|
932
|
+
transition: 'background-color 0.2s ease, color 0.2s ease',
|
|
933
|
+
}}
|
|
934
|
+
>
|
|
935
|
+
{t('product')}
|
|
936
|
+
</button>
|
|
937
|
+
<button
|
|
938
|
+
type="button"
|
|
939
|
+
className={`pc-consent-view-toggle-btn ${viewMode === 'purpose' ? 'is-active' : ''}`}
|
|
940
|
+
onClick={() => handleViewModeChange('purpose')}
|
|
941
|
+
style={{
|
|
942
|
+
minWidth: '5.25rem',
|
|
943
|
+
minHeight: '1.75rem',
|
|
944
|
+
padding: '0.3125rem 0.75rem',
|
|
945
|
+
border: 'none',
|
|
946
|
+
backgroundColor:
|
|
947
|
+
viewMode === 'purpose' ? getVar('--pc-primary') : 'transparent',
|
|
948
|
+
color:
|
|
949
|
+
viewMode === 'purpose'
|
|
950
|
+
? getVar('--pc-text-inverse')
|
|
951
|
+
: getVar('--pc-text-secondary'),
|
|
952
|
+
fontSize: '0.6875rem',
|
|
953
|
+
fontWeight: 600,
|
|
954
|
+
cursor: 'pointer',
|
|
955
|
+
borderRadius: '9999px',
|
|
956
|
+
transition: 'background-color 0.2s ease, color 0.2s ease',
|
|
957
|
+
}}
|
|
958
|
+
>
|
|
959
|
+
{t('purpose')}
|
|
960
|
+
</button>
|
|
961
|
+
</div>
|
|
962
|
+
</div>
|
|
963
|
+
</div>
|
|
964
|
+
|
|
965
|
+
{activeTab === 'nominated' && nominatorLabels.length > 0 && (
|
|
966
|
+
<PCText
|
|
967
|
+
size="sm"
|
|
968
|
+
color="secondary"
|
|
556
969
|
style={{
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
fontSize: getVar('--pc-font-size-sm'),
|
|
562
|
-
fontWeight: 600,
|
|
563
|
-
background: 'none',
|
|
564
|
-
border: 'none',
|
|
565
|
-
cursor: 'pointer',
|
|
566
|
-
borderBottom: activeTab === 'nominated' ? `2px solid ${getVar('--pc-primary')}` : '2px solid transparent',
|
|
567
|
-
color: activeTab === 'nominated' ? getVar('--pc-primary') : getVar('--pc-text-secondary'),
|
|
568
|
-
transition: 'color 0.2s, border-color 0.2s',
|
|
970
|
+
margin: 0,
|
|
971
|
+
marginBottom: getVar('--pc-spacing-sm'),
|
|
972
|
+
paddingLeft: getVar('--pc-spacing-lg'),
|
|
973
|
+
paddingRight: getVar('--pc-spacing-lg'),
|
|
569
974
|
}}
|
|
570
975
|
>
|
|
571
|
-
{t('
|
|
572
|
-
<span style={{
|
|
573
|
-
|
|
574
|
-
backgroundColor: getVar('--pc-bg-tertiary'),
|
|
575
|
-
padding: '0.125rem 0.5rem',
|
|
576
|
-
fontSize: getVar('--pc-font-size-xs'),
|
|
577
|
-
color: getVar('--pc-text-secondary'),
|
|
578
|
-
}}>
|
|
579
|
-
{nominatedBadge}
|
|
976
|
+
{t('managingConsentsFor')}{' '}
|
|
977
|
+
<span style={{ fontWeight: 600, color: getVar('--pc-text-primary') }}>
|
|
978
|
+
{nominatorLabels.join(', ')}
|
|
580
979
|
</span>
|
|
581
|
-
</
|
|
582
|
-
|
|
980
|
+
</PCText>
|
|
981
|
+
)}
|
|
583
982
|
|
|
584
983
|
{/* Filters Section: Search on left, filters and export on the right */}
|
|
585
984
|
<div style={{
|
|
586
985
|
display: 'flex',
|
|
587
986
|
flexDirection: 'column',
|
|
588
|
-
gap: getVar('--pc-spacing-
|
|
987
|
+
gap: getVar('--pc-spacing-sm'),
|
|
589
988
|
width: '100%',
|
|
989
|
+
padding: '0.75rem 0 0.625rem',
|
|
590
990
|
}} className="pc-filters-responsive">
|
|
591
991
|
<div style={{ width: '100%' }} className="pc-search-responsive">
|
|
592
992
|
<PCInput
|
|
593
993
|
type="search"
|
|
994
|
+
size="sm"
|
|
594
995
|
clearable
|
|
595
996
|
clearAriaLabel={t('clearInput')}
|
|
596
|
-
placeholder={
|
|
997
|
+
placeholder={composedSearchPlaceholder}
|
|
597
998
|
value={searchQuery}
|
|
598
999
|
onClear={() => {
|
|
599
1000
|
setSearchQuery('');
|
|
600
1001
|
setCurrentPage(DEFAULT_PAGE);
|
|
601
1002
|
}}
|
|
602
|
-
prefix={<Search size={
|
|
1003
|
+
prefix={<Search size={14} strokeWidth={1.8} />}
|
|
603
1004
|
onChange={(e) => {
|
|
604
1005
|
setSearchQuery(e.target.value);
|
|
605
1006
|
setCurrentPage(DEFAULT_PAGE);
|
|
@@ -613,57 +1014,124 @@ export default function ConsentManager({ variant }: ConsentManagerProps) {
|
|
|
613
1014
|
<div style={{
|
|
614
1015
|
display: 'flex',
|
|
615
1016
|
flexDirection: 'column',
|
|
616
|
-
gap: getVar('--pc-spacing-
|
|
1017
|
+
gap: getVar('--pc-spacing-sm'),
|
|
617
1018
|
width: '100%',
|
|
618
1019
|
}} className="pc-filters-row-responsive">
|
|
619
1020
|
<PCSelect
|
|
620
1021
|
className="pc-filter-select"
|
|
1022
|
+
size="sm"
|
|
621
1023
|
placeholder={t('status')}
|
|
622
|
-
|
|
623
|
-
|
|
1024
|
+
prefix={<Filter size={12} strokeWidth={1.8} />}
|
|
1025
|
+
options={statusFilterOptions}
|
|
1026
|
+
value={statusSelectValue}
|
|
624
1027
|
onChange={handleStatusFilterChange}
|
|
625
|
-
|
|
626
|
-
clearable={statusFilter !== null}
|
|
627
|
-
clearAriaLabel={t('clearSelection')}
|
|
628
|
-
dropdownMatchTriggerWidth
|
|
1028
|
+
showSelectedCheck
|
|
629
1029
|
style={{
|
|
630
1030
|
width: '100%',
|
|
631
1031
|
}}
|
|
632
1032
|
/>
|
|
633
1033
|
<PCSelect
|
|
634
1034
|
className="pc-filter-select"
|
|
1035
|
+
size="sm"
|
|
635
1036
|
placeholder={t('product')}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
options={productOptions}
|
|
640
|
-
value={productFilter}
|
|
1037
|
+
prefix={<Circle size={10} strokeWidth={2} />}
|
|
1038
|
+
options={productFilterOptions}
|
|
1039
|
+
value={productSelectValue}
|
|
641
1040
|
onChange={handleProductFilterChange}
|
|
642
|
-
|
|
643
|
-
clearable={productFilter !== null}
|
|
644
|
-
clearAriaLabel={t('clearSelection')}
|
|
645
|
-
dropdownMatchTriggerWidth
|
|
1041
|
+
showSelectedCheck
|
|
646
1042
|
style={{
|
|
647
1043
|
width: '100%',
|
|
648
1044
|
}}
|
|
649
1045
|
/>
|
|
1046
|
+
<PCText
|
|
1047
|
+
size="sm"
|
|
1048
|
+
color="secondary"
|
|
1049
|
+
className="pc-filters-summary"
|
|
1050
|
+
style={{
|
|
1051
|
+
margin: 0,
|
|
1052
|
+
whiteSpace: 'nowrap',
|
|
1053
|
+
alignSelf: 'center',
|
|
1054
|
+
}}
|
|
1055
|
+
>
|
|
1056
|
+
{summaryText}
|
|
1057
|
+
</PCText>
|
|
650
1058
|
<div className="pc-export-button-responsive">
|
|
651
1059
|
<PCButton
|
|
1060
|
+
size="sm"
|
|
652
1061
|
variant="outline"
|
|
653
1062
|
onClick={() => setExportModal(true)}
|
|
654
1063
|
style={{
|
|
655
1064
|
whiteSpace: 'nowrap',
|
|
656
1065
|
width: '100%',
|
|
657
|
-
minHeight: '2.25rem',
|
|
658
|
-
padding: '0.4375rem 0.875rem',
|
|
659
1066
|
}}
|
|
660
1067
|
>
|
|
661
|
-
<ArrowUpToLine size={
|
|
1068
|
+
<ArrowUpToLine size={13} />
|
|
662
1069
|
<span>{t('export')}</span>
|
|
663
1070
|
</PCButton>
|
|
664
1071
|
</div>
|
|
665
1072
|
</div>
|
|
666
1073
|
</div>
|
|
1074
|
+
{isLoading ? (
|
|
1075
|
+
<ConsentManagerTableSkeleton />
|
|
1076
|
+
) : (
|
|
1077
|
+
<>
|
|
1078
|
+
<div className="pc-consent-desktop-view">
|
|
1079
|
+
{viewMode === 'product' ? (
|
|
1080
|
+
<AdvancedTable
|
|
1081
|
+
table={productTable}
|
|
1082
|
+
variant={variant}
|
|
1083
|
+
classNames={groupedTableClassNames}
|
|
1084
|
+
isLoading={isLoading}
|
|
1085
|
+
expandedComponent={(row) => {
|
|
1086
|
+
const cardKey = getCardKey(row.original);
|
|
1087
|
+
return (
|
|
1088
|
+
<ConsentManagerExpandedRow
|
|
1089
|
+
group={row.original}
|
|
1090
|
+
onModify={handleOpenModal}
|
|
1091
|
+
extraPurposes={extraPurposesByKey[cardKey]}
|
|
1092
|
+
hasMore={hasMoreByKey[cardKey] ?? row.original.has_more_purposes}
|
|
1093
|
+
isLoadingMore={loadingMoreByKey[cardKey]}
|
|
1094
|
+
onLoadMore={handleLoadMorePurposes}
|
|
1095
|
+
/>
|
|
1096
|
+
);
|
|
1097
|
+
}}
|
|
1098
|
+
/>
|
|
1099
|
+
) : (
|
|
1100
|
+
<AdvancedTable
|
|
1101
|
+
table={purposeTable}
|
|
1102
|
+
variant={variant}
|
|
1103
|
+
classNames={purposeTableClassNames}
|
|
1104
|
+
isLoading={isLoading}
|
|
1105
|
+
/>
|
|
1106
|
+
)}
|
|
1107
|
+
</div>
|
|
1108
|
+
|
|
1109
|
+
<div className="pc-consent-mobile-view">
|
|
1110
|
+
{viewMode === 'product' ? (
|
|
1111
|
+
<ConsentManagerProductCards
|
|
1112
|
+
groups={consentGroups}
|
|
1113
|
+
onModify={handleOpenModal}
|
|
1114
|
+
extraPurposesByKey={extraPurposesByKey}
|
|
1115
|
+
hasMoreByKey={hasMoreByKey}
|
|
1116
|
+
loadingMoreByKey={loadingMoreByKey}
|
|
1117
|
+
onLoadMore={handleLoadMorePurposes}
|
|
1118
|
+
cardKeyFor={getCardKey}
|
|
1119
|
+
/>
|
|
1120
|
+
) : (
|
|
1121
|
+
<ConsentManagerPurposeCards
|
|
1122
|
+
rows={consentRows}
|
|
1123
|
+
onModify={handleOpenModal}
|
|
1124
|
+
/>
|
|
1125
|
+
)}
|
|
1126
|
+
</div>
|
|
1127
|
+
</>
|
|
1128
|
+
)}
|
|
1129
|
+
|
|
1130
|
+
{viewMode === 'product' && (
|
|
1131
|
+
<div className="pc-consent-pagination-wrap">
|
|
1132
|
+
<AdvancedTablePagination table={productTable} />
|
|
1133
|
+
</div>
|
|
1134
|
+
)}
|
|
667
1135
|
</div>
|
|
668
1136
|
|
|
669
1137
|
<PCModal
|
|
@@ -687,7 +1155,7 @@ export default function ConsentManager({ variant }: ConsentManagerProps) {
|
|
|
687
1155
|
}
|
|
688
1156
|
footer={
|
|
689
1157
|
<div className="pc-modal-footer-actions">
|
|
690
|
-
<PCButton variant="
|
|
1158
|
+
<PCButton variant="secondary" onClick={() => setExportModal(false)}>
|
|
691
1159
|
{t('cancel')}
|
|
692
1160
|
</PCButton>
|
|
693
1161
|
<PCButton variant="primary" onClick={handleExport}>
|
|
@@ -703,23 +1171,6 @@ export default function ConsentManager({ variant }: ConsentManagerProps) {
|
|
|
703
1171
|
</div>
|
|
704
1172
|
</PCModal>
|
|
705
1173
|
|
|
706
|
-
{isLoading ? (
|
|
707
|
-
<ConsentManagerTableSkeleton />
|
|
708
|
-
) : (
|
|
709
|
-
<AdvancedTable table={table} variant={variant} isLoading={isLoading} />
|
|
710
|
-
)}
|
|
711
|
-
|
|
712
|
-
{/* Always show pagination, even during loading */}
|
|
713
|
-
<div style={{
|
|
714
|
-
...pcStyles.flex,
|
|
715
|
-
...pcStyles.flexCol,
|
|
716
|
-
...pcStyles.itemsCenter,
|
|
717
|
-
...pcStyles.justifyBetween,
|
|
718
|
-
...pcStyles.gapSm,
|
|
719
|
-
}} className="pc-flex-responsive">
|
|
720
|
-
<AdvancedTablePagination table={table} />
|
|
721
|
-
</div>
|
|
722
|
-
|
|
723
1174
|
<ModifyConsentModal
|
|
724
1175
|
isOpen={isModalOpen}
|
|
725
1176
|
onClose={handleCloseModal}
|