mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-03-13 02:18:16 +01:00
feat(settings): display frontend/backend versions and warn on client-server mismatch (#5571)
# Description of Changes ## Summary This PR improves the **Preferences → General → Software Updates** section by: - Showing **separate version labels** for **Frontend (Tauri client)** and **Backend (server/AppConfig)** across all locales. - Adding a **version mismatch detection** in `GeneralSection`, comparing the Tauri app version against the backend `AppConfig` version and displaying a **warning banner** when they differ. ## Why Running a Tauri desktop client against a different backend version can lead to: - Compatibility issues (API/UI expectations drifting) - Runtime errors due to schema/behavior changes - Increased security risk if the client and server are not kept in sync Surfacing both versions and warning on mismatch makes these situations visible and easier to diagnose. [stirling-pdf-2.4.1.exe.zip](https://github.com/user-attachments/files/24846696/stirling-pdf-2.4.1.exe.zip) <img width="967" height="362" alt="image" src="https://github.com/user-attachments/assets/8cd2a7d9-47ca-4caf-930b-4ec0a4c6317a" /> [stirling-pdf-2.4.0.exe.zip](https://github.com/user-attachments/files/24846864/stirling-pdf-2.4.0.exe.zip) <img width="951" height="395" alt="image" src="https://github.com/user-attachments/assets/70ba15eb-ec13-4737-9cae-1f6da3c18c1a" /> --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details.
This commit is contained in:
@@ -1,15 +1,32 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Paper, Stack, Switch, Text, Tooltip, NumberInput, SegmentedControl, Code, Group, Anchor, ActionIcon, Button, Badge, Alert } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePreferences } from '@app/contexts/PreferencesContext';
|
||||
import { useAppConfig } from '@app/contexts/AppConfigContext';
|
||||
import type { ToolPanelMode } from '@app/constants/toolPanel';
|
||||
import LocalIcon from '@app/components/shared/LocalIcon';
|
||||
import { updateService, UpdateSummary } from '@app/services/updateService';
|
||||
import UpdateModal from '@app/components/shared/UpdateModal';
|
||||
import React, { useState, useEffect, useMemo } from "react";
|
||||
import {
|
||||
Paper,
|
||||
Stack,
|
||||
Switch,
|
||||
Text,
|
||||
Tooltip,
|
||||
NumberInput,
|
||||
SegmentedControl,
|
||||
Code,
|
||||
Group,
|
||||
Anchor,
|
||||
ActionIcon,
|
||||
Button,
|
||||
Badge,
|
||||
Alert,
|
||||
} from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { usePreferences } from "@app/contexts/PreferencesContext";
|
||||
import { useAppConfig } from "@app/contexts/AppConfigContext";
|
||||
import type { ToolPanelMode } from "@app/constants/toolPanel";
|
||||
import LocalIcon from "@app/components/shared/LocalIcon";
|
||||
import { updateService, UpdateSummary } from "@app/services/updateService";
|
||||
import UpdateModal from "@app/components/shared/UpdateModal";
|
||||
import { getVersion } from "@tauri-apps/api/app";
|
||||
import { isTauri } from "@tauri-apps/api/core";
|
||||
|
||||
const DEFAULT_AUTO_UNZIP_FILE_LIMIT = 4;
|
||||
const BANNER_DISMISSED_KEY = 'stirlingpdf_features_banner_dismissed';
|
||||
const BANNER_DISMISSED_KEY = "stirlingpdf_features_banner_dismissed";
|
||||
|
||||
interface GeneralSectionProps {
|
||||
hideTitle?: boolean;
|
||||
@@ -22,11 +39,15 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({ hideTitle = false }) =>
|
||||
const [fileLimitInput, setFileLimitInput] = useState<number | string>(preferences.autoUnzipFileLimit);
|
||||
const [bannerDismissed, setBannerDismissed] = useState(() => {
|
||||
// Check localStorage on mount
|
||||
return localStorage.getItem(BANNER_DISMISSED_KEY) === 'true';
|
||||
return localStorage.getItem(BANNER_DISMISSED_KEY) === "true";
|
||||
});
|
||||
const [updateSummary, setUpdateSummary] = useState<UpdateSummary | null>(null);
|
||||
const [updateModalOpened, setUpdateModalOpened] = useState(false);
|
||||
const [checkingUpdate, setCheckingUpdate] = useState(false);
|
||||
const [mismatchVersion, setMismatchVersion] = useState(false);
|
||||
const isTauriApp = useMemo(() => isTauri(), []);
|
||||
const [appVersion, setAppVersion] = useState<string | null>(null);
|
||||
const frontendVersionLabel = appVersion ?? t("common.loading", "Loading...");
|
||||
|
||||
// Sync local state with preference changes
|
||||
useEffect(() => {
|
||||
@@ -49,7 +70,7 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({ hideTitle = false }) =>
|
||||
const machineInfo = {
|
||||
machineType: config.machineType,
|
||||
activeSecurity: config.activeSecurity ?? false,
|
||||
licenseType: config.license ?? 'NORMAL',
|
||||
licenseType: config.license ?? "NORMAL",
|
||||
};
|
||||
|
||||
const summary = await updateService.getUpdateSummary(config.appVersion, machineInfo);
|
||||
@@ -68,67 +89,126 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({ hideTitle = false }) =>
|
||||
setCheckingUpdate(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isTauriApp) {
|
||||
setMismatchVersion(false);
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
const fetchFrontendVersion = async () => {
|
||||
try {
|
||||
const frontendVersion = await getVersion();
|
||||
if (!cancelled) {
|
||||
setAppVersion(frontendVersion);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[GeneralSection] Failed to fetch frontend version:", error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchFrontendVersion();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [isTauriApp]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isTauriApp) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!appVersion || !config?.appVersion) {
|
||||
setMismatchVersion(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (appVersion !== config.appVersion) {
|
||||
console.warn("[GeneralSection] Mismatch between Tauri version and AppConfig version:", {
|
||||
backendVersion: config.appVersion,
|
||||
frontendVersion: appVersion,
|
||||
});
|
||||
setMismatchVersion(true);
|
||||
} else {
|
||||
setMismatchVersion(false);
|
||||
}
|
||||
}, [isTauriApp, appVersion, config?.appVersion]);
|
||||
|
||||
// Check if login is disabled
|
||||
const loginDisabled = !config?.enableLogin;
|
||||
|
||||
const handleDismissBanner = () => {
|
||||
setBannerDismissed(true);
|
||||
localStorage.setItem(BANNER_DISMISSED_KEY, 'true');
|
||||
localStorage.setItem(BANNER_DISMISSED_KEY, "true");
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
{!hideTitle && (
|
||||
<div>
|
||||
<Text fw={600} size="lg">{t('settings.general.title', 'General')}</Text>
|
||||
<Text fw={600} size="lg">
|
||||
{t("settings.general.title", "General")}
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t('settings.general.description', 'Configure general application preferences.')}
|
||||
{t("settings.general.description", "Configure general application preferences.")}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loginDisabled && !bannerDismissed && (
|
||||
<Paper withBorder p="md" radius="md" style={{ background: 'var(--mantine-color-blue-0)', position: 'relative' }}>
|
||||
<Paper withBorder p="md" radius="md" style={{ background: "var(--mantine-color-blue-0)", position: "relative" }}>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
size="sm"
|
||||
style={{ position: 'absolute', top: '0.5rem', right: '0.5rem' }}
|
||||
style={{ position: "absolute", top: "0.5rem", right: "0.5rem" }}
|
||||
onClick={handleDismissBanner}
|
||||
aria-label={t('settings.general.enableFeatures.dismiss', 'Dismiss')}
|
||||
aria-label={t("settings.general.enableFeatures.dismiss", "Dismiss")}
|
||||
>
|
||||
<LocalIcon icon="close-rounded" width="1rem" height="1rem" />
|
||||
</ActionIcon>
|
||||
<Stack gap="sm">
|
||||
<Group gap="xs">
|
||||
<LocalIcon icon="admin-panel-settings-rounded" width="1.2rem" height="1.2rem" style={{ color: 'var(--mantine-color-blue-6)' }} />
|
||||
<Text fw={600} size="sm" style={{ color: 'var(--mantine-color-blue-9)' }}>
|
||||
{t('settings.general.enableFeatures.title', 'For System Administrators')}
|
||||
<LocalIcon
|
||||
icon="admin-panel-settings-rounded"
|
||||
width="1.2rem"
|
||||
height="1.2rem"
|
||||
style={{ color: "var(--mantine-color-blue-6)" }}
|
||||
/>
|
||||
<Text fw={600} size="sm" style={{ color: "var(--mantine-color-blue-9)" }}>
|
||||
{t("settings.general.enableFeatures.title", "For System Administrators")}
|
||||
</Text>
|
||||
</Group>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t('settings.general.enableFeatures.intro', 'Enable user authentication, team management, and workspace features for your organization.')}
|
||||
{t(
|
||||
"settings.general.enableFeatures.intro",
|
||||
"Enable user authentication, team management, and workspace features for your organization.",
|
||||
)}
|
||||
</Text>
|
||||
<Group gap="xs" wrap="wrap">
|
||||
<Text size="sm" c="dimmed">
|
||||
{t('settings.general.enableFeatures.action', 'Configure')}
|
||||
{t("settings.general.enableFeatures.action", "Configure")}
|
||||
</Text>
|
||||
<Code>SECURITY_ENABLELOGIN=true</Code>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t('settings.general.enableFeatures.and', 'and')}
|
||||
{t("settings.general.enableFeatures.and", "and")}
|
||||
</Text>
|
||||
<Code>DISABLE_ADDITIONAL_FEATURES=false</Code>
|
||||
</Group>
|
||||
<Text size="xs" c="dimmed" fs="italic">
|
||||
{t('settings.general.enableFeatures.benefit', 'Enables user roles, team collaboration, admin controls, and enterprise features.')}
|
||||
{t(
|
||||
"settings.general.enableFeatures.benefit",
|
||||
"Enables user roles, team collaboration, admin controls, and enterprise features.",
|
||||
)}
|
||||
</Text>
|
||||
<Anchor
|
||||
href="https://docs.stirlingpdf.com/Configuration/System%20and%20Security/"
|
||||
target="_blank"
|
||||
size="sm"
|
||||
style={{ color: 'var(--mantine-color-blue-6)' }}
|
||||
style={{ color: "var(--mantine-color-blue-6)" }}
|
||||
>
|
||||
{t('settings.general.enableFeatures.learnMore', 'Learn more in documentation')} →
|
||||
{t("settings.general.enableFeatures.learnMore", "Learn more in documentation")} →
|
||||
</Anchor>
|
||||
</Stack>
|
||||
</Paper>
|
||||
@@ -142,36 +222,52 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({ hideTitle = false }) =>
|
||||
<Group justify="space-between" align="center">
|
||||
<div>
|
||||
<Text fw={600} size="sm">
|
||||
{t('settings.general.updates.title', 'Software Updates')}
|
||||
{t("settings.general.updates.title", "Software Updates")}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('settings.general.updates.description', 'Check for updates and view version information')}
|
||||
{t("settings.general.updates.description", "Check for updates and view version information")}
|
||||
</Text>
|
||||
</div>
|
||||
{updateSummary && (
|
||||
<Badge
|
||||
color={updateSummary.max_priority === 'urgent' ? 'red' : 'blue'}
|
||||
variant="filled"
|
||||
>
|
||||
{updateSummary.max_priority === 'urgent'
|
||||
? t('update.urgentUpdateAvailable', 'Urgent Update')
|
||||
: t('update.updateAvailable', 'Update Available')}
|
||||
<Badge color={updateSummary.max_priority === "urgent" ? "red" : "blue"} variant="filled">
|
||||
{updateSummary.max_priority === "urgent"
|
||||
? t("update.urgentUpdateAvailable", "Urgent Update")
|
||||
: t("update.updateAvailable", "Update Available")}
|
||||
</Badge>
|
||||
)}
|
||||
</Group>
|
||||
</div>
|
||||
|
||||
{isTauriApp && (
|
||||
<Group justify="space-between" align="center">
|
||||
<div>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t("settings.general.updates.currentFrontendVersion", "Current Frontend Version")}:{" "}
|
||||
<Text component="span" fw={500}>
|
||||
{frontendVersionLabel}
|
||||
</Text>
|
||||
</Text>
|
||||
{mismatchVersion && (
|
||||
<Text size="sm" c="red" mt={4}>
|
||||
{t(
|
||||
"settings.general.updates.versionMismatch",
|
||||
"Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version.",
|
||||
)}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
</Group>
|
||||
)}
|
||||
<Group justify="space-between" align="center">
|
||||
<div>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t('settings.general.updates.currentVersion', 'Current Version')}:{' '}
|
||||
{t("settings.general.updates.currentBackendVersion", "Current Backend Version")}:{" "}
|
||||
<Text component="span" fw={500}>
|
||||
{config.appVersion}
|
||||
</Text>
|
||||
</Text>
|
||||
{updateSummary && (
|
||||
<Text size="sm" c="dimmed" mt={4}>
|
||||
{t('settings.general.updates.latestVersion', 'Latest Version')}:{' '}
|
||||
{t("settings.general.updates.latestVersion", "Latest Version")}:{" "}
|
||||
<Text component="span" fw={500} c="blue">
|
||||
{updateSummary.latest_version}
|
||||
</Text>
|
||||
@@ -186,16 +282,16 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({ hideTitle = false }) =>
|
||||
loading={checkingUpdate}
|
||||
leftSection={<LocalIcon icon="refresh-rounded" width="1rem" height="1rem" />}
|
||||
>
|
||||
{t('settings.general.updates.checkForUpdates', 'Check for Updates')}
|
||||
{t("settings.general.updates.checkForUpdates", "Check for Updates")}
|
||||
</Button>
|
||||
{updateSummary && (
|
||||
<Button
|
||||
size="sm"
|
||||
color={updateSummary.max_priority === 'urgent' ? 'red' : 'blue'}
|
||||
color={updateSummary.max_priority === "urgent" ? "red" : "blue"}
|
||||
onClick={() => setUpdateModalOpened(true)}
|
||||
leftSection={<LocalIcon icon="system-update-alt-rounded" width="1rem" height="1rem" />}
|
||||
>
|
||||
{t('settings.general.updates.viewDetails', 'View Details')}
|
||||
{t("settings.general.updates.viewDetails", "View Details")}
|
||||
</Button>
|
||||
)}
|
||||
</Group>
|
||||
@@ -204,15 +300,15 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({ hideTitle = false }) =>
|
||||
{updateSummary?.any_breaking && (
|
||||
<Alert
|
||||
color="orange"
|
||||
title={t('update.breakingChangesDetected', 'Breaking Changes Detected')}
|
||||
title={t("update.breakingChangesDetected", "Breaking Changes Detected")}
|
||||
styles={{
|
||||
title: { fontWeight: 600 }
|
||||
title: { fontWeight: 600 },
|
||||
}}
|
||||
>
|
||||
<Text size="sm">
|
||||
{t(
|
||||
'update.breakingChangesMessage',
|
||||
'Some versions contain breaking changes. Please review the migration guides before updating.'
|
||||
"update.breakingChangesMessage",
|
||||
"Some versions contain breaking changes. Please review the migration guides before updating.",
|
||||
)}
|
||||
</Text>
|
||||
</Alert>
|
||||
@@ -223,87 +319,102 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({ hideTitle = false }) =>
|
||||
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<Stack gap="md">
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">
|
||||
{t('settings.general.defaultToolPickerMode', 'Default tool picker mode')}
|
||||
{t("settings.general.defaultToolPickerMode", "Default tool picker mode")}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('settings.general.defaultToolPickerModeDescription', 'Choose whether the tool picker opens in fullscreen or sidebar by default')}
|
||||
{t(
|
||||
"settings.general.defaultToolPickerModeDescription",
|
||||
"Choose whether the tool picker opens in fullscreen or sidebar by default",
|
||||
)}
|
||||
</Text>
|
||||
</div>
|
||||
<SegmentedControl
|
||||
value={preferences.defaultToolPanelMode}
|
||||
onChange={(val: string) => updatePreference('defaultToolPanelMode', val as ToolPanelMode)}
|
||||
onChange={(val: string) => updatePreference("defaultToolPanelMode", val as ToolPanelMode)}
|
||||
data={[
|
||||
{ label: t('settings.general.mode.sidebar', 'Sidebar'), value: 'sidebar' },
|
||||
{ label: t('settings.general.mode.fullscreen', 'Fullscreen'), value: 'fullscreen' },
|
||||
{ label: t("settings.general.mode.sidebar", "Sidebar"), value: "sidebar" },
|
||||
{ label: t("settings.general.mode.fullscreen", "Fullscreen"), value: "fullscreen" },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">
|
||||
{t('settings.general.hideUnavailableTools', 'Hide unavailable tools')}
|
||||
{t("settings.general.hideUnavailableTools", "Hide unavailable tools")}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('settings.general.hideUnavailableToolsDescription', 'Remove tools that have been disabled by your server instead of showing them greyed out.')}
|
||||
{t(
|
||||
"settings.general.hideUnavailableToolsDescription",
|
||||
"Remove tools that have been disabled by your server instead of showing them greyed out.",
|
||||
)}
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={preferences.hideUnavailableTools}
|
||||
onChange={(event) => updatePreference('hideUnavailableTools', event.currentTarget.checked)}
|
||||
onChange={(event) => updatePreference("hideUnavailableTools", event.currentTarget.checked)}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">
|
||||
{t('settings.general.hideUnavailableConversions', 'Hide unavailable conversions')}
|
||||
{t("settings.general.hideUnavailableConversions", "Hide unavailable conversions")}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('settings.general.hideUnavailableConversionsDescription', 'Remove disabled conversion options in the Convert tool instead of showing them greyed out.')}
|
||||
{t(
|
||||
"settings.general.hideUnavailableConversionsDescription",
|
||||
"Remove disabled conversion options in the Convert tool instead of showing them greyed out.",
|
||||
)}
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={preferences.hideUnavailableConversions}
|
||||
onChange={(event) => updatePreference('hideUnavailableConversions', event.currentTarget.checked)}
|
||||
onChange={(event) => updatePreference("hideUnavailableConversions", event.currentTarget.checked)}
|
||||
/>
|
||||
</div>
|
||||
<Tooltip
|
||||
label={t('settings.general.autoUnzipTooltip', 'Automatically extract ZIP files returned from API operations. Disable to keep ZIP files intact. This does not affect automation workflows.')}
|
||||
label={t(
|
||||
"settings.general.autoUnzipTooltip",
|
||||
"Automatically extract ZIP files returned from API operations. Disable to keep ZIP files intact. This does not affect automation workflows.",
|
||||
)}
|
||||
multiline
|
||||
w={300}
|
||||
withArrow
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', cursor: 'help' }}>
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", cursor: "help" }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">
|
||||
{t('settings.general.autoUnzip', 'Auto-unzip API responses')}
|
||||
{t("settings.general.autoUnzip", "Auto-unzip API responses")}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('settings.general.autoUnzipDescription', 'Automatically extract files from ZIP responses')}
|
||||
{t("settings.general.autoUnzipDescription", "Automatically extract files from ZIP responses")}
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={preferences.autoUnzip}
|
||||
onChange={(event) => updatePreference('autoUnzip', event.currentTarget.checked)}
|
||||
onChange={(event) => updatePreference("autoUnzip", event.currentTarget.checked)}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip
|
||||
label={t('settings.general.autoUnzipFileLimitTooltip', 'Only unzip if the ZIP contains this many files or fewer. Set higher to extract larger ZIPs.')}
|
||||
label={t(
|
||||
"settings.general.autoUnzipFileLimitTooltip",
|
||||
"Only unzip if the ZIP contains this many files or fewer. Set higher to extract larger ZIPs.",
|
||||
)}
|
||||
multiline
|
||||
w={300}
|
||||
withArrow
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', cursor: 'help' }}>
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", cursor: "help" }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">
|
||||
{t('settings.general.autoUnzipFileLimit', 'Auto-unzip file limit')}
|
||||
{t("settings.general.autoUnzipFileLimit", "Auto-unzip file limit")}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('settings.general.autoUnzipFileLimitDescription', 'Maximum number of files to extract from ZIP')}
|
||||
{t("settings.general.autoUnzipFileLimitDescription", "Maximum number of files to extract from ZIP")}
|
||||
</Text>
|
||||
</div>
|
||||
<NumberInput
|
||||
@@ -311,9 +422,12 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({ hideTitle = false }) =>
|
||||
onChange={setFileLimitInput}
|
||||
onBlur={() => {
|
||||
const numValue = Number(fileLimitInput);
|
||||
const finalValue = (!fileLimitInput || isNaN(numValue) || numValue < 1 || numValue > 100) ? DEFAULT_AUTO_UNZIP_FILE_LIMIT : numValue;
|
||||
const finalValue =
|
||||
!fileLimitInput || isNaN(numValue) || numValue < 1 || numValue > 100
|
||||
? DEFAULT_AUTO_UNZIP_FILE_LIMIT
|
||||
: numValue;
|
||||
setFileLimitInput(finalValue);
|
||||
updatePreference('autoUnzipFileLimit', finalValue);
|
||||
updatePreference("autoUnzipFileLimit", finalValue);
|
||||
}}
|
||||
min={1}
|
||||
max={100}
|
||||
@@ -336,7 +450,7 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({ hideTitle = false }) =>
|
||||
machineInfo={{
|
||||
machineType: config.machineType,
|
||||
activeSecurity: config.activeSecurity ?? false,
|
||||
licenseType: config.license ?? 'NORMAL',
|
||||
licenseType: config.license ?? "NORMAL",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user