Files
leocrm/frontend/src/store/workspaceStore.ts
T
Agent Zero 26506a5027
Check Cross-Plugin Imports / check (push) Has been cancelled
feat(N3): Backend respektiert X-Workspace-ID bei Listen — contacts/dms/mail/calendar (#367)
- Core-Resolver resolve_workspace_scope(): Zuweisungs-Check, leere Werte fallen weg; Exemptions System-Admin + workspaces:configure_modules (Editor-Deadlock)
- require_workspace_scope(module_key) FastAPI-Dependency (deps.py)
- expand_folder_scope(): Ordner-Subtree (zyklensicher) für ContactFolder + DMS Folder; scope_uuid_set() fail-closed
- contacts: folder_ids-Subtree + contact_types auf GET /contacts, List-Cache bei aktivem Scope deaktiviert (Cache-Leak-Gefahr)
- dms: folder_ids-Subtree + file_types (semantische Matcher) auf /files, Baum-Reduktion auf /folders
- mail: account_ids auf /mails, /threads, /accounts
- calendar: calendar_ids auf /calendar/entries, /calendars
- Frontend-Defaults: getModuleConfig() im workspaceStore, ContactsList default_saved_view_id, Calendar default_view
- Tests: 21/21 neu (TDD rot→grün), Regression 81 passed, Checker 0, tsc clean, Vitest grün, Build OK
2026-09-01 10:27:23 +02:00

137 lines
4.2 KiB
TypeScript

import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import { setActiveWorkspaceId as setApiClientWorkspaceId } from '@/api/client';
export interface WorkspaceModuleConfig {
module_key: string;
is_visible: boolean;
menu_order: number;
config: Record<string, any>;
}
export interface WorkspaceWidgetConfig {
id: string;
widget_key: string;
position_x: number;
position_y: number;
width: number;
height: number;
config: Record<string, any>;
}
export interface WorkspaceInfo {
id: string;
name: string;
icon: string;
description: string | null;
is_default: boolean;
is_active: boolean;
role?: 'member' | 'manager' | 'admin';
is_user_default?: boolean;
modules?: WorkspaceModuleConfig[];
}
export interface WorkspaceContextState {
workspace_id: string | null;
name?: string;
icon?: string;
role?: string;
modules: { module_key: string; menu_order: number; config: Record<string, any>; is_visible?: boolean }[];
widgets: WorkspaceWidgetConfig[];
error?: string;
}
interface WorkspaceStoreState {
// Active workspace ID (per-tab via sessionStorage)
activeWorkspaceId: string | null;
// Cached context
context: WorkspaceContextState | null;
// Available workspaces for the user
myWorkspaces: WorkspaceInfo[];
// Loading states
isLoading: boolean;
// Actions
setActiveWorkspace: (id: string | null) => void;
setContext: (ctx: WorkspaceContextState | null) => void;
setMyWorkspaces: (workspaces: WorkspaceInfo[]) => void;
setLoading: (loading: boolean) => void;
// Helpers
isModuleVisible: (moduleKey: string, isSystemAdmin?: boolean) => boolean;
visibleModuleKeys: () => Set<string>;
getModuleConfig: (moduleKey: string) => Record<string, any>;
hasWorkspaces: () => boolean;
// Reset
reset: () => void;
}
const SESSION_STORAGE_KEY = 'leocrm-active-workspace';
export const useWorkspaceStore = create<WorkspaceStoreState>()(
persist(
(set, get) => ({
activeWorkspaceId: sessionStorage.getItem(SESSION_STORAGE_KEY),
context: null,
myWorkspaces: [],
isLoading: false,
setActiveWorkspace: (id) => {
if (id) {
sessionStorage.setItem(SESSION_STORAGE_KEY, id);
} else {
sessionStorage.removeItem(SESSION_STORAGE_KEY);
}
setApiClientWorkspaceId(id);
set({ activeWorkspaceId: id, context: null });
},
setContext: (ctx) => set({ context: ctx }),
setMyWorkspaces: (workspaces) => set({ myWorkspaces: workspaces }),
setLoading: (loading) => set({ isLoading: loading }),
isModuleVisible: (moduleKey: string, isSystemAdmin?: boolean) => {
// System admins see all modules regardless of workspace
if (isSystemAdmin) return true;
// While permissions are loading, show nothing (fail-closed)
if (isSystemAdmin === undefined) return false;
// If no workspace context, show all (backward compatible)
const ctx = get().context;
if (!ctx?.workspace_id || !ctx?.modules?.length) return true;
return get().visibleModuleKeys().has(moduleKey);
},
visibleModuleKeys: () => {
const ctx = get().context;
// ARCH-004: respect is_visible=false from workspace module config.
// Modules without the flag stay visible (backward compatible).
return new Set(
(ctx?.modules ?? [])
.filter(m => m.is_visible !== false)
.map(m => m.module_key)
);
},
getModuleConfig: (moduleKey: string) => {
// N3: scope defaults (e.g. default_saved_view_id, default_view) come
// from the active workspace context; no context = no defaults.
const ctx = get().context;
const mod = (ctx?.modules ?? []).find(m => m.module_key === moduleKey);
return mod?.config ?? {};
},
hasWorkspaces: () => get().myWorkspaces.length > 0,
reset: () => {
sessionStorage.removeItem(SESSION_STORAGE_KEY);
set({ activeWorkspaceId: null, context: null, myWorkspaces: [], isLoading: false });
},
}),
{
name: 'leocrm-workspace-store',
storage: createJSONStorage(() => sessionStorage),
partialize: (state) => ({ activeWorkspaceId: state.activeWorkspaceId }),
}
)
);