2
0

♻️ (editor) Improve webhook creation

Remove terrible useEffects
This commit is contained in:
Baptiste Arnaud
2023-02-15 14:51:58 +01:00
parent 6e066c44e1
commit ac464eabdf
23 changed files with 481 additions and 528 deletions

View File

@@ -14,15 +14,6 @@ const nextConfig = withTM({
experimental: { experimental: {
outputFileTracingRoot: path.join(__dirname, '../../'), outputFileTracingRoot: path.join(__dirname, '../../'),
}, },
async redirects() {
return [
{
source: '/typebots/:typebotId',
destination: '/typebots/:typebotId/edit',
permanent: true,
},
]
},
headers: async () => { headers: async () => {
return [ return [
{ {

View File

@@ -15,7 +15,7 @@
"dependencies": { "dependencies": {
"@chakra-ui/anatomy": "^2.1.1", "@chakra-ui/anatomy": "^2.1.1",
"@chakra-ui/css-reset": "2.0.11", "@chakra-ui/css-reset": "2.0.11",
"@chakra-ui/react": "2.4.9", "@chakra-ui/react": "2.5.0",
"@chakra-ui/theme-tools": "^2.0.16", "@chakra-ui/theme-tools": "^2.0.16",
"@codemirror/lang-css": "6.0.1", "@codemirror/lang-css": "6.0.1",
"@codemirror/lang-html": "6.4.1", "@codemirror/lang-html": "6.4.1",
@@ -63,7 +63,7 @@
"emails": "workspace:*", "emails": "workspace:*",
"emojilib": "3.0.8", "emojilib": "3.0.8",
"focus-visible": "5.2.0", "focus-visible": "5.2.0",
"framer-motion": "8.5.4", "framer-motion": "9.0.2",
"google-auth-library": "8.7.0", "google-auth-library": "8.7.0",
"google-spreadsheet": "3.3.0", "google-spreadsheet": "3.3.0",
"got": "12.5.3", "got": "12.5.3",

View File

@@ -32,14 +32,16 @@ export const TextBox = ({
debounceTimeout = 1000, debounceTimeout = 1000,
label, label,
moreInfoTooltip, moreInfoTooltip,
defaultValue,
isRequired,
...props ...props
}: TextBoxProps) => { }: TextBoxProps) => {
const textBoxRef = useRef<(HTMLInputElement & HTMLTextAreaElement) | null>( const textBoxRef = useRef<(HTMLInputElement & HTMLTextAreaElement) | null>(
null null
) )
const [value, setValue] = useState<string>(props.defaultValue ?? '') const [value, setValue] = useState<string>(defaultValue ?? '')
const [carretPosition, setCarretPosition] = useState<number>( const [carretPosition, setCarretPosition] = useState<number>(
props.defaultValue?.length ?? 0 defaultValue?.length ?? 0
) )
const [isTouched, setIsTouched] = useState(false) const [isTouched, setIsTouched] = useState(false)
const debounced = useDebouncedCallback( const debounced = useDebouncedCallback(
@@ -50,10 +52,9 @@ export const TextBox = ({
) )
useEffect(() => { useEffect(() => {
if (props.defaultValue !== value && value === '' && !isTouched) if (isTouched || defaultValue === value) return
setValue(props.defaultValue ?? '') setValue(defaultValue ?? '')
// eslint-disable-next-line react-hooks/exhaustive-deps }, [defaultValue, isTouched, value])
}, [props.defaultValue])
useEffect( useEffect(
() => () => { () => () => {
@@ -111,7 +112,7 @@ export const TextBox = ({
) )
return ( return (
<FormControl isRequired={props.isRequired}> <FormControl isRequired={isRequired}>
{label && ( {label && (
<FormLabel> <FormLabel>
{label}{' '} {label}{' '}

View File

@@ -22,17 +22,16 @@ import {
VariableForTest, VariableForTest,
ResponseVariableMapping, ResponseVariableMapping,
WebhookBlock, WebhookBlock,
defaultWebhookAttributes,
Webhook,
MakeComBlock, MakeComBlock,
PabblyConnectBlock, PabblyConnectBlock,
Webhook,
} from 'models' } from 'models'
import { DropdownList } from '@/components/DropdownList' import { DropdownList } from '@/components/DropdownList'
import { CodeEditor } from '@/components/CodeEditor' import { CodeEditor } from '@/components/CodeEditor'
import { HeadersInputs, QueryParamsInputs } from './KeyValueInputs' import { HeadersInputs, QueryParamsInputs } from './KeyValueInputs'
import { VariableForTestInputs } from './VariableForTestInputs' import { VariableForTestInputs } from './VariableForTestInputs'
import { DataVariableInputs } from './ResponseMappingInputs' import { DataVariableInputs } from './ResponseMappingInputs'
import { byId } from 'utils' import { byId, env } from 'utils'
import { ExternalLinkIcon } from '@/components/icons' import { ExternalLinkIcon } from '@/components/icons'
import { useToast } from '@/hooks/useToast' import { useToast } from '@/hooks/useToast'
import { SwitchWithLabel } from '@/components/SwitchWithLabel' import { SwitchWithLabel } from '@/components/SwitchWithLabel'
@@ -41,11 +40,15 @@ import { executeWebhook } from '../../queries/executeWebhookQuery'
import { getDeepKeys } from '../../utils/getDeepKeys' import { getDeepKeys } from '../../utils/getDeepKeys'
import { Input } from '@/components/inputs' import { Input } from '@/components/inputs'
import { convertVariablesForTestToVariables } from '../../utils/convertVariablesForTestToVariables' import { convertVariablesForTestToVariables } from '../../utils/convertVariablesForTestToVariables'
import { useDebouncedCallback } from 'use-debounce'
const debounceWebhookTimeout = 2000
type Provider = { type Provider = {
name: 'Make.com' | 'Pabbly Connect' name: 'Pabbly Connect'
url: string url: string
} }
type Props = { type Props = {
block: WebhookBlock | MakeComBlock | PabblyConnectBlock block: WebhookBlock | MakeComBlock | PabblyConnectBlock
onOptionsChange: (options: WebhookOptions) => void onOptionsChange: (options: WebhookOptions) => void
@@ -61,39 +64,28 @@ export const WebhookSettings = ({
const [isTestResponseLoading, setIsTestResponseLoading] = useState(false) const [isTestResponseLoading, setIsTestResponseLoading] = useState(false)
const [testResponse, setTestResponse] = useState<string>() const [testResponse, setTestResponse] = useState<string>()
const [responseKeys, setResponseKeys] = useState<string[]>([]) const [responseKeys, setResponseKeys] = useState<string[]>([])
const { showToast } = useToast() const { showToast } = useToast()
const [localWebhook, setLocalWebhook] = useState( const [localWebhook, _setLocalWebhook] = useState(
webhooks.find(byId(webhookId)) webhooks.find(byId(webhookId))
) )
const updateWebhookDebounced = useDebouncedCallback(
async (newLocalWebhook) => {
await updateWebhook(newLocalWebhook.id, newLocalWebhook)
},
env('E2E_TEST') === 'true' ? 0 : debounceWebhookTimeout
)
useEffect(() => { const setLocalWebhook = (newLocalWebhook: Webhook) => {
if (localWebhook) return _setLocalWebhook(newLocalWebhook)
const incomingWebhook = webhooks.find(byId(webhookId)) updateWebhookDebounced(newLocalWebhook)
setLocalWebhook(incomingWebhook) }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [webhooks])
useEffect(() => { useEffect(
if (!typebot) return () => () => {
if (!localWebhook) { updateWebhookDebounced.flush()
const newWebhook = { },
id: webhookId, [updateWebhookDebounced]
...defaultWebhookAttributes, )
typebotId: typebot.id,
} as Webhook
updateWebhook(webhookId, newWebhook)
}
return () => {
setLocalWebhook((localWebhook) => {
if (!localWebhook) return
updateWebhook(webhookId, localWebhook).then()
return localWebhook
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const handleUrlChange = (url?: string) => const handleUrlChange = (url?: string) =>
localWebhook && setLocalWebhook({ ...localWebhook, url: url ?? null }) localWebhook && setLocalWebhook({ ...localWebhook, url: url ?? null })
@@ -126,8 +118,7 @@ export const WebhookSettings = ({
const handleTestRequestClick = async () => { const handleTestRequestClick = async () => {
if (!typebot || !localWebhook) return if (!typebot || !localWebhook) return
setIsTestResponseLoading(true) setIsTestResponseLoading(true)
await updateWebhook(localWebhook.id, localWebhook) await Promise.all([updateWebhook(localWebhook.id, localWebhook), save()])
await save()
const { data, error } = await executeWebhook( const { data, error } = await executeWebhook(
typebot.id, typebot.id,
convertVariablesForTestToVariables( convertVariablesForTestToVariables(
@@ -152,6 +143,7 @@ export const WebhookSettings = ({
) )
if (!localWebhook) return <Spinner /> if (!localWebhook) return <Spinner />
return ( return (
<Stack spacing={4}> <Stack spacing={4}>
{provider && ( {provider && (

View File

@@ -1,4 +1,4 @@
export { duplicateWebhookQueries } from './queries/duplicateWebhookQuery' export { duplicateWebhookQuery } from './queries/duplicateWebhookQuery'
export { WebhookSettings } from './components/WebhookSettings' export { WebhookSettings } from './components/WebhookSettings'
export { WebhookContent } from './components/WebhookContent' export { WebhookContent } from './components/WebhookContent'
export { WebhookIcon } from './components/WebhookIcon' export { WebhookIcon } from './components/WebhookIcon'

View File

@@ -0,0 +1,14 @@
import { Webhook } from 'models'
import { sendRequest } from 'utils'
type Props = {
typebotId: string
data: Partial<Omit<Webhook, 'typebotId'>>
}
export const createWebhookQuery = ({ typebotId, data }: Props) =>
sendRequest<{ webhook: Webhook }>({
method: 'POST',
url: `/api/typebots/${typebotId}/webhooks`,
body: { data },
})

View File

@@ -1,17 +1,27 @@
import { Webhook } from 'models' import { Webhook } from 'models'
import { sendRequest } from 'utils' import { sendRequest } from 'utils'
import { saveWebhookQuery } from './saveWebhookQuery' import { createWebhookQuery } from './createWebhookQuery'
export const duplicateWebhookQueries = async ( type Props = {
typebotId: string, existingIds: { typebotId: string; webhookId: string }
existingWebhookId: string, newIds: { typebotId: string; webhookId: string }
newWebhookId: string }
): Promise<Webhook | undefined> => { export const duplicateWebhookQuery = async ({
existingIds,
newIds,
}: Props): Promise<Webhook | undefined> => {
const { data } = await sendRequest<{ webhook: Webhook }>( const { data } = await sendRequest<{ webhook: Webhook }>(
`/api/webhooks/${existingWebhookId}` `/api/typebots/${existingIds.typebotId}/webhooks/${existingIds.webhookId}`
) )
if (!data) return if (!data) return
const newWebhook = { ...data.webhook, id: newWebhookId, typebotId } const newWebhook = {
await saveWebhookQuery(newWebhook.id, newWebhook) ...data.webhook,
id: newIds.webhookId,
typebotId: newIds.typebotId,
}
await createWebhookQuery({
typebotId: newIds.typebotId,
data: { ...data.webhook, id: newIds.webhookId },
})
return newWebhook return newWebhook
} }

View File

@@ -1,12 +0,0 @@
import { Webhook } from 'models'
import { sendRequest } from 'utils'
export const saveWebhookQuery = (
webhookId: string,
webhook: Partial<Webhook>
) =>
sendRequest<{ webhook: Webhook }>({
method: 'PUT',
url: `/api/webhooks/${webhookId}`,
body: webhook,
})

View File

@@ -0,0 +1,15 @@
import { Webhook } from 'models'
import { sendRequest } from 'utils'
type Props = {
typebotId: string
webhookId: string
data: Partial<Omit<Webhook, 'id' | 'typebotId'>>
}
export const updateWebhookQuery = ({ typebotId, webhookId, data }: Props) =>
sendRequest<{ webhook: Webhook }>({
method: 'PATCH',
url: `/api/typebots/${typebotId}/webhooks/${webhookId}`,
body: { data },
})

View File

@@ -1,4 +1,4 @@
import { duplicateWebhookQueries } from '@/features/blocks/integrations/webhook' import { duplicateWebhookQuery } from '@/features/blocks/integrations/webhook'
import { createId } from '@paralleldrive/cuid2' import { createId } from '@paralleldrive/cuid2'
import { Plan, Prisma } from 'db' import { Plan, Prisma } from 'db'
import { import {
@@ -25,11 +25,13 @@ export const importTypebotQuery = async (typebot: Typebot, userPlan: Plan) => {
.filter(isWebhookBlock) .filter(isWebhookBlock)
await Promise.all( await Promise.all(
webhookBlocks.map((s) => webhookBlocks.map((s) =>
duplicateWebhookQueries( duplicateWebhookQuery({
newTypebot.id, existingIds: { typebotId: typebot.id, webhookId: s.webhookId },
s.webhookId, newIds: {
webhookIdsMapping.get(s.webhookId) as string typebotId: newTypebot.id,
) webhookId: webhookIdsMapping.get(s.webhookId) as string,
},
})
) )
) )
return { data, error } return { data, error }

View File

@@ -11,6 +11,7 @@ import { Router, useRouter } from 'next/router'
import { import {
createContext, createContext,
ReactNode, ReactNode,
useCallback,
useContext, useContext,
useEffect, useEffect,
useMemo, useMemo,
@@ -34,7 +35,7 @@ import {
updatePublishedTypebotQuery, updatePublishedTypebotQuery,
deletePublishedTypebotQuery, deletePublishedTypebotQuery,
} from '@/features/publish/queries' } from '@/features/publish/queries'
import { saveWebhookQuery } from '@/features/blocks/integrations/webhook/queries/saveWebhookQuery' import { updateWebhookQuery } from '@/features/blocks/integrations/webhook/queries/updateWebhookQuery'
import { import {
checkIfTypebotsAreEqual, checkIfTypebotsAreEqual,
checkIfPublished, checkIfPublished,
@@ -43,6 +44,8 @@ import {
parsePublicTypebotToTypebot, parsePublicTypebotToTypebot,
} from '@/features/publish/utils' } from '@/features/publish/utils'
import { useAutoSave } from '@/hooks/useAutoSave' import { useAutoSave } from '@/hooks/useAutoSave'
import { createWebhookQuery } from '@/features/blocks/integrations/webhook/queries/createWebhookQuery'
import { duplicateWebhookQuery } from '@/features/blocks/integrations/webhook/queries/duplicateWebhookQuery'
const autoSaveTimeout = 10000 const autoSaveTimeout = 10000
@@ -306,20 +309,61 @@ export const TypebotProvider = ({
return saveTypebot() return saveTypebot()
} }
const updateWebhook = async ( const updateWebhook = useCallback(
webhookId: string, async (webhookId: string, updates: Partial<Webhook>) => {
updates: Partial<Webhook> if (!typebot) return
const { data } = await updateWebhookQuery({
typebotId: typebot.id,
webhookId,
data: updates,
})
if (data)
mutate({
typebot,
publishedTypebot,
webhooks: (webhooks ?? []).map((w) =>
w.id === webhookId ? data.webhook : w
),
})
},
[mutate, publishedTypebot, typebot, webhooks]
)
const createWebhook = async (data: Partial<Webhook>) => {
if (!typebot) return
const response = await createWebhookQuery({
typebotId: typebot.id,
data,
})
if (!response.data?.webhook) return
mutate({
typebot,
publishedTypebot,
webhooks: (webhooks ?? []).concat(response.data?.webhook),
})
}
const duplicateWebhook = async (
existingWebhookId: string,
newWebhookId: string
) => { ) => {
if (!typebot) return if (!typebot) return
const { data } = await saveWebhookQuery(webhookId, updates) const newWebhook = await duplicateWebhookQuery({
if (data) existingIds: {
mutate({ typebotId: typebot.id,
typebot, webhookId: existingWebhookId,
publishedTypebot, },
webhooks: (webhooks ?? []).map((w) => newIds: {
w.id === webhookId ? data.webhook : w typebotId: typebot.id,
), webhookId: newWebhookId,
}) },
})
if (!newWebhook) return
mutate({
typebot,
publishedTypebot,
webhooks: (webhooks ?? []).concat(newWebhook),
})
} }
return ( return (
@@ -343,8 +387,14 @@ export const TypebotProvider = ({
updateTypebot: updateLocalTypebot, updateTypebot: updateLocalTypebot,
restorePublishedTypebot, restorePublishedTypebot,
updateWebhook, updateWebhook,
...groupsActions(setLocalTypebot as SetTypebot), ...groupsActions(setLocalTypebot as SetTypebot, {
...blocksAction(setLocalTypebot as SetTypebot), onWebhookBlockCreated: createWebhook,
onWebhookBlockDuplicated: duplicateWebhook,
}),
...blocksAction(setLocalTypebot as SetTypebot, {
onWebhookBlockCreated: createWebhook,
onWebhookBlockDuplicated: duplicateWebhook,
}),
...variablesAction(setLocalTypebot as SetTypebot), ...variablesAction(setLocalTypebot as SetTypebot),
...edgesAction(setLocalTypebot as SetTypebot), ...edgesAction(setLocalTypebot as SetTypebot),
...itemsAction(setLocalTypebot as SetTypebot), ...itemsAction(setLocalTypebot as SetTypebot),

View File

@@ -4,6 +4,7 @@ import {
DraggableBlock, DraggableBlock,
DraggableBlockType, DraggableBlockType,
BlockIndices, BlockIndices,
Webhook,
} from 'models' } from 'models'
import { WritableDraft } from 'immer/dist/types/types-external' import { WritableDraft } from 'immer/dist/types/types-external'
import { SetTypebot } from '../TypebotProvider' import { SetTypebot } from '../TypebotProvider'
@@ -29,7 +30,18 @@ export type BlocksActions = {
deleteBlock: (indices: BlockIndices) => void deleteBlock: (indices: BlockIndices) => void
} }
export const blocksAction = (setTypebot: SetTypebot): BlocksActions => ({ export type WebhookCallBacks = {
onWebhookBlockCreated: (data: Partial<Webhook>) => void
onWebhookBlockDuplicated: (
existingWebhookId: string,
newWebhookId: string
) => void
}
export const blocksAction = (
setTypebot: SetTypebot,
{ onWebhookBlockCreated, onWebhookBlockDuplicated }: WebhookCallBacks
): BlocksActions => ({
createBlock: ( createBlock: (
groupId: string, groupId: string,
block: DraggableBlock | DraggableBlockType, block: DraggableBlock | DraggableBlockType,
@@ -37,7 +49,13 @@ export const blocksAction = (setTypebot: SetTypebot): BlocksActions => ({
) => ) =>
setTypebot((typebot) => setTypebot((typebot) =>
produce(typebot, (typebot) => { produce(typebot, (typebot) => {
createBlockDraft(typebot, block, groupId, indices) createBlockDraft(
typebot,
block,
groupId,
indices,
onWebhookBlockCreated
)
}) })
), ),
updateBlock: ( updateBlock: (
@@ -54,7 +72,10 @@ export const blocksAction = (setTypebot: SetTypebot): BlocksActions => ({
setTypebot((typebot) => setTypebot((typebot) =>
produce(typebot, (typebot) => { produce(typebot, (typebot) => {
const block = { ...typebot.groups[groupIndex].blocks[blockIndex] } const block = { ...typebot.groups[groupIndex].blocks[blockIndex] }
const newBlock = duplicateBlockDraft(block.groupId)(block) const newBlock = duplicateBlockDraft(block.groupId)(
block,
onWebhookBlockDuplicated
)
typebot.groups[groupIndex].blocks.splice(blockIndex + 1, 0, newBlock) typebot.groups[groupIndex].blocks.splice(blockIndex + 1, 0, newBlock)
}) })
), ),
@@ -81,7 +102,8 @@ export const createBlockDraft = (
typebot: WritableDraft<Typebot>, typebot: WritableDraft<Typebot>,
block: DraggableBlock | DraggableBlockType, block: DraggableBlock | DraggableBlockType,
groupId: string, groupId: string,
{ groupIndex, blockIndex }: BlockIndices { groupIndex, blockIndex }: BlockIndices,
onWebhookBlockCreated?: (data: Partial<Webhook>) => void
) => { ) => {
const blocks = typebot.groups[groupIndex].blocks const blocks = typebot.groups[groupIndex].blocks
if ( if (
@@ -91,7 +113,13 @@ export const createBlockDraft = (
) )
deleteEdgeDraft(typebot, blocks[blockIndex - 1].outgoingEdgeId as string) deleteEdgeDraft(typebot, blocks[blockIndex - 1].outgoingEdgeId as string)
typeof block === 'string' typeof block === 'string'
? createNewBlock(typebot, block, groupId, { groupIndex, blockIndex }) ? createNewBlock(
typebot,
block,
groupId,
{ groupIndex, blockIndex },
onWebhookBlockCreated
)
: moveBlockToGroup(typebot, block, groupId, { groupIndex, blockIndex }) : moveBlockToGroup(typebot, block, groupId, { groupIndex, blockIndex })
removeEmptyGroups(typebot) removeEmptyGroups(typebot)
} }
@@ -100,10 +128,13 @@ const createNewBlock = async (
typebot: WritableDraft<Typebot>, typebot: WritableDraft<Typebot>,
type: DraggableBlockType, type: DraggableBlockType,
groupId: string, groupId: string,
{ groupIndex, blockIndex }: BlockIndices { groupIndex, blockIndex }: BlockIndices,
onWebhookBlockCreated?: (data: Partial<Webhook>) => void
) => { ) => {
const newBlock = parseNewBlock(type, groupId) const newBlock = parseNewBlock(type, groupId)
typebot.groups[groupIndex].blocks.splice(blockIndex ?? 0, 0, newBlock) typebot.groups[groupIndex].blocks.splice(blockIndex ?? 0, 0, newBlock)
if (onWebhookBlockCreated && 'webhookId' in newBlock && newBlock.webhookId)
onWebhookBlockCreated({ id: newBlock.webhookId })
} }
const moveBlockToGroup = ( const moveBlockToGroup = (
@@ -140,7 +171,10 @@ const moveBlockToGroup = (
export const duplicateBlockDraft = export const duplicateBlockDraft =
(groupId: string) => (groupId: string) =>
(block: Block): Block => { (
block: Block,
onWebhookBlockDuplicated: WebhookCallBacks['onWebhookBlockDuplicated']
): Block => {
const blockId = createId() const blockId = createId()
if (blockHasItems(block)) if (blockHasItems(block))
return { return {
@@ -150,14 +184,17 @@ export const duplicateBlockDraft =
items: block.items.map(duplicateItemDraft(blockId)), items: block.items.map(duplicateItemDraft(blockId)),
outgoingEdgeId: undefined, outgoingEdgeId: undefined,
} as Block } as Block
if (isWebhookBlock(block)) if (isWebhookBlock(block)) {
const newWebhookId = createId()
onWebhookBlockDuplicated(block.webhookId, newWebhookId)
return { return {
...block, ...block,
groupId, groupId,
id: blockId, id: blockId,
webhookId: createId(), webhookId: newWebhookId,
outgoingEdgeId: undefined, outgoingEdgeId: undefined,
} }
}
return { return {
...block, ...block,
groupId, groupId,

View File

@@ -6,6 +6,7 @@ import {
deleteGroupDraft, deleteGroupDraft,
createBlockDraft, createBlockDraft,
duplicateBlockDraft, duplicateBlockDraft,
WebhookCallBacks,
} from './blocks' } from './blocks'
import { Coordinates } from '@/features/graph' import { Coordinates } from '@/features/graph'
@@ -22,7 +23,10 @@ export type GroupsActions = {
deleteGroup: (groupIndex: number) => void deleteGroup: (groupIndex: number) => void
} }
const groupsActions = (setTypebot: SetTypebot): GroupsActions => ({ const groupsActions = (
setTypebot: SetTypebot,
{ onWebhookBlockCreated, onWebhookBlockDuplicated }: WebhookCallBacks
): GroupsActions => ({
createGroup: ({ createGroup: ({
id, id,
block, block,
@@ -42,7 +46,13 @@ const groupsActions = (setTypebot: SetTypebot): GroupsActions => ({
blocks: [], blocks: [],
} }
typebot.groups.push(newGroup) typebot.groups.push(newGroup)
createBlockDraft(typebot, block, newGroup.id, indices) createBlockDraft(
typebot,
block,
newGroup.id,
indices,
onWebhookBlockCreated
)
}) })
), ),
updateGroup: (groupIndex: number, updates: Partial<Omit<Group, 'id'>>) => updateGroup: (groupIndex: number, updates: Partial<Omit<Group, 'id'>>) =>
@@ -61,7 +71,9 @@ const groupsActions = (setTypebot: SetTypebot): GroupsActions => ({
...group, ...group,
title: `${group.title} copy`, title: `${group.title} copy`,
id, id,
blocks: group.blocks.map(duplicateBlockDraft(id)), blocks: group.blocks.map((block) =>
duplicateBlockDraft(id)(block, onWebhookBlockDuplicated)
),
graphCoordinates: { graphCoordinates: {
x: group.graphCoordinates.x + 200, x: group.graphCoordinates.x + 200,
y: group.graphCoordinates.y + 100, y: group.graphCoordinates.y + 100,

View File

@@ -159,7 +159,7 @@ export const TypebotButton = ({
<MenuItem onClick={handleUnpublishClick}>Unpublish</MenuItem> <MenuItem onClick={handleUnpublishClick}>Unpublish</MenuItem>
)} )}
<MenuItem onClick={handleDuplicateClick}>Duplicate</MenuItem> <MenuItem onClick={handleDuplicateClick}>Duplicate</MenuItem>
<MenuItem color="red" onClick={handleDeleteClick}> <MenuItem color="red.400" onClick={handleDeleteClick}>
Delete Delete
</MenuItem> </MenuItem>
</MoreButton> </MoreButton>

View File

@@ -223,7 +223,12 @@ export const Graph = ({
const zoomOut = () => zoom({ delta: -zoomButtonsScaleBlock }) const zoomOut = () => zoom({ delta: -zoomButtonsScaleBlock })
return ( return (
<Flex ref={graphContainerRef} position="relative" {...props}> <Flex
ref={graphContainerRef}
position="relative"
style={{ touchAction: 'none' }}
{...props}
>
<ZoomButtons onZoomInClick={zoomIn} onZoomOutClick={zoomOut} /> <ZoomButtons onZoomInClick={zoomIn} onZoomOutClick={zoomOut} />
<Flex <Flex
flex="1" flex="1"

View File

@@ -16,7 +16,6 @@ import {
Block, Block,
BlockOptions, BlockOptions,
BlockWithOptions, BlockWithOptions,
Webhook,
} from 'models' } from 'models'
import { useRef } from 'react' import { useRef } from 'react'
import { DateInputSettingsBody } from '@/features/blocks/inputs/date' import { DateInputSettingsBody } from '@/features/blocks/inputs/date'
@@ -45,7 +44,6 @@ import { ScriptSettings } from '@/features/blocks/logic/script/components/Script
type Props = { type Props = {
block: BlockWithOptions block: BlockWithOptions
webhook?: Webhook
onExpandClick: () => void onExpandClick: () => void
onBlockChange: (updates: Partial<Block>) => void onBlockChange: (updates: Partial<Block>) => void
} }
@@ -93,7 +91,6 @@ export const BlockSettings = ({
onBlockChange, onBlockChange,
}: { }: {
block: BlockWithOptions block: BlockWithOptions
webhook?: Webhook
onBlockChange: (block: Partial<Block>) => void onBlockChange: (block: Partial<Block>) => void
}): JSX.Element => { }): JSX.Element => {
const handleOptionsChange = (options: BlockOptions) => { const handleOptionsChange = (options: BlockOptions) => {

View File

@@ -195,6 +195,7 @@ const NonMemoizedDraggableGroupNode = ({
transform: `translate(${currentCoordinates?.x ?? 0}px, ${ transform: `translate(${currentCoordinates?.x ?? 0}px, ${
currentCoordinates?.y ?? 0 currentCoordinates?.y ?? 0
}px)`, }px)`,
touchAction: 'none',
}} }}
onMouseEnter={handleMouseEnter} onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave} onMouseLeave={handleMouseLeave}

View File

@@ -112,9 +112,7 @@ export const MembersList = () => {
{isDefined(seatsLimit) && ( {isDefined(seatsLimit) && (
<Heading fontSize="2xl"> <Heading fontSize="2xl">
Members{' '} Members{' '}
{seatsLimit === -1 {seatsLimit === -1 ? '' : `(${currentMembersCount}/${seatsLimit})`}
? ''
: `(${currentMembersCount + invitations.length}/${seatsLimit})`}
</Heading> </Heading>
)} )}
{workspace?.id && canEdit && ( {workspace?.id && canEdit && (

View File

@@ -88,6 +88,9 @@ test('can manage members', async ({ page }) => {
await page.goto('/typebots') await page.goto('/typebots')
await page.click('text=Settings & Members') await page.click('text=Settings & Members')
await page.click('text="Members"') await page.click('text="Members"')
await expect(
page.getByRole('heading', { name: 'Members (1/5)' })
).toBeVisible()
await expect(page.locator('text="user@email.com"').nth(1)).toBeVisible() await expect(page.locator('text="user@email.com"').nth(1)).toBeVisible()
await expect(page.locator('button >> text="Invite"')).toBeEnabled() await expect(page.locator('button >> text="Invite"')).toBeEnabled()
await page.fill( await page.fill(

View File

@@ -1,22 +1,24 @@
import prisma from '@/lib/prisma' import prisma from '@/lib/prisma'
import { defaultWebhookAttributes } from 'models' import { defaultWebhookAttributes, Webhook } from 'models'
import { NextApiRequest, NextApiResponse } from 'next' import { NextApiRequest, NextApiResponse } from 'next'
import { canWriteTypebots } from '@/utils/api/dbRules'
import { getAuthenticatedUser } from '@/features/auth/api' import { getAuthenticatedUser } from '@/features/auth/api'
import { forbidden, methodNotAllowed, notAuthenticated } from 'utils/api' import { methodNotAllowed, notAuthenticated, notFound } from 'utils/api'
import { getTypebot } from '@/features/typebot/api/utils/getTypebot'
const handler = async (req: NextApiRequest, res: NextApiResponse) => { const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req) const user = await getAuthenticatedUser(req)
if (!user) return notAuthenticated(res) if (!user) return notAuthenticated(res)
if (req.method === 'POST') { if (req.method === 'POST') {
const typebotId = req.query.typebotId as string const typebotId = req.query.typebotId as string
const typebot = await prisma.typebot.findFirst({ const data = req.body.data as Partial<Webhook>
where: canWriteTypebots(typebotId, user), const typebot = await getTypebot({
select: { id: true }, accessLevel: 'write',
user,
typebotId,
}) })
if (!typebot) return forbidden(res) if (!typebot) return notFound(res)
const webhook = await prisma.webhook.create({ const webhook = await prisma.webhook.create({
data: { typebotId, ...defaultWebhookAttributes }, data: { ...defaultWebhookAttributes, ...data, typebotId },
}) })
return res.send({ webhook }) return res.send({ webhook })
} }

View File

@@ -1,4 +1,5 @@
import prisma from '@/lib/prisma' import prisma from '@/lib/prisma'
import { Webhook } from 'models'
import { NextApiRequest, NextApiResponse } from 'next' import { NextApiRequest, NextApiResponse } from 'next'
import { getAuthenticatedUser } from '@/features/auth/api' import { getAuthenticatedUser } from '@/features/auth/api'
import { import {
@@ -6,48 +7,44 @@ import {
forbidden, forbidden,
methodNotAllowed, methodNotAllowed,
notAuthenticated, notAuthenticated,
notFound,
} from 'utils/api' } from 'utils/api'
import { getTypebot } from '@/features/typebot/api/utils/getTypebot' import { getTypebot } from '@/features/typebot/api/utils/getTypebot'
const handler = async (req: NextApiRequest, res: NextApiResponse) => { const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req) const user = await getAuthenticatedUser(req)
if (!user) return notAuthenticated(res) const typebotId = req.query.typebotId as string
const webhookId = req.query.webhookId as string const webhookId = req.query.webhookId as string
if (!user) return notAuthenticated(res)
if (req.method === 'GET') { if (req.method === 'GET') {
const typebot = getTypebot({
accessLevel: 'read',
typebotId,
user,
})
if (!typebot) return notFound(res)
const webhook = await prisma.webhook.findFirst({ const webhook = await prisma.webhook.findFirst({
where: { where: {
id: webhookId, id: webhookId,
typebot: { typebotId,
OR: [
{ workspace: { members: { some: { userId: user.id } } } },
{
collaborators: {
some: {
userId: user.id,
},
},
},
],
},
}, },
}) })
return res.send({ webhook }) return res.send({ webhook })
} }
if (req.method === 'PUT') { if (req.method === 'PATCH') {
const data = req.body const data = req.body.data as Partial<Webhook>
if (!('typebotId' in data)) return badRequest(res) if (!('typebotId' in data)) return badRequest(res)
const typebot = await getTypebot({ const typebot = await getTypebot({
accessLevel: 'write', accessLevel: 'write',
typebotId: data.typebotId, typebotId,
user, user,
}) })
if (!typebot) return forbidden(res) if (!typebot) return forbidden(res)
const webhook = await prisma.webhook.upsert({ const webhook = await prisma.webhook.update({
where: { where: {
id: webhookId, id: webhookId,
}, },
create: data, data,
update: data,
}) })
return res.send({ webhook }) return res.send({ webhook })
} }

View File

@@ -11,7 +11,7 @@
}, },
"dependencies": { "dependencies": {
"@chakra-ui/icon": "3.0.15", "@chakra-ui/icon": "3.0.15",
"@chakra-ui/react": "2.4.9", "@chakra-ui/react": "2.5.0",
"@emotion/react": "11.10.5", "@emotion/react": "11.10.5",
"@emotion/styled": "11.10.5", "@emotion/styled": "11.10.5",
"@vercel/analytics": "0.1.8", "@vercel/analytics": "0.1.8",
@@ -20,7 +20,7 @@
"aos": "2.3.4", "aos": "2.3.4",
"db": "workspace:*", "db": "workspace:*",
"focus-visible": "5.2.0", "focus-visible": "5.2.0",
"framer-motion": "8.5.4", "framer-motion": "9.0.2",
"models": "workspace:*", "models": "workspace:*",
"next": "13.1.6", "next": "13.1.6",
"react": "18.2.0", "react": "18.2.0",

606
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff