310a9f0542
Backend:
- Widget CRUD: get_widgets, create_widget, update_widget, delete_widget
- Manager role check: is_workspace_manager
- Cross-tenant validation: verify_user_same_tenant (UserTenant)
- Default workspace seeding: seed_default_workspace with 12 standard modules
- Set user default workspace: set_user_default_workspace
- Fix create_workspace default uniqueness (unset others before insert)
- Widget CRUD routes: GET/POST/PUT/DELETE /{workspace_id}/widgets
- Set-default route: POST /{workspace_id}/set-default
- Cross-tenant validation in assign_user route
Frontend:
- workspaceStore (Zustand): central state with sessionStorage persistence
- API client interceptor: X-Workspace-ID header on all requests
- useWorkspace hook refactored to use workspaceStore
- Widget API hooks: useWorkspaceWidgets, useCreateWorkspaceWidget, etc.
- useSetDefaultWorkspace hook
- Settings route: /settings/workspaces with WorkspaceManagerPage
- Settings nav item for Workspaces
Tests:
- 25 backend tests (CRUD, modules, widgets, users, manager, seeding, context, isolation)
- 12 frontend tests (workspaceStore state, visibility, persistence, reset)
- 48/48 backend tests passing
- 12/12 frontend tests passing
77 lines
2.9 KiB
TypeScript
77 lines
2.9 KiB
TypeScript
import { useEffect, useCallback } from 'react';
|
|
import { useMyWorkspaces, useWorkspaceContext } from '@/api/hooks/workspaces';
|
|
import { useWorkspaceStore, type WorkspaceInfo, type WorkspaceContextState } from '@/store/workspaceStore';
|
|
import { useAuthStore } from '@/store/authStore';
|
|
|
|
/**
|
|
* Manages the active workspace for the current browser tab.
|
|
*
|
|
* Uses the central workspaceStore (Zustand) for state management.
|
|
* sessionStorage provides per-tab persistence.
|
|
* X-Workspace-ID header is sent automatically by the API client interceptor.
|
|
*
|
|
* Workspaces are UI/navigation context only — they never affect permissions.
|
|
*/
|
|
export function useWorkspace() {
|
|
const activeWorkspaceId = useWorkspaceStore(s => s.activeWorkspaceId);
|
|
const context = useWorkspaceStore(s => s.context);
|
|
const myWorkspacesFromStore = useWorkspaceStore(s => s.myWorkspaces);
|
|
const setActiveWorkspace = useWorkspaceStore(s => s.setActiveWorkspace);
|
|
const setContext = useWorkspaceStore(s => s.setContext);
|
|
const setMyWorkspaces = useWorkspaceStore(s => s.setMyWorkspaces);
|
|
const isModuleVisibleFromStore = useWorkspaceStore(s => s.isModuleVisible);
|
|
|
|
const user = useAuthStore.getState().user;
|
|
const isSystemAdmin = user?.is_system_admin;
|
|
|
|
// Fetch my workspaces and sync to store
|
|
const { data: myWorkspacesData } = useMyWorkspaces();
|
|
const { data: contextData } = useWorkspaceContext(activeWorkspaceId);
|
|
|
|
// Sync fetched workspaces to store
|
|
useEffect(() => {
|
|
if (myWorkspacesData?.items) {
|
|
setMyWorkspaces(myWorkspacesData.items as WorkspaceInfo[]);
|
|
}
|
|
}, [myWorkspacesData, setMyWorkspaces]);
|
|
|
|
// Sync fetched context to store
|
|
useEffect(() => {
|
|
if (contextData !== undefined) {
|
|
setContext(contextData as WorkspaceContextState | null);
|
|
}
|
|
}, [contextData, setContext]);
|
|
|
|
// Auto-select default workspace if none selected
|
|
useEffect(() => {
|
|
if (!activeWorkspaceId && myWorkspacesData?.items?.length) {
|
|
const defaultWs = myWorkspacesData.items.find(w => w.is_user_default) || myWorkspacesData.items[0];
|
|
if (defaultWs) {
|
|
setActiveWorkspace(defaultWs.id);
|
|
}
|
|
}
|
|
}, [activeWorkspaceId, myWorkspacesData, setActiveWorkspace]);
|
|
|
|
// Switch workspace (per-tab)
|
|
const switchWorkspace = useCallback((workspaceId: string | null) => {
|
|
setActiveWorkspace(workspaceId);
|
|
}, [setActiveWorkspace]);
|
|
|
|
// Check if a module is visible in the current workspace
|
|
const isModuleVisible = useCallback((moduleKey: string): boolean => {
|
|
return isModuleVisibleFromStore(moduleKey, isSystemAdmin);
|
|
}, [isModuleVisibleFromStore, isSystemAdmin]);
|
|
|
|
const visibleModuleKeys = useWorkspaceStore(s => s.visibleModuleKeys());
|
|
|
|
return {
|
|
activeWorkspaceId,
|
|
activeWorkspace: context,
|
|
myWorkspaces: myWorkspacesFromStore,
|
|
switchWorkspace,
|
|
isModuleVisible,
|
|
visibleModuleKeys,
|
|
hasWorkspaces: myWorkspacesFromStore.length > 0,
|
|
};
|
|
}
|