feat: add delete workspace feature #96

This commit is contained in:
moonrailgun 2024-09-14 22:09:45 +08:00
parent 943f7f594b
commit 2b9a14c969
17 changed files with 323 additions and 81 deletions

View File

@ -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}

View File

@ -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 (

View File

@ -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,
})); }));
} }

View File

@ -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>

View File

@ -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,

View File

@ -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>

View 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>
);
}

View File

@ -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) {

View File

@ -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,
},
});
}
}; };

View File

@ -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(),

View File

@ -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,

View File

@ -0,0 +1,5 @@
-- DropForeignKey
ALTER TABLE "User" DROP CONSTRAINT "User_currentWorkspaceId_fkey";
-- AlterTable
ALTER TABLE "User" ALTER COLUMN "currentWorkspaceId" DROP NOT NULL;

View File

@ -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[]

View File

@ -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(),

View File

@ -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(),

View File

@ -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);
}), }),
}); });

View File

@ -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,