@redacto.io/consent-sdk-react 3.6.0 → 5.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/CHANGELOG.md +12 -0
- package/dist/index.d.mts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +36 -7
- package/dist/index.mjs +36 -7
- package/dist/privacy-center.d.mts +134 -51
- package/dist/privacy-center.d.ts +134 -51
- package/dist/privacy-center.js +6391 -2741
- package/dist/privacy-center.mjs +6135 -2482
- package/package.json +1 -1
- package/src/RedactoNoticeConsent/RedactoNoticeConsent.tsx +5 -5
- package/src/RedactoNoticeConsent/language-codes.ts +37 -1
- package/src/RedactoNoticeConsentInline/RedactoNoticeConsentInline.tsx +0 -3
- package/src/RedactoNoticeConsentInline/api/index.ts +0 -1
- package/src/RedactoPrivacyCenter/PrivacyCenterCaseDetailsPage.tsx +154 -0
- package/src/RedactoPrivacyCenter/PrivacyCenterHomePage.tsx +3 -0
- package/src/RedactoPrivacyCenter/PrivacyCenterLayout.tsx +81 -50
- package/src/RedactoPrivacyCenter/RedactoPrivacyCenter.tsx +4 -0
- package/src/RedactoPrivacyCenter/api/actions.ts +68 -36
- package/src/RedactoPrivacyCenter/api/client.ts +125 -14
- package/src/RedactoPrivacyCenter/components/CaseDetails/AttachmentCard.tsx +38 -0
- package/src/RedactoPrivacyCenter/components/CaseDetails/ChatBubble.tsx +31 -0
- package/src/RedactoPrivacyCenter/components/CaseDetails/DocumentRequestCard.tsx +73 -0
- package/src/RedactoPrivacyCenter/components/CaseDetails/MessageComposer.tsx +87 -0
- package/src/RedactoPrivacyCenter/components/CaseDetails/MessageItem.tsx +95 -0
- package/src/RedactoPrivacyCenter/components/CaseDetails/MessagesTab.tsx +147 -0
- package/src/RedactoPrivacyCenter/components/CaseDetails/RequestDetailsTab.tsx +171 -0
- package/src/RedactoPrivacyCenter/components/CaseDetails/StatusEvent.tsx +23 -0
- package/src/RedactoPrivacyCenter/components/CaseDetails/useFileUpload.ts +116 -0
- package/src/RedactoPrivacyCenter/components/CaseDetails/useMessagePolling.ts +36 -0
- package/src/RedactoPrivacyCenter/components/CaseDetails/utils.ts +56 -0
- package/src/RedactoPrivacyCenter/components/CaseDetailsModal.tsx +1 -0
- package/src/RedactoPrivacyCenter/components/CaseHistory.tsx +6 -13
- package/src/RedactoPrivacyCenter/components/ConsentManager.tsx +6 -0
- package/src/RedactoPrivacyCenter/components/ConsentManagerColumns.tsx +0 -1
- package/src/RedactoPrivacyCenter/components/DataRequest/CorrectionDataSection.tsx +2 -2
- package/src/RedactoPrivacyCenter/components/DataRequest/PurposeSelectionSection.tsx +2 -3
- package/src/RedactoPrivacyCenter/components/Form.tsx +152 -11
- package/src/RedactoPrivacyCenter/components/GrievanceTable.tsx +4 -6
- package/src/RedactoPrivacyCenter/components/GrievanceTablePagination.tsx +9 -2
- package/src/RedactoPrivacyCenter/components/ModifyConsentModal.tsx +1 -0
- package/src/RedactoPrivacyCenter/components/Pagination.tsx +12 -12
- package/src/RedactoPrivacyCenter/components/PrivacyCenterLayout.tsx +3 -1
- package/src/RedactoPrivacyCenter/components/RequestSubmit.tsx +15 -1
- package/src/RedactoPrivacyCenter/components/SideMenuBarPC/SideBarPC.tsx +8 -8
- package/src/RedactoPrivacyCenter/components/SideMenuBarPC/SideMenuPC.tsx +7 -11
- package/src/RedactoPrivacyCenter/components/SideMenuBarPC/types.ts +1 -1
- package/src/RedactoPrivacyCenter/components/activitiesColumns.tsx +1 -1
- package/src/RedactoPrivacyCenter/components/caseHistoryColumns.tsx +4 -24
- package/src/RedactoPrivacyCenter/components/constant.ts +3 -7
- package/src/RedactoPrivacyCenter/components/types.ts +12 -4
- package/src/RedactoPrivacyCenter/context/AuthContext.tsx +30 -16
- package/src/RedactoPrivacyCenter/context/BaseUrlContext.tsx +3 -6
- package/src/RedactoPrivacyCenter/context/NavigationContext.tsx +35 -3
- package/src/RedactoPrivacyCenter/context/OrganizationContext.tsx +1 -1
- package/src/RedactoPrivacyCenter/context/types.ts +14 -0
- package/src/RedactoPrivacyCenter/lib/api.ts +101 -3
- package/src/RedactoPrivacyCenter/lib/constants.ts +46 -1
- package/src/RedactoPrivacyCenter/lib/i18n.ts +1 -1
- package/src/RedactoPrivacyCenter/lib/queries.ts +106 -2
- package/src/RedactoPrivacyCenter/lib/types.ts +186 -1
- package/src/RedactoPrivacyCenter/lib/utils.ts +1 -0
- package/src/RedactoPrivacyCenter/locales/as/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/bho/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/bn/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/brx/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/doi/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/en/translation.json +69 -2
- package/src/RedactoPrivacyCenter/locales/gom/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/gu/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/hi/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/kn/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/ks/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/mai/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/ml/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/mni-Mtei/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/mr/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/ne/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/or/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/pa/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/sa/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/sat/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/sd/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/ta/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/te/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/ur/translation.json +93 -1
- package/src/RedactoPrivacyCenter/styles/injectStyles.ts +725 -0
- package/src/RedactoPrivacyCenter/styles/pcStyles.ts +0 -6
- package/src/RedactoPrivacyCenter/table/AdvancedTable.tsx +5 -3
- package/src/RedactoPrivacyCenter/table/AdvancedTablePagination.tsx +9 -12
- package/src/RedactoPrivacyCenter/ui/Footer.tsx +4 -2
- package/src/RedactoPrivacyCenter/ui/PCAccordion.tsx +24 -18
- package/src/RedactoPrivacyCenter/ui/PCCollapse.tsx +30 -20
- package/src/RedactoPrivacyCenter/ui/PCInput.tsx +17 -13
- package/src/RedactoPrivacyCenter/ui/PCLoader.tsx +3 -2
- package/src/RedactoPrivacyCenter/ui/PCModal.tsx +3 -1
- package/src/RedactoPrivacyCenter/ui/PCPopover.tsx +0 -2
- package/src/RedactoPrivacyCenter/ui/PCSelect.tsx +10 -4
- package/src/RedactoPrivacyCenter/ui/PCTable.tsx +44 -32
- package/src/RedactoPrivacyCenter/ui/PCText.tsx +5 -3
- package/src/RedactoPrivacyCenter/ui/StatusBadge.tsx +0 -2
- package/src/RedactoPrivacyCenter/ui/ThemeToggle.tsx +4 -2
- package/src/privacy-center.ts +1 -0
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { apiFetch } from "./fetcher";
|
|
2
2
|
import type { ApiFetchOptions, ApiResponse } from "./fetcher";
|
|
3
|
+
import type {
|
|
4
|
+
CaseMessage,
|
|
5
|
+
MessageDocument,
|
|
6
|
+
UploadDocumentResponse,
|
|
7
|
+
} from "../lib/types";
|
|
3
8
|
|
|
4
9
|
/**
|
|
5
10
|
* Base request parameters for consent service calls
|
|
@@ -81,6 +86,7 @@ export function createConsentClient(baseUrl: string) {
|
|
|
81
86
|
const errorData = (await response.json()) as {
|
|
82
87
|
detail?: unknown;
|
|
83
88
|
message?: string;
|
|
89
|
+
code?: string;
|
|
84
90
|
} | null;
|
|
85
91
|
if (errorData) {
|
|
86
92
|
if ("detail" in errorData && errorData.detail) {
|
|
@@ -90,6 +96,9 @@ export function createConsentClient(baseUrl: string) {
|
|
|
90
96
|
: JSON.stringify(errorData.detail, null, 2);
|
|
91
97
|
} else if ("message" in errorData && errorData.message) {
|
|
92
98
|
errorMessage = errorData.message;
|
|
99
|
+
} else if ("code" in errorData && errorData.code) {
|
|
100
|
+
// e.g. { code: "empty_file" } or { code: "document_already_exists" }
|
|
101
|
+
errorMessage = errorData.code;
|
|
93
102
|
}
|
|
94
103
|
}
|
|
95
104
|
} catch {
|
|
@@ -104,7 +113,8 @@ export function createConsentClient(baseUrl: string) {
|
|
|
104
113
|
}
|
|
105
114
|
}
|
|
106
115
|
|
|
107
|
-
|
|
116
|
+
// Prepend HTTP status so callers can branch on it without losing context
|
|
117
|
+
throw new Error(`[${response.status}] ${errorMessage}`);
|
|
108
118
|
}
|
|
109
119
|
|
|
110
120
|
// Parse JSON response
|
|
@@ -139,6 +149,18 @@ export function createConsentClient(baseUrl: string) {
|
|
|
139
149
|
}
|
|
140
150
|
}
|
|
141
151
|
|
|
152
|
+
function privacyCenterUrl(baseRequest: BaseRequest, path: string): string {
|
|
153
|
+
return `${baseUrl}/organisations/${baseRequest.organisationUuid}/workspaces/${baseRequest.workspaceUuid}/dsar/privacy-center${path}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function caseUrl(baseRequest: BaseRequest, caseUuid: string, path = ''): string {
|
|
157
|
+
return privacyCenterUrl(baseRequest, `/cases/${caseUuid}${path}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function resolveAuthToken(authToken: string | undefined, baseRequest: BaseRequest): string {
|
|
161
|
+
return authToken || baseRequest.authToken;
|
|
162
|
+
}
|
|
163
|
+
|
|
142
164
|
return {
|
|
143
165
|
/**
|
|
144
166
|
* Get form data (privacy center)
|
|
@@ -153,7 +175,7 @@ export function createConsentClient(baseUrl: string) {
|
|
|
153
175
|
const queryParams = new URLSearchParams();
|
|
154
176
|
if (language) queryParams.append("language", language);
|
|
155
177
|
const queryString = queryParams.toString();
|
|
156
|
-
const url =
|
|
178
|
+
const url = privacyCenterUrl(baseRequest, `/form/data${queryString ? `?${queryString}` : ""}`);
|
|
157
179
|
const { useSessionAuth = false, getAuthToken, ...fetchOptions } = options || {};
|
|
158
180
|
|
|
159
181
|
// Case management uses session-based auth (X-Auth-Token)
|
|
@@ -172,7 +194,7 @@ export function createConsentClient(baseUrl: string) {
|
|
|
172
194
|
...fetchOptions,
|
|
173
195
|
method: "POST",
|
|
174
196
|
body: JSON.stringify({ contact }),
|
|
175
|
-
authToken: authToken
|
|
197
|
+
authToken: resolveAuthToken(authToken, baseRequest),
|
|
176
198
|
});
|
|
177
199
|
},
|
|
178
200
|
|
|
@@ -185,7 +207,7 @@ export function createConsentClient(baseUrl: string) {
|
|
|
185
207
|
authToken?: string,
|
|
186
208
|
options?: ApiFetchOptions & { useSessionAuth?: boolean }
|
|
187
209
|
): Promise<unknown> {
|
|
188
|
-
const url =
|
|
210
|
+
const url = privacyCenterUrl(baseRequest, '/case');
|
|
189
211
|
const { useSessionAuth = false, getAuthToken, ...fetchOptions } = options || {};
|
|
190
212
|
|
|
191
213
|
// Case management uses session-based auth (X-Auth-Token)
|
|
@@ -204,7 +226,7 @@ export function createConsentClient(baseUrl: string) {
|
|
|
204
226
|
...fetchOptions,
|
|
205
227
|
method: "POST",
|
|
206
228
|
body: JSON.stringify(payload),
|
|
207
|
-
authToken: authToken
|
|
229
|
+
authToken: resolveAuthToken(authToken, baseRequest),
|
|
208
230
|
});
|
|
209
231
|
},
|
|
210
232
|
|
|
@@ -325,12 +347,12 @@ export function createConsentClient(baseUrl: string) {
|
|
|
325
347
|
if (product_uuid) queryParams.append("product_uuid", product_uuid);
|
|
326
348
|
if (language) queryParams.append("language", language);
|
|
327
349
|
const queryString = queryParams.toString();
|
|
328
|
-
const url =
|
|
350
|
+
const url = privacyCenterUrl(baseRequest, `/user-consents${queryString ? `?${queryString}` : ""}`);
|
|
329
351
|
return privacyCenterApiFetch(url, {
|
|
330
352
|
...options,
|
|
331
353
|
method: "POST",
|
|
332
354
|
body: JSON.stringify({ contact }),
|
|
333
|
-
authToken: authToken
|
|
355
|
+
authToken: resolveAuthToken(authToken, baseRequest),
|
|
334
356
|
});
|
|
335
357
|
},
|
|
336
358
|
|
|
@@ -347,7 +369,7 @@ export function createConsentClient(baseUrl: string) {
|
|
|
347
369
|
nominatorContact?: string,
|
|
348
370
|
dataElementUuids?: string[]
|
|
349
371
|
): Promise<unknown> {
|
|
350
|
-
const url =
|
|
372
|
+
const url = privacyCenterUrl(baseRequest, `/manage-consent?status=${status}`);
|
|
351
373
|
const body: Record<string, unknown> = { purpose_uuid: purposeId, contact };
|
|
352
374
|
if (nominatorContact) {
|
|
353
375
|
body.nominator_contact = nominatorContact;
|
|
@@ -359,7 +381,7 @@ export function createConsentClient(baseUrl: string) {
|
|
|
359
381
|
...options,
|
|
360
382
|
method: "POST",
|
|
361
383
|
body: JSON.stringify(body),
|
|
362
|
-
authToken: authToken
|
|
384
|
+
authToken: resolveAuthToken(authToken, baseRequest),
|
|
363
385
|
});
|
|
364
386
|
},
|
|
365
387
|
|
|
@@ -379,10 +401,10 @@ export function createConsentClient(baseUrl: string) {
|
|
|
379
401
|
if (limit !== undefined) queryParams.append("limit", limit.toString());
|
|
380
402
|
if (language) queryParams.append("language", language);
|
|
381
403
|
const queryString = queryParams.toString();
|
|
382
|
-
const url =
|
|
404
|
+
const url = privacyCenterUrl(baseRequest, `/activities${queryString ? `?${queryString}` : ""}`);
|
|
383
405
|
return privacyCenterApiFetch<unknown>(url, {
|
|
384
406
|
...options,
|
|
385
|
-
authToken: authToken
|
|
407
|
+
authToken: resolveAuthToken(authToken, baseRequest),
|
|
386
408
|
});
|
|
387
409
|
},
|
|
388
410
|
|
|
@@ -391,7 +413,7 @@ export function createConsentClient(baseUrl: string) {
|
|
|
391
413
|
*/
|
|
392
414
|
async getCaseHistory(
|
|
393
415
|
baseRequest: BaseRequest,
|
|
394
|
-
|
|
416
|
+
_contact: string,
|
|
395
417
|
offset?: number,
|
|
396
418
|
limit?: number,
|
|
397
419
|
language?: string,
|
|
@@ -403,10 +425,99 @@ export function createConsentClient(baseUrl: string) {
|
|
|
403
425
|
if (limit !== undefined) queryParams.append("limit", limit.toString());
|
|
404
426
|
if (language) queryParams.append("language", language);
|
|
405
427
|
const queryString = queryParams.toString();
|
|
406
|
-
const url =
|
|
428
|
+
const url = privacyCenterUrl(baseRequest, `/cases?${queryString}`);
|
|
407
429
|
return privacyCenterApiFetch<unknown>(url, {
|
|
408
430
|
...options,
|
|
409
|
-
authToken: authToken
|
|
431
|
+
authToken: resolveAuthToken(authToken, baseRequest),
|
|
432
|
+
});
|
|
433
|
+
},
|
|
434
|
+
|
|
435
|
+
// ─── Case Communication ─────────────────────────────────────────────────
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Get messages for a case (privacy center)
|
|
439
|
+
*/
|
|
440
|
+
async getCaseMessages(
|
|
441
|
+
baseRequest: BaseRequest,
|
|
442
|
+
caseUuid: string,
|
|
443
|
+
authToken?: string
|
|
444
|
+
): Promise<CaseMessage[]> {
|
|
445
|
+
const url = caseUrl(baseRequest, caseUuid, '/messages');
|
|
446
|
+
return privacyCenterApiFetch<CaseMessage[]>(url, {
|
|
447
|
+
authToken: resolveAuthToken(authToken, baseRequest),
|
|
448
|
+
});
|
|
449
|
+
},
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Send a message on a case (privacy center)
|
|
453
|
+
*/
|
|
454
|
+
async sendCaseMessage(
|
|
455
|
+
baseRequest: BaseRequest,
|
|
456
|
+
caseUuid: string,
|
|
457
|
+
body: string,
|
|
458
|
+
documentUuids: string[],
|
|
459
|
+
authToken?: string
|
|
460
|
+
): Promise<CaseMessage> {
|
|
461
|
+
const url = caseUrl(baseRequest, caseUuid, '/messages');
|
|
462
|
+
return privacyCenterApiFetch<CaseMessage>(url, {
|
|
463
|
+
method: 'POST',
|
|
464
|
+
body: JSON.stringify({ body, document_uuids: documentUuids }),
|
|
465
|
+
authToken: resolveAuthToken(authToken, baseRequest),
|
|
466
|
+
});
|
|
467
|
+
},
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Upload a document on a case (multipart/form-data, privacy center)
|
|
471
|
+
*/
|
|
472
|
+
async uploadCaseDocument(
|
|
473
|
+
baseRequest: BaseRequest,
|
|
474
|
+
caseUuid: string,
|
|
475
|
+
file: File,
|
|
476
|
+
authToken?: string
|
|
477
|
+
): Promise<UploadDocumentResponse> {
|
|
478
|
+
const url = caseUrl(baseRequest, caseUuid, '/upload-document');
|
|
479
|
+
const formData = new FormData();
|
|
480
|
+
formData.append('file', file, file.name); // field name must be exactly "file"
|
|
481
|
+
// "payload" is required by the privacy-center endpoint (omitting it causes 422)
|
|
482
|
+
formData.append('payload', JSON.stringify({ metadata: null }));
|
|
483
|
+
return privacyCenterApiFetch<UploadDocumentResponse>(url, {
|
|
484
|
+
method: 'POST',
|
|
485
|
+
body: formData,
|
|
486
|
+
authToken: resolveAuthToken(authToken, baseRequest),
|
|
487
|
+
});
|
|
488
|
+
},
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Submit a document against a handler document request (privacy center)
|
|
492
|
+
*/
|
|
493
|
+
async submitDocumentForRequest(
|
|
494
|
+
baseRequest: BaseRequest,
|
|
495
|
+
caseUuid: string,
|
|
496
|
+
requestEventUuid: string,
|
|
497
|
+
documentUuid: string,
|
|
498
|
+
body: string,
|
|
499
|
+
authToken?: string
|
|
500
|
+
): Promise<CaseMessage> {
|
|
501
|
+
const url = caseUrl(baseRequest, caseUuid, `/document-requests/${requestEventUuid}/submit`);
|
|
502
|
+
return privacyCenterApiFetch<CaseMessage>(url, {
|
|
503
|
+
method: 'POST',
|
|
504
|
+
body: JSON.stringify({ document_uuid: documentUuid, body }),
|
|
505
|
+
authToken: resolveAuthToken(authToken, baseRequest),
|
|
506
|
+
});
|
|
507
|
+
},
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Get document info/download URL for a case document (privacy center)
|
|
511
|
+
*/
|
|
512
|
+
async getCaseDocument(
|
|
513
|
+
baseRequest: BaseRequest,
|
|
514
|
+
caseUuid: string,
|
|
515
|
+
documentUuid: string,
|
|
516
|
+
authToken?: string
|
|
517
|
+
): Promise<MessageDocument> {
|
|
518
|
+
const url = caseUrl(baseRequest, caseUuid, `/documents/${documentUuid}`);
|
|
519
|
+
return privacyCenterApiFetch<MessageDocument>(url, {
|
|
520
|
+
authToken: resolveAuthToken(authToken, baseRequest),
|
|
410
521
|
});
|
|
411
522
|
},
|
|
412
523
|
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { useTranslation } from 'react-i18next';
|
|
2
|
+
import { IoDocumentText } from 'react-icons/io5';
|
|
3
|
+
import type { AttachmentCardProps } from '../../lib/types';
|
|
4
|
+
import { formatFileSize, getDownloadUrl } from './utils';
|
|
5
|
+
|
|
6
|
+
export const AttachmentCard = ({ doc }: AttachmentCardProps) => {
|
|
7
|
+
const { t } = useTranslation();
|
|
8
|
+
const downloadUrl = getDownloadUrl(doc.file_url);
|
|
9
|
+
const fileName = doc.file_name || t('document');
|
|
10
|
+
|
|
11
|
+
const content = (
|
|
12
|
+
<>
|
|
13
|
+
<IoDocumentText size={20} className="pc-attachment-icon" />
|
|
14
|
+
<div className="pc-attachment-info">
|
|
15
|
+
<div className="pc-attachment-name">{fileName}</div>
|
|
16
|
+
<div className="pc-attachment-size">
|
|
17
|
+
{doc.file_size ? formatFileSize(doc.file_size) : ''}
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
</>
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
if (downloadUrl) {
|
|
24
|
+
return (
|
|
25
|
+
<a
|
|
26
|
+
href={downloadUrl}
|
|
27
|
+
target="_blank"
|
|
28
|
+
rel="noopener noreferrer"
|
|
29
|
+
aria-label={t('downloadAttachment', { fileName })}
|
|
30
|
+
className="pc-attachment-card"
|
|
31
|
+
>
|
|
32
|
+
{content}
|
|
33
|
+
</a>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return <div className="pc-attachment-card">{content}</div>;
|
|
38
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ChatBubbleProps } from '../../lib/types';
|
|
2
|
+
import { AttachmentCard } from './AttachmentCard';
|
|
3
|
+
|
|
4
|
+
export const ChatBubble = ({
|
|
5
|
+
message,
|
|
6
|
+
isPrincipal,
|
|
7
|
+
senderLabel,
|
|
8
|
+
timestamp,
|
|
9
|
+
}: ChatBubbleProps) => {
|
|
10
|
+
const direction = isPrincipal ? 'sent' : 'received';
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<div className={`pc-chat-bubble-wrap pc-chat-bubble-wrap--${direction}`}>
|
|
14
|
+
<span className="pc-chat-sender">
|
|
15
|
+
{senderLabel} · {timestamp}
|
|
16
|
+
</span>
|
|
17
|
+
|
|
18
|
+
{/* Text bubble */}
|
|
19
|
+
{message.body && (
|
|
20
|
+
<div className={`pc-chat-bubble pc-chat-bubble--${direction}`}>
|
|
21
|
+
{message.body}
|
|
22
|
+
</div>
|
|
23
|
+
)}
|
|
24
|
+
|
|
25
|
+
{/* Attachment cards */}
|
|
26
|
+
{message.documents.map((doc) => (
|
|
27
|
+
<AttachmentCard key={doc.uuid} doc={doc} />
|
|
28
|
+
))}
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { useTranslation } from 'react-i18next';
|
|
2
|
+
import { IoAttach } from 'react-icons/io5';
|
|
3
|
+
import { PCButton, PCLoader } from '../../ui';
|
|
4
|
+
import type { DocumentRequestCardProps } from '../../lib/types';
|
|
5
|
+
import { UPLOAD_ACTIVE_STATUSES, DOCUMENT_REQUEST_STATUS_LABEL, STATUS_COLORS } from '../../lib/constants';
|
|
6
|
+
|
|
7
|
+
export const DocumentRequestCard = ({
|
|
8
|
+
message,
|
|
9
|
+
onUpload,
|
|
10
|
+
isSubmitting,
|
|
11
|
+
}: DocumentRequestCardProps) => {
|
|
12
|
+
const { t } = useTranslation();
|
|
13
|
+
const meta = message.document_request_metadata;
|
|
14
|
+
if (!meta) return null;
|
|
15
|
+
|
|
16
|
+
const canUpload = UPLOAD_ACTIVE_STATUSES.includes(meta.document_request_status);
|
|
17
|
+
const statusInfo = DOCUMENT_REQUEST_STATUS_LABEL[meta.document_request_status];
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div
|
|
21
|
+
className="pc-doc-request-card"
|
|
22
|
+
style={{
|
|
23
|
+
border: `1.5px dashed ${STATUS_COLORS.info.border}`,
|
|
24
|
+
backgroundColor: STATUS_COLORS.info.bg,
|
|
25
|
+
}}
|
|
26
|
+
>
|
|
27
|
+
<div className="pc-doc-request-header">
|
|
28
|
+
<IoAttach size={16} className="pc-doc-request-icon" style={{ color: STATUS_COLORS.info.color }} />
|
|
29
|
+
<div>
|
|
30
|
+
<div className="pc-doc-request-title" style={{ color: STATUS_COLORS.infoDark }}>
|
|
31
|
+
{t('documentRequestedLabel', { title: meta.title })}
|
|
32
|
+
</div>
|
|
33
|
+
{meta.accepted_document && (
|
|
34
|
+
<div className="pc-doc-request-accepted" style={{ color: STATUS_COLORS.info.color }}>
|
|
35
|
+
{t('acceptedLabel', { accepted: meta.accepted_document })}
|
|
36
|
+
</div>
|
|
37
|
+
)}
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<span
|
|
41
|
+
className="pc-doc-request-status"
|
|
42
|
+
style={{ color: statusInfo.color, border: `1px solid ${statusInfo.color}` }}
|
|
43
|
+
>
|
|
44
|
+
{t(statusInfo.labelKey)}
|
|
45
|
+
</span>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
{canUpload && (
|
|
49
|
+
<PCButton
|
|
50
|
+
variant="outline"
|
|
51
|
+
size="sm"
|
|
52
|
+
disabled={isSubmitting}
|
|
53
|
+
onClick={() => onUpload(message.uuid)}
|
|
54
|
+
className="pc-doc-request-upload-btn"
|
|
55
|
+
>
|
|
56
|
+
{isSubmitting
|
|
57
|
+
? <PCLoader variant="spinner" size="sm" />
|
|
58
|
+
: <IoAttach size={14} />}
|
|
59
|
+
{t('uploadAndSubmit')}
|
|
60
|
+
</PCButton>
|
|
61
|
+
)}
|
|
62
|
+
|
|
63
|
+
{meta.document_request_status === 'rejected' && meta.rejection_reason && (
|
|
64
|
+
<div
|
|
65
|
+
className="pc-doc-request-rejection"
|
|
66
|
+
style={{ color: STATUS_COLORS.error.color, backgroundColor: STATUS_COLORS.error.bg }}
|
|
67
|
+
>
|
|
68
|
+
{t('rejectionReasonLabel', { reason: meta.rejection_reason })}
|
|
69
|
+
</div>
|
|
70
|
+
)}
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { useTranslation } from 'react-i18next';
|
|
2
|
+
import { IoAttach, IoSend, IoDocumentText } from 'react-icons/io5';
|
|
3
|
+
import { PCButton, PCLoader } from '../../ui';
|
|
4
|
+
import type { MessageComposerProps } from '../../lib/types';
|
|
5
|
+
import { formatFileSize } from './utils';
|
|
6
|
+
|
|
7
|
+
export const MessageComposer = ({
|
|
8
|
+
messageText,
|
|
9
|
+
onMessageTextChange,
|
|
10
|
+
pendingUpload,
|
|
11
|
+
onRemovePendingUpload,
|
|
12
|
+
onAttachClick,
|
|
13
|
+
onSend,
|
|
14
|
+
isSending,
|
|
15
|
+
isUploading,
|
|
16
|
+
}: MessageComposerProps) => {
|
|
17
|
+
const { t } = useTranslation();
|
|
18
|
+
|
|
19
|
+
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
20
|
+
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
|
|
21
|
+
e.preventDefault();
|
|
22
|
+
onSend();
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className="pc-composer">
|
|
28
|
+
{pendingUpload && (
|
|
29
|
+
<div className="pc-composer-pending-row">
|
|
30
|
+
<div className="pc-composer-pending-pill">
|
|
31
|
+
<IoDocumentText size={14} />
|
|
32
|
+
<span>{pendingUpload.file_name}</span>
|
|
33
|
+
<span className="pc-composer-pending-size">
|
|
34
|
+
{formatFileSize(pendingUpload.file_size)}
|
|
35
|
+
</span>
|
|
36
|
+
<button
|
|
37
|
+
type="button"
|
|
38
|
+
aria-label={t('removeAttachment')}
|
|
39
|
+
onClick={onRemovePendingUpload}
|
|
40
|
+
className="pc-composer-remove-btn"
|
|
41
|
+
>
|
|
42
|
+
×
|
|
43
|
+
</button>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
)}
|
|
47
|
+
|
|
48
|
+
<div className="pc-composer-textarea-wrap">
|
|
49
|
+
<textarea
|
|
50
|
+
value={messageText}
|
|
51
|
+
onChange={(e) => onMessageTextChange(e.target.value)}
|
|
52
|
+
onKeyDown={handleKeyDown}
|
|
53
|
+
placeholder={t('writeMessagePlaceholder')}
|
|
54
|
+
rows={3}
|
|
55
|
+
className="pc-composer-textarea"
|
|
56
|
+
/>
|
|
57
|
+
</div>
|
|
58
|
+
<div className="pc-composer-actions">
|
|
59
|
+
<PCButton
|
|
60
|
+
variant="outline"
|
|
61
|
+
size="sm"
|
|
62
|
+
disabled={isUploading || isSending}
|
|
63
|
+
onClick={onAttachClick}
|
|
64
|
+
className="pc-composer-btn"
|
|
65
|
+
>
|
|
66
|
+
{isUploading
|
|
67
|
+
? <PCLoader variant="spinner" size="sm" />
|
|
68
|
+
: <IoAttach size={16} />}
|
|
69
|
+
{t('attachFile')}
|
|
70
|
+
</PCButton>
|
|
71
|
+
<PCButton
|
|
72
|
+
size="sm"
|
|
73
|
+
disabled={(!messageText.trim() && !pendingUpload) || isSending || isUploading}
|
|
74
|
+
onClick={onSend}
|
|
75
|
+
className="pc-composer-btn"
|
|
76
|
+
>
|
|
77
|
+
{isSending ? <PCLoader variant="spinner" size="sm" /> : null}
|
|
78
|
+
{t('send')}
|
|
79
|
+
<IoSend size={14} />
|
|
80
|
+
</PCButton>
|
|
81
|
+
</div>
|
|
82
|
+
<div className="pc-composer-footer">
|
|
83
|
+
{t('messagesSecureFooter')}
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { useTranslation } from 'react-i18next';
|
|
2
|
+
import {
|
|
3
|
+
IoCheckmarkCircle, IoCloseCircle, IoAlertCircle,
|
|
4
|
+
} from 'react-icons/io5';
|
|
5
|
+
import type { MessageItemProps } from '../../lib/types';
|
|
6
|
+
import { STATUS_COLORS } from '../../lib/constants';
|
|
7
|
+
import { formatMessageTime } from './utils';
|
|
8
|
+
import { DocumentRequestCard } from './DocumentRequestCard';
|
|
9
|
+
import { StatusEvent } from './StatusEvent';
|
|
10
|
+
import { ChatBubble } from './ChatBubble';
|
|
11
|
+
|
|
12
|
+
export const MessageItem = ({
|
|
13
|
+
message,
|
|
14
|
+
onUploadForRequest,
|
|
15
|
+
isSubmittingForRequest,
|
|
16
|
+
}: MessageItemProps) => {
|
|
17
|
+
const { t } = useTranslation();
|
|
18
|
+
const isPrincipal = message.sender_role === 'data_principal';
|
|
19
|
+
const timestamp = formatMessageTime(message.created_at);
|
|
20
|
+
const senderLabel = isPrincipal ? t('senderYou') : message.triggered_by_email || t('senderPrivacyTeam');
|
|
21
|
+
|
|
22
|
+
switch (message.message_type) {
|
|
23
|
+
case 'document_requested':
|
|
24
|
+
return (
|
|
25
|
+
<>
|
|
26
|
+
{message.body && (
|
|
27
|
+
<div className="pc-chat-bubble-wrap pc-chat-bubble-wrap--received">
|
|
28
|
+
<span className="pc-chat-sender">
|
|
29
|
+
{senderLabel} · {timestamp}
|
|
30
|
+
</span>
|
|
31
|
+
<div className="pc-chat-bubble pc-chat-bubble--received">
|
|
32
|
+
{message.body}
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
)}
|
|
36
|
+
<DocumentRequestCard
|
|
37
|
+
message={message}
|
|
38
|
+
onUpload={onUploadForRequest}
|
|
39
|
+
isSubmitting={isSubmittingForRequest}
|
|
40
|
+
/>
|
|
41
|
+
</>
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
case 'document_approved':
|
|
45
|
+
return (
|
|
46
|
+
<StatusEvent
|
|
47
|
+
icon={<IoCheckmarkCircle size={18} />}
|
|
48
|
+
color={STATUS_COLORS.success.color}
|
|
49
|
+
bgColor={STATUS_COLORS.success.bg}
|
|
50
|
+
borderColor={STATUS_COLORS.success.border}
|
|
51
|
+
text={t('documentAccepted')}
|
|
52
|
+
timestamp={timestamp}
|
|
53
|
+
/>
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
case 'document_rejected':
|
|
57
|
+
return (
|
|
58
|
+
<StatusEvent
|
|
59
|
+
icon={<IoCloseCircle size={18} />}
|
|
60
|
+
color={STATUS_COLORS.error.color}
|
|
61
|
+
bgColor={STATUS_COLORS.error.bg}
|
|
62
|
+
borderColor={STATUS_COLORS.error.border}
|
|
63
|
+
text={t('documentRejected')}
|
|
64
|
+
subText={message.document_request_metadata?.rejection_reason}
|
|
65
|
+
timestamp={timestamp}
|
|
66
|
+
/>
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
case 'document_replacement_requested':
|
|
70
|
+
return (
|
|
71
|
+
<StatusEvent
|
|
72
|
+
icon={<IoAlertCircle size={18} />}
|
|
73
|
+
color={STATUS_COLORS.warning.color}
|
|
74
|
+
bgColor={STATUS_COLORS.warning.bg}
|
|
75
|
+
borderColor={STATUS_COLORS.warning.border}
|
|
76
|
+
text={t('replacementRequested')}
|
|
77
|
+
subText={message.body}
|
|
78
|
+
timestamp={timestamp}
|
|
79
|
+
/>
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
case 'document_submitted':
|
|
83
|
+
case 'document_replaced':
|
|
84
|
+
case 'message_sent':
|
|
85
|
+
default:
|
|
86
|
+
return (
|
|
87
|
+
<ChatBubble
|
|
88
|
+
message={message}
|
|
89
|
+
isPrincipal={isPrincipal}
|
|
90
|
+
senderLabel={senderLabel}
|
|
91
|
+
timestamp={timestamp}
|
|
92
|
+
/>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
};
|