@redacto.io/consent-sdk-react 9.1.0 → 9.2.0-beta.1
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 +10 -9
- package/CHANGELOG.md +12 -0
- package/dist/index.js +2 -4
- package/dist/index.mjs +2 -4
- package/dist/privacy-center.d.mts +56 -2
- package/dist/privacy-center.d.ts +56 -2
- package/dist/privacy-center.js +4840 -1874
- package/dist/privacy-center.mjs +5124 -2158
- package/package.json +1 -1
- package/src/RedactoNoticeConsent/RedactoNoticeConsent.tsx +2 -4
- package/src/RedactoNoticeConsent/api/types.ts +1 -0
- package/src/RedactoPrivacyCenter/PrivacyCenterCaseDetailsPage.tsx +3 -1
- package/src/RedactoPrivacyCenter/PrivacyCenterHomePage.tsx +3 -0
- package/src/RedactoPrivacyCenter/PrivacyCenterMyReceiptPage.tsx +26 -0
- package/src/RedactoPrivacyCenter/api/actions.ts +66 -0
- package/src/RedactoPrivacyCenter/api/client.ts +79 -0
- package/src/RedactoPrivacyCenter/components/CaseDetails/DocumentRequestCard.tsx +19 -13
- package/src/RedactoPrivacyCenter/components/CaseDetails/FinalizedTooltipWrapper.tsx +25 -0
- package/src/RedactoPrivacyCenter/components/CaseDetails/MessageComposer.tsx +39 -30
- package/src/RedactoPrivacyCenter/components/CaseDetails/MessageItem.tsx +2 -0
- package/src/RedactoPrivacyCenter/components/CaseDetails/MessagesTab.tsx +10 -3
- package/src/RedactoPrivacyCenter/components/CaseDetails/utils.ts +21 -6
- package/src/RedactoPrivacyCenter/components/DataRequest/ProductSelectionSection.tsx +222 -0
- package/src/RedactoPrivacyCenter/components/DataRequest/PurposeSelectionSection.tsx +12 -0
- package/src/RedactoPrivacyCenter/components/Form.tsx +843 -162
- package/src/RedactoPrivacyCenter/components/ReceiptsList.tsx +492 -0
- package/src/RedactoPrivacyCenter/components/RequestSubmit.tsx +284 -71
- package/src/RedactoPrivacyCenter/components/SideMenuBarPC/constants.tsx +6 -1
- package/src/RedactoPrivacyCenter/components/groupHelpers.ts +56 -0
- package/src/RedactoPrivacyCenter/components/receiptColumns.tsx +203 -0
- package/src/RedactoPrivacyCenter/components/types.ts +26 -2
- package/src/RedactoPrivacyCenter/context/NavigationContext.tsx +2 -2
- package/src/RedactoPrivacyCenter/lib/api.ts +95 -1
- package/src/RedactoPrivacyCenter/lib/constants.ts +17 -0
- package/src/RedactoPrivacyCenter/lib/types.ts +115 -0
- package/src/RedactoPrivacyCenter/locales/as/translation.json +40 -1
- package/src/RedactoPrivacyCenter/locales/bho/translation.json +40 -1
- package/src/RedactoPrivacyCenter/locales/bn/translation.json +40 -1
- package/src/RedactoPrivacyCenter/locales/brx/translation.json +40 -1
- package/src/RedactoPrivacyCenter/locales/doi/translation.json +40 -1
- package/src/RedactoPrivacyCenter/locales/en/translation.json +46 -1
- package/src/RedactoPrivacyCenter/locales/gom/translation.json +40 -1
- package/src/RedactoPrivacyCenter/locales/gu/translation.json +40 -1
- package/src/RedactoPrivacyCenter/locales/hi/translation.json +40 -1
- package/src/RedactoPrivacyCenter/locales/kn/translation.json +40 -1
- package/src/RedactoPrivacyCenter/locales/ks/translation.json +40 -1
- package/src/RedactoPrivacyCenter/locales/mai/translation.json +40 -1
- package/src/RedactoPrivacyCenter/locales/ml/translation.json +40 -1
- package/src/RedactoPrivacyCenter/locales/mni-Mtei/translation.json +40 -1
- package/src/RedactoPrivacyCenter/locales/mr/translation.json +40 -1
- package/src/RedactoPrivacyCenter/locales/ne/translation.json +40 -1
- package/src/RedactoPrivacyCenter/locales/or/translation.json +40 -1
- package/src/RedactoPrivacyCenter/locales/pa/translation.json +40 -1
- package/src/RedactoPrivacyCenter/locales/sa/translation.json +40 -1
- package/src/RedactoPrivacyCenter/locales/sat/translation.json +40 -1
- package/src/RedactoPrivacyCenter/locales/sd/translation.json +40 -1
- package/src/RedactoPrivacyCenter/locales/ta/translation.json +40 -1
- package/src/RedactoPrivacyCenter/locales/te/translation.json +40 -1
- package/src/RedactoPrivacyCenter/locales/ur/translation.json +40 -1
- package/src/RedactoPrivacyCenter/styles/injectStyles.ts +301 -0
- package/src/RedactoPrivacyCenter/ui/PCTooltip.tsx +3 -1
- package/tests/Form.test.tsx +1170 -0
- package/tests/ProductSelectionSection.test.tsx +293 -0
- package/tests/RequestSubmit.test.tsx +295 -0
package/package.json
CHANGED
|
@@ -4130,13 +4130,11 @@ export const RedactoNoticeConsent = ({
|
|
|
4130
4130
|
content.dpo_info.dpo_text || ""
|
|
4131
4131
|
)}
|
|
4132
4132
|
</span>{" "}
|
|
4133
|
-
{content.dpo_info.
|
|
4133
|
+
{content.dpo_info.dpo_email && (
|
|
4134
4134
|
<a
|
|
4135
4135
|
id="dpo-dpo-link"
|
|
4136
4136
|
style={{ ...styles.link, ...linkStyle }}
|
|
4137
|
-
href={content.dpo_info.
|
|
4138
|
-
target="_blank"
|
|
4139
|
-
rel="noopener noreferrer"
|
|
4137
|
+
href={`mailto:${content.dpo_info.dpo_email}`}
|
|
4140
4138
|
>
|
|
4141
4139
|
{getTranslatedDpoText(
|
|
4142
4140
|
"dpo_anchor_text",
|
|
@@ -144,7 +144,9 @@ export const PrivacyCenterCaseDetailsPage = () => {
|
|
|
144
144
|
|
|
145
145
|
{/* Tab content */}
|
|
146
146
|
<div className="pc-case-tab-content">
|
|
147
|
-
{activeTab === 'messages' &&
|
|
147
|
+
{activeTab === 'messages' && (
|
|
148
|
+
<MessagesTab caseUuid={selectedCase.uuid} caseStatus={selectedCase.status} />
|
|
149
|
+
)}
|
|
148
150
|
{activeTab === 'request-details' && (
|
|
149
151
|
<RequestDetailsTab selectedCase={selectedCase} request_details={request_details} requestTypeLabel={requestTypeLabel} />
|
|
150
152
|
)}
|
|
@@ -4,6 +4,7 @@ import { useInternalNavigation } from './context/NavigationContext';
|
|
|
4
4
|
import { PrivacyCenterConsentManagerPage } from './PrivacyCenterConsentManagerPage';
|
|
5
5
|
import { PrivacyCenterFormPage } from './PrivacyCenterFormPage';
|
|
6
6
|
import { PrivacyCenterActivityPage } from './PrivacyCenterActivityPage';
|
|
7
|
+
import { PrivacyCenterMyReceiptPage } from './PrivacyCenterMyReceiptPage';
|
|
7
8
|
import { PrivacyCenterCaseDetailsPage } from './PrivacyCenterCaseDetailsPage';
|
|
8
9
|
import { NotificationsPage } from './NotificationsPage';
|
|
9
10
|
|
|
@@ -23,6 +24,8 @@ export function PrivacyCenterHomePage() {
|
|
|
23
24
|
return <PrivacyCenterFormPage />;
|
|
24
25
|
case 'activity':
|
|
25
26
|
return <PrivacyCenterActivityPage />;
|
|
27
|
+
case 'receipts':
|
|
28
|
+
return <PrivacyCenterMyReceiptPage />;
|
|
26
29
|
case 'case-details':
|
|
27
30
|
return <PrivacyCenterCaseDetailsPage />;
|
|
28
31
|
case 'notifications':
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import i18n from './lib/i18n';
|
|
3
|
+
import ReceiptsList from './components/ReceiptsList';
|
|
4
|
+
|
|
5
|
+
export function PrivacyCenterMyReceiptPage() {
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
if (!i18n.language) {
|
|
8
|
+
i18n.changeLanguage('en');
|
|
9
|
+
}
|
|
10
|
+
}, []);
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<div
|
|
14
|
+
style={{
|
|
15
|
+
width: '100%',
|
|
16
|
+
display: 'flex',
|
|
17
|
+
flexDirection: 'column',
|
|
18
|
+
gap: '0.5rem',
|
|
19
|
+
}}
|
|
20
|
+
>
|
|
21
|
+
<div style={{ paddingTop: '0.25rem' }}>
|
|
22
|
+
<ReceiptsList />
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -419,6 +419,72 @@ export async function fetchActivitiesServer(params: {
|
|
|
419
419
|
}
|
|
420
420
|
}
|
|
421
421
|
|
|
422
|
+
export async function fetchReceiptsServer(params: {
|
|
423
|
+
organizationId: string;
|
|
424
|
+
workspaceId: string;
|
|
425
|
+
skip?: number;
|
|
426
|
+
limit?: number;
|
|
427
|
+
notice_uuid?: string;
|
|
428
|
+
event_type?: string;
|
|
429
|
+
status?: string;
|
|
430
|
+
created_after?: string;
|
|
431
|
+
created_before?: string;
|
|
432
|
+
language?: string;
|
|
433
|
+
authToken?: string;
|
|
434
|
+
consentClient?: ReturnType<typeof import('./client').createConsentClient>;
|
|
435
|
+
}): Promise<unknown | null> {
|
|
436
|
+
try {
|
|
437
|
+
if (!params.consentClient) {
|
|
438
|
+
throw new Error('Consent client is required');
|
|
439
|
+
}
|
|
440
|
+
// Privacy center endpoints use JWT Bearer tokens, not session-based auth
|
|
441
|
+
const baseRequest = buildBaseRequest(params.organizationId, params.workspaceId, params.authToken);
|
|
442
|
+
|
|
443
|
+
const result = await params.consentClient.getReceipts(
|
|
444
|
+
baseRequest,
|
|
445
|
+
{
|
|
446
|
+
skip: params.skip,
|
|
447
|
+
limit: params.limit,
|
|
448
|
+
notice_uuid: params.notice_uuid,
|
|
449
|
+
event_type: params.event_type,
|
|
450
|
+
status: params.status,
|
|
451
|
+
created_after: params.created_after,
|
|
452
|
+
created_before: params.created_before,
|
|
453
|
+
language: params.language,
|
|
454
|
+
},
|
|
455
|
+
params.authToken
|
|
456
|
+
);
|
|
457
|
+
return result;
|
|
458
|
+
} catch (error: any) {
|
|
459
|
+
return handleServerActionError(error, 'fetching receipts');
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export async function fetchReceiptPdfServer(params: {
|
|
464
|
+
organizationId: string;
|
|
465
|
+
workspaceId: string;
|
|
466
|
+
receiptUuid: string;
|
|
467
|
+
authToken?: string;
|
|
468
|
+
consentClient?: ReturnType<typeof import('./client').createConsentClient>;
|
|
469
|
+
}): Promise<Blob | null> {
|
|
470
|
+
try {
|
|
471
|
+
if (!params.consentClient) {
|
|
472
|
+
throw new Error('Consent client is required');
|
|
473
|
+
}
|
|
474
|
+
const baseRequest = buildBaseRequest(params.organizationId, params.workspaceId, params.authToken);
|
|
475
|
+
|
|
476
|
+
const result = await params.consentClient.getReceiptPdf(
|
|
477
|
+
baseRequest,
|
|
478
|
+
params.receiptUuid,
|
|
479
|
+
params.authToken
|
|
480
|
+
);
|
|
481
|
+
return result;
|
|
482
|
+
} catch (error: any) {
|
|
483
|
+
// Re-throw 404s (receipt doesn't exist or not yours) so the UI can show a targeted message
|
|
484
|
+
return handleServerActionError(error, 'downloading receipt PDF', true);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
422
488
|
export async function fetchCaseHistoryServer(params: {
|
|
423
489
|
organizationId: string;
|
|
424
490
|
workspaceId: string;
|
|
@@ -464,6 +464,85 @@ export function createConsentClient(baseUrl: string) {
|
|
|
464
464
|
});
|
|
465
465
|
},
|
|
466
466
|
|
|
467
|
+
/**
|
|
468
|
+
* Get receipts (privacy center) — consent receipts for the authenticated user
|
|
469
|
+
*/
|
|
470
|
+
async getReceipts(
|
|
471
|
+
baseRequest: BaseRequest,
|
|
472
|
+
params: {
|
|
473
|
+
skip?: number;
|
|
474
|
+
limit?: number;
|
|
475
|
+
notice_uuid?: string;
|
|
476
|
+
event_type?: string;
|
|
477
|
+
status?: string;
|
|
478
|
+
created_after?: string;
|
|
479
|
+
created_before?: string;
|
|
480
|
+
language?: string;
|
|
481
|
+
},
|
|
482
|
+
authToken?: string,
|
|
483
|
+
options?: Omit<ApiFetchOptions, "getAuthToken">
|
|
484
|
+
): Promise<unknown> {
|
|
485
|
+
const queryParams = new URLSearchParams();
|
|
486
|
+
if (params.skip !== undefined) queryParams.append("skip", params.skip.toString());
|
|
487
|
+
if (params.limit !== undefined) queryParams.append("limit", params.limit.toString());
|
|
488
|
+
if (params.notice_uuid) queryParams.append("notice_uuid", params.notice_uuid);
|
|
489
|
+
if (params.event_type) queryParams.append("event_type", params.event_type);
|
|
490
|
+
if (params.status) queryParams.append("status", params.status);
|
|
491
|
+
if (params.created_after) queryParams.append("created_after", params.created_after);
|
|
492
|
+
if (params.created_before) queryParams.append("created_before", params.created_before);
|
|
493
|
+
if (params.language) queryParams.append("language", params.language);
|
|
494
|
+
const queryString = queryParams.toString();
|
|
495
|
+
const url = privacyCenterUrl(baseRequest, `/receipts${queryString ? `?${queryString}` : ""}`);
|
|
496
|
+
return privacyCenterApiFetch<unknown>(url, {
|
|
497
|
+
...options,
|
|
498
|
+
method: "GET",
|
|
499
|
+
cache: "no-store",
|
|
500
|
+
authToken: resolveAuthToken(authToken, baseRequest),
|
|
501
|
+
});
|
|
502
|
+
},
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Download a single receipt as a PDF (privacy center).
|
|
506
|
+
* Returns raw PDF bytes as a Blob — the caller builds an object URL to open/save it.
|
|
507
|
+
* Uses a direct fetch (not privacyCenterApiFetch) because the response is binary, not JSON,
|
|
508
|
+
* and the Authorization header must be sent (anchor navigation wouldn't carry it).
|
|
509
|
+
*/
|
|
510
|
+
async getReceiptPdf(
|
|
511
|
+
baseRequest: BaseRequest,
|
|
512
|
+
receiptUuid: string,
|
|
513
|
+
authToken?: string
|
|
514
|
+
): Promise<Blob> {
|
|
515
|
+
const url = privacyCenterUrl(baseRequest, `/receipts/${receiptUuid}/pdf`);
|
|
516
|
+
const token = resolveAuthToken(authToken, baseRequest);
|
|
517
|
+
const requestHeaders = new Headers();
|
|
518
|
+
requestHeaders.set("Accept", "application/pdf");
|
|
519
|
+
if (token) {
|
|
520
|
+
const trimmedToken = token.trim();
|
|
521
|
+
requestHeaders.set(
|
|
522
|
+
"Authorization",
|
|
523
|
+
trimmedToken.startsWith("Bearer ") ? trimmedToken : `Bearer ${trimmedToken}`
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
const response = await fetch(url, { method: "GET", headers: requestHeaders });
|
|
527
|
+
if (!response.ok) {
|
|
528
|
+
let errorMessage = `Request failed with status ${response.status}`;
|
|
529
|
+
try {
|
|
530
|
+
const errorData = (await response.json()) as { detail?: unknown; error_code?: string } | null;
|
|
531
|
+
if (errorData?.detail) {
|
|
532
|
+
errorMessage =
|
|
533
|
+
typeof errorData.detail === "string" ? errorData.detail : JSON.stringify(errorData.detail);
|
|
534
|
+
} else if (errorData?.error_code) {
|
|
535
|
+
errorMessage = errorData.error_code;
|
|
536
|
+
}
|
|
537
|
+
} catch {
|
|
538
|
+
// non-JSON error body — keep default message
|
|
539
|
+
}
|
|
540
|
+
// Prepend HTTP status so callers can branch on it (e.g. 404 = receipt not found)
|
|
541
|
+
throw new Error(`[${response.status}] ${errorMessage}`);
|
|
542
|
+
}
|
|
543
|
+
return response.blob();
|
|
544
|
+
},
|
|
545
|
+
|
|
467
546
|
/**
|
|
468
547
|
* Get case history (privacy center)
|
|
469
548
|
*/
|
|
@@ -3,15 +3,19 @@ import { IoAttach } from 'react-icons/io5';
|
|
|
3
3
|
import { PCButton, PCLoader } from '../../ui';
|
|
4
4
|
import type { DocumentRequestCardProps } from '../../lib/types';
|
|
5
5
|
import { UPLOAD_ACTIVE_STATUSES, DOCUMENT_REQUEST_STATUS_LABEL, STATUS_COLORS } from '../../lib/constants';
|
|
6
|
+
import { FinalizedTooltipWrapper } from './FinalizedTooltipWrapper';
|
|
6
7
|
|
|
7
8
|
export const DocumentRequestCard = ({
|
|
8
9
|
message,
|
|
9
10
|
onUpload,
|
|
10
11
|
isSubmitting,
|
|
12
|
+
isCaseFinalized,
|
|
11
13
|
}: DocumentRequestCardProps) => {
|
|
12
14
|
const { t } = useTranslation();
|
|
13
15
|
const meta = message.document_request_metadata;
|
|
14
|
-
if (!meta)
|
|
16
|
+
if (!meta) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
15
19
|
|
|
16
20
|
const canUpload = UPLOAD_ACTIVE_STATUSES.includes(meta.document_request_status);
|
|
17
21
|
const statusInfo = DOCUMENT_REQUEST_STATUS_LABEL[meta.document_request_status];
|
|
@@ -46,18 +50,20 @@ export const DocumentRequestCard = ({
|
|
|
46
50
|
</div>
|
|
47
51
|
|
|
48
52
|
{canUpload && (
|
|
49
|
-
<
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
53
|
+
<FinalizedTooltipWrapper enabled={isCaseFinalized} autoWidth>
|
|
54
|
+
<PCButton
|
|
55
|
+
variant="outline"
|
|
56
|
+
size="sm"
|
|
57
|
+
disabled={isSubmitting || isCaseFinalized}
|
|
58
|
+
onClick={isCaseFinalized ? undefined : () => onUpload(message.uuid)}
|
|
59
|
+
className="pc-doc-request-upload-btn"
|
|
60
|
+
>
|
|
61
|
+
{isSubmitting
|
|
62
|
+
? <PCLoader variant="spinner" size="sm" />
|
|
63
|
+
: <IoAttach size={14} />}
|
|
64
|
+
{t('uploadAndSubmit')}
|
|
65
|
+
</PCButton>
|
|
66
|
+
</FinalizedTooltipWrapper>
|
|
61
67
|
)}
|
|
62
68
|
|
|
63
69
|
{meta.document_request_status === 'rejected' && meta.rejection_reason && (
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { CSSProperties } from 'react';
|
|
2
|
+
import { PCTooltip } from '../../ui';
|
|
3
|
+
import { useCaseFinalizedTooltip } from '../../lib/constants';
|
|
4
|
+
import type { FinalizedTooltipWrapperProps } from '../../lib/types';
|
|
5
|
+
|
|
6
|
+
const AUTO_WIDTH_TRIGGER_STYLE: CSSProperties = { width: 'auto' };
|
|
7
|
+
|
|
8
|
+
export const FinalizedTooltipWrapper = ({
|
|
9
|
+
enabled,
|
|
10
|
+
autoWidth = false,
|
|
11
|
+
children,
|
|
12
|
+
}: FinalizedTooltipWrapperProps) => {
|
|
13
|
+
const tooltip = useCaseFinalizedTooltip();
|
|
14
|
+
if (!enabled) {
|
|
15
|
+
return children;
|
|
16
|
+
}
|
|
17
|
+
return (
|
|
18
|
+
<PCTooltip
|
|
19
|
+
content={tooltip}
|
|
20
|
+
triggerStyle={autoWidth ? AUTO_WIDTH_TRIGGER_STYLE : undefined}
|
|
21
|
+
>
|
|
22
|
+
{children}
|
|
23
|
+
</PCTooltip>
|
|
24
|
+
);
|
|
25
|
+
};
|
|
@@ -3,6 +3,7 @@ import { IoAttach, IoSend, IoDocumentText } from 'react-icons/io5';
|
|
|
3
3
|
import { PCButton, PCLoader } from '../../ui';
|
|
4
4
|
import type { MessageComposerProps } from '../../lib/types';
|
|
5
5
|
import { formatFileSize } from './utils';
|
|
6
|
+
import { FinalizedTooltipWrapper } from './FinalizedTooltipWrapper';
|
|
6
7
|
|
|
7
8
|
export const MessageComposer = ({
|
|
8
9
|
messageText,
|
|
@@ -13,6 +14,7 @@ export const MessageComposer = ({
|
|
|
13
14
|
onSend,
|
|
14
15
|
isSending,
|
|
15
16
|
isUploading,
|
|
17
|
+
isCaseFinalized,
|
|
16
18
|
}: MessageComposerProps) => {
|
|
17
19
|
const { t } = useTranslation();
|
|
18
20
|
|
|
@@ -46,38 +48,45 @@ export const MessageComposer = ({
|
|
|
46
48
|
)}
|
|
47
49
|
|
|
48
50
|
<div className="pc-composer-textarea-wrap">
|
|
49
|
-
<
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
<FinalizedTooltipWrapper enabled={isCaseFinalized}>
|
|
52
|
+
<textarea
|
|
53
|
+
value={messageText}
|
|
54
|
+
onChange={(e) => onMessageTextChange(e.target.value)}
|
|
55
|
+
onKeyDown={handleKeyDown}
|
|
56
|
+
placeholder={t('writeMessagePlaceholder')}
|
|
57
|
+
rows={3}
|
|
58
|
+
className="pc-composer-textarea"
|
|
59
|
+
disabled={isCaseFinalized}
|
|
60
|
+
/>
|
|
61
|
+
</FinalizedTooltipWrapper>
|
|
57
62
|
</div>
|
|
58
63
|
<div className="pc-composer-actions">
|
|
59
|
-
<
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
64
|
+
<FinalizedTooltipWrapper enabled={isCaseFinalized} autoWidth>
|
|
65
|
+
<PCButton
|
|
66
|
+
variant="outline"
|
|
67
|
+
size="sm"
|
|
68
|
+
disabled={isUploading || isSending || isCaseFinalized}
|
|
69
|
+
onClick={onAttachClick}
|
|
70
|
+
className="pc-composer-btn"
|
|
71
|
+
>
|
|
72
|
+
{isUploading
|
|
73
|
+
? <PCLoader variant="spinner" size="sm" />
|
|
74
|
+
: <IoAttach size={16} />}
|
|
75
|
+
{t('attachFile')}
|
|
76
|
+
</PCButton>
|
|
77
|
+
</FinalizedTooltipWrapper>
|
|
78
|
+
<FinalizedTooltipWrapper enabled={isCaseFinalized} autoWidth>
|
|
79
|
+
<PCButton
|
|
80
|
+
size="sm"
|
|
81
|
+
disabled={(!messageText.trim() && !pendingUpload) || isSending || isUploading || isCaseFinalized}
|
|
82
|
+
onClick={onSend}
|
|
83
|
+
className="pc-composer-btn"
|
|
84
|
+
>
|
|
85
|
+
{isSending ? <PCLoader variant="spinner" size="sm" /> : null}
|
|
86
|
+
{t('send')}
|
|
87
|
+
<IoSend size={14} />
|
|
88
|
+
</PCButton>
|
|
89
|
+
</FinalizedTooltipWrapper>
|
|
81
90
|
</div>
|
|
82
91
|
<div className="pc-composer-footer">
|
|
83
92
|
{t('messagesSecureFooter')}
|
|
@@ -13,6 +13,7 @@ export const MessageItem = ({
|
|
|
13
13
|
message,
|
|
14
14
|
onUploadForRequest,
|
|
15
15
|
isSubmittingForRequest,
|
|
16
|
+
isCaseFinalized,
|
|
16
17
|
}: MessageItemProps) => {
|
|
17
18
|
const { t } = useTranslation();
|
|
18
19
|
const isPrincipal = message.sender_role === 'data_principal';
|
|
@@ -37,6 +38,7 @@ export const MessageItem = ({
|
|
|
37
38
|
message={message}
|
|
38
39
|
onUpload={onUploadForRequest}
|
|
39
40
|
isSubmitting={isSubmittingForRequest}
|
|
41
|
+
isCaseFinalized={isCaseFinalized}
|
|
40
42
|
/>
|
|
41
43
|
</>
|
|
42
44
|
);
|
|
@@ -10,15 +10,18 @@ import {
|
|
|
10
10
|
useUploadCaseDocument,
|
|
11
11
|
useSubmitDocumentForRequest,
|
|
12
12
|
} from '../../lib/queries';
|
|
13
|
-
import { STATUS_COLORS } from '../../lib/constants';
|
|
13
|
+
import { STATUS_COLORS, useCaseFinalizedTooltip } from '../../lib/constants';
|
|
14
14
|
import type { MessagesTabProps } from '../../lib/types';
|
|
15
15
|
import { MessageItem } from './MessageItem';
|
|
16
16
|
import { MessageComposer } from './MessageComposer';
|
|
17
17
|
import { useMessagePolling } from './useMessagePolling';
|
|
18
18
|
import { useFileUpload } from './useFileUpload';
|
|
19
|
+
import { isTerminalCaseStatus } from './utils';
|
|
19
20
|
|
|
20
|
-
export const MessagesTab = ({ caseUuid }: MessagesTabProps) => {
|
|
21
|
+
export const MessagesTab = ({ caseUuid, caseStatus }: MessagesTabProps) => {
|
|
22
|
+
const isCaseFinalized = isTerminalCaseStatus(caseStatus);
|
|
21
23
|
const { t } = useTranslation();
|
|
24
|
+
const caseFinalizedTooltip = useCaseFinalizedTooltip();
|
|
22
25
|
const { organization } = useOrganization();
|
|
23
26
|
const { accessToken, refreshToken, onError, updateTokens } = useAuth();
|
|
24
27
|
const authContext = useMemo(
|
|
@@ -83,7 +86,9 @@ export const MessagesTab = ({ caseUuid }: MessagesTabProps) => {
|
|
|
83
86
|
}}
|
|
84
87
|
>
|
|
85
88
|
<span>🔒</span>
|
|
86
|
-
<span className="pc-messages-notice-text">
|
|
89
|
+
<span className="pc-messages-notice-text">
|
|
90
|
+
{isCaseFinalized ? caseFinalizedTooltip : t('secureMessagesNotice')}
|
|
91
|
+
</span>
|
|
87
92
|
<button
|
|
88
93
|
type="button"
|
|
89
94
|
onClick={() => refetch()}
|
|
@@ -128,6 +133,7 @@ export const MessagesTab = ({ caseUuid }: MessagesTabProps) => {
|
|
|
128
133
|
message={msg}
|
|
129
134
|
onUploadForRequest={triggerUploadForRequest}
|
|
130
135
|
isSubmittingForRequest={submittingForRequest === msg.uuid}
|
|
136
|
+
isCaseFinalized={isCaseFinalized}
|
|
131
137
|
/>
|
|
132
138
|
))}
|
|
133
139
|
</div>
|
|
@@ -141,6 +147,7 @@ export const MessagesTab = ({ caseUuid }: MessagesTabProps) => {
|
|
|
141
147
|
onSend={handleSend}
|
|
142
148
|
isSending={isSending}
|
|
143
149
|
isUploading={isUploading}
|
|
150
|
+
isCaseFinalized={isCaseFinalized}
|
|
144
151
|
/>
|
|
145
152
|
</div>
|
|
146
153
|
);
|
|
@@ -1,14 +1,29 @@
|
|
|
1
|
-
import { MAX_FILE_SIZE, ALLOWED_MIME_TYPES } from '../../lib/constants';
|
|
1
|
+
import { MAX_FILE_SIZE, ALLOWED_MIME_TYPES, TERMINAL_CASE_STATUSES } from '../../lib/constants';
|
|
2
2
|
import type { CaseStatusVariant } from '../../lib/types';
|
|
3
3
|
|
|
4
|
+
export const isTerminalCaseStatus = (status: string | undefined | null): boolean => {
|
|
5
|
+
if (!status) {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
return (TERMINAL_CASE_STATUSES as readonly string[]).includes(status.toLowerCase());
|
|
9
|
+
};
|
|
10
|
+
|
|
4
11
|
export const getVar = (v: string) => `var(${v})`;
|
|
5
12
|
|
|
6
13
|
export const getCaseStatusVariant = (status: string): CaseStatusVariant => {
|
|
7
|
-
const
|
|
8
|
-
if (
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
if (
|
|
14
|
+
const normalized = status.toLowerCase().replace(/_/g, '');
|
|
15
|
+
if (normalized === 'completed') {
|
|
16
|
+
return 'success';
|
|
17
|
+
}
|
|
18
|
+
if (normalized === 'rejected' || normalized === 'declined') {
|
|
19
|
+
return 'error';
|
|
20
|
+
}
|
|
21
|
+
if (normalized === 'processing' || normalized === 'inprogress' || normalized === 'expired') {
|
|
22
|
+
return 'warning';
|
|
23
|
+
}
|
|
24
|
+
if (normalized === 'initiated') {
|
|
25
|
+
return 'info';
|
|
26
|
+
}
|
|
12
27
|
return 'secondary';
|
|
13
28
|
};
|
|
14
29
|
|