✨ Add webhook blocks API public endpoints
This commit is contained in:
@ -1,186 +1,170 @@
|
||||
import {
|
||||
createContext,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { byId } from 'utils'
|
||||
import { Plan, Workspace, WorkspaceRole } from 'db'
|
||||
import { WorkspaceRole } from 'db'
|
||||
import { useUser } from '../account/UserProvider'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useTypebot } from '../editor/providers/TypebotProvider'
|
||||
import { useWorkspaces } from './hooks/useWorkspaces'
|
||||
import { createWorkspaceQuery } from './queries/createWorkspaceQuery'
|
||||
import { deleteWorkspaceQuery } from './queries/deleteWorkspaceQuery'
|
||||
import { updateWorkspaceQuery } from './queries/updateWorkspaceQuery'
|
||||
import { WorkspaceWithMembers } from './types'
|
||||
import { trpc } from '@/lib/trpc'
|
||||
import { Workspace } from 'models'
|
||||
import { useToast } from '@/hooks/useToast'
|
||||
import { parseNewName, setWorkspaceIdInLocalStorage } from './utils'
|
||||
import { useTypebot } from '../editor'
|
||||
|
||||
const workspaceContext = createContext<{
|
||||
workspaces?: WorkspaceWithMembers[]
|
||||
isLoading: boolean
|
||||
workspace?: WorkspaceWithMembers
|
||||
canEdit: boolean
|
||||
workspaces: Pick<Workspace, 'id' | 'name' | 'icon' | 'plan'>[]
|
||||
workspace?: Workspace
|
||||
currentRole?: WorkspaceRole
|
||||
switchWorkspace: (workspaceId: string) => void
|
||||
createWorkspace: (name?: string) => Promise<void>
|
||||
updateWorkspace: (
|
||||
workspaceId: string,
|
||||
updates: Partial<Workspace>
|
||||
) => Promise<void>
|
||||
updateWorkspace: (updates: { icon?: string; name?: string }) => void
|
||||
deleteCurrentWorkspace: () => Promise<void>
|
||||
refreshWorkspace: (expectedUpdates: Partial<Workspace>) => void
|
||||
refreshWorkspace: () => void
|
||||
//@ts-ignore
|
||||
}>({})
|
||||
|
||||
type WorkspaceContextProps = {
|
||||
typebotId?: string
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const getNewWorkspaceName = (
|
||||
userFullName: string | undefined,
|
||||
existingWorkspaces: Workspace[]
|
||||
) => {
|
||||
const workspaceName = userFullName
|
||||
? `${userFullName}'s workspace`
|
||||
: 'My workspace'
|
||||
let newName = workspaceName
|
||||
let i = 1
|
||||
while (existingWorkspaces.find((w) => w.name === newName)) {
|
||||
newName = `${workspaceName} (${i})`
|
||||
i++
|
||||
}
|
||||
return newName
|
||||
}
|
||||
|
||||
export const WorkspaceProvider = ({ children }: WorkspaceContextProps) => {
|
||||
export const WorkspaceProvider = ({
|
||||
typebotId,
|
||||
children,
|
||||
}: WorkspaceContextProps) => {
|
||||
const { query } = useRouter()
|
||||
const { user } = useUser()
|
||||
const userId = user?.id
|
||||
const [workspaceId, setWorkspaceId] = useState<string | undefined>()
|
||||
|
||||
const { typebot } = useTypebot()
|
||||
const { workspaces, isLoading, mutate } = useWorkspaces({ userId })
|
||||
const [currentWorkspace, setCurrentWorkspace] =
|
||||
useState<WorkspaceWithMembers>()
|
||||
const [pendingWorkspaceId, setPendingWorkspaceId] = useState<string>()
|
||||
|
||||
const canEdit =
|
||||
workspaces
|
||||
?.find(byId(currentWorkspace?.id))
|
||||
?.members.find((m) => m.userId === userId)?.role === WorkspaceRole.ADMIN
|
||||
const trpcContext = trpc.useContext()
|
||||
|
||||
const currentRole = currentWorkspace?.members.find(
|
||||
(m) => m.userId === userId
|
||||
const { data: workspacesData } = trpc.workspace.listWorkspaces.useQuery(
|
||||
undefined,
|
||||
{
|
||||
enabled: !!user,
|
||||
}
|
||||
)
|
||||
const workspaces = useMemo(
|
||||
() => workspacesData?.workspaces ?? [],
|
||||
[workspacesData?.workspaces]
|
||||
)
|
||||
|
||||
const { data: workspaceData } = trpc.workspace.getWorkspace.useQuery(
|
||||
{ workspaceId: workspaceId! },
|
||||
{ enabled: !!workspaceId }
|
||||
)
|
||||
|
||||
const { data: membersData } = trpc.workspace.listMembersInWorkspace.useQuery(
|
||||
{ workspaceId: workspaceId! },
|
||||
{ enabled: !!workspaceId }
|
||||
)
|
||||
|
||||
const workspace = workspaceData?.workspace
|
||||
const members = membersData?.members
|
||||
|
||||
const { showToast } = useToast()
|
||||
|
||||
const createWorkspaceMutation = trpc.workspace.createWorkspace.useMutation({
|
||||
onError: (error) => showToast({ description: error.message }),
|
||||
onSuccess: async () => {
|
||||
trpcContext.workspace.listWorkspaces.invalidate()
|
||||
},
|
||||
})
|
||||
|
||||
const updateWorkspaceMutation = trpc.workspace.updateWorkspace.useMutation({
|
||||
onError: (error) => showToast({ description: error.message }),
|
||||
onSuccess: async () => {
|
||||
trpcContext.workspace.getWorkspace.invalidate()
|
||||
},
|
||||
})
|
||||
|
||||
const deleteWorkspaceMutation = trpc.workspace.deleteWorkspace.useMutation({
|
||||
onError: (error) => showToast({ description: error.message }),
|
||||
onSuccess: async () => {
|
||||
trpcContext.workspace.listWorkspaces.invalidate()
|
||||
},
|
||||
})
|
||||
|
||||
const currentRole = members?.find(
|
||||
(member) =>
|
||||
member.user.email === user?.email && member.workspaceId === workspaceId
|
||||
)?.role
|
||||
|
||||
useEffect(() => {
|
||||
if (!workspaces || workspaces.length === 0 || currentWorkspace) return
|
||||
if (
|
||||
!workspaces ||
|
||||
workspaces.length === 0 ||
|
||||
workspaceId ||
|
||||
(typebotId && !typebot?.workspaceId)
|
||||
)
|
||||
return
|
||||
const lastWorspaceId =
|
||||
pendingWorkspaceId ??
|
||||
typebot?.workspaceId ??
|
||||
query.workspaceId?.toString() ??
|
||||
localStorage.getItem('workspaceId')
|
||||
const defaultWorkspace = lastWorspaceId
|
||||
? workspaces.find(byId(lastWorspaceId))
|
||||
: workspaces.find((w) =>
|
||||
w.members.some(
|
||||
(m) => m.userId === userId && m.role === WorkspaceRole.ADMIN
|
||||
)
|
||||
)
|
||||
setCurrentWorkspace(defaultWorkspace ?? workspaces[0])
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [workspaces?.length])
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentWorkspace?.id) return
|
||||
localStorage.setItem('workspaceId', currentWorkspace.id)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentWorkspace?.id])
|
||||
const defaultWorkspaceId = lastWorspaceId
|
||||
? workspaces.find(byId(lastWorspaceId))?.id
|
||||
: members?.find((member) => member.role === WorkspaceRole.ADMIN)
|
||||
?.workspaceId
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentWorkspace) return setPendingWorkspaceId(typebot?.workspaceId)
|
||||
if (!typebot?.workspaceId || typebot.workspaceId === currentWorkspace.id)
|
||||
return
|
||||
switchWorkspace(typebot.workspaceId)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [typebot?.workspaceId])
|
||||
const newWorkspaceId = defaultWorkspaceId ?? workspaces[0].id
|
||||
setWorkspaceIdInLocalStorage(newWorkspaceId)
|
||||
setWorkspaceId(newWorkspaceId)
|
||||
}, [
|
||||
members,
|
||||
query.workspaceId,
|
||||
typebot?.workspaceId,
|
||||
typebotId,
|
||||
userId,
|
||||
workspaceId,
|
||||
workspaces,
|
||||
])
|
||||
|
||||
const switchWorkspace = (workspaceId: string) => {
|
||||
const newWorkspace = workspaces?.find(byId(workspaceId))
|
||||
if (!newWorkspace) return
|
||||
setCurrentWorkspace(newWorkspace)
|
||||
setWorkspaceId(workspaceId)
|
||||
setWorkspaceIdInLocalStorage(workspaceId)
|
||||
}
|
||||
|
||||
const createWorkspace = async (userFullName?: string) => {
|
||||
if (!workspaces) return
|
||||
const newWorkspaceName = getNewWorkspaceName(userFullName, workspaces)
|
||||
const { data, error } = await createWorkspaceQuery({
|
||||
name: newWorkspaceName,
|
||||
plan: Plan.FREE,
|
||||
})
|
||||
if (error || !data) return
|
||||
const { workspace } = data
|
||||
const newWorkspace = {
|
||||
...workspace,
|
||||
members: [
|
||||
{
|
||||
role: WorkspaceRole.ADMIN,
|
||||
userId: userId as string,
|
||||
workspaceId: workspace.id as string,
|
||||
},
|
||||
],
|
||||
}
|
||||
mutate({
|
||||
workspaces: [...workspaces, newWorkspace],
|
||||
})
|
||||
setCurrentWorkspace(newWorkspace)
|
||||
const name = parseNewName(userFullName, workspaces)
|
||||
const { workspace } = await createWorkspaceMutation.mutateAsync({ name })
|
||||
setWorkspaceId(workspace.id)
|
||||
}
|
||||
|
||||
const updateWorkspace = async (
|
||||
workspaceId: string,
|
||||
updates: Partial<Workspace>
|
||||
) => {
|
||||
const { data } = await updateWorkspaceQuery({ id: workspaceId, ...updates })
|
||||
if (!data || !currentWorkspace) return
|
||||
setCurrentWorkspace({ ...currentWorkspace, ...updates })
|
||||
mutate({
|
||||
workspaces: (workspaces ?? []).map((w) =>
|
||||
w.id === workspaceId ? { ...data.workspace, members: w.members } : w
|
||||
),
|
||||
const updateWorkspace = (updates: { icon?: string; name?: string }) => {
|
||||
if (!workspaceId) return
|
||||
updateWorkspaceMutation.mutate({
|
||||
workspaceId,
|
||||
...updates,
|
||||
})
|
||||
}
|
||||
|
||||
const deleteCurrentWorkspace = async () => {
|
||||
if (!currentWorkspace || !workspaces || workspaces.length < 2) return
|
||||
const { data } = await deleteWorkspaceQuery(currentWorkspace.id)
|
||||
if (!data || !currentWorkspace) return
|
||||
const newWorkspaces = (workspaces ?? []).filter((w) =>
|
||||
w.id === currentWorkspace.id
|
||||
? { ...data.workspace, members: w.members }
|
||||
: w
|
||||
)
|
||||
setCurrentWorkspace(newWorkspaces[0])
|
||||
mutate({
|
||||
workspaces: newWorkspaces,
|
||||
})
|
||||
if (!workspaceId || !workspaces || workspaces.length < 2) return
|
||||
await deleteWorkspaceMutation.mutateAsync({ workspaceId })
|
||||
setWorkspaceId(workspaces[0].id)
|
||||
}
|
||||
|
||||
const refreshWorkspace = (expectedUpdates: Partial<Workspace>) => {
|
||||
if (!currentWorkspace) return
|
||||
const updatedWorkspace = { ...currentWorkspace, ...expectedUpdates }
|
||||
mutate({
|
||||
workspaces: (workspaces ?? []).map((w) =>
|
||||
w.id === currentWorkspace.id ? updatedWorkspace : w
|
||||
),
|
||||
})
|
||||
setCurrentWorkspace(updatedWorkspace)
|
||||
const refreshWorkspace = () => {
|
||||
trpcContext.workspace.getWorkspace.invalidate()
|
||||
}
|
||||
|
||||
return (
|
||||
<workspaceContext.Provider
|
||||
value={{
|
||||
workspaces,
|
||||
workspace: currentWorkspace,
|
||||
isLoading,
|
||||
canEdit,
|
||||
workspace,
|
||||
currentRole,
|
||||
switchWorkspace,
|
||||
createWorkspace,
|
||||
|
1
apps/builder/src/features/workspace/api/index.ts
Normal file
1
apps/builder/src/features/workspace/api/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './router'
|
@ -0,0 +1,56 @@
|
||||
import prisma from '@/lib/prisma'
|
||||
import { authenticatedProcedure } from '@/utils/server/trpc'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { Plan } from 'db'
|
||||
import { Workspace, workspaceSchema } from 'models'
|
||||
import { z } from 'zod'
|
||||
|
||||
export const createWorkspaceProcedure = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'POST',
|
||||
path: '/workspaces',
|
||||
protect: true,
|
||||
summary: 'Create workspace',
|
||||
tags: ['Workspace'],
|
||||
},
|
||||
})
|
||||
.input(workspaceSchema.pick({ name: true }))
|
||||
.output(
|
||||
z.object({
|
||||
workspace: workspaceSchema,
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input: { name }, ctx: { user } }) => {
|
||||
const existingWorkspaceNames = (await prisma.workspace.findMany({
|
||||
where: {
|
||||
members: {
|
||||
some: {
|
||||
userId: user.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
select: { name: true },
|
||||
})) satisfies Pick<Workspace, 'name'>[]
|
||||
|
||||
if (existingWorkspaceNames.some((workspace) => workspace.name === name))
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'Workspace with same name already exists',
|
||||
})
|
||||
|
||||
const plan =
|
||||
process.env.ADMIN_EMAIL === user.email ? Plan.LIFETIME : Plan.FREE
|
||||
|
||||
const newWorkspace = (await prisma.workspace.create({
|
||||
data: {
|
||||
name,
|
||||
members: { create: [{ role: 'ADMIN', userId: user.id }] },
|
||||
plan,
|
||||
},
|
||||
})) satisfies Workspace
|
||||
|
||||
return {
|
||||
workspace: newWorkspace,
|
||||
}
|
||||
})
|
@ -0,0 +1,33 @@
|
||||
import prisma from '@/lib/prisma'
|
||||
import { authenticatedProcedure } from '@/utils/server/trpc'
|
||||
import { z } from 'zod'
|
||||
|
||||
export const deleteWorkspaceProcedure = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'DELETE',
|
||||
path: '/workspaces/{workspaceId}',
|
||||
protect: true,
|
||||
summary: 'Delete workspace',
|
||||
tags: ['Workspace'],
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
workspaceId: z.string(),
|
||||
})
|
||||
)
|
||||
.output(
|
||||
z.object({
|
||||
message: z.string(),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input: { workspaceId }, ctx: { user } }) => {
|
||||
await prisma.workspace.deleteMany({
|
||||
where: { members: { some: { userId: user.id } }, id: workspaceId },
|
||||
})
|
||||
|
||||
return {
|
||||
message: 'Workspace deleted',
|
||||
}
|
||||
})
|
@ -0,0 +1,38 @@
|
||||
import prisma from '@/lib/prisma'
|
||||
import { authenticatedProcedure } from '@/utils/server/trpc'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { Workspace, workspaceSchema } from 'models'
|
||||
import { z } from 'zod'
|
||||
|
||||
export const getWorkspaceProcedure = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'GET',
|
||||
path: '/workspaces/{workspaceId}',
|
||||
protect: true,
|
||||
summary: 'Get workspace',
|
||||
tags: ['Workspace'],
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
workspaceId: z.string(),
|
||||
})
|
||||
)
|
||||
.output(
|
||||
z.object({
|
||||
workspace: workspaceSchema,
|
||||
})
|
||||
)
|
||||
.query(async ({ input: { workspaceId }, ctx: { user } }) => {
|
||||
const workspace = (await prisma.workspace.findFirst({
|
||||
where: { members: { some: { userId: user.id } }, id: workspaceId },
|
||||
})) satisfies Workspace | null
|
||||
|
||||
if (!workspace)
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: 'No workspaces found' })
|
||||
|
||||
return {
|
||||
workspace,
|
||||
}
|
||||
})
|
@ -0,0 +1,7 @@
|
||||
export * from './createWorkspaceProcedure'
|
||||
export * from './deleteWorkspaceProcedure'
|
||||
export * from './getWorkspaceProcedure'
|
||||
export * from './listInvitationsInWorkspaceProcedure'
|
||||
export * from './listMembersInWorkspaceProcedure'
|
||||
export * from './listWorkspacesProcedure'
|
||||
export * from './updateWorkspaceProcedure'
|
@ -0,0 +1,43 @@
|
||||
import prisma from '@/lib/prisma'
|
||||
import { authenticatedProcedure } from '@/utils/server/trpc'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { WorkspaceInvitation, workspaceInvitationSchema } from 'models'
|
||||
import { z } from 'zod'
|
||||
|
||||
export const listInvitationsInWorkspaceProcedure = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'GET',
|
||||
path: '/workspaces/{workspaceId}/invitations',
|
||||
protect: true,
|
||||
summary: 'List invitations in workspace',
|
||||
tags: ['Workspace'],
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
workspaceId: z.string(),
|
||||
})
|
||||
)
|
||||
.output(
|
||||
z.object({
|
||||
invitations: z.array(workspaceInvitationSchema),
|
||||
})
|
||||
)
|
||||
.query(async ({ input: { workspaceId }, ctx: { user } }) => {
|
||||
const invitations = (await prisma.workspaceInvitation.findMany({
|
||||
where: {
|
||||
workspaceId,
|
||||
workspace: { members: { some: { userId: user.id } } },
|
||||
},
|
||||
select: { createdAt: true, email: true, type: true },
|
||||
})) satisfies WorkspaceInvitation[]
|
||||
|
||||
if (!invitations)
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
message: 'No invitations found',
|
||||
})
|
||||
|
||||
return { invitations }
|
||||
})
|
@ -0,0 +1,37 @@
|
||||
import prisma from '@/lib/prisma'
|
||||
import { authenticatedProcedure } from '@/utils/server/trpc'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { WorkspaceMember, workspaceMemberSchema } from 'models'
|
||||
import { z } from 'zod'
|
||||
|
||||
export const listMembersInWorkspaceProcedure = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'GET',
|
||||
path: '/workspaces/{workspaceId}/members',
|
||||
protect: true,
|
||||
summary: 'List members in workspace',
|
||||
tags: ['Workspace'],
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
workspaceId: z.string(),
|
||||
})
|
||||
)
|
||||
.output(
|
||||
z.object({
|
||||
members: z.array(workspaceMemberSchema),
|
||||
})
|
||||
)
|
||||
.query(async ({ input: { workspaceId }, ctx: { user } }) => {
|
||||
const members = (await prisma.memberInWorkspace.findMany({
|
||||
where: { userId: user.id, workspaceId },
|
||||
include: { user: { select: { name: true, email: true, image: true } } },
|
||||
})) satisfies WorkspaceMember[]
|
||||
|
||||
if (!members)
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: 'No members found' })
|
||||
|
||||
return { members }
|
||||
})
|
@ -0,0 +1,35 @@
|
||||
import prisma from '@/lib/prisma'
|
||||
import { authenticatedProcedure } from '@/utils/server/trpc'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { Workspace, workspaceSchema } from 'models'
|
||||
import { z } from 'zod'
|
||||
|
||||
export const listWorkspacesProcedure = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'GET',
|
||||
path: '/workspaces',
|
||||
protect: true,
|
||||
summary: 'List workspaces',
|
||||
tags: ['Workspace'],
|
||||
},
|
||||
})
|
||||
.input(z.void())
|
||||
.output(
|
||||
z.object({
|
||||
workspaces: z.array(
|
||||
workspaceSchema.pick({ id: true, name: true, icon: true, plan: true })
|
||||
),
|
||||
})
|
||||
)
|
||||
.query(async ({ ctx: { user } }) => {
|
||||
const workspaces = (await prisma.workspace.findMany({
|
||||
where: { members: { some: { userId: user.id } } },
|
||||
select: { name: true, id: true, icon: true, plan: true },
|
||||
})) satisfies Pick<Workspace, 'id' | 'name' | 'icon' | 'plan'>[]
|
||||
|
||||
if (!workspaces)
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: 'No workspaces found' })
|
||||
|
||||
return { workspaces }
|
||||
})
|
@ -0,0 +1,45 @@
|
||||
import prisma from '@/lib/prisma'
|
||||
import { authenticatedProcedure } from '@/utils/server/trpc'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { Workspace, workspaceSchema } from 'models'
|
||||
import { z } from 'zod'
|
||||
|
||||
export const updateWorkspaceProcedure = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'PATCH',
|
||||
path: '/workspaces/{workspaceId}',
|
||||
protect: true,
|
||||
summary: 'Update workspace',
|
||||
tags: ['Workspace'],
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
name: z.string().optional(),
|
||||
icon: z.string().optional(),
|
||||
workspaceId: z.string(),
|
||||
})
|
||||
)
|
||||
.output(
|
||||
z.object({
|
||||
workspace: workspaceSchema,
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input: { workspaceId, ...updates }, ctx: { user } }) => {
|
||||
await prisma.workspace.updateMany({
|
||||
where: { members: { some: { userId: user.id } }, id: workspaceId },
|
||||
data: updates,
|
||||
})
|
||||
|
||||
const workspace = (await prisma.workspace.findFirst({
|
||||
where: { members: { some: { userId: user.id } }, id: workspaceId },
|
||||
})) satisfies Workspace | null
|
||||
|
||||
if (!workspace)
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: 'Workspace not found' })
|
||||
|
||||
return {
|
||||
workspace,
|
||||
}
|
||||
})
|
18
apps/builder/src/features/workspace/api/router.ts
Normal file
18
apps/builder/src/features/workspace/api/router.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { router } from '@/utils/server/trpc'
|
||||
import {
|
||||
createWorkspaceProcedure,
|
||||
deleteWorkspaceProcedure,
|
||||
getWorkspaceProcedure,
|
||||
listMembersInWorkspaceProcedure,
|
||||
listWorkspacesProcedure,
|
||||
updateWorkspaceProcedure,
|
||||
} from './procedures'
|
||||
|
||||
export const workspaceRouter = router({
|
||||
listWorkspaces: listWorkspacesProcedure,
|
||||
getWorkspace: getWorkspaceProcedure,
|
||||
listMembersInWorkspace: listMembersInWorkspaceProcedure,
|
||||
createWorkspace: createWorkspaceProcedure,
|
||||
updateWorkspace: updateWorkspaceProcedure,
|
||||
deleteWorkspace: deleteWorkspaceProcedure,
|
||||
})
|
@ -23,11 +23,13 @@ import { Member } from '../../types'
|
||||
|
||||
export const MembersList = () => {
|
||||
const { user } = useUser()
|
||||
const { workspace, canEdit } = useWorkspace()
|
||||
const { workspace, currentRole } = useWorkspace()
|
||||
const { members, invitations, isLoading, mutate } = useMembers({
|
||||
workspaceId: workspace?.id,
|
||||
})
|
||||
|
||||
const canEdit = currentRole === WorkspaceRole.ADMIN
|
||||
|
||||
const handleDeleteMemberClick = (memberId: string) => async () => {
|
||||
if (!workspace) return
|
||||
await deleteMemberQuery(workspace.id, memberId)
|
||||
|
@ -0,0 +1,98 @@
|
||||
import { EmojiOrImageIcon } from '@/components/EmojiOrImageIcon'
|
||||
import {
|
||||
HardDriveIcon,
|
||||
ChevronLeftIcon,
|
||||
PlusIcon,
|
||||
LogOutIcon,
|
||||
} from '@/components/icons'
|
||||
import { PlanTag } from '@/features/billing'
|
||||
import { trpc } from '@/lib/trpc'
|
||||
import {
|
||||
Menu,
|
||||
MenuButton,
|
||||
Button,
|
||||
HStack,
|
||||
SkeletonCircle,
|
||||
MenuList,
|
||||
MenuItem,
|
||||
Text,
|
||||
} from '@chakra-ui/react'
|
||||
import { Workspace } from 'models'
|
||||
|
||||
type Props = {
|
||||
currentWorkspace?: Workspace
|
||||
onWorkspaceSelected: (workspaceId: string) => void
|
||||
onCreateNewWorkspaceClick: () => void
|
||||
onLogoutClick: () => void
|
||||
}
|
||||
|
||||
export const WorkspaceDropdown = ({
|
||||
currentWorkspace,
|
||||
onWorkspaceSelected,
|
||||
onLogoutClick,
|
||||
onCreateNewWorkspaceClick,
|
||||
}: Props) => {
|
||||
const { data } = trpc.workspace.listWorkspaces.useQuery()
|
||||
|
||||
const workspaces = data?.workspaces ?? []
|
||||
|
||||
return (
|
||||
<Menu placement="bottom-end">
|
||||
<MenuButton as={Button} variant="outline" px="2">
|
||||
<HStack>
|
||||
<SkeletonCircle
|
||||
isLoaded={currentWorkspace !== undefined}
|
||||
alignItems="center"
|
||||
display="flex"
|
||||
boxSize="20px"
|
||||
>
|
||||
<EmojiOrImageIcon
|
||||
boxSize="20px"
|
||||
icon={currentWorkspace?.icon}
|
||||
defaultIcon={HardDriveIcon}
|
||||
/>
|
||||
</SkeletonCircle>
|
||||
{currentWorkspace && (
|
||||
<>
|
||||
<Text noOfLines={1} maxW="200px">
|
||||
{currentWorkspace.name}
|
||||
</Text>
|
||||
<PlanTag plan={currentWorkspace.plan} />
|
||||
</>
|
||||
)}
|
||||
<ChevronLeftIcon transform="rotate(-90deg)" />
|
||||
</HStack>
|
||||
</MenuButton>
|
||||
<MenuList>
|
||||
{workspaces
|
||||
?.filter((workspace) => workspace.id !== currentWorkspace?.id)
|
||||
.map((workspace) => (
|
||||
<MenuItem
|
||||
key={workspace.id}
|
||||
onClick={() => onWorkspaceSelected(workspace.id)}
|
||||
>
|
||||
<HStack>
|
||||
<EmojiOrImageIcon
|
||||
icon={workspace.icon}
|
||||
boxSize="16px"
|
||||
defaultIcon={HardDriveIcon}
|
||||
/>
|
||||
<Text>{workspace.name}</Text>
|
||||
<PlanTag plan={workspace.plan} />
|
||||
</HStack>
|
||||
</MenuItem>
|
||||
))}
|
||||
<MenuItem onClick={onCreateNewWorkspaceClick} icon={<PlusIcon />}>
|
||||
New workspace
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={onLogoutClick}
|
||||
icon={<LogOutIcon />}
|
||||
color="orange.500"
|
||||
>
|
||||
Log out
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
)
|
||||
}
|
@ -19,12 +19,11 @@ export const WorkspaceSettingsForm = ({ onClose }: { onClose: () => void }) => {
|
||||
|
||||
const handleNameChange = (name: string) => {
|
||||
if (!workspace?.id) return
|
||||
updateWorkspace(workspace?.id, { name })
|
||||
updateWorkspace({ name })
|
||||
}
|
||||
|
||||
const handleChangeIcon = (icon: string) => {
|
||||
if (!workspace?.id) return
|
||||
updateWorkspace(workspace?.id, { icon })
|
||||
updateWorkspace({ icon })
|
||||
}
|
||||
|
||||
const handleDeleteClick = async () => {
|
||||
|
@ -15,7 +15,7 @@ import {
|
||||
UsersIcon,
|
||||
} from '@/components/icons'
|
||||
import { EmojiOrImageIcon } from '@/components/EmojiOrImageIcon'
|
||||
import { GraphNavigation, User, Workspace } from 'db'
|
||||
import { GraphNavigation, User, Workspace, WorkspaceRole } from 'db'
|
||||
import { useState } from 'react'
|
||||
import { MembersList } from './MembersList'
|
||||
import { WorkspaceSettingsForm } from './WorkspaceSettingsForm'
|
||||
@ -44,9 +44,13 @@ export const WorkspaceSettingsModal = ({
|
||||
workspace,
|
||||
onClose,
|
||||
}: Props) => {
|
||||
const { canEdit } = useWorkspace()
|
||||
const { currentRole } = useWorkspace()
|
||||
const [selectedTab, setSelectedTab] = useState<SettingsTab>('my-account')
|
||||
|
||||
console.log(currentRole)
|
||||
|
||||
const canEditWorkspace = currentRole === WorkspaceRole.ADMIN
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} size="4xl">
|
||||
<ModalOverlay />
|
||||
@ -94,7 +98,7 @@ export const WorkspaceSettingsModal = ({
|
||||
<Text pl="4" color="gray.500">
|
||||
Workspace
|
||||
</Text>
|
||||
{canEdit && (
|
||||
{canEditWorkspace && (
|
||||
<Button
|
||||
variant={
|
||||
selectedTab === 'workspace-settings' ? 'solid' : 'ghost'
|
||||
@ -124,7 +128,7 @@ export const WorkspaceSettingsModal = ({
|
||||
>
|
||||
Members
|
||||
</Button>
|
||||
{canEdit && (
|
||||
{canEditWorkspace && (
|
||||
<Button
|
||||
variant={selectedTab === 'billing' ? 'solid' : 'ghost'}
|
||||
onClick={() => setSelectedTab('billing')}
|
||||
|
@ -1 +1,2 @@
|
||||
export { WorkspaceSettingsModal } from './WorkspaceSettingsModal'
|
||||
export * from './WorkspaceSettingsModal'
|
||||
export * from './WorkspaceDropdown'
|
||||
|
@ -1,17 +0,0 @@
|
||||
import { fetcher } from '@/utils/helpers'
|
||||
import useSWR from 'swr'
|
||||
import { WorkspaceWithMembers } from '../types'
|
||||
|
||||
export const useWorkspaces = ({ userId }: { userId?: string }) => {
|
||||
const { data, error, mutate } = useSWR<
|
||||
{
|
||||
workspaces: WorkspaceWithMembers[]
|
||||
},
|
||||
Error
|
||||
>(userId ? `/api/workspaces` : null, fetcher)
|
||||
return {
|
||||
workspaces: data?.workspaces,
|
||||
isLoading: !error && !data,
|
||||
mutate,
|
||||
}
|
||||
}
|
@ -1,2 +1,2 @@
|
||||
export { WorkspaceProvider, useWorkspace } from './WorkspaceProvider'
|
||||
export { WorkspaceSettingsModal } from './components/WorkspaceSettingsModal'
|
||||
export * from './components'
|
||||
|
@ -1,28 +0,0 @@
|
||||
import { Workspace } from 'db'
|
||||
import { sendRequest } from 'utils'
|
||||
|
||||
export const createWorkspaceQuery = async (
|
||||
body: Omit<
|
||||
Workspace,
|
||||
| 'id'
|
||||
| 'icon'
|
||||
| 'createdAt'
|
||||
| 'stripeId'
|
||||
| 'additionalChatsIndex'
|
||||
| 'additionalStorageIndex'
|
||||
| 'chatsLimitFirstEmailSentAt'
|
||||
| 'chatsLimitSecondEmailSentAt'
|
||||
| 'storageLimitFirstEmailSentAt'
|
||||
| 'storageLimitSecondEmailSentAt'
|
||||
| 'customChatsLimit'
|
||||
| 'customStorageLimit'
|
||||
| 'customSeatsLimit'
|
||||
>
|
||||
) =>
|
||||
sendRequest<{
|
||||
workspace: Workspace
|
||||
}>({
|
||||
url: `/api/workspaces`,
|
||||
method: 'POST',
|
||||
body,
|
||||
})
|
@ -1,10 +0,0 @@
|
||||
import { Workspace } from 'db'
|
||||
import { sendRequest } from 'utils'
|
||||
|
||||
export const deleteWorkspaceQuery = (workspaceId: string) =>
|
||||
sendRequest<{
|
||||
workspace: Workspace
|
||||
}>({
|
||||
url: `/api/workspaces/${workspaceId}`,
|
||||
method: 'DELETE',
|
||||
})
|
@ -1,11 +0,0 @@
|
||||
import { Workspace } from 'db'
|
||||
import { sendRequest } from 'utils'
|
||||
|
||||
export const updateWorkspaceQuery = async (updates: Partial<Workspace>) =>
|
||||
sendRequest<{
|
||||
workspace: Workspace
|
||||
}>({
|
||||
url: `/api/workspaces/${updates.id}`,
|
||||
method: 'PATCH',
|
||||
body: updates,
|
||||
})
|
@ -1,9 +1,7 @@
|
||||
import { MemberInWorkspace, Workspace } from 'db'
|
||||
import { MemberInWorkspace } from 'db'
|
||||
|
||||
export type Member = MemberInWorkspace & {
|
||||
name: string | null
|
||||
image: string | null
|
||||
email: string | null
|
||||
}
|
||||
|
||||
export type WorkspaceWithMembers = Workspace & { members: MemberInWorkspace[] }
|
||||
|
2
apps/builder/src/features/workspace/utils/index.ts
Normal file
2
apps/builder/src/features/workspace/utils/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './parseNewName'
|
||||
export * from './setWorkspaceIdInLocalStorage'
|
17
apps/builder/src/features/workspace/utils/parseNewName.ts
Normal file
17
apps/builder/src/features/workspace/utils/parseNewName.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { Workspace } from 'models'
|
||||
|
||||
export const parseNewName = (
|
||||
userFullName: string | undefined,
|
||||
existingWorkspaces: Pick<Workspace, 'name'>[]
|
||||
) => {
|
||||
const workspaceName = userFullName
|
||||
? `${userFullName}'s workspace`
|
||||
: 'My workspace'
|
||||
let newName = workspaceName
|
||||
let i = 1
|
||||
while (existingWorkspaces.find((w) => w.name === newName)) {
|
||||
newName = `${workspaceName} (${i})`
|
||||
i++
|
||||
}
|
||||
return newName
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
export const setWorkspaceIdInLocalStorage = (workspaceId: string) => {
|
||||
localStorage.setItem('workspaceId', workspaceId)
|
||||
}
|
Reference in New Issue
Block a user