feat: add create workspace and switch workspace
This commit is contained in:
parent
ebd1e5eb66
commit
fac0838d8c
@ -12,6 +12,11 @@ const useSocketStore = create<{
|
||||
}));
|
||||
|
||||
export function createSocketIOClient(workspaceId: string) {
|
||||
const prev = useSocketStore.getState().socket;
|
||||
if (prev) {
|
||||
prev.disconnect();
|
||||
}
|
||||
|
||||
const socket = io(`/${workspaceId}`, {
|
||||
transports: ['websocket'],
|
||||
reconnectionDelayMax: 10000,
|
||||
|
11
src/client/components/DevContainer.tsx
Normal file
11
src/client/components/DevContainer.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import { isDev } from '@/utils/env';
|
||||
import React, { PropsWithChildren } from 'react';
|
||||
|
||||
export const DevContainer: React.FC<PropsWithChildren> = React.memo((props) => {
|
||||
if (isDev) {
|
||||
return <>{props.children}</>;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
DevContainer.displayName = 'DevContainer';
|
@ -1,14 +1,35 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import React, { useState } from 'react';
|
||||
import { cn } from '@/utils/style';
|
||||
import { useUserInfo } from '@/store/user';
|
||||
import { RiRocket2Fill } from 'react-icons/ri';
|
||||
import { setUserInfo, useUserInfo } from '@/store/user';
|
||||
import { LuPlusCircle } from 'react-icons/lu';
|
||||
import { useTranslation } from '@i18next-toolkit/react';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from './ui/popover';
|
||||
import { Button } from './ui/button';
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
CommandSeparator,
|
||||
} from './ui/command';
|
||||
import { CaretSortIcon, CheckIcon } from '@radix-ui/react-icons';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from './ui/dialog';
|
||||
import { Label } from './ui/label';
|
||||
import { Input } from './ui/input';
|
||||
import { useEvent, useEventWithLoading } from '@/hooks/useEvent';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar';
|
||||
import { trpc } from '@/api/trpc';
|
||||
import { showErrorToast } from '@/utils/error';
|
||||
import { first, upperCase } from 'lodash-es';
|
||||
|
||||
interface WorkspaceSwitcherProps {
|
||||
isCollapsed: boolean;
|
||||
@ -16,41 +37,241 @@ interface WorkspaceSwitcherProps {
|
||||
export const WorkspaceSwitcher: React.FC<WorkspaceSwitcherProps> = React.memo(
|
||||
(props) => {
|
||||
const userInfo = useUserInfo();
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [showNewWorkspaceDialog, setShowNewWorkspaceDialog] = useState(false);
|
||||
const [newWorkspaceName, setNewWorkspaceName] = useState('');
|
||||
const createWorkspaceMutation = trpc.workspace.create.useMutation({
|
||||
onSuccess: (userInfo) => {
|
||||
setUserInfo(userInfo);
|
||||
},
|
||||
});
|
||||
const switchWorkspaceMutation = trpc.workspace.switch.useMutation({
|
||||
onSuccess: (userInfo) => {
|
||||
setUserInfo(userInfo);
|
||||
},
|
||||
});
|
||||
|
||||
const handleSwitchWorkspace = useEvent(
|
||||
async (workspace: { id: string; name: string }) => {
|
||||
setOpen(false);
|
||||
|
||||
if (userInfo?.currentWorkspace.id === workspace.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await switchWorkspaceMutation.mutateAsync({
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
} catch (err) {
|
||||
showErrorToast(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const [handleCreateNewWorkspace, isCreateLoading] = useEventWithLoading(
|
||||
async () => {
|
||||
try {
|
||||
await createWorkspaceMutation.mutateAsync({
|
||||
name: newWorkspaceName,
|
||||
});
|
||||
|
||||
setShowNewWorkspaceDialog(false);
|
||||
} catch (err) {
|
||||
showErrorToast(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (!userInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Select value={userInfo.currentWorkspace.id}>
|
||||
<SelectTrigger
|
||||
className={cn(
|
||||
'flex items-center gap-2 [&>span]:line-clamp-1 [&>span]:flex [&>span]:w-full [&>span]:items-center [&>span]:gap-1 [&>span]:truncate [&_svg]:h-4 [&_svg]:w-4 [&_svg]:shrink-0',
|
||||
props.isCollapsed &&
|
||||
'flex h-9 w-9 shrink-0 items-center justify-center p-0 [&>span]:w-auto [&>svg]:hidden'
|
||||
)}
|
||||
aria-label="Select workspace"
|
||||
>
|
||||
<SelectValue placeholder="Select workspace">
|
||||
<RiRocket2Fill />
|
||||
const currentWorkspace = userInfo.currentWorkspace;
|
||||
|
||||
<span className={cn('ml-2', props.isCollapsed && 'hidden')}>
|
||||
{userInfo.currentWorkspace.name}
|
||||
</span>
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{userInfo.workspaces.map((w) => (
|
||||
<SelectItem key={w.workspace.id} value={w.workspace.id}>
|
||||
<div className="flex items-center gap-3 [&_svg]:h-4 [&_svg]:w-4 [&_svg]:shrink-0 [&_svg]:text-foreground">
|
||||
<RiRocket2Fill />
|
||||
{w.workspace.name}
|
||||
return (
|
||||
<Dialog
|
||||
open={showNewWorkspaceDialog}
|
||||
onOpenChange={setShowNewWorkspaceDialog}
|
||||
>
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className={cn(
|
||||
'w-full justify-between',
|
||||
props.isCollapsed &&
|
||||
'flex h-9 w-9 items-center justify-center p-0'
|
||||
)}
|
||||
>
|
||||
<Avatar
|
||||
className={cn('h-5 w-5', props.isCollapsed ? '' : 'mr-2')}
|
||||
>
|
||||
<AvatarImage
|
||||
src={`https://avatar.vercel.sh/${currentWorkspace.name}.png`}
|
||||
alt={currentWorkspace.name}
|
||||
className="grayscale"
|
||||
/>
|
||||
<AvatarFallback>
|
||||
{upperCase(first(currentWorkspace.name))}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
<span className={cn(props.isCollapsed && 'hidden')}>
|
||||
{currentWorkspace.name}
|
||||
</span>
|
||||
|
||||
<CaretSortIcon
|
||||
className={cn(
|
||||
'ml-auto h-4 w-4 shrink-0 opacity-50',
|
||||
props.isCollapsed && 'hidden'
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[200px] p-0" align="start">
|
||||
<Command>
|
||||
<CommandList>
|
||||
<CommandEmpty>{t('No workspace found.')}</CommandEmpty>
|
||||
<CommandGroup key="workspace" heading={t('Workspace')}>
|
||||
{userInfo.workspaces.map(({ workspace }) => (
|
||||
<CommandItem
|
||||
key={workspace.id}
|
||||
onSelect={() => {
|
||||
handleSwitchWorkspace(workspace);
|
||||
}}
|
||||
className="text-sm"
|
||||
>
|
||||
<Avatar className="mr-2 h-5 w-5">
|
||||
<AvatarImage
|
||||
src={`https://avatar.vercel.sh/${workspace.name}.png`}
|
||||
alt={workspace.name}
|
||||
className="grayscale"
|
||||
/>
|
||||
<AvatarFallback>
|
||||
{upperCase(first(workspace.name))}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
{workspace.name}
|
||||
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
'ml-auto h-4 w-4',
|
||||
currentWorkspace.id === workspace.id
|
||||
? 'opacity-100'
|
||||
: 'opacity-0'
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
|
||||
<CommandSeparator />
|
||||
|
||||
<CommandList>
|
||||
<CommandGroup key="create">
|
||||
<DialogTrigger asChild>
|
||||
<CommandItem
|
||||
aria-selected="false"
|
||||
onSelect={() => {
|
||||
setOpen(false);
|
||||
setShowNewWorkspaceDialog(true);
|
||||
}}
|
||||
>
|
||||
<LuPlusCircle className="mr-2" size={20} />
|
||||
{t('Create Workspace')}
|
||||
</CommandItem>
|
||||
</DialogTrigger>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('Create Workspace')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t('Create a new workspace to cooperate with team members.')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div>
|
||||
<div className="space-y-4 py-2 pb-4">
|
||||
<div className="space-y-2">
|
||||
<Label>{t('Workspace Name')}</Label>
|
||||
<Input
|
||||
value={newWorkspaceName}
|
||||
onChange={(e) => setNewWorkspaceName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowNewWorkspaceDialog(false)}
|
||||
>
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
loading={isCreateLoading}
|
||||
onClick={handleCreateNewWorkspace}
|
||||
>
|
||||
{t('Create')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
// return (
|
||||
// <Select value={userInfo.currentWorkspace.id}>
|
||||
// <SelectTrigger
|
||||
// className={cn(
|
||||
// 'flex items-center gap-2 [&>span]:line-clamp-1 [&>span]:flex [&>span]:w-full [&>span]:items-center [&>span]:gap-1 [&>span]:truncate [&_svg]:h-4 [&_svg]:w-4 [&_svg]:shrink-0',
|
||||
// props.isCollapsed &&
|
||||
// 'flex h-9 w-9 shrink-0 items-center justify-center p-0 [&>span]:w-auto [&>svg]:hidden'
|
||||
// )}
|
||||
// aria-label="Select workspace"
|
||||
// >
|
||||
// <SelectValue placeholder="Select workspace">
|
||||
// <RiRocket2Fill />
|
||||
|
||||
// <span className={cn('ml-2', props.isCollapsed && 'hidden')}>
|
||||
// {userInfo.currentWorkspace.name}
|
||||
// </span>
|
||||
// </SelectValue>
|
||||
// </SelectTrigger>
|
||||
// <SelectContent>
|
||||
// {userInfo.workspaces.map((w) => (
|
||||
// <SelectItem key={w.workspace.id} value={w.workspace.id}>
|
||||
// <div className="[&_svg]:text-foreground flex items-center gap-3 [&_svg]:h-4 [&_svg]:w-4 [&_svg]:shrink-0">
|
||||
// <RiRocket2Fill />
|
||||
// {w.workspace.name}
|
||||
// </div>
|
||||
// </SelectItem>
|
||||
// ))}
|
||||
|
||||
// <SelectSeparator />
|
||||
|
||||
// <SelectItem
|
||||
// value="create"
|
||||
// onClick={() => console.log('aa')}
|
||||
// onSelect={() => console.log('bbb')}
|
||||
// >
|
||||
// <div className="[&_svg]:text-foreground flex items-center gap-3 [&_svg]:h-4 [&_svg]:w-4 [&_svg]:shrink-0">
|
||||
// <LuPlus />
|
||||
// {t('Create Workspace')}
|
||||
// </div>
|
||||
// </SelectItem>
|
||||
// </SelectContent>
|
||||
// </Select>
|
||||
// );
|
||||
}
|
||||
);
|
||||
WorkspaceSwitcher.displayName = 'WorkspaceSwitcher';
|
||||
|
@ -16,13 +16,19 @@ import {
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { useEvent } from '@/hooks/useEvent';
|
||||
import { useSettingsStore } from '@/store/settings';
|
||||
import { useCurrentWorkspaceId, useUserInfo, useUserStore } from '@/store/user';
|
||||
import {
|
||||
setUserInfo,
|
||||
useCurrentWorkspaceId,
|
||||
useUserInfo,
|
||||
useUserStore,
|
||||
} from '@/store/user';
|
||||
import { languages } from '@/utils/constants';
|
||||
import { useTranslation, setLanguage } from '@i18next-toolkit/react';
|
||||
import { useNavigate } from '@tanstack/react-router';
|
||||
import { version } from '@/utils/env';
|
||||
import React from 'react';
|
||||
import { LuMoreVertical } from 'react-icons/lu';
|
||||
import { trpc } from '@/api/trpc';
|
||||
|
||||
interface UserConfigProps {
|
||||
isCollapsed: boolean;
|
||||
@ -46,6 +52,11 @@ export const UserConfig: React.FC<UserConfigProps> = React.memo((props) => {
|
||||
|
||||
return [];
|
||||
});
|
||||
const switchWorkspaceMutation = trpc.workspace.switch.useMutation({
|
||||
onSuccess: (userInfo) => {
|
||||
setUserInfo(userInfo);
|
||||
},
|
||||
});
|
||||
|
||||
const handleChangeColorSchema = useEvent((colorScheme) => {
|
||||
useSettingsStore.setState({
|
||||
@ -125,7 +136,12 @@ export const UserConfig: React.FC<UserConfigProps> = React.memo((props) => {
|
||||
<DropdownMenuRadioItem
|
||||
key={workspace.id}
|
||||
value={workspace.id}
|
||||
disabled={true}
|
||||
disabled={workspace.id === workspaceId}
|
||||
onSelect={() =>
|
||||
switchWorkspaceMutation.mutateAsync({
|
||||
workspaceId: workspace.id,
|
||||
})
|
||||
}
|
||||
>
|
||||
{workspace.name}
|
||||
</DropdownMenuRadioItem>
|
||||
|
@ -54,6 +54,7 @@ const AvatarFallback = React.forwardRef<
|
||||
'bg-muted flex h-full w-full items-center justify-center rounded-full',
|
||||
className
|
||||
)}
|
||||
delayMs={5000}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
@ -13,7 +13,7 @@ const Command = React.forwardRef<
|
||||
<CommandPrimitive
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex h-full w-full flex-col overflow-hidden rounded-md bg-white text-zinc-950 dark:bg-zinc-950 dark:text-zinc-50',
|
||||
'bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@ -27,7 +27,7 @@ const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogContent className="overflow-hidden p-0">
|
||||
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-zinc-500 dark:[&_[cmdk-group-heading]]:text-zinc-400 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||
{children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
@ -44,7 +44,7 @@ const CommandInput = React.forwardRef<
|
||||
<CommandPrimitive.Input
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-zinc-500 disabled:cursor-not-allowed disabled:opacity-50 dark:placeholder:text-zinc-400',
|
||||
'placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@ -87,7 +87,7 @@ const CommandGroup = React.forwardRef<
|
||||
<CommandPrimitive.Group
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'overflow-hidden p-1 text-zinc-950 dark:text-zinc-50 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-zinc-500 dark:[&_[cmdk-group-heading]]:text-zinc-400',
|
||||
'text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@ -102,7 +102,7 @@ const CommandSeparator = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn('-mx-1 h-px bg-zinc-200 dark:bg-zinc-800', className)}
|
||||
className={cn('bg-border -mx-1 h-px', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
@ -115,7 +115,7 @@ const CommandItem = React.forwardRef<
|
||||
<CommandPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-zinc-100 aria-selected:text-zinc-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:aria-selected:bg-zinc-800 dark:aria-selected:text-zinc-50',
|
||||
'data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@ -131,7 +131,7 @@ const CommandShortcut = ({
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
'ml-auto text-xs tracking-widest text-zinc-500 dark:text-zinc-400',
|
||||
'text-muted-foreground ml-auto text-xs tracking-widest',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
27
src/client/utils/error.ts
Normal file
27
src/client/utils/error.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { TRPCClientError } from '@trpc/client';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
/**
|
||||
* Show common error toast with auto process error object
|
||||
*/
|
||||
export function showErrorToast(err: any) {
|
||||
console.error(err);
|
||||
|
||||
if (err instanceof TRPCClientError) {
|
||||
try {
|
||||
const json = JSON.parse(err.message);
|
||||
toast.error(json[0].message);
|
||||
} catch {
|
||||
toast.error(err.message);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (err instanceof Error) {
|
||||
toast.error(err.message);
|
||||
return;
|
||||
}
|
||||
|
||||
toast.error(String(err));
|
||||
}
|
@ -21,7 +21,7 @@ export async function getUserCount(): Promise<number> {
|
||||
return count;
|
||||
}
|
||||
|
||||
const createUserSelect = {
|
||||
export const createUserSelect = {
|
||||
id: true,
|
||||
username: true,
|
||||
nickname: true,
|
||||
|
@ -1,5 +1,6 @@
|
||||
import {
|
||||
OpenApiMetaInfo,
|
||||
protectProedure,
|
||||
publicProcedure,
|
||||
router,
|
||||
workspaceOwnerProcedure,
|
||||
@ -7,13 +8,119 @@ import {
|
||||
} from '../trpc.js';
|
||||
import { z } from 'zod';
|
||||
import { prisma } from '../../model/_client.js';
|
||||
import { workspaceDashboardLayoutSchema } from '../../model/_schema/index.js';
|
||||
import {
|
||||
userInfoSchema,
|
||||
workspaceDashboardLayoutSchema,
|
||||
} from '../../model/_schema/index.js';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { OPENAPI_TAG } from '../../utils/const.js';
|
||||
import { OpenApiMeta } from 'trpc-openapi';
|
||||
import { getServerCount } from '../../model/serverStatus.js';
|
||||
import { ROLES, slugRegex } from '@tianji/shared';
|
||||
import { createUserSelect } from '../../model/user.js';
|
||||
|
||||
export const workspaceRouter = router({
|
||||
create: protectProedure
|
||||
.meta(
|
||||
buildWorkspaceOpenapi({
|
||||
method: 'POST',
|
||||
path: '/create',
|
||||
})
|
||||
)
|
||||
.input(
|
||||
z.object({
|
||||
name: z
|
||||
.string()
|
||||
.max(60)
|
||||
.min(4)
|
||||
.regex(slugRegex, { message: 'no a valid name' }),
|
||||
})
|
||||
)
|
||||
.output(userInfoSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { name } = input;
|
||||
const userId = ctx.user.id;
|
||||
|
||||
const existed = await prisma.workspace.findFirst({
|
||||
where: {
|
||||
name,
|
||||
},
|
||||
});
|
||||
|
||||
if (existed) {
|
||||
throw new Error('This workspace has been existed');
|
||||
}
|
||||
|
||||
const userInfo = await prisma.$transaction(async (p) => {
|
||||
const newWorkspace = await p.workspace.create({
|
||||
data: {
|
||||
name,
|
||||
},
|
||||
});
|
||||
|
||||
return await p.user.update({
|
||||
data: {
|
||||
currentWorkspaceId: newWorkspace.id,
|
||||
workspaces: {
|
||||
create: {
|
||||
workspaceId: newWorkspace.id,
|
||||
role: ROLES.owner,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
select: createUserSelect,
|
||||
});
|
||||
});
|
||||
|
||||
return userInfo;
|
||||
}),
|
||||
switch: protectProedure
|
||||
.meta(
|
||||
buildWorkspaceOpenapi({
|
||||
method: 'POST',
|
||||
path: '/switch',
|
||||
})
|
||||
)
|
||||
.input(
|
||||
z.object({
|
||||
workspaceId: z.string(),
|
||||
})
|
||||
)
|
||||
.output(userInfoSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const userId = ctx.user.id;
|
||||
const { workspaceId } = input;
|
||||
|
||||
const targetWorkspace = await prisma.workspace.findFirst({
|
||||
where: {
|
||||
id: workspaceId,
|
||||
users: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!targetWorkspace) {
|
||||
throw new Error('Target Workspace not found!');
|
||||
}
|
||||
|
||||
const userInfo = await prisma.user.update({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
data: {
|
||||
currentWorkspaceId: targetWorkspace.id,
|
||||
},
|
||||
select: createUserSelect,
|
||||
});
|
||||
|
||||
return userInfo;
|
||||
}),
|
||||
getUserWorkspaceRole: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
@ -40,7 +147,7 @@ export const workspaceRouter = router({
|
||||
.meta(
|
||||
buildWorkspaceOpenapi({
|
||||
method: 'GET',
|
||||
path: '/getServiceCount',
|
||||
path: '/{workspaceId}/getServiceCount',
|
||||
})
|
||||
)
|
||||
.output(
|
||||
@ -103,6 +210,9 @@ export const workspaceRouter = router({
|
||||
feed,
|
||||
};
|
||||
}),
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
updateDashboardOrder: workspaceOwnerProcedure
|
||||
.input(
|
||||
z.object({
|
||||
@ -121,6 +231,9 @@ export const workspaceRouter = router({
|
||||
},
|
||||
});
|
||||
}),
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
saveDashboardLayout: workspaceOwnerProcedure
|
||||
.input(
|
||||
z.object({
|
||||
@ -147,7 +260,7 @@ function buildWorkspaceOpenapi(meta: OpenApiMetaInfo): OpenApiMeta {
|
||||
tags: [OPENAPI_TAG.WORKSPACE],
|
||||
protect: true,
|
||||
...meta,
|
||||
path: `/workspace/{workspaceId}${meta.path}`,
|
||||
path: `/workspace/${meta.path}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user