Files
igny8/frontend/src/store/settingsStore.ts

253 lines
8.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Settings Store (Zustand)
* Manages account and module settings
*/
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import {
fetchAccountSettings,
fetchAccountSetting,
createAccountSetting,
updateAccountSetting,
deleteAccountSetting,
fetchModuleSettings,
createModuleSetting,
updateModuleSetting,
fetchModuleEnableSettings,
updateModuleEnableSettings,
AccountSetting,
ModuleSetting,
ModuleEnableSettings,
AccountSettingsError,
} from '../services/api';
const getAccountSettingsErrorMessage = (error: AccountSettingsError): string => {
switch (error.type) {
case 'ACCOUNT_SETTINGS_NOT_FOUND':
return 'No account-level settings have been created yet.';
case 'ACCOUNT_SETTINGS_VALIDATION_ERROR':
return error.message || 'The account settings request contained invalid data.';
default:
return error.message || 'Unexpected error while loading account settings.';
}
};
interface SettingsState {
accountSettings: Record<string, AccountSetting>;
moduleSettings: Record<string, Record<string, ModuleSetting>>;
moduleEnableSettings: ModuleEnableSettings | null;
loading: boolean;
error: string | null;
// Actions
loadAccountSettings: () => Promise<void>;
loadAccountSetting: (key: string) => Promise<void>;
updateAccountSetting: (key: string, value: any) => Promise<void>;
loadModuleSettings: (moduleName: string) => Promise<void>;
updateModuleSetting: (moduleName: string, key: string, value: any) => Promise<void>;
loadModuleEnableSettings: () => Promise<void>;
updateModuleEnableSettings: (data: Partial<ModuleEnableSettings>) => Promise<void>;
isModuleEnabled: (moduleName: string) => boolean;
reset: () => void;
}
export const useSettingsStore = create<SettingsState>()(
persist<SettingsState>(
(set, get) => ({
accountSettings: {},
moduleSettings: {},
moduleEnableSettings: null,
loading: false,
error: null,
_moduleEnableLastFetched: 0 as number | undefined,
_moduleEnableInFlight: null as Promise<ModuleEnableSettings> | null,
loadAccountSettings: async () => {
set({ loading: true, error: null });
try {
const response = await fetchAccountSettings();
const settingsMap: Record<string, AccountSetting> = {};
response.results.forEach(setting => {
settingsMap[setting.key] = setting;
});
set({ accountSettings: settingsMap, loading: false });
} catch (error: any) {
if (error instanceof AccountSettingsError) {
const message = getAccountSettingsErrorMessage(error);
// Not found should not be treated as failure just indicate no settings yet
if (error.type === 'ACCOUNT_SETTINGS_NOT_FOUND') {
set({ accountSettings: {}, loading: false, error: null });
} else {
set({ error: message, loading: false });
}
} else {
set({ error: error.message, loading: false });
}
}
},
loadAccountSetting: async (key: string) => {
try {
const setting = await fetchAccountSetting(key);
set(state => ({
accountSettings: { ...state.accountSettings, [key]: setting }
}));
} catch (error: any) {
if (error instanceof AccountSettingsError) {
if (error.type === 'ACCOUNT_SETTINGS_NOT_FOUND') {
// Silently ignore missing setting it just hasn't been created yet
return;
}
set({ error: getAccountSettingsErrorMessage(error) });
return;
}
set({ error: error.message });
}
},
updateAccountSetting: async (key: string, value: any) => {
set({ loading: true, error: null });
try {
const existing = get().accountSettings[key];
let setting: AccountSetting;
if (existing) {
setting = await updateAccountSetting(key, { config: value });
} else {
setting = await createAccountSetting({ key, config: value });
}
set(state => ({
accountSettings: { ...state.accountSettings, [key]: setting },
loading: false
}));
} catch (error: any) {
if (error instanceof AccountSettingsError) {
const message = getAccountSettingsErrorMessage(error);
set({ error: message, loading: false });
throw error;
}
set({ error: error.message, loading: false });
throw error;
}
},
loadModuleSettings: async (moduleName: string) => {
set({ loading: true, error: null });
try {
const settings = await fetchModuleSettings(moduleName);
const settingsMap: Record<string, ModuleSetting> = {};
settings.forEach(setting => {
settingsMap[setting.key] = setting;
});
set(state => ({
moduleSettings: {
...state.moduleSettings,
[moduleName]: settingsMap
},
loading: false
}));
} catch (error: any) {
set({ error: error.message, loading: false });
}
},
updateModuleSetting: async (moduleName: string, key: string, value: any) => {
set({ loading: true, error: null });
try {
const existing = get().moduleSettings[moduleName]?.[key];
let setting: ModuleSetting;
if (existing) {
setting = await updateModuleSetting(moduleName, key, { config: value });
} else {
setting = await createModuleSetting({ module_name: moduleName, key, config: value });
}
set(state => ({
moduleSettings: {
...state.moduleSettings,
[moduleName]: {
...(state.moduleSettings[moduleName] || {}),
[key]: setting
}
},
loading: false
}));
} catch (error: any) {
set({ error: error.message, loading: false });
throw error;
}
},
loadModuleEnableSettings: async () => {
const state = get() as any;
const now = Date.now();
// Use cached value if fetched within last 60s
if (state.moduleEnableSettings && state._moduleEnableLastFetched && now - state._moduleEnableLastFetched < 60000) {
return;
}
// Coalesce concurrent calls
if (state._moduleEnableInFlight) {
await state._moduleEnableInFlight;
return;
}
set({ loading: true, error: null });
try {
const inFlight = fetchModuleEnableSettings();
(state as any)._moduleEnableInFlight = inFlight;
const settings = await inFlight;
set({ moduleEnableSettings: settings, loading: false, _moduleEnableLastFetched: Date.now() });
} catch (error: any) {
// On 429/403, avoid loops; cache the failure timestamp and do not retry automatically
if (error?.status === 429 || error?.status === 403) {
set({ loading: false, _moduleEnableLastFetched: Date.now() });
return;
}
set({ error: error.message, loading: false, _moduleEnableLastFetched: Date.now() });
} finally {
(get() as any)._moduleEnableInFlight = null;
}
},
updateModuleEnableSettings: async (data: Partial<ModuleEnableSettings>) => {
set({ loading: true, error: null });
try {
const settings = await updateModuleEnableSettings(data);
set({ moduleEnableSettings: settings, loading: false });
} catch (error: any) {
set({ error: error.message, loading: false });
throw error;
}
},
isModuleEnabled: (moduleName: string): boolean => {
const settings = get().moduleEnableSettings;
if (!settings) return true; // Default to enabled if not loaded
const enabledKey = `${moduleName}_enabled` as keyof ModuleEnableSettings;
return settings[enabledKey] !== false; // Default to true if not set
},
reset: () => {
set({
accountSettings: {},
moduleSettings: {},
moduleEnableSettings: null,
loading: false,
error: null,
});
},
}),
{
name: 'settings-storage',
partialize: (state) => ({
accountSettings: state.accountSettings,
moduleSettings: state.moduleSettings,
moduleEnableSettings: state.moduleEnableSettings,
}),
}
)
);