Forráskód Böngészése

Feature: Admin Set Default Nav-Menu Order (#761)

Feature: Admin Set Default Nav-Menu Order (#761)
Thomas Rambach 2 hónapja
szülő
commit
2cac7d0172

+ 14 - 0
backend/app/api/routes/settings.py

@@ -220,6 +220,20 @@ async def reset_settings(
     return DEFAULT_SETTINGS
 
 
+@router.get("/default-sidebar-order")
+async def get_default_sidebar_order(
+    db: AsyncSession = Depends(get_db),
+):
+    """Get the admin-set default sidebar order.
+
+    Intentionally unauthenticated: non-admin users need to read this value to apply
+    the default sidebar order, but may lack SETTINGS_READ permission.
+    The value is non-sensitive (sidebar item IDs only).
+    """
+    value = await get_setting(db, "default_sidebar_order")
+    return {"default_sidebar_order": value or ""}
+
+
 @router.get("/check-ffmpeg")
 async def check_ffmpeg():
     """Check if ffmpeg is installed and available."""

+ 29 - 1
backend/app/schemas/settings.py

@@ -1,4 +1,6 @@
-from pydantic import BaseModel, Field
+import json
+
+from pydantic import BaseModel, Field, field_validator
 
 
 class AppSettings(BaseModel):
@@ -184,6 +186,12 @@ class AppSettings(BaseModel):
         description="Enable user email notifications for print job events (requires Advanced Authentication)",
     )
 
+    # Default sidebar order (admin-set for all users)
+    default_sidebar_order: str = Field(
+        default="",
+        description="JSON object with 'order' key containing array of sidebar item IDs (empty = no default)",
+    )
+
 
 class AppSettingsUpdate(BaseModel):
     """Schema for updating settings (all fields optional)."""
@@ -252,3 +260,23 @@ class AppSettingsUpdate(BaseModel):
     prometheus_token: str | None = None
     low_stock_threshold: float | None = Field(default=None, ge=0.1, le=99.9)
     user_notifications_enabled: bool | None = None
+    default_sidebar_order: str | None = None
+
+    @field_validator("default_sidebar_order")
+    @classmethod
+    def validate_default_sidebar_order(cls, v: str | None) -> str | None:
+        if v is None or v == "":
+            return v
+        try:
+            parsed = json.loads(v)
+        except json.JSONDecodeError:
+            raise ValueError("default_sidebar_order must be valid JSON or empty")
+        if isinstance(parsed, dict):
+            order = parsed.get("order")
+        elif isinstance(parsed, list):
+            order = parsed
+        else:
+            raise ValueError("default_sidebar_order must be a JSON object with 'order' key or a JSON array")
+        if not isinstance(order, list) or not all(isinstance(item, str) for item in order):
+            raise ValueError("sidebar order must be an array of strings")
+        return v

+ 3 - 0
frontend/src/api/client.ts

@@ -866,6 +866,8 @@ export interface AppSettings {
   low_stock_threshold: number;
   // User email notifications toggle
   user_notifications_enabled: boolean;
+  // Default sidebar order (admin-set for all users)
+  default_sidebar_order: string;
 }
 
 export type AppSettingsUpdate = Partial<AppSettings>;
@@ -3159,6 +3161,7 @@ export const api = {
 
   // Settings
   getSettings: () => request<AppSettings>('/settings/'),
+  getDefaultSidebarOrder: () => request<{ default_sidebar_order: string }>('/settings/default-sidebar-order'),
   updateSettings: (data: AppSettingsUpdate) =>
     request<AppSettings>('/settings/', {
       method: 'PUT',

+ 33 - 0
frontend/src/components/Layout.tsx

@@ -117,6 +117,39 @@ export function Layout() {
     staleTime: 5 * 60 * 1000, // 5 minutes
   });
 
+  // Fetch default sidebar order via a public endpoint (no settings:read needed)
+  const { data: defaultSidebarData } = useQuery({
+    queryKey: ['default-sidebar-order'],
+    queryFn: api.getDefaultSidebarOrder,
+    staleTime: 5 * 60 * 1000, // 5 minutes
+  });
+
+  // Apply admin default sidebar order once per user (skipped if already applied).
+  // Uses a per-user localStorage flag to prevent re-application.
+  useEffect(() => {
+    const defaultOrder = defaultSidebarData?.default_sidebar_order;
+    if (!defaultOrder) return;
+    // Wait for auth state to settle before applying to avoid double-execution
+    if (authEnabled && !user) return;
+    const appliedKey = user ? `sidebarDefaultApplied_${user.id}` : 'sidebarDefaultApplied';
+    if (localStorage.getItem(appliedKey)) return;
+    try {
+      const parsed = JSON.parse(defaultOrder);
+      const orderArr = Array.isArray(parsed) ? parsed : parsed.order;
+      if (!Array.isArray(orderArr) || orderArr.length === 0) return;
+      // Filter to valid sidebar item IDs only
+      const validIds = new Set(defaultNavItems.map(i => i.id));
+      const filtered = orderArr.filter((id: string) => typeof id === 'string' && (validIds.has(id) || isExternalLinkId(id)));
+      if (filtered.length > 0) {
+        setSidebarOrder(filtered);
+        saveSidebarOrder(filtered);
+        localStorage.setItem(appliedKey, '1');
+      }
+    } catch (e) {
+      console.error('Failed to apply default sidebar order:', e);
+    }
+  }, [defaultSidebarData?.default_sidebar_order, setSidebarOrder, user, authEnabled]);
+
   // Check advanced auth status for conditional nav items
   const { data: advancedAuthStatus } = useQuery({
     queryKey: ['advancedAuthStatus'],

+ 5 - 0
frontend/src/i18n/locales/de.ts

@@ -1730,6 +1730,11 @@ export default {
     slicerBambuStudio: 'Bambu Studio',
     slicerOrcaSlicer: 'OrcaSlicer',
     sidebarOrderDescription: 'Elemente in der Seitenleiste per Drag & Drop neu anordnen. Hier auf Standardreihenfolge zurücksetzen.',
+    setDefault: 'Standard setzen',
+    sidebarOrderSetDefaultHint: 'Standard setzen übernimmt die aktuelle Menüreihenfolge für Benutzer, die ihre noch nicht angepasst haben.',
+    sidebarDefaultSet: 'Standard-Menüreihenfolge wurde festgelegt.',
+    sidebarDefaultCleared: 'Standard-Menüreihenfolge gelöscht.',
+    sidebarDefaultFailed: 'Festlegen der Standard-Menüreihenfolge fehlgeschlagen.',
     reset: 'Zurücksetzen',
     // Appearance
     darkMode: 'Dunkelmodus',

+ 5 - 0
frontend/src/i18n/locales/en.ts

@@ -1730,6 +1730,11 @@ export default {
     slicerBambuStudio: 'Bambu Studio',
     slicerOrcaSlicer: 'OrcaSlicer',
     sidebarOrderDescription: 'Drag items in the sidebar to reorder. Reset to default order here.',
+    setDefault: 'Set Default',
+    sidebarOrderSetDefaultHint: 'Set default applies the current menu order to users who haven\'t customized theirs.',
+    sidebarDefaultSet: 'Default menu order has been set.',
+    sidebarDefaultCleared: 'Default menu order cleared.',
+    sidebarDefaultFailed: 'Failed to set default menu order.',
     reset: 'Reset',
     // Appearance
     darkMode: 'Dark Mode',

+ 5 - 0
frontend/src/i18n/locales/fr.ts

@@ -1730,6 +1730,11 @@ export default {
     slicerBambuStudio: 'Bambu Studio',
     slicerOrcaSlicer: 'OrcaSlicer',
     sidebarOrderDescription: 'Glissez les éléments dans la barre latérale pour réorganiser. Réinitialiser l\'ordre par défaut ici.',
+    setDefault: 'Définir par défaut',
+    sidebarOrderSetDefaultHint: 'Définir par défaut applique l\'ordre actuel du menu aux utilisateurs qui n\'ont pas encore personnalisé le leur.',
+    sidebarDefaultSet: 'L\'ordre du menu par défaut a été défini.',
+    sidebarDefaultCleared: 'Ordre du menu par défaut effacé.',
+    sidebarDefaultFailed: 'Échec de la définition de l\'ordre du menu par défaut.',
     reset: 'Réinitialiser',
     darkMode: 'Mode sombre',
     lightMode: 'Mode clair',

+ 5 - 0
frontend/src/i18n/locales/it.ts

@@ -1729,6 +1729,11 @@ export default {
     slicerBambuStudio: 'Bambu Studio',
     slicerOrcaSlicer: 'OrcaSlicer',
     sidebarOrderDescription: 'Trascina gli elementi nella barra laterale per riordinare. Ripristina l\'ordine predefinito qui.',
+    setDefault: 'Imposta predefinito',
+    sidebarOrderSetDefaultHint: 'Imposta predefinito applica l\'ordine attuale del menu agli utenti che non hanno ancora personalizzato il proprio.',
+    sidebarDefaultSet: 'L\'ordine predefinito del menu è stato impostato.',
+    sidebarDefaultCleared: 'Ordine predefinito del menu cancellato.',
+    sidebarDefaultFailed: 'Impossibile impostare l\'ordine predefinito del menu.',
     reset: 'Ripristina',
     darkMode: 'Modalità scura',
     lightMode: 'Modalità chiara',

+ 5 - 0
frontend/src/i18n/locales/ja.ts

@@ -1729,6 +1729,11 @@ export default {
     slicerBambuStudio: 'Bambu Studio',
     slicerOrcaSlicer: 'OrcaSlicer',
     sidebarOrderDescription: 'サイドバーの項目をドラッグして並べ替え。ここでデフォルトの順序にリセット。',
+    setDefault: 'デフォルト設定',
+    sidebarOrderSetDefaultHint: 'デフォルト設定は、まだカスタマイズしていないユーザーに現在のメニュー順序を適用します。',
+    sidebarDefaultSet: 'デフォルトメニュー順序を設定しました。',
+    sidebarDefaultCleared: 'デフォルトメニュー順序をクリアしました。',
+    sidebarDefaultFailed: 'デフォルトメニュー順序の設定に失敗しました。',
     reset: 'リセット',
     // Appearance
     darkMode: 'ダークモード',

+ 5 - 0
frontend/src/i18n/locales/pt-BR.ts

@@ -1729,6 +1729,11 @@ export default {
     slicerBambuStudio: 'Bambu Studio',
     slicerOrcaSlicer: 'OrcaSlicer',
     sidebarOrderDescription: 'Arraste itens na barra lateral para reordenar. Restaurar ordem padrão aqui.',
+    setDefault: 'Definir padrão',
+    sidebarOrderSetDefaultHint: 'Definir padrão aplica a ordem atual do menu aos usuários que ainda não personalizaram o seu.',
+    sidebarDefaultSet: 'Ordem padrão do menu foi definida.',
+    sidebarDefaultCleared: 'Ordem padrão do menu removida.',
+    sidebarDefaultFailed: 'Falha ao definir a ordem padrão do menu.',
     reset: 'Redefinir',
     darkMode: 'Modo escuro',
     lightMode: 'Modo claro',

+ 5 - 0
frontend/src/i18n/locales/zh-CN.ts

@@ -1729,6 +1729,11 @@ export default {
     slicerBambuStudio: 'Bambu Studio',
     slicerOrcaSlicer: 'OrcaSlicer',
     sidebarOrderDescription: '拖拽侧边栏项目以重新排序。在此处重置为默认顺序。',
+    setDefault: '设为默认',
+    sidebarOrderSetDefaultHint: '设为默认将当前菜单顺序应用于尚未自定义的用户。',
+    sidebarDefaultSet: '已设置默认菜单顺序。',
+    sidebarDefaultCleared: '已清除默认菜单顺序。',
+    sidebarDefaultFailed: '设置默认菜单顺序失败。',
     reset: '重置',
     darkMode: '深色模式',
     lightMode: '浅色模式',

+ 54 - 11
frontend/src/pages/SettingsPage.tsx

@@ -25,6 +25,7 @@ import { VirtualPrinterList } from '../components/VirtualPrinterList';
 import { GitHubBackupSettings } from '../components/GitHubBackupSettings';
 import { EmailSettings } from '../components/EmailSettings';
 import { APIBrowser } from '../components/APIBrowser';
+import { Toggle } from '../components/Toggle';
 import { virtualPrinterApi } from '../api/client';
 import { defaultNavItems, getDefaultView, setDefaultView } from '../components/Layout';
 import { availableLanguages } from '../i18n';
@@ -75,7 +76,7 @@ export function SettingsPage() {
   const [searchParams, setSearchParams] = useSearchParams();
   const { t, i18n } = useTranslation();
   const { showToast } = useToast();
-  const { authEnabled, user, refreshAuth } = useAuth();
+  const { authEnabled, user, refreshAuth, hasPermission } = useAuth();
   const {
     mode,
     darkStyle, darkBackground, darkAccent,
@@ -179,6 +180,37 @@ export function SettingsPage() {
     window.location.reload();
   };
 
+  const isDefaultSidebarEnabled = !!localSettings?.default_sidebar_order;
+
+  const handleToggleDefaultSidebarOrder = async (enabled: boolean) => {
+    try {
+      if (enabled) {
+        let orderArr: string[];
+        const stored = localStorage.getItem('sidebarOrder');
+        try {
+          orderArr = stored ? JSON.parse(stored) : defaultNavItems.map(i => i.id);
+        } catch {
+          orderArr = defaultNavItems.map(i => i.id);
+        }
+        if (!Array.isArray(orderArr) || orderArr.length === 0) {
+          orderArr = defaultNavItems.map(i => i.id);
+        }
+        const payload = JSON.stringify({ order: orderArr });
+        await api.updateSettings({ default_sidebar_order: payload });
+        setLocalSettings(prev => prev ? { ...prev, default_sidebar_order: payload } : prev);
+        showToast(t('settings.sidebarDefaultSet'), 'success');
+      } else {
+        await api.updateSettings({ default_sidebar_order: '' });
+        setLocalSettings(prev => prev ? { ...prev, default_sidebar_order: '' } : prev);
+        showToast(t('settings.sidebarDefaultCleared'), 'success');
+      }
+      queryClient.invalidateQueries({ queryKey: ['settings'] });
+      queryClient.invalidateQueries({ queryKey: ['default-sidebar-order'] });
+    } catch {
+      showToast(t('settings.sidebarDefaultFailed'), 'error');
+    }
+  };
+
   const { data: settings, isLoading } = useQuery({
     queryKey: ['settings'],
     queryFn: api.getSettings,
@@ -376,8 +408,6 @@ export function SettingsPage() {
   });
 
   // User management queries and mutations
-  const { hasPermission } = useAuth();
-
   const { data: usersData = [], isLoading: usersLoading } = useQuery({
     queryKey: ['users'],
     queryFn: () => api.getUsers(),
@@ -1177,16 +1207,29 @@ export function SettingsPage() {
                   <p className="text-white">{t('settings.sidebarOrder')}</p>
                   <p className="text-sm text-bambu-gray">
                     {t('settings.sidebarOrderDescription')}
+                    {authEnabled && hasPermission('settings:update') && ` ${t('settings.sidebarOrderSetDefaultHint')}`}
                   </p>
                 </div>
-                <Button
-                  variant="secondary"
-                  size="sm"
-                  onClick={handleResetSidebarOrder}
-                >
-                  <RotateCcw className="w-4 h-4" />
-                  {t('settings.reset')}
-                </Button>
+                <div className="flex items-center gap-2 shrink-0">
+                  <Button
+                    variant="secondary"
+                    size="sm"
+                    onClick={handleResetSidebarOrder}
+                  >
+                    <RotateCcw className="w-4 h-4" />
+                    {t('settings.reset')}
+                  </Button>
+                  {authEnabled && hasPermission('settings:update') && (
+                    <div className="flex items-center gap-2">
+                      <span className="text-sm text-bambu-gray whitespace-nowrap">{t('settings.setDefault')}</span>
+                      <Toggle
+                        checked={isDefaultSidebarEnabled}
+                        onChange={handleToggleDefaultSidebarOrder}
+                        disabled={isLoading}
+                      />
+                    </div>
+                  )}
+                </div>
               </div>
             </CardContent>
           </Card>

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 0 - 0
static/assets/index-BI_XE14_.js


+ 1 - 1
static/index.html

@@ -23,7 +23,7 @@
 
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-Dwo_5Mjd.js"></script>
+    <script type="module" crossorigin src="/assets/index-BI_XE14_.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-DI9VPacx.css">
   </head>
   <body>

Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott