mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 03:20:46 +02:00
fileshare (#5414)
Co-authored-by: ConnorYoh <[email protected]> Co-authored-by: Connor Yoh <[email protected]> Co-authored-by: EthanHealy01 <[email protected]> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
ConnorYoh
Connor Yoh
EthanHealy01
Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
parent
47cad0a131
commit
28613caf8a
@@ -15,6 +15,7 @@ import AdminFeaturesSection from '@app/components/shared/config/configSections/A
|
||||
import AdminEndpointsSection from '@app/components/shared/config/configSections/AdminEndpointsSection';
|
||||
import AdminAuditSection from '@app/components/shared/config/configSections/AdminAuditSection';
|
||||
import AdminUsageSection from '@app/components/shared/config/configSections/AdminUsageSection';
|
||||
import AdminStorageSharingSection from '@app/components/shared/config/configSections/AdminStorageSharingSection';
|
||||
import ApiKeys from '@app/components/shared/config/configSections/ApiKeys';
|
||||
import AccountSection from '@app/components/shared/config/configSections/AccountSection';
|
||||
import GeneralSection from '@app/components/shared/config/configSections/GeneralSection';
|
||||
@@ -98,6 +99,14 @@ export const useConfigNavSections = (
|
||||
disabled: requiresLogin,
|
||||
disabledTooltip: requiresLogin ? enableLoginTooltip : undefined
|
||||
},
|
||||
{
|
||||
key: 'adminStorageSharing',
|
||||
label: t('settings.configuration.storageSharing', 'File Storage & Sharing'),
|
||||
icon: 'storage-rounded',
|
||||
component: <AdminStorageSharingSection />,
|
||||
disabled: requiresLogin,
|
||||
disabledTooltip: requiresLogin ? enableLoginTooltip : undefined
|
||||
},
|
||||
{
|
||||
key: 'adminEndpoints',
|
||||
label: t('settings.configuration.endpoints', 'Endpoints'),
|
||||
|
||||
+294
@@ -0,0 +1,294 @@
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Anchor, Group, Loader, Paper, Stack, Switch, Text } from '@mantine/core';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { alert } from '@app/components/toast';
|
||||
import RestartConfirmationModal from '@app/components/shared/config/RestartConfirmationModal';
|
||||
import { useRestartServer } from '@app/components/shared/config/useRestartServer';
|
||||
import { useAdminSettings } from '@app/hooks/useAdminSettings';
|
||||
import PendingBadge from '@app/components/shared/config/PendingBadge';
|
||||
import apiClient from '@app/services/apiClient';
|
||||
import { useLoginRequired } from '@app/hooks/useLoginRequired';
|
||||
import LoginRequiredBanner from '@app/components/shared/config/LoginRequiredBanner';
|
||||
import { SettingsStickyFooter } from '@app/components/shared/config/SettingsStickyFooter';
|
||||
import { useSettingsDirty } from '@app/hooks/useSettingsDirty';
|
||||
|
||||
interface StorageSharingSettingsData {
|
||||
enabled?: boolean;
|
||||
sharing?: {
|
||||
enabled?: boolean;
|
||||
linkEnabled?: boolean;
|
||||
emailEnabled?: boolean;
|
||||
};
|
||||
signing?: {
|
||||
enabled?: boolean;
|
||||
};
|
||||
system?: {
|
||||
frontendUrl?: string;
|
||||
};
|
||||
mail?: {
|
||||
enabled?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export default function AdminStorageSharingSection() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { loginEnabled, validateLoginEnabled, getDisabledStyles } = useLoginRequired();
|
||||
const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer();
|
||||
|
||||
const {
|
||||
settings,
|
||||
setSettings,
|
||||
loading,
|
||||
saving,
|
||||
fetchSettings,
|
||||
saveSettings,
|
||||
isFieldPending,
|
||||
} = useAdminSettings<StorageSharingSettingsData>({
|
||||
sectionName: 'storage',
|
||||
fetchTransformer: async () => {
|
||||
const [storageResponse, systemResponse, mailResponse] = await Promise.all([
|
||||
apiClient.get('/api/v1/admin/settings/section/storage'),
|
||||
apiClient.get('/api/v1/admin/settings/section/system'),
|
||||
apiClient.get('/api/v1/admin/settings/section/mail'),
|
||||
]);
|
||||
|
||||
const storageData = storageResponse.data || {};
|
||||
const systemData = systemResponse.data || {};
|
||||
const mailData = mailResponse.data || {};
|
||||
|
||||
return {
|
||||
...storageData,
|
||||
system: { frontendUrl: systemData.frontendUrl || '' },
|
||||
mail: { enabled: mailData.enabled || false },
|
||||
};
|
||||
},
|
||||
saveTransformer: (currentSettings) => ({
|
||||
sectionData: {
|
||||
enabled: currentSettings.enabled,
|
||||
sharing: {
|
||||
enabled: currentSettings.sharing?.enabled,
|
||||
linkEnabled: currentSettings.sharing?.linkEnabled,
|
||||
emailEnabled: currentSettings.sharing?.emailEnabled,
|
||||
},
|
||||
signing: {
|
||||
enabled: currentSettings.signing?.enabled,
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (loginEnabled) {
|
||||
fetchSettings();
|
||||
}
|
||||
}, [loginEnabled]);
|
||||
|
||||
const storageEnabled = settings.enabled ?? false;
|
||||
const sharingEnabled = storageEnabled && (settings.sharing?.enabled ?? false);
|
||||
const frontendUrlConfigured = Boolean(settings.system?.frontendUrl?.trim());
|
||||
const mailEnabled = Boolean(settings.mail?.enabled);
|
||||
|
||||
const { isDirty, resetToSnapshot, markSaved } = useSettingsDirty(settings, loading);
|
||||
|
||||
const handleDiscard = useCallback(() => {
|
||||
setSettings(resetToSnapshot());
|
||||
}, [resetToSnapshot, setSettings]);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!validateLoginEnabled()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
markSaved();
|
||||
await saveSettings();
|
||||
showRestartModal();
|
||||
} catch (_error) {
|
||||
alert({
|
||||
alertType: 'error',
|
||||
title: t('admin.error', 'Error'),
|
||||
body: t('admin.settings.saveError', 'Failed to save settings'),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (loginEnabled && loading) {
|
||||
return (
|
||||
<Stack align="center" justify="center" h={200}>
|
||||
<Loader size="lg" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="settings-section-container">
|
||||
<div className="settings-section-content">
|
||||
<Stack gap="lg">
|
||||
<LoginRequiredBanner show={!loginEnabled} />
|
||||
<div>
|
||||
<Text fw={600} size="lg">{t('admin.settings.storage.title', 'File Storage & Sharing')}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t('admin.settings.storage.description', 'Control server storage and sharing options.')}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<Stack gap="md">
|
||||
<Group justify="space-between" align="center">
|
||||
<Text fw={600} size="sm">{t('admin.settings.storage.enabled.label', 'Enable Server File Storage')}</Text>
|
||||
{isFieldPending('enabled') && <PendingBadge show={true} />}
|
||||
</Group>
|
||||
<Text size="xs" c="dimmed">
|
||||
{t('admin.settings.storage.enabled.description', 'Allow users to store files on the server.')}
|
||||
</Text>
|
||||
<Switch
|
||||
checked={storageEnabled}
|
||||
onChange={(e) => setSettings({ ...settings, enabled: e.currentTarget.checked })}
|
||||
disabled={!loginEnabled}
|
||||
styles={getDisabledStyles()}
|
||||
/>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<Stack gap="md">
|
||||
<Group justify="space-between" align="center">
|
||||
<Text fw={600} size="sm">{t('admin.settings.storage.sharing.enabled.label', 'Enable Sharing')}</Text>
|
||||
{isFieldPending('sharing.enabled') && <PendingBadge show={true} />}
|
||||
</Group>
|
||||
<Text size="xs" c="dimmed">
|
||||
{t('admin.settings.storage.sharing.enabled.description', 'Allow users to share stored files.')}
|
||||
</Text>
|
||||
<Switch
|
||||
checked={settings.sharing?.enabled ?? false}
|
||||
onChange={(e) =>
|
||||
setSettings({
|
||||
...settings,
|
||||
sharing: { ...settings.sharing, enabled: e.currentTarget.checked },
|
||||
})
|
||||
}
|
||||
disabled={!loginEnabled || !storageEnabled}
|
||||
styles={getDisabledStyles()}
|
||||
/>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<Stack gap="md">
|
||||
<Group justify="space-between" align="center">
|
||||
<Text fw={600} size="sm">{t('admin.settings.storage.sharing.links.label', 'Enable Share Links')}</Text>
|
||||
{isFieldPending('sharing.linkEnabled') && <PendingBadge show={true} />}
|
||||
</Group>
|
||||
<Text size="xs" c="dimmed">
|
||||
{t('admin.settings.storage.sharing.links.description', 'Allow sharing via signed-in links.')}
|
||||
</Text>
|
||||
{!frontendUrlConfigured && (
|
||||
<Text size="xs" c="orange">
|
||||
{t('admin.settings.storage.sharing.links.frontendUrlNote', 'Requires a Frontend URL. ')}
|
||||
<Anchor
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
navigate('/settings/adminGeneral#frontendUrl');
|
||||
}}
|
||||
c="orange"
|
||||
td="underline"
|
||||
>
|
||||
{t('admin.settings.storage.sharing.links.frontendUrlLink', 'Configure in System Settings')}
|
||||
</Anchor>
|
||||
</Text>
|
||||
)}
|
||||
<Switch
|
||||
checked={settings.sharing?.linkEnabled ?? false}
|
||||
onChange={(e) =>
|
||||
setSettings({
|
||||
...settings,
|
||||
sharing: { ...settings.sharing, linkEnabled: e.currentTarget.checked },
|
||||
})
|
||||
}
|
||||
disabled={!loginEnabled || !sharingEnabled || !frontendUrlConfigured}
|
||||
styles={getDisabledStyles()}
|
||||
/>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<Stack gap="md">
|
||||
<Group justify="space-between" align="center">
|
||||
<Text fw={600} size="sm">{t('admin.settings.storage.sharing.email.label', 'Enable Email Sharing')}</Text>
|
||||
{isFieldPending('sharing.emailEnabled') && <PendingBadge show={true} />}
|
||||
</Group>
|
||||
<Text size="xs" c="dimmed">
|
||||
{t('admin.settings.storage.sharing.email.description', 'Allow sharing with email addresses.')}
|
||||
</Text>
|
||||
{!mailEnabled && (
|
||||
<Text size="xs" c="orange">
|
||||
{t('admin.settings.storage.sharing.email.mailNote', 'Requires mail configuration. ')}
|
||||
<Anchor
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
navigate('/settings/adminConnections');
|
||||
}}
|
||||
c="orange"
|
||||
td="underline"
|
||||
>
|
||||
{t('admin.settings.storage.sharing.email.mailLink', 'Configure Mail Settings')}
|
||||
</Anchor>
|
||||
</Text>
|
||||
)}
|
||||
<Switch
|
||||
checked={settings.sharing?.emailEnabled ?? false}
|
||||
onChange={(e) =>
|
||||
setSettings({
|
||||
...settings,
|
||||
sharing: { ...settings.sharing, emailEnabled: e.currentTarget.checked },
|
||||
})
|
||||
}
|
||||
disabled={!loginEnabled || !sharingEnabled || !mailEnabled}
|
||||
styles={getDisabledStyles()}
|
||||
/>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<Stack gap="md">
|
||||
<Group justify="space-between" align="center">
|
||||
<Text fw={600} size="sm">{t('admin.settings.storage.signing.enabled.label', 'Enable Group Signing (Alpha)')}</Text>
|
||||
{isFieldPending('signing.enabled') && <PendingBadge show={true} />}
|
||||
</Group>
|
||||
<Text size="xs" c="dimmed">
|
||||
{t('admin.settings.storage.signing.enabled.description', 'Allow users to create multi-participant document signing sessions. Requires server file storage to be enabled.')}
|
||||
</Text>
|
||||
<Switch
|
||||
checked={settings.signing?.enabled ?? false}
|
||||
onChange={(e) =>
|
||||
setSettings({
|
||||
...settings,
|
||||
signing: { ...settings.signing, enabled: e.currentTarget.checked },
|
||||
})
|
||||
}
|
||||
disabled={!loginEnabled || !storageEnabled}
|
||||
styles={getDisabledStyles()}
|
||||
/>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
<RestartConfirmationModal
|
||||
opened={restartModalOpened}
|
||||
onClose={closeRestartModal}
|
||||
onRestart={restartServer}
|
||||
/>
|
||||
</Stack>
|
||||
</div>
|
||||
<SettingsStickyFooter
|
||||
isDirty={isDirty}
|
||||
saving={saving}
|
||||
loginEnabled={loginEnabled}
|
||||
onSave={handleSave}
|
||||
onDiscard={handleDiscard}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Stack,
|
||||
Card,
|
||||
Text,
|
||||
Badge,
|
||||
Group,
|
||||
Button,
|
||||
Loader,
|
||||
Alert,
|
||||
TextInput,
|
||||
FileInput,
|
||||
Select,
|
||||
} from '@mantine/core';
|
||||
import { useParticipantSession } from '@app/hooks/workflow/useParticipantSession';
|
||||
import InfoIcon from '@mui/icons-material/Info';
|
||||
import DownloadIcon from '@mui/icons-material/Download';
|
||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
|
||||
interface ParticipantViewProps {
|
||||
token: string;
|
||||
}
|
||||
|
||||
const ParticipantView: React.FC<ParticipantViewProps> = ({ token }) => {
|
||||
const { session, participant, loading, error, submitSignature, decline, downloadDocument } =
|
||||
useParticipantSession(token);
|
||||
|
||||
const [certType, setCertType] = useState<string>('P12');
|
||||
const [password, setPassword] = useState<string>('');
|
||||
const [certFile, setCertFile] = useState<File | null>(null);
|
||||
const [location, setLocation] = useState<string>('');
|
||||
const [reason, setReason] = useState<string>('Document Signing');
|
||||
const [showSignature, _setShowSignature] = useState<boolean>(true);
|
||||
const [pageNumber, setPageNumber] = useState<number>(1);
|
||||
const [declineReason, _setDeclineReason] = useState<string>('');
|
||||
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
||||
const [notification, setNotification] = useState<{ type: 'success' | 'error'; message: string } | null>(null);
|
||||
|
||||
const handleSubmitSignature = async () => {
|
||||
if (!certFile && certType !== 'SERVER') {
|
||||
setNotification({ type: 'error', message: 'Please select a certificate file' });
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
setNotification(null);
|
||||
try {
|
||||
await submitSignature({
|
||||
participantToken: token,
|
||||
certType,
|
||||
password,
|
||||
p12File: certType === 'P12' ? certFile || undefined : undefined,
|
||||
jksFile: certType === 'JKS' ? certFile || undefined : undefined,
|
||||
showSignature,
|
||||
pageNumber,
|
||||
location,
|
||||
reason,
|
||||
showLogo: true,
|
||||
});
|
||||
setNotification({ type: 'success', message: 'Signature submitted successfully!' });
|
||||
} catch (err: any) {
|
||||
setNotification({ type: 'error', message: `Failed to submit signature: ${err.message}` });
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDecline = async () => {
|
||||
if (window.confirm('Are you sure you want to decline signing this document?')) {
|
||||
setNotification(null);
|
||||
try {
|
||||
await decline(token, declineReason || 'Declined by participant');
|
||||
setNotification({ type: 'success', message: 'You have declined this signing request.' });
|
||||
} catch (err: any) {
|
||||
setNotification({ type: 'error', message: `Failed to decline: ${err.message}` });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (loading && !session) {
|
||||
return (
|
||||
<Stack align="center" justify="center" p="xl">
|
||||
<Loader size="lg" />
|
||||
<Text c="dimmed">Loading session...</Text>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Alert icon={<InfoIcon fontSize="small" />} color="red" title="Error">
|
||||
{error}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (!session || !participant) {
|
||||
return (
|
||||
<Alert icon={<InfoIcon fontSize="small" />} color="orange">
|
||||
Session not found or access denied.
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
switch (status) {
|
||||
case 'SIGNED':
|
||||
return <Badge color="green">Signed</Badge>;
|
||||
case 'DECLINED':
|
||||
return <Badge color="red">Declined</Badge>;
|
||||
case 'VIEWED':
|
||||
return <Badge color="blue">Viewed</Badge>;
|
||||
case 'NOTIFIED':
|
||||
return <Badge color="yellow">Notified</Badge>;
|
||||
case 'PENDING':
|
||||
return <Badge color="gray">Pending</Badge>;
|
||||
default:
|
||||
return <Badge>{status}</Badge>;
|
||||
}
|
||||
};
|
||||
|
||||
const canSign = !participant.hasCompleted && !participant.isExpired && session.status === 'IN_PROGRESS';
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
{notification && (
|
||||
<Alert
|
||||
icon={notification.type === 'success' ? <CheckCircleIcon fontSize="small" /> : <InfoIcon fontSize="small" />}
|
||||
color={notification.type === 'success' ? 'green' : 'red'}
|
||||
withCloseButton
|
||||
onClose={() => setNotification(null)}
|
||||
>
|
||||
{notification.message}
|
||||
</Alert>
|
||||
)}
|
||||
<Card shadow="sm" padding="md" radius="md" withBorder>
|
||||
<Stack gap="sm">
|
||||
<Group justify="space-between">
|
||||
<div>
|
||||
<Text fw={500} size="lg">
|
||||
{session.documentName}
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
From: {session.ownerUsername}
|
||||
</Text>
|
||||
</div>
|
||||
{getStatusBadge(participant.status)}
|
||||
</Group>
|
||||
|
||||
{session.message && (
|
||||
<Alert icon={<InfoIcon fontSize="small" />} color="blue" variant="light">
|
||||
<Text size="sm">{session.message}</Text>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{session.dueDate && (
|
||||
<Text size="sm" c="dimmed">
|
||||
Due Date: {session.dueDate}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Group gap="xs" mt="sm">
|
||||
<Button
|
||||
size="sm"
|
||||
leftSection={<DownloadIcon fontSize="small" />}
|
||||
onClick={() => downloadDocument(token)}
|
||||
variant="light"
|
||||
>
|
||||
Download Document
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
{canSign && (
|
||||
<Card shadow="sm" padding="md" radius="md" withBorder>
|
||||
<Stack gap="md">
|
||||
<Text fw={500} size="lg">
|
||||
Sign Document
|
||||
</Text>
|
||||
|
||||
<Select
|
||||
label="Certificate Type"
|
||||
value={certType}
|
||||
onChange={(value) => setCertType(value || 'P12')}
|
||||
data={[
|
||||
{ value: 'P12', label: 'P12/PKCS12 Certificate' },
|
||||
{ value: 'JKS', label: 'JKS Keystore' },
|
||||
{ value: 'SERVER', label: 'Server Certificate (if available)' },
|
||||
]}
|
||||
size="sm"
|
||||
/>
|
||||
|
||||
{certType !== 'SERVER' && (
|
||||
<>
|
||||
<FileInput
|
||||
label="Certificate File"
|
||||
placeholder="Select certificate file"
|
||||
value={certFile}
|
||||
onChange={setCertFile}
|
||||
accept=".p12,.pfx,.jks"
|
||||
size="sm"
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label="Certificate Password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.currentTarget.value)}
|
||||
size="sm"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<TextInput
|
||||
label="Location"
|
||||
placeholder="e.g., San Francisco, CA"
|
||||
value={location}
|
||||
onChange={(e) => setLocation(e.currentTarget.value)}
|
||||
size="sm"
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label="Reason"
|
||||
placeholder="e.g., Document approval"
|
||||
value={reason}
|
||||
onChange={(e) => setReason(e.currentTarget.value)}
|
||||
size="sm"
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label="Page Number (optional)"
|
||||
type="number"
|
||||
value={pageNumber}
|
||||
onChange={(e) => setPageNumber(parseInt(e.currentTarget.value) || 1)}
|
||||
size="sm"
|
||||
min={1}
|
||||
/>
|
||||
|
||||
<Group gap="xs">
|
||||
<Button
|
||||
leftSection={<CheckCircleIcon fontSize="small" />}
|
||||
onClick={handleSubmitSignature}
|
||||
loading={isSubmitting}
|
||||
color="green"
|
||||
>
|
||||
Submit Signature
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
leftSection={<CancelIcon fontSize="small" />}
|
||||
onClick={handleDecline}
|
||||
color="red"
|
||||
variant="light"
|
||||
>
|
||||
Decline
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{participant.hasCompleted && (
|
||||
<Alert icon={<CheckCircleIcon fontSize="small" />} color="green">
|
||||
You have {participant.status === 'SIGNED' ? 'signed' : 'declined'} this document.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{participant.isExpired && (
|
||||
<Alert icon={<InfoIcon fontSize="small" />} color="orange">
|
||||
Your access to this document has expired.
|
||||
</Alert>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default ParticipantView;
|
||||
Reference in New Issue
Block a user