Refactor settings system and improve context initialization
Backend: - Add type validation and coercion for settings API - Implement SettingStorage and SettingType in registry - Improve CRUD operations for settings Frontend: - Refactor Theme, Language, Sidebar, ViewMode contexts - Simplify admin components (GeneralTab, SettingsTab, UsersTab) - Add new settings endpoints to API client - Improve App initialization flow Infrastructure: - Update Dockerfile and docker-compose.yml - Add .dockerignore - Update Makefile and README 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { createContext, useContext, useState, useEffect, useCallback } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useAuth } from './AuthContext';
|
||||
import { settingsAPI } from '../api/client';
|
||||
|
||||
export type SidebarMode = 'collapsed' | 'expanded' | 'toggle' | 'dynamic';
|
||||
|
||||
@@ -23,47 +24,57 @@ const SidebarContext = createContext<SidebarContextType | undefined>(undefined);
|
||||
|
||||
export function SidebarProvider({ children }: { children: ReactNode }) {
|
||||
const { token } = useAuth();
|
||||
const [userCollapsed, setUserCollapsed] = useState(true);
|
||||
const [userCollapsed, setUserCollapsed] = useState(() => {
|
||||
const key = 'user_sidebar_collapsed';
|
||||
const saved = localStorage.getItem(key);
|
||||
if (saved === 'true') return true;
|
||||
if (saved === 'false') return false;
|
||||
|
||||
// Backward-compatibility: migrate legacy key
|
||||
const legacy = localStorage.getItem('sidebarCollapsed');
|
||||
if (legacy === 'true' || legacy === 'false') {
|
||||
localStorage.setItem(key, legacy);
|
||||
localStorage.removeItem('sidebarCollapsed');
|
||||
return legacy === 'true';
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
const [isMobileOpen, setIsMobileOpen] = useState(false);
|
||||
const [sidebarMode, setSidebarModeState] = useState<SidebarMode>('toggle');
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const [showLogo, setShowLogo] = useState(false);
|
||||
|
||||
// Load sidebar mode from backend
|
||||
useEffect(() => {
|
||||
const loadSidebarMode = async () => {
|
||||
try {
|
||||
if (!token) {
|
||||
setSidebarModeState('toggle');
|
||||
setShowLogo(false);
|
||||
return;
|
||||
}
|
||||
useEffect(() => {
|
||||
const loadSidebarMode = async () => {
|
||||
try {
|
||||
if (!token) {
|
||||
setSidebarModeState('toggle');
|
||||
setShowLogo(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch('/api/v1/settings/ui', {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const parseBool = (val: any, defaultVal: boolean): boolean => {
|
||||
if (val === undefined || val === null) return defaultVal;
|
||||
if (val === true || val === 'true' || val === 'True' || val === 1 || val === '1') return true;
|
||||
if (val === false || val === 'false' || val === 'False' || val === 0 || val === '0') return false;
|
||||
return defaultVal;
|
||||
};
|
||||
const data = await settingsAPI.getUi();
|
||||
const parseBool = (val: any, defaultVal: boolean): boolean => {
|
||||
if (val === undefined || val === null) return defaultVal;
|
||||
if (val === true || val === 'true' || val === 'True' || val === 1 || val === '1') return true;
|
||||
if (val === false || val === 'false' || val === 'False' || val === 0 || val === '0') return false;
|
||||
return defaultVal;
|
||||
};
|
||||
|
||||
if (data.sidebar_mode && ['collapsed', 'expanded', 'toggle', 'dynamic'].includes(data.sidebar_mode)) {
|
||||
setSidebarModeState(data.sidebar_mode as SidebarMode);
|
||||
}
|
||||
if (data.show_logo !== undefined) {
|
||||
setShowLogo(parseBool(data.show_logo, false));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load sidebar mode:', error);
|
||||
}
|
||||
};
|
||||
loadSidebarMode();
|
||||
}, [token]);
|
||||
if (data.sidebar_mode && ['collapsed', 'expanded', 'toggle', 'dynamic'].includes(data.sidebar_mode as string)) {
|
||||
setSidebarModeState(data.sidebar_mode as SidebarMode);
|
||||
}
|
||||
if (data.show_logo !== undefined) {
|
||||
setShowLogo(parseBool(data.show_logo, false));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load sidebar mode:', error);
|
||||
}
|
||||
};
|
||||
loadSidebarMode();
|
||||
}, [token]);
|
||||
|
||||
// Compute isCollapsed based on mode
|
||||
const isCollapsed = sidebarMode === 'collapsed' ? true :
|
||||
@@ -76,7 +87,11 @@ export function SidebarProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
const toggleCollapse = () => {
|
||||
if (canToggle) {
|
||||
setUserCollapsed((prev) => !prev);
|
||||
setUserCollapsed((prev) => {
|
||||
const next = !prev;
|
||||
localStorage.setItem('user_sidebar_collapsed', String(next));
|
||||
return next;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -88,25 +103,15 @@ export function SidebarProvider({ children }: { children: ReactNode }) {
|
||||
setIsMobileOpen(false);
|
||||
};
|
||||
|
||||
const setSidebarMode = useCallback(async (mode: SidebarMode) => {
|
||||
try {
|
||||
if (!token) return;
|
||||
const response = await fetch('/api/v1/settings/sidebar_mode', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ value: mode }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setSidebarModeState(mode);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save sidebar mode:', error);
|
||||
}
|
||||
}, [token]);
|
||||
const setSidebarMode = useCallback(async (mode: SidebarMode) => {
|
||||
try {
|
||||
if (!token) return;
|
||||
await settingsAPI.updateSetting('sidebar_mode', mode);
|
||||
setSidebarModeState(mode);
|
||||
} catch (error) {
|
||||
console.error('Failed to save sidebar mode:', error);
|
||||
}
|
||||
}, [token]);
|
||||
|
||||
return (
|
||||
<SidebarContext.Provider
|
||||
|
||||
Reference in New Issue
Block a user