26506a5027
Check Cross-Plugin Imports / check (push) Has been cancelled
- 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
137 lines
4.2 KiB
TypeScript
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 }),
|
|
}
|
|
)
|
|
);
|