fix(client): fix debounce on settings page causing annoying bug
This commit is contained in:
parent
c13eda12e0
commit
003d90943c
|
|
@ -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 });
|
|
||||||
showSnackbar('Settings updated successfully', 'success');
|
// 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');
|
||||||
|
}
|
||||||
} 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);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue