Phase 6: Workspaces — Widget CRUD, Manager-Check, Cross-Tenant, Zustand Store, Settings Route
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
This commit is contained in:
@@ -132,3 +132,87 @@ export function useRemoveWorkspaceUser() {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Widget Hooks ─────────────────────────────────────────────
|
||||
|
||||
export interface WorkspaceWidget {
|
||||
id: string;
|
||||
workspace_id: string;
|
||||
widget_key: string;
|
||||
position_x: number;
|
||||
position_y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
config: Record<string, any>;
|
||||
}
|
||||
|
||||
export function useWorkspaceWidgets(workspaceId: string | null) {
|
||||
return useQuery<{ items: WorkspaceWidget[]; total: number }>({
|
||||
queryKey: ['workspace-widgets', workspaceId],
|
||||
queryFn: () => apiGet(`/api/v1/workspaces/${workspaceId}/widgets`),
|
||||
enabled: !!workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateWorkspaceWidget() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ workspaceId, ...data }: {
|
||||
workspaceId: string;
|
||||
widget_key: string;
|
||||
position_x?: number;
|
||||
position_y?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
config?: Record<string, any>;
|
||||
}) => apiPost(`/api/v1/workspaces/${workspaceId}/widgets`, data),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['workspace-widgets'] });
|
||||
qc.invalidateQueries({ queryKey: ['workspace-context'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateWorkspaceWidget() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ workspaceId, widgetId, ...data }: {
|
||||
workspaceId: string;
|
||||
widgetId: string;
|
||||
position_x?: number;
|
||||
position_y?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
config?: Record<string, any>;
|
||||
}) => apiPut(`/api/v1/workspaces/${workspaceId}/widgets/${widgetId}`, data),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['workspace-widgets'] });
|
||||
qc.invalidateQueries({ queryKey: ['workspace-context'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteWorkspaceWidget() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ workspaceId, widgetId }: { workspaceId: string; widgetId: string }) =>
|
||||
apiDelete(`/api/v1/workspaces/${workspaceId}/widgets/${widgetId}`),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['workspace-widgets'] });
|
||||
qc.invalidateQueries({ queryKey: ['workspace-context'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Set Default Workspace ────────────────────────────────────
|
||||
|
||||
export function useSetDefaultWorkspace() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (workspaceId: string) =>
|
||||
apiPost(`/api/v1/workspaces/${workspaceId}/set-default`),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['my-workspaces'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user