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 { 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 { useTranslation } from '@i18next-toolkit/react';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from './ui/popover';
|
||||
@ -30,6 +36,7 @@ import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar';
|
||||
import { trpc } from '@/api/trpc';
|
||||
import { showErrorToast } from '@/utils/error';
|
||||
import { first, upperCase } from 'lodash-es';
|
||||
import { Empty } from 'antd';
|
||||
|
||||
interface WorkspaceSwitcherProps {
|
||||
isCollapsed: boolean;
|
||||
@ -41,6 +48,7 @@ export const WorkspaceSwitcher: React.FC<WorkspaceSwitcherProps> = React.memo(
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [showNewWorkspaceDialog, setShowNewWorkspaceDialog] = useState(false);
|
||||
const [newWorkspaceName, setNewWorkspaceName] = useState('');
|
||||
const currentWorkspace = useCurrentWorkspaceSafe();
|
||||
const createWorkspaceMutation = trpc.workspace.create.useMutation({
|
||||
onSuccess: (userInfo) => {
|
||||
setUserInfo(userInfo);
|
||||
@ -56,7 +64,7 @@ export const WorkspaceSwitcher: React.FC<WorkspaceSwitcherProps> = React.memo(
|
||||
async (workspace: { id: string; name: string }) => {
|
||||
setOpen(false);
|
||||
|
||||
if (userInfo?.currentWorkspace.id === workspace.id) {
|
||||
if (userInfo?.currentWorkspaceId === workspace.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -64,6 +72,7 @@ export const WorkspaceSwitcher: React.FC<WorkspaceSwitcherProps> = React.memo(
|
||||
await switchWorkspaceMutation.mutateAsync({
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
changeUserCurrentWorkspace(workspace.id);
|
||||
} catch (err) {
|
||||
showErrorToast(err);
|
||||
}
|
||||
@ -88,8 +97,6 @@ export const WorkspaceSwitcher: React.FC<WorkspaceSwitcherProps> = React.memo(
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentWorkspace = userInfo.currentWorkspace;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
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'
|
||||
)}
|
||||
>
|
||||
<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>
|
||||
{currentWorkspace ? (
|
||||
<>
|
||||
<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(
|
||||
'flex-1 overflow-hidden text-ellipsis text-left',
|
||||
props.isCollapsed && 'hidden'
|
||||
)}
|
||||
>
|
||||
{currentWorkspace.name}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
'flex-1 overflow-hidden text-ellipsis text-left',
|
||||
props.isCollapsed && 'hidden'
|
||||
)}
|
||||
>
|
||||
{currentWorkspace.name}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<span>{t('Select Workspace')}</span>
|
||||
)}
|
||||
|
||||
<CaretSortIcon
|
||||
className={cn(
|
||||
@ -141,6 +154,15 @@ export const WorkspaceSwitcher: React.FC<WorkspaceSwitcherProps> = React.memo(
|
||||
<CommandList>
|
||||
<CommandEmpty>{t('No workspace found.')}</CommandEmpty>
|
||||
<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 }) => (
|
||||
<CommandItem
|
||||
key={workspace.id}
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { useUserInfo } from '@/store/user';
|
||||
import { useCurrentWorkspaceSafe } from '@/store/user';
|
||||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
export const LayoutHeader: React.FC = React.memo(() => {
|
||||
const userInfo = useUserInfo();
|
||||
const currentWorkspace = useCurrentWorkspaceSafe();
|
||||
let title = 'Tianji - Insight into everything';
|
||||
if (userInfo) {
|
||||
title = userInfo.currentWorkspace.name + ' | ' + title;
|
||||
if (currentWorkspace) {
|
||||
title = currentWorkspace.name + ' | ' + title;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -18,6 +18,7 @@ import { useEvent } from '@/hooks/useEvent';
|
||||
import { useSettingsStore } from '@/store/settings';
|
||||
import {
|
||||
setUserInfo,
|
||||
useCurrentWorkspace,
|
||||
useCurrentWorkspaceId,
|
||||
useUserInfo,
|
||||
useUserStore,
|
||||
@ -41,6 +42,7 @@ export const UserConfig: React.FC<UserConfigProps> = React.memo((props) => {
|
||||
const navigate = useNavigate();
|
||||
const colorScheme = useSettingsStore((state) => state.colorScheme);
|
||||
const workspaceId = useCurrentWorkspaceId();
|
||||
const currentWorkspace = useCurrentWorkspace();
|
||||
const workspaces = useUserStore((state) => {
|
||||
const userInfo = state.info;
|
||||
if (userInfo) {
|
||||
@ -48,7 +50,7 @@ export const UserConfig: React.FC<UserConfigProps> = React.memo((props) => {
|
||||
id: w.workspace.id,
|
||||
name: w.workspace.name,
|
||||
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 { deleteWorkspaceWebsite } from '../../api/model/website';
|
||||
import { useRequest } from '../../hooks/useRequest';
|
||||
@ -18,6 +18,8 @@ import { useTranslation } from '@i18next-toolkit/react';
|
||||
import { useNavigate } from '@tanstack/react-router';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
|
||||
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(
|
||||
(props) => {
|
||||
@ -51,6 +53,14 @@ export const WebsiteConfig: React.FC<{ websiteId: string }> = React.memo(
|
||||
workspaceId,
|
||||
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>
|
||||
<Button size="large" htmlType="submit">
|
||||
{t('Save')}
|
||||
</Button>
|
||||
<Button type="submit">{t('Save')}</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</TabsContent>
|
||||
<TabsContent value="data">
|
||||
<AlertConfirm
|
||||
title={t('Delete Website')}
|
||||
onConfirm={() => handleDeleteWebsite()}
|
||||
>
|
||||
<Button type="primary" danger={true}>
|
||||
{t('Delete Website')}
|
||||
</Button>
|
||||
</AlertConfirm>
|
||||
<Card>
|
||||
<CardHeader className="text-lg font-bold">
|
||||
{t('Danger Zone')}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div>
|
||||
<AlertConfirm
|
||||
title={t('Delete Website')}
|
||||
onConfirm={() => handleDeleteWebsite()}
|
||||
>
|
||||
<Button variant="destructive">
|
||||
{t('Delete Website')}
|
||||
</Button>
|
||||
</AlertConfirm>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
@ -13,6 +13,7 @@
|
||||
import { Route as rootRoute } from './routes/__root'
|
||||
import { Route as WebsiteImport } from './routes/website'
|
||||
import { Route as TelemetryImport } from './routes/telemetry'
|
||||
import { Route as SwitchWorkspaceImport } from './routes/switchWorkspace'
|
||||
import { Route as SurveyImport } from './routes/survey'
|
||||
import { Route as SettingsImport } from './routes/settings'
|
||||
import { Route as ServerImport } from './routes/server'
|
||||
@ -58,6 +59,11 @@ const TelemetryRoute = TelemetryImport.update({
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const SwitchWorkspaceRoute = SwitchWorkspaceImport.update({
|
||||
path: '/switchWorkspace',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const SurveyRoute = SurveyImport.update({
|
||||
path: '/survey',
|
||||
getParentRoute: () => rootRoute,
|
||||
@ -258,6 +264,10 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof SurveyImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/switchWorkspace': {
|
||||
preLoaderRoute: typeof SwitchWorkspaceImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/telemetry': {
|
||||
preLoaderRoute: typeof TelemetryImport
|
||||
parentRoute: typeof rootRoute
|
||||
@ -391,6 +401,7 @@ export const routeTree = rootRoute.addChildren([
|
||||
SurveySurveyIdEditRoute,
|
||||
SurveySurveyIdIndexRoute,
|
||||
]),
|
||||
SwitchWorkspaceRoute,
|
||||
TelemetryRoute.addChildren([TelemetryTelemetryIdRoute, TelemetryAddRoute]),
|
||||
WebsiteRoute.addChildren([
|
||||
WebsiteAddRoute,
|
||||
|
@ -37,6 +37,8 @@ import { Button } from '@/components/ui/button';
|
||||
import { useEventWithLoading } from '@/hooks/useEvent';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
import { AlertConfirm } from '@/components/AlertConfirm';
|
||||
import { ROLES } from '@tianji/shared';
|
||||
|
||||
export const Route = createFileRoute('/settings/workspace')({
|
||||
beforeLoad: routeAuthBeforeLoad,
|
||||
@ -54,7 +56,7 @@ const columnHelper = createColumnHelper<MemberInfo>();
|
||||
|
||||
function PageComponent() {
|
||||
const { t } = useTranslation();
|
||||
const { id: workspaceId, name } = useCurrentWorkspace();
|
||||
const { id: workspaceId, name, role } = useCurrentWorkspace();
|
||||
const { data: members = [], refetch: refetchMembers } =
|
||||
trpc.workspace.members.useQuery({
|
||||
workspaceId,
|
||||
@ -69,6 +71,10 @@ function PageComponent() {
|
||||
onSuccess: defaultSuccessHandler,
|
||||
onError: defaultErrorHandler,
|
||||
});
|
||||
const deleteWorkspaceMutation = trpc.workspace.delete.useMutation({
|
||||
onSuccess: defaultSuccessHandler,
|
||||
onError: defaultErrorHandler,
|
||||
});
|
||||
|
||||
const [handleInvite, isLoading] = useEventWithLoading(
|
||||
async (values: InviteFormValues) => {
|
||||
@ -173,6 +179,41 @@ function PageComponent() {
|
||||
<DataTable columns={columns} data={members} />
|
||||
</CardContent>
|
||||
</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>
|
||||
</ScrollArea>
|
||||
</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 { AppRouterOutput } from '../api/trpc';
|
||||
|
||||
type UserLoginInfo = NonNullable<AppRouterOutput['user']['info']>;
|
||||
export type UserLoginInfo = NonNullable<AppRouterOutput['user']['info']>;
|
||||
|
||||
interface UserState {
|
||||
info: UserLoginInfo | null;
|
||||
}
|
||||
|
||||
export const useUserStore = create<UserState>(() => ({
|
||||
info: null,
|
||||
}));
|
||||
export const useUserStore = createWithEqualityFn<UserState>(
|
||||
() => ({
|
||||
info: null,
|
||||
}),
|
||||
shallow
|
||||
);
|
||||
|
||||
export function setUserInfo(info: UserLoginInfo) {
|
||||
if (!info.currentWorkspace && info.workspaces[0]) {
|
||||
// Make sure currentWorkspace existed
|
||||
info.currentWorkspace = {
|
||||
...info.workspaces[0].workspace,
|
||||
};
|
||||
}
|
||||
|
||||
useUserStore.setState({
|
||||
info,
|
||||
});
|
||||
|
||||
// create socketio after login
|
||||
if (info.currentWorkspace) {
|
||||
createSocketIOClient(info.currentWorkspace.id);
|
||||
if (info.currentWorkspaceId) {
|
||||
createSocketIOClient(info.currentWorkspaceId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,10 +35,56 @@ export function useIsLogined() {
|
||||
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() {
|
||||
const currentWorkspace = useUserStore(
|
||||
(state) => state.info?.currentWorkspace
|
||||
);
|
||||
const currentWorkspace = useCurrentWorkspaceSafe();
|
||||
|
||||
if (!currentWorkspace) {
|
||||
throw new Error('No Workspace Id');
|
||||
@ -50,9 +93,13 @@ export function useCurrentWorkspace() {
|
||||
return currentWorkspace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Direct return current workspace id
|
||||
* NOTICE: its will throw error if no workspace id
|
||||
*/
|
||||
export function useCurrentWorkspaceId() {
|
||||
const currentWorkspaceId = useUserStore(
|
||||
(state) => state.info?.currentWorkspace?.id
|
||||
(state) => state.info?.currentWorkspaceId
|
||||
);
|
||||
|
||||
if (!currentWorkspaceId) {
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { UserLoginInfo } from '@/store/user';
|
||||
import { FileBaseRouteOptions, redirect } from '@tanstack/react-router';
|
||||
|
||||
export const routeAuthBeforeLoad: FileBaseRouteOptions['beforeLoad'] = ({
|
||||
context,
|
||||
location,
|
||||
}) => {
|
||||
if (!(context as any).userInfo) {
|
||||
const userInfo: UserLoginInfo | undefined = (context as any).userInfo;
|
||||
if (!userInfo) {
|
||||
throw redirect({
|
||||
to: '/login',
|
||||
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(),
|
||||
updatedAt: z.date(),
|
||||
deletedAt: z.date().nullable(),
|
||||
currentWorkspace: workspaceSchema,
|
||||
currentWorkspaceId: z.string().nullable(),
|
||||
workspaces: z.array(
|
||||
z.object({
|
||||
role: z.string(),
|
||||
|
@ -33,12 +33,7 @@ export const createUserSelect = {
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
deletedAt: true,
|
||||
currentWorkspace: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
currentWorkspaceId: true,
|
||||
workspaces: {
|
||||
select: {
|
||||
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)
|
||||
updatedAt DateTime @updatedAt @db.Timestamptz(6)
|
||||
deletedAt DateTime? @db.Timestamptz(6)
|
||||
currentWorkspaceId String @db.VarChar(30)
|
||||
|
||||
currentWorkspace Workspace @relation(fields: [currentWorkspaceId], references: [id])
|
||||
currentWorkspaceId String? @db.VarChar(30)
|
||||
|
||||
accounts Account[]
|
||||
sessions Session[]
|
||||
@ -97,7 +95,6 @@ model Workspace {
|
||||
telemetryList Telemetry[]
|
||||
|
||||
// for user currentWorkspace
|
||||
selectedUsers User[] // user list who select this workspace, not use in most of case
|
||||
workspaceDailyUsage WorkspaceDailyUsage[]
|
||||
workspaceAuditLog WorkspaceAuditLog[]
|
||||
surveys Survey[]
|
||||
|
@ -1,6 +1,6 @@
|
||||
import * as z from "zod"
|
||||
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({
|
||||
id: z.string(),
|
||||
@ -14,11 +14,10 @@ export const UserModelSchema = z.object({
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
deletedAt: z.date().nullish(),
|
||||
currentWorkspaceId: z.string(),
|
||||
currentWorkspaceId: z.string().nullish(),
|
||||
})
|
||||
|
||||
export interface CompleteUser extends z.infer<typeof UserModelSchema> {
|
||||
currentWorkspace: CompleteWorkspace
|
||||
accounts: CompleteAccount[]
|
||||
sessions: CompleteSession[]
|
||||
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
|
||||
*/
|
||||
export const RelatedUserModelSchema: z.ZodSchema<CompleteUser> = z.lazy(() => UserModelSchema.extend({
|
||||
currentWorkspace: RelatedWorkspaceModelSchema,
|
||||
accounts: RelatedAccountModelSchema.array(),
|
||||
sessions: RelatedSessionModelSchema.array(),
|
||||
workspaces: RelatedWorkspacesOnUsersModelSchema.array(),
|
||||
|
@ -1,6 +1,6 @@
|
||||
import * as z from "zod"
|
||||
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
|
||||
type Literal = boolean | number | string
|
||||
@ -31,7 +31,6 @@ export interface CompleteWorkspace extends z.infer<typeof WorkspaceModelSchema>
|
||||
monitors: CompleteMonitor[]
|
||||
monitorStatusPages: CompleteMonitorStatusPage[]
|
||||
telemetryList: CompleteTelemetry[]
|
||||
selectedUsers: CompleteUser[]
|
||||
workspaceDailyUsage: CompleteWorkspaceDailyUsage[]
|
||||
workspaceAuditLog: CompleteWorkspaceAuditLog[]
|
||||
surveys: CompleteSurvey[]
|
||||
@ -50,7 +49,6 @@ export const RelatedWorkspaceModelSchema: z.ZodSchema<CompleteWorkspace> = z.laz
|
||||
monitors: RelatedMonitorModelSchema.array(),
|
||||
monitorStatusPages: RelatedMonitorStatusPageModelSchema.array(),
|
||||
telemetryList: RelatedTelemetryModelSchema.array(),
|
||||
selectedUsers: RelatedUserModelSchema.array(),
|
||||
workspaceDailyUsage: RelatedWorkspaceDailyUsageModelSchema.array(),
|
||||
workspaceAuditLog: RelatedWorkspaceAuditLogModelSchema.array(),
|
||||
surveys: RelatedSurveyModelSchema.array(),
|
||||
|
@ -138,7 +138,7 @@ export const userRouter = router({
|
||||
info: protectProedure
|
||||
.input(z.void())
|
||||
.output(userInfoSchema.nullable())
|
||||
.query(async ({ input, ctx }) => {
|
||||
.query(async ({ ctx }) => {
|
||||
return getUserInfo(ctx.user.id);
|
||||
}),
|
||||
});
|
||||
|
@ -23,6 +23,7 @@ import {
|
||||
leaveWorkspace,
|
||||
} from '../../model/user.js';
|
||||
import { WorkspacesOnUsersModelSchema } from '../../prisma/zod/workspacesonusers.js';
|
||||
import { monitorManager } from '../../model/monitor/index.js';
|
||||
|
||||
export const workspaceRouter = router({
|
||||
create: protectProedure
|
||||
@ -104,7 +105,7 @@ export const workspaceRouter = router({
|
||||
id: workspaceId,
|
||||
users: {
|
||||
some: {
|
||||
userId,
|
||||
userId, // make sure is member of this workspace
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -143,6 +144,19 @@ export const workspaceRouter = router({
|
||||
const { workspaceId } = input;
|
||||
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({
|
||||
where: {
|
||||
id: workspaceId,
|
||||
|
Loading…
Reference in New Issue
Block a user