@redacto.io/consent-sdk-react 9.3.0-beta.1 → 9.4.0-beta.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 (48) hide show
  1. package/.turbo/turbo-build.log +10 -10
  2. package/CHANGELOG.md +17 -0
  3. package/README.md +93 -12
  4. package/dist/index.d.mts +39 -1
  5. package/dist/index.d.ts +39 -1
  6. package/dist/index.js +4454 -334
  7. package/dist/index.mjs +4454 -335
  8. package/dist/privacy-center.d.mts +4 -1
  9. package/dist/privacy-center.d.ts +4 -1
  10. package/dist/privacy-center.js +3905 -3660
  11. package/dist/privacy-center.mjs +4172 -3921
  12. package/package.json +1 -1
  13. package/src/RedactoNoticeAssisted/RedactoNoticeAssisted.test.tsx +847 -0
  14. package/src/RedactoNoticeAssisted/RedactoNoticeAssisted.tsx +2133 -0
  15. package/src/RedactoNoticeAssisted/api/index.ts +269 -0
  16. package/src/RedactoNoticeAssisted/api/types.ts +180 -0
  17. package/src/RedactoNoticeAssisted/assets/redacto-logo.png +0 -0
  18. package/src/RedactoNoticeAssisted/constants.ts +51 -0
  19. package/src/RedactoNoticeAssisted/i18n.ts +1463 -0
  20. package/src/RedactoNoticeAssisted/index.ts +1 -0
  21. package/src/RedactoNoticeAssisted/injectStyles.ts +251 -0
  22. package/src/RedactoNoticeAssisted/styles.ts +730 -0
  23. package/src/RedactoNoticeAssisted/types.ts +24 -0
  24. package/src/RedactoNoticeAssisted/useMediaQuery.ts +36 -0
  25. package/src/RedactoPrivacyCenter/NotificationsPage.tsx +8 -0
  26. package/src/RedactoPrivacyCenter/api/actions.ts +6 -11
  27. package/src/RedactoPrivacyCenter/api/client.ts +15 -3
  28. package/src/RedactoPrivacyCenter/api/fetcher.ts +80 -1
  29. package/src/RedactoPrivacyCenter/components/ActivitiesList.tsx +193 -213
  30. package/src/RedactoPrivacyCenter/components/ActivityTimeline.tsx +239 -0
  31. package/src/RedactoPrivacyCenter/components/CaseDetails/MessagesTab.tsx +3 -1
  32. package/src/RedactoPrivacyCenter/components/CaseDetails/useMessagePolling.ts +8 -1
  33. package/src/RedactoPrivacyCenter/components/ConsentManager.tsx +23 -17
  34. package/src/RedactoPrivacyCenter/components/ConsentManagerCells.tsx +24 -10
  35. package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationBell.tsx +16 -3
  36. package/src/RedactoPrivacyCenter/components/constant.ts +25 -0
  37. package/src/RedactoPrivacyCenter/components/types.ts +32 -1
  38. package/src/RedactoPrivacyCenter/lib/consent-manager/utils.tsx +25 -23
  39. package/src/RedactoPrivacyCenter/lib/constants.ts +23 -10
  40. package/src/RedactoPrivacyCenter/lib/queries.ts +9 -2
  41. package/src/RedactoPrivacyCenter/lib/utils.refresh-circuit.test.ts +127 -0
  42. package/src/RedactoPrivacyCenter/lib/utils.refresh-storage.test.ts +85 -0
  43. package/src/RedactoPrivacyCenter/lib/utils.ts +213 -63
  44. package/src/RedactoPrivacyCenter/locales/en/translation.json +3 -0
  45. package/src/RedactoPrivacyCenter/styles/injectStyles.ts +16 -0
  46. package/src/index.ts +2 -0
  47. package/src/shared/api-errors.ts +9 -0
  48. package/src/RedactoPrivacyCenter/components/activitiesColumns.tsx +0 -141
@@ -1,22 +1,20 @@
1
- import { useState, useEffect, useCallback, useMemo } from 'react';
1
+ import { useState, useEffect, useCallback, useMemo, useRef, type CSSProperties } from 'react';
2
2
  import { useTranslation } from 'react-i18next';
3
- import { AdvancedTable, AdvancedTablePagination } from '../table';
4
- import { pcStyles } from '../styles/pcStyles';
5
- import { getCoreRowModel, useReactTable } from '@tanstack/react-table';
3
+ import { History } from 'lucide-react';
6
4
  import { fetchActivities } from '../lib/api';
7
5
  import { useOrganization } from '../context/OrganizationContext';
8
6
  import { useAuth } from '../context/AuthContext';
9
7
  import { useBaseUrl } from '../context/BaseUrlContext';
10
8
  import type { Activity } from '../lib/types';
11
- import { useActivitiesColumns } from './activitiesColumns';
12
9
  import { useCurrentLanguage } from '../lib/useCurrentLanguage';
13
10
  import { toast } from '../utils/toast';
11
+ import { getVar } from '../lib/utils';
12
+ import { ACTIVITY_PAGE_SIZE } from '../lib/constants';
13
+ import { PCText, PCLoader, PCEmpty } from '../ui';
14
+ import ActivityTimeline from './ActivityTimeline';
15
+ import { ACTIVITY_FETCH_ERROR_MESSAGE } from './constant';
14
16
 
15
- export default function ActivitiesList({
16
- variant,
17
- }: {
18
- variant?: 'modern' | 'minimal';
19
- }) {
17
+ export default function ActivitiesList() {
20
18
  const { t } = useTranslation();
21
19
  const { organization } = useOrganization();
22
20
  const { accessToken, refreshToken, onError, updateTokens } = useAuth();
@@ -35,235 +33,217 @@ export default function ActivitiesList({
35
33
  // Track language changes for localized API calls
36
34
  const { langParam } = useCurrentLanguage();
37
35
 
38
- const [activityData, setActivityData] = useState<Activity[]>([]);
36
+ const [activities, setActivities] = useState<Activity[]>([]);
39
37
  const [totalCount, setTotalCount] = useState(0);
40
- const [isLoading, setIsLoading] = useState(false);
41
- const [currentPage, setCurrentPage] = useState(1);
42
- const [pageSize, setPageSize] = useState(10);
43
- // Preserve previous data during loading to prevent pagination from disappearing
44
- const [previousData, setPreviousData] = useState<Activity[]>([]);
45
- const [previousTotalCount, setPreviousTotalCount] = useState(0);
46
-
47
- const columns = useActivitiesColumns();
38
+ const [isLoadingInitial, setIsLoadingInitial] = useState(true);
39
+ const [isLoadingMore, setIsLoadingMore] = useState(false);
40
+
41
+ const hasMore = activities.length < totalCount;
42
+
43
+ // Refs let the IntersectionObserver callback read fresh values without
44
+ // re-subscribing on every state change, and guard against double-fetch.
45
+ const isFetchingRef = useRef(false);
46
+ // Bumped on every reset; an in-flight loadMore from a prior org/workspace/
47
+ // language context checks it before appending so stale pages can't leak in.
48
+ const requestVersionRef = useRef(0);
49
+ const hasMoreRef = useRef(hasMore);
50
+ hasMoreRef.current = hasMore;
51
+ const countRef = useRef(0);
52
+ countRef.current = activities.length;
53
+ const sentinelRef = useRef<HTMLDivElement>(null);
54
+
55
+ const fetchPage = useCallback(
56
+ async (offset: number) => {
57
+ if (!organizationId || !workspaceId) return null;
58
+ return fetchActivities(
59
+ organizationId,
60
+ workspaceId,
61
+ offset,
62
+ ACTIVITY_PAGE_SIZE,
63
+ authContext,
64
+ consentClient,
65
+ langParam
66
+ );
67
+ },
68
+ [organizationId, workspaceId, authContext, consentClient, langParam]
69
+ );
48
70
 
49
- const handleGetActivities = useCallback(
50
- async (page: number = currentPage, size: number = pageSize) => {
51
- setIsLoading(true);
71
+ // Initial load (and reset) whenever org / workspace / language changes.
72
+ useEffect(() => {
73
+ let cancelled = false;
74
+ const requestVersion = ++requestVersionRef.current;
75
+ isFetchingRef.current = false;
76
+ setIsLoadingInitial(true);
77
+ setActivities([]);
78
+ setTotalCount(0);
79
+
80
+ (async () => {
52
81
  try {
53
- if (organizationId && workspaceId) {
54
- const offset = (page - 1) * size;
55
- const response = await fetchActivities(
56
- organizationId,
57
- workspaceId,
58
- offset,
59
- size,
60
- authContext,
61
- consentClient,
62
- langParam
63
- );
64
- if (!response) {
65
- throw new Error('Failed to fetch activity data');
66
- }
67
- setActivityData(response.data ?? []);
68
- setTotalCount(response.pagination?.total_count ?? 0);
69
- if (page !== currentPage) setCurrentPage(page);
70
- if (size !== pageSize) setPageSize(size);
82
+ const response = await fetchPage(0);
83
+ if (cancelled || requestVersion !== requestVersionRef.current) {
84
+ return;
71
85
  }
86
+ if (organizationId && workspaceId && !response) {
87
+ throw new Error(ACTIVITY_FETCH_ERROR_MESSAGE);
88
+ }
89
+ setActivities(response?.data ?? []);
90
+ setTotalCount(response?.pagination?.total_count ?? 0);
72
91
  } catch (error) {
92
+ if (cancelled || requestVersion !== requestVersionRef.current) {
93
+ return;
94
+ }
73
95
  console.error(error);
74
- setActivityData([]);
96
+ setActivities([]);
75
97
  setTotalCount(0);
76
- setPreviousData([]);
77
- setPreviousTotalCount(0);
78
98
  toast.error(t('failedToFetchActivityData'));
79
99
  } finally {
80
- setIsLoading(false);
100
+ if (!cancelled && requestVersion === requestVersionRef.current) {
101
+ setIsLoadingInitial(false);
102
+ }
81
103
  }
82
- },
83
- [organizationId, workspaceId, currentPage, pageSize, authContext, consentClient, langParam]
84
- );
104
+ })();
85
105
 
86
- // Preserve previous data when starting to load new page
87
- useEffect(() => {
88
- if (activityData.length > 0) {
89
- setPreviousData(activityData);
90
- setPreviousTotalCount(totalCount);
106
+ return () => {
107
+ cancelled = true;
108
+ };
109
+ }, [fetchPage, organizationId, workspaceId, t]);
110
+
111
+ const loadMore = useCallback(async () => {
112
+ if (isFetchingRef.current || !hasMoreRef.current) {
113
+ return;
91
114
  }
92
- }, [activityData, totalCount]);
115
+ const requestVersion = requestVersionRef.current;
116
+ isFetchingRef.current = true;
117
+ setIsLoadingMore(true);
118
+ try {
119
+ const response = await fetchPage(countRef.current);
120
+ if (requestVersion !== requestVersionRef.current) {
121
+ return;
122
+ }
123
+ if (!response) {
124
+ throw new Error(ACTIVITY_FETCH_ERROR_MESSAGE);
125
+ }
126
+ setActivities((prev) => [...prev, ...(response.data ?? [])]);
127
+ setTotalCount((prev) => response.pagination?.total_count ?? prev);
128
+ } catch (error) {
129
+ if (requestVersion !== requestVersionRef.current) {
130
+ return;
131
+ }
132
+ console.error(error);
133
+ toast.error(t('failedToFetchActivityData'));
134
+ } finally {
135
+ if (requestVersion === requestVersionRef.current) {
136
+ isFetchingRef.current = false;
137
+ setIsLoadingMore(false);
138
+ }
139
+ }
140
+ }, [fetchPage, t]);
93
141
 
142
+ // Auto-load as the sentinel nears the viewport. Re-subscribing when the
143
+ // loaded count changes lets a short list keep filling (the observer fires its
144
+ // initial callback on observe) until the viewport is full or all are loaded.
94
145
  useEffect(() => {
95
- void handleGetActivities();
96
- }, [organizationId, workspaceId, handleGetActivities]);
97
-
98
- // Use previous data during loading to maintain pagination visibility
99
- const displayData = isLoading && activityData.length === 0 ? previousData : activityData;
100
- const displayTotalCount = isLoading && totalCount === 0 ? previousTotalCount : totalCount;
146
+ const node = sentinelRef.current;
147
+ if (!node || isLoadingInitial) {
148
+ return;
149
+ }
101
150
 
102
- const table = useReactTable<Activity>({
103
- data: displayData,
104
- columns: columns,
105
- getCoreRowModel: getCoreRowModel(),
106
- manualPagination: true,
107
- pageCount: Math.ceil(displayTotalCount / pageSize) || 0,
108
- state: {
109
- pagination: {
110
- pageIndex: currentPage - 1,
111
- pageSize: pageSize,
151
+ const observer = new IntersectionObserver(
152
+ (entries) => {
153
+ if (entries[0]?.isIntersecting) {
154
+ void loadMore();
155
+ }
112
156
  },
113
- },
114
- onPaginationChange: (updater) => {
115
- const next =
116
- typeof updater === 'function'
117
- ? updater({ pageIndex: currentPage - 1, pageSize })
118
- : updater;
119
- const nextPage = (next as any).pageIndex + 1;
120
- const nextSize = (next as any).pageSize;
121
- if (nextPage === currentPage && nextSize === pageSize) return;
122
- setCurrentPage(nextPage);
123
- setPageSize(nextSize);
124
- void handleGetActivities(nextPage, nextSize);
125
- },
126
- });
127
-
128
- return (
129
- <div style={{ width: '100%' }}>
130
- {isLoading ? (
131
- <ActivitiesTableSkeleton
132
- labels={[t('activity'), t('activityType'), t('activityDescription'), t('timestamp')]}
133
- />
134
- ) : (
135
- <AdvancedTable table={table} variant={variant} isLoading={false} />
136
- )}
157
+ { root: null, rootMargin: '600px 0px' }
158
+ );
159
+ observer.observe(node);
160
+ return () => observer.disconnect();
161
+ }, [loadMore, isLoadingInitial, activities.length]);
162
+
163
+ let content;
164
+ if (isLoadingInitial) {
165
+ content = <ActivityTimelineSkeleton />;
166
+ } else if (activities.length === 0) {
167
+ content = (
168
+ <PCEmpty
169
+ icon={<History size={28} aria-hidden />}
170
+ title={t('noActivitiesYet')}
171
+ description={t('noActivitiesDescription')}
172
+ />
173
+ );
174
+ } else {
175
+ content = (
176
+ <>
177
+ <ActivityTimeline activities={activities} />
178
+
179
+ {/* Fixed-height footer: the sentinel stays mounted and the spinner
180
+ occupies reserved space, so loading never shifts the layout. */}
181
+ {hasMore ? (
182
+ <div style={{
183
+ position: 'relative',
184
+ minHeight: '2.75rem',
185
+ display: 'flex',
186
+ alignItems: 'center',
187
+ justifyContent: 'center',
188
+ }}>
189
+ <div
190
+ ref={sentinelRef}
191
+ aria-hidden
192
+ style={{ position: 'absolute', bottom: 0, left: 0, height: 1, width: 1 }}
193
+ />
194
+ {isLoadingMore && <PCLoader size="sm" />}
195
+ </div>
196
+ ) : (
197
+ <div style={{
198
+ display: 'flex',
199
+ alignItems: 'center',
200
+ justifyContent: 'center',
201
+ minHeight: '2.75rem',
202
+ }}>
203
+ <PCText as="span" size="xs" color="tertiary">
204
+ {t('allCaughtUp')}
205
+ </PCText>
206
+ </div>
207
+ )}
208
+ </>
209
+ );
210
+ }
137
211
 
138
- {/* Always show pagination, even during loading */}
139
- <div style={{
140
- ...pcStyles.flex,
141
- ...pcStyles.flexCol,
142
- ...pcStyles.itemsCenter,
143
- ...pcStyles.justifyBetween,
144
- ...pcStyles.gapSm,
145
- marginTop: '0.25rem',
146
- }} className="pc-flex-responsive">
147
- <AdvancedTablePagination table={table} />
148
- </div>
149
- </div>
150
- );
212
+ return <div style={{ width: '100%' }}>{content}</div>;
151
213
  }
152
214
 
153
- // Skeleton loader for Activities table
154
- function ActivitiesTableSkeleton({ labels }: { labels: string[] }) {
155
- const getVar = (varName: string): string => `var(${varName})`;
215
+ // Skeleton loader mirroring the timeline layout (time bar + small disc + content bars).
216
+ function ActivityTimelineSkeleton() {
217
+ const pulse = (extra: CSSProperties): CSSProperties => ({
218
+ animation: 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite',
219
+ backgroundColor: getVar('--pc-bg-tertiary'),
220
+ borderRadius: getVar('--pc-border-radius-md'),
221
+ ...extra,
222
+ });
156
223
 
157
224
  return (
158
- <div style={{
159
- borderRadius: getVar('--pc-border-radius-md'),
160
- border: `1px solid ${getVar('--pc-border-color')}`,
161
- backgroundColor: getVar('--pc-bg-primary'),
162
- }}>
163
- {/* Table Header */}
164
- <div style={{
165
- borderBottom: `1px solid ${getVar('--pc-border-color')}`,
166
- backgroundColor: getVar('--pc-bg-secondary'),
167
- }}>
168
- <div style={{
169
- display: 'grid',
170
- gridTemplateColumns: 'repeat(4, 1fr)',
171
- gap: getVar('--pc-spacing-sm'),
172
- paddingLeft: getVar('--pc-spacing-md'),
173
- paddingRight: getVar('--pc-spacing-md'),
174
- paddingTop: '0.625rem',
175
- paddingBottom: '0.625rem',
176
- }}>
177
- {labels.map((_, i) => (
178
- <div
179
- key={i}
180
- style={{
181
- height: '1rem',
182
- width: '6rem',
183
- animation: 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite',
184
- borderRadius: getVar('--pc-border-radius-md'),
185
- backgroundColor: getVar('--pc-bg-tertiary'),
186
- }}
187
- />
188
- ))}
189
- </div>
190
- </div>
191
-
192
- {/* Table Rows */}
193
- {Array.from({ length: 4 }).map((_, index) => (
225
+ <div style={{ width: '100%' }}>
226
+ <div style={{ ...pulse({ height: '0.75rem', width: '7rem' }), marginBottom: getVar('--pc-spacing-md') }} />
227
+ {Array.from({ length: 6 }).map((_, index) => (
194
228
  <div
195
229
  key={index}
196
230
  style={{
197
- borderBottom: `1px solid ${getVar('--pc-border-color')}`,
198
- backgroundColor: getVar('--pc-bg-primary'),
231
+ display: 'grid',
232
+ gridTemplateColumns: '64px 34px 1fr',
233
+ columnGap: '12px',
234
+ marginBottom: getVar('--pc-spacing-sm'),
199
235
  }}
200
236
  >
201
- <div style={{
202
- display: 'grid',
203
- gridTemplateColumns: 'repeat(4, 1fr)',
204
- gap: getVar('--pc-spacing-sm'),
205
- paddingLeft: getVar('--pc-spacing-md'),
206
- paddingRight: getVar('--pc-spacing-md'),
207
- paddingTop: '0.625rem',
208
- paddingBottom: '0.625rem',
209
- }}>
210
- {/* Activity Title */}
211
- <div style={{ ...pcStyles.flex, ...pcStyles.flexCol, ...pcStyles.gapXs }}>
212
- <div style={{
213
- height: '1rem',
214
- width: '8rem',
215
- animation: 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite',
216
- borderRadius: getVar('--pc-border-radius-md'),
217
- backgroundColor: getVar('--pc-bg-tertiary'),
218
- }} />
219
- </div>
220
-
221
- {/* Activity Type */}
222
- <div style={{ ...pcStyles.flex, ...pcStyles.itemsCenter }}>
223
- <div style={{
224
- height: '1.5rem',
225
- width: '5rem',
226
- animation: 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite',
227
- borderRadius: '9999px',
228
- backgroundColor: getVar('--pc-bg-tertiary'),
229
- }} />
230
- </div>
231
-
232
- {/* Description */}
233
- <div style={{ ...pcStyles.flex, ...pcStyles.flexCol, ...pcStyles.gapXs }}>
234
- <div style={{
235
- height: '0.75rem',
236
- width: '10rem',
237
- animation: 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite',
238
- borderRadius: getVar('--pc-border-radius-md'),
239
- backgroundColor: getVar('--pc-bg-tertiary'),
240
- }} />
241
- <div style={{
242
- height: '0.75rem',
243
- width: '8rem',
244
- animation: 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite',
245
- borderRadius: getVar('--pc-border-radius-md'),
246
- backgroundColor: getVar('--pc-bg-tertiary'),
247
- }} />
248
- </div>
249
-
250
- {/* Timestamp */}
251
- <div style={{ ...pcStyles.flex, ...pcStyles.flexCol, ...pcStyles.gapXs }}>
252
- <div style={{
253
- height: '1rem',
254
- width: '6rem',
255
- animation: 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite',
256
- borderRadius: getVar('--pc-border-radius-md'),
257
- backgroundColor: getVar('--pc-bg-tertiary'),
258
- }} />
259
- <div style={{
260
- height: '0.75rem',
261
- width: '5rem',
262
- animation: 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite',
263
- borderRadius: getVar('--pc-border-radius-md'),
264
- backgroundColor: getVar('--pc-bg-tertiary'),
265
- }} />
266
- </div>
237
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: '0.25rem', paddingTop: '0.5rem' }}>
238
+ <div style={pulse({ height: '0.75rem', width: '3rem' })} />
239
+ <div style={pulse({ height: '0.625rem', width: '2.25rem' })} />
240
+ </div>
241
+ <div style={{ display: 'flex', justifyContent: 'center', paddingTop: '0.5rem' }}>
242
+ <div style={pulse({ width: 30, height: 30, borderRadius: '9999px' })} />
243
+ </div>
244
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '0.4375rem', padding: '0.5rem 0.625rem' }}>
245
+ <div style={pulse({ height: '0.875rem', width: '40%' })} />
246
+ <div style={pulse({ height: '0.75rem', width: '72%' })} />
267
247
  </div>
268
248
  </div>
269
249
  ))}