feat: ✨ Add collaboration
This commit is contained in:
@ -10,7 +10,7 @@ export const SubscriptionTag = ({ plan }: { plan?: Plan }) => {
|
||||
return <Tag colorScheme="yellow">Lifetime plan</Tag>
|
||||
}
|
||||
case Plan.OFFERED: {
|
||||
return <Tag>Offered</Tag>
|
||||
return <Tag colorScheme="yellow">Offered</Tag>
|
||||
}
|
||||
case Plan.PRO: {
|
||||
return <Tag colorScheme="orange">Pro plan</Tag>
|
||||
|
@ -12,6 +12,7 @@ import {
|
||||
Wrap,
|
||||
} from '@chakra-ui/react'
|
||||
import { useTypebotDnd } from 'contexts/TypebotDndContext'
|
||||
import { useUser } from 'contexts/UserContext'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { createFolder, useFolders } from 'services/folders'
|
||||
import {
|
||||
@ -19,11 +20,13 @@ import {
|
||||
TypebotInDashboard,
|
||||
useTypebots,
|
||||
} from 'services/typebots'
|
||||
import { useSharedTypebotsCount } from 'services/user/sharedTypebots'
|
||||
import { AnnoucementModal } from './annoucements/AnnoucementModal'
|
||||
import { BackButton } from './FolderContent/BackButton'
|
||||
import { CreateBotButton } from './FolderContent/CreateBotButton'
|
||||
import { CreateFolderButton } from './FolderContent/CreateFolderButton'
|
||||
import { ButtonSkeleton, FolderButton } from './FolderContent/FolderButton'
|
||||
import { SharedTypebotsButton } from './FolderContent/SharedTypebotsButton'
|
||||
import { TypebotButton } from './FolderContent/TypebotButton'
|
||||
import { TypebotCardOverlay } from './FolderContent/TypebotButtonOverlay'
|
||||
|
||||
@ -32,6 +35,7 @@ type Props = { folder: DashboardFolder | null }
|
||||
const dragDistanceTolerance = 20
|
||||
|
||||
export const FolderContent = ({ folder }: Props) => {
|
||||
const { user } = useUser()
|
||||
const [isCreatingFolder, setIsCreatingFolder] = useState(false)
|
||||
const {
|
||||
setDraggedTypebot,
|
||||
@ -75,6 +79,17 @@ export const FolderContent = ({ folder }: Props) => {
|
||||
},
|
||||
})
|
||||
|
||||
const { totalSharedTypebots, isLoading: isSharedTypebotsCountLoading } =
|
||||
useSharedTypebotsCount({
|
||||
userId: folder === null ? user?.id : undefined,
|
||||
onError: (error) => {
|
||||
toast({
|
||||
title: "Couldn't fetch shared typebots",
|
||||
description: error.message,
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
typebots &&
|
||||
@ -182,6 +197,8 @@ export const FolderContent = ({ folder }: Props) => {
|
||||
folderId={folder?.id}
|
||||
isLoading={isTypebotLoading}
|
||||
/>
|
||||
{isSharedTypebotsCountLoading && <ButtonSkeleton />}
|
||||
{totalSharedTypebots > 0 && <SharedTypebotsButton />}
|
||||
{isFolderLoading && <ButtonSkeleton />}
|
||||
{folders &&
|
||||
folders.map((folder) => (
|
||||
|
@ -0,0 +1,40 @@
|
||||
import React from 'react'
|
||||
import { Button, Flex, Text, VStack, WrapItem } from '@chakra-ui/react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { UsersIcon } from 'assets/icons'
|
||||
|
||||
export const SharedTypebotsButton = () => {
|
||||
const router = useRouter()
|
||||
|
||||
const handleTypebotClick = () => router.push(`/typebots/shared`)
|
||||
|
||||
return (
|
||||
<Button
|
||||
as={WrapItem}
|
||||
onClick={handleTypebotClick}
|
||||
display="flex"
|
||||
flexDir="column"
|
||||
variant="outline"
|
||||
color="gray.800"
|
||||
w="225px"
|
||||
h="270px"
|
||||
mr={{ sm: 6 }}
|
||||
mb={6}
|
||||
rounded="lg"
|
||||
whiteSpace="normal"
|
||||
cursor="pointer"
|
||||
>
|
||||
<VStack spacing="4">
|
||||
<Flex
|
||||
boxSize="45px"
|
||||
rounded="full"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
>
|
||||
<UsersIcon fontSize="50" color="orange.300" />
|
||||
</Flex>
|
||||
<Text>Shared with me</Text>
|
||||
</VStack>
|
||||
</Button>
|
||||
)
|
||||
}
|
@ -22,13 +22,15 @@ import { useDebounce } from 'use-debounce'
|
||||
|
||||
type ChatbotCardProps = {
|
||||
typebot: Pick<Typebot, 'id' | 'publishedTypebotId' | 'name'>
|
||||
onTypebotDeleted: () => void
|
||||
onMouseDown: (e: React.MouseEvent<HTMLButtonElement>) => void
|
||||
isReadOnly?: boolean
|
||||
onTypebotDeleted?: () => void
|
||||
onMouseDown?: (e: React.MouseEvent<HTMLButtonElement>) => void
|
||||
}
|
||||
|
||||
export const TypebotButton = ({
|
||||
typebot,
|
||||
onTypebotDeleted,
|
||||
isReadOnly = false,
|
||||
onMouseDown,
|
||||
}: ChatbotCardProps) => {
|
||||
const router = useRouter()
|
||||
@ -55,13 +57,14 @@ export const TypebotButton = ({
|
||||
}
|
||||
|
||||
const handleDeleteTypebotClick = async () => {
|
||||
if (isReadOnly) return
|
||||
const { error } = await deleteTypebot(typebot.id)
|
||||
if (error)
|
||||
return toast({
|
||||
title: "Couldn't delete typebot",
|
||||
description: error.message,
|
||||
})
|
||||
onTypebotDeleted()
|
||||
if (onTypebotDeleted) onTypebotDeleted()
|
||||
}
|
||||
|
||||
const handleDuplicateClick = async (e: React.MouseEvent) => {
|
||||
@ -98,28 +101,32 @@ export const TypebotButton = ({
|
||||
onMouseDown={onMouseDown}
|
||||
cursor="pointer"
|
||||
>
|
||||
<IconButton
|
||||
icon={<GripIcon />}
|
||||
pos="absolute"
|
||||
top="20px"
|
||||
left="20px"
|
||||
aria-label="Drag"
|
||||
cursor="grab"
|
||||
variant="ghost"
|
||||
colorScheme="blue"
|
||||
size="sm"
|
||||
/>
|
||||
<MoreButton
|
||||
pos="absolute"
|
||||
top="20px"
|
||||
right="20px"
|
||||
aria-label={`Show ${typebot.name} menu`}
|
||||
>
|
||||
<MenuItem onClick={handleDuplicateClick}>Duplicate</MenuItem>
|
||||
<MenuItem color="red" onClick={handleDeleteClick}>
|
||||
Delete
|
||||
</MenuItem>
|
||||
</MoreButton>
|
||||
{!isReadOnly && (
|
||||
<>
|
||||
<IconButton
|
||||
icon={<GripIcon />}
|
||||
pos="absolute"
|
||||
top="20px"
|
||||
left="20px"
|
||||
aria-label="Drag"
|
||||
cursor="grab"
|
||||
variant="ghost"
|
||||
colorScheme="blue"
|
||||
size="sm"
|
||||
/>
|
||||
<MoreButton
|
||||
pos="absolute"
|
||||
top="20px"
|
||||
right="20px"
|
||||
aria-label={`Show ${typebot.name} menu`}
|
||||
>
|
||||
<MenuItem onClick={handleDuplicateClick}>Duplicate</MenuItem>
|
||||
<MenuItem color="red" onClick={handleDeleteClick}>
|
||||
Delete
|
||||
</MenuItem>
|
||||
</MoreButton>
|
||||
</>
|
||||
)}
|
||||
<VStack spacing="4">
|
||||
<Flex
|
||||
boxSize="45px"
|
||||
@ -137,20 +144,22 @@ export const TypebotButton = ({
|
||||
</Flex>
|
||||
<Text>{typebot.name}</Text>
|
||||
</VStack>
|
||||
<ConfirmModal
|
||||
message={
|
||||
<Text>
|
||||
Are you sure you want to delete your Typebot "{typebot.name}
|
||||
".
|
||||
<br />
|
||||
All associated data will be lost.
|
||||
</Text>
|
||||
}
|
||||
confirmButtonLabel="Delete"
|
||||
onConfirm={handleDeleteTypebotClick}
|
||||
isOpen={isDeleteOpen}
|
||||
onClose={onDeleteClose}
|
||||
/>
|
||||
{!isReadOnly && (
|
||||
<ConfirmModal
|
||||
message={
|
||||
<Text>
|
||||
Are you sure you want to delete your Typebot "{typebot.name}
|
||||
".
|
||||
<br />
|
||||
All associated data will be lost.
|
||||
</Text>
|
||||
}
|
||||
confirmButtonLabel="Delete"
|
||||
onConfirm={handleDeleteTypebotClick}
|
||||
isOpen={isDeleteOpen}
|
||||
onClose={onDeleteClose}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ import {
|
||||
Tooltip,
|
||||
} from '@chakra-ui/react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { createCustomDomain } from 'services/customDomains'
|
||||
import { createCustomDomain } from 'services/user'
|
||||
|
||||
const hostnameRegex =
|
||||
/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/
|
||||
|
@ -15,7 +15,7 @@ import { ChevronLeftIcon, PlusIcon, TrashIcon } from 'assets/icons'
|
||||
import React, { useState } from 'react'
|
||||
import { useUser } from 'contexts/UserContext'
|
||||
import { CustomDomainModal } from './CustomDomainModal'
|
||||
import { deleteCustomDomain, useCustomDomains } from 'services/customDomains'
|
||||
import { deleteCustomDomain, useCustomDomains } from 'services/user'
|
||||
|
||||
type Props = Omit<MenuButtonProps, 'type'> & {
|
||||
currentCustomDomain?: string
|
||||
|
@ -15,7 +15,7 @@ import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { useUser } from 'contexts/UserContext'
|
||||
import { useRouter } from 'next/router'
|
||||
import { CredentialsType } from 'models'
|
||||
import { deleteCredentials, useCredentials } from 'services/credentials'
|
||||
import { deleteCredentials, useCredentials } from 'services/user'
|
||||
|
||||
type Props = Omit<MenuButtonProps, 'type'> & {
|
||||
type: CredentialsType
|
||||
|
@ -12,7 +12,7 @@ import {
|
||||
import { useUser } from 'contexts/UserContext'
|
||||
import { CredentialsType, SmtpCredentialsData } from 'models'
|
||||
import React, { useState } from 'react'
|
||||
import { createCredentials } from 'services/credentials'
|
||||
import { createCredentials } from 'services/user'
|
||||
import { testSmtpConfig } from 'services/integrations'
|
||||
import { isNotDefined } from 'utils'
|
||||
import { SmtpConfigForm } from './SmtpConfigForm'
|
||||
|
@ -0,0 +1,248 @@
|
||||
import {
|
||||
Stack,
|
||||
HStack,
|
||||
Input,
|
||||
Button,
|
||||
useToast,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
SkeletonCircle,
|
||||
SkeletonText,
|
||||
} from '@chakra-ui/react'
|
||||
import { ChevronLeftIcon } from 'assets/icons'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { useUser } from 'contexts/UserContext'
|
||||
import { CollaborationType } from 'db'
|
||||
import React, { FormEvent, useState } from 'react'
|
||||
import {
|
||||
deleteCollaborator,
|
||||
updateCollaborator,
|
||||
useCollaborators,
|
||||
} from 'services/typebots/collaborators'
|
||||
import {
|
||||
useInvitations,
|
||||
updateInvitation,
|
||||
deleteInvitation,
|
||||
sendInvitation,
|
||||
} from 'services/typebots/invitations'
|
||||
import {
|
||||
CollaboratorIdentityContent,
|
||||
CollaboratorItem,
|
||||
} from './CollaboratorButton'
|
||||
|
||||
export const CollaborationList = () => {
|
||||
const { user } = useUser()
|
||||
const { typebot, owner } = useTypebot()
|
||||
const [invitationType, setInvitationType] = useState<CollaborationType>(
|
||||
CollaborationType.READ
|
||||
)
|
||||
const [invitationEmail, setInvitationEmail] = useState('')
|
||||
const [isSendingInvitation, setIsSendingInvitation] = useState(false)
|
||||
|
||||
console.log(user, owner)
|
||||
const isOwner = user?.email === owner?.email
|
||||
|
||||
const toast = useToast({
|
||||
position: 'top-right',
|
||||
status: 'error',
|
||||
})
|
||||
const {
|
||||
collaborators,
|
||||
isLoading: isCollaboratorsLoading,
|
||||
mutate: mutateCollaborators,
|
||||
} = useCollaborators({
|
||||
typebotId: typebot?.id,
|
||||
onError: (e) =>
|
||||
toast({
|
||||
title: "Couldn't fetch collaborators",
|
||||
description: e.message,
|
||||
}),
|
||||
})
|
||||
const {
|
||||
invitations,
|
||||
isLoading: isInvitationsLoading,
|
||||
mutate: mutateInvitations,
|
||||
} = useInvitations({
|
||||
typebotId: typebot?.id,
|
||||
onError: (e) =>
|
||||
toast({ title: "Couldn't fetch collaborators", description: e.message }),
|
||||
})
|
||||
|
||||
const handleChangeInvitationCollabType =
|
||||
(email: string) => async (type: CollaborationType) => {
|
||||
if (!typebot || !isOwner) return
|
||||
const { error } = await updateInvitation(typebot?.id, email, { type })
|
||||
if (error) return toast({ title: error.name, description: error.message })
|
||||
mutateInvitations({
|
||||
invitations: (invitations ?? []).map((i) =>
|
||||
i.email === email ? { ...i, type } : i
|
||||
),
|
||||
})
|
||||
}
|
||||
const handleDeleteInvitation = (email: string) => async () => {
|
||||
if (!typebot || !isOwner) return
|
||||
const { error } = await deleteInvitation(typebot?.id, email)
|
||||
if (error) return toast({ title: error.name, description: error.message })
|
||||
mutateInvitations({
|
||||
invitations: (invitations ?? []).filter((i) => i.email !== email),
|
||||
})
|
||||
}
|
||||
|
||||
const handleChangeCollaborationType =
|
||||
(userId: string) => async (type: CollaborationType) => {
|
||||
if (!typebot || !isOwner) return
|
||||
const { error } = await updateCollaborator(typebot?.id, userId, { type })
|
||||
if (error) return toast({ title: error.name, description: error.message })
|
||||
mutateCollaborators({
|
||||
collaborators: (collaborators ?? []).map((c) =>
|
||||
c.userId === userId ? { ...c, type } : c
|
||||
),
|
||||
})
|
||||
}
|
||||
const handleDeleteCollaboration = (userId: string) => async () => {
|
||||
if (!typebot || !isOwner) return
|
||||
const { error } = await deleteCollaborator(typebot?.id, userId)
|
||||
if (error) return toast({ title: error.name, description: error.message })
|
||||
mutateCollaborators({
|
||||
collaborators: (collaborators ?? []).filter((c) => c.userId !== userId),
|
||||
})
|
||||
}
|
||||
|
||||
const handleInvitationSubmit = async (e: FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!typebot || !isOwner) return
|
||||
setIsSendingInvitation(true)
|
||||
const { error } = await sendInvitation(typebot.id, {
|
||||
email: invitationEmail,
|
||||
type: invitationType,
|
||||
})
|
||||
setIsSendingInvitation(false)
|
||||
mutateInvitations({ invitations: invitations ?? [] })
|
||||
mutateCollaborators({ collaborators: collaborators ?? [] })
|
||||
if (error) return toast({ title: error.name, description: error.message })
|
||||
toast({ status: 'success', title: 'Invitation sent! 📧' })
|
||||
setInvitationEmail('')
|
||||
}
|
||||
|
||||
const hasNobody =
|
||||
(collaborators ?? []).length > 0 ||
|
||||
((invitations ?? []).length > 0 &&
|
||||
!isInvitationsLoading &&
|
||||
!isCollaboratorsLoading)
|
||||
|
||||
return (
|
||||
<Stack spacing={2}>
|
||||
{isOwner && (
|
||||
<HStack
|
||||
as="form"
|
||||
onSubmit={handleInvitationSubmit}
|
||||
pt="4"
|
||||
px="4"
|
||||
pb={hasNobody ? '0' : '4'}
|
||||
>
|
||||
<Input
|
||||
size="sm"
|
||||
placeholder="colleague@company.com"
|
||||
name="inviteEmail"
|
||||
value={invitationEmail}
|
||||
onChange={(e) => setInvitationEmail(e.target.value)}
|
||||
rounded="md"
|
||||
/>
|
||||
|
||||
<CollaborationTypeMenuButton
|
||||
type={invitationType}
|
||||
onChange={setInvitationType}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
colorScheme="blue"
|
||||
isLoading={isSendingInvitation}
|
||||
flexShrink="0"
|
||||
type="submit"
|
||||
>
|
||||
Invite
|
||||
</Button>
|
||||
</HStack>
|
||||
)}
|
||||
{owner && (collaborators ?? []).length > 0 && (
|
||||
<CollaboratorIdentityContent
|
||||
email={owner.email ?? ''}
|
||||
name={owner.name ?? undefined}
|
||||
image={owner.image ?? undefined}
|
||||
tag="Owner"
|
||||
/>
|
||||
)}
|
||||
{invitations?.map(({ email, type }) => (
|
||||
<CollaboratorItem
|
||||
key={email}
|
||||
email={email}
|
||||
type={type}
|
||||
isOwner={isOwner}
|
||||
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={isOwner}
|
||||
onDeleteClick={handleDeleteCollaboration(user.email ?? '')}
|
||||
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}>
|
||||
<MenuItem onClick={() => onChange(CollaborationType.READ)}>
|
||||
{convertCollaborationTypeEnumToReadable(CollaborationType.READ)}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => onChange(CollaborationType.WRITE)}>
|
||||
{convertCollaborationTypeEnumToReadable(CollaborationType.WRITE)}
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
|
||||
export const convertCollaborationTypeEnumToReadable = (
|
||||
type: CollaborationType
|
||||
) => {
|
||||
switch (type) {
|
||||
case CollaborationType.READ:
|
||||
return 'Can view'
|
||||
case CollaborationType.WRITE:
|
||||
return 'Can edit'
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
import {
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
PopoverContent,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
} from '@chakra-ui/react'
|
||||
import { UsersIcon } from 'assets/icons'
|
||||
import React from 'react'
|
||||
import { CollaborationList } from './CollaborationList'
|
||||
|
||||
export const CollaborationMenuButton = () => {
|
||||
return (
|
||||
<Popover isLazy placement="bottom-end">
|
||||
<PopoverTrigger>
|
||||
<span>
|
||||
<Tooltip label="Invite users to collaborate">
|
||||
<IconButton
|
||||
icon={<UsersIcon />}
|
||||
aria-label="Show collaboration menu"
|
||||
/>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent shadow="lg" width="430px">
|
||||
<CollaborationList />
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
@ -0,0 +1,101 @@
|
||||
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'} isTruncated>
|
||||
{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'
|
@ -14,6 +14,7 @@ import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
||||
import { useRouter } from 'next/router'
|
||||
import React from 'react'
|
||||
import { PublishButton } from '../buttons/PublishButton'
|
||||
import { CollaborationMenuButton } from './CollaborationMenuButton'
|
||||
import { EditableTypebotName } from './EditableTypebotName'
|
||||
|
||||
export const headerHeight = 56
|
||||
@ -153,6 +154,7 @@ export const TypebotHeader = () => {
|
||||
</HStack>
|
||||
|
||||
<HStack right="40px" pos="absolute">
|
||||
<CollaborationMenuButton />
|
||||
{router.pathname.includes('/edit') && (
|
||||
<Button onClick={handlePreviewClick}>Preview</Button>
|
||||
)}
|
||||
|
Reference in New Issue
Block a user