mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-26 23:51:14 +02:00
feat(workspaces): implement workspace limits feature with backend integration and UI updates
This commit is contained in:
parent
1385ff4789
commit
38b784fbac
12 changed files with 176 additions and 18 deletions
|
|
@ -14,7 +14,7 @@ import { announcementsDialogAtom } from "@/atoms/layout/dialogs.atom";
|
|||
import { removeChatTabAtom, syncChatTabAtom, type Tab } from "@/atoms/tabs/tabs.atom";
|
||||
import { currentUserAtom } from "@/atoms/user/user-query.atoms";
|
||||
import { deleteWorkspaceMutationAtom } from "@/atoms/workspaces/workspace-mutation.atoms";
|
||||
import { workspacesAtom } from "@/atoms/workspaces/workspace-query.atoms";
|
||||
import { workspaceLimitsAtom, workspacesAtom } from "@/atoms/workspaces/workspace-query.atoms";
|
||||
import { ActionLogDialog } from "@/components/agent-action-log/action-log-dialog";
|
||||
import { AnnouncementSpotlight } from "@/components/announcements/AnnouncementSpotlight";
|
||||
import { AnnouncementsDialog } from "@/components/announcements/AnnouncementsDialog";
|
||||
|
|
@ -84,6 +84,7 @@ export function LayoutDataProvider({
|
|||
refetch: refetchWorkspaces,
|
||||
isSuccess: workspacesLoaded,
|
||||
} = useAtomValue(workspacesAtom);
|
||||
const { data: workspaceLimits } = useAtomValue(workspaceLimitsAtom);
|
||||
const { mutateAsync: deleteWorkspace } = useAtomValue(deleteWorkspaceMutationAtom);
|
||||
const currentThreadState = useAtomValue(currentThreadAtom);
|
||||
const resetCurrentThread = useSetAtom(resetCurrentThreadAtom);
|
||||
|
|
@ -225,6 +226,13 @@ export function LayoutDataProvider({
|
|||
createdAt: space.created_at,
|
||||
}));
|
||||
}, [workspacesData]);
|
||||
const maxWorkspacesPerUser = workspaceLimits?.max_workspaces_per_user;
|
||||
const ownedWorkspaceCount = workspaces.reduce(
|
||||
(count, space) => count + (space.isOwner ? 1 : 0),
|
||||
0
|
||||
);
|
||||
const isAtWorkspaceLimit =
|
||||
maxWorkspacesPerUser !== undefined && ownedWorkspaceCount >= maxWorkspacesPerUser;
|
||||
|
||||
// Find active workspace from list, falling back to the route-scoped detail query.
|
||||
const activeWorkspace: Workspace | null = useMemo(() => {
|
||||
|
|
@ -335,8 +343,14 @@ export function LayoutDataProvider({
|
|||
);
|
||||
|
||||
const handleAddWorkspace = useCallback(() => {
|
||||
if (isAtWorkspaceLimit) {
|
||||
toast.error(
|
||||
`Workspace limit reached. You can own at most ${maxWorkspacesPerUser} workspaces.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
setIsCreateWorkspaceDialogOpen(true);
|
||||
}, []);
|
||||
}, [isAtWorkspaceLimit, maxWorkspacesPerUser]);
|
||||
|
||||
const setAnnouncementsDialog = useSetAtom(announcementsDialogAtom);
|
||||
|
||||
|
|
@ -663,6 +677,8 @@ export function LayoutDataProvider({
|
|||
onWorkspaceDelete={handleWorkspaceDeleteClick}
|
||||
onWorkspaceSettings={handleWorkspaceSettings}
|
||||
onAddWorkspace={handleAddWorkspace}
|
||||
isAtWorkspaceLimit={isAtWorkspaceLimit}
|
||||
maxWorkspacesPerUser={maxWorkspacesPerUser}
|
||||
workspace={activeWorkspace}
|
||||
navItems={navItems}
|
||||
onNavItemClick={handleNavItemClick}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { useRouter } from "next/navigation";
|
|||
import { useTranslations } from "next-intl";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import * as z from "zod";
|
||||
import { createWorkspaceMutationAtom } from "@/atoms/workspaces/workspace-mutation.atoms";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
@ -87,6 +88,7 @@ export function CreateWorkspaceDialog({ open, onOpenChange }: CreateWorkspaceDia
|
|||
);
|
||||
} catch (error) {
|
||||
console.error("Failed to create workspace:", error);
|
||||
toast.error(error instanceof Error ? error.message : "Failed to create workspace");
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ interface IconRailProps {
|
|||
onWorkspaceDelete?: (workspace: Workspace) => void;
|
||||
onWorkspaceSettings?: (workspace: Workspace) => void;
|
||||
onAddWorkspace: () => void;
|
||||
isAtWorkspaceLimit?: boolean;
|
||||
maxWorkspacesPerUser?: number;
|
||||
isSingleRailMode?: boolean;
|
||||
onNewChat?: () => void;
|
||||
navItems?: NavItem[];
|
||||
|
|
@ -42,6 +44,8 @@ export function IconRail({
|
|||
onWorkspaceDelete,
|
||||
onWorkspaceSettings,
|
||||
onAddWorkspace,
|
||||
isAtWorkspaceLimit = false,
|
||||
maxWorkspacesPerUser,
|
||||
isSingleRailMode = false,
|
||||
onNewChat,
|
||||
navItems = [],
|
||||
|
|
@ -78,6 +82,10 @@ export function IconRail({
|
|||
})),
|
||||
]
|
||||
: [];
|
||||
const addWorkspaceLabel =
|
||||
isAtWorkspaceLimit && maxWorkspacesPerUser !== undefined
|
||||
? `Workspace limit reached: ${maxWorkspacesPerUser}`
|
||||
: "Add workspace";
|
||||
|
||||
return (
|
||||
<div className={cn("flex h-full w-14 min-h-0 flex-col items-center", className)}>
|
||||
|
|
@ -99,18 +107,21 @@ export function IconRail({
|
|||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onAddWorkspace}
|
||||
className="h-10 w-10 rounded-lg border-2 border-dashed border-muted-foreground/30 hover:border-muted-foreground/50"
|
||||
>
|
||||
<Plus className="h-5 w-5 text-muted-foreground" />
|
||||
<span className="sr-only">Add workspace</span>
|
||||
</Button>
|
||||
<span className="inline-flex">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onAddWorkspace}
|
||||
disabled={isAtWorkspaceLimit}
|
||||
className="h-10 w-10 rounded-lg border-2 border-dashed border-muted-foreground/30 hover:border-muted-foreground/50 disabled:opacity-50"
|
||||
>
|
||||
<Plus className="h-5 w-5 text-muted-foreground" />
|
||||
<span className="sr-only">{addWorkspaceLabel}</span>
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={8}>
|
||||
Add workspace
|
||||
{addWorkspaceLabel}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
|
|
|
|||
|
|
@ -95,6 +95,8 @@ interface LayoutShellProps {
|
|||
onWorkspaceDelete?: (workspace: Workspace) => void;
|
||||
onWorkspaceSettings?: (workspace: Workspace) => void;
|
||||
onAddWorkspace: () => void;
|
||||
isAtWorkspaceLimit?: boolean;
|
||||
maxWorkspacesPerUser?: number;
|
||||
workspace: Workspace | null;
|
||||
navItems: NavItem[];
|
||||
onNavItemClick?: (item: NavItem) => void;
|
||||
|
|
@ -198,6 +200,8 @@ export function LayoutShell({
|
|||
onWorkspaceDelete,
|
||||
onWorkspaceSettings,
|
||||
onAddWorkspace,
|
||||
isAtWorkspaceLimit = false,
|
||||
maxWorkspacesPerUser,
|
||||
workspace,
|
||||
navItems,
|
||||
onNavItemClick,
|
||||
|
|
@ -280,6 +284,8 @@ export function LayoutShell({
|
|||
activeWorkspaceId={activeWorkspaceId}
|
||||
onWorkspaceSelect={onWorkspaceSelect}
|
||||
onAddWorkspace={onAddWorkspace}
|
||||
isAtWorkspaceLimit={isAtWorkspaceLimit}
|
||||
maxWorkspacesPerUser={maxWorkspacesPerUser}
|
||||
workspace={workspace}
|
||||
navItems={navItems}
|
||||
onNavItemClick={onNavItemClick}
|
||||
|
|
@ -352,6 +358,8 @@ export function LayoutShell({
|
|||
onWorkspaceDelete={onWorkspaceDelete}
|
||||
onWorkspaceSettings={onWorkspaceSettings}
|
||||
onAddWorkspace={onAddWorkspace}
|
||||
isAtWorkspaceLimit={isAtWorkspaceLimit}
|
||||
maxWorkspacesPerUser={maxWorkspacesPerUser}
|
||||
isSingleRailMode={false}
|
||||
user={user}
|
||||
onUserSettings={onUserSettings}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ interface MobileSidebarProps {
|
|||
activeWorkspaceId: number | null;
|
||||
onWorkspaceSelect: (id: number) => void;
|
||||
onAddWorkspace: () => void;
|
||||
isAtWorkspaceLimit?: boolean;
|
||||
maxWorkspacesPerUser?: number;
|
||||
workspace: Workspace | null;
|
||||
navItems: NavItem[];
|
||||
onNavItemClick?: (item: NavItem) => void;
|
||||
|
|
@ -67,6 +69,8 @@ export function MobileSidebar({
|
|||
onWorkspaceSelect,
|
||||
|
||||
onAddWorkspace,
|
||||
isAtWorkspaceLimit = false,
|
||||
maxWorkspacesPerUser,
|
||||
workspace,
|
||||
navItems,
|
||||
onNavItemClick,
|
||||
|
|
@ -107,6 +111,10 @@ export function MobileSidebar({
|
|||
onChatSelect(chat);
|
||||
onOpenChange(false);
|
||||
};
|
||||
const addWorkspaceLabel =
|
||||
isAtWorkspaceLimit && maxWorkspacesPerUser !== undefined
|
||||
? `Workspace limit reached: ${maxWorkspacesPerUser}`
|
||||
: "Add workspace";
|
||||
|
||||
return (
|
||||
<Sheet open={isOpen} onOpenChange={onOpenChange}>
|
||||
|
|
@ -136,10 +144,12 @@ export function MobileSidebar({
|
|||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onAddWorkspace}
|
||||
disabled={isAtWorkspaceLimit}
|
||||
title={addWorkspaceLabel}
|
||||
className="h-10 w-10 shrink-0 rounded-lg border-2 border-dashed border-muted-foreground/30 hover:border-muted-foreground/50"
|
||||
>
|
||||
<Plus className="h-5 w-5 text-muted-foreground" />
|
||||
<span className="sr-only">Add workspace</span>
|
||||
<span className="sr-only">{addWorkspaceLabel}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue