fix(client): fix debounce on settings page causing annoying bug

This commit is contained in:
perf3ct 2025-06-14 16:28:27 +00:00
parent c13eda12e0
commit 003d90943c
1 changed files with 100 additions and 18 deletions

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { import {
Box, Box,
Container, Container,
@ -128,6 +128,29 @@ interface WebDAVTabContentProps {
onShowSnackbar: (message: string, severity: 'success' | 'error' | 'warning' | 'info') => void; onShowSnackbar: (message: string, severity: 'success' | 'error' | 'warning' | 'info') => void;
} }
// Debounce utility function
function useDebounce<T extends (...args: any[]) => any>(func: T, delay: number): T {
const timeoutRef = React.useRef<NodeJS.Timeout | null>(null);
const debouncedFunc = useCallback((...args: Parameters<T>) => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => func(...args), delay);
}, [func, delay]) as T;
// Cleanup on unmount
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);
return debouncedFunc;
}
const WebDAVTabContent: React.FC<WebDAVTabContentProps> = ({ const WebDAVTabContent: React.FC<WebDAVTabContentProps> = ({
settings, settings,
loading, loading,
@ -140,8 +163,64 @@ const WebDAVTabContent: React.FC<WebDAVTabContentProps> = ({
const [estimatingCrawl, setEstimatingCrawl] = useState(false); const [estimatingCrawl, setEstimatingCrawl] = useState(false);
const [newFolder, setNewFolder] = useState(''); const [newFolder, setNewFolder] = useState('');
// Local state for input fields to prevent focus loss
const [localWebdavServerUrl, setLocalWebdavServerUrl] = useState(settings.webdavServerUrl);
const [localWebdavUsername, setLocalWebdavUsername] = useState(settings.webdavUsername);
const [localWebdavPassword, setLocalWebdavPassword] = useState(settings.webdavPassword);
const [localSyncInterval, setLocalSyncInterval] = useState(settings.webdavSyncIntervalMinutes);
// Update local state when settings change from outside (like initial load)
useEffect(() => {
setLocalWebdavServerUrl(settings.webdavServerUrl);
setLocalWebdavUsername(settings.webdavUsername);
setLocalWebdavPassword(settings.webdavPassword);
setLocalSyncInterval(settings.webdavSyncIntervalMinutes);
}, [settings.webdavServerUrl, settings.webdavUsername, settings.webdavPassword, settings.webdavSyncIntervalMinutes]);
// Debounced update functions
const debouncedUpdateServerUrl = useDebounce((value: string) => {
onSettingsChange('webdavServerUrl', value);
}, 500);
const debouncedUpdateUsername = useDebounce((value: string) => {
onSettingsChange('webdavUsername', value);
}, 500);
const debouncedUpdatePassword = useDebounce((value: string) => {
onSettingsChange('webdavPassword', value);
}, 500);
const debouncedUpdateSyncInterval = useDebounce((value: number) => {
onSettingsChange('webdavSyncIntervalMinutes', value);
}, 500);
// Input change handlers
const handleServerUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setLocalWebdavServerUrl(value);
debouncedUpdateServerUrl(value);
};
const handleUsernameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setLocalWebdavUsername(value);
debouncedUpdateUsername(value);
};
const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setLocalWebdavPassword(value);
debouncedUpdatePassword(value);
};
const handleSyncIntervalChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = parseInt(e.target.value);
setLocalSyncInterval(value);
debouncedUpdateSyncInterval(value);
};
const testConnection = async () => { const testConnection = async () => {
if (!settings.webdavServerUrl || !settings.webdavUsername || !settings.webdavPassword) { if (!localWebdavServerUrl || !localWebdavUsername || !localWebdavPassword) {
onShowSnackbar('Please fill in all WebDAV connection details', 'warning'); onShowSnackbar('Please fill in all WebDAV connection details', 'warning');
return; return;
} }
@ -149,9 +228,9 @@ const WebDAVTabContent: React.FC<WebDAVTabContentProps> = ({
setTestingConnection(true); setTestingConnection(true);
try { try {
const response = await api.post('/webdav/test-connection', { const response = await api.post('/webdav/test-connection', {
server_url: settings.webdavServerUrl, server_url: localWebdavServerUrl,
username: settings.webdavUsername, username: localWebdavUsername,
password: settings.webdavPassword, password: localWebdavPassword,
server_type: 'nextcloud' server_type: 'nextcloud'
}); });
setConnectionResult(response.data); setConnectionResult(response.data);
@ -249,8 +328,8 @@ const WebDAVTabContent: React.FC<WebDAVTabContentProps> = ({
<TextField <TextField
fullWidth fullWidth
label="Server URL" label="Server URL"
value={settings.webdavServerUrl} value={localWebdavServerUrl}
onChange={(e) => onSettingsChange('webdavServerUrl', e.target.value)} onChange={handleServerUrlChange}
disabled={loading} disabled={loading}
placeholder="https://cloud.example.com" placeholder="https://cloud.example.com"
helperText="Full URL to your WebDAV server" helperText="Full URL to your WebDAV server"
@ -276,8 +355,8 @@ const WebDAVTabContent: React.FC<WebDAVTabContentProps> = ({
<TextField <TextField
fullWidth fullWidth
label="Username" label="Username"
value={settings.webdavUsername} value={localWebdavUsername}
onChange={(e) => onSettingsChange('webdavUsername', e.target.value)} onChange={handleUsernameChange}
disabled={loading} disabled={loading}
/> />
</Grid> </Grid>
@ -286,8 +365,8 @@ const WebDAVTabContent: React.FC<WebDAVTabContentProps> = ({
fullWidth fullWidth
label="Password / App Password" label="Password / App Password"
type="password" type="password"
value={settings.webdavPassword} value={localWebdavPassword}
onChange={(e) => onSettingsChange('webdavPassword', e.target.value)} onChange={handlePasswordChange}
disabled={loading} disabled={loading}
helperText="For Nextcloud/ownCloud, use an app password" helperText="For Nextcloud/ownCloud, use an app password"
/> />
@ -364,8 +443,8 @@ const WebDAVTabContent: React.FC<WebDAVTabContentProps> = ({
fullWidth fullWidth
type="number" type="number"
label="Sync Interval (minutes)" label="Sync Interval (minutes)"
value={settings.webdavSyncIntervalMinutes} value={localSyncInterval}
onChange={(e) => onSettingsChange('webdavSyncIntervalMinutes', parseInt(e.target.value))} onChange={handleSyncIntervalChange}
disabled={loading} disabled={loading}
inputProps={{ min: 15, max: 1440 }} inputProps={{ min: 15, max: 1440 }}
helperText="How often to check for new files" helperText="How often to check for new files"
@ -618,7 +697,6 @@ const SettingsPage: React.FC = () => {
}; };
const handleSettingsChange = async (key: keyof Settings, value: any): Promise<void> => { const handleSettingsChange = async (key: keyof Settings, value: any): Promise<void> => {
setLoading(true);
try { try {
// Convert camelCase to snake_case for API // Convert camelCase to snake_case for API
const snakeCase = (str: string): string => str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`); const snakeCase = (str: string): string => str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
@ -628,13 +706,17 @@ const SettingsPage: React.FC = () => {
const updatePayload = { [apiKey]: value }; const updatePayload = { [apiKey]: value };
await api.put('/settings', updatePayload); await api.put('/settings', updatePayload);
setSettings({ ...settings, [key]: value });
// Only update state after successful API call
setSettings(prevSettings => ({ ...prevSettings, [key]: value }));
// Only show success message for non-text inputs to reduce noise
if (typeof value !== 'string') {
showSnackbar('Settings updated successfully', 'success'); showSnackbar('Settings updated successfully', 'success');
}
} catch (error) { } catch (error) {
console.error('Error updating settings:', error); console.error('Error updating settings:', error);
showSnackbar('Failed to update settings', 'error'); showSnackbar('Failed to update settings', 'error');
} finally {
setLoading(false);
} }
}; };