[feature] Reset config settings

This commit is contained in:
PauI Ostrovckij
2025-11-19 11:32:48 +03:00
parent 8c0c1ef66d
commit d897080a4d
5 changed files with 205 additions and 44 deletions

View File

@ -1,6 +1,6 @@
import {useState, useRef} from 'react'; import {useState, useRef, useEffect} from 'react';
import {useSelector, useDispatch} from 'react-redux'; import {useSelector, useDispatch} from 'react-redux';
import {saveConfig, selectConfig} from '../../store/slices/configSlice'; import {saveConfig, resetConfig, selectConfig} from '../../store/slices/configSlice';
import {getNestedValue} from '../../utils/getNestedValue'; import {getNestedValue} from '../../utils/getNestedValue';
import {mergeNestedObjects} from '../../utils/mergeNestedObjects'; import {mergeNestedObjects} from '../../utils/mergeNestedObjects';
import {useFieldValidation} from '../../hooks/useFieldValidation'; import {useFieldValidation} from '../../hooks/useFieldValidation';
@ -8,7 +8,7 @@ import PageHeader from '../../components/PageHeader/PageHeader';
import PageDescription from '../../components/PageDescription/PageDescription'; import PageDescription from '../../components/PageDescription/PageDescription';
import Tabs from '../../components/Tabs/Tabs'; import Tabs from '../../components/Tabs/Tabs';
import Input from '../../components/Input/Input'; import Input from '../../components/Input/Input';
import FixedSaveButton from '../../components/FixedSaveButton/FixedSaveButton'; import FixedSaveButtonGroup from '../../components/FixedSaveButtonGroup/FixedSaveButtonGroup';
import styles from './Expiration.module.scss'; import styles from './Expiration.module.scss';
const expirationTabs = [ const expirationTabs = [
@ -46,11 +46,12 @@ function Expiration() {
}; };
// Reset state and errors to global config // Reset state and errors to global config
const resetToGlobalConfig = () => { const resetToGlobalConfig = source => {
if (config) { const src = source || config;
if (src) {
const settings = {}; const settings = {};
Object.keys(CONFIG_PATHS).forEach(key => { Object.keys(CONFIG_PATHS).forEach(key => {
const value = getNestedValue(config, CONFIG_PATHS[key], ''); const value = getNestedValue(src, CONFIG_PATHS[key], '');
settings[key] = value; settings[key] = value;
}); });
setLocalSettings(settings); setLocalSettings(settings);
@ -74,6 +75,12 @@ function Expiration() {
hasInitialized.current = true; hasInitialized.current = true;
} }
// Sync from Redux when config changes (e.g., after reset), unless user has local edits
useEffect(() => {
if (config && !hasChanges) {
resetToGlobalConfig();
}
}, [config]); // eslint-disable-line react-hooks/exhaustive-deps
// Handle field changes // Handle field changes
const handleFieldChange = (field, value) => { const handleFieldChange = (field, value) => {
setLocalSettings(prev => ({ setLocalSettings(prev => ({
@ -129,6 +136,24 @@ function Expiration() {
setHasChanges(false); setHasChanges(false);
}; };
// Handle reset for active tab
const handleReset = async () => {
const confirmed = window.confirm('Reset settings on this tab to defaults?');
if (!confirmed) return;
try {
const paths =
activeTab === 'garbage-collection'
? [CONFIG_PATHS.filesCron, CONFIG_PATHS.documentsCron, CONFIG_PATHS.files, CONFIG_PATHS.filesremovedatonce]
: [CONFIG_PATHS.sessionidle, CONFIG_PATHS.sessionabsolute];
const merged = await dispatch(resetConfig(paths)).unwrap();
resetToGlobalConfig(merged);
setHasChanges(false);
} catch (e) {
console.error('Failed to reset expiration settings:', e);
alert('Failed to reset settings. Please try again.');
}
};
// Render tab content // Render tab content
const renderTabContent = () => { const renderTabContent = () => {
switch (activeTab) { switch (activeTab) {
@ -224,9 +249,20 @@ function Expiration() {
{renderTabContent()} {renderTabContent()}
</Tabs> </Tabs>
<FixedSaveButton onClick={handleSave} disabled={!hasChanges || hasValidationErrors()}> <FixedSaveButtonGroup
Save Changes buttons={[
</FixedSaveButton> {
text: 'Save Changes',
onClick: handleSave,
disabled: !hasChanges || hasValidationErrors()
},
{
text: 'Reset to Defaults',
onClick: handleReset,
disabled: false
}
]}
/>
</div> </div>
); );
} }

View File

@ -1,13 +1,13 @@
import {useState, useEffect} from 'react'; import {useState, useEffect} from 'react';
import {useSelector, useDispatch} from 'react-redux'; import {useSelector, useDispatch} from 'react-redux';
import {saveConfig, selectConfig} from '../../store/slices/configSlice'; import {saveConfig, resetConfig, selectConfig} from '../../store/slices/configSlice';
import {getNestedValue} from '../../utils/getNestedValue'; import {getNestedValue} from '../../utils/getNestedValue';
import {mergeNestedObjects} from '../../utils/mergeNestedObjects'; import {mergeNestedObjects} from '../../utils/mergeNestedObjects';
import {useFieldValidation} from '../../hooks/useFieldValidation'; import {useFieldValidation} from '../../hooks/useFieldValidation';
import PageHeader from '../../components/PageHeader/PageHeader'; import PageHeader from '../../components/PageHeader/PageHeader';
import PageDescription from '../../components/PageDescription/PageDescription'; import PageDescription from '../../components/PageDescription/PageDescription';
import Input from '../../components/Input/Input'; import Input from '../../components/Input/Input';
import FixedSaveButton from '../../components/FixedSaveButton/FixedSaveButton'; import FixedSaveButtonGroup from '../../components/FixedSaveButtonGroup/FixedSaveButtonGroup';
import styles from './FileLimits.module.scss'; import styles from './FileLimits.module.scss';
function FileLimits() { function FileLimits() {
@ -163,6 +163,20 @@ function FileLimits() {
setHasChanges(false); setHasChanges(false);
}; };
// Handle reset to defaults
const handleReset = async () => {
const confirmed = window.confirm('Reset file limits to defaults? This cannot be undone.');
if (!confirmed) return;
try {
await dispatch(resetConfig(['FileConverter.converter.maxDownloadBytes', 'FileConverter.converter.inputLimits'])).unwrap();
setHasChanges(false);
} catch (error) {
console.error('Error resetting file limits:', error);
alert('Failed to reset file limits. Please try again.');
}
};
return ( return (
<div className={`${styles.fileLimits} ${styles.pageWithFixedSave}`}> <div className={`${styles.fileLimits} ${styles.pageWithFixedSave}`}>
<PageHeader>File Size Limits</PageHeader> <PageHeader>File Size Limits</PageHeader>
@ -230,9 +244,20 @@ function FileLimits() {
</div> </div>
</div> </div>
<FixedSaveButton onClick={handleSave} disabled={!hasChanges || hasValidationErrors()}> <FixedSaveButtonGroup
Save Changes buttons={[
</FixedSaveButton> {
text: 'Save Changes',
onClick: handleSave,
disabled: !hasChanges || hasValidationErrors()
},
{
text: 'Reset to Defaults',
onClick: handleReset,
disabled: false
}
]}
/>
</div> </div>
); );
} }

View File

@ -1,11 +1,11 @@
import {useState, useRef} from 'react'; import {useState, useRef, useEffect} from 'react';
import {useDispatch, useSelector} from 'react-redux'; import {useDispatch, useSelector} from 'react-redux';
import {saveConfig, selectConfig} from '../../store/slices/configSlice'; import {saveConfig, resetConfig, selectConfig} from '../../store/slices/configSlice';
import {getNestedValue} from '../../utils/getNestedValue'; import {getNestedValue} from '../../utils/getNestedValue';
import {mergeNestedObjects} from '../../utils/mergeNestedObjects'; import {mergeNestedObjects} from '../../utils/mergeNestedObjects';
import {useFieldValidation} from '../../hooks/useFieldValidation'; import {useFieldValidation} from '../../hooks/useFieldValidation';
import Checkbox from '../../components/Checkbox/Checkbox'; import Checkbox from '../../components/Checkbox/Checkbox';
import FixedSaveButton from '../../components/FixedSaveButton/FixedSaveButton'; import FixedSaveButtonGroup from '../../components/FixedSaveButtonGroup/FixedSaveButtonGroup';
import PageHeader from '../../components/PageHeader/PageHeader'; import PageHeader from '../../components/PageHeader/PageHeader';
import PageDescription from '../../components/PageDescription/PageDescription'; import PageDescription from '../../components/PageDescription/PageDescription';
import styles from './RequestFiltering.module.scss'; import styles from './RequestFiltering.module.scss';
@ -28,12 +28,13 @@ function RequestFiltering() {
}; };
const hasInitialized = useRef(false); const hasInitialized = useRef(false);
const resetToGlobalConfig = () => { const resetToGlobalConfig = source => {
if (config) { const src = source || config;
if (src) {
const newSettings = {}; const newSettings = {};
Object.keys(CONFIG_PATHS).forEach(key => { Object.keys(CONFIG_PATHS).forEach(key => {
const path = CONFIG_PATHS[key]; const path = CONFIG_PATHS[key];
newSettings[key] = getNestedValue(config, path, false); newSettings[key] = getNestedValue(src, path, false);
}); });
setLocalSettings(newSettings); setLocalSettings(newSettings);
} }
@ -44,6 +45,12 @@ function RequestFiltering() {
hasInitialized.current = true; hasInitialized.current = true;
} }
// Sync from Redux when config changes (e.g., after reset), unless user has local edits
useEffect(() => {
if (config && !hasChanges) {
resetToGlobalConfig();
}
}, [config]); // eslint-disable-line react-hooks/exhaustive-deps
// Handle field changes // Handle field changes
const handleFieldChange = (field, value) => { const handleFieldChange = (field, value) => {
setLocalSettings(prev => ({ setLocalSettings(prev => ({
@ -82,6 +89,19 @@ function RequestFiltering() {
setHasChanges(false); setHasChanges(false);
}; };
const handleReset = async () => {
const confirmed = window.confirm('Reset request filtering settings to defaults?');
if (!confirmed) return;
try {
const merged = await dispatch(resetConfig(Object.values(CONFIG_PATHS))).unwrap();
resetToGlobalConfig(merged);
setHasChanges(false);
} catch (e) {
console.error('Failed to reset request filtering:', e);
alert('Failed to reset settings. Please try again.');
}
};
return ( return (
<div className={`${styles.requestFiltering} ${styles.pageWithFixedSave}`}> <div className={`${styles.requestFiltering} ${styles.pageWithFixedSave}`}>
<PageHeader>Request Filtering</PageHeader> <PageHeader>Request Filtering</PageHeader>
@ -114,9 +134,20 @@ function RequestFiltering() {
</div> </div>
</div> </div>
<FixedSaveButton onClick={handleSave} disabled={!hasChanges || hasValidationErrors()}> <FixedSaveButtonGroup
Save Changes buttons={[
</FixedSaveButton> {
text: 'Save Changes',
onClick: handleSave,
disabled: !hasChanges || hasValidationErrors()
},
{
text: 'Reset to Defaults',
onClick: handleReset,
disabled: false
}
]}
/>
</div> </div>
); );
} }

View File

@ -1,6 +1,6 @@
import {useState, useRef} from 'react'; import {useState, useRef, useEffect} from 'react';
import {useSelector, useDispatch} from 'react-redux'; import {useSelector, useDispatch} from 'react-redux';
import {saveConfig, selectConfig} from '../../store/slices/configSlice'; import {saveConfig, resetConfig, selectConfig} from '../../store/slices/configSlice';
import {getNestedValue} from '../../utils/getNestedValue'; import {getNestedValue} from '../../utils/getNestedValue';
import {mergeNestedObjects} from '../../utils/mergeNestedObjects'; import {mergeNestedObjects} from '../../utils/mergeNestedObjects';
import {useFieldValidation} from '../../hooks/useFieldValidation'; import {useFieldValidation} from '../../hooks/useFieldValidation';
@ -8,7 +8,7 @@ import PageHeader from '../../components/PageHeader/PageHeader';
import PageDescription from '../../components/PageDescription/PageDescription'; import PageDescription from '../../components/PageDescription/PageDescription';
import Tabs from '../../components/Tabs/Tabs'; import Tabs from '../../components/Tabs/Tabs';
import AccessRules from '../../components/AccessRules/AccessRules'; import AccessRules from '../../components/AccessRules/AccessRules';
import FixedSaveButton from '../../components/FixedSaveButton/FixedSaveButton'; import FixedSaveButtonGroup from '../../components/FixedSaveButtonGroup/FixedSaveButtonGroup';
import styles from './SecuritySettings.module.scss'; import styles from './SecuritySettings.module.scss';
const securityTabs = [{key: 'ip-filtering', label: 'IP Filtering'}]; const securityTabs = [{key: 'ip-filtering', label: 'IP Filtering'}];
@ -23,9 +23,10 @@ function SecuritySettings() {
const [hasChanges, setHasChanges] = useState(false); const [hasChanges, setHasChanges] = useState(false);
// Reset state and errors to global config // Reset state and errors to global config
const resetToGlobalConfig = () => { const resetToGlobalConfig = source => {
if (config) { const src = source || config;
const ipFilterRules = getNestedValue(config, 'services.CoAuthoring.ipfilter.rules', []); if (src) {
const ipFilterRules = getNestedValue(src, 'services.CoAuthoring.ipfilter.rules', []);
const uiRules = ipFilterRules.map(rule => ({ const uiRules = ipFilterRules.map(rule => ({
type: rule.allowed ? 'Allow' : 'Deny', type: rule.allowed ? 'Allow' : 'Deny',
value: rule.address value: rule.address
@ -50,6 +51,12 @@ function SecuritySettings() {
hasInitialized.current = true; hasInitialized.current = true;
} }
// Sync from Redux when config changes (e.g., after reset), unless user has local edits
useEffect(() => {
if (config && !hasChanges) {
resetToGlobalConfig();
}
}, [config]); // eslint-disable-line react-hooks/exhaustive-deps
// Handle rules changes // Handle rules changes
const handleRulesChange = newRules => { const handleRulesChange = newRules => {
setLocalRules(newRules); setLocalRules(newRules);
@ -85,6 +92,23 @@ function SecuritySettings() {
setHasChanges(false); setHasChanges(false);
}; };
const handleReset = async () => {
const confirmed = window.confirm('Reset security settings on this tab to defaults?');
if (!confirmed) return;
try {
// Only reset active tab settings
const paths = activeTab === 'ip-filtering' ? ['services.CoAuthoring.ipfilter.rules'] : [];
if (paths.length > 0) {
const merged = await dispatch(resetConfig(paths)).unwrap();
resetToGlobalConfig(merged);
}
setHasChanges(false);
} catch (e) {
console.error('Failed to reset security settings:', e);
alert('Failed to reset settings. Please try again.');
}
};
const renderTabContent = () => { const renderTabContent = () => {
switch (activeTab) { switch (activeTab) {
case 'ip-filtering': case 'ip-filtering':
@ -110,9 +134,20 @@ function SecuritySettings() {
{renderTabContent()} {renderTabContent()}
</Tabs> </Tabs>
<FixedSaveButton onClick={handleSave} disabled={!hasChanges || hasValidationErrors()}> <FixedSaveButtonGroup
Save Changes buttons={[
</FixedSaveButton> {
text: 'Save Changes',
onClick: handleSave,
disabled: !hasChanges || hasValidationErrors()
},
{
text: 'Reset to Defaults',
onClick: handleReset,
disabled: false
}
]}
/>
</div> </div>
); );
} }

View File

@ -1,6 +1,6 @@
import {useState, useRef} from 'react'; import {useState, useRef, useEffect} from 'react';
import {useSelector, useDispatch} from 'react-redux'; import {useSelector, useDispatch} from 'react-redux';
import {saveConfig, selectConfig, rotateWopiKeysAction} from '../../store/slices/configSlice'; import {saveConfig, resetConfig, selectConfig, rotateWopiKeysAction} from '../../store/slices/configSlice';
import {getNestedValue} from '../../utils/getNestedValue'; import {getNestedValue} from '../../utils/getNestedValue';
import {mergeNestedObjects} from '../../utils/mergeNestedObjects'; import {mergeNestedObjects} from '../../utils/mergeNestedObjects';
import {useFieldValidation} from '../../hooks/useFieldValidation'; import {useFieldValidation} from '../../hooks/useFieldValidation';
@ -10,7 +10,7 @@ import PageDescription from '../../components/PageDescription/PageDescription';
import ToggleSwitch from '../../components/ToggleSwitch/ToggleSwitch'; import ToggleSwitch from '../../components/ToggleSwitch/ToggleSwitch';
import Input from '../../components/Input/Input'; import Input from '../../components/Input/Input';
import Checkbox from '../../components/Checkbox/Checkbox'; import Checkbox from '../../components/Checkbox/Checkbox';
import FixedSaveButton from '../../components/FixedSaveButton/FixedSaveButton'; import FixedSaveButtonGroup from '../../components/FixedSaveButtonGroup/FixedSaveButtonGroup';
import Note from '../../components/Note/Note'; import Note from '../../components/Note/Note';
import styles from './WOPISettings.module.scss'; import styles from './WOPISettings.module.scss';
@ -31,14 +31,17 @@ function WOPISettings() {
const wopiPublicKey = getNestedValue(config, 'wopi.publicKey', ''); const wopiPublicKey = getNestedValue(config, 'wopi.publicKey', '');
const configRefreshLockInterval = getNestedValue(config, 'wopi.refreshLockInterval', '10m'); const configRefreshLockInterval = getNestedValue(config, 'wopi.refreshLockInterval', '10m');
const resetToGlobalConfig = () => { const resetToGlobalConfig = source => {
if (config) { const src = source || config;
setLocalWopiEnabled(configWopiEnabled); if (src) {
const srcEnabled = getNestedValue(src, 'wopi.enable');
const srcInterval = getNestedValue(src, 'wopi.refreshLockInterval');
setLocalWopiEnabled(srcEnabled);
setLocalRotateKeys(false); setLocalRotateKeys(false);
setLocalRefreshLockInterval(configRefreshLockInterval); setLocalRefreshLockInterval(srcInterval);
setHasChanges(false); setHasChanges(false);
validateField('wopi.enable', configWopiEnabled); validateField('wopi.enable', srcEnabled);
validateField('wopi.refreshLockInterval', configRefreshLockInterval); validateField('wopi.refreshLockInterval', srcInterval);
} }
}; };
@ -48,6 +51,12 @@ function WOPISettings() {
hasInitialized.current = true; hasInitialized.current = true;
} }
// Sync from Redux when config changes (e.g., after reset), unless user has local edits
useEffect(() => {
if (config && !hasChanges) {
resetToGlobalConfig();
}
}, [config]); // eslint-disable-line react-hooks/exhaustive-deps
const handleWopiEnabledChange = enabled => { const handleWopiEnabledChange = enabled => {
setLocalWopiEnabled(enabled); setLocalWopiEnabled(enabled);
// If WOPI is disabled, uncheck rotate keys // If WOPI is disabled, uncheck rotate keys
@ -118,6 +127,20 @@ function WOPISettings() {
} }
}; };
const handleReset = async () => {
const confirmed = window.confirm('Reset WOPI settings to defaults?');
if (!confirmed) return;
try {
const merged = await dispatch(resetConfig(['wopi.enable', 'wopi.refreshLockInterval'])).unwrap();
resetToGlobalConfig(merged);
setHasChanges(false);
setLocalRotateKeys(false);
} catch (e) {
console.error('Failed to reset WOPI settings:', e);
alert('Failed to reset settings. Please try again.');
}
};
return ( return (
<div className={`${styles.wopiSettings} ${styles.pageWithFixedSave}`}> <div className={`${styles.wopiSettings} ${styles.pageWithFixedSave}`}>
<PageHeader>WOPI Settings</PageHeader> <PageHeader>WOPI Settings</PageHeader>
@ -175,9 +198,20 @@ function WOPISettings() {
</> </>
)} )}
<FixedSaveButton onClick={handleSave} disabled={!hasChanges || hasValidationErrors()}> <FixedSaveButtonGroup
Save Changes buttons={[
</FixedSaveButton> {
text: 'Save Changes',
onClick: handleSave,
disabled: !hasChanges || hasValidationErrors()
},
{
text: 'Reset to Defaults',
onClick: handleReset,
disabled: false
}
]}
/>
</div> </div>
); );
} }