feat: add delete workspace feature #96
This commit is contained in:
parent
943f7f594b
commit
2b9a14c969
@ -1,6 +1,12 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { cn } from '@/utils/style';
|
import { cn } from '@/utils/style';
|
||||||
import { setUserInfo, useUserInfo } from '@/store/user';
|
import {
|
||||||
|
changeUserCurrentWorkspace,
|
||||||
|
setUserInfo,
|
||||||
|
useCurrentWorkspace,
|
||||||
|
useCurrentWorkspaceSafe,
|
||||||
|
useUserInfo,
|
||||||
|
} from '@/store/user';
|
||||||
import { LuPlusCircle } from 'react-icons/lu';
|
import { LuPlusCircle } from 'react-icons/lu';
|
||||||
import { useTranslation } from '@i18next-toolkit/react';
|
import { useTranslation } from '@i18next-toolkit/react';
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from './ui/popover';
|
import { Popover, PopoverContent, PopoverTrigger } from './ui/popover';
|
||||||
@ -30,6 +36,7 @@ import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar';
|
|||||||
import { trpc } from '@/api/trpc';
|
import { trpc } from '@/api/trpc';
|
||||||
import { showErrorToast } from '@/utils/error';
|
import { showErrorToast } from '@/utils/error';
|
||||||
import { first, upperCase } from 'lodash-es';
|
import { first, upperCase } from 'lodash-es';
|
||||||
|
import { Empty } from 'antd';
|
||||||
|
|
||||||
interface WorkspaceSwitcherProps {
|
interface WorkspaceSwitcherProps {
|
||||||
isCollapsed: boolean;
|
isCollapsed: boolean;
|
||||||
@ -41,6 +48,7 @@ export const WorkspaceSwitcher: React.FC<WorkspaceSwitcherProps> = React.memo(
|
|||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(false);
|
||||||
const [showNewWorkspaceDialog, setShowNewWorkspaceDialog] = useState(false);
|
const [showNewWorkspaceDialog, setShowNewWorkspaceDialog] = useState(false);
|
||||||
const [newWorkspaceName, setNewWorkspaceName] = useState('');
|
const [newWorkspaceName, setNewWorkspaceName] = useState('');
|
||||||
|
const currentWorkspace = useCurrentWorkspaceSafe();
|
||||||
const createWorkspaceMutation = trpc.workspace.create.useMutation({
|
const createWorkspaceMutation = trpc.workspace.create.useMutation({
|
||||||
onSuccess: (userInfo) => {
|
onSuccess: (userInfo) => {
|
||||||
setUserInfo(userInfo);
|
setUserInfo(userInfo);
|
||||||
@ -56,7 +64,7 @@ export const WorkspaceSwitcher: React.FC<WorkspaceSwitcherProps> = React.memo(
|
|||||||
async (workspace: { id: string; name: string }) => {
|
async (workspace: { id: string; name: string }) => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
|
|
||||||
if (userInfo?.currentWorkspace.id === workspace.id) {
|
if (userInfo?.currentWorkspaceId === workspace.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,6 +72,7 @@ export const WorkspaceSwitcher: React.FC<WorkspaceSwitcherProps> = React.memo(
|
|||||||
await switchWorkspaceMutation.mutateAsync({
|
await switchWorkspaceMutation.mutateAsync({
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
});
|
});
|
||||||
|
changeUserCurrentWorkspace(workspace.id);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showErrorToast(err);
|
showErrorToast(err);
|
||||||
}
|
}
|
||||||
@ -88,8 +97,6 @@ export const WorkspaceSwitcher: React.FC<WorkspaceSwitcherProps> = React.memo(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentWorkspace = userInfo.currentWorkspace;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
open={showNewWorkspaceDialog}
|
open={showNewWorkspaceDialog}
|
||||||
@ -106,27 +113,33 @@ export const WorkspaceSwitcher: React.FC<WorkspaceSwitcherProps> = React.memo(
|
|||||||
props.isCollapsed && 'h-9 w-9 items-center justify-center p-0'
|
props.isCollapsed && 'h-9 w-9 items-center justify-center p-0'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Avatar
|
{currentWorkspace ? (
|
||||||
className={cn('h-5 w-5', props.isCollapsed ? '' : 'mr-2')}
|
<>
|
||||||
>
|
<Avatar
|
||||||
<AvatarImage
|
className={cn('h-5 w-5', props.isCollapsed ? '' : 'mr-2')}
|
||||||
src={`https://avatar.vercel.sh/${currentWorkspace.name}.png`}
|
>
|
||||||
alt={currentWorkspace.name}
|
<AvatarImage
|
||||||
className="grayscale"
|
src={`https://avatar.vercel.sh/${currentWorkspace.name}.png`}
|
||||||
/>
|
alt={currentWorkspace.name}
|
||||||
<AvatarFallback>
|
className="grayscale"
|
||||||
{upperCase(first(currentWorkspace.name))}
|
/>
|
||||||
</AvatarFallback>
|
<AvatarFallback>
|
||||||
</Avatar>
|
{upperCase(first(currentWorkspace.name))}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex-1 overflow-hidden text-ellipsis text-left',
|
'flex-1 overflow-hidden text-ellipsis text-left',
|
||||||
props.isCollapsed && 'hidden'
|
props.isCollapsed && 'hidden'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{currentWorkspace.name}
|
{currentWorkspace.name}
|
||||||
</span>
|
</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span>{t('Select Workspace')}</span>
|
||||||
|
)}
|
||||||
|
|
||||||
<CaretSortIcon
|
<CaretSortIcon
|
||||||
className={cn(
|
className={cn(
|
||||||
@ -141,6 +154,15 @@ export const WorkspaceSwitcher: React.FC<WorkspaceSwitcherProps> = React.memo(
|
|||||||
<CommandList>
|
<CommandList>
|
||||||
<CommandEmpty>{t('No workspace found.')}</CommandEmpty>
|
<CommandEmpty>{t('No workspace found.')}</CommandEmpty>
|
||||||
<CommandGroup key="workspace" heading={t('Workspace')}>
|
<CommandGroup key="workspace" heading={t('Workspace')}>
|
||||||
|
{userInfo.workspaces.length === 0 && (
|
||||||
|
<Empty
|
||||||
|
imageStyle={{ width: 80, height: 80, margin: 'auto' }}
|
||||||
|
description={t(
|
||||||
|
'Not any workspace has been found, please create first'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{userInfo.workspaces.map(({ workspace }) => (
|
{userInfo.workspaces.map(({ workspace }) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={workspace.id}
|
key={workspace.id}
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { useUserInfo } from '@/store/user';
|
import { useCurrentWorkspaceSafe } from '@/store/user';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
|
|
||||||
export const LayoutHeader: React.FC = React.memo(() => {
|
export const LayoutHeader: React.FC = React.memo(() => {
|
||||||
const userInfo = useUserInfo();
|
const currentWorkspace = useCurrentWorkspaceSafe();
|
||||||
let title = 'Tianji - Insight into everything';
|
let title = 'Tianji - Insight into everything';
|
||||||
if (userInfo) {
|
if (currentWorkspace) {
|
||||||
title = userInfo.currentWorkspace.name + ' | ' + title;
|
title = currentWorkspace.name + ' | ' + title;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -18,6 +18,7 @@ import { useEvent } from '@/hooks/useEvent';
|
|||||||
import { useSettingsStore } from '@/store/settings';
|
import { useSettingsStore } from '@/store/settings';
|
||||||
import {
|
import {
|
||||||
setUserInfo,
|
setUserInfo,
|
||||||
|
useCurrentWorkspace,
|
||||||
useCurrentWorkspaceId,
|
useCurrentWorkspaceId,
|
||||||
useUserInfo,
|
useUserInfo,
|
||||||
useUserStore,
|
useUserStore,
|
||||||
@ -41,6 +42,7 @@ export const UserConfig: React.FC<UserConfigProps> = React.memo((props) => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const colorScheme = useSettingsStore((state) => state.colorScheme);
|
const colorScheme = useSettingsStore((state) => state.colorScheme);
|
||||||
const workspaceId = useCurrentWorkspaceId();
|
const workspaceId = useCurrentWorkspaceId();
|
||||||
|
const currentWorkspace = useCurrentWorkspace();
|
||||||
const workspaces = useUserStore((state) => {
|
const workspaces = useUserStore((state) => {
|
||||||
const userInfo = state.info;
|
const userInfo = state.info;
|
||||||
if (userInfo) {
|
if (userInfo) {
|
||||||
@ -48,7 +50,7 @@ export const UserConfig: React.FC<UserConfigProps> = React.memo((props) => {
|
|||||||
id: w.workspace.id,
|
id: w.workspace.id,
|
||||||
name: w.workspace.name,
|
name: w.workspace.name,
|
||||||
role: w.role,
|
role: w.role,
|
||||||
current: userInfo.currentWorkspace?.id === w.workspace.id,
|
current: currentWorkspace.id === w.workspace.id,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Button, Form, Input, message, Popconfirm } from 'antd';
|
import { Form, Input, message } from 'antd';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { deleteWorkspaceWebsite } from '../../api/model/website';
|
import { deleteWorkspaceWebsite } from '../../api/model/website';
|
||||||
import { useRequest } from '../../hooks/useRequest';
|
import { useRequest } from '../../hooks/useRequest';
|
||||||
@ -18,6 +18,8 @@ import { useTranslation } from '@i18next-toolkit/react';
|
|||||||
import { useNavigate } from '@tanstack/react-router';
|
import { useNavigate } from '@tanstack/react-router';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
|
||||||
import { AlertConfirm } from '../AlertConfirm';
|
import { AlertConfirm } from '../AlertConfirm';
|
||||||
|
import { Button } from '../ui/button';
|
||||||
|
import { Card, CardContent, CardHeader } from '../ui/card';
|
||||||
|
|
||||||
export const WebsiteConfig: React.FC<{ websiteId: string }> = React.memo(
|
export const WebsiteConfig: React.FC<{ websiteId: string }> = React.memo(
|
||||||
(props) => {
|
(props) => {
|
||||||
@ -51,6 +53,14 @@ export const WebsiteConfig: React.FC<{ websiteId: string }> = React.memo(
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
websiteId,
|
websiteId,
|
||||||
});
|
});
|
||||||
|
trpcUtils.website.all.refetch({ workspaceId });
|
||||||
|
|
||||||
|
navigate({
|
||||||
|
to: '/website/$websiteId',
|
||||||
|
params: {
|
||||||
|
websiteId,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -135,21 +145,28 @@ export const WebsiteConfig: React.FC<{ websiteId: string }> = React.memo(
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Button size="large" htmlType="submit">
|
<Button type="submit">{t('Save')}</Button>
|
||||||
{t('Save')}
|
|
||||||
</Button>
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="data">
|
<TabsContent value="data">
|
||||||
<AlertConfirm
|
<Card>
|
||||||
title={t('Delete Website')}
|
<CardHeader className="text-lg font-bold">
|
||||||
onConfirm={() => handleDeleteWebsite()}
|
{t('Danger Zone')}
|
||||||
>
|
</CardHeader>
|
||||||
<Button type="primary" danger={true}>
|
<CardContent>
|
||||||
{t('Delete Website')}
|
<div>
|
||||||
</Button>
|
<AlertConfirm
|
||||||
</AlertConfirm>
|
title={t('Delete Website')}
|
||||||
|
onConfirm={() => handleDeleteWebsite()}
|
||||||
|
>
|
||||||
|
<Button variant="destructive">
|
||||||
|
{t('Delete Website')}
|
||||||
|
</Button>
|
||||||
|
</AlertConfirm>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
import { Route as rootRoute } from './routes/__root'
|
import { Route as rootRoute } from './routes/__root'
|
||||||
import { Route as WebsiteImport } from './routes/website'
|
import { Route as WebsiteImport } from './routes/website'
|
||||||
import { Route as TelemetryImport } from './routes/telemetry'
|
import { Route as TelemetryImport } from './routes/telemetry'
|
||||||
|
import { Route as SwitchWorkspaceImport } from './routes/switchWorkspace'
|
||||||
import { Route as SurveyImport } from './routes/survey'
|
import { Route as SurveyImport } from './routes/survey'
|
||||||
import { Route as SettingsImport } from './routes/settings'
|
import { Route as SettingsImport } from './routes/settings'
|
||||||
import { Route as ServerImport } from './routes/server'
|
import { Route as ServerImport } from './routes/server'
|
||||||
@ -58,6 +59,11 @@ const TelemetryRoute = TelemetryImport.update({
|
|||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
|
const SwitchWorkspaceRoute = SwitchWorkspaceImport.update({
|
||||||
|
path: '/switchWorkspace',
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
const SurveyRoute = SurveyImport.update({
|
const SurveyRoute = SurveyImport.update({
|
||||||
path: '/survey',
|
path: '/survey',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
@ -258,6 +264,10 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof SurveyImport
|
preLoaderRoute: typeof SurveyImport
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute
|
||||||
}
|
}
|
||||||
|
'/switchWorkspace': {
|
||||||
|
preLoaderRoute: typeof SwitchWorkspaceImport
|
||||||
|
parentRoute: typeof rootRoute
|
||||||
|
}
|
||||||
'/telemetry': {
|
'/telemetry': {
|
||||||
preLoaderRoute: typeof TelemetryImport
|
preLoaderRoute: typeof TelemetryImport
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute
|
||||||
@ -391,6 +401,7 @@ export const routeTree = rootRoute.addChildren([
|
|||||||
SurveySurveyIdEditRoute,
|
SurveySurveyIdEditRoute,
|
||||||
SurveySurveyIdIndexRoute,
|
SurveySurveyIdIndexRoute,
|
||||||
]),
|
]),
|
||||||
|
SwitchWorkspaceRoute,
|
||||||
TelemetryRoute.addChildren([TelemetryTelemetryIdRoute, TelemetryAddRoute]),
|
TelemetryRoute.addChildren([TelemetryTelemetryIdRoute, TelemetryAddRoute]),
|
||||||
WebsiteRoute.addChildren([
|
WebsiteRoute.addChildren([
|
||||||
WebsiteAddRoute,
|
WebsiteAddRoute,
|
||||||
|
@ -37,6 +37,8 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { useEventWithLoading } from '@/hooks/useEvent';
|
import { useEventWithLoading } from '@/hooks/useEvent';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { AlertConfirm } from '@/components/AlertConfirm';
|
||||||
|
import { ROLES } from '@tianji/shared';
|
||||||
|
|
||||||
export const Route = createFileRoute('/settings/workspace')({
|
export const Route = createFileRoute('/settings/workspace')({
|
||||||
beforeLoad: routeAuthBeforeLoad,
|
beforeLoad: routeAuthBeforeLoad,
|
||||||
@ -54,7 +56,7 @@ const columnHelper = createColumnHelper<MemberInfo>();
|
|||||||
|
|
||||||
function PageComponent() {
|
function PageComponent() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { id: workspaceId, name } = useCurrentWorkspace();
|
const { id: workspaceId, name, role } = useCurrentWorkspace();
|
||||||
const { data: members = [], refetch: refetchMembers } =
|
const { data: members = [], refetch: refetchMembers } =
|
||||||
trpc.workspace.members.useQuery({
|
trpc.workspace.members.useQuery({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
@ -69,6 +71,10 @@ function PageComponent() {
|
|||||||
onSuccess: defaultSuccessHandler,
|
onSuccess: defaultSuccessHandler,
|
||||||
onError: defaultErrorHandler,
|
onError: defaultErrorHandler,
|
||||||
});
|
});
|
||||||
|
const deleteWorkspaceMutation = trpc.workspace.delete.useMutation({
|
||||||
|
onSuccess: defaultSuccessHandler,
|
||||||
|
onError: defaultErrorHandler,
|
||||||
|
});
|
||||||
|
|
||||||
const [handleInvite, isLoading] = useEventWithLoading(
|
const [handleInvite, isLoading] = useEventWithLoading(
|
||||||
async (values: InviteFormValues) => {
|
async (values: InviteFormValues) => {
|
||||||
@ -173,6 +179,41 @@ function PageComponent() {
|
|||||||
<DataTable columns={columns} data={members} />
|
<DataTable columns={columns} data={members} />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{role === ROLES.owner && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="text-lg font-bold">
|
||||||
|
{t('Danger Zone')}
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div>
|
||||||
|
<AlertConfirm
|
||||||
|
title={'Confirm to delete this workspace'}
|
||||||
|
description={t(
|
||||||
|
'All content in this workspace will be destory and can not recover.'
|
||||||
|
)}
|
||||||
|
onConfirm={async () => {
|
||||||
|
await deleteWorkspaceMutation.mutateAsync({
|
||||||
|
workspaceId,
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload();
|
||||||
|
}, 1000);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
loading={deleteWorkspaceMutation.isLoading}
|
||||||
|
variant="destructive"
|
||||||
|
>
|
||||||
|
{t('Delete Workspace')}
|
||||||
|
</Button>
|
||||||
|
</AlertConfirm>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</CommonWrapper>
|
</CommonWrapper>
|
||||||
|
79
src/client/routes/switchWorkspace.tsx
Normal file
79
src/client/routes/switchWorkspace.tsx
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import {
|
||||||
|
createFileRoute,
|
||||||
|
redirect,
|
||||||
|
useNavigate,
|
||||||
|
useSearch,
|
||||||
|
} from '@tanstack/react-router';
|
||||||
|
import { useTranslation } from '@i18next-toolkit/react';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { useCurrentWorkspaceSafe, type UserLoginInfo } from '../store/user';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardFooter,
|
||||||
|
CardHeader,
|
||||||
|
} from '@/components/ui/card';
|
||||||
|
import { WorkspaceSwitcher } from '@/components/WorkspaceSwitcher';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { useEvent } from '@/hooks/useEvent';
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/switchWorkspace')({
|
||||||
|
validateSearch: z.object({
|
||||||
|
// redirect: z.string().catch('/'),
|
||||||
|
redirect: z.string().optional(),
|
||||||
|
}),
|
||||||
|
beforeLoad: ({ context }) => {
|
||||||
|
const userInfo: UserLoginInfo | undefined = (context as any).userInfo;
|
||||||
|
|
||||||
|
if (
|
||||||
|
userInfo &&
|
||||||
|
userInfo.currentWorkspaceId &&
|
||||||
|
userInfo.workspaces.some(
|
||||||
|
(w) => w.workspace.id === userInfo.currentWorkspaceId
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw redirect({
|
||||||
|
to: '/',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
component: PageComponent,
|
||||||
|
});
|
||||||
|
|
||||||
|
function PageComponent() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const currentWorkspace = useCurrentWorkspaceSafe();
|
||||||
|
const search = Route.useSearch();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleEnter = useEvent(() => {
|
||||||
|
navigate({
|
||||||
|
to: search.redirect ?? '/',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<Card className="min-w-[320px] bg-zinc-50 dark:bg-zinc-900">
|
||||||
|
<CardHeader>
|
||||||
|
<div className="text-center">
|
||||||
|
<img className="m-auto h-24 w-24" src="/icon.svg" />
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="text-center">
|
||||||
|
<div className="mb-2 text-lg font-bold">{t('Select Workspace')}</div>
|
||||||
|
|
||||||
|
<WorkspaceSwitcher isCollapsed={false} />
|
||||||
|
</CardContent>
|
||||||
|
|
||||||
|
{currentWorkspace && (
|
||||||
|
<CardFooter className="justify-end">
|
||||||
|
<Button size="sm" onClick={handleEnter}>
|
||||||
|
{t('Enter')}
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,32 +1,29 @@
|
|||||||
import { create } from 'zustand';
|
import { shallow } from 'zustand/shallow';
|
||||||
|
import { createWithEqualityFn } from 'zustand/traditional';
|
||||||
import { createSocketIOClient } from '../api/socketio';
|
import { createSocketIOClient } from '../api/socketio';
|
||||||
import { AppRouterOutput } from '../api/trpc';
|
import { AppRouterOutput } from '../api/trpc';
|
||||||
|
|
||||||
type UserLoginInfo = NonNullable<AppRouterOutput['user']['info']>;
|
export type UserLoginInfo = NonNullable<AppRouterOutput['user']['info']>;
|
||||||
|
|
||||||
interface UserState {
|
interface UserState {
|
||||||
info: UserLoginInfo | null;
|
info: UserLoginInfo | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useUserStore = create<UserState>(() => ({
|
export const useUserStore = createWithEqualityFn<UserState>(
|
||||||
info: null,
|
() => ({
|
||||||
}));
|
info: null,
|
||||||
|
}),
|
||||||
|
shallow
|
||||||
|
);
|
||||||
|
|
||||||
export function setUserInfo(info: UserLoginInfo) {
|
export function setUserInfo(info: UserLoginInfo) {
|
||||||
if (!info.currentWorkspace && info.workspaces[0]) {
|
|
||||||
// Make sure currentWorkspace existed
|
|
||||||
info.currentWorkspace = {
|
|
||||||
...info.workspaces[0].workspace,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
useUserStore.setState({
|
useUserStore.setState({
|
||||||
info,
|
info,
|
||||||
});
|
});
|
||||||
|
|
||||||
// create socketio after login
|
// create socketio after login
|
||||||
if (info.currentWorkspace) {
|
if (info.currentWorkspaceId) {
|
||||||
createSocketIOClient(info.currentWorkspace.id);
|
createSocketIOClient(info.currentWorkspaceId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,10 +35,56 @@ export function useIsLogined() {
|
|||||||
return !!useUserInfo();
|
return !!useUserInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function changeUserCurrentWorkspace(currentWorkspaceId: string) {
|
||||||
|
const currentUserInfo = useUserStore.getState().info;
|
||||||
|
if (!currentUserInfo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
useUserStore.setState({
|
||||||
|
info: {
|
||||||
|
...currentUserInfo,
|
||||||
|
currentWorkspaceId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
createSocketIOClient(currentWorkspaceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCurrentWorkspaceSafe() {
|
||||||
|
const currentWorkspace = useUserStore((state) => {
|
||||||
|
if (!state.info) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentWorkspaceId = state.info.currentWorkspaceId;
|
||||||
|
if (!currentWorkspaceId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentWorkspace = state.info?.workspaces.find(
|
||||||
|
(w) => w.workspace.id === currentWorkspaceId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!currentWorkspace) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: currentWorkspace.workspace.id,
|
||||||
|
name: currentWorkspace.workspace.name,
|
||||||
|
role: currentWorkspace.role,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return currentWorkspace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Direct return current workspace info
|
||||||
|
* NOTICE: its will throw error if no workspace id
|
||||||
|
*/
|
||||||
export function useCurrentWorkspace() {
|
export function useCurrentWorkspace() {
|
||||||
const currentWorkspace = useUserStore(
|
const currentWorkspace = useCurrentWorkspaceSafe();
|
||||||
(state) => state.info?.currentWorkspace
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!currentWorkspace) {
|
if (!currentWorkspace) {
|
||||||
throw new Error('No Workspace Id');
|
throw new Error('No Workspace Id');
|
||||||
@ -50,9 +93,13 @@ export function useCurrentWorkspace() {
|
|||||||
return currentWorkspace;
|
return currentWorkspace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Direct return current workspace id
|
||||||
|
* NOTICE: its will throw error if no workspace id
|
||||||
|
*/
|
||||||
export function useCurrentWorkspaceId() {
|
export function useCurrentWorkspaceId() {
|
||||||
const currentWorkspaceId = useUserStore(
|
const currentWorkspaceId = useUserStore(
|
||||||
(state) => state.info?.currentWorkspace?.id
|
(state) => state.info?.currentWorkspaceId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!currentWorkspaceId) {
|
if (!currentWorkspaceId) {
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
|
import { UserLoginInfo } from '@/store/user';
|
||||||
import { FileBaseRouteOptions, redirect } from '@tanstack/react-router';
|
import { FileBaseRouteOptions, redirect } from '@tanstack/react-router';
|
||||||
|
|
||||||
export const routeAuthBeforeLoad: FileBaseRouteOptions['beforeLoad'] = ({
|
export const routeAuthBeforeLoad: FileBaseRouteOptions['beforeLoad'] = ({
|
||||||
context,
|
context,
|
||||||
location,
|
location,
|
||||||
}) => {
|
}) => {
|
||||||
if (!(context as any).userInfo) {
|
const userInfo: UserLoginInfo | undefined = (context as any).userInfo;
|
||||||
|
if (!userInfo) {
|
||||||
throw redirect({
|
throw redirect({
|
||||||
to: '/login',
|
to: '/login',
|
||||||
search: {
|
search: {
|
||||||
@ -12,4 +14,18 @@ export const routeAuthBeforeLoad: FileBaseRouteOptions['beforeLoad'] = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!userInfo.currentWorkspaceId ||
|
||||||
|
userInfo.workspaces.every(
|
||||||
|
(w) => w.workspace.id !== userInfo.currentWorkspaceId
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw redirect({
|
||||||
|
to: '/switchWorkspace',
|
||||||
|
search: {
|
||||||
|
redirect: location.href,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
@ -31,7 +31,7 @@ export const userInfoSchema = z.object({
|
|||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
deletedAt: z.date().nullable(),
|
deletedAt: z.date().nullable(),
|
||||||
currentWorkspace: workspaceSchema,
|
currentWorkspaceId: z.string().nullable(),
|
||||||
workspaces: z.array(
|
workspaces: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
role: z.string(),
|
role: z.string(),
|
||||||
|
@ -33,12 +33,7 @@ export const createUserSelect = {
|
|||||||
createdAt: true,
|
createdAt: true,
|
||||||
updatedAt: true,
|
updatedAt: true,
|
||||||
deletedAt: true,
|
deletedAt: true,
|
||||||
currentWorkspace: {
|
currentWorkspaceId: true,
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
workspaces: {
|
workspaces: {
|
||||||
select: {
|
select: {
|
||||||
role: true,
|
role: true,
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "User" DROP CONSTRAINT "User_currentWorkspaceId_fkey";
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "User" ALTER COLUMN "currentWorkspaceId" DROP NOT NULL;
|
@ -29,9 +29,7 @@ model User {
|
|||||||
createdAt DateTime @default(now()) @db.Timestamptz(6)
|
createdAt DateTime @default(now()) @db.Timestamptz(6)
|
||||||
updatedAt DateTime @updatedAt @db.Timestamptz(6)
|
updatedAt DateTime @updatedAt @db.Timestamptz(6)
|
||||||
deletedAt DateTime? @db.Timestamptz(6)
|
deletedAt DateTime? @db.Timestamptz(6)
|
||||||
currentWorkspaceId String @db.VarChar(30)
|
currentWorkspaceId String? @db.VarChar(30)
|
||||||
|
|
||||||
currentWorkspace Workspace @relation(fields: [currentWorkspaceId], references: [id])
|
|
||||||
|
|
||||||
accounts Account[]
|
accounts Account[]
|
||||||
sessions Session[]
|
sessions Session[]
|
||||||
@ -97,7 +95,6 @@ model Workspace {
|
|||||||
telemetryList Telemetry[]
|
telemetryList Telemetry[]
|
||||||
|
|
||||||
// for user currentWorkspace
|
// for user currentWorkspace
|
||||||
selectedUsers User[] // user list who select this workspace, not use in most of case
|
|
||||||
workspaceDailyUsage WorkspaceDailyUsage[]
|
workspaceDailyUsage WorkspaceDailyUsage[]
|
||||||
workspaceAuditLog WorkspaceAuditLog[]
|
workspaceAuditLog WorkspaceAuditLog[]
|
||||||
surveys Survey[]
|
surveys Survey[]
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import * as z from "zod"
|
import * as z from "zod"
|
||||||
import * as imports from "./schemas/index.js"
|
import * as imports from "./schemas/index.js"
|
||||||
import { CompleteWorkspace, RelatedWorkspaceModelSchema, CompleteAccount, RelatedAccountModelSchema, CompleteSession, RelatedSessionModelSchema, CompleteWorkspacesOnUsers, RelatedWorkspacesOnUsersModelSchema } from "./index.js"
|
import { CompleteAccount, RelatedAccountModelSchema, CompleteSession, RelatedSessionModelSchema, CompleteWorkspacesOnUsers, RelatedWorkspacesOnUsersModelSchema } from "./index.js"
|
||||||
|
|
||||||
export const UserModelSchema = z.object({
|
export const UserModelSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
@ -14,11 +14,10 @@ export const UserModelSchema = z.object({
|
|||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
deletedAt: z.date().nullish(),
|
deletedAt: z.date().nullish(),
|
||||||
currentWorkspaceId: z.string(),
|
currentWorkspaceId: z.string().nullish(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export interface CompleteUser extends z.infer<typeof UserModelSchema> {
|
export interface CompleteUser extends z.infer<typeof UserModelSchema> {
|
||||||
currentWorkspace: CompleteWorkspace
|
|
||||||
accounts: CompleteAccount[]
|
accounts: CompleteAccount[]
|
||||||
sessions: CompleteSession[]
|
sessions: CompleteSession[]
|
||||||
workspaces: CompleteWorkspacesOnUsers[]
|
workspaces: CompleteWorkspacesOnUsers[]
|
||||||
@ -30,7 +29,6 @@ export interface CompleteUser extends z.infer<typeof UserModelSchema> {
|
|||||||
* NOTE: Lazy required in case of potential circular dependencies within schema
|
* NOTE: Lazy required in case of potential circular dependencies within schema
|
||||||
*/
|
*/
|
||||||
export const RelatedUserModelSchema: z.ZodSchema<CompleteUser> = z.lazy(() => UserModelSchema.extend({
|
export const RelatedUserModelSchema: z.ZodSchema<CompleteUser> = z.lazy(() => UserModelSchema.extend({
|
||||||
currentWorkspace: RelatedWorkspaceModelSchema,
|
|
||||||
accounts: RelatedAccountModelSchema.array(),
|
accounts: RelatedAccountModelSchema.array(),
|
||||||
sessions: RelatedSessionModelSchema.array(),
|
sessions: RelatedSessionModelSchema.array(),
|
||||||
workspaces: RelatedWorkspacesOnUsersModelSchema.array(),
|
workspaces: RelatedWorkspacesOnUsersModelSchema.array(),
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import * as z from "zod"
|
import * as z from "zod"
|
||||||
import * as imports from "./schemas/index.js"
|
import * as imports from "./schemas/index.js"
|
||||||
import { CompleteWorkspacesOnUsers, RelatedWorkspacesOnUsersModelSchema, CompleteWebsite, RelatedWebsiteModelSchema, CompleteNotification, RelatedNotificationModelSchema, CompleteMonitor, RelatedMonitorModelSchema, CompleteMonitorStatusPage, RelatedMonitorStatusPageModelSchema, CompleteTelemetry, RelatedTelemetryModelSchema, CompleteUser, RelatedUserModelSchema, CompleteWorkspaceDailyUsage, RelatedWorkspaceDailyUsageModelSchema, CompleteWorkspaceAuditLog, RelatedWorkspaceAuditLogModelSchema, CompleteSurvey, RelatedSurveyModelSchema, CompleteFeedChannel, RelatedFeedChannelModelSchema } from "./index.js"
|
import { CompleteWorkspacesOnUsers, RelatedWorkspacesOnUsersModelSchema, CompleteWebsite, RelatedWebsiteModelSchema, CompleteNotification, RelatedNotificationModelSchema, CompleteMonitor, RelatedMonitorModelSchema, CompleteMonitorStatusPage, RelatedMonitorStatusPageModelSchema, CompleteTelemetry, RelatedTelemetryModelSchema, CompleteWorkspaceDailyUsage, RelatedWorkspaceDailyUsageModelSchema, CompleteWorkspaceAuditLog, RelatedWorkspaceAuditLogModelSchema, CompleteSurvey, RelatedSurveyModelSchema, CompleteFeedChannel, RelatedFeedChannelModelSchema } from "./index.js"
|
||||||
|
|
||||||
// Helper schema for JSON fields
|
// Helper schema for JSON fields
|
||||||
type Literal = boolean | number | string
|
type Literal = boolean | number | string
|
||||||
@ -31,7 +31,6 @@ export interface CompleteWorkspace extends z.infer<typeof WorkspaceModelSchema>
|
|||||||
monitors: CompleteMonitor[]
|
monitors: CompleteMonitor[]
|
||||||
monitorStatusPages: CompleteMonitorStatusPage[]
|
monitorStatusPages: CompleteMonitorStatusPage[]
|
||||||
telemetryList: CompleteTelemetry[]
|
telemetryList: CompleteTelemetry[]
|
||||||
selectedUsers: CompleteUser[]
|
|
||||||
workspaceDailyUsage: CompleteWorkspaceDailyUsage[]
|
workspaceDailyUsage: CompleteWorkspaceDailyUsage[]
|
||||||
workspaceAuditLog: CompleteWorkspaceAuditLog[]
|
workspaceAuditLog: CompleteWorkspaceAuditLog[]
|
||||||
surveys: CompleteSurvey[]
|
surveys: CompleteSurvey[]
|
||||||
@ -50,7 +49,6 @@ export const RelatedWorkspaceModelSchema: z.ZodSchema<CompleteWorkspace> = z.laz
|
|||||||
monitors: RelatedMonitorModelSchema.array(),
|
monitors: RelatedMonitorModelSchema.array(),
|
||||||
monitorStatusPages: RelatedMonitorStatusPageModelSchema.array(),
|
monitorStatusPages: RelatedMonitorStatusPageModelSchema.array(),
|
||||||
telemetryList: RelatedTelemetryModelSchema.array(),
|
telemetryList: RelatedTelemetryModelSchema.array(),
|
||||||
selectedUsers: RelatedUserModelSchema.array(),
|
|
||||||
workspaceDailyUsage: RelatedWorkspaceDailyUsageModelSchema.array(),
|
workspaceDailyUsage: RelatedWorkspaceDailyUsageModelSchema.array(),
|
||||||
workspaceAuditLog: RelatedWorkspaceAuditLogModelSchema.array(),
|
workspaceAuditLog: RelatedWorkspaceAuditLogModelSchema.array(),
|
||||||
surveys: RelatedSurveyModelSchema.array(),
|
surveys: RelatedSurveyModelSchema.array(),
|
||||||
|
@ -138,7 +138,7 @@ export const userRouter = router({
|
|||||||
info: protectProedure
|
info: protectProedure
|
||||||
.input(z.void())
|
.input(z.void())
|
||||||
.output(userInfoSchema.nullable())
|
.output(userInfoSchema.nullable())
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ ctx }) => {
|
||||||
return getUserInfo(ctx.user.id);
|
return getUserInfo(ctx.user.id);
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
@ -23,6 +23,7 @@ import {
|
|||||||
leaveWorkspace,
|
leaveWorkspace,
|
||||||
} from '../../model/user.js';
|
} from '../../model/user.js';
|
||||||
import { WorkspacesOnUsersModelSchema } from '../../prisma/zod/workspacesonusers.js';
|
import { WorkspacesOnUsersModelSchema } from '../../prisma/zod/workspacesonusers.js';
|
||||||
|
import { monitorManager } from '../../model/monitor/index.js';
|
||||||
|
|
||||||
export const workspaceRouter = router({
|
export const workspaceRouter = router({
|
||||||
create: protectProedure
|
create: protectProedure
|
||||||
@ -104,7 +105,7 @@ export const workspaceRouter = router({
|
|||||||
id: workspaceId,
|
id: workspaceId,
|
||||||
users: {
|
users: {
|
||||||
some: {
|
some: {
|
||||||
userId,
|
userId, // make sure is member of this workspace
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -143,6 +144,19 @@ export const workspaceRouter = router({
|
|||||||
const { workspaceId } = input;
|
const { workspaceId } = input;
|
||||||
const userId = ctx.user.id;
|
const userId = ctx.user.id;
|
||||||
|
|
||||||
|
const monitors = await prisma.monitor.findMany({
|
||||||
|
where: {
|
||||||
|
workspaceId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
monitors.map((m) => monitorManager.delete(workspaceId, m.id))
|
||||||
|
);
|
||||||
|
|
||||||
await prisma.workspace.delete({
|
await prisma.workspace.delete({
|
||||||
where: {
|
where: {
|
||||||
id: workspaceId,
|
id: workspaceId,
|
||||||
|
Loading…
Reference in New Issue
Block a user