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:
Anthony Stirling
2026-03-25 11:00:40 +00:00
committed by GitHub
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
181 changed files with 25715 additions and 124 deletions
@@ -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'),
@@ -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>
);
}