✨ (editor) Add unpublish and close typebot options
Introducing more menu items on the "Publised" button in the editor. You can now unpublish a typebot and close it to new responses
This commit is contained in:
@ -24,7 +24,7 @@ import { LiteBadge } from './LiteBadge'
|
||||
import { getViewerUrl, isEmpty } from 'utils'
|
||||
|
||||
export type TypebotViewerProps = {
|
||||
typebot: PublicTypebot
|
||||
typebot: Omit<PublicTypebot, 'updatedAt' | 'createdAt'>
|
||||
isPreview?: boolean
|
||||
apiHost?: string
|
||||
style?: CSSProperties
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { TypebotViewerProps } from 'components/TypebotViewer'
|
||||
import { Log } from 'db'
|
||||
import { Edge, PublicTypebot, Typebot } from 'models'
|
||||
import React, {
|
||||
@ -20,7 +21,7 @@ export type LinkedTypebotQueue = {
|
||||
|
||||
const typebotContext = createContext<{
|
||||
currentTypebotId: string
|
||||
typebot: PublicTypebot
|
||||
typebot: TypebotViewerProps['typebot']
|
||||
linkedTypebots: LinkedTypebot[]
|
||||
apiHost: string
|
||||
isPreview: boolean
|
||||
@ -49,13 +50,14 @@ export const TypebotContext = ({
|
||||
onNewLog,
|
||||
}: {
|
||||
children: ReactNode
|
||||
typebot: PublicTypebot
|
||||
typebot: TypebotViewerProps['typebot']
|
||||
apiHost: string
|
||||
isLoading: boolean
|
||||
isPreview: boolean
|
||||
onNewLog: (log: Omit<Log, 'id' | 'createdAt' | 'resultId'>) => void
|
||||
}) => {
|
||||
const [localTypebot, setLocalTypebot] = useState<PublicTypebot>(typebot)
|
||||
const [localTypebot, setLocalTypebot] =
|
||||
useState<TypebotViewerProps['typebot']>(typebot)
|
||||
const [linkedTypebots, setLinkedTypebots] = useState<LinkedTypebot[]>([])
|
||||
const [currentTypebotId, setCurrentTypebotId] = useState(typebot.typebotId)
|
||||
const [linkedBotQueue, setLinkedBotQueue] = useState<LinkedTypebotQueue>([])
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { TypebotViewerProps } from 'components/TypebotViewer'
|
||||
import { LinkedTypebot } from 'contexts/TypebotContext'
|
||||
import { Log } from 'db'
|
||||
import {
|
||||
@ -26,7 +27,7 @@ type EdgeId = string
|
||||
type LogicContext = {
|
||||
isPreview: boolean
|
||||
apiHost: string
|
||||
typebot: PublicTypebot
|
||||
typebot: TypebotViewerProps['typebot']
|
||||
linkedTypebots: LinkedTypebot[]
|
||||
currentTypebotId: string
|
||||
pushEdgeIdInLinkedTypebotQueue: (bot: {
|
||||
@ -46,7 +47,7 @@ export const executeLogic = async (
|
||||
context: LogicContext
|
||||
): Promise<{
|
||||
nextEdgeId?: EdgeId
|
||||
linkedTypebot?: PublicTypebot | LinkedTypebot
|
||||
linkedTypebot?: TypebotViewerProps['typebot'] | LinkedTypebot
|
||||
}> => {
|
||||
switch (block.type) {
|
||||
case LogicBlockType.SET_VARIABLE:
|
||||
|
@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Typebot" ADD COLUMN "isClosed" BOOLEAN NOT NULL DEFAULT false;
|
@ -168,7 +168,8 @@ model Typebot {
|
||||
publishedTypebot PublicTypebot?
|
||||
results Result[]
|
||||
webhooks Webhook[]
|
||||
isArchived Boolean @default(false)
|
||||
isArchived Boolean @default(false)
|
||||
isClosed Boolean @default(false)
|
||||
}
|
||||
|
||||
model Invitation {
|
||||
|
@ -55,6 +55,8 @@ const typebotSchema = z.object({
|
||||
customDomain: z.string().nullable(),
|
||||
workspaceId: z.string(),
|
||||
resultsTablePreferences: resultsTablePreferencesSchema.optional(),
|
||||
isArchived: z.boolean(),
|
||||
isClosed: z.boolean(),
|
||||
})
|
||||
|
||||
export type Typebot = z.infer<typeof typebotSchema>
|
||||
|
@ -1,4 +1,3 @@
|
||||
export * from './utils'
|
||||
export * from './results'
|
||||
export * from './pricing'
|
||||
export * from './playwright'
|
||||
|
@ -6,6 +6,7 @@
|
||||
"main": "./index.ts",
|
||||
"types": "./index.ts",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.26.1",
|
||||
"@types/nodemailer": "6.4.6",
|
||||
"aws-sdk": "2.1227.0",
|
||||
"cuid": "2.1.8",
|
||||
|
@ -1,65 +0,0 @@
|
||||
import { PrismaClient } from 'db'
|
||||
import cuid from 'cuid'
|
||||
|
||||
type CreateFakeResultsProps = {
|
||||
typebotId: string
|
||||
count: number
|
||||
customResultIdPrefix?: string
|
||||
isChronological?: boolean
|
||||
fakeStorage?: number
|
||||
}
|
||||
|
||||
export const injectFakeResults =
|
||||
(prisma: PrismaClient) =>
|
||||
async ({
|
||||
count,
|
||||
customResultIdPrefix,
|
||||
typebotId,
|
||||
isChronological,
|
||||
fakeStorage,
|
||||
}: CreateFakeResultsProps) => {
|
||||
const resultIdPrefix = customResultIdPrefix ?? cuid()
|
||||
await prisma.result.createMany({
|
||||
data: [
|
||||
...Array.from(Array(count)).map((_, idx) => {
|
||||
const today = new Date()
|
||||
const rand = Math.random()
|
||||
return {
|
||||
id: `${resultIdPrefix}-result${idx}`,
|
||||
typebotId,
|
||||
createdAt: isChronological
|
||||
? new Date(
|
||||
today.setTime(today.getTime() + 1000 * 60 * 60 * 24 * idx)
|
||||
)
|
||||
: new Date(),
|
||||
isCompleted: rand > 0.5,
|
||||
hasStarted: true,
|
||||
}
|
||||
}),
|
||||
],
|
||||
})
|
||||
return createAnswers(prisma)({ fakeStorage, resultIdPrefix, count })
|
||||
}
|
||||
|
||||
const createAnswers =
|
||||
(prisma: PrismaClient) =>
|
||||
({
|
||||
count,
|
||||
resultIdPrefix,
|
||||
fakeStorage,
|
||||
}: { resultIdPrefix: string } & Pick<
|
||||
CreateFakeResultsProps,
|
||||
'fakeStorage' | 'count'
|
||||
>) => {
|
||||
return prisma.answer.createMany({
|
||||
data: [
|
||||
...Array.from(Array(count)).map((_, idx) => ({
|
||||
resultId: `${resultIdPrefix}-result${idx}`,
|
||||
content: `content${idx}`,
|
||||
blockId: 'block1',
|
||||
groupId: 'block1',
|
||||
storageUsed: fakeStorage ? Math.round(fakeStorage / count) : null,
|
||||
})),
|
||||
],
|
||||
})
|
||||
}
|
168
packages/utils/playwright/databaseActions.ts
Normal file
168
packages/utils/playwright/databaseActions.ts
Normal file
@ -0,0 +1,168 @@
|
||||
import { Plan, PrismaClient, User, Workspace, WorkspaceRole } from 'db'
|
||||
import cuid from 'cuid'
|
||||
import { Typebot, Webhook } from 'models'
|
||||
import { readFileSync } from 'fs'
|
||||
import { proWorkspaceId, userId } from './databaseSetup'
|
||||
import {
|
||||
parseTestTypebot,
|
||||
parseTypebotToPublicTypebot,
|
||||
} from './databaseHelpers'
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
type CreateFakeResultsProps = {
|
||||
typebotId: string
|
||||
count: number
|
||||
customResultIdPrefix?: string
|
||||
isChronological?: boolean
|
||||
fakeStorage?: number
|
||||
}
|
||||
|
||||
export const injectFakeResults = async ({
|
||||
count,
|
||||
customResultIdPrefix,
|
||||
typebotId,
|
||||
isChronological,
|
||||
fakeStorage,
|
||||
}: CreateFakeResultsProps) => {
|
||||
const resultIdPrefix = customResultIdPrefix ?? cuid()
|
||||
await prisma.result.createMany({
|
||||
data: [
|
||||
...Array.from(Array(count)).map((_, idx) => {
|
||||
const today = new Date()
|
||||
const rand = Math.random()
|
||||
return {
|
||||
id: `${resultIdPrefix}-result${idx}`,
|
||||
typebotId,
|
||||
createdAt: isChronological
|
||||
? new Date(
|
||||
today.setTime(today.getTime() + 1000 * 60 * 60 * 24 * idx)
|
||||
)
|
||||
: new Date(),
|
||||
isCompleted: rand > 0.5,
|
||||
hasStarted: true,
|
||||
}
|
||||
}),
|
||||
],
|
||||
})
|
||||
return createAnswers({ fakeStorage, resultIdPrefix, count })
|
||||
}
|
||||
|
||||
const createAnswers = ({
|
||||
count,
|
||||
resultIdPrefix,
|
||||
fakeStorage,
|
||||
}: { resultIdPrefix: string } & Pick<
|
||||
CreateFakeResultsProps,
|
||||
'fakeStorage' | 'count'
|
||||
>) => {
|
||||
return prisma.answer.createMany({
|
||||
data: [
|
||||
...Array.from(Array(count)).map((_, idx) => ({
|
||||
resultId: `${resultIdPrefix}-result${idx}`,
|
||||
content: `content${idx}`,
|
||||
blockId: 'block1',
|
||||
groupId: 'block1',
|
||||
storageUsed: fakeStorage ? Math.round(fakeStorage / count) : null,
|
||||
})),
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
export const importTypebotInDatabase = async (
|
||||
path: string,
|
||||
updates?: Partial<Typebot>
|
||||
) => {
|
||||
const typebot: Typebot = {
|
||||
...JSON.parse(readFileSync(path).toString()),
|
||||
workspaceId: proWorkspaceId,
|
||||
...updates,
|
||||
}
|
||||
await prisma.typebot.create({
|
||||
data: typebot,
|
||||
})
|
||||
return prisma.publicTypebot.create({
|
||||
data: parseTypebotToPublicTypebot(
|
||||
updates?.id ? `${updates?.id}-public` : 'publicBot',
|
||||
typebot
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
export const deleteWorkspaces = async (workspaceIds: string[]) => {
|
||||
await prisma.workspace.deleteMany({
|
||||
where: { id: { in: workspaceIds } },
|
||||
})
|
||||
}
|
||||
|
||||
export const createWorkspaces = async (workspaces: Partial<Workspace>[]) => {
|
||||
const workspaceIds = workspaces.map((workspace) => workspace.id ?? cuid())
|
||||
await prisma.workspace.createMany({
|
||||
data: workspaces.map((workspace, index) => ({
|
||||
id: workspaceIds[index],
|
||||
name: 'Free workspace',
|
||||
plan: Plan.FREE,
|
||||
...workspace,
|
||||
})),
|
||||
})
|
||||
await prisma.memberInWorkspace.createMany({
|
||||
data: workspaces.map((_, index) => ({
|
||||
userId,
|
||||
workspaceId: workspaceIds[index],
|
||||
role: WorkspaceRole.ADMIN,
|
||||
})),
|
||||
})
|
||||
return workspaceIds
|
||||
}
|
||||
|
||||
export const updateUser = (data: Partial<User>) =>
|
||||
prisma.user.update({
|
||||
data,
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
})
|
||||
|
||||
export const createWebhook = async (
|
||||
typebotId: string,
|
||||
webhookProps?: Partial<Webhook>
|
||||
) => {
|
||||
try {
|
||||
await prisma.webhook.delete({ where: { id: 'webhook1' } })
|
||||
} catch {}
|
||||
return prisma.webhook.create({
|
||||
data: { method: 'GET', typebotId, id: 'webhook1', ...webhookProps },
|
||||
})
|
||||
}
|
||||
|
||||
export const createTypebots = async (partialTypebots: Partial<Typebot>[]) => {
|
||||
const typebotsWithId = partialTypebots.map((typebot) => {
|
||||
const typebotId = typebot.id ?? cuid()
|
||||
return {
|
||||
...typebot,
|
||||
id: typebotId,
|
||||
publicId: typebotId + '-public',
|
||||
}
|
||||
})
|
||||
await prisma.typebot.createMany({
|
||||
data: typebotsWithId.map(parseTestTypebot),
|
||||
})
|
||||
return prisma.publicTypebot.createMany({
|
||||
data: typebotsWithId.map((t) =>
|
||||
parseTypebotToPublicTypebot(t.id + '-public', parseTestTypebot(t))
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
export const updateTypebot = async (
|
||||
partialTypebot: Partial<Typebot> & { id: string }
|
||||
) => {
|
||||
await prisma.typebot.updateMany({
|
||||
where: { id: partialTypebot.id },
|
||||
data: partialTypebot,
|
||||
})
|
||||
return prisma.publicTypebot.updateMany({
|
||||
where: { typebotId: partialTypebot.id },
|
||||
data: partialTypebot,
|
||||
})
|
||||
}
|
86
packages/utils/playwright/databaseHelpers.ts
Normal file
86
packages/utils/playwright/databaseHelpers.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import cuid from 'cuid'
|
||||
import {
|
||||
Block,
|
||||
defaultSettings,
|
||||
defaultTheme,
|
||||
PublicTypebot,
|
||||
Typebot,
|
||||
} from 'models'
|
||||
import { proWorkspaceId } from './databaseSetup'
|
||||
|
||||
export const parseTestTypebot = (
|
||||
partialTypebot: Partial<Typebot>
|
||||
): Typebot => ({
|
||||
id: cuid(),
|
||||
workspaceId: proWorkspaceId,
|
||||
folderId: null,
|
||||
name: 'My typebot',
|
||||
theme: defaultTheme,
|
||||
settings: defaultSettings,
|
||||
publicId: null,
|
||||
updatedAt: new Date().toISOString(),
|
||||
createdAt: new Date().toISOString(),
|
||||
publishedTypebotId: null,
|
||||
customDomain: null,
|
||||
icon: null,
|
||||
isArchived: false,
|
||||
isClosed: false,
|
||||
variables: [{ id: 'var1', name: 'var1' }],
|
||||
...partialTypebot,
|
||||
edges: [
|
||||
{
|
||||
id: 'edge1',
|
||||
from: { groupId: 'block0', blockId: 'block0' },
|
||||
to: { groupId: 'block1' },
|
||||
},
|
||||
],
|
||||
groups: [
|
||||
{
|
||||
id: 'block0',
|
||||
title: 'Group #0',
|
||||
blocks: [
|
||||
{
|
||||
id: 'block0',
|
||||
type: 'start',
|
||||
groupId: 'block0',
|
||||
label: 'Start',
|
||||
outgoingEdgeId: 'edge1',
|
||||
},
|
||||
],
|
||||
graphCoordinates: { x: 0, y: 0 },
|
||||
},
|
||||
...(partialTypebot.groups ?? []),
|
||||
],
|
||||
})
|
||||
|
||||
export const parseTypebotToPublicTypebot = (
|
||||
id: string,
|
||||
typebot: Typebot
|
||||
): Omit<PublicTypebot, 'createdAt' | 'updatedAt'> => ({
|
||||
id,
|
||||
groups: typebot.groups,
|
||||
typebotId: typebot.id,
|
||||
theme: typebot.theme,
|
||||
settings: typebot.settings,
|
||||
variables: typebot.variables,
|
||||
edges: typebot.edges,
|
||||
})
|
||||
|
||||
export const parseDefaultGroupWithBlock = (
|
||||
block: Partial<Block>
|
||||
): Pick<Typebot, 'groups'> => ({
|
||||
groups: [
|
||||
{
|
||||
graphCoordinates: { x: 200, y: 200 },
|
||||
id: 'block1',
|
||||
blocks: [
|
||||
{
|
||||
id: 'block1',
|
||||
groupId: 'block1',
|
||||
...block,
|
||||
} as Block,
|
||||
],
|
||||
title: 'Group #1',
|
||||
},
|
||||
],
|
||||
})
|
146
packages/utils/playwright/databaseSetup.ts
Normal file
146
packages/utils/playwright/databaseSetup.ts
Normal file
@ -0,0 +1,146 @@
|
||||
import { GraphNavigation, Plan, PrismaClient, WorkspaceRole } from 'db'
|
||||
import { CredentialsType } from 'models'
|
||||
import { encrypt } from '../api'
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
export const apiToken = 'jirowjgrwGREHE'
|
||||
|
||||
export const userId = 'userId'
|
||||
export const otherUserId = 'otherUserId'
|
||||
|
||||
export const proWorkspaceId = 'proWorkspace'
|
||||
export const freeWorkspaceId = 'freeWorkspace'
|
||||
export const starterWorkspaceId = 'starterWorkspace'
|
||||
export const lifetimeWorkspaceId = 'lifetimeWorkspaceId'
|
||||
|
||||
const setupWorkspaces = async () => {
|
||||
await prisma.workspace.create({
|
||||
data: {
|
||||
id: freeWorkspaceId,
|
||||
name: 'Free workspace',
|
||||
plan: Plan.FREE,
|
||||
},
|
||||
})
|
||||
await prisma.workspace.createMany({
|
||||
data: [
|
||||
{
|
||||
id: starterWorkspaceId,
|
||||
name: 'Starter workspace',
|
||||
stripeId: 'cus_LnPDugJfa18N41',
|
||||
plan: Plan.STARTER,
|
||||
},
|
||||
{
|
||||
id: proWorkspaceId,
|
||||
name: 'Pro workspace',
|
||||
plan: Plan.PRO,
|
||||
},
|
||||
{
|
||||
id: lifetimeWorkspaceId,
|
||||
name: 'Lifetime workspace',
|
||||
plan: Plan.LIFETIME,
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
export const setupUsers = async () => {
|
||||
await prisma.user.create({
|
||||
data: {
|
||||
id: userId,
|
||||
email: 'user@email.com',
|
||||
name: 'John Doe',
|
||||
graphNavigation: GraphNavigation.TRACKPAD,
|
||||
apiTokens: {
|
||||
createMany: {
|
||||
data: [
|
||||
{
|
||||
name: 'Token 1',
|
||||
token: apiToken,
|
||||
createdAt: new Date(2022, 1, 1),
|
||||
},
|
||||
{
|
||||
name: 'Github',
|
||||
token: 'jirowjgrwGREHEgdrgithub',
|
||||
createdAt: new Date(2022, 1, 2),
|
||||
},
|
||||
{
|
||||
name: 'N8n',
|
||||
token: 'jirowjgrwGREHrgwhrwn8n',
|
||||
createdAt: new Date(2022, 1, 3),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
await prisma.user.create({
|
||||
data: { id: otherUserId, email: 'other-user@email.com', name: 'James Doe' },
|
||||
})
|
||||
return prisma.memberInWorkspace.createMany({
|
||||
data: [
|
||||
{
|
||||
role: WorkspaceRole.ADMIN,
|
||||
userId,
|
||||
workspaceId: freeWorkspaceId,
|
||||
},
|
||||
{
|
||||
role: WorkspaceRole.ADMIN,
|
||||
userId,
|
||||
workspaceId: starterWorkspaceId,
|
||||
},
|
||||
{
|
||||
role: WorkspaceRole.ADMIN,
|
||||
userId,
|
||||
workspaceId: proWorkspaceId,
|
||||
},
|
||||
{
|
||||
role: WorkspaceRole.ADMIN,
|
||||
userId,
|
||||
workspaceId: lifetimeWorkspaceId,
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
const setupCredentials = () => {
|
||||
const { encryptedData, iv } = encrypt({
|
||||
expiry_date: 1642441058842,
|
||||
access_token:
|
||||
'ya29.A0ARrdaM--PV_87ebjywDJpXKb77NBFJl16meVUapYdfNv6W6ZzqqC47fNaPaRjbDbOIIcp6f49cMaX5ndK9TAFnKwlVqz3nrK9nLKqgyDIhYsIq47smcAIZkK56SWPx3X3DwAFqRu2UPojpd2upWwo-3uJrod',
|
||||
// This token is linked to a test Google account (typebot.test.user@gmail.com)
|
||||
refresh_token:
|
||||
'1//039xWRt8YaYa3CgYIARAAGAMSNwF-L9Iru9FyuTrDSa7lkSceggPho83kJt2J29G69iEhT1C6XV1vmo6bQS9puL_R2t8FIwR3gek',
|
||||
})
|
||||
return prisma.credentials.createMany({
|
||||
data: [
|
||||
{
|
||||
name: 'pro-user@email.com',
|
||||
type: CredentialsType.GOOGLE_SHEETS,
|
||||
data: encryptedData,
|
||||
workspaceId: proWorkspaceId,
|
||||
iv,
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
export const setupDatabase = async () => {
|
||||
await setupWorkspaces()
|
||||
await setupUsers()
|
||||
return setupCredentials()
|
||||
}
|
||||
|
||||
export const teardownDatabase = async () => {
|
||||
await prisma.workspace.deleteMany({
|
||||
where: {
|
||||
members: {
|
||||
some: { userId: { in: [userId, otherUserId] } },
|
||||
},
|
||||
},
|
||||
})
|
||||
await prisma.user.deleteMany({
|
||||
where: { id: { in: [userId, otherUserId] } },
|
||||
})
|
||||
return prisma.webhook.deleteMany()
|
||||
}
|
30
packages/utils/playwright/testHelpers.ts
Normal file
30
packages/utils/playwright/testHelpers.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
|
||||
export const mockSessionResponsesToOtherUser = async (page: Page) =>
|
||||
page.route('/api/auth/session', (route) => {
|
||||
if (route.request().method() === 'GET') {
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
body: '{"user":{"id":"otherUserId","name":"James Doe","email":"other-user@email.com","emailVerified":null,"image":"https://avatars.githubusercontent.com/u/16015833?v=4","stripeId":null,"graphNavigation": "TRACKPAD"}}',
|
||||
})
|
||||
}
|
||||
return route.continue()
|
||||
})
|
||||
|
||||
export const typebotViewer = (page: Page) =>
|
||||
page.frameLocator('#typebot-iframe')
|
||||
|
||||
export const waitForSuccessfulPutRequest = (page: Page) =>
|
||||
page.waitForResponse(
|
||||
(resp) => resp.request().method() === 'PUT' && resp.status() === 200
|
||||
)
|
||||
|
||||
export const waitForSuccessfulPostRequest = (page: Page) =>
|
||||
page.waitForResponse(
|
||||
(resp) => resp.request().method() === 'POST' && resp.status() === 200
|
||||
)
|
||||
|
||||
export const waitForSuccessfulDeleteRequest = (page: Page) =>
|
||||
page.waitForResponse(
|
||||
(resp) => resp.request().method() === 'DELETE' && resp.status() === 200
|
||||
)
|
Reference in New Issue
Block a user