feat: global platform v2 update
This commit is contained in:
468
components/providers/workspace-provider.tsx
Normal file
468
components/providers/workspace-provider.tsx
Normal file
@@ -0,0 +1,468 @@
|
||||
"use client";
|
||||
|
||||
// ============================================================================
|
||||
// Workspace Provider
|
||||
// ============================================================================
|
||||
|
||||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
useState,
|
||||
useEffect,
|
||||
useCallback,
|
||||
} from "react";
|
||||
import { useParams, useRouter, usePathname } from "next/navigation";
|
||||
import { workspacesApi, projectsApi, membersApi, authApi } from "@/lib/api";
|
||||
import type {
|
||||
Workspace,
|
||||
Project,
|
||||
Permission,
|
||||
PermissionKey,
|
||||
} from "@/lib/types/api";
|
||||
import { STORAGE_KEYS } from "@/lib/utils/constants";
|
||||
import { isDemoWorkspace, demoApi, demoWorkspace, demoProjects } from "@/lib/demo";
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
// ============================================================================
|
||||
|
||||
interface WorkspaceContextType {
|
||||
// Workspaces
|
||||
workspaces: Workspace[];
|
||||
currentWorkspace: Workspace | null;
|
||||
setCurrentWorkspace: (workspace: Workspace) => void;
|
||||
|
||||
// Projects (Target Channels)
|
||||
projects: Project[];
|
||||
selectedProjects: Project[];
|
||||
currentProject: Project | null; // First selected or null (for backward compatibility)
|
||||
setSelectedProjects: (projects: Project[]) => void;
|
||||
toggleProject: (project: Project) => void;
|
||||
selectAllProjects: () => void;
|
||||
clearProjectSelection: () => void;
|
||||
|
||||
// Helper for API filtering
|
||||
// Returns project_id only if single project selected, undefined otherwise
|
||||
getProjectFilterId: () => string | undefined;
|
||||
// Returns true if all projects are selected
|
||||
allProjectsSelected: boolean;
|
||||
|
||||
// Demo mode
|
||||
isDemoMode: boolean;
|
||||
|
||||
// Permissions
|
||||
permissions: Permission[];
|
||||
hasPermission: (key: PermissionKey, projectId?: string) => boolean;
|
||||
isAdmin: boolean;
|
||||
|
||||
// Loading states
|
||||
isLoading: boolean;
|
||||
isLoadingProjects: boolean;
|
||||
error: string | null;
|
||||
|
||||
// Actions
|
||||
refreshWorkspaces: () => Promise<Workspace[]>;
|
||||
refreshProjects: () => Promise<void>;
|
||||
createWorkspace: (name: string) => Promise<Workspace>;
|
||||
}
|
||||
|
||||
const WorkspaceContext = createContext<WorkspaceContextType | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// Hook
|
||||
// ============================================================================
|
||||
|
||||
export const useWorkspace = () => {
|
||||
const context = useContext(WorkspaceContext);
|
||||
if (!context) {
|
||||
throw new Error("useWorkspace must be used within WorkspaceProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Provider
|
||||
// ============================================================================
|
||||
|
||||
interface WorkspaceProviderProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const WorkspaceProvider: React.FC<WorkspaceProviderProps> = ({
|
||||
children,
|
||||
}) => {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
// State
|
||||
const [workspaces, setWorkspaces] = useState<Workspace[]>([]);
|
||||
const [currentWorkspace, setCurrentWorkspaceState] = useState<Workspace | null>(null);
|
||||
const [projects, setProjects] = useState<Project[]>([]);
|
||||
const [selectedProjects, setSelectedProjectsState] = useState<Project[]>([]);
|
||||
const [permissions, setPermissions] = useState<Permission[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isLoadingProjects, setIsLoadingProjects] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Get workspaceId from URL params
|
||||
const workspaceIdFromUrl = params?.workspaceId as string | undefined;
|
||||
|
||||
// Check if demo mode
|
||||
const isDemoMode = isDemoWorkspace(workspaceIdFromUrl);
|
||||
|
||||
// Current project (first selected for backward compatibility)
|
||||
const currentProject = selectedProjects.length > 0 ? selectedProjects[0] : null;
|
||||
|
||||
// Check if all projects are selected
|
||||
const allProjectsSelected =
|
||||
projects.length > 0 && selectedProjects.length === projects.length;
|
||||
|
||||
// Get project_id for API filtering
|
||||
// Returns undefined if multiple projects selected (to get workspace-wide data)
|
||||
const getProjectFilterId = useCallback(() => {
|
||||
if (selectedProjects.length === 1) {
|
||||
return selectedProjects[0].id;
|
||||
}
|
||||
// Multiple projects selected - return undefined to get all data
|
||||
return undefined;
|
||||
}, [selectedProjects]);
|
||||
|
||||
// ============================================================================
|
||||
// Permission Helpers
|
||||
// ============================================================================
|
||||
|
||||
const isAdmin = permissions.some((p) => p.key === "admin_full");
|
||||
|
||||
const hasPermission = useCallback(
|
||||
(key: PermissionKey, projectId?: string): boolean => {
|
||||
// Admin has all permissions
|
||||
if (isAdmin) return true;
|
||||
|
||||
// Find permission by key
|
||||
const permission = permissions.find((p) => p.key === key);
|
||||
if (!permission) return false;
|
||||
|
||||
// If no scopes defined, permission applies globally
|
||||
if (!permission.scopes || permission.scopes.length === 0) return true;
|
||||
|
||||
// If projectId provided, check if it's in scopes
|
||||
if (projectId) {
|
||||
return permission.scopes.some(
|
||||
(s) => s.type === "project" && s.id === projectId
|
||||
);
|
||||
}
|
||||
|
||||
// Permission exists but is scoped - need projectId to verify
|
||||
return true;
|
||||
},
|
||||
[permissions, isAdmin]
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// Load Workspaces
|
||||
// ============================================================================
|
||||
|
||||
const loadWorkspaces = useCallback(async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Demo mode - use mock data
|
||||
if (isDemoMode) {
|
||||
setWorkspaces([demoWorkspace]);
|
||||
setCurrentWorkspaceState(demoWorkspace);
|
||||
setIsLoading(false);
|
||||
return [demoWorkspace];
|
||||
}
|
||||
|
||||
const response = await workspacesApi.list({ size: 100 });
|
||||
setWorkspaces(response.items);
|
||||
|
||||
// If we have workspaceId in URL, select that workspace
|
||||
if (workspaceIdFromUrl && response.items.length > 0) {
|
||||
const workspace = response.items.find((w) => w.id === workspaceIdFromUrl);
|
||||
if (workspace) {
|
||||
setCurrentWorkspaceState(workspace);
|
||||
localStorage.setItem(STORAGE_KEYS.SELECTED_WORKSPACE, workspace.id);
|
||||
} else {
|
||||
// Workspace from URL not found, redirect to first available
|
||||
const firstWorkspace = response.items[0];
|
||||
setCurrentWorkspaceState(firstWorkspace);
|
||||
localStorage.setItem(STORAGE_KEYS.SELECTED_WORKSPACE, firstWorkspace.id);
|
||||
// Redirect to correct workspace
|
||||
const newPath = pathname.replace(
|
||||
`/dashboard/${workspaceIdFromUrl}`,
|
||||
`/dashboard/${firstWorkspace.id}`
|
||||
);
|
||||
router.replace(newPath);
|
||||
}
|
||||
} else if (response.items.length > 0) {
|
||||
// Try to restore from localStorage or use first
|
||||
const savedId = localStorage.getItem(STORAGE_KEYS.SELECTED_WORKSPACE);
|
||||
const saved = savedId
|
||||
? response.items.find((w) => w.id === savedId)
|
||||
: null;
|
||||
const workspace = saved || response.items[0];
|
||||
setCurrentWorkspaceState(workspace);
|
||||
localStorage.setItem(STORAGE_KEYS.SELECTED_WORKSPACE, workspace.id);
|
||||
}
|
||||
|
||||
return response.items;
|
||||
} catch (err: any) {
|
||||
console.error("Failed to load workspaces:", err);
|
||||
setError(err?.error?.message || "Ошибка загрузки воркспейсов");
|
||||
return [];
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [workspaceIdFromUrl, pathname, router, isDemoMode]);
|
||||
|
||||
// ============================================================================
|
||||
// Load Projects & Permissions
|
||||
// ============================================================================
|
||||
|
||||
const loadProjectsAndPermissions = useCallback(async (workspaceId: string) => {
|
||||
try {
|
||||
setIsLoadingProjects(true);
|
||||
|
||||
// Demo mode - use mock data
|
||||
if (isDemoMode) {
|
||||
const activeProjects = demoProjects.filter((p) => p.status === "active");
|
||||
setProjects(activeProjects);
|
||||
setPermissions([{ key: "admin_full" }]);
|
||||
setSelectedProjectsState(activeProjects.length > 0 ? [activeProjects[0]] : []);
|
||||
setIsLoadingProjects(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Load projects
|
||||
const projectsResponse = await projectsApi.list(workspaceId, { size: 100 });
|
||||
const activeProjects = projectsResponse.items.filter(
|
||||
(p) => p.status === "active"
|
||||
);
|
||||
setProjects(activeProjects);
|
||||
|
||||
// Load current user's permissions
|
||||
const me = await authApi.me();
|
||||
const membersResponse = await membersApi.list(workspaceId, { size: 100 });
|
||||
// Find member by user.id (not by member record id)
|
||||
const currentMember = membersResponse.items.find((m) => m.user.id === me.id);
|
||||
|
||||
if (currentMember) {
|
||||
// If member found but no permissions, they're likely the creator - grant admin
|
||||
if (currentMember.permissions.length === 0) {
|
||||
setPermissions([{ key: "admin_full" }]);
|
||||
} else {
|
||||
setPermissions(currentMember.permissions);
|
||||
}
|
||||
} else {
|
||||
// User not in members list - might be creator, check if only workspace
|
||||
// Grant admin access by default for workspace creators
|
||||
if (membersResponse.items.length === 0) {
|
||||
setPermissions([{ key: "admin_full" }]);
|
||||
} else {
|
||||
setPermissions([]);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore selected projects from localStorage
|
||||
const savedProjectIds = localStorage.getItem(
|
||||
`${STORAGE_KEYS.SELECTED_PROJECT}_${workspaceId}`
|
||||
);
|
||||
if (savedProjectIds && activeProjects.length > 0) {
|
||||
try {
|
||||
const ids = JSON.parse(savedProjectIds) as string[];
|
||||
const savedProjects = activeProjects.filter((p) => ids.includes(p.id));
|
||||
if (savedProjects.length > 0) {
|
||||
setSelectedProjectsState(savedProjects);
|
||||
} else if (activeProjects.length > 0) {
|
||||
setSelectedProjectsState([activeProjects[0]]);
|
||||
}
|
||||
} catch {
|
||||
// Old format (single id), migrate to array
|
||||
const savedProject = activeProjects.find((p) => p.id === savedProjectIds);
|
||||
if (savedProject) {
|
||||
setSelectedProjectsState([savedProject]);
|
||||
} else if (activeProjects.length > 0) {
|
||||
setSelectedProjectsState([activeProjects[0]]);
|
||||
}
|
||||
}
|
||||
} else if (activeProjects.length > 0) {
|
||||
setSelectedProjectsState([activeProjects[0]]);
|
||||
} else {
|
||||
setSelectedProjectsState([]);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error("Failed to load projects/permissions:", err);
|
||||
} finally {
|
||||
setIsLoadingProjects(false);
|
||||
}
|
||||
}, [isDemoMode]);
|
||||
|
||||
// ============================================================================
|
||||
// Set Current Workspace
|
||||
// ============================================================================
|
||||
|
||||
const setCurrentWorkspace = useCallback(
|
||||
(workspace: Workspace) => {
|
||||
setCurrentWorkspaceState(workspace);
|
||||
localStorage.setItem(STORAGE_KEYS.SELECTED_WORKSPACE, workspace.id);
|
||||
|
||||
// Update URL if we're in dashboard
|
||||
if (pathname.startsWith("/dashboard/")) {
|
||||
const currentWorkspaceId = workspaceIdFromUrl;
|
||||
if (currentWorkspaceId && currentWorkspaceId !== workspace.id) {
|
||||
const newPath = pathname.replace(
|
||||
`/dashboard/${currentWorkspaceId}`,
|
||||
`/dashboard/${workspace.id}`
|
||||
);
|
||||
router.push(newPath);
|
||||
}
|
||||
} else if (pathname === "/dashboard" || pathname === "/") {
|
||||
router.push(`/dashboard/${workspace.id}`);
|
||||
}
|
||||
|
||||
// Load projects for new workspace
|
||||
loadProjectsAndPermissions(workspace.id);
|
||||
},
|
||||
[pathname, router, workspaceIdFromUrl, loadProjectsAndPermissions]
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// Project Selection Methods
|
||||
// ============================================================================
|
||||
|
||||
const saveSelectedProjects = useCallback(
|
||||
(selected: Project[]) => {
|
||||
if (currentWorkspace) {
|
||||
localStorage.setItem(
|
||||
`${STORAGE_KEYS.SELECTED_PROJECT}_${currentWorkspace.id}`,
|
||||
JSON.stringify(selected.map((p) => p.id))
|
||||
);
|
||||
}
|
||||
},
|
||||
[currentWorkspace]
|
||||
);
|
||||
|
||||
const setSelectedProjects = useCallback(
|
||||
(selected: Project[]) => {
|
||||
setSelectedProjectsState(selected);
|
||||
saveSelectedProjects(selected);
|
||||
},
|
||||
[saveSelectedProjects]
|
||||
);
|
||||
|
||||
const toggleProject = useCallback(
|
||||
(project: Project) => {
|
||||
setSelectedProjectsState((prev) => {
|
||||
const isSelected = prev.some((p) => p.id === project.id);
|
||||
let newSelection: Project[];
|
||||
|
||||
if (isSelected) {
|
||||
// Don't allow deselecting if it's the only one
|
||||
if (prev.length <= 1) {
|
||||
return prev;
|
||||
}
|
||||
newSelection = prev.filter((p) => p.id !== project.id);
|
||||
} else {
|
||||
newSelection = [...prev, project];
|
||||
}
|
||||
|
||||
saveSelectedProjects(newSelection);
|
||||
return newSelection;
|
||||
});
|
||||
},
|
||||
[saveSelectedProjects]
|
||||
);
|
||||
|
||||
const selectAllProjects = useCallback(() => {
|
||||
setSelectedProjectsState(projects);
|
||||
saveSelectedProjects(projects);
|
||||
}, [projects, saveSelectedProjects]);
|
||||
|
||||
const clearProjectSelection = useCallback(() => {
|
||||
if (projects.length > 0) {
|
||||
const firstProject = [projects[0]];
|
||||
setSelectedProjectsState(firstProject);
|
||||
saveSelectedProjects(firstProject);
|
||||
}
|
||||
}, [projects, saveSelectedProjects]);
|
||||
|
||||
// ============================================================================
|
||||
// Create Workspace
|
||||
// ============================================================================
|
||||
|
||||
const createWorkspace = useCallback(async (name: string): Promise<Workspace> => {
|
||||
const workspace = await workspacesApi.create({ name });
|
||||
setWorkspaces((prev) => [...prev, workspace]);
|
||||
return workspace;
|
||||
}, []);
|
||||
|
||||
// ============================================================================
|
||||
// Effects
|
||||
// ============================================================================
|
||||
|
||||
// Initial load
|
||||
useEffect(() => {
|
||||
loadWorkspaces();
|
||||
}, [loadWorkspaces]);
|
||||
|
||||
// Load projects when workspace changes
|
||||
useEffect(() => {
|
||||
if (currentWorkspace) {
|
||||
loadProjectsAndPermissions(currentWorkspace.id);
|
||||
}
|
||||
}, [currentWorkspace?.id, loadProjectsAndPermissions]);
|
||||
|
||||
// ============================================================================
|
||||
// Render
|
||||
// ============================================================================
|
||||
|
||||
return (
|
||||
<WorkspaceContext.Provider
|
||||
value={{
|
||||
// Workspaces
|
||||
workspaces,
|
||||
currentWorkspace,
|
||||
setCurrentWorkspace,
|
||||
|
||||
// Projects
|
||||
projects,
|
||||
selectedProjects,
|
||||
currentProject,
|
||||
setSelectedProjects,
|
||||
toggleProject,
|
||||
selectAllProjects,
|
||||
clearProjectSelection,
|
||||
getProjectFilterId,
|
||||
allProjectsSelected,
|
||||
|
||||
// Demo mode
|
||||
isDemoMode,
|
||||
|
||||
// Permissions
|
||||
permissions,
|
||||
hasPermission,
|
||||
isAdmin,
|
||||
|
||||
// Loading states
|
||||
isLoading,
|
||||
isLoadingProjects,
|
||||
error,
|
||||
|
||||
// Actions
|
||||
refreshWorkspaces: loadWorkspaces,
|
||||
refreshProjects: () =>
|
||||
currentWorkspace
|
||||
? loadProjectsAndPermissions(currentWorkspace.id)
|
||||
: Promise.resolve(),
|
||||
createWorkspace,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</WorkspaceContext.Provider>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user