2
0

refactor(models): 🎨 Build types from validation schemas

This commit is contained in:
Baptiste Arnaud
2022-06-07 08:49:12 +02:00
parent c67fa7d9c1
commit e79ff09b0f
62 changed files with 1268 additions and 734 deletions

View File

@ -5,7 +5,7 @@ import {
useEffect,
useState,
} from 'react'
import { byId, isNotEmpty } from 'utils'
import { byId } from 'utils'
import { MemberInWorkspace, Plan, Workspace, WorkspaceRole } from 'db'
import {
createNewWorkspace,
@ -87,8 +87,11 @@ export const WorkspaceContext = ({ children }: { children: ReactNode }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [typebot?.workspaceId])
const switchWorkspace = (workspaceId: string) =>
setCurrentWorkspace(workspaces?.find(byId(workspaceId)))
const switchWorkspace = (workspaceId: string) => {
const newWorkspace = workspaces?.find(byId(workspaceId))
if (!newWorkspace) return
setCurrentWorkspace(newWorkspace)
}
const createWorkspace = async (name?: string) => {
if (!workspaces) return

View File

@ -70,11 +70,13 @@ export const ResultsContent = () => {
</HStack>
</Flex>
<Flex pt={['10px', '60px']} w="full" justify="center">
{publishedTypebot &&
{workspace &&
publishedTypebot &&
(isAnalytics ? (
<AnalyticsContent stats={stats} />
) : (
<SubmissionsContent
workspaceId={workspace.id}
typebotId={publishedTypebot.typebotId}
onDeleteResults={handleDeletedResults}
totalResults={stats?.totalStarts ?? 0}

View File

@ -18,12 +18,14 @@ import { Plan } from 'db'
import { useToast } from 'components/shared/hooks/useToast'
type Props = {
workspaceId: string
typebotId: string
totalResults: number
totalHiddenResults?: number
onDeleteResults: (total: number) => void
}
export const SubmissionsContent = ({
workspaceId,
typebotId,
totalResults,
totalHiddenResults,
@ -51,6 +53,7 @@ export const SubmissionsContent = ({
const resultHeader = parseResultHeader(blocksAndVariables)
const { data, mutate, setSize, hasMore } = useResults({
workspaceId,
typebotId,
onError: (err) => showToast({ title: err.name, description: err.message }),
})
@ -125,7 +128,7 @@ export const SubmissionsContent = ({
const getAllTableData = async () => {
if (!publishedTypebot) return []
const results = await getAllResults(typebotId)
const results = await getAllResults(workspaceId, typebotId)
return convertResultsToTableData(results, resultHeader)
}

View File

@ -4,13 +4,19 @@ import { NextApiRequest, NextApiResponse } from 'next'
import { canReadTypebot, canWriteTypebot } from 'services/api/dbRules'
import { getAuthenticatedUser } from 'services/api/utils'
import { isFreePlan } from 'services/workspace'
import { forbidden, methodNotAllowed, notAuthenticated } from 'utils'
import {
badRequest,
forbidden,
methodNotAllowed,
notAuthenticated,
} from 'utils'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req)
if (!user) return notAuthenticated(res)
const workspaceId = req.query.workspaceId as string | undefined
if (req.method === 'GET') {
if (!workspaceId) return badRequest(res, 'workspaceId is required')
const workspace = await prisma.workspace.findFirst({
where: { id: workspaceId, members: { some: { userId: user.id } } },
select: { plan: true },

View File

@ -143,12 +143,13 @@ test.describe.parallel('Editor', () => {
await page.goto(`/typebots/${typebotId}/edit`)
await page.click('[data-testid="editable-icon"]')
await expect(page.locator('text="My awesome typebot"')).toBeVisible()
await page.fill('input[placeholder="Search..."]', 'love')
await page.click('text="😍"')
await page.click('text="My awesome typebot"')
await page.fill('input[value="My awesome typebot"]', 'My superb typebot')
await page.press('input[value="My superb typebot"]', 'Enter')
await page.goto(`/typebots`)
await page.click('[aria-label="Navigate back"]')
await expect(page.locator('text="😍"')).toBeVisible()
await expect(page.locator('text="My superb typebot"')).toBeVisible()
})

View File

@ -78,6 +78,7 @@ test.describe.parallel('Settings page', () => {
)
await page.goto(`/typebots/${typebotId}/settings`)
await page.click('button:has-text("Metadata")')
await page.waitForTimeout(1000)
// Fav icon
const favIconImg = page.locator('img >> nth=0')

View File

@ -2,7 +2,7 @@ import { ResultWithAnswers, VariableWithValue, ResultHeaderCell } from 'models'
import useSWRInfinite from 'swr/infinite'
import { stringify } from 'qs'
import { Answer } from 'db'
import { isDefined, sendRequest } from 'utils'
import { isDefined, isEmpty, sendRequest } from 'utils'
import { fetcher } from 'services/utils'
import { HStack, Text } from '@chakra-ui/react'
import { CodeIcon, CalendarIcon } from 'assets/icons'
@ -11,6 +11,7 @@ import { StepIcon } from 'components/editor/StepsSideBar/StepIcon'
const paginationLimit = 50
const getKey = (
workspaceId: string,
typebotId: string,
pageIndex: number,
previousPageData: {
@ -18,16 +19,19 @@ const getKey = (
}
) => {
if (previousPageData && previousPageData.results.length === 0) return null
if (pageIndex === 0) return `/api/typebots/${typebotId}/results?limit=50`
if (pageIndex === 0)
return `/api/typebots/${typebotId}/results?limit=50&workspaceId=${workspaceId}`
return `/api/typebots/${typebotId}/results?lastResultId=${
previousPageData.results[previousPageData.results.length - 1].id
}&limit=${paginationLimit}`
}&limit=${paginationLimit}&workspaceId=${workspaceId}`
}
export const useResults = ({
workspaceId,
typebotId,
onError,
}: {
workspaceId: string
typebotId: string
onError: (error: Error) => void
}) => {
@ -40,9 +44,14 @@ export const useResults = ({
previousPageData: {
results: ResultWithAnswers[]
}
) => getKey(typebotId, pageIndex, previousPageData),
) => getKey(workspaceId, typebotId, pageIndex, previousPageData),
fetcher,
{ revalidateAll: true }
{
revalidateAll: true,
dedupingInterval: isEmpty(process.env.NEXT_PUBLIC_E2E_TEST)
? undefined
: 0,
}
)
if (error) onError(error)
@ -80,12 +89,12 @@ export const deleteAllResults = async (typebotId: string) =>
method: 'DELETE',
})
export const getAllResults = async (typebotId: string) => {
export const getAllResults = async (workspaceId: string, typebotId: string) => {
const results = []
let hasMore = true
let lastResultId: string | undefined = undefined
do {
const query = stringify({ limit: 200, lastResultId })
const query = stringify({ limit: 200, lastResultId, workspaceId })
const { data } = await sendRequest<{ results: ResultWithAnswers[] }>({
url: `/api/typebots/${typebotId}/results?${query}`,
method: 'GET',

View File

@ -45,7 +45,10 @@
"typescript": "^4.6.4"
},
"peerDependencies": {
"react": "^18.1.0"
"react": "^18.1.0",
"utils": "*",
"db": "*",
"models": "*"
},
"scripts": {
"build": "yarn rollup -c",

View File

@ -0,0 +1,20 @@
/*
Warnings:
- Made the column `workspaceId` on table `Credentials` required. This step will fail if there are existing NULL values in that column.
- Made the column `workspaceId` on table `CustomDomain` required. This step will fail if there are existing NULL values in that column.
- Made the column `workspaceId` on table `DashboardFolder` required. This step will fail if there are existing NULL values in that column.
- Made the column `workspaceId` on table `Typebot` required. This step will fail if there are existing NULL values in that column.
*/
-- AlterTable
ALTER TABLE "Credentials" ALTER COLUMN "workspaceId" SET NOT NULL;
-- AlterTable
ALTER TABLE "CustomDomain" ALTER COLUMN "workspaceId" SET NOT NULL;
-- AlterTable
ALTER TABLE "DashboardFolder" ALTER COLUMN "workspaceId" SET NOT NULL;
-- AlterTable
ALTER TABLE "Typebot" ALTER COLUMN "workspaceId" SET NOT NULL;

View File

@ -114,15 +114,15 @@ enum GraphNavigation {
model CustomDomain {
name String @id
createdAt DateTime @default(now())
workspaceId String?
workspace Workspace? @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
workspaceId String
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
}
model Credentials {
id String @id @default(cuid())
createdAt DateTime @default(now())
workspaceId String?
workspace Workspace? @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
workspaceId String
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
data String // Encrypted data
name String
type String
@ -154,8 +154,8 @@ model DashboardFolder {
parentFolder DashboardFolder? @relation("ParentChild", fields: [parentFolderId], references: [id])
childrenFolder DashboardFolder[] @relation("ParentChild")
typebots Typebot[]
workspaceId String?
workspace Workspace? @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
workspaceId String
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
}
model Typebot {
@ -179,8 +179,8 @@ model Typebot {
collaborators CollaboratorsOnTypebots[]
invitations Invitation[]
webhooks Webhook[]
workspaceId String?
workspace Workspace? @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
workspaceId String
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
}
model Invitation {

View File

@ -1,20 +1,31 @@
{
"name": "models",
"version": "1.0.0",
"main": "dist/index.js",
"types": "dist/types/index.d.ts",
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
"types": "dist/index.d.ts",
"license": "AGPL-3.0-or-later",
"private": true,
"devDependencies": {
"typescript": "^4.6.4"
"typescript": "^4.6.4",
"@rollup/plugin-commonjs": "^22.0.0",
"@rollup/plugin-node-resolve": "^13.3.0",
"@rollup/plugin-typescript": "^8.3.2",
"rollup": "^2.72.1",
"rollup-plugin-dts": "^4.2.1",
"rollup-plugin-peer-deps-external": "^2.2.4"
},
"dependencies": {
"next": "^12.1.6",
"db": "*",
"@udecode/plate-core": "^11.0.0"
"next": "^12.1.6",
"zod": "^3.17.3"
},
"peerDependencies": {
"next": "^12.1.6",
"db": "*"
},
"scripts": {
"build": "tsc",
"dx": "tsc --watch --preserveWatchOutput"
"build": "yarn rollup -c",
"dx": "yarn rollup -c --watch"
}
}

View File

@ -0,0 +1,38 @@
import resolve from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
import typescript from '@rollup/plugin-typescript'
import dts from 'rollup-plugin-dts'
import peerDepsExternal from 'rollup-plugin-peer-deps-external'
const packageJson = require('./package.json')
export default [
{
input: 'src/index.ts',
output: [
{
file: packageJson.main,
format: 'cjs',
sourcemap: true,
inlineDynamicImports: true,
},
{
file: packageJson.module,
format: 'esm',
sourcemap: true,
inlineDynamicImports: true,
},
],
plugins: [
peerDepsExternal(),
resolve(),
commonjs(),
typescript({ tsconfig: './tsconfig.json' }),
],
},
{
input: 'dist/esm/types/index.d.ts',
output: [{ file: 'dist/index.d.ts', format: 'esm' }],
plugins: [dts()],
},
]

View File

@ -1,5 +1,6 @@
import { Result as ResultFromPrisma } from 'db'
import { Answer, InputStepType, VariableWithValue } from '.'
import { Answer, VariableWithValue } from '.'
import { InputStepType } from './typebot/steps/shared'
export type Result = Omit<ResultFromPrisma, 'createdAt' | 'variables'> & {
createdAt: string

View File

@ -1,29 +1,32 @@
export type Settings = {
general: GeneralSettings
typingEmulation: TypingEmulation
metadata: Metadata
}
import { z } from 'zod'
export type GeneralSettings = {
isBrandingEnabled: boolean
isNewResultOnRefreshEnabled?: boolean
isInputPrefillEnabled?: boolean
isHideQueryParamsEnabled?: boolean
}
const generalSettings = z.object({
isBrandingEnabled: z.boolean(),
isTypingEmulationEnabled: z.boolean().optional(),
isInputPrefillEnabled: z.boolean().optional(),
isHideQueryParamsEnabled: z.boolean().optional(),
isNewResultOnRefreshEnabled: z.boolean().optional(),
})
export type TypingEmulation = {
enabled: boolean
speed: number
maxDelay: number
}
const typingEmulation = z.object({
enabled: z.boolean(),
speed: z.number(),
maxDelay: z.number(),
})
export type Metadata = {
title?: string
description: string
imageUrl?: string
favIconUrl?: string
customHeadCode?: string
}
const metadataSchema = z.object({
title: z.string().optional(),
description: z.string().optional(),
imageUrl: z.string().optional(),
favIconUrl: z.string().optional(),
customHeadCode: z.string().optional(),
})
export const settingsSchema = z.object({
general: generalSettings,
typingEmulation: typingEmulation,
metadata: metadataSchema,
})
export const defaultSettings: Settings = {
general: {
@ -38,3 +41,8 @@ export const defaultSettings: Settings = {
'Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form.',
},
}
export type Settings = z.infer<typeof settingsSchema>
export type GeneralSettings = z.infer<typeof generalSettings>
export type TypingEmulation = z.infer<typeof typingEmulation>
export type Metadata = z.infer<typeof metadataSchema>

View File

@ -1,80 +0,0 @@
import { StepBase } from '.'
import { TElement } from '@udecode/plate-core'
export type BubbleStep =
| TextBubbleStep
| ImageBubbleStep
| VideoBubbleStep
| EmbedBubbleStep
export enum BubbleStepType {
TEXT = 'text',
IMAGE = 'image',
VIDEO = 'video',
EMBED = 'embed',
}
export type BubbleStepContent =
| TextBubbleContent
| ImageBubbleContent
| VideoBubbleContent
| EmbedBubbleContent
export type TextBubbleStep = StepBase & {
type: BubbleStepType.TEXT
content: TextBubbleContent
}
export type ImageBubbleStep = StepBase & {
type: BubbleStepType.IMAGE
content: ImageBubbleContent
}
export type VideoBubbleStep = StepBase & {
type: BubbleStepType.VIDEO
content: VideoBubbleContent
}
export type EmbedBubbleStep = StepBase & {
type: BubbleStepType.EMBED
content: EmbedBubbleContent
}
export type TextBubbleContent = {
html: string
richText: TElement[]
plainText: string
}
export type ImageBubbleContent = {
url?: string
}
export type EmbedBubbleContent = {
url?: string
height: number
}
export enum VideoBubbleContentType {
URL = 'url',
YOUTUBE = 'youtube',
VIMEO = 'vimeo',
}
export type VideoBubbleContent = {
type?: VideoBubbleContentType
url?: string
id?: string
}
export const defaultTextBubbleContent: TextBubbleContent = {
html: '',
richText: [],
plainText: '',
}
export const defaultImageBubbleContent: ImageBubbleContent = {}
export const defaultVideoBubbleContent: VideoBubbleContent = {}
export const defaultEmbedBubbleContent: EmbedBubbleContent = { height: 400 }

View File

@ -0,0 +1,18 @@
import { z } from 'zod'
import { embedBubbleContentSchema, embedBubbleStepSchema } from './embed'
import { imageBubbleContentSchema, imageBubbleStepSchema } from './image'
import { textBubbleContentSchema, textBubbleStepSchema } from './text'
import { videoBubbleContentSchema, videoBubbleStepSchema } from './video'
export const bubbleStepContentSchema = textBubbleContentSchema
.or(imageBubbleContentSchema)
.or(videoBubbleContentSchema)
.or(embedBubbleContentSchema)
export const bubbleStepSchema = textBubbleStepSchema
.or(imageBubbleStepSchema)
.or(videoBubbleStepSchema)
.or(embedBubbleStepSchema)
export type BubbleStep = z.infer<typeof bubbleStepSchema>
export type BubbleStepContent = z.infer<typeof bubbleStepContentSchema>

View File

@ -0,0 +1,19 @@
import { stepBaseSchema, BubbleStepType } from '../shared'
import { z } from 'zod'
export const embedBubbleContentSchema = z.object({
url: z.string().optional(),
height: z.number(),
})
export const embedBubbleStepSchema = stepBaseSchema.and(
z.object({
type: z.enum([BubbleStepType.EMBED]),
content: embedBubbleContentSchema,
})
)
export const defaultEmbedBubbleContent: EmbedBubbleContent = { height: 400 }
export type EmbedBubbleStep = z.infer<typeof embedBubbleStepSchema>
export type EmbedBubbleContent = z.infer<typeof embedBubbleContentSchema>

View File

@ -0,0 +1,18 @@
import { z } from 'zod'
import { stepBaseSchema, BubbleStepType } from '../shared'
export const imageBubbleContentSchema = z.object({
url: z.string().optional(),
})
export const imageBubbleStepSchema = stepBaseSchema.and(
z.object({
type: z.enum([BubbleStepType.IMAGE]),
content: imageBubbleContentSchema,
})
)
export const defaultImageBubbleContent: ImageBubbleContent = {}
export type ImageBubbleStep = z.infer<typeof imageBubbleStepSchema>
export type ImageBubbleContent = z.infer<typeof imageBubbleContentSchema>

View File

@ -0,0 +1,5 @@
export * from './bubble'
export * from './text'
export * from './image'
export * from './video'
export * from './embed'

View File

@ -0,0 +1,25 @@
import { stepBaseSchema, BubbleStepType } from '../shared'
import { z } from 'zod'
export const defaultTextBubbleContent: TextBubbleContent = {
html: '',
richText: [],
plainText: '',
}
export const textBubbleContentSchema = z.object({
html: z.string(),
richText: z.array(z.any()),
plainText: z.string(),
})
export type TextBubbleContent = z.infer<typeof textBubbleContentSchema>
export const textBubbleStepSchema = stepBaseSchema.and(
z.object({
type: z.enum([BubbleStepType.TEXT]),
content: textBubbleContentSchema,
})
)
export type TextBubbleStep = z.infer<typeof textBubbleStepSchema>

View File

@ -0,0 +1,26 @@
import { stepBaseSchema, BubbleStepType } from '../shared'
import { z } from 'zod'
export enum VideoBubbleContentType {
URL = 'url',
YOUTUBE = 'youtube',
VIMEO = 'vimeo',
}
export const videoBubbleContentSchema = z.object({
url: z.string().optional(),
id: z.string().optional(),
type: z.nativeEnum(VideoBubbleContentType).optional(),
})
export const videoBubbleStepSchema = stepBaseSchema.and(
z.object({
type: z.enum([BubbleStepType.VIDEO]),
content: videoBubbleContentSchema,
})
)
export const defaultVideoBubbleContent: VideoBubbleContent = {}
export type VideoBubbleStep = z.infer<typeof videoBubbleStepSchema>
export type VideoBubbleContent = z.infer<typeof videoBubbleContentSchema>

View File

@ -1,6 +1,7 @@
export * from './steps'
export * from './bubble'
export * from './inputs'
export * from './input'
export * from './logic'
export * from './integration'
export * from './item'
export * from './shared'

View File

@ -0,0 +1,40 @@
import { z } from 'zod'
import {
stepBaseSchema,
InputStepType,
defaultButtonLabel,
optionBaseSchema,
itemBaseSchema,
ItemType,
} from '../shared'
export const choiceInputOptionsSchema = optionBaseSchema.and(
z.object({
isMultipleChoice: z.boolean(),
buttonLabel: z.string(),
})
)
export const defaultChoiceInputOptions: ChoiceInputOptions = {
buttonLabel: defaultButtonLabel,
isMultipleChoice: false,
}
export const choiceInputSchema = stepBaseSchema.and(
z.object({
type: z.enum([InputStepType.CHOICE]),
items: z.array(z.any()),
options: choiceInputOptionsSchema,
})
)
export const buttonItemSchema = itemBaseSchema.and(
z.object({
type: z.literal(ItemType.BUTTON),
content: z.string().optional(),
})
)
export type ButtonItem = z.infer<typeof buttonItemSchema>
export type ChoiceInputStep = z.infer<typeof choiceInputSchema>
export type ChoiceInputOptions = z.infer<typeof choiceInputOptionsSchema>

View File

@ -0,0 +1,35 @@
import { z } from 'zod'
import {
stepBaseSchema,
InputStepType,
defaultButtonLabel,
optionBaseSchema,
} from '../shared'
export const dateInputOptionsSchema = optionBaseSchema.and(
z.object({
labels: z.object({
button: z.string(),
from: z.string(),
to: z.string(),
}),
hasTime: z.boolean(),
isRange: z.boolean(),
})
)
export const dateInputSchema = stepBaseSchema.and(
z.object({
type: z.enum([InputStepType.DATE]),
options: dateInputOptionsSchema,
})
)
export const defaultDateInputOptions: DateInputOptions = {
hasTime: false,
isRange: false,
labels: { button: defaultButtonLabel, from: 'From:', to: 'To:' },
}
export type DateInputStep = z.infer<typeof dateInputSchema>
export type DateInputOptions = z.infer<typeof dateInputOptionsSchema>

View File

@ -0,0 +1,35 @@
import { z } from 'zod'
import {
defaultButtonLabel,
InputStepType,
optionBaseSchema,
stepBaseSchema,
} from '../shared'
import { textInputOptionsBaseSchema } from './text'
export const emailInputOptionsSchema = optionBaseSchema
.and(textInputOptionsBaseSchema)
.and(
z.object({
retryMessageContent: z.string(),
})
)
export const emailInputSchema = stepBaseSchema.and(
z.object({
type: z.enum([InputStepType.EMAIL]),
options: emailInputOptionsSchema,
})
)
export const defaultEmailInputOptions: EmailInputOptions = {
labels: {
button: defaultButtonLabel,
placeholder: 'Type your email...',
},
retryMessageContent:
"This email doesn't seem to be valid. Can you type it again?",
}
export type EmailInputStep = z.infer<typeof emailInputSchema>
export type EmailInputOptions = z.infer<typeof emailInputOptionsSchema>

View File

@ -0,0 +1,9 @@
export * from './input'
export * from './text'
export * from './email'
export * from './number'
export * from './url'
export * from './date'
export * from './choice'
export * from './payment'
export * from './phone'

View File

@ -0,0 +1,36 @@
import { z } from 'zod'
import { optionBaseSchema } from '../shared'
import { choiceInputOptionsSchema, choiceInputSchema } from './choice'
import { dateInputOptionsSchema, dateInputSchema } from './date'
import { emailInputOptionsSchema, emailInputSchema } from './email'
import { numberInputOptionsSchema, numberInputSchema } from './number'
import { paymentInputOptionsSchema, paymentInputSchema } from './payment'
import {
phoneNumberInputOptionsSchema,
phoneNumberInputStepSchema,
} from './phone'
import { textInputOptionsSchema, textInputSchema } from './text'
import { urlInputOptionsSchema, urlInputSchema } from './url'
export type OptionBase = z.infer<typeof optionBaseSchema>
export const inputStepOptionsSchema = textInputOptionsSchema
.or(choiceInputOptionsSchema)
.or(emailInputOptionsSchema)
.or(numberInputOptionsSchema)
.or(urlInputOptionsSchema)
.or(phoneNumberInputOptionsSchema)
.or(dateInputOptionsSchema)
.or(paymentInputOptionsSchema)
export const inputStepSchema = textInputSchema
.or(numberInputSchema)
.or(emailInputSchema)
.or(urlInputSchema)
.or(dateInputSchema)
.or(phoneNumberInputStepSchema)
.or(choiceInputSchema)
.or(paymentInputSchema)
export type InputStep = z.infer<typeof inputStepSchema>
export type InputStepOptions = z.infer<typeof inputStepOptionsSchema>

View File

@ -0,0 +1,32 @@
import { z } from 'zod'
import {
defaultButtonLabel,
InputStepType,
optionBaseSchema,
stepBaseSchema,
} from '../shared'
import { textInputOptionsBaseSchema } from './text'
export const numberInputOptionsSchema = optionBaseSchema
.and(textInputOptionsBaseSchema)
.and(
z.object({
min: z.number().optional(),
max: z.number().optional(),
step: z.number().optional(),
})
)
export const numberInputSchema = stepBaseSchema.and(
z.object({
type: z.enum([InputStepType.NUMBER]),
options: numberInputOptionsSchema,
})
)
export const defaultNumberInputOptions: NumberInputOptions = {
labels: { button: defaultButtonLabel, placeholder: 'Type a number...' },
}
export type NumberInputStep = z.infer<typeof numberInputSchema>
export type NumberInputOptions = z.infer<typeof numberInputOptionsSchema>

View File

@ -0,0 +1,49 @@
import { z } from 'zod'
import { InputStepType, optionBaseSchema, stepBaseSchema } from '../shared'
export type CreditCardDetails = {
number: string
exp_month: string
exp_year: string
cvc: string
}
export enum PaymentProvider {
STRIPE = 'Stripe',
}
export const paymentInputOptionsSchema = optionBaseSchema.and(
z.object({
provider: z.nativeEnum(PaymentProvider),
labels: z.object({
button: z.string(),
success: z.string().optional(),
}),
additionalInformation: z
.object({
name: z.string().optional(),
email: z.string().optional(),
phoneNumber: z.string().optional(),
})
.optional(),
credentialsId: z.string().optional(),
currency: z.string(),
amount: z.string().optional(),
})
)
export const paymentInputSchema = stepBaseSchema.and(
z.object({
type: z.enum([InputStepType.PAYMENT]),
options: paymentInputOptionsSchema,
})
)
export const defaultPaymentInputOptions: PaymentInputOptions = {
provider: PaymentProvider.STRIPE,
labels: { button: 'Pay', success: 'Success' },
currency: 'USD',
}
export type PaymentInputStep = z.infer<typeof paymentInputSchema>
export type PaymentInputOptions = z.infer<typeof paymentInputOptionsSchema>

View File

@ -0,0 +1,38 @@
import { z } from 'zod'
import {
defaultButtonLabel,
InputStepType,
optionBaseSchema,
stepBaseSchema,
} from '../shared'
import { textInputOptionsBaseSchema } from './text'
export const phoneNumberInputOptionsSchema = optionBaseSchema
.and(textInputOptionsBaseSchema)
.and(
z.object({
retryMessageContent: z.string(),
defaultCountryCode: z.string().optional(),
})
)
export const phoneNumberInputStepSchema = stepBaseSchema.and(
z.object({
type: z.enum([InputStepType.PHONE]),
options: phoneNumberInputOptionsSchema,
})
)
export const defaultPhoneInputOptions: PhoneNumberInputOptions = {
labels: {
button: defaultButtonLabel,
placeholder: 'Type your phone number...',
},
retryMessageContent:
"This phone number doesn't seem to be valid. Can you type it again?",
}
export type PhoneNumberInputStep = z.infer<typeof phoneNumberInputStepSchema>
export type PhoneNumberInputOptions = z.infer<
typeof phoneNumberInputOptionsSchema
>

View File

@ -0,0 +1,37 @@
import { z } from 'zod'
import {
defaultButtonLabel,
InputStepType,
optionBaseSchema,
stepBaseSchema,
} from '../shared'
export const textInputOptionsBaseSchema = z.object({
labels: z.object({
placeholder: z.string(),
button: z.string(),
}),
})
export const textInputOptionsSchema = textInputOptionsBaseSchema
.and(optionBaseSchema)
.and(
z.object({
isLong: z.boolean(),
})
)
export const defaultTextInputOptions: TextInputOptions = {
isLong: false,
labels: { button: defaultButtonLabel, placeholder: 'Type your answer...' },
}
export const textInputSchema = stepBaseSchema.and(
z.object({
type: z.enum([InputStepType.TEXT]),
options: textInputOptionsSchema,
})
)
export type TextInputStep = z.infer<typeof textInputSchema>
export type TextInputOptions = z.infer<typeof textInputOptionsSchema>

View File

@ -0,0 +1,35 @@
import { z } from 'zod'
import {
defaultButtonLabel,
InputStepType,
optionBaseSchema,
stepBaseSchema,
} from '../shared'
import { textInputOptionsBaseSchema } from './text'
export const urlInputOptionsSchema = optionBaseSchema
.and(textInputOptionsBaseSchema)
.and(
z.object({
retryMessageContent: z.string(),
})
)
export const urlInputSchema = stepBaseSchema.and(
z.object({
type: z.enum([InputStepType.URL]),
options: urlInputOptionsSchema,
})
)
export const defaultUrlInputOptions: UrlInputOptions = {
labels: {
button: defaultButtonLabel,
placeholder: 'Type a URL...',
},
retryMessageContent:
"This URL doesn't seem to be valid. Can you type it again?",
}
export type UrlInputStep = z.infer<typeof urlInputSchema>
export type UrlInputOptions = z.infer<typeof urlInputOptionsSchema>

View File

@ -1,203 +0,0 @@
import { ItemBase, ItemType } from '.'
import { StepBase } from './steps'
export type InputStep =
| TextInputStep
| NumberInputStep
| EmailInputStep
| UrlInputStep
| DateInputStep
| PhoneNumberInputStep
| ChoiceInputStep
| PaymentInputStep
export enum InputStepType {
TEXT = 'text input',
NUMBER = 'number input',
EMAIL = 'email input',
URL = 'url input',
DATE = 'date input',
PHONE = 'phone number input',
CHOICE = 'choice input',
PAYMENT = 'payment input',
}
export type InputStepOptions =
| TextInputOptions
| NumberInputOptions
| EmailInputOptions
| DateInputOptions
| UrlInputOptions
| PhoneNumberInputOptions
| ChoiceInputOptions
| PaymentInputOptions
export type TextInputStep = StepBase & {
type: InputStepType.TEXT
options: TextInputOptions
}
export type NumberInputStep = StepBase & {
type: InputStepType.NUMBER
options: NumberInputOptions
}
export type EmailInputStep = StepBase & {
type: InputStepType.EMAIL
options: EmailInputOptions
}
export type UrlInputStep = StepBase & {
type: InputStepType.URL
options: UrlInputOptions
}
export type DateInputStep = StepBase & {
type: InputStepType.DATE
options: DateInputOptions
}
export type PhoneNumberInputStep = StepBase & {
type: InputStepType.PHONE
options: PhoneNumberInputOptions
}
export type ChoiceInputStep = StepBase & {
type: InputStepType.CHOICE
items: ButtonItem[]
options: ChoiceInputOptions
}
export type ButtonItem = ItemBase & {
type: ItemType.BUTTON
content?: string
}
export type PaymentInputStep = StepBase & {
type: InputStepType.PAYMENT
options: PaymentInputOptions
}
export type CreditCardDetails = {
number: string
exp_month: string
exp_year: string
cvc: string
}
type OptionBase = { variableId?: string }
type InputTextOptionsBase = {
labels: { placeholder: string; button: string }
}
export type ChoiceInputOptions = OptionBase & {
isMultipleChoice: boolean
buttonLabel: string
}
export type DateInputOptions = OptionBase & {
labels: { button: string; from: string; to: string }
hasTime: boolean
isRange: boolean
}
export type EmailInputOptions = OptionBase & {
labels: { placeholder: string; button: string }
retryMessageContent: string
}
export type UrlInputOptions = OptionBase & {
labels: { placeholder: string; button: string }
retryMessageContent: string
}
export type PhoneNumberInputOptions = OptionBase & {
labels: { placeholder: string; button: string }
retryMessageContent: string
defaultCountryCode?: string
}
export type TextInputOptions = OptionBase &
InputTextOptionsBase & {
isLong: boolean
}
export type NumberInputOptions = OptionBase &
InputTextOptionsBase & {
min?: number
max?: number
step?: number
}
export enum PaymentProvider {
STRIPE = 'Stripe',
}
export type PaymentInputOptions = OptionBase & {
provider: PaymentProvider
amount?: string
currency: string
credentialsId?: string
additionalInformation?: {
name?: string
email?: string
phoneNumber?: string
}
labels: { button: string; success?: string }
}
const defaultButtonLabel = 'Send'
export const defaultTextInputOptions: TextInputOptions = {
isLong: false,
labels: { button: defaultButtonLabel, placeholder: 'Type your answer...' },
}
export const defaultNumberInputOptions: NumberInputOptions = {
labels: { button: defaultButtonLabel, placeholder: 'Type a number...' },
}
export const defaultEmailInputOptions: EmailInputOptions = {
labels: {
button: defaultButtonLabel,
placeholder: 'Type your email...',
},
retryMessageContent:
"This email doesn't seem to be valid. Can you type it again?",
}
export const defaultUrlInputOptions: UrlInputOptions = {
labels: {
button: defaultButtonLabel,
placeholder: 'Type a URL...',
},
retryMessageContent:
"This URL doesn't seem to be valid. Can you type it again?",
}
export const defaultDateInputOptions: DateInputOptions = {
hasTime: false,
isRange: false,
labels: { button: defaultButtonLabel, from: 'From:', to: 'To:' },
}
export const defaultPhoneInputOptions: PhoneNumberInputOptions = {
labels: {
button: defaultButtonLabel,
placeholder: 'Type your phone number...',
},
retryMessageContent:
"This phone number doesn't seem to be valid. Can you type it again?",
}
export const defaultChoiceInputOptions: ChoiceInputOptions = {
buttonLabel: defaultButtonLabel,
isMultipleChoice: false,
}
export const defaultPaymentInputOptions: PaymentInputOptions = {
provider: PaymentProvider.STRIPE,
labels: { button: 'Pay', success: 'Success' },
currency: 'USD',
}

View File

@ -1,156 +0,0 @@
import { StepBase } from '.'
export type IntegrationStep =
| GoogleSheetsStep
| GoogleAnalyticsStep
| WebhookStep
| SendEmailStep
| ZapierStep
| MakeComStep
| PabblyConnectStep
export type IntegrationStepOptions =
| GoogleSheetsOptions
| GoogleAnalyticsOptions
| WebhookOptions
| SendEmailOptions
export enum IntegrationStepType {
GOOGLE_SHEETS = 'Google Sheets',
GOOGLE_ANALYTICS = 'Google Analytics',
WEBHOOK = 'Webhook',
EMAIL = 'Email',
ZAPIER = 'Zapier',
MAKE_COM = 'Make.com',
PABBLY_CONNECT = 'Pabbly',
}
export type GoogleSheetsStep = StepBase & {
type: IntegrationStepType.GOOGLE_SHEETS
options: GoogleSheetsOptions
}
export type GoogleAnalyticsStep = StepBase & {
type: IntegrationStepType.GOOGLE_ANALYTICS
options: GoogleAnalyticsOptions
}
export type WebhookStep = StepBase & {
type: IntegrationStepType.WEBHOOK
options: WebhookOptions
webhookId: string
}
export type ZapierStep = Omit<WebhookStep, 'type'> & {
type: IntegrationStepType.ZAPIER
}
export type MakeComStep = Omit<WebhookStep, 'type'> & {
type: IntegrationStepType.MAKE_COM
}
export type PabblyConnectStep = Omit<WebhookStep, 'type'> & {
type: IntegrationStepType.PABBLY_CONNECT
}
export type SendEmailStep = StepBase & {
type: IntegrationStepType.EMAIL
options: SendEmailOptions
}
export type SendEmailOptions = {
credentialsId: string | 'default'
recipients: string[]
replyTo?: string
cc?: string[]
bcc?: string[]
subject?: string
body?: string
}
export type GoogleAnalyticsOptions = {
trackingId?: string
category?: string
action?: string
label?: string
value?: number
}
export enum GoogleSheetsAction {
GET = 'Get data from sheet',
INSERT_ROW = 'Insert a row',
UPDATE_ROW = 'Update a row',
}
export type GoogleSheetsOptions =
| GoogleSheetsOptionsBase
| GoogleSheetsGetOptions
| GoogleSheetsInsertRowOptions
| GoogleSheetsUpdateRowOptions
export type GoogleSheetsOptionsBase = {
credentialsId?: string
spreadsheetId?: string
sheetId?: string
}
export type Cell = { id: string; column?: string; value?: string }
export type ExtractingCell = {
id: string
column?: string
variableId?: string
}
export type GoogleSheetsGetOptions = NonNullable<GoogleSheetsOptionsBase> & {
action: GoogleSheetsAction.GET
referenceCell?: Cell
cellsToExtract: ExtractingCell[]
}
export type GoogleSheetsInsertRowOptions =
NonNullable<GoogleSheetsOptionsBase> & {
action: GoogleSheetsAction.INSERT_ROW
cellsToInsert: Cell[]
}
export type GoogleSheetsUpdateRowOptions =
NonNullable<GoogleSheetsOptionsBase> & {
action: GoogleSheetsAction.UPDATE_ROW
referenceCell?: Cell
cellsToUpsert: Cell[]
}
export type ResponseVariableMapping = {
id: string
bodyPath?: string
variableId?: string
}
export type WebhookOptions = {
variablesForTest: VariableForTest[]
responseVariableMapping: ResponseVariableMapping[]
isAdvancedConfig?: boolean
isCustomBody?: boolean
}
export type VariableForTest = {
id: string
variableId?: string
value?: string
}
export const defaultGoogleSheetsOptions: GoogleSheetsOptions = {}
export const defaultGoogleAnalyticsOptions: GoogleAnalyticsOptions = {}
export const defaultWebhookOptions: Omit<WebhookOptions, 'webhookId'> = {
responseVariableMapping: [],
variablesForTest: [],
isAdvancedConfig: false,
isCustomBody: false,
}
export const defaultSendEmailOptions: SendEmailOptions = {
credentialsId: 'default',
recipients: [],
}

View File

@ -0,0 +1,24 @@
import { z } from 'zod'
import { IntegrationStepType, stepBaseSchema } from '../shared'
export const googleAnalyticsOptionsSchema = z.object({
trackingId: z.string().optional(),
category: z.string().optional(),
action: z.string().optional(),
label: z.string().optional(),
value: z.number().optional(),
})
export const googleAnalyticsStepSchema = stepBaseSchema.and(
z.object({
type: z.enum([IntegrationStepType.GOOGLE_ANALYTICS]),
options: googleAnalyticsOptionsSchema,
})
)
export const defaultGoogleAnalyticsOptions: GoogleAnalyticsOptions = {}
export type GoogleAnalyticsStep = z.infer<typeof googleAnalyticsStepSchema>
export type GoogleAnalyticsOptions = z.infer<
typeof googleAnalyticsOptionsSchema
>

View File

@ -0,0 +1,80 @@
import { z } from 'zod'
import { IntegrationStepType, stepBaseSchema } from '../shared'
export enum GoogleSheetsAction {
GET = 'Get data from sheet',
INSERT_ROW = 'Insert a row',
UPDATE_ROW = 'Update a row',
}
const cellSchema = z.object({
column: z.string().optional(),
value: z.string().optional(),
id: z.string(),
})
const extractingCellSchema = z.object({
column: z.string().optional(),
id: z.string(),
variableId: z.string().optional(),
})
const googleSheetsOptionsBaseSchema = z.object({
credentialsId: z.string().optional(),
sheetId: z.string().optional(),
spreadsheetId: z.string().optional(),
})
const googleSheetsGetOptionsSchema = googleSheetsOptionsBaseSchema.and(
z.object({
action: z.enum([GoogleSheetsAction.GET]),
referenceCell: cellSchema.optional(),
cellsToExtract: z.array(extractingCellSchema),
})
)
const googleSheetsInsertRowOptionsSchema = googleSheetsOptionsBaseSchema.and(
z.object({
action: z.enum([GoogleSheetsAction.INSERT_ROW]),
cellsToInsert: z.array(cellSchema),
})
)
const googleSheetsUpdateRowOptionsSchema = googleSheetsOptionsBaseSchema.and(
z.object({
action: z.enum([GoogleSheetsAction.UPDATE_ROW]),
cellsToUpsert: z.array(cellSchema),
referenceCell: cellSchema.optional(),
})
)
export const googleSheetsOptionsSchema = googleSheetsOptionsBaseSchema
.or(googleSheetsGetOptionsSchema)
.or(googleSheetsInsertRowOptionsSchema)
.or(googleSheetsUpdateRowOptionsSchema)
export const googleSheetsStepSchema = stepBaseSchema.and(
z.object({
type: z.enum([IntegrationStepType.GOOGLE_SHEETS]),
options: googleSheetsOptionsSchema,
})
)
export const defaultGoogleSheetsOptions: GoogleSheetsOptions = {}
export type GoogleSheetsStep = z.infer<typeof googleSheetsStepSchema>
export type GoogleSheetsOptions = z.infer<typeof googleSheetsOptionsSchema>
export type GoogleSheetsOptionsBase = z.infer<
typeof googleSheetsOptionsBaseSchema
>
export type GoogleSheetsGetOptions = z.infer<
typeof googleSheetsGetOptionsSchema
>
export type GoogleSheetsInsertRowOptions = z.infer<
typeof googleSheetsInsertRowOptionsSchema
>
export type GoogleSheetsUpdateRowOptions = z.infer<
typeof googleSheetsUpdateRowOptionsSchema
>
export type Cell = z.infer<typeof cellSchema>
export type ExtractingCell = z.infer<typeof extractingCellSchema>

View File

@ -0,0 +1,8 @@
export * from './integration'
export * from './webhook'
export * from './googleAnalytics'
export * from './pabblyConnect'
export * from './makeCom'
export * from './zapier'
export * from './sendEmail'
export * from './googleSheets'

View File

@ -0,0 +1,32 @@
import { z } from 'zod'
import {
googleAnalyticsOptionsSchema,
googleAnalyticsStepSchema,
} from './googleAnalytics'
import {
googleSheetsOptionsSchema,
googleSheetsStepSchema,
} from './googleSheets'
import { makeComStepSchema } from './makeCom'
import { pabblyConnectStepSchema } from './pabblyConnect'
import { sendEmailOptionsSchema, sendEmailStepSchema } from './sendEmail'
import { webhookOptionsSchema, webhookStepSchema } from './webhook'
import { zapierStepSchema } from './zapier'
const integrationStepOptionsSchema = googleSheetsOptionsSchema
.or(googleAnalyticsOptionsSchema)
.or(webhookOptionsSchema)
.or(sendEmailOptionsSchema)
export const integrationStepSchema = googleSheetsStepSchema
.or(googleAnalyticsStepSchema)
.or(webhookStepSchema)
.or(sendEmailStepSchema)
.or(zapierStepSchema)
.or(makeComStepSchema)
.or(pabblyConnectStepSchema)
export type IntegrationStep = z.infer<typeof integrationStepSchema>
export type IntegrationStepOptions = z.infer<
typeof integrationStepOptionsSchema
>

View File

@ -0,0 +1,13 @@
import { z } from 'zod'
import { IntegrationStepType, stepBaseSchema } from '../shared'
import { webhookOptionsSchema } from './webhook'
export const makeComStepSchema = stepBaseSchema.and(
z.object({
type: z.enum([IntegrationStepType.MAKE_COM]),
options: webhookOptionsSchema,
webhookId: z.string(),
})
)
export type MakeComStep = z.infer<typeof makeComStepSchema>

View File

@ -0,0 +1,13 @@
import { z } from 'zod'
import { IntegrationStepType, stepBaseSchema } from '../shared'
import { webhookOptionsSchema } from './webhook'
export const pabblyConnectStepSchema = stepBaseSchema.and(
z.object({
type: z.enum([IntegrationStepType.PABBLY_CONNECT]),
options: webhookOptionsSchema,
webhookId: z.string(),
})
)
export type PabblyConnectStep = z.infer<typeof pabblyConnectStepSchema>

View File

@ -0,0 +1,27 @@
import { z } from 'zod'
import { IntegrationStepType, stepBaseSchema } from '../shared'
export const sendEmailOptionsSchema = z.object({
credentialsId: z.string(),
recipients: z.array(z.string()),
subject: z.string().optional(),
body: z.string().optional(),
replyTo: z.string().optional(),
cc: z.array(z.string()).optional(),
bcc: z.array(z.string()).optional(),
})
export const sendEmailStepSchema = stepBaseSchema.and(
z.object({
type: z.enum([IntegrationStepType.EMAIL]),
options: sendEmailOptionsSchema,
})
)
export const defaultSendEmailOptions: SendEmailOptions = {
credentialsId: 'default',
recipients: [],
}
export type SendEmailStep = z.infer<typeof sendEmailStepSchema>
export type SendEmailOptions = z.infer<typeof sendEmailOptionsSchema>

View File

@ -0,0 +1,43 @@
import { z } from 'zod'
import { IntegrationStepType, stepBaseSchema } from '../shared'
const variableForTestSchema = z.object({
id: z.string(),
variableId: z.string().optional(),
value: z.string().optional(),
})
const responseVariableMappingSchema = z.object({
id: z.string(),
variableId: z.string().optional(),
bodyPath: z.string().optional(),
})
export const webhookOptionsSchema = z.object({
variablesForTest: z.array(variableForTestSchema),
responseVariableMapping: z.array(responseVariableMappingSchema),
isAdvancedConfig: z.boolean().optional(),
isCustomBody: z.boolean().optional(),
})
export const webhookStepSchema = stepBaseSchema.and(
z.object({
type: z.enum([IntegrationStepType.WEBHOOK]),
options: webhookOptionsSchema,
webhookId: z.string(),
})
)
export const defaultWebhookOptions: Omit<WebhookOptions, 'webhookId'> = {
responseVariableMapping: [],
variablesForTest: [],
isAdvancedConfig: false,
isCustomBody: false,
}
export type WebhookStep = z.infer<typeof webhookStepSchema>
export type WebhookOptions = z.infer<typeof webhookOptionsSchema>
export type ResponseVariableMapping = z.infer<
typeof responseVariableMappingSchema
>
export type VariableForTest = z.infer<typeof variableForTestSchema>

View File

@ -0,0 +1,13 @@
import { z } from 'zod'
import { IntegrationStepType, stepBaseSchema } from '../shared'
import { webhookOptionsSchema } from './webhook'
export const zapierStepSchema = stepBaseSchema.and(
z.object({
type: z.enum([IntegrationStepType.ZAPIER]),
options: webhookOptionsSchema,
webhookId: z.string(),
})
)
export type ZapierStep = z.infer<typeof zapierStepSchema>

View File

@ -1,20 +1,14 @@
import { ButtonItem, ConditionItem } from '.'
export type Item = ButtonItem | ConditionItem
export enum ItemType {
BUTTON,
CONDITION,
}
export type ItemBase = {
id: string
stepId: string
outgoingEdgeId?: string
}
import { z } from 'zod'
import { itemBaseSchema } from './shared'
import { buttonItemSchema } from './input'
import { conditionItemSchema } from './logic'
export type ItemIndices = {
blockIndex: number
stepIndex: number
itemIndex: number
}
const itemScema = buttonItemSchema.or(conditionItemSchema)
export type ItemBase = z.infer<typeof itemBaseSchema>
export type Item = z.infer<typeof itemScema>

View File

@ -1,113 +0,0 @@
import { ItemType, StepBase } from '.'
import { ItemBase } from './item'
export type LogicStep =
| SetVariableStep
| ConditionStep
| RedirectStep
| CodeStep
| TypebotLinkStep
export type LogicStepOptions =
| SetVariableOptions
| RedirectOptions
| CodeOptions
| TypebotLinkOptions
export enum LogicStepType {
SET_VARIABLE = 'Set variable',
CONDITION = 'Condition',
REDIRECT = 'Redirect',
CODE = 'Code',
TYPEBOT_LINK = 'Typebot link',
}
export type SetVariableStep = StepBase & {
type: LogicStepType.SET_VARIABLE
options: SetVariableOptions
}
export type ConditionStep = StepBase & {
type: LogicStepType.CONDITION
items: [ConditionItem]
}
export type ConditionItem = ItemBase & {
type: ItemType.CONDITION
content: ConditionContent
}
export type RedirectStep = StepBase & {
type: LogicStepType.REDIRECT
options: RedirectOptions
}
export type CodeStep = StepBase & {
type: LogicStepType.CODE
options: CodeOptions
}
export type TypebotLinkStep = StepBase & {
type: LogicStepType.TYPEBOT_LINK
options: TypebotLinkOptions
}
export enum LogicalOperator {
OR = 'OR',
AND = 'AND',
}
export enum ComparisonOperators {
EQUAL = 'Equal to',
NOT_EQUAL = 'Not equal',
CONTAINS = 'Contains',
GREATER = 'Greater than',
LESS = 'Less than',
IS_SET = 'Is set',
}
export type ConditionContent = {
comparisons: Comparison[]
logicalOperator: LogicalOperator
}
export type Comparison = {
id: string
variableId?: string
comparisonOperator?: ComparisonOperators
value?: string
}
export type SetVariableOptions = {
variableId?: string
expressionToEvaluate?: string
isCode?: boolean
}
export type RedirectOptions = {
url?: string
isNewTab: boolean
}
export type CodeOptions = {
name: string
content?: string
}
export type TypebotLinkOptions = {
typebotId?: string | 'current'
blockId?: string
}
export const defaultSetVariablesOptions: SetVariableOptions = {}
export const defaultConditionContent: ConditionContent = {
comparisons: [],
logicalOperator: LogicalOperator.AND,
}
export const defaultRedirectOptions: RedirectOptions = { isNewTab: false }
export const defaultCodeOptions: CodeOptions = { name: 'Code snippet' }
export const defaultTypebotLinkOptions: TypebotLinkOptions = {}

View File

@ -0,0 +1,19 @@
import { z } from 'zod'
import { LogicStepType, stepBaseSchema } from '../shared'
export const codeOptionsSchema = z.object({
name: z.string(),
content: z.string().optional(),
})
export const codeStepSchema = stepBaseSchema.and(
z.object({
type: z.enum([LogicStepType.CODE]),
options: codeOptionsSchema,
})
)
export const defaultCodeOptions: CodeOptions = { name: 'Code snippet' }
export type CodeStep = z.infer<typeof codeStepSchema>
export type CodeOptions = z.infer<typeof codeOptionsSchema>

View File

@ -0,0 +1,58 @@
import { z } from 'zod'
import {
itemBaseSchema,
ItemType,
LogicStepType,
stepBaseSchema,
} from '../shared'
export enum LogicalOperator {
OR = 'OR',
AND = 'AND',
}
export enum ComparisonOperators {
EQUAL = 'Equal to',
NOT_EQUAL = 'Not equal',
CONTAINS = 'Contains',
GREATER = 'Greater than',
LESS = 'Less than',
IS_SET = 'Is set',
}
const comparisonSchema = z.object({
id: z.string(),
variableId: z.string().optional(),
comparisonOperator: z.nativeEnum(ComparisonOperators).optional(),
value: z.string().optional(),
})
const conditionContentSchema = z.object({
logicalOperator: z.nativeEnum(LogicalOperator),
comparisons: z.array(comparisonSchema),
})
export const conditionItemSchema = itemBaseSchema.and(
z.object({
type: z.literal(ItemType.CONDITION),
content: conditionContentSchema,
})
)
export const conditionStepSchema = stepBaseSchema.and(
z.object({
type: z.enum([LogicStepType.CONDITION]),
items: z.array(conditionItemSchema),
options: z.object({}),
})
)
export const defaultConditionContent: ConditionContent = {
comparisons: [],
logicalOperator: LogicalOperator.AND,
}
export type ConditionItem = z.infer<typeof conditionItemSchema>
export type Comparison = z.infer<typeof comparisonSchema>
export type ConditionStep = z.infer<typeof conditionStepSchema>
export type ConditionContent = z.infer<typeof conditionContentSchema>

View File

@ -0,0 +1,6 @@
export * from './logic'
export * from './code'
export * from './condition'
export * from './redirect'
export * from './setVariable'
export * from './typebotLink'

View File

@ -0,0 +1,20 @@
import { z } from 'zod'
import { codeOptionsSchema, codeStepSchema } from './code'
import { conditionStepSchema } from './condition'
import { redirectOptionsSchema, redirectStepSchema } from './redirect'
import { setVariableOptionsSchema, setVariableStepSchema } from './setVariable'
import { typebotLinkOptionsSchema, typebotLinkStepSchema } from './typebotLink'
const logicStepOptionsSchema = codeOptionsSchema
.or(redirectOptionsSchema)
.or(setVariableOptionsSchema)
.or(typebotLinkOptionsSchema)
export const logicStepSchema = codeStepSchema
.or(conditionStepSchema)
.or(redirectStepSchema)
.or(typebotLinkStepSchema)
.or(setVariableStepSchema)
export type LogicStep = z.infer<typeof logicStepSchema>
export type LogicStepOptions = z.infer<typeof logicStepOptionsSchema>

View File

@ -0,0 +1,19 @@
import { z } from 'zod'
import { LogicStepType, stepBaseSchema } from '../shared'
export const redirectOptionsSchema = z.object({
url: z.string().optional(),
isNewTab: z.boolean(),
})
export const redirectStepSchema = stepBaseSchema.and(
z.object({
type: z.enum([LogicStepType.REDIRECT]),
options: redirectOptionsSchema,
})
)
export const defaultRedirectOptions: RedirectOptions = { isNewTab: false }
export type RedirectStep = z.infer<typeof redirectStepSchema>
export type RedirectOptions = z.infer<typeof redirectOptionsSchema>

View File

@ -0,0 +1,20 @@
import { z } from 'zod'
import { LogicStepType, stepBaseSchema } from '../shared'
export const setVariableOptionsSchema = z.object({
variableId: z.string().optional(),
expressionToEvaluate: z.string().optional(),
isCode: z.boolean().optional(),
})
export const setVariableStepSchema = stepBaseSchema.and(
z.object({
type: z.enum([LogicStepType.SET_VARIABLE]),
options: setVariableOptionsSchema,
})
)
export const defaultSetVariablesOptions: SetVariableOptions = {}
export type SetVariableStep = z.infer<typeof setVariableStepSchema>
export type SetVariableOptions = z.infer<typeof setVariableOptionsSchema>

View File

@ -0,0 +1,19 @@
import { z } from 'zod'
import { LogicStepType, stepBaseSchema } from '../shared'
export const typebotLinkOptionsSchema = z.object({
typebotId: z.string().optional(),
blockId: z.string().optional(),
})
export const typebotLinkStepSchema = stepBaseSchema.and(
z.object({
type: z.enum([LogicStepType.TYPEBOT_LINK]),
options: typebotLinkOptionsSchema,
})
)
export const defaultTypebotLinkOptions: TypebotLinkOptions = {}
export type TypebotLinkStep = z.infer<typeof typebotLinkStepSchema>
export type TypebotLinkOptions = z.infer<typeof typebotLinkOptionsSchema>

View File

@ -0,0 +1,60 @@
import { z } from 'zod'
export const stepBaseSchema = z.object({
id: z.string(),
blockId: z.string(),
outgoingEdgeId: z.string().optional(),
})
export const defaultButtonLabel = 'Send'
export const optionBaseSchema = z.object({
variableId: z.string().optional(),
})
export const itemBaseSchema = z.object({
id: z.string(),
stepId: z.string(),
outgoingEdgeId: z.string().optional(),
})
export enum ItemType {
BUTTON,
CONDITION,
}
export enum BubbleStepType {
TEXT = 'text',
IMAGE = 'image',
VIDEO = 'video',
EMBED = 'embed',
}
export enum InputStepType {
TEXT = 'text input',
NUMBER = 'number input',
EMAIL = 'email input',
URL = 'url input',
DATE = 'date input',
PHONE = 'phone number input',
CHOICE = 'choice input',
PAYMENT = 'payment input',
}
export enum LogicStepType {
SET_VARIABLE = 'Set variable',
CONDITION = 'Condition',
REDIRECT = 'Redirect',
CODE = 'Code',
TYPEBOT_LINK = 'Typebot link',
}
export enum IntegrationStepType {
GOOGLE_SHEETS = 'Google Sheets',
GOOGLE_ANALYTICS = 'Google Analytics',
WEBHOOK = 'Webhook',
EMAIL = 'Email',
ZAPIER = 'Zapier',
MAKE_COM = 'Make.com',
PABBLY_CONNECT = 'Pabbly',
}

View File

@ -1,21 +1,21 @@
import {
InputStepOptions,
IntegrationStepOptions,
IntegrationStepType,
Item,
LogicStepOptions,
} from '.'
import { BubbleStep, BubbleStepType } from './bubble'
import { InputStep, InputStepType } from './inputs'
import { IntegrationStep } from './integration'
import { ConditionStep, LogicStep, LogicStepType } from './logic'
export type Step =
| StartStep
| BubbleStep
| InputStep
| LogicStep
| IntegrationStep
import { BubbleStep, bubbleStepSchema } from './bubble'
import { InputStep, inputStepSchema } from './input'
import { IntegrationStep, integrationStepSchema } from './integration'
import { ConditionStep, LogicStep, logicStepSchema } from './logic'
import { z } from 'zod'
import {
BubbleStepType,
InputStepType,
IntegrationStepType,
LogicStepType,
stepBaseSchema,
} from './shared'
export type DraggableStep = BubbleStep | InputStep | LogicStep | IntegrationStep
@ -49,14 +49,26 @@ export type StepOptions =
export type StepWithItems = Omit<Step, 'items'> & { items: Item[] }
export type StepBase = { id: string; blockId: string; outgoingEdgeId?: string }
export type StepBase = z.infer<typeof stepBaseSchema>
export type StartStep = StepBase & {
type: 'start'
label: string
}
const startStepSchema = stepBaseSchema.and(
z.object({
type: z.literal('start'),
label: z.string(),
})
)
export type StartStep = z.infer<typeof startStepSchema>
export type StepIndices = {
blockIndex: number
stepIndex: number
}
export const stepSchema = startStepSchema
.or(bubbleStepSchema)
.or(inputStepSchema)
.or(logicStepSchema)
.or(integrationStepSchema)
export type Step = z.infer<typeof stepSchema>

View File

@ -1,36 +1,29 @@
export type Theme = {
general: GeneralTheme
chat: ChatTheme
customCss?: string
}
import { z } from 'zod'
export type GeneralTheme = {
font: string
background: Background
}
const avatarPropsSchema = z.object({
isEnabled: z.boolean(),
url: z.string().optional(),
})
export type AvatarProps = {
isEnabled: boolean
url?: string
}
const containerColorsSchema = z.object({
backgroundColor: z.string(),
color: z.string(),
})
export type ChatTheme = {
hostAvatar?: AvatarProps
guestAvatar?: AvatarProps
hostBubbles: ContainerColors
guestBubbles: ContainerColors
buttons: ContainerColors
inputs: InputColors
}
const inputColorsSchema = containerColorsSchema.and(
z.object({
placeholderColor: z.string(),
})
)
export type ContainerColors = {
backgroundColor: string
color: string
}
export type InputColors = ContainerColors & {
placeholderColor: string
}
const chatThemeSchema = z.object({
hostAvatar: avatarPropsSchema.optional(),
guestAvatar: avatarPropsSchema.optional(),
hostBubbles: containerColorsSchema,
guestBubbles: containerColorsSchema,
buttons: containerColorsSchema,
inputs: inputColorsSchema,
})
export enum BackgroundType {
COLOR = 'Color',
@ -38,10 +31,21 @@ export enum BackgroundType {
NONE = 'None',
}
export type Background = {
type: BackgroundType
content?: string
}
const backgroundSchema = z.object({
type: z.nativeEnum(BackgroundType),
content: z.string().optional(),
})
const generalThemeSchema = z.object({
font: z.string(),
background: backgroundSchema,
})
export const themeSchema = z.object({
general: generalThemeSchema,
chat: chatThemeSchema,
customCss: z.string().optional(),
})
export const defaultTheme: Theme = {
chat: {
@ -56,3 +60,11 @@ export const defaultTheme: Theme = {
},
general: { font: 'Open Sans', background: { type: BackgroundType.NONE } },
}
export type Theme = z.infer<typeof themeSchema>
export type ChatTheme = z.infer<typeof chatThemeSchema>
export type AvatarProps = z.infer<typeof avatarPropsSchema>
export type GeneralTheme = z.infer<typeof generalThemeSchema>
export type Background = z.infer<typeof backgroundSchema>
export type ContainerColors = z.infer<typeof containerColorsSchema>
export type InputColors = z.infer<typeof inputColorsSchema>

View File

@ -1,46 +1,56 @@
import { Typebot as TypebotFromPrisma } from 'db'
import { Settings } from './settings'
import { Step } from './steps/steps'
import { Theme } from './theme'
import { Variable } from './variable'
import { z } from 'zod'
import { settingsSchema } from './settings'
import { stepSchema } from './steps'
import { themeSchema } from './theme'
import { variableSchema } from './variable'
export type Typebot = Omit<
TypebotFromPrisma,
| 'blocks'
| 'theme'
| 'settings'
| 'variables'
| 'edges'
| 'createdAt'
| 'updatedAt'
> & {
blocks: Block[]
variables: Variable[]
edges: Edge[]
theme: Theme
settings: Settings
createdAt: string
updatedAt: string
}
const blockSchema = z.object({
id: z.string(),
title: z.string(),
graphCoordinates: z.object({
x: z.number(),
y: z.number(),
}),
steps: z.array(stepSchema),
})
export type Block = {
id: string
title: string
graphCoordinates: {
x: number
y: number
}
steps: Step[]
}
const sourceSchema = z.object({
blockId: z.string(),
stepId: z.string(),
itemId: z.string().optional(),
})
export type Source = {
blockId: string
stepId: string
itemId?: string
}
export type Target = { blockId: string; stepId?: string }
export type Edge = {
id: string
from: Source
to: Target
}
const targetSchema = z.object({
blockId: z.string(),
stepId: z.string().optional(),
})
const edgeSchema = z.object({
id: z.string(),
from: sourceSchema,
to: targetSchema,
})
const typebotSchema = z.object({
id: z.string(),
name: z.string(),
blocks: z.array(blockSchema),
edges: z.array(edgeSchema),
variables: z.array(variableSchema),
theme: themeSchema,
settings: settingsSchema,
createdAt: z.string(),
updatedAt: z.string(),
icon: z.string().nullable(),
publishedTypebotId: z.string().nullable(),
folderId: z.string().nullable(),
publicId: z.string().nullable(),
customDomain: z.string().nullable(),
workspaceId: z.string(),
})
export type Typebot = z.infer<typeof typebotSchema>
export type Target = z.infer<typeof targetSchema>
export type Source = z.infer<typeof sourceSchema>
export type Edge = z.infer<typeof edgeSchema>
export type Block = z.infer<typeof blockSchema>

View File

@ -1,9 +1,12 @@
export type Variable = {
id: string
name: string
value?: string | number
}
import { z } from 'zod'
export const variableSchema = z.object({
id: z.string(),
name: z.string(),
value: z.string().or(z.number()).optional(),
})
export type VariableWithValue = Omit<Variable, 'value'> & {
value: string
}
export type Variable = z.infer<typeof variableSchema>

View File

@ -1,13 +1,17 @@
{
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"target": "es5",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"module": "ESNext",
"declaration": true,
"declarationDir": "./dist/types",
"outDir": "./dist"
"declarationDir": "types",
"sourceMap": true,
"outDir": "dist",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"emitDeclarationOnly": true
}
}

View File

@ -20,6 +20,10 @@
"models": "*",
"next": "^12.1.6"
},
"peerDependencies": {
"next": "^12.1.6",
"models": "*"
},
"scripts": {
"build": "yarn rollup -c",
"dx": "yarn rollup -c --watch"

View File

@ -25,7 +25,7 @@ export default [
],
plugins: [
peerDepsExternal(),
resolve({ preferBuiltins: true }),
resolve(),
commonjs(),
typescript({ tsconfig: './tsconfig.json' }),
],

View File

@ -1,19 +1,23 @@
import { NextApiRequest, NextApiResponse } from 'next'
export const methodNotAllowed = (res: NextApiResponse) =>
res.status(405).json({ message: 'Method Not Allowed' })
export const methodNotAllowed = (
res: NextApiResponse,
customMessage?: string
) => res.status(405).json({ message: customMessage ?? 'Method Not Allowed' })
export const notAuthenticated = (res: NextApiResponse) =>
res.status(401).json({ message: 'Not authenticated' })
export const notAuthenticated = (
res: NextApiResponse,
customMessage?: string
) => res.status(401).json({ message: customMessage ?? 'Not authenticated' })
export const notFound = (res: NextApiResponse) =>
res.status(404).json({ message: 'Not found' })
export const notFound = (res: NextApiResponse, customMessage?: string) =>
res.status(404).json({ message: customMessage ?? 'Not found' })
export const badRequest = (res: NextApiResponse) =>
res.status(400).json({ message: 'Bad Request' })
export const badRequest = (res: NextApiResponse, customMessage?: any) =>
res.status(400).json({ message: customMessage ?? 'Bad Request' })
export const forbidden = (res: NextApiResponse) =>
res.status(403).json({ message: 'Bad Request' })
export const forbidden = (res: NextApiResponse, customMessage?: string) =>
res.status(403).json({ message: customMessage ?? 'Bad Request' })
export const initMiddleware =
(

View File

@ -15406,6 +15406,11 @@ yocto-queue@^0.1.0:
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
zod@^3.17.3:
version "3.17.3"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.17.3.tgz#86abbc670ff0063a4588d85a4dcc917d6e4af2ba"
integrity sha512-4oKP5zvG6GGbMlqBkI5FESOAweldEhSOZ6LI6cG+JzUT7ofj1ZOC0PJudpQOpT1iqOFpYYtX5Pw0+o403y4bcg==
zustand@^3.4.2, zustand@^3.7.2:
version "3.7.2"
resolved "https://registry.yarnpkg.com/zustand/-/zustand-3.7.2.tgz#7b44c4f4a5bfd7a8296a3957b13e1c346f42514d"