♻️ (editor) Improve webhook creation
Remove terrible useEffects
This commit is contained in:
@@ -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 [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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}{' '}
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
})
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
|
||||||
})
|
|
||||||
@@ -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 },
|
||||||
|
})
|
||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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 })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 })
|
||||||
}
|
}
|
||||||
@@ -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
606
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user