@redacto.io/consent-sdk-react 9.1.0 → 9.2.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 +10 -14
- package/CHANGELOG.md +6 -0
- package/dist/index.js +2 -4
- package/dist/index.mjs +2 -4
- package/dist/privacy-center.js +860 -851
- package/dist/privacy-center.mjs +810 -801
- 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/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/PurposeSelectionSection.tsx +12 -0
- package/src/RedactoPrivacyCenter/components/Form.tsx +15 -129
- package/src/RedactoPrivacyCenter/lib/constants.ts +12 -0
- package/src/RedactoPrivacyCenter/lib/types.ts +10 -0
- package/src/RedactoPrivacyCenter/locales/as/translation.json +1 -0
- package/src/RedactoPrivacyCenter/locales/bho/translation.json +1 -0
- package/src/RedactoPrivacyCenter/locales/bn/translation.json +1 -0
- package/src/RedactoPrivacyCenter/locales/brx/translation.json +1 -0
- package/src/RedactoPrivacyCenter/locales/doi/translation.json +1 -0
- package/src/RedactoPrivacyCenter/locales/en/translation.json +2 -0
- package/src/RedactoPrivacyCenter/locales/gom/translation.json +1 -0
- package/src/RedactoPrivacyCenter/locales/gu/translation.json +1 -0
- package/src/RedactoPrivacyCenter/locales/hi/translation.json +1 -0
- package/src/RedactoPrivacyCenter/locales/kn/translation.json +1 -0
- package/src/RedactoPrivacyCenter/locales/ks/translation.json +1 -0
- package/src/RedactoPrivacyCenter/locales/mai/translation.json +1 -0
- package/src/RedactoPrivacyCenter/locales/ml/translation.json +1 -0
- package/src/RedactoPrivacyCenter/locales/mni-Mtei/translation.json +1 -0
- package/src/RedactoPrivacyCenter/locales/mr/translation.json +1 -0
- package/src/RedactoPrivacyCenter/locales/ne/translation.json +1 -0
- package/src/RedactoPrivacyCenter/locales/or/translation.json +1 -0
- package/src/RedactoPrivacyCenter/locales/pa/translation.json +1 -0
- package/src/RedactoPrivacyCenter/locales/sa/translation.json +1 -0
- package/src/RedactoPrivacyCenter/locales/sat/translation.json +1 -0
- package/src/RedactoPrivacyCenter/locales/sd/translation.json +1 -0
- package/src/RedactoPrivacyCenter/locales/ta/translation.json +1 -0
- package/src/RedactoPrivacyCenter/locales/te/translation.json +1 -0
- package/src/RedactoPrivacyCenter/locales/ur/translation.json +1 -0
- package/src/RedactoPrivacyCenter/ui/PCTooltip.tsx +3 -1
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
|
)}
|
|
@@ -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
|
|
|
@@ -145,6 +145,18 @@ export default function PurposeSelectionSection({
|
|
|
145
145
|
? t('whatDataToAccess')
|
|
146
146
|
: t('whatDataToDelete')}
|
|
147
147
|
</p>
|
|
148
|
+
<p style={{
|
|
149
|
+
marginTop: '0.25rem',
|
|
150
|
+
marginBottom: '0.5rem',
|
|
151
|
+
paddingLeft: getVar('--pc-spacing-sm'),
|
|
152
|
+
borderLeft: `2px solid ${getVar('--pc-primary')}`,
|
|
153
|
+
fontSize: pcStyles.textXs.fontSize,
|
|
154
|
+
color: getVar('--pc-text-secondary'),
|
|
155
|
+
lineHeight: 1.4,
|
|
156
|
+
}}>
|
|
157
|
+
{t('revocationNotDeletionNote') ||
|
|
158
|
+
'Revoking consent does not delete data the organization already holds. You can still request access to or erasure of this data, including for purposes whose consent you have withdrawn.'}
|
|
159
|
+
</p>
|
|
148
160
|
{!isErasure && (
|
|
149
161
|
<PCCheckbox
|
|
150
162
|
label={t('selectAll')}
|
|
@@ -9,7 +9,6 @@ import { AiOutlineClose } from 'react-icons/ai';
|
|
|
9
9
|
import { PiUploadBold } from 'react-icons/pi';
|
|
10
10
|
import { toast } from '../utils/toast';
|
|
11
11
|
import { PCButton, PCCheckbox, PCInput, PCLoader, PCSelect, PCTextarea } from '../ui';
|
|
12
|
-
import { PCModal } from '../ui/PCModal';
|
|
13
12
|
import { pcStyles } from '../styles/pcStyles';
|
|
14
13
|
import { createCaseRequest, getFormData } from '../lib/api';
|
|
15
14
|
import { ensureValidToken } from '../lib/utils';
|
|
@@ -41,8 +40,7 @@ import NominationDataSection from './DataRequest/NominationDataSection';
|
|
|
41
40
|
import PurposeSelectionSection from './DataRequest/PurposeSelectionSection';
|
|
42
41
|
import RequestSubmit from './RequestSubmit';
|
|
43
42
|
import type { CaseHistoryProps } from './types';
|
|
44
|
-
import {
|
|
45
|
-
import { MAX_DISPLAYED_PURPOSE_NAMES_IN_ERASURE_MODAL } from './constant';
|
|
43
|
+
import { FiChevronDown, FiChevronUp, FiX } from 'react-icons/fi';
|
|
46
44
|
|
|
47
45
|
const getVar = (varName: string): string => `var(${varName})`;
|
|
48
46
|
|
|
@@ -134,10 +132,6 @@ export default function Form({
|
|
|
134
132
|
const [isTranslating, setIsTranslating] = useState(false);
|
|
135
133
|
const [isRequestDetailsExpanded, setIsRequestDetailsExpanded] =
|
|
136
134
|
useState(true);
|
|
137
|
-
const [isErasureConsentModalOpen, setIsErasureConsentModalOpen] =
|
|
138
|
-
useState(false);
|
|
139
|
-
const [revokeConsentOnFulfilment, setRevokeConsentOnFulfilment] =
|
|
140
|
-
useState(false);
|
|
141
135
|
|
|
142
136
|
const resetFileInput = () => {
|
|
143
137
|
if (fileInputRef.current) {
|
|
@@ -164,11 +158,13 @@ export default function Form({
|
|
|
164
158
|
|
|
165
159
|
const uniqueElements = new Map();
|
|
166
160
|
extractedPurposes.forEach((purpose) => {
|
|
167
|
-
// Ensure data_elements is an array and filter out any null/undefined elements
|
|
168
|
-
//
|
|
161
|
+
// Ensure data_elements is an array and filter out any null/undefined elements.
|
|
162
|
+
// Include every data element the org holds for this user, regardless of current
|
|
163
|
+
// consent state. Revoking consent does not delete data (TECH-4089), so purposes
|
|
164
|
+
// with revoked/withdrawn consent must remain raisable for access/erasure/correction.
|
|
169
165
|
const validDataElements = Array.isArray(purpose.data_elements)
|
|
170
166
|
? purpose.data_elements.filter(
|
|
171
|
-
(element) => element !== null && element !== undefined && element.uuid
|
|
167
|
+
(element) => element !== null && element !== undefined && element.uuid
|
|
172
168
|
)
|
|
173
169
|
: [];
|
|
174
170
|
|
|
@@ -188,13 +184,14 @@ export default function Form({
|
|
|
188
184
|
Array.from(uniqueElements.values()) as DataElement[]
|
|
189
185
|
);
|
|
190
186
|
|
|
191
|
-
// Ensure data_elements are properly preserved with all properties
|
|
192
|
-
//
|
|
187
|
+
// Ensure data_elements are properly preserved with all properties.
|
|
188
|
+
// Include data elements regardless of consent state (revocation ≠ deletion,
|
|
189
|
+
// TECH-4089); only drop purposes that genuinely have no data elements.
|
|
193
190
|
const processedPurposes = extractedPurposes
|
|
194
191
|
.map((p) => {
|
|
195
192
|
const validDataElements = Array.isArray(p.data_elements)
|
|
196
193
|
? p.data_elements
|
|
197
|
-
.filter((de) => de !== null && de !== undefined && de.
|
|
194
|
+
.filter((de) => de !== null && de !== undefined && de.uuid)
|
|
198
195
|
.map((de) => ({
|
|
199
196
|
...de,
|
|
200
197
|
selected: false,
|
|
@@ -321,13 +318,14 @@ export default function Form({
|
|
|
321
318
|
.map((p) => p?.purpose)
|
|
322
319
|
.filter((p) => p !== null && p !== undefined);
|
|
323
320
|
|
|
324
|
-
// Ensure data_elements are properly preserved with all properties
|
|
325
|
-
//
|
|
321
|
+
// Ensure data_elements are properly preserved with all properties.
|
|
322
|
+
// Include data elements regardless of consent state (revocation ≠ deletion,
|
|
323
|
+
// TECH-4089); only drop purposes that genuinely have no data elements.
|
|
326
324
|
const processedPurposes = extractedPurposes
|
|
327
325
|
.map((p) => {
|
|
328
326
|
const validDataElements = Array.isArray(p.data_elements)
|
|
329
327
|
? p.data_elements
|
|
330
|
-
.filter((de) => de !== null && de !== undefined && de.
|
|
328
|
+
.filter((de) => de !== null && de !== undefined && de.uuid)
|
|
331
329
|
.map((de) => ({
|
|
332
330
|
...de,
|
|
333
331
|
selected: false,
|
|
@@ -505,47 +503,13 @@ export default function Form({
|
|
|
505
503
|
return translatedName;
|
|
506
504
|
}, [data]);
|
|
507
505
|
|
|
508
|
-
const selectedErasurePurposeList = useMemo(
|
|
509
|
-
() =>
|
|
510
|
-
erasurePurposes
|
|
511
|
-
.filter((p) => p.selected || p.data_elements.some((de) => de.selected))
|
|
512
|
-
.map((p) => p.name)
|
|
513
|
-
.filter(Boolean),
|
|
514
|
-
[erasurePurposes]
|
|
515
|
-
);
|
|
516
|
-
|
|
517
|
-
const selectedErasurePurposeNames = useMemo(() => {
|
|
518
|
-
if (
|
|
519
|
-
selectedErasurePurposeList.length === 0 ||
|
|
520
|
-
selectedErasurePurposeList.length > MAX_DISPLAYED_PURPOSE_NAMES_IN_ERASURE_MODAL
|
|
521
|
-
) {
|
|
522
|
-
return t('revokeConsentModalYourPurposes');
|
|
523
|
-
}
|
|
524
|
-
return selectedErasurePurposeList.join(', ');
|
|
525
|
-
}, [selectedErasurePurposeList, t]);
|
|
526
|
-
|
|
527
506
|
const handleSubmitClick = () => {
|
|
528
|
-
if (!isFormValid){
|
|
529
|
-
return
|
|
530
|
-
};
|
|
531
|
-
if (requestType === RequestTypeEnum.ERASURE) {
|
|
532
|
-
setIsErasureConsentModalOpen(true);
|
|
507
|
+
if (!isFormValid) {
|
|
533
508
|
return;
|
|
534
509
|
}
|
|
535
|
-
setRevokeConsentOnFulfilment(false);
|
|
536
510
|
void handleSubmit(false);
|
|
537
511
|
};
|
|
538
512
|
|
|
539
|
-
const handleErasureModalChoice = (revoke: boolean) => {
|
|
540
|
-
setRevokeConsentOnFulfilment(revoke);
|
|
541
|
-
setIsErasureConsentModalOpen(false);
|
|
542
|
-
void handleSubmit(revoke);
|
|
543
|
-
};
|
|
544
|
-
|
|
545
|
-
const handleErasureModalClose = () => {
|
|
546
|
-
setIsErasureConsentModalOpen(false);
|
|
547
|
-
};
|
|
548
|
-
|
|
549
513
|
const handleSubmit = async (revokeConsent: boolean = false) => {
|
|
550
514
|
if (!isFormValid){
|
|
551
515
|
return
|
|
@@ -923,8 +887,6 @@ export default function Form({
|
|
|
923
887
|
caseId={caseId || ''}
|
|
924
888
|
contact={formData?.contact}
|
|
925
889
|
onCreateRequest={onCreateRequest}
|
|
926
|
-
revokeConsentOnFulfilment={revokeConsentOnFulfilment}
|
|
927
|
-
revokedPurposeNames={selectedErasurePurposeNames}
|
|
928
890
|
/>
|
|
929
891
|
) : (
|
|
930
892
|
<div>
|
|
@@ -1511,82 +1473,6 @@ export default function Form({
|
|
|
1511
1473
|
</div>
|
|
1512
1474
|
</div>
|
|
1513
1475
|
)}
|
|
1514
|
-
<PCModal
|
|
1515
|
-
isOpen={isErasureConsentModalOpen}
|
|
1516
|
-
onClose={handleErasureModalClose}
|
|
1517
|
-
size="md"
|
|
1518
|
-
showCloseButton={false}
|
|
1519
|
-
footer={
|
|
1520
|
-
<div className="pc-erasure-modal-footer">
|
|
1521
|
-
<PCButton
|
|
1522
|
-
variant="secondary"
|
|
1523
|
-
onClick={() => handleErasureModalChoice(false)}
|
|
1524
|
-
disabled={isLoading}
|
|
1525
|
-
>
|
|
1526
|
-
{t('revokeConsentNo')}
|
|
1527
|
-
</PCButton>
|
|
1528
|
-
<PCButton
|
|
1529
|
-
variant="primary"
|
|
1530
|
-
onClick={() => handleErasureModalChoice(true)}
|
|
1531
|
-
disabled={isLoading}
|
|
1532
|
-
isLoading={isLoading}
|
|
1533
|
-
>
|
|
1534
|
-
{t('revokeConsentYes')}
|
|
1535
|
-
</PCButton>
|
|
1536
|
-
</div>
|
|
1537
|
-
}
|
|
1538
|
-
>
|
|
1539
|
-
<div className="pc-erasure-modal-content">
|
|
1540
|
-
<div className="pc-erasure-modal-header">
|
|
1541
|
-
<div className="pc-erasure-modal-icon">
|
|
1542
|
-
<FiAlertCircle size={24} />
|
|
1543
|
-
</div>
|
|
1544
|
-
<p className="pc-erasure-modal-title">
|
|
1545
|
-
{t('revokeConsentModalTitle')}
|
|
1546
|
-
</p>
|
|
1547
|
-
<p className="pc-erasure-modal-question">
|
|
1548
|
-
{t('revokeConsentModalQuestion')}
|
|
1549
|
-
</p>
|
|
1550
|
-
</div>
|
|
1551
|
-
|
|
1552
|
-
<div className="pc-erasure-modal-purposes">
|
|
1553
|
-
<p className="pc-erasure-modal-purposes-label">
|
|
1554
|
-
{t('revokeConsentModalPurposesLabel')}
|
|
1555
|
-
</p>
|
|
1556
|
-
{selectedErasurePurposeList.length > 0 ? (
|
|
1557
|
-
<>
|
|
1558
|
-
{selectedErasurePurposeList
|
|
1559
|
-
.slice(0, MAX_DISPLAYED_PURPOSE_NAMES_IN_ERASURE_MODAL)
|
|
1560
|
-
.map((name) => (
|
|
1561
|
-
<div key={name} className="pc-erasure-modal-purpose-item">
|
|
1562
|
-
{name}
|
|
1563
|
-
</div>
|
|
1564
|
-
))}
|
|
1565
|
-
{selectedErasurePurposeList.length > MAX_DISPLAYED_PURPOSE_NAMES_IN_ERASURE_MODAL && (
|
|
1566
|
-
<span className="pc-erasure-modal-purposes-placeholder">
|
|
1567
|
-
{t('revokeConsentModalAndMore', {
|
|
1568
|
-
count: selectedErasurePurposeList.length - MAX_DISPLAYED_PURPOSE_NAMES_IN_ERASURE_MODAL,
|
|
1569
|
-
})}
|
|
1570
|
-
</span>
|
|
1571
|
-
)}
|
|
1572
|
-
</>
|
|
1573
|
-
) : (
|
|
1574
|
-
<span className="pc-erasure-modal-purposes-placeholder">
|
|
1575
|
-
{t('revokeConsentModalYourPurposes')}
|
|
1576
|
-
</span>
|
|
1577
|
-
)}
|
|
1578
|
-
</div>
|
|
1579
|
-
|
|
1580
|
-
<div className="pc-erasure-modal-warning">
|
|
1581
|
-
<p className="pc-erasure-modal-warning-text">
|
|
1582
|
-
<span className="pc-erasure-modal-warning-label">
|
|
1583
|
-
{t('revokeConsentModalIfYesLabel')}
|
|
1584
|
-
</span>{' '}
|
|
1585
|
-
{t('revokeConsentModalIfYesBody')}
|
|
1586
|
-
</p>
|
|
1587
|
-
</div>
|
|
1588
|
-
</div>
|
|
1589
|
-
</PCModal>
|
|
1590
1476
|
</>
|
|
1591
1477
|
);
|
|
1592
1478
|
}
|
|
@@ -138,6 +138,18 @@ export const STATUS_COLORS = {
|
|
|
138
138
|
infoBanner: { color: '#1d4ed8', bg: '#eff6ff', border: '#bfdbfe' },
|
|
139
139
|
} as const;
|
|
140
140
|
|
|
141
|
+
// ─── Case statuses ────────────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
export const TERMINAL_CASE_STATUSES = ['completed', 'rejected', 'declined', 'expired'] as const;
|
|
144
|
+
|
|
145
|
+
export const CASE_FINALIZED_TOOLTIP_KEY = 'caseFinalizedMessagesTooltip';
|
|
146
|
+
export const CASE_FINALIZED_TOOLTIP_FALLBACK = 'This case is finalized — messages can no longer be sent.';
|
|
147
|
+
|
|
148
|
+
export const useCaseFinalizedTooltip = () => {
|
|
149
|
+
const { t } = useTranslation();
|
|
150
|
+
return t(CASE_FINALIZED_TOOLTIP_KEY, CASE_FINALIZED_TOOLTIP_FALLBACK);
|
|
151
|
+
};
|
|
152
|
+
|
|
141
153
|
// ─── Document request ─────────────────────────────────────────────────────────
|
|
142
154
|
|
|
143
155
|
export const UPLOAD_ACTIVE_STATUSES: DocumentRequestStatus[] = ['pending', 'replacement_requested'];
|
|
@@ -509,6 +509,7 @@ export type MessageItemProps = Readonly<{
|
|
|
509
509
|
message: CaseMessage;
|
|
510
510
|
onUploadForRequest: (requestEventUuid: string) => void;
|
|
511
511
|
isSubmittingForRequest: boolean;
|
|
512
|
+
isCaseFinalized: boolean;
|
|
512
513
|
}>;
|
|
513
514
|
|
|
514
515
|
export type ChatBubbleProps = Readonly<{
|
|
@@ -526,6 +527,7 @@ export type DocumentRequestCardProps = Readonly<{
|
|
|
526
527
|
message: CaseMessage;
|
|
527
528
|
onUpload: (requestEventUuid: string) => void;
|
|
528
529
|
isSubmitting: boolean;
|
|
530
|
+
isCaseFinalized: boolean;
|
|
529
531
|
}>
|
|
530
532
|
|
|
531
533
|
export type StatusEventProps = Readonly<{
|
|
@@ -540,6 +542,7 @@ export type StatusEventProps = Readonly<{
|
|
|
540
542
|
|
|
541
543
|
export type MessagesTabProps = Readonly<{
|
|
542
544
|
caseUuid: string;
|
|
545
|
+
caseStatus: string;
|
|
543
546
|
}>;
|
|
544
547
|
|
|
545
548
|
export type MessageComposerProps = Readonly<{
|
|
@@ -551,6 +554,13 @@ export type MessageComposerProps = Readonly<{
|
|
|
551
554
|
onSend: () => void;
|
|
552
555
|
isSending: boolean;
|
|
553
556
|
isUploading: boolean;
|
|
557
|
+
isCaseFinalized: boolean;
|
|
558
|
+
}>;
|
|
559
|
+
|
|
560
|
+
export type FinalizedTooltipWrapperProps = Readonly<{
|
|
561
|
+
enabled: boolean;
|
|
562
|
+
autoWidth?: boolean;
|
|
563
|
+
children: React.ReactElement;
|
|
554
564
|
}>;
|
|
555
565
|
|
|
556
566
|
export type CaseStatusVariant =
|
|
@@ -202,6 +202,7 @@
|
|
|
202
202
|
"submitted": "দাখিল কৰা হৈছে",
|
|
203
203
|
"responseDue": "প্ৰতিক্ৰিয়া বাকী",
|
|
204
204
|
"secureMessagesNotice": "আপোনাৰ বাৰ্তাসমূহ কেৱল গোপনীয়তা দলক সুৰক্ষিতভাৱে প্ৰেৰণ কৰা হয়।",
|
|
205
|
+
"caseFinalizedMessagesTooltip": "এই গোচৰটো চূড়ান্ত কৰা হৈছে — এতিয়া বাৰ্তা পঠাব নোৱাৰি।",
|
|
205
206
|
"noMessagesYet": "এতিয়ালৈকে কোনো বাৰ্তা নাই",
|
|
206
207
|
"writeMessagePlaceholder": "গোপনীয়তা দলক বাৰ্তা লিখক...",
|
|
207
208
|
"attachFile": "ফাইল সংলগ্ন কৰক",
|
|
@@ -202,6 +202,7 @@
|
|
|
202
202
|
"submitted": "जमा भइल",
|
|
203
203
|
"responseDue": "जवाब बाकी बा",
|
|
204
204
|
"secureMessagesNotice": "रउआ संदेश बस गोपनीयता टीम के सुरक्षित तरीका से भेजल जाला।",
|
|
205
|
+
"caseFinalizedMessagesTooltip": "ई केस अंतिम हो गइल बा — अब संदेस ना भेजल जा सके।",
|
|
205
206
|
"noMessagesYet": "अबहीं तक कवनो संदेश नइखे",
|
|
206
207
|
"writeMessagePlaceholder": "गोपनीयता टीम के संदेश लिखीं...",
|
|
207
208
|
"attachFile": "फाइल जोड़ीं",
|
|
@@ -202,6 +202,7 @@
|
|
|
202
202
|
"submitted": "জমা দেওয়া হয়েছে",
|
|
203
203
|
"responseDue": "প্রতিক্রিয়া বাকি",
|
|
204
204
|
"secureMessagesNotice": "আপনার বার্তাগুলি শুধুমাত্র গোপনীয়তা দলের কাছে নিরাপদে পাঠানো হয়।",
|
|
205
|
+
"caseFinalizedMessagesTooltip": "এই কেসটি চূড়ান্ত করা হয়েছে — বার্তা আর পাঠানো যাবে না।",
|
|
205
206
|
"noMessagesYet": "এখনো কোনো বার্তা নেই",
|
|
206
207
|
"writeMessagePlaceholder": "গোপনীয়তা দলকে একটি বার্তা লিখুন...",
|
|
207
208
|
"attachFile": "ফাইল সংযুক্ত করুন",
|
|
@@ -202,6 +202,7 @@
|
|
|
202
202
|
"submitted": "दाखिल जादों",
|
|
203
203
|
"responseDue": "फिननाय बाकी",
|
|
204
204
|
"secureMessagesNotice": "नोंथांनि खौरांफोर सिगां गोपनीयथा हानजाखौ सुरक्षित बायदिजों दैथायदों।",
|
|
205
|
+
"caseFinalizedMessagesTooltip": "बे केसआ जोबनाय जायो — दानै खौरांखौ दैथाय हुनो हाया।",
|
|
205
206
|
"noMessagesYet": "दानो सम कोनो खौरां गैया",
|
|
206
207
|
"writeMessagePlaceholder": "गोपनीयथा हानजाखौ खौरां लिर...",
|
|
207
208
|
"attachFile": "फाइल लाफाब",
|