♻️ (builder) Change to features-centric folder structure
This commit is contained in:
committed by
Baptiste Arnaud
parent
3686465a85
commit
643571fe7d
123
apps/builder/src/features/collaboration/collaboration.spec.ts
Normal file
123
apps/builder/src/features/collaboration/collaboration.spec.ts
Normal file
@ -0,0 +1,123 @@
|
||||
import test, { expect } from '@playwright/test'
|
||||
import cuid from 'cuid'
|
||||
import { CollaborationType, Plan, WorkspaceRole } from 'db'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { InputBlockType, defaultTextInputOptions } from 'models'
|
||||
import {
|
||||
createTypebots,
|
||||
injectFakeResults,
|
||||
} from 'utils/playwright/databaseActions'
|
||||
import { parseDefaultGroupWithBlock } from 'utils/playwright/databaseHelpers'
|
||||
import { userId } from 'utils/playwright/databaseSetup'
|
||||
import { createFolder } from '@/test/utils/databaseActions'
|
||||
|
||||
test.describe('Typebot owner', () => {
|
||||
test('Can invite collaborators', async ({ page }) => {
|
||||
const typebotId = cuid()
|
||||
const guestWorkspaceId = cuid()
|
||||
await prisma.workspace.create({
|
||||
data: {
|
||||
id: guestWorkspaceId,
|
||||
name: 'Guest Workspace',
|
||||
plan: Plan.FREE,
|
||||
members: {
|
||||
createMany: {
|
||||
data: [{ role: WorkspaceRole.ADMIN, userId }],
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
await createTypebots([
|
||||
{
|
||||
id: typebotId,
|
||||
name: 'Guest typebot',
|
||||
workspaceId: guestWorkspaceId,
|
||||
...parseDefaultGroupWithBlock({
|
||||
type: InputBlockType.TEXT,
|
||||
options: defaultTextInputOptions,
|
||||
}),
|
||||
},
|
||||
])
|
||||
await page.goto(`/typebots/${typebotId}/edit`)
|
||||
await page.click('button[aria-label="Show collaboration menu"]')
|
||||
await expect(page.locator('text=Free user')).toBeHidden()
|
||||
await page.fill(
|
||||
'input[placeholder="colleague@company.com"]',
|
||||
'guest@email.com'
|
||||
)
|
||||
await page.click('text=Can view')
|
||||
await page.click('text=Can edit')
|
||||
await page.click('text=Invite')
|
||||
await expect(page.locator('text=Pending')).toBeVisible()
|
||||
await expect(page.locator('text=Free user')).toBeHidden()
|
||||
await page.fill(
|
||||
'input[placeholder="colleague@company.com"]',
|
||||
'other-user@email.com'
|
||||
)
|
||||
await page.click('text=Can edit')
|
||||
await page.click('text=Can view')
|
||||
await page.click('text=Invite')
|
||||
await expect(page.locator('text=James Doe')).toBeVisible()
|
||||
await page.click('text="guest@email.com"')
|
||||
await page.click('text="Remove"')
|
||||
await expect(page.locator('text="guest@email.com"')).toBeHidden()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Guest', () => {
|
||||
test('should have shared typebots displayed', async ({ page }) => {
|
||||
const typebotId = cuid()
|
||||
const guestWorkspaceId = cuid()
|
||||
await prisma.workspace.create({
|
||||
data: {
|
||||
id: guestWorkspaceId,
|
||||
name: 'Guest Workspace #2',
|
||||
plan: Plan.FREE,
|
||||
members: {
|
||||
createMany: {
|
||||
data: [{ role: WorkspaceRole.GUEST, userId }],
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
await createTypebots([
|
||||
{
|
||||
id: typebotId,
|
||||
name: 'Guest typebot',
|
||||
workspaceId: guestWorkspaceId,
|
||||
...parseDefaultGroupWithBlock({
|
||||
type: InputBlockType.TEXT,
|
||||
options: defaultTextInputOptions,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'Another typebot',
|
||||
workspaceId: guestWorkspaceId,
|
||||
},
|
||||
])
|
||||
await prisma.collaboratorsOnTypebots.create({
|
||||
data: {
|
||||
typebotId,
|
||||
userId,
|
||||
type: CollaborationType.READ,
|
||||
},
|
||||
})
|
||||
await createFolder(guestWorkspaceId, 'Guest folder')
|
||||
await injectFakeResults({ typebotId, count: 10 })
|
||||
await page.goto(`/typebots`)
|
||||
await page.click('text=Pro workspace')
|
||||
await page.click('text=Guest workspace #2')
|
||||
await expect(page.locator('text=Guest typebot')).toBeVisible()
|
||||
await expect(page.locator('text=Another typebot')).toBeHidden()
|
||||
await expect(page.locator('text=Guest folder')).toBeHidden()
|
||||
await page.click('text=Guest typebot')
|
||||
await page.click('button[aria-label="Show collaboration menu"]')
|
||||
await page.click('text=Everyone at Guest workspace')
|
||||
await expect(page.locator('text="Remove"')).toBeHidden()
|
||||
await expect(page.locator('text=John Doe')).toBeVisible()
|
||||
await page.click('text=Group #1', { force: true })
|
||||
await expect(page.locator('input[value="Group #1"]')).toBeHidden()
|
||||
await page.goto(`/typebots/${typebotId}/results`)
|
||||
await expect(page.locator('text="See logs" >> nth=9')).toBeVisible()
|
||||
})
|
||||
})
|
@ -0,0 +1,259 @@
|
||||
import {
|
||||
Stack,
|
||||
HStack,
|
||||
Input,
|
||||
Button,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
SkeletonCircle,
|
||||
SkeletonText,
|
||||
Text,
|
||||
Tag,
|
||||
Flex,
|
||||
} from '@chakra-ui/react'
|
||||
import { ChevronLeftIcon } from '@/components/icons'
|
||||
import { useToast } from '@/hooks/useToast'
|
||||
import { useTypebot } from '@/features/editor'
|
||||
import { useWorkspace } from '@/features/workspace'
|
||||
import { CollaborationType, WorkspaceRole } from 'db'
|
||||
import React, { FormEvent, useState } from 'react'
|
||||
import { CollaboratorItem } from './CollaboratorButton'
|
||||
import { EmojiOrImageIcon } from '@/components/EmojiOrImageIcon'
|
||||
import { useCollaborators } from '../../hooks/useCollaborators'
|
||||
import { useInvitations } from '../../hooks/useInvitations'
|
||||
import { updateInvitationQuery } from '../../queries/updateInvitationQuery'
|
||||
import { deleteInvitationQuery } from '../../queries/deleteInvitationQuery'
|
||||
import { updateCollaboratorQuery } from '../../queries/updateCollaboratorQuery'
|
||||
import { deleteCollaboratorQuery } from '../../queries/deleteCollaboratorQuery'
|
||||
import { sendInvitationQuery } from '../../queries/sendInvitationQuery'
|
||||
|
||||
export const CollaborationList = () => {
|
||||
const { currentRole, workspace } = useWorkspace()
|
||||
const { typebot } = useTypebot()
|
||||
const [invitationType, setInvitationType] = useState<CollaborationType>(
|
||||
CollaborationType.READ
|
||||
)
|
||||
const [invitationEmail, setInvitationEmail] = useState('')
|
||||
const [isSendingInvitation, setIsSendingInvitation] = useState(false)
|
||||
|
||||
const hasFullAccess =
|
||||
(currentRole && currentRole !== WorkspaceRole.GUEST) || false
|
||||
|
||||
const { showToast } = useToast()
|
||||
const {
|
||||
collaborators,
|
||||
isLoading: isCollaboratorsLoading,
|
||||
mutate: mutateCollaborators,
|
||||
} = useCollaborators({
|
||||
typebotId: typebot?.id,
|
||||
onError: (e) =>
|
||||
showToast({
|
||||
title: "Couldn't fetch collaborators",
|
||||
description: e.message,
|
||||
}),
|
||||
})
|
||||
const {
|
||||
invitations,
|
||||
isLoading: isInvitationsLoading,
|
||||
mutate: mutateInvitations,
|
||||
} = useInvitations({
|
||||
typebotId: typebot?.id,
|
||||
onError: (e) =>
|
||||
showToast({
|
||||
title: "Couldn't fetch invitations",
|
||||
description: e.message,
|
||||
}),
|
||||
})
|
||||
|
||||
const handleChangeInvitationCollabType =
|
||||
(email: string) => async (type: CollaborationType) => {
|
||||
if (!typebot || !hasFullAccess) return
|
||||
const { error } = await updateInvitationQuery(typebot?.id, email, {
|
||||
email,
|
||||
typebotId: typebot.id,
|
||||
type,
|
||||
})
|
||||
if (error)
|
||||
return showToast({ title: error.name, description: error.message })
|
||||
mutateInvitations({
|
||||
invitations: (invitations ?? []).map((i) =>
|
||||
i.email === email ? { ...i, type } : i
|
||||
),
|
||||
})
|
||||
}
|
||||
const handleDeleteInvitation = (email: string) => async () => {
|
||||
if (!typebot || !hasFullAccess) return
|
||||
const { error } = await deleteInvitationQuery(typebot?.id, email)
|
||||
if (error)
|
||||
return showToast({ title: error.name, description: error.message })
|
||||
mutateInvitations({
|
||||
invitations: (invitations ?? []).filter((i) => i.email !== email),
|
||||
})
|
||||
}
|
||||
|
||||
const handleChangeCollaborationType =
|
||||
(userId: string) => async (type: CollaborationType) => {
|
||||
if (!typebot || !hasFullAccess) return
|
||||
const { error } = await updateCollaboratorQuery(typebot?.id, userId, {
|
||||
userId,
|
||||
type,
|
||||
typebotId: typebot.id,
|
||||
})
|
||||
if (error)
|
||||
return showToast({ title: error.name, description: error.message })
|
||||
mutateCollaborators({
|
||||
collaborators: (collaborators ?? []).map((c) =>
|
||||
c.userId === userId ? { ...c, type } : c
|
||||
),
|
||||
})
|
||||
}
|
||||
const handleDeleteCollaboration = (userId: string) => async () => {
|
||||
if (!typebot || !hasFullAccess) return
|
||||
const { error } = await deleteCollaboratorQuery(typebot?.id, userId)
|
||||
if (error)
|
||||
return showToast({ title: error.name, description: error.message })
|
||||
mutateCollaborators({
|
||||
collaborators: (collaborators ?? []).filter((c) => c.userId !== userId),
|
||||
})
|
||||
}
|
||||
|
||||
const handleInvitationSubmit = async (e: FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!typebot || !hasFullAccess) return
|
||||
setIsSendingInvitation(true)
|
||||
const { error } = await sendInvitationQuery(typebot.id, {
|
||||
email: invitationEmail,
|
||||
type: invitationType,
|
||||
})
|
||||
setIsSendingInvitation(false)
|
||||
mutateInvitations({ invitations: invitations ?? [] })
|
||||
mutateCollaborators({ collaborators: collaborators ?? [] })
|
||||
if (error)
|
||||
return showToast({ title: error.name, description: error.message })
|
||||
showToast({ status: 'success', title: 'Invitation sent! 📧' })
|
||||
setInvitationEmail('')
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack spacing={4} py="4">
|
||||
<HStack as="form" onSubmit={handleInvitationSubmit} px="4">
|
||||
<Input
|
||||
size="sm"
|
||||
placeholder="colleague@company.com"
|
||||
name="inviteEmail"
|
||||
value={invitationEmail}
|
||||
onChange={(e) => setInvitationEmail(e.target.value)}
|
||||
rounded="md"
|
||||
isDisabled={!hasFullAccess}
|
||||
/>
|
||||
|
||||
{hasFullAccess && (
|
||||
<CollaborationTypeMenuButton
|
||||
type={invitationType}
|
||||
onChange={setInvitationType}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
colorScheme="blue"
|
||||
isLoading={isSendingInvitation}
|
||||
flexShrink={0}
|
||||
type="submit"
|
||||
isDisabled={!hasFullAccess}
|
||||
>
|
||||
Invite
|
||||
</Button>
|
||||
</HStack>
|
||||
{workspace && (
|
||||
<Flex py="2" px="4" justifyContent="space-between">
|
||||
<HStack minW={0}>
|
||||
<EmojiOrImageIcon icon={workspace.icon} />
|
||||
<Text fontSize="15px" noOfLines={1}>
|
||||
Everyone at {workspace.name}
|
||||
</Text>
|
||||
</HStack>
|
||||
<Tag flexShrink={0}>
|
||||
{convertCollaborationTypeEnumToReadable(
|
||||
CollaborationType.FULL_ACCESS
|
||||
)}
|
||||
</Tag>
|
||||
</Flex>
|
||||
)}
|
||||
{invitations?.map(({ email, type }) => (
|
||||
<CollaboratorItem
|
||||
key={email}
|
||||
email={email}
|
||||
type={type}
|
||||
isOwner={hasFullAccess}
|
||||
onDeleteClick={handleDeleteInvitation(email)}
|
||||
onChangeCollaborationType={handleChangeInvitationCollabType(email)}
|
||||
isGuest
|
||||
/>
|
||||
))}
|
||||
{collaborators?.map(({ user, type, userId }) => (
|
||||
<CollaboratorItem
|
||||
key={userId}
|
||||
email={user.email ?? ''}
|
||||
image={user.image ?? undefined}
|
||||
name={user.name ?? undefined}
|
||||
type={type}
|
||||
isOwner={hasFullAccess}
|
||||
onDeleteClick={handleDeleteCollaboration(userId ?? '')}
|
||||
onChangeCollaborationType={handleChangeCollaborationType(userId)}
|
||||
/>
|
||||
))}
|
||||
{(isCollaboratorsLoading || isInvitationsLoading) && (
|
||||
<HStack p="4">
|
||||
<SkeletonCircle boxSize="32px" />
|
||||
<SkeletonText width="200px" noOfLines={2} />
|
||||
</HStack>
|
||||
)}
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
const CollaborationTypeMenuButton = ({
|
||||
type,
|
||||
onChange,
|
||||
}: {
|
||||
type: CollaborationType
|
||||
onChange: (type: CollaborationType) => void
|
||||
}) => {
|
||||
return (
|
||||
<Menu placement="bottom-end">
|
||||
<MenuButton
|
||||
flexShrink={0}
|
||||
size="sm"
|
||||
as={Button}
|
||||
rightIcon={<ChevronLeftIcon transform={'rotate(-90deg)'} />}
|
||||
>
|
||||
{convertCollaborationTypeEnumToReadable(type)}
|
||||
</MenuButton>
|
||||
<MenuList minW={0}>
|
||||
<Stack maxH={'35vh'} overflowY="scroll" spacing="0">
|
||||
<MenuItem onClick={() => onChange(CollaborationType.READ)}>
|
||||
{convertCollaborationTypeEnumToReadable(CollaborationType.READ)}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => onChange(CollaborationType.WRITE)}>
|
||||
{convertCollaborationTypeEnumToReadable(CollaborationType.WRITE)}
|
||||
</MenuItem>
|
||||
</Stack>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
|
||||
export const convertCollaborationTypeEnumToReadable = (
|
||||
type: CollaborationType
|
||||
) => {
|
||||
switch (type) {
|
||||
case CollaborationType.READ:
|
||||
return 'Can view'
|
||||
case CollaborationType.WRITE:
|
||||
return 'Can edit'
|
||||
case CollaborationType.FULL_ACCESS:
|
||||
return 'Full access'
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
import {
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
PopoverContent,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
} from '@chakra-ui/react'
|
||||
import { UsersIcon } from '@/components/icons'
|
||||
import React from 'react'
|
||||
import { CollaborationList } from './CollaborationList'
|
||||
|
||||
export const CollaborationMenuButton = ({
|
||||
isLoading,
|
||||
}: {
|
||||
isLoading: boolean
|
||||
}) => {
|
||||
return (
|
||||
<Popover isLazy placement="bottom-end">
|
||||
<PopoverTrigger>
|
||||
<span>
|
||||
<Tooltip label="Invite users to collaborate">
|
||||
<IconButton
|
||||
isLoading={isLoading}
|
||||
icon={<UsersIcon />}
|
||||
aria-label="Show collaboration menu"
|
||||
size="sm"
|
||||
/>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent shadow="lg" width="430px">
|
||||
<CollaborationList />
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
@ -0,0 +1,105 @@
|
||||
import {
|
||||
Avatar,
|
||||
HStack,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
Stack,
|
||||
Tag,
|
||||
Text,
|
||||
} from '@chakra-ui/react'
|
||||
import { CollaborationType } from 'db'
|
||||
import React from 'react'
|
||||
import { convertCollaborationTypeEnumToReadable } from './CollaborationList'
|
||||
|
||||
type Props = {
|
||||
image?: string
|
||||
name?: string
|
||||
email: string
|
||||
type: CollaborationType
|
||||
isGuest?: boolean
|
||||
isOwner: boolean
|
||||
onDeleteClick: () => void
|
||||
onChangeCollaborationType: (type: CollaborationType) => void
|
||||
}
|
||||
|
||||
export const CollaboratorItem = ({
|
||||
email,
|
||||
name,
|
||||
image,
|
||||
type,
|
||||
isGuest = false,
|
||||
isOwner,
|
||||
onDeleteClick,
|
||||
onChangeCollaborationType,
|
||||
}: Props) => {
|
||||
const handleEditClick = () =>
|
||||
onChangeCollaborationType(CollaborationType.WRITE)
|
||||
const handleViewClick = () =>
|
||||
onChangeCollaborationType(CollaborationType.READ)
|
||||
return (
|
||||
<Menu placement="bottom-end">
|
||||
<MenuButton _hover={{ backgroundColor: 'gray.100' }} borderRadius="md">
|
||||
<CollaboratorIdentityContent
|
||||
email={email}
|
||||
name={name}
|
||||
image={image}
|
||||
isGuest={isGuest}
|
||||
tag={convertCollaborationTypeEnumToReadable(type)}
|
||||
/>
|
||||
</MenuButton>
|
||||
{isOwner && (
|
||||
<MenuList shadow="lg">
|
||||
<MenuItem onClick={handleEditClick}>
|
||||
{convertCollaborationTypeEnumToReadable(CollaborationType.WRITE)}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleViewClick}>
|
||||
{convertCollaborationTypeEnumToReadable(CollaborationType.READ)}
|
||||
</MenuItem>
|
||||
<MenuItem color="red.500" onClick={onDeleteClick}>
|
||||
Remove
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
)}
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
|
||||
export const CollaboratorIdentityContent = ({
|
||||
name,
|
||||
tag,
|
||||
isGuest = false,
|
||||
image,
|
||||
email,
|
||||
}: {
|
||||
name?: string
|
||||
tag?: string
|
||||
image?: string
|
||||
isGuest?: boolean
|
||||
email: string
|
||||
}) => (
|
||||
<HStack justifyContent="space-between" maxW="full" py="2" px="4">
|
||||
<HStack minW={0}>
|
||||
<Avatar name={name} src={image} size="sm" />
|
||||
<Stack spacing={0} minW="0">
|
||||
{name && (
|
||||
<Text textAlign="left" fontSize="15px">
|
||||
{name}
|
||||
</Text>
|
||||
)}
|
||||
<Text
|
||||
color="gray.500"
|
||||
fontSize={name ? '14px' : 'inherit'}
|
||||
noOfLines={1}
|
||||
>
|
||||
{email}
|
||||
</Text>
|
||||
</Stack>
|
||||
</HStack>
|
||||
<HStack flexShrink={0}>
|
||||
{isGuest && <Tag color="gray.400">Pending</Tag>}
|
||||
<Tag>{tag}</Tag>
|
||||
</HStack>
|
||||
</HStack>
|
||||
)
|
@ -0,0 +1 @@
|
||||
export * from './CollaborationMenuButton'
|
@ -0,0 +1,22 @@
|
||||
import { fetcher } from '@/utils/helpers'
|
||||
import useSWR from 'swr'
|
||||
import { Collaborator } from '../types'
|
||||
|
||||
export const useCollaborators = ({
|
||||
typebotId,
|
||||
onError,
|
||||
}: {
|
||||
typebotId?: string
|
||||
onError: (error: Error) => void
|
||||
}) => {
|
||||
const { data, error, mutate } = useSWR<
|
||||
{ collaborators: Collaborator[] },
|
||||
Error
|
||||
>(typebotId ? `/api/typebots/${typebotId}/collaborators` : null, fetcher)
|
||||
if (error) onError(error)
|
||||
return {
|
||||
collaborators: data?.collaborators,
|
||||
isLoading: !error && !data,
|
||||
mutate,
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
import { fetcher } from '@/utils/helpers'
|
||||
import { Invitation } from 'db'
|
||||
import useSWR from 'swr'
|
||||
import { env } from 'utils'
|
||||
|
||||
export const useInvitations = ({
|
||||
typebotId,
|
||||
onError,
|
||||
}: {
|
||||
typebotId?: string
|
||||
onError: (error: Error) => void
|
||||
}) => {
|
||||
const { data, error, mutate } = useSWR<{ invitations: Invitation[] }, Error>(
|
||||
typebotId ? `/api/typebots/${typebotId}/invitations` : null,
|
||||
fetcher,
|
||||
{
|
||||
dedupingInterval: env('E2E_TEST') === 'true' ? 0 : undefined,
|
||||
}
|
||||
)
|
||||
if (error) onError(error)
|
||||
return {
|
||||
invitations: data?.invitations,
|
||||
isLoading: !error && !data,
|
||||
mutate,
|
||||
}
|
||||
}
|
1
apps/builder/src/features/collaboration/index.ts
Normal file
1
apps/builder/src/features/collaboration/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { CollaborationMenuButton } from './components/CollaborationMenuButton'
|
@ -0,0 +1,7 @@
|
||||
import { sendRequest } from 'utils'
|
||||
|
||||
export const deleteCollaboratorQuery = (typebotId: string, userId: string) =>
|
||||
sendRequest({
|
||||
method: 'DELETE',
|
||||
url: `/api/typebots/${typebotId}/collaborators/${userId}`,
|
||||
})
|
@ -0,0 +1,7 @@
|
||||
import { sendRequest } from 'utils'
|
||||
|
||||
export const deleteInvitationQuery = (typebotId: string, email: string) =>
|
||||
sendRequest({
|
||||
method: 'DELETE',
|
||||
url: `/api/typebots/${typebotId}/invitations/${email}`,
|
||||
})
|
@ -0,0 +1,12 @@
|
||||
import { CollaborationType } from 'db'
|
||||
import { sendRequest } from 'utils'
|
||||
|
||||
export const sendInvitationQuery = (
|
||||
typebotId: string,
|
||||
{ email, type }: { email: string; type: CollaborationType }
|
||||
) =>
|
||||
sendRequest({
|
||||
method: 'POST',
|
||||
url: `/api/typebots/${typebotId}/invitations`,
|
||||
body: { email, type },
|
||||
})
|
@ -0,0 +1,13 @@
|
||||
import { CollaboratorsOnTypebots } from 'db'
|
||||
import { sendRequest } from 'utils'
|
||||
|
||||
export const updateCollaboratorQuery = (
|
||||
typebotId: string,
|
||||
userId: string,
|
||||
collaborator: CollaboratorsOnTypebots
|
||||
) =>
|
||||
sendRequest({
|
||||
method: 'PATCH',
|
||||
url: `/api/typebots/${typebotId}/collaborators/${userId}`,
|
||||
body: collaborator,
|
||||
})
|
@ -0,0 +1,13 @@
|
||||
import { Invitation } from 'db'
|
||||
import { sendRequest } from 'utils'
|
||||
|
||||
export const updateInvitationQuery = (
|
||||
typebotId: string,
|
||||
email: string,
|
||||
invitation: Omit<Invitation, 'createdAt' | 'id'>
|
||||
) =>
|
||||
sendRequest({
|
||||
method: 'PATCH',
|
||||
url: `/api/typebots/${typebotId}/invitations/${email}`,
|
||||
body: invitation,
|
||||
})
|
9
apps/builder/src/features/collaboration/types.ts
Normal file
9
apps/builder/src/features/collaboration/types.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { CollaboratorsOnTypebots } from 'db'
|
||||
|
||||
export type Collaborator = CollaboratorsOnTypebots & {
|
||||
user: {
|
||||
name: string | null
|
||||
image: string | null
|
||||
email: string | null
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user