feat(editor): ✨ Custom icon on typebot
This commit is contained in:
@ -4,6 +4,7 @@ import {
|
|||||||
Flex,
|
Flex,
|
||||||
IconButton,
|
IconButton,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
|
Tag,
|
||||||
Text,
|
Text,
|
||||||
useDisclosure,
|
useDisclosure,
|
||||||
useToast,
|
useToast,
|
||||||
@ -14,14 +15,15 @@ import { useRouter } from 'next/router'
|
|||||||
import { isMobile } from 'services/utils'
|
import { isMobile } from 'services/utils'
|
||||||
import { MoreButton } from 'components/dashboard/FolderContent/MoreButton'
|
import { MoreButton } from 'components/dashboard/FolderContent/MoreButton'
|
||||||
import { ConfirmModal } from 'components/modals/ConfirmModal'
|
import { ConfirmModal } from 'components/modals/ConfirmModal'
|
||||||
import { GlobeIcon, GripIcon, ToolIcon } from 'assets/icons'
|
import { GripIcon } from 'assets/icons'
|
||||||
import { deleteTypebot, duplicateTypebot } from 'services/typebots'
|
import { deleteTypebot, duplicateTypebot } from 'services/typebots'
|
||||||
import { Typebot } from 'models'
|
import { Typebot } from 'models'
|
||||||
import { useTypebotDnd } from 'contexts/TypebotDndContext'
|
import { useTypebotDnd } from 'contexts/TypebotDndContext'
|
||||||
import { useDebounce } from 'use-debounce'
|
import { useDebounce } from 'use-debounce'
|
||||||
|
import { TypebotIcon } from 'components/shared/TypebotHeader/TypebotIcon'
|
||||||
|
|
||||||
type ChatbotCardProps = {
|
type ChatbotCardProps = {
|
||||||
typebot: Pick<Typebot, 'id' | 'publishedTypebotId' | 'name'>
|
typebot: Pick<Typebot, 'id' | 'publishedTypebotId' | 'name' | 'icon'>
|
||||||
isReadOnly?: boolean
|
isReadOnly?: boolean
|
||||||
onTypebotDeleted?: () => void
|
onTypebotDeleted?: () => void
|
||||||
onMouseDown?: (e: React.MouseEvent<HTMLButtonElement>) => void
|
onMouseDown?: (e: React.MouseEvent<HTMLButtonElement>) => void
|
||||||
@ -101,6 +103,18 @@ export const TypebotButton = ({
|
|||||||
onMouseDown={onMouseDown}
|
onMouseDown={onMouseDown}
|
||||||
cursor="pointer"
|
cursor="pointer"
|
||||||
>
|
>
|
||||||
|
{typebot.publishedTypebotId && (
|
||||||
|
<Tag
|
||||||
|
colorScheme="blue"
|
||||||
|
variant="solid"
|
||||||
|
rounded="full"
|
||||||
|
pos="absolute"
|
||||||
|
top="27px"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Live
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
{!isReadOnly && (
|
{!isReadOnly && (
|
||||||
<>
|
<>
|
||||||
<IconButton
|
<IconButton
|
||||||
@ -129,18 +143,12 @@ export const TypebotButton = ({
|
|||||||
)}
|
)}
|
||||||
<VStack spacing="4">
|
<VStack spacing="4">
|
||||||
<Flex
|
<Flex
|
||||||
boxSize="45px"
|
|
||||||
rounded="full"
|
rounded="full"
|
||||||
justifyContent="center"
|
justifyContent="center"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
bgColor={typebot.publishedTypebotId ? 'blue.500' : 'gray.400'}
|
fontSize={'4xl'}
|
||||||
color="white"
|
|
||||||
>
|
>
|
||||||
{typebot.publishedTypebotId ? (
|
{<TypebotIcon icon={typebot.icon} boxSize={'35px'} />}
|
||||||
<GlobeIcon fontSize="20px" />
|
|
||||||
) : (
|
|
||||||
<ToolIcon fill="white" fontSize="20px" />
|
|
||||||
)}
|
|
||||||
</Flex>
|
</Flex>
|
||||||
<Text>{typebot.name}</Text>
|
<Text>{typebot.name}</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
|
@ -1,31 +1,59 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { ChangeEvent, useEffect, useState } from 'react'
|
||||||
import { Button, Flex, HStack, Stack, Text } from '@chakra-ui/react'
|
import {
|
||||||
|
Button,
|
||||||
|
Flex,
|
||||||
|
HStack,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Input as ClassicInput,
|
||||||
|
SimpleGrid,
|
||||||
|
GridItem,
|
||||||
|
} from '@chakra-ui/react'
|
||||||
import { SearchContextManager } from '@giphy/react-components'
|
import { SearchContextManager } from '@giphy/react-components'
|
||||||
import { UploadButton } from '../buttons/UploadButton'
|
import { UploadButton } from '../buttons/UploadButton'
|
||||||
import { GiphySearch } from './GiphySearch'
|
import { GiphySearch } from './GiphySearch'
|
||||||
import { useTypebot } from 'contexts/TypebotContext'
|
import { useTypebot } from 'contexts/TypebotContext'
|
||||||
import { useDebounce } from 'use-debounce'
|
import { useDebounce } from 'use-debounce'
|
||||||
import { Input } from '../Textbox'
|
import { Input } from '../Textbox'
|
||||||
|
import { BaseEmoji, emojiIndex } from 'emoji-mart'
|
||||||
|
import { emojis } from './emojis'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
url?: string
|
url?: string
|
||||||
onSubmit: (url: string) => void
|
isEmojiEnabled?: boolean
|
||||||
isGiphyEnabled?: boolean
|
isGiphyEnabled?: boolean
|
||||||
|
onSubmit: (url: string) => void
|
||||||
|
onClose?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ImageUploadContent = ({
|
export const ImageUploadContent = ({
|
||||||
url,
|
url,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
|
isEmojiEnabled = false,
|
||||||
isGiphyEnabled = true,
|
isGiphyEnabled = true,
|
||||||
|
onClose,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [currentTab, setCurrentTab] = useState<'link' | 'upload' | 'giphy'>(
|
const [currentTab, setCurrentTab] = useState<
|
||||||
'upload'
|
'link' | 'upload' | 'giphy' | 'emoji'
|
||||||
)
|
>(isEmojiEnabled ? 'emoji' : 'upload')
|
||||||
|
|
||||||
|
const handleSubmit = (url: string) => {
|
||||||
|
onSubmit(url)
|
||||||
|
onClose && onClose()
|
||||||
|
}
|
||||||
|
|
||||||
const handleSubmit = (url: string) => onSubmit(url)
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
<HStack>
|
<HStack>
|
||||||
|
{isEmojiEnabled && (
|
||||||
|
<Button
|
||||||
|
variant={currentTab === 'emoji' ? 'solid' : 'ghost'}
|
||||||
|
onClick={() => setCurrentTab('emoji')}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Emoji
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
variant={currentTab === 'upload' ? 'solid' : 'ghost'}
|
variant={currentTab === 'upload' ? 'solid' : 'ghost'}
|
||||||
onClick={() => setCurrentTab('upload')}
|
onClick={() => setCurrentTab('upload')}
|
||||||
@ -61,7 +89,7 @@ const BodyContent = ({
|
|||||||
url,
|
url,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
}: {
|
}: {
|
||||||
tab: 'upload' | 'link' | 'giphy'
|
tab: 'upload' | 'link' | 'giphy' | 'emoji'
|
||||||
url?: string
|
url?: string
|
||||||
onSubmit: (url: string) => void
|
onSubmit: (url: string) => void
|
||||||
}) => {
|
}) => {
|
||||||
@ -72,6 +100,8 @@ const BodyContent = ({
|
|||||||
return <EmbedLinkContent initialUrl={url} onNewUrl={onSubmit} />
|
return <EmbedLinkContent initialUrl={url} onNewUrl={onSubmit} />
|
||||||
case 'giphy':
|
case 'giphy':
|
||||||
return <GiphyContent onNewUrl={onSubmit} />
|
return <GiphyContent onNewUrl={onSubmit} />
|
||||||
|
case 'emoji':
|
||||||
|
return <EmojiContent onEmojiSelected={onSubmit} />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,6 +144,55 @@ const EmbedLinkContent = ({ initialUrl, onNewUrl }: ContentProps) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const EmojiContent = ({
|
||||||
|
onEmojiSelected,
|
||||||
|
}: {
|
||||||
|
onEmojiSelected: (emoji: string) => void
|
||||||
|
}) => {
|
||||||
|
const [searchValue, setSearchValue] = useState('')
|
||||||
|
const [filteredEmojis, setFilteredEmojis] = useState<string[]>(emojis)
|
||||||
|
|
||||||
|
const handleEmojiClick = (emoji: string) => () => onEmojiSelected(emoji)
|
||||||
|
|
||||||
|
const handleSearchChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setSearchValue(e.target.value)
|
||||||
|
setFilteredEmojis(
|
||||||
|
emojiIndex.search(e.target.value)?.map((o) => (o as BaseEmoji).native) ??
|
||||||
|
emojis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack>
|
||||||
|
<ClassicInput
|
||||||
|
placeholder="Search..."
|
||||||
|
value={searchValue}
|
||||||
|
onChange={handleSearchChange}
|
||||||
|
/>
|
||||||
|
<SimpleGrid
|
||||||
|
maxH="350px"
|
||||||
|
overflowY="scroll"
|
||||||
|
overflowX="hidden"
|
||||||
|
spacing={0}
|
||||||
|
columns={7}
|
||||||
|
>
|
||||||
|
{filteredEmojis.map((emoji) => (
|
||||||
|
<GridItem key={emoji}>
|
||||||
|
<Button
|
||||||
|
onClick={handleEmojiClick(emoji)}
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
fontSize="xl"
|
||||||
|
>
|
||||||
|
{emoji}
|
||||||
|
</Button>
|
||||||
|
</GridItem>
|
||||||
|
))}
|
||||||
|
</SimpleGrid>
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const GiphyContent = ({ onNewUrl }: ContentProps) => {
|
const GiphyContent = ({ onNewUrl }: ContentProps) => {
|
||||||
if (!process.env.NEXT_PUBLIC_GIPHY_API_KEY)
|
if (!process.env.NEXT_PUBLIC_GIPHY_API_KEY)
|
||||||
return <Text>NEXT_PUBLIC_GIPHY_API_KEY is missing in environment</Text>
|
return <Text>NEXT_PUBLIC_GIPHY_API_KEY is missing in environment</Text>
|
||||||
|
1816
apps/builder/components/shared/ImageUploadContent/emojis.ts
Normal file
1816
apps/builder/components/shared/ImageUploadContent/emojis.ts
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,48 @@
|
|||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
Tooltip,
|
||||||
|
chakra,
|
||||||
|
PopoverTrigger,
|
||||||
|
PopoverContent,
|
||||||
|
} from '@chakra-ui/react'
|
||||||
|
import React from 'react'
|
||||||
|
import { ImageUploadContent } from '../ImageUploadContent'
|
||||||
|
import { TypebotIcon } from './TypebotIcon'
|
||||||
|
|
||||||
|
type Props = { icon?: string | null; onChangeIcon: (icon: string) => void }
|
||||||
|
|
||||||
|
export const EditableTypebotIcon = ({ icon, onChangeIcon }: Props) => {
|
||||||
|
return (
|
||||||
|
<Popover isLazy>
|
||||||
|
{({ onClose }) => (
|
||||||
|
<>
|
||||||
|
<Tooltip label="Change icon">
|
||||||
|
<chakra.span
|
||||||
|
cursor="pointer"
|
||||||
|
px="2"
|
||||||
|
rounded="md"
|
||||||
|
_hover={{ bgColor: 'gray.100' }}
|
||||||
|
transition="background-color 0.2s"
|
||||||
|
data-testid="editable-icon"
|
||||||
|
>
|
||||||
|
<PopoverTrigger>
|
||||||
|
<chakra.span>
|
||||||
|
<TypebotIcon icon={icon} emojiFontSize="2xl" />
|
||||||
|
</chakra.span>
|
||||||
|
</PopoverTrigger>
|
||||||
|
</chakra.span>
|
||||||
|
</Tooltip>
|
||||||
|
<PopoverContent p="2">
|
||||||
|
<ImageUploadContent
|
||||||
|
url={icon ?? ''}
|
||||||
|
onSubmit={onChangeIcon}
|
||||||
|
isGiphyEnabled={false}
|
||||||
|
isEmojiEnabled={true}
|
||||||
|
onClose={onClose}
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Popover>
|
||||||
|
)
|
||||||
|
}
|
@ -24,7 +24,6 @@ export const EditableTypebotName = ({ name, onNewName }: EditableProps) => {
|
|||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
display="flex"
|
display="flex"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
minW="100px"
|
|
||||||
/>
|
/>
|
||||||
<EditableInput />
|
<EditableInput />
|
||||||
</Editable>
|
</Editable>
|
||||||
|
@ -16,6 +16,7 @@ import React from 'react'
|
|||||||
import { isNotDefined } from 'utils'
|
import { isNotDefined } from 'utils'
|
||||||
import { PublishButton } from '../buttons/PublishButton'
|
import { PublishButton } from '../buttons/PublishButton'
|
||||||
import { CollaborationMenuButton } from './CollaborationMenuButton'
|
import { CollaborationMenuButton } from './CollaborationMenuButton'
|
||||||
|
import { EditableTypebotIcon } from './EditableTypebotIcons'
|
||||||
import { EditableTypebotName } from './EditableTypebotName'
|
import { EditableTypebotName } from './EditableTypebotName'
|
||||||
|
|
||||||
export const headerHeight = 56
|
export const headerHeight = 56
|
||||||
@ -26,6 +27,7 @@ export const TypebotHeader = () => {
|
|||||||
const {
|
const {
|
||||||
typebot,
|
typebot,
|
||||||
updateOnBothTypebots,
|
updateOnBothTypebots,
|
||||||
|
updateTypebot,
|
||||||
save,
|
save,
|
||||||
undo,
|
undo,
|
||||||
redo,
|
redo,
|
||||||
@ -37,6 +39,8 @@ export const TypebotHeader = () => {
|
|||||||
|
|
||||||
const handleNameSubmit = (name: string) => updateOnBothTypebots({ name })
|
const handleNameSubmit = (name: string) => updateOnBothTypebots({ name })
|
||||||
|
|
||||||
|
const handleChangeIcon = (icon: string) => updateTypebot({ icon })
|
||||||
|
|
||||||
const handlePreviewClick = async () => {
|
const handlePreviewClick = async () => {
|
||||||
save().then()
|
save().then()
|
||||||
setRightPanel(RightPanel.PREVIEW)
|
setRightPanel(RightPanel.PREVIEW)
|
||||||
@ -50,7 +54,7 @@ export const TypebotHeader = () => {
|
|||||||
align="center"
|
align="center"
|
||||||
pos="relative"
|
pos="relative"
|
||||||
h={`${headerHeight}px`}
|
h={`${headerHeight}px`}
|
||||||
zIndex={2}
|
zIndex={100}
|
||||||
bgColor="white"
|
bgColor="white"
|
||||||
flexShrink={0}
|
flexShrink={0}
|
||||||
>
|
>
|
||||||
@ -105,7 +109,7 @@ export const TypebotHeader = () => {
|
|||||||
align="center"
|
align="center"
|
||||||
spacing="6"
|
spacing="6"
|
||||||
>
|
>
|
||||||
<HStack alignItems="center">
|
<HStack alignItems="center" spacing={4}>
|
||||||
<IconButton
|
<IconButton
|
||||||
as={NextChakraLink}
|
as={NextChakraLink}
|
||||||
aria-label="Navigate back"
|
aria-label="Navigate back"
|
||||||
@ -118,33 +122,42 @@ export const TypebotHeader = () => {
|
|||||||
: '/typebots'
|
: '/typebots'
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{typebot?.name && (
|
<HStack spacing={1}>
|
||||||
<EditableTypebotName
|
<EditableTypebotIcon
|
||||||
name={typebot?.name}
|
icon={typebot?.icon}
|
||||||
onNewName={handleNameSubmit}
|
onChangeIcon={handleChangeIcon}
|
||||||
/>
|
/>
|
||||||
)}
|
{typebot?.name && (
|
||||||
<Tooltip label="Undo">
|
<EditableTypebotName
|
||||||
<IconButton
|
name={typebot?.name}
|
||||||
display={['none', 'flex']}
|
onNewName={handleNameSubmit}
|
||||||
icon={<UndoIcon />}
|
/>
|
||||||
size="sm"
|
)}
|
||||||
aria-label="Undo"
|
</HStack>
|
||||||
onClick={undo}
|
|
||||||
isDisabled={!canUndo}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip label="Redo">
|
<HStack>
|
||||||
<IconButton
|
<Tooltip label="Undo">
|
||||||
display={['none', 'flex']}
|
<IconButton
|
||||||
icon={<RedoIcon />}
|
display={['none', 'flex']}
|
||||||
size="sm"
|
icon={<UndoIcon />}
|
||||||
aria-label="Redo"
|
size="sm"
|
||||||
onClick={redo}
|
aria-label="Undo"
|
||||||
isDisabled={!canRedo}
|
onClick={undo}
|
||||||
/>
|
isDisabled={!canUndo}
|
||||||
</Tooltip>
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip label="Redo">
|
||||||
|
<IconButton
|
||||||
|
display={['none', 'flex']}
|
||||||
|
icon={<RedoIcon />}
|
||||||
|
size="sm"
|
||||||
|
aria-label="Redo"
|
||||||
|
onClick={redo}
|
||||||
|
isDisabled={!canRedo}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</HStack>
|
||||||
</HStack>
|
</HStack>
|
||||||
{isSavingLoading && (
|
{isSavingLoading && (
|
||||||
<HStack>
|
<HStack>
|
||||||
|
37
apps/builder/components/shared/TypebotHeader/TypebotIcon.tsx
Normal file
37
apps/builder/components/shared/TypebotHeader/TypebotIcon.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { ToolIcon } from 'assets/icons'
|
||||||
|
import React from 'react'
|
||||||
|
import { chakra, Image } from '@chakra-ui/react'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
icon?: string | null
|
||||||
|
emojiFontSize?: string
|
||||||
|
boxSize?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TypebotIcon = ({
|
||||||
|
icon,
|
||||||
|
boxSize = '25px',
|
||||||
|
emojiFontSize,
|
||||||
|
}: Props) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{icon ? (
|
||||||
|
icon.startsWith('http') ? (
|
||||||
|
<Image
|
||||||
|
src={icon}
|
||||||
|
boxSize={boxSize}
|
||||||
|
objectFit={icon.endsWith('.svg') ? undefined : 'cover'}
|
||||||
|
alt="typebot icon"
|
||||||
|
rounded="md"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<chakra.span role="img" fontSize={emojiFontSize}>
|
||||||
|
{icon}
|
||||||
|
</chakra.span>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<ToolIcon boxSize={boxSize} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
@ -36,7 +36,7 @@ export const UploadButton = ({
|
|||||||
id="file-input"
|
id="file-input"
|
||||||
display="none"
|
display="none"
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
accept=".jpg, .jpeg, .png"
|
accept=".jpg, .jpeg, .png, .svg"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
as="label"
|
as="label"
|
||||||
|
@ -52,6 +52,7 @@ type UpdateTypebotPayload = Partial<{
|
|||||||
publicId: string
|
publicId: string
|
||||||
name: string
|
name: string
|
||||||
publishedTypebotId: string
|
publishedTypebotId: string
|
||||||
|
icon: string
|
||||||
}>
|
}>
|
||||||
|
|
||||||
export type SetTypebot = (
|
export type SetTypebot = (
|
||||||
@ -167,10 +168,6 @@ export const TypebotContext = ({
|
|||||||
new Date(typebot.updatedAt) >
|
new Date(typebot.updatedAt) >
|
||||||
new Date(currentTypebotRef.current.updatedAt)
|
new Date(currentTypebotRef.current.updatedAt)
|
||||||
) {
|
) {
|
||||||
console.log(
|
|
||||||
new Date(typebot.updatedAt),
|
|
||||||
new Date(currentTypebotRef.current.updatedAt)
|
|
||||||
)
|
|
||||||
setLocalTypebot({ ...typebot })
|
setLocalTypebot({ ...typebot })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,6 +45,7 @@
|
|||||||
"db": "*",
|
"db": "*",
|
||||||
"deep-object-diff": "^1.1.7",
|
"deep-object-diff": "^1.1.7",
|
||||||
"dequal": "^2.0.2",
|
"dequal": "^2.0.2",
|
||||||
|
"emoji-mart": "^3.0.1",
|
||||||
"focus-visible": "^5.2.0",
|
"focus-visible": "^5.2.0",
|
||||||
"framer-motion": "^4",
|
"framer-motion": "^4",
|
||||||
"google-auth-library": "^7.14.0",
|
"google-auth-library": "^7.14.0",
|
||||||
@ -87,6 +88,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.19.2",
|
"@playwright/test": "^1.19.2",
|
||||||
"@types/canvas-confetti": "^1.4.2",
|
"@types/canvas-confetti": "^1.4.2",
|
||||||
|
"@types/emoji-mart": "^3.0.9",
|
||||||
"@types/google-spreadsheet": "^3.1.5",
|
"@types/google-spreadsheet": "^3.1.5",
|
||||||
"@types/jsonwebtoken": "8.5.8",
|
"@types/jsonwebtoken": "8.5.8",
|
||||||
"@types/micro-cors": "^0.1.2",
|
"@types/micro-cors": "^0.1.2",
|
||||||
|
@ -46,7 +46,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
folderId,
|
folderId,
|
||||||
},
|
},
|
||||||
orderBy: { createdAt: 'desc' },
|
orderBy: { createdAt: 'desc' },
|
||||||
select: { name: true, publishedTypebotId: true, id: true },
|
select: { name: true, publishedTypebotId: true, id: true, icon: true },
|
||||||
})
|
})
|
||||||
return res.send({ typebots })
|
return res.send({ typebots })
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,14 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
const sharedTypebots = await prisma.collaboratorsOnTypebots.findMany({
|
const sharedTypebots = await prisma.collaboratorsOnTypebots.findMany({
|
||||||
where: { userId: user.id },
|
where: { userId: user.id },
|
||||||
include: {
|
include: {
|
||||||
typebot: { select: { name: true, publishedTypebotId: true, id: true } },
|
typebot: {
|
||||||
|
select: {
|
||||||
|
name: true,
|
||||||
|
publishedTypebotId: true,
|
||||||
|
id: true,
|
||||||
|
icon: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return res.send({
|
return res.send({
|
||||||
|
@ -173,6 +173,7 @@ const parseTestTypebot = (partialTypebot: Partial<Typebot>): Typebot => ({
|
|||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
publishedTypebotId: null,
|
publishedTypebotId: null,
|
||||||
customDomain: null,
|
customDomain: null,
|
||||||
|
icon: null,
|
||||||
variables: [{ id: 'var1', name: 'var1' }],
|
variables: [{ id: 'var1', name: 'var1' }],
|
||||||
...partialTypebot,
|
...partialTypebot,
|
||||||
edges: [
|
edges: [
|
||||||
|
@ -125,4 +125,28 @@ test.describe.parallel('Editor', () => {
|
|||||||
await page.click('button[aria-label="Redo"]')
|
await page.click('button[aria-label="Redo"]')
|
||||||
await expect(page.locator('text="Block #1"')).toBeHidden()
|
await expect(page.locator('text="Block #1"')).toBeHidden()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('Rename and icon change should work', async ({ page }) => {
|
||||||
|
const typebotId = cuid()
|
||||||
|
await createTypebots([
|
||||||
|
{
|
||||||
|
id: typebotId,
|
||||||
|
name: 'My awesome typebot',
|
||||||
|
...parseDefaultBlockWithStep({
|
||||||
|
type: InputStepType.TEXT,
|
||||||
|
options: defaultTextInputOptions,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
await page.goto(`/typebots/${typebotId}/edit`)
|
||||||
|
await page.click('text="My awesome typebot"')
|
||||||
|
await page.fill('input[value="My awesome typebot"]', 'My superb typebot')
|
||||||
|
await page.click('[data-testid="editable-icon"]')
|
||||||
|
await page.fill('input[placeholder="Search..."]', 'love')
|
||||||
|
await page.click('text="😍"')
|
||||||
|
await page.goto(`/typebots`)
|
||||||
|
await expect(page.locator('text="😍"')).toBeVisible()
|
||||||
|
await expect(page.locator('text="My superb typebot"')).toBeVisible()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -37,6 +37,7 @@ export const parsePublicTypebotToTypebot = (
|
|||||||
publishedTypebotId: typebot.id,
|
publishedTypebotId: typebot.id,
|
||||||
folderId: existingTypebot.folderId,
|
folderId: existingTypebot.folderId,
|
||||||
ownerId: existingTypebot.ownerId,
|
ownerId: existingTypebot.ownerId,
|
||||||
|
icon: existingTypebot.icon,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const createPublishedTypebot = async (typebot: PublicTypebot) =>
|
export const createPublishedTypebot = async (typebot: PublicTypebot) =>
|
||||||
|
@ -56,7 +56,7 @@ import { Plan, User } from 'db'
|
|||||||
|
|
||||||
export type TypebotInDashboard = Pick<
|
export type TypebotInDashboard = Pick<
|
||||||
Typebot,
|
Typebot,
|
||||||
'id' | 'name' | 'publishedTypebotId'
|
'id' | 'name' | 'publishedTypebotId' | 'icon'
|
||||||
>
|
>
|
||||||
export const useTypebots = ({
|
export const useTypebots = ({
|
||||||
folderId,
|
folderId,
|
||||||
@ -351,6 +351,7 @@ export const parseNewTypebot = ({
|
|||||||
| 'publishedTypebotId'
|
| 'publishedTypebotId'
|
||||||
| 'publicId'
|
| 'publicId'
|
||||||
| 'customDomain'
|
| 'customDomain'
|
||||||
|
| 'icon'
|
||||||
> => {
|
> => {
|
||||||
const startBlockId = cuid()
|
const startBlockId = cuid()
|
||||||
const startStepId = cuid()
|
const startStepId = cuid()
|
||||||
|
@ -30,7 +30,12 @@ export const useSharedTypebots = ({
|
|||||||
onError: (error: Error) => void
|
onError: (error: Error) => void
|
||||||
}) => {
|
}) => {
|
||||||
const { data, error, mutate } = useSWR<
|
const { data, error, mutate } = useSWR<
|
||||||
{ sharedTypebots: Pick<Typebot, 'name' | 'id' | 'publishedTypebotId'>[] },
|
{
|
||||||
|
sharedTypebots: Pick<
|
||||||
|
Typebot,
|
||||||
|
'name' | 'id' | 'publishedTypebotId' | 'icon'
|
||||||
|
>[]
|
||||||
|
},
|
||||||
Error
|
Error
|
||||||
>(userId ? `/api/users/${userId}/sharedTypebots` : null, fetcher)
|
>(userId ? `/api/users/${userId}/sharedTypebots` : null, fetcher)
|
||||||
if (error) onError(error)
|
if (error) onError(error)
|
||||||
|
@ -76,6 +76,7 @@ const parseTestTypebot = (partialTypebot: Partial<Typebot>): Typebot => ({
|
|||||||
folderId: null,
|
folderId: null,
|
||||||
name: 'My typebot',
|
name: 'My typebot',
|
||||||
ownerId: 'proUser',
|
ownerId: 'proUser',
|
||||||
|
icon: null,
|
||||||
theme: defaultTheme,
|
theme: defaultTheme,
|
||||||
settings: defaultSettings,
|
settings: defaultSettings,
|
||||||
publicId: partialTypebot.id + '-public',
|
publicId: partialTypebot.id + '-public',
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
"@prisma/client": "^3.10.0"
|
"@prisma/client": "^3.10.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dx": "dotenv -e ../../apps/builder/.env.local prisma db push && yarn start:sutdio ",
|
"dx": "dotenv -e ../../apps/builder/.env.local prisma db push && yarn generate:schema && yarn start:sutdio ",
|
||||||
"build": "yarn generate:schema",
|
"build": "yarn generate:schema",
|
||||||
"start:sutdio": "dotenv -e ../../apps/builder/.env.local -v BROWSER=none prisma studio",
|
"start:sutdio": "dotenv -e ../../apps/builder/.env.local -v BROWSER=none prisma studio",
|
||||||
"generate:schema": "dotenv -e ../../apps/builder/.env.local prisma generate",
|
"generate:schema": "dotenv -e ../../apps/builder/.env.local prisma generate",
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Typebot" ADD COLUMN "icon" TEXT;
|
@ -114,6 +114,7 @@ model Typebot {
|
|||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @default(now()) @updatedAt
|
updatedAt DateTime @default(now()) @updatedAt
|
||||||
|
icon String?
|
||||||
name String
|
name String
|
||||||
ownerId String
|
ownerId String
|
||||||
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
|
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
|
||||||
|
15
yarn.lock
15
yarn.lock
@ -4104,6 +4104,13 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
cssnano "*"
|
cssnano "*"
|
||||||
|
|
||||||
|
"@types/emoji-mart@^3.0.9":
|
||||||
|
version "3.0.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/emoji-mart/-/emoji-mart-3.0.9.tgz#2f7ef5d9ec194f28029c46c81a5fc1e5b0efa73c"
|
||||||
|
integrity sha512-qdBo/2Y8MXaJ/2spKjDZocuq79GpnOhkwMHnK2GnVFa8WYFgfA+ei6sil3aeWQPCreOKIx9ogPpR5+7MaOqYAA==
|
||||||
|
dependencies:
|
||||||
|
"@types/react" "*"
|
||||||
|
|
||||||
"@types/eslint-scope@^3.7.3":
|
"@types/eslint-scope@^3.7.3":
|
||||||
version "3.7.3"
|
version "3.7.3"
|
||||||
resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.3.tgz#125b88504b61e3c8bc6f870882003253005c3224"
|
resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.3.tgz#125b88504b61e3c8bc6f870882003253005c3224"
|
||||||
@ -7315,6 +7322,14 @@ emittery@^0.8.1:
|
|||||||
resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.8.1.tgz#bb23cc86d03b30aa75a7f734819dee2e1ba70860"
|
resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.8.1.tgz#bb23cc86d03b30aa75a7f734819dee2e1ba70860"
|
||||||
integrity sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg==
|
integrity sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg==
|
||||||
|
|
||||||
|
emoji-mart@^3.0.1:
|
||||||
|
version "3.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/emoji-mart/-/emoji-mart-3.0.1.tgz#9ce86706e02aea0506345f98464814a662ca54c6"
|
||||||
|
integrity sha512-sxpmMKxqLvcscu6mFn9ITHeZNkGzIvD0BSNFE/LJESPbCA8s1jM6bCDPjWbV31xHq7JXaxgpHxLB54RCbBZSlg==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.0.0"
|
||||||
|
prop-types "^15.6.0"
|
||||||
|
|
||||||
emoji-regex@^8.0.0:
|
emoji-regex@^8.0.0:
|
||||||
version "8.0.0"
|
version "8.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
|
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
|
||||||
|
Reference in New Issue
Block a user