mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-15 11:00:47 +02:00
settings menu reworks (#5864)
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
import { Button, Group, Text } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface SettingsStickyFooterProps {
|
||||
isDirty: boolean;
|
||||
saving: boolean;
|
||||
loginEnabled: boolean;
|
||||
onSave: () => void;
|
||||
onDiscard: () => void;
|
||||
}
|
||||
|
||||
export function SettingsStickyFooter({
|
||||
isDirty,
|
||||
saving,
|
||||
loginEnabled,
|
||||
onSave,
|
||||
onDiscard,
|
||||
}: SettingsStickyFooterProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!isDirty || !loginEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="settings-sticky-footer">
|
||||
<Group justify="space-between" w="100%">
|
||||
<Text size="sm" c="dimmed">
|
||||
{t('admin.settings.unsavedChanges.hint', 'You have unsaved changes')}
|
||||
</Text>
|
||||
<Group gap="sm">
|
||||
<Button variant="default" onClick={onDiscard} size="sm">
|
||||
{t('admin.settings.discard', 'Discard')}
|
||||
</Button>
|
||||
<Button onClick={onSave} loading={saving} size="sm">
|
||||
{t('admin.settings.save', 'Save Changes')}
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -23,7 +23,9 @@ interface ProviderCardProps {
|
||||
settings?: Record<string, any>;
|
||||
onSave?: (settings: Record<string, any>) => void;
|
||||
onDisconnect?: () => void;
|
||||
onChange?: (settings: Record<string, any>) => void;
|
||||
disabled?: boolean;
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
export default function ProviderCard({
|
||||
@@ -32,18 +34,19 @@ export default function ProviderCard({
|
||||
settings = {},
|
||||
onSave,
|
||||
onDisconnect,
|
||||
onChange,
|
||||
disabled = false,
|
||||
readOnly = false,
|
||||
}: ProviderCardProps) {
|
||||
const { t } = useTranslation();
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [localSettings, setLocalSettings] = useState<Record<string, any>>(settings);
|
||||
|
||||
// Keep local settings in sync with incoming settings (values loaded from settings.yml)
|
||||
// Update whenever parent settings change, whether expanded or not (important for Discard to work)
|
||||
useEffect(() => {
|
||||
if (!expanded) {
|
||||
setLocalSettings(settings);
|
||||
}
|
||||
}, [settings, expanded]);
|
||||
setLocalSettings(settings);
|
||||
}, [settings]);
|
||||
|
||||
// Initialize local settings with defaults when opening an unconfigured provider
|
||||
const handleConnectToggle = () => {
|
||||
@@ -63,7 +66,12 @@ export default function ProviderCard({
|
||||
|
||||
const handleFieldChange = (key: string, value: any) => {
|
||||
if (disabled) return; // Block changes when disabled
|
||||
setLocalSettings((prev) => ({ ...prev, [key]: value }));
|
||||
const updated = { ...localSettings, [key]: value };
|
||||
setLocalSettings(updated);
|
||||
// Notify parent of changes if onChange callback provided
|
||||
if (onChange) {
|
||||
onChange(updated);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
@@ -225,22 +233,26 @@ export default function ProviderCard({
|
||||
<Stack gap="md" mt="xs">
|
||||
{provider.fields.map((field) => renderField(field))}
|
||||
|
||||
<Group justify="flex-end" mt="sm">
|
||||
{onDisconnect && (
|
||||
<Button
|
||||
variant="outline"
|
||||
color="red"
|
||||
size="sm"
|
||||
onClick={onDisconnect}
|
||||
disabled={disabled}
|
||||
>
|
||||
{t('admin.settings.connections.disconnect', 'Disconnect')}
|
||||
</Button>
|
||||
)}
|
||||
<Button size="sm" onClick={handleSave} disabled={disabled}>
|
||||
{t('admin.settings.save', 'Save Changes')}
|
||||
</Button>
|
||||
</Group>
|
||||
{!readOnly && (onSave || onDisconnect) && (
|
||||
<Group justify="flex-end" mt="sm">
|
||||
{onDisconnect && (
|
||||
<Button
|
||||
variant="outline"
|
||||
color="red"
|
||||
size="sm"
|
||||
onClick={onDisconnect}
|
||||
disabled={disabled}
|
||||
>
|
||||
{t('admin.settings.connections.disconnect', 'Disconnect')}
|
||||
</Button>
|
||||
)}
|
||||
{onSave && (
|
||||
<Button size="sm" onClick={handleSave} disabled={disabled}>
|
||||
{t('admin.settings.save', 'Save Changes')}
|
||||
</Button>
|
||||
)}
|
||||
</Group>
|
||||
)}
|
||||
</Stack>
|
||||
</Collapse>
|
||||
</Stack>
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import { useUnsavedChanges } from '@app/contexts/UnsavedChangesContext';
|
||||
|
||||
interface UseSettingsDirtyReturn<T> {
|
||||
isDirty: boolean;
|
||||
resetToSnapshot: () => T;
|
||||
markSaved: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for managing dirty state in settings sections
|
||||
* Handles JSON snapshot comparison and UnsavedChangesContext integration
|
||||
*/
|
||||
export function useSettingsDirty<T>(settings: T, loading: boolean): UseSettingsDirtyReturn<T> {
|
||||
const { setIsDirty } = useUnsavedChanges();
|
||||
const [originalSettingsSnapshot, setOriginalSettingsSnapshot] = useState<string>('');
|
||||
const [isDirty, setLocalIsDirty] = useState(false);
|
||||
const isInitialLoad = useRef(true);
|
||||
const justSavedRef = useRef(false);
|
||||
|
||||
// Snapshot original settings after initial load OR after successful save (when refetch completes)
|
||||
useEffect(() => {
|
||||
if (loading || Object.keys(settings as Record<string, unknown>).length === 0) return;
|
||||
|
||||
// After initial load: set snapshot
|
||||
if (isInitialLoad.current) {
|
||||
setOriginalSettingsSnapshot(JSON.stringify(settings));
|
||||
isInitialLoad.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// After save: update snapshot to new server state so dirty tracking is accurate
|
||||
if (justSavedRef.current) {
|
||||
setOriginalSettingsSnapshot(JSON.stringify(settings));
|
||||
setLocalIsDirty(false);
|
||||
setIsDirty(false);
|
||||
justSavedRef.current = false;
|
||||
}
|
||||
}, [loading, settings, setIsDirty]);
|
||||
|
||||
// Track dirty state by comparing current settings to snapshot
|
||||
useEffect(() => {
|
||||
if (!originalSettingsSnapshot || loading) return;
|
||||
|
||||
const currentSnapshot = JSON.stringify(settings);
|
||||
const dirty = currentSnapshot !== originalSettingsSnapshot;
|
||||
setLocalIsDirty(dirty);
|
||||
setIsDirty(dirty);
|
||||
}, [settings, originalSettingsSnapshot, loading, setIsDirty]);
|
||||
|
||||
// Clean up dirty state on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
setIsDirty(false);
|
||||
};
|
||||
}, [setIsDirty]);
|
||||
|
||||
const resetToSnapshot = (): T => {
|
||||
if (originalSettingsSnapshot) {
|
||||
try {
|
||||
return JSON.parse(originalSettingsSnapshot) as T;
|
||||
} catch (e) {
|
||||
console.error('Failed to parse original settings:', e);
|
||||
return settings;
|
||||
}
|
||||
}
|
||||
return settings;
|
||||
};
|
||||
|
||||
const markSaved = () => {
|
||||
justSavedRef.current = true;
|
||||
};
|
||||
|
||||
return {
|
||||
isDirty,
|
||||
resetToSnapshot,
|
||||
markSaved,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user