@redacto.io/consent-sdk-react 9.2.0-beta.2 → 9.3.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.
- package/.turbo/turbo-build.log +25 -26
- package/.turbo/turbo-test.log +203 -0
- package/.turbo/turbo-verify-pack.log +6 -0
- package/CHANGELOG.md +24 -0
- package/dist/privacy-center.js +2428 -2223
- package/dist/privacy-center.mjs +2572 -2359
- package/package.json +3 -3
- package/src/RedactoPrivacyCenter/PrivacyCenterLayout.tsx +6 -3
- package/src/RedactoPrivacyCenter/components/AppShell/AppShell.tsx +36 -5
- package/src/RedactoPrivacyCenter/components/AppShell/TopAppBar.tsx +3 -2
- package/src/RedactoPrivacyCenter/components/PrivacyCenterEmptyState.tsx +76 -0
- package/src/RedactoPrivacyCenter/components/ReceiptsList.tsx +3 -41
- package/src/RedactoPrivacyCenter/context/DataAvailabilityContext.tsx +169 -0
- package/src/RedactoPrivacyCenter/locales/as/translation.json +2 -0
- package/src/RedactoPrivacyCenter/locales/bho/translation.json +2 -0
- package/src/RedactoPrivacyCenter/locales/bn/translation.json +2 -0
- package/src/RedactoPrivacyCenter/locales/brx/translation.json +2 -0
- package/src/RedactoPrivacyCenter/locales/doi/translation.json +2 -0
- package/src/RedactoPrivacyCenter/locales/en/translation.json +2 -0
- package/src/RedactoPrivacyCenter/locales/gom/translation.json +2 -0
- package/src/RedactoPrivacyCenter/locales/gu/translation.json +2 -0
- package/src/RedactoPrivacyCenter/locales/hi/translation.json +2 -0
- package/src/RedactoPrivacyCenter/locales/kn/translation.json +2 -0
- package/src/RedactoPrivacyCenter/locales/ks/translation.json +2 -0
- package/src/RedactoPrivacyCenter/locales/mai/translation.json +2 -0
- package/src/RedactoPrivacyCenter/locales/ml/translation.json +2 -0
- package/src/RedactoPrivacyCenter/locales/mni-Mtei/translation.json +2 -0
- package/src/RedactoPrivacyCenter/locales/mr/translation.json +2 -0
- package/src/RedactoPrivacyCenter/locales/ne/translation.json +2 -0
- package/src/RedactoPrivacyCenter/locales/or/translation.json +2 -0
- package/src/RedactoPrivacyCenter/locales/pa/translation.json +2 -0
- package/src/RedactoPrivacyCenter/locales/sa/translation.json +2 -0
- package/src/RedactoPrivacyCenter/locales/sat/translation.json +2 -0
- package/src/RedactoPrivacyCenter/locales/sd/translation.json +2 -0
- package/src/RedactoPrivacyCenter/locales/ta/translation.json +2 -0
- package/src/RedactoPrivacyCenter/locales/te/translation.json +2 -0
- package/src/RedactoPrivacyCenter/locales/ur/translation.json +2 -0
- package/src/RedactoPrivacyCenter/styles/injectStyles.ts +1 -1
- package/tests/DataAvailabilityContext.test.tsx +123 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@redacto.io/consent-sdk-react",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.3.0-beta.0",
|
|
4
4
|
"main": "dist/index.cjs",
|
|
5
5
|
"module": "dist/index.mjs",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -32,11 +32,11 @@
|
|
|
32
32
|
"@types/lodash": "^4.17.0",
|
|
33
33
|
"@types/react": "^19.1.3",
|
|
34
34
|
"@types/react-dom": "^19.1.3",
|
|
35
|
-
"@vitest/coverage-v8": "
|
|
35
|
+
"@vitest/coverage-v8": "^4.1.8",
|
|
36
36
|
"jsdom": "^26.1.0",
|
|
37
37
|
"tsup": "^8.4.0",
|
|
38
38
|
"typescript": "5.8.2",
|
|
39
|
-
"vitest": "^
|
|
39
|
+
"vitest": "^4.1.8"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
42
|
"react": "^16.8.0 || ^17 || ^18 || ^19",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useEffect, useState, useRef, type ReactNode } from 'react';
|
|
2
2
|
import { OrganizationProvider } from './context/OrganizationContext';
|
|
3
|
+
import { DataAvailabilityProvider } from './context/DataAvailabilityContext';
|
|
3
4
|
import type { OrganizationDetail } from './lib/types';
|
|
4
5
|
import {
|
|
5
6
|
checkAndRefreshAuth,
|
|
@@ -161,9 +162,11 @@ export function PrivacyCenterLayout({ children, slug, onBack }: PrivacyCenterLay
|
|
|
161
162
|
|
|
162
163
|
return (
|
|
163
164
|
<OrganizationProvider organization={organization}>
|
|
164
|
-
<
|
|
165
|
-
{
|
|
166
|
-
|
|
165
|
+
<DataAvailabilityProvider>
|
|
166
|
+
<AppShell onBack={onBack}>
|
|
167
|
+
{children}
|
|
168
|
+
</AppShell>
|
|
169
|
+
</DataAvailabilityProvider>
|
|
167
170
|
</OrganizationProvider>
|
|
168
171
|
);
|
|
169
172
|
}
|
|
@@ -19,6 +19,9 @@ import { TopAppBar } from './TopAppBar';
|
|
|
19
19
|
import { SidebarNav } from './SidebarNav';
|
|
20
20
|
import { MobileNavDrawer } from './MobileNavDrawer';
|
|
21
21
|
import { PageHeader } from './PageHeader';
|
|
22
|
+
import { PCLoader } from '../../ui';
|
|
23
|
+
import { useDataAvailability } from '../../context/DataAvailabilityContext';
|
|
24
|
+
import { PrivacyCenterEmptyState } from '../PrivacyCenterEmptyState';
|
|
22
25
|
import type { PrivacyCenterLayoutProps } from '../types';
|
|
23
26
|
|
|
24
27
|
export default function AppShell({
|
|
@@ -35,6 +38,13 @@ export default function AppShell({
|
|
|
35
38
|
|
|
36
39
|
const isMobile = useMediaQuery('(max-width: 639px)');
|
|
37
40
|
|
|
41
|
+
// Keep the shell minimal — top bar + a content spinner — until we've CONFIRMED
|
|
42
|
+
// the user has data. Hiding the sidebar/tabs/bell during 'checking' (not just
|
|
43
|
+
// 'empty') stops the nav from flashing in on refresh only to disappear when
|
|
44
|
+
// the API resolves to "no data". The branded top bar stays visible throughout.
|
|
45
|
+
const { status } = useDataAvailability();
|
|
46
|
+
const hideNav = status !== 'has-data';
|
|
47
|
+
|
|
38
48
|
// Close mobile drawer when crossing the breakpoint
|
|
39
49
|
useEffect(() => {
|
|
40
50
|
if (!isMobile && isMobileNavOpen) setMobileNavOpen(false);
|
|
@@ -110,14 +120,15 @@ export default function AppShell({
|
|
|
110
120
|
<main className="pc-root pc-shell-root" style={rootStyle}>
|
|
111
121
|
<TopAppBar
|
|
112
122
|
ref={hamburgerRef}
|
|
113
|
-
showHamburger={isMobile}
|
|
123
|
+
showHamburger={isMobile && !hideNav}
|
|
114
124
|
onOpenMobileNav={() => setMobileNavOpen(true)}
|
|
115
125
|
locale={locale}
|
|
116
126
|
onLocaleChange={setLocale}
|
|
117
127
|
onBack={onBack}
|
|
128
|
+
showNotifications={!hideNav}
|
|
118
129
|
/>
|
|
119
130
|
<div className="pc-shell-body" style={bodyStyle}>
|
|
120
|
-
{!isMobile && (
|
|
131
|
+
{!isMobile && !hideNav && (
|
|
121
132
|
<SidebarNav
|
|
122
133
|
className={sidebarClassName}
|
|
123
134
|
isExpanded={sidebar.isExpanded}
|
|
@@ -126,8 +137,28 @@ export default function AppShell({
|
|
|
126
137
|
)}
|
|
127
138
|
<section className="pc-shell-content" style={mainStyle}>
|
|
128
139
|
<div className={contentClassName} style={contentWrapperStyle}>
|
|
129
|
-
|
|
130
|
-
|
|
140
|
+
{status === 'checking' ? (
|
|
141
|
+
<div
|
|
142
|
+
style={{
|
|
143
|
+
...pcStyles.flex,
|
|
144
|
+
...pcStyles.itemsCenter,
|
|
145
|
+
...pcStyles.justifyCenter,
|
|
146
|
+
flex: 1,
|
|
147
|
+
minHeight: 240,
|
|
148
|
+
}}
|
|
149
|
+
>
|
|
150
|
+
<PCLoader variant="spinner" color="info" size="lg" />
|
|
151
|
+
</div>
|
|
152
|
+
) : status === 'empty' ? (
|
|
153
|
+
<div style={{ flex: 1, minWidth: 0, width: '100%' }}>
|
|
154
|
+
<PrivacyCenterEmptyState />
|
|
155
|
+
</div>
|
|
156
|
+
) : (
|
|
157
|
+
<>
|
|
158
|
+
<PageHeader />
|
|
159
|
+
<div style={{ flex: 1, minWidth: 0, width: '100%' }}>{children}</div>
|
|
160
|
+
</>
|
|
161
|
+
)}
|
|
131
162
|
<div
|
|
132
163
|
style={{
|
|
133
164
|
...pcStyles.flex,
|
|
@@ -143,7 +174,7 @@ export default function AppShell({
|
|
|
143
174
|
</div>
|
|
144
175
|
</section>
|
|
145
176
|
</div>
|
|
146
|
-
{isMobile && (
|
|
177
|
+
{isMobile && !hideNav && (
|
|
147
178
|
<MobileNavDrawer
|
|
148
179
|
isOpen={isMobileNavOpen}
|
|
149
180
|
onClose={() => setMobileNavOpen(false)}
|
|
@@ -26,10 +26,11 @@ type TopAppBarProps = {
|
|
|
26
26
|
locale: string;
|
|
27
27
|
onLocaleChange: (next: string) => void;
|
|
28
28
|
onBack?: 'back' | 'signout';
|
|
29
|
+
showNotifications?: boolean;
|
|
29
30
|
};
|
|
30
31
|
|
|
31
32
|
export const TopAppBar = forwardRef<HTMLButtonElement, TopAppBarProps>(function TopAppBar(
|
|
32
|
-
{ onOpenMobileNav, showHamburger, locale, onLocaleChange, onBack },
|
|
33
|
+
{ onOpenMobileNav, showHamburger, locale, onLocaleChange, onBack, showNotifications = true },
|
|
33
34
|
hamburgerRef,
|
|
34
35
|
) {
|
|
35
36
|
const { t } = useTranslation();
|
|
@@ -170,7 +171,7 @@ export const TopAppBar = forwardRef<HTMLButtonElement, TopAppBarProps>(function
|
|
|
170
171
|
onChange={handleLanguageChange}
|
|
171
172
|
/>
|
|
172
173
|
)}
|
|
173
|
-
<NotificationBell />
|
|
174
|
+
{showNotifications && <NotificationBell />}
|
|
174
175
|
<UserMenu onBack={onBack} />
|
|
175
176
|
</div>
|
|
176
177
|
</header>
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { useTranslation } from 'react-i18next';
|
|
2
|
+
import character from '../assets/character.svg';
|
|
3
|
+
import { PCText, PCTitle } from '../ui';
|
|
4
|
+
import { pcStyles } from '../styles/pcStyles';
|
|
5
|
+
import { getVar } from '../lib/utils';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Full-screen content shown (in place of the consent manager / tabs) when the
|
|
9
|
+
* user has no data anywhere — no consents, no data-request cases, no activity.
|
|
10
|
+
* Purely presentational: a friendly illustration, heading, and subtitle.
|
|
11
|
+
*/
|
|
12
|
+
export function PrivacyCenterEmptyState() {
|
|
13
|
+
const { t } = useTranslation();
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div
|
|
17
|
+
style={{
|
|
18
|
+
...pcStyles.flex,
|
|
19
|
+
...pcStyles.flexCol,
|
|
20
|
+
...pcStyles.itemsCenter,
|
|
21
|
+
...pcStyles.justifyCenter,
|
|
22
|
+
...pcStyles.textCenter,
|
|
23
|
+
width: '100%',
|
|
24
|
+
maxWidth: 460,
|
|
25
|
+
margin: '0 auto',
|
|
26
|
+
minHeight: 'min(62vh, 540px)',
|
|
27
|
+
paddingTop: getVar('--pc-spacing-2xl'),
|
|
28
|
+
paddingBottom: getVar('--pc-spacing-2xl'),
|
|
29
|
+
gap: getVar('--pc-spacing-xl'),
|
|
30
|
+
}}
|
|
31
|
+
>
|
|
32
|
+
<div
|
|
33
|
+
aria-hidden="true"
|
|
34
|
+
style={{
|
|
35
|
+
display: 'flex',
|
|
36
|
+
alignItems: 'center',
|
|
37
|
+
justifyContent: 'center',
|
|
38
|
+
width: 168,
|
|
39
|
+
height: 168,
|
|
40
|
+
borderRadius: '50%',
|
|
41
|
+
background:
|
|
42
|
+
'color-mix(in srgb, var(--pc-primary) 8%, var(--pc-bg-shell))',
|
|
43
|
+
flexShrink: 0,
|
|
44
|
+
}}
|
|
45
|
+
>
|
|
46
|
+
<img
|
|
47
|
+
src={character}
|
|
48
|
+
alt=""
|
|
49
|
+
style={{ width: 104, height: 'auto', display: 'block' }}
|
|
50
|
+
/>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div
|
|
54
|
+
style={{
|
|
55
|
+
...pcStyles.flex,
|
|
56
|
+
...pcStyles.flexCol,
|
|
57
|
+
...pcStyles.itemsCenter,
|
|
58
|
+
gap: getVar('--pc-spacing-sm'),
|
|
59
|
+
}}
|
|
60
|
+
>
|
|
61
|
+
<PCTitle as="h2" style={{ margin: 0 }}>
|
|
62
|
+
{t('noDataTitle')}
|
|
63
|
+
</PCTitle>
|
|
64
|
+
<PCText
|
|
65
|
+
as="p"
|
|
66
|
+
size="base"
|
|
67
|
+
color="secondary"
|
|
68
|
+
align="center"
|
|
69
|
+
style={{ margin: 0, maxWidth: 400 }}
|
|
70
|
+
>
|
|
71
|
+
{t('noDataDescription')}
|
|
72
|
+
</PCText>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -13,7 +13,7 @@ import { useAuth } from '../context/AuthContext';
|
|
|
13
13
|
import { useBaseUrl } from '../context/BaseUrlContext';
|
|
14
14
|
import { useCurrentLanguage } from '../lib/useCurrentLanguage';
|
|
15
15
|
import { toast } from '../utils/toast';
|
|
16
|
-
import type { Receipt, ReceiptEventType
|
|
16
|
+
import type { Receipt, ReceiptEventType } from '../lib/types';
|
|
17
17
|
import { useReceiptColumns } from './receiptColumns';
|
|
18
18
|
|
|
19
19
|
/**
|
|
@@ -89,7 +89,6 @@ export default function ReceiptsList() {
|
|
|
89
89
|
|
|
90
90
|
// ─── Filters ────────────────────────────────────────────────────────────────
|
|
91
91
|
const [eventType, setEventType] = useState<ReceiptEventType | ''>('');
|
|
92
|
-
const [status, setStatus] = useState<ReceiptPurposeStatus | ''>('');
|
|
93
92
|
const [datePreset, setDatePreset] = useState<string>(''); // '' | '1' | '7' | '30' | '90' | 'custom'
|
|
94
93
|
const [customFrom, setCustomFrom] = useState<string>('');
|
|
95
94
|
const [customTo, setCustomTo] = useState<string>('');
|
|
@@ -104,15 +103,6 @@ export default function ReceiptsList() {
|
|
|
104
103
|
],
|
|
105
104
|
[t]
|
|
106
105
|
);
|
|
107
|
-
const statusOptions = useMemo<SelectOption[]>(
|
|
108
|
-
() => [
|
|
109
|
-
{ value: '', label: t('all') },
|
|
110
|
-
{ value: 'CONSENTED', label: t('consented') },
|
|
111
|
-
{ value: 'DECLINED', label: t('declined') },
|
|
112
|
-
{ value: 'WITHDRAWN', label: t('withdrawn') },
|
|
113
|
-
],
|
|
114
|
-
[t]
|
|
115
|
-
);
|
|
116
106
|
const dateOptions = useMemo<SelectOption[]>(
|
|
117
107
|
() => [
|
|
118
108
|
{ value: '', label: t('allTime') },
|
|
@@ -126,9 +116,8 @@ export default function ReceiptsList() {
|
|
|
126
116
|
);
|
|
127
117
|
|
|
128
118
|
const eventValue = eventTypeOptions.find((o) => o.value === eventType) ?? null;
|
|
129
|
-
const statusValue = statusOptions.find((o) => o.value === status) ?? null;
|
|
130
119
|
const dateValue = dateOptions.find((o) => o.value === datePreset) ?? null;
|
|
131
|
-
const hasActiveFilters = Boolean(eventType ||
|
|
120
|
+
const hasActiveFilters = Boolean(eventType || datePreset);
|
|
132
121
|
const isCustomRangeInvalid = useMemo(
|
|
133
122
|
() =>
|
|
134
123
|
datePreset === 'custom' &&
|
|
@@ -142,9 +131,6 @@ export default function ReceiptsList() {
|
|
|
142
131
|
if (eventType && eventValue?.label) {
|
|
143
132
|
filters.push({ id: 'event', label: t('event'), value: eventValue.label });
|
|
144
133
|
}
|
|
145
|
-
if (status && statusValue?.label) {
|
|
146
|
-
filters.push({ id: 'status', label: t('status'), value: statusValue.label });
|
|
147
|
-
}
|
|
148
134
|
if (datePreset && dateValue?.label) {
|
|
149
135
|
let dateLabel = dateValue.label;
|
|
150
136
|
if (datePreset === 'custom') {
|
|
@@ -156,16 +142,12 @@ export default function ReceiptsList() {
|
|
|
156
142
|
}
|
|
157
143
|
|
|
158
144
|
return filters;
|
|
159
|
-
}, [eventType, eventValue,
|
|
145
|
+
}, [eventType, eventValue, datePreset, dateValue, customFrom, customTo, t]);
|
|
160
146
|
|
|
161
147
|
const handleEventChange = (opt: SelectOption | null) => {
|
|
162
148
|
setEventType((opt ? String(opt.value) : '') as ReceiptEventType | '');
|
|
163
149
|
setCurrentPage(1);
|
|
164
150
|
};
|
|
165
|
-
const handleStatusChange = (opt: SelectOption | null) => {
|
|
166
|
-
setStatus((opt ? String(opt.value) : '') as ReceiptPurposeStatus | '');
|
|
167
|
-
setCurrentPage(1);
|
|
168
|
-
};
|
|
169
151
|
const handleDateChange = (opt: SelectOption | null) => {
|
|
170
152
|
const nextDatePreset = opt ? String(opt.value) : '';
|
|
171
153
|
setDatePreset(nextDatePreset);
|
|
@@ -177,7 +159,6 @@ export default function ReceiptsList() {
|
|
|
177
159
|
};
|
|
178
160
|
const handleClearFilters = () => {
|
|
179
161
|
setEventType('');
|
|
180
|
-
setStatus('');
|
|
181
162
|
setDatePreset('');
|
|
182
163
|
setCustomFrom('');
|
|
183
164
|
setCustomTo('');
|
|
@@ -199,7 +180,6 @@ export default function ReceiptsList() {
|
|
|
199
180
|
skip,
|
|
200
181
|
limit: size,
|
|
201
182
|
event_type: eventType || undefined,
|
|
202
|
-
status: status || undefined,
|
|
203
183
|
created_after,
|
|
204
184
|
created_before,
|
|
205
185
|
language: langParam,
|
|
@@ -229,7 +209,6 @@ export default function ReceiptsList() {
|
|
|
229
209
|
currentPage,
|
|
230
210
|
pageSize,
|
|
231
211
|
eventType,
|
|
232
|
-
status,
|
|
233
212
|
datePreset,
|
|
234
213
|
customFrom,
|
|
235
214
|
customTo,
|
|
@@ -375,23 +354,6 @@ export default function ReceiptsList() {
|
|
|
375
354
|
dropdownMatchTriggerWidth
|
|
376
355
|
style={{ width: '100%' }}
|
|
377
356
|
/>
|
|
378
|
-
<PCSelect
|
|
379
|
-
className="pc-filter-select pc-receipts-filter-control"
|
|
380
|
-
size="sm"
|
|
381
|
-
label={t('status')}
|
|
382
|
-
placeholder={t('all')}
|
|
383
|
-
options={statusOptions}
|
|
384
|
-
value={statusValue}
|
|
385
|
-
onChange={handleStatusChange}
|
|
386
|
-
onClear={() => {
|
|
387
|
-
setStatus('');
|
|
388
|
-
setCurrentPage(1);
|
|
389
|
-
}}
|
|
390
|
-
clearable={Boolean(status)}
|
|
391
|
-
showSelectedCheck
|
|
392
|
-
dropdownMatchTriggerWidth
|
|
393
|
-
style={{ width: '100%' }}
|
|
394
|
-
/>
|
|
395
357
|
<PCSelect
|
|
396
358
|
className="pc-filter-select pc-receipts-filter-control"
|
|
397
359
|
size="sm"
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useCallback,
|
|
4
|
+
useContext,
|
|
5
|
+
useEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
type ReactNode,
|
|
10
|
+
} from 'react';
|
|
11
|
+
import { fetchUserConsents, fetchActivities, fetchCaseHistory } from '../lib/api';
|
|
12
|
+
import { useCurrentLanguage } from '../lib/useCurrentLanguage';
|
|
13
|
+
import { useOrganization } from './OrganizationContext';
|
|
14
|
+
import { useAuth } from './AuthContext';
|
|
15
|
+
import { useBaseUrl } from './BaseUrlContext';
|
|
16
|
+
|
|
17
|
+
export type DataAvailabilityStatus = 'checking' | 'has-data' | 'empty';
|
|
18
|
+
|
|
19
|
+
type DataAvailabilityContextValue = {
|
|
20
|
+
status: DataAvailabilityStatus;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const DataAvailabilityContext = createContext<DataAvailabilityContextValue | null>(null);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Determines whether the authenticated user has ANY data in the Privacy Center
|
|
27
|
+
* (consents, data-request cases, or activity). When all three are empty we
|
|
28
|
+
* surface a dedicated empty state instead of dropping the user into a shell
|
|
29
|
+
* full of empty tables.
|
|
30
|
+
*
|
|
31
|
+
* Fail-open by design: the underlying fetchers return `null` on non-auth
|
|
32
|
+
* errors (and may reject if a post-refresh retry throws). A `null`/rejected
|
|
33
|
+
* signal is treated as "has data" so a transient or auth error never wrongly
|
|
34
|
+
* locks a user out — the empty state shows ONLY when all three signals
|
|
35
|
+
* conclusively resolve to empty.
|
|
36
|
+
*/
|
|
37
|
+
export const DataAvailabilityProvider = ({ children }: { children: ReactNode }) => {
|
|
38
|
+
const { organization } = useOrganization();
|
|
39
|
+
const { accessToken, refreshToken, onError, updateTokens } = useAuth();
|
|
40
|
+
const { consentClient } = useBaseUrl();
|
|
41
|
+
const { langParam } = useCurrentLanguage();
|
|
42
|
+
|
|
43
|
+
const organizationId = organization?.uuid;
|
|
44
|
+
const workspaceId = organization?.workspaces[0]?.uuid;
|
|
45
|
+
|
|
46
|
+
// Memoized to keep the fetch identity stable across renders (mirrors the
|
|
47
|
+
// canonical pattern in ActivitiesList / AuthContext).
|
|
48
|
+
const authContext = useMemo(
|
|
49
|
+
() => ({ accessToken, refreshToken, onError, updateTokens }),
|
|
50
|
+
[accessToken, refreshToken, onError, updateTokens],
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const [status, setStatus] = useState<DataAvailabilityStatus>('checking');
|
|
54
|
+
|
|
55
|
+
// Monotonic run id guards against a stale in-flight check (org/token changed)
|
|
56
|
+
// overwriting a newer one; mountedRef guards against setState after unmount.
|
|
57
|
+
const runIdRef = useRef(0);
|
|
58
|
+
const mountedRef = useRef(true);
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
mountedRef.current = true;
|
|
61
|
+
return () => {
|
|
62
|
+
mountedRef.current = false;
|
|
63
|
+
};
|
|
64
|
+
}, []);
|
|
65
|
+
|
|
66
|
+
const runCheck = useCallback(async () => {
|
|
67
|
+
if (!organizationId || !workspaceId) return;
|
|
68
|
+
|
|
69
|
+
const runId = ++runIdRef.current;
|
|
70
|
+
setStatus('checking');
|
|
71
|
+
|
|
72
|
+
// Cases require the user's email (same source the request Form reads).
|
|
73
|
+
const contact =
|
|
74
|
+
typeof window !== 'undefined' ? sessionStorage.getItem('email') : null;
|
|
75
|
+
|
|
76
|
+
// Each signal resolves to a boolean "is empty". A `null` result (the
|
|
77
|
+
// fetchers' non-auth error path) or a rejection (rare post-refresh retry
|
|
78
|
+
// throw) resolves to `false` so the signal counts as "has data" — the
|
|
79
|
+
// empty state shows ONLY when all three conclusively resolve to empty.
|
|
80
|
+
const consentsEmptyP: Promise<boolean> = fetchUserConsents({
|
|
81
|
+
organizationId,
|
|
82
|
+
workspaceId,
|
|
83
|
+
limit: 1,
|
|
84
|
+
nominatedLimit: 1,
|
|
85
|
+
language: langParam,
|
|
86
|
+
authContext,
|
|
87
|
+
consentClient,
|
|
88
|
+
})
|
|
89
|
+
.then(
|
|
90
|
+
(v) =>
|
|
91
|
+
!!v &&
|
|
92
|
+
v.status_summary?.total_purposes === 0 &&
|
|
93
|
+
(v.direct?.length ?? 0) === 0 &&
|
|
94
|
+
(v.nominated?.length ?? 0) === 0 &&
|
|
95
|
+
(v.nominators?.length ?? 0) === 0,
|
|
96
|
+
)
|
|
97
|
+
.catch(() => false);
|
|
98
|
+
|
|
99
|
+
// No contact => we can't check cases, so treat as empty and rely on the
|
|
100
|
+
// other two signals' fail-open. With a contact, a null/rejected result
|
|
101
|
+
// fails open (not empty).
|
|
102
|
+
const casesEmptyP: Promise<boolean> = contact
|
|
103
|
+
? fetchCaseHistory(
|
|
104
|
+
organizationId,
|
|
105
|
+
workspaceId,
|
|
106
|
+
contact,
|
|
107
|
+
0,
|
|
108
|
+
1,
|
|
109
|
+
authContext,
|
|
110
|
+
consentClient,
|
|
111
|
+
langParam,
|
|
112
|
+
)
|
|
113
|
+
.then((v) => !!v && (v.data?.length ?? 0) === 0)
|
|
114
|
+
.catch(() => false)
|
|
115
|
+
: Promise.resolve(true);
|
|
116
|
+
|
|
117
|
+
const activitiesEmptyP: Promise<boolean> = fetchActivities(
|
|
118
|
+
organizationId,
|
|
119
|
+
workspaceId,
|
|
120
|
+
0,
|
|
121
|
+
1,
|
|
122
|
+
authContext,
|
|
123
|
+
consentClient,
|
|
124
|
+
langParam,
|
|
125
|
+
)
|
|
126
|
+
.then((v) => !!v && (v.pagination?.total_count ?? 0) === 0)
|
|
127
|
+
.catch(() => false);
|
|
128
|
+
|
|
129
|
+
const [consentsEmpty, casesEmpty, activitiesEmpty] = await Promise.all([
|
|
130
|
+
consentsEmptyP,
|
|
131
|
+
casesEmptyP,
|
|
132
|
+
activitiesEmptyP,
|
|
133
|
+
]);
|
|
134
|
+
|
|
135
|
+
// Ignore if a newer check superseded this one or we unmounted.
|
|
136
|
+
if (!mountedRef.current || runId !== runIdRef.current) return;
|
|
137
|
+
|
|
138
|
+
setStatus(
|
|
139
|
+
consentsEmpty && casesEmpty && activitiesEmpty ? 'empty' : 'has-data',
|
|
140
|
+
);
|
|
141
|
+
}, [organizationId, workspaceId, authContext, consentClient, langParam]);
|
|
142
|
+
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
if (organizationId && workspaceId && accessToken) {
|
|
145
|
+
void runCheck();
|
|
146
|
+
}
|
|
147
|
+
}, [organizationId, workspaceId, accessToken, runCheck]);
|
|
148
|
+
|
|
149
|
+
const value = useMemo<DataAvailabilityContextValue>(
|
|
150
|
+
() => ({ status }),
|
|
151
|
+
[status],
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<DataAvailabilityContext.Provider value={value}>
|
|
156
|
+
{children}
|
|
157
|
+
</DataAvailabilityContext.Provider>
|
|
158
|
+
);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
export const useDataAvailability = (): DataAvailabilityContextValue => {
|
|
162
|
+
const ctx = useContext(DataAvailabilityContext);
|
|
163
|
+
if (!ctx) {
|
|
164
|
+
throw new Error(
|
|
165
|
+
'useDataAvailability must be used within a DataAvailabilityProvider',
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
return ctx;
|
|
169
|
+
};
|
|
@@ -182,6 +182,8 @@
|
|
|
182
182
|
"grievanceTypes": "অভিযোগৰ প্ৰকাৰ",
|
|
183
183
|
"close": "বন্ধ কৰক",
|
|
184
184
|
"noData": "ডেটা নাই",
|
|
185
|
+
"noDataTitle": "এতিয়ালৈকে দেখুৱাবলৈ একো নাই",
|
|
186
|
+
"noDataDescription": "আপোনাৰ একাউণ্টৰ সৈতে সংযুক্ত কোনো সন্মতি, অনুৰোধ বা কাৰ্য্যাৱলী আমি এতিয়ালৈকে বিচাৰি পোৱা নাই। উপলব্ধ হোৱাৰ লগে লগে আপোনাৰ গোপনীয়তা ৰেকৰ্ডসমূহ ইয়াত দেখা যাব।",
|
|
185
187
|
"rowsPerPage": "পৃষ্ঠাত শাৰী:",
|
|
186
188
|
"pageOf": "পৃষ্ঠা {{current}} / {{total}}",
|
|
187
189
|
"rowsSelected": "{{total}}-ৰ {{count}} শাৰী নিৰ্বাচিত",
|
|
@@ -182,6 +182,8 @@
|
|
|
182
182
|
"grievanceTypes": "शिकायत प्रकार",
|
|
183
183
|
"close": "बन्द करीं",
|
|
184
184
|
"noData": "कोनो डेटा नै",
|
|
185
|
+
"noDataTitle": "अबहीं देखावे खातिर कुछ नइखे",
|
|
186
|
+
"noDataDescription": "रउरा खाता से जुड़ल कवनो सहमति, अनुरोध, या गतिविधि अबहीं तक ना मिलल। रउरा गोपनीयता रिकॉर्ड उपलब्ध होते ई इहाँ देखाई दिही।",
|
|
185
187
|
"rowsPerPage": "पृष्ठ प्रति पंक्ति:",
|
|
186
188
|
"pageOf": "पृष्ठ {{current}} / {{total}}",
|
|
187
189
|
"rowsSelected": "{{total}} में से {{count}} पंक्ति चुनल गइल",
|
|
@@ -182,6 +182,8 @@
|
|
|
182
182
|
"grievanceTypes": "অভিযোগের ধরন",
|
|
183
183
|
"close": "বন্ধ করুন",
|
|
184
184
|
"noData": "কোনো ডেটা নেই",
|
|
185
|
+
"noDataTitle": "এখনো দেখানোর মতো কিছু নেই",
|
|
186
|
+
"noDataDescription": "আপনার অ্যাকাউন্টের সাথে সম্পর্কিত কোনো সম্মতি, অনুরোধ বা ক্রিয়াকলাপ আমরা এখনো খুঁজে পাইনি। উপলব্ধ হওয়ার সাথে সাথেই আপনার গোপনীয়তা রেকর্ডগুলি এখানে প্রদর্শিত হবে।",
|
|
185
187
|
"rowsPerPage": "প্রতি পৃষ্ঠায় সারি:",
|
|
186
188
|
"pageOf": "পৃষ্ঠা {{current}} / {{total}}",
|
|
187
189
|
"rowsSelected": "{{total}}-এর মধ্যে {{count}} সারি নির্বাচিত",
|
|
@@ -182,6 +182,8 @@
|
|
|
182
182
|
"grievanceTypes": "गुनासो थाखो",
|
|
183
183
|
"close": "बन्द खालाम",
|
|
184
184
|
"noData": "डेटा नंगौ",
|
|
185
|
+
"noDataTitle": "दानो दिनथिनो थाखाय जेबो गैया",
|
|
186
|
+
"noDataDescription": "दानो सम नोंथांनि एकाउन्टजों सोमोन्दो थानाय जेबो सहमति, हाबाय एबा खामानि जों मोननो हायाखै। नोंथांनि गोपनीयथा रिकर्डफोर मोननो हानाय बादि बेयावनो दिनथिजागोन।",
|
|
185
187
|
"rowsPerPage": "पृष्ठनि पंक्ति:",
|
|
186
188
|
"pageOf": "पृष्ठ {{current}} / {{total}}",
|
|
187
189
|
"rowsSelected": "{{total}} निफ्राय {{count}} पंक्ति सायख 'खै",
|
|
@@ -182,6 +182,8 @@
|
|
|
182
182
|
"grievanceTypes": "शिकायत प्रकार",
|
|
183
183
|
"close": "बंद करो",
|
|
184
184
|
"noData": "कोनो डेटा नै",
|
|
185
|
+
"noDataTitle": "अजें दिक्खाने आस्तै किछ नेईं",
|
|
186
|
+
"noDataDescription": "तुंदे खाते कन्नै जुड़ी दी कोई बी सहमति, बेनती जां गतिविधि अजें तक नेईं मिली। तुंदे गोपनीयता रिकॉर्ड उपलब्ध होंदे गै इत्थें दिक्खने गी मिलङन।",
|
|
185
187
|
"rowsPerPage": "पृष्ठ प्रति पंक्ति:",
|
|
186
188
|
"pageOf": "पृष्ठ {{current}} / {{total}}",
|
|
187
189
|
"rowsSelected": "{{total}} मध्ये {{count}} पंक्ति चुनल गेल",
|
|
@@ -195,6 +195,8 @@
|
|
|
195
195
|
"grievanceTypes": "Grievance Types",
|
|
196
196
|
"close": "Close",
|
|
197
197
|
"noData": "No Data",
|
|
198
|
+
"noDataTitle": "Nothing to show yet",
|
|
199
|
+
"noDataDescription": "We couldn't find any consents, requests, or activity linked to your account yet. Your privacy records will appear here once they're available.",
|
|
198
200
|
"rowsPerPage": "Rows per page:",
|
|
199
201
|
"pageOf": "Page {{current}} of {{total}}",
|
|
200
202
|
"rowsSelected": "{{count}} of {{total}} row(s) selected",
|
|
@@ -182,6 +182,8 @@
|
|
|
182
182
|
"grievanceTypes": "तक्रार प्रकार",
|
|
183
183
|
"close": "बंद करात",
|
|
184
184
|
"noData": "डेटा ना",
|
|
185
|
+
"noDataTitle": "अजून दाखोवपाक कांय ना",
|
|
186
|
+
"noDataDescription": "तुमच्या खात्याकडेन जोडिल्ली कसलीच संमती, मागण्यो वा कृती आमकां अजून मेळूंक नात. उपलब्ध जातकच तुमच्यो गोपनीयता नोंदी हांगा दिसतल्यो.",
|
|
185
187
|
"rowsPerPage": "पानाक पंक्ती:",
|
|
186
188
|
"pageOf": "पान {{current}} / {{total}}",
|
|
187
189
|
"rowsSelected": "{{total}} मध्ये {{count}} पंक्ती निवडली",
|
|
@@ -182,6 +182,8 @@
|
|
|
182
182
|
"grievanceTypes": "ફરિયાદ પ્રકારો",
|
|
183
183
|
"close": "બંધ કરો",
|
|
184
184
|
"noData": "ડેટા નથી",
|
|
185
|
+
"noDataTitle": "હજુ સુધી બતાવવા માટે કંઈ નથી",
|
|
186
|
+
"noDataDescription": "તમારા ખાતા સાથે જોડાયેલી કોઈ સંમતિઓ, વિનંતીઓ અથવા પ્રવૃત્તિ અમને હજુ સુધી મળી નથી. તમારા ગોપનીયતા રેકોર્ડ ઉપલબ્ધ થતાં જ અહીં દેખાશે.",
|
|
185
187
|
"rowsPerPage": "દર પૃષ્ઠ પંક્તિઓ:",
|
|
186
188
|
"pageOf": "પૃષ્ઠ {{current}} / {{total}}",
|
|
187
189
|
"rowsSelected": "{{total}} માંથી {{count}} પંક્તિ પસંદ",
|
|
@@ -185,6 +185,8 @@
|
|
|
185
185
|
"grievanceTypes": "शिकायत प्रकार",
|
|
186
186
|
"close": "बंद करें",
|
|
187
187
|
"noData": "कोई डेटा नहीं",
|
|
188
|
+
"noDataTitle": "अभी दिखाने के लिए कुछ नहीं",
|
|
189
|
+
"noDataDescription": "हमें अभी तक आपके खाते से जुड़ी कोई सहमति, अनुरोध या गतिविधि नहीं मिली। उपलब्ध होते ही आपके गोपनीयता रिकॉर्ड यहाँ दिखाई देंगे।",
|
|
188
190
|
"rowsPerPage": "प्रति पृष्ठ पंक्तियाँ:",
|
|
189
191
|
"pageOf": "पृष्ठ {{current}} / {{total}}",
|
|
190
192
|
"rowsSelected": "कुल {{total}} में से {{count}} पंक्तियाँ चयनित",
|
|
@@ -182,6 +182,8 @@
|
|
|
182
182
|
"grievanceTypes": "ಪರಾತಿ ವಿಧಗಳು",
|
|
183
183
|
"close": "ಮುಚ್ಚಿ",
|
|
184
184
|
"noData": "ಡೇಟಾ ಇಲ್ಲ",
|
|
185
|
+
"noDataTitle": "ಇನ್ನೂ ತೋರಿಸಲು ಏನೂ ಇಲ್ಲ",
|
|
186
|
+
"noDataDescription": "ನಿಮ್ಮ ಖಾತೆಗೆ ಸಂಬಂಧಿಸಿದ ಯಾವುದೇ ಸಮ್ಮತಿಗಳು, ವಿನಂತಿಗಳು ಅಥವಾ ಚಟುವಟಿಕೆ ನಮಗೆ ಇನ್ನೂ ಕಂಡುಬಂದಿಲ್ಲ. ನಿಮ್ಮ ಗೌಪ್ಯತಾ ದಾಖಲೆಗಳು ಲಭ್ಯವಾದ ತಕ್ಷಣ ಅವು ಇಲ್ಲಿ ಗೋಚರಿಸುತ್ತವೆ.",
|
|
185
187
|
"rowsPerPage": "ಪುಟಕ್ಕೆ ಸಾಲುಗಳು:",
|
|
186
188
|
"pageOf": "ಪುಟ {{current}} / {{total}}",
|
|
187
189
|
"rowsSelected": "{{total}} ರಲ್ಲಿ {{count}} ಸಾಲು ಆಯ್ಕೆಮಾಡಲಾಗಿದೆ",
|
|
@@ -182,6 +182,8 @@
|
|
|
182
182
|
"grievanceTypes": "شکایت قسم",
|
|
183
183
|
"close": "بند کرو",
|
|
184
184
|
"noData": "ڈیٹا کانہہ",
|
|
185
|
+
"noDataTitle": "ابٕ تام ہاوُن خٲطرٕ کانٛہہ نٕ",
|
|
186
|
+
"noDataDescription": "اسہِ تُہنٛدِ اکاؤنٹَس سٕتۍ جُڑٕمٕژ کانٛہہِ رضامندی، درخواستٕ یا سرگرمی ابٕ تام نٕ لَب۔ تُہنٛد رازداری ریکارڈ یہِ دستیاب سپدِتھ یِتہِ کنہِ وُچھنہٕ یِیِہٕ۔",
|
|
185
187
|
"rowsPerPage": "صفحہ فی قطاراں:",
|
|
186
188
|
"pageOf": "صفحہ {{current}} / {{total}}",
|
|
187
189
|
"rowsSelected": "{{total}} منز {{count}} قطار چُنِت",
|
|
@@ -182,6 +182,8 @@
|
|
|
182
182
|
"grievanceTypes": "शिकायत प्रकार",
|
|
183
183
|
"close": "बन्द करू",
|
|
184
184
|
"noData": "कोनो डेटा नै",
|
|
185
|
+
"noDataTitle": "अखन तक देखाबय लेल किछु नहि",
|
|
186
|
+
"noDataDescription": "अहाँक खाता सँ जुड़ल कोनो सहमति, अनुरोध, वा गतिविधि अखन तक नहि भेटल। उपलब्ध होइते अहाँक गोपनीयता रिकॉर्ड एतय देखाएत।",
|
|
185
187
|
"rowsPerPage": "पृष्ठ प्रति पंक्ति:",
|
|
186
188
|
"pageOf": "पृष्ठ {{current}} / {{total}}",
|
|
187
189
|
"rowsSelected": "{{total}} मध्ये {{count}} पंक्ति चुनल गेल",
|
|
@@ -182,6 +182,8 @@
|
|
|
182
182
|
"grievanceTypes": "പരാതി തരങ്ങൾ",
|
|
183
183
|
"close": "അടയ്ക്കുക",
|
|
184
184
|
"noData": "ഡാറ്റയില്ല",
|
|
185
|
+
"noDataTitle": "ഇതുവരെ കാണിക്കാൻ ഒന്നുമില്ല",
|
|
186
|
+
"noDataDescription": "നിങ്ങളുടെ അക്കൗണ്ടുമായി ബന്ധിപ്പിച്ച സമ്മതങ്ങളോ അഭ്യർത്ഥനകളോ പ്രവൃത്തിയോ ഇതുവരെ ഞങ്ങൾക്ക് കണ്ടെത്താനായില്ല. നിങ്ങളുടെ സ്വകാര്യതാ രേഖകൾ ലഭ്യമാകുമ്പോൾ അവ ഇവിടെ ദൃശ്യമാകും.",
|
|
185
187
|
"rowsPerPage": "പേജിന് വരികൾ:",
|
|
186
188
|
"pageOf": "പേജ് {{current}} / {{total}}",
|
|
187
189
|
"rowsSelected": "{{total}}-ൽ {{count}} വരി തിരഞ്ഞെടുത്തു",
|