2
0

build: add pnpm

This commit is contained in:
Baptiste Arnaud
2022-08-08 08:21:36 +02:00
parent 8c3b5058f1
commit ee338f62dc
183 changed files with 19442 additions and 18364 deletions

View File

@ -4,4 +4,11 @@ node_modules
npm-debug.log npm-debug.log
README.md README.md
.next .next
.git .git
.github
.turbo
landing-page
docs
scripts
wordpress

View File

@ -12,15 +12,16 @@ jobs:
- name: Log Info - name: Log Info
run: echo ${{ github.event.deployment_status.target_url }} && echo ${{ github.event.deployment }} && echo ${{ github.event.deployment_status.environment }} run: echo ${{ github.event.deployment_status.target_url }} && echo ${{ github.event.deployment }} && echo ${{ github.event.deployment_status.environment }}
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: pnpm/action-setup@v2.2.2
- name: Install dependencies - name: Install dependencies
run: yarn run: pnpm i --frozen-lockfile
- name: Build dependencies - name: Build dependencies
run: yarn turbo run build --scope=builder --include-dependencies run: pnpm turbo run build --filter="builder^..."
- name: Install Playwright - name: Install Playwright
run: npx playwright install --with-deps run: npx playwright install --with-deps
- name: Run tests - name: Run tests
working-directory: ./apps/builder working-directory: ./apps/builder
run: yarn test run: pnpm test
env: env:
PLAYWRIGHT_BUILDER_TEST_BASE_URL: ${{ github.event.deployment_status.target_url }} PLAYWRIGHT_BUILDER_TEST_BASE_URL: ${{ github.event.deployment_status.target_url }}
DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }} DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }}

View File

@ -12,12 +12,10 @@ jobs:
working-directory: ./packages/typebot-js working-directory: ./packages/typebot-js
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-node@v1 - uses: pnpm/action-setup@v2.2.2
with: - run: pnpm i --frozen-lockfile
node-version: 16 - run: pnpm test
- run: yarn - run: pnpm build
- run: yarn test
- run: yarn build
- uses: JS-DevTools/npm-publish@v1 - uses: JS-DevTools/npm-publish@v1
with: with:
package: './packages/typebot-js/package.json' package: './packages/typebot-js/package.json'

View File

@ -9,8 +9,8 @@ on:
branches: [main] branches: [main]
jobs: jobs:
push_images_to_docker_hub: push_builder:
name: Push images to Docker Hub name: Builder
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out the repo - name: Check out the repo
@ -27,17 +27,6 @@ jobs:
type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}} type=semver,pattern={{major}}
- name: Extract Viewer meta
id: viewer-meta
uses: docker/metadata-action@v4
with:
images: baptistearno/typebot-viewer
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
- name: Log in to Docker Hub - name: Log in to Docker Hub
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
uses: docker/login-action@v2 uses: docker/login-action@v2
@ -54,6 +43,30 @@ jobs:
labels: ${{ steps.builder-meta.outputs.labels }} labels: ${{ steps.builder-meta.outputs.labels }}
build-args: | build-args: |
SCOPE=builder SCOPE=builder
push_viewer:
name: Viewer
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v3
- name: Extract Viewer meta
id: viewer-meta
uses: docker/metadata-action@v4
with:
images: baptistearno/typebot-viewer
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
- name: Log in to Docker Hub
if: github.event_name != 'pull_request'
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push viewer image - name: Build and push viewer image
uses: docker/build-push-action@v3 uses: docker/build-push-action@v3

1
.gitignore vendored
View File

@ -5,7 +5,6 @@ node_modules
workspace.code-workspace workspace.code-workspace
.DS_Store .DS_Store
.turbo .turbo
yarn-error.log
authenticatedState.json authenticatedState.json
playwright-report playwright-report
dist dist

1
.npmrc Normal file
View File

@ -0,0 +1 @@
public-hoist-pattern[]=*prisma*

View File

@ -1,51 +1,37 @@
# https://github.com/vercel/turborepo/issues/215#issuecomment-1027058056 FROM node:18-slim AS base
FROM node:16-slim AS base
WORKDIR /app WORKDIR /app
ARG SCOPE ARG SCOPE
ENV SCOPE=${SCOPE} ENV SCOPE=${SCOPE}
RUN npm --global install pnpm
FROM base AS pruner
RUN yarn global add turbo@1.2.9
COPY . .
RUN turbo prune --scope="${SCOPE}" --docker
FROM base AS installer
COPY --from=pruner /app/out/json/ .
COPY --from=pruner /app/out/yarn.lock ./yarn.lock
RUN yarn install --frozen-lockfile
FROM base AS builder FROM base AS builder
SHELL ["/bin/bash", "-o", "pipefail", "-c"] RUN apt-get -qy update && apt-get -qy --no-install-recommends install openssl git
COPY --from=installer /app/ . COPY pnpm-lock.yaml .npmrc pnpm-workspace.yaml ./
COPY --from=pruner /app/out/full/ . RUN pnpm fetch
COPY ./apps/${SCOPE}/.env.docker ./apps/${SCOPE}/.env.production ADD . ./
COPY ./apps/${SCOPE}/.env.docker ./apps/${SCOPE}/.env.local RUN pnpm install -r --offline
RUN apt-get -qy update && apt-get -qy --no-install-recommends install openssl RUN pnpm turbo run build --filter=${SCOPE}...
RUN yarn turbo run build --scope="${SCOPE}" --include-dependencies --no-deps
RUN find . -name node_modules | xargs rm -rf
FROM base AS runner FROM base AS runner
WORKDIR /app WORKDIR /app
ENV NODE_ENV production ENV NODE_ENV production
COPY ./packages/db/prisma ./prisma
COPY --from=installer /app/node_modules ./node_modules
COPY --from=builder /app/apps/${SCOPE}/next.config.js ./
COPY --from=builder /app/apps/${SCOPE}/public ./public
COPY --from=builder /app/apps/${SCOPE}/package.json ./package.json
COPY --from=builder /app/apps/${SCOPE}/.next/standalone ./
COPY --from=builder /app/apps/${SCOPE}/.next/static ./.next/static
COPY --from=builder /app/apps/${SCOPE}/.env.docker ./.env.production
RUN apt-get -qy update \ RUN apt-get -qy update \
&& apt-get -qy --no-install-recommends install \ && apt-get -qy --no-install-recommends install \
openssl \ openssl \
&& apt-get autoremove -yq \ && apt-get autoremove -yq \
&& apt-get clean \ && apt-get clean \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
COPY ./packages/db/prisma ./prisma
COPY ./apps/${SCOPE}/.env.docker ./apps/${SCOPE}/.env.production
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/apps/${SCOPE}/public ./apps/${SCOPE}/public
COPY --from=builder --chown=node:node /app/apps/${SCOPE}/.next/standalone ./
COPY --from=builder --chown=node:node /app/apps/${SCOPE}/.next/static ./apps/${SCOPE}/.next/static
COPY env.sh ${SCOPE}-entrypoint.sh ./ COPY env.sh ${SCOPE}-entrypoint.sh ./
RUN chmod +x ./"${SCOPE}"-entrypoint.sh \ RUN chmod +x ./${SCOPE}-entrypoint.sh \
&& chmod +x ./env.sh && chmod +x ./env.sh
ENTRYPOINT ./"${SCOPE}"-entrypoint.sh ENTRYPOINT ./${SCOPE}-entrypoint.sh
EXPOSE 3000 EXPOSE 3000
ENV PORT 3000 ENV PORT 3000

View File

@ -54,7 +54,7 @@ Interested in self-hosting Typebot on your server? Take a look at the [self-host
```sh ```sh
cd typebot.io cd typebot.io
yarn pnpm i
``` ```
3. Set up environment variables 3. Set up environment variables
@ -69,7 +69,7 @@ Interested in self-hosting Typebot on your server? Take a look at the [self-host
5. Start the builder and viewer 5. Start the builder and viewer
```sh ```sh
yarn dev pnpm dev
``` ```
Builder is available at `http://localhost:3000` Builder is available at `http://localhost:3000`
@ -86,14 +86,14 @@ Interested in self-hosting Typebot on your server? Take a look at the [self-host
```sh ```sh
cd apps/landing-page cd apps/landing-page
yarn dev pnpm dev
``` ```
7. (Optionnal) Start the docs 7. (Optionnal) Start the docs
```sh ```sh
cd apps/docs cd apps/docs
yarn start pnpm start
``` ```
## Contribute ## Contribute

View File

@ -6,3 +6,4 @@ NEXT_PUBLIC_GIPHY_API_KEY=
NEXT_PUBLIC_STRIPE_PUBLIC_KEY= NEXT_PUBLIC_STRIPE_PUBLIC_KEY=
NEXT_PUBLIC_SENTRY_DSN= NEXT_PUBLIC_SENTRY_DSN=
NEXT_PUBLIC_VIEWER_INTERNAL_URL= NEXT_PUBLIC_VIEWER_INTERNAL_URL=
NEXT_PUBLIC_E2E_TEST=

View File

@ -1,5 +1,11 @@
import { FlexProps, Flex, Box, Divider, Text } from '@chakra-ui/react' import {
import { useColorModeValue } from '@chakra-ui/system' FlexProps,
Flex,
Box,
Divider,
Text,
useColorModeValue,
} from '@chakra-ui/react'
import React from 'react' import React from 'react'
export const DividerWithText = (props: FlexProps) => { export const DividerWithText = (props: FlexProps) => {

View File

@ -11,7 +11,7 @@ import { useUser } from 'contexts/UserContext'
import { Answer, Typebot } from 'models' import { Answer, Typebot } from 'models'
import React, { useEffect, useRef, useState } from 'react' import React, { useEffect, useRef, useState } from 'react'
import { parseTypebotToPublicTypebot } from 'services/publicTypebot' import { parseTypebotToPublicTypebot } from 'services/publicTypebot'
import { sendRequest } from 'utils' import { getViewerUrl, sendRequest } from 'utils'
import confetti from 'canvas-confetti' import confetti from 'canvas-confetti'
import { useToast } from 'components/shared/hooks/useToast' import { useToast } from 'components/shared/hooks/useToast'
@ -119,6 +119,7 @@ export const OnboardingModal = ({ totalTypebots }: Props) => {
<ModalBody> <ModalBody>
{typebot && ( {typebot && (
<TypebotViewer <TypebotViewer
apiHost={getViewerUrl({ isBuilder: true })}
typebot={parseTypebotToPublicTypebot(typebot)} typebot={parseTypebotToPublicTypebot(typebot)}
predefinedVariables={{ predefinedVariables={{
Name: user?.name?.split(' ')[0] ?? undefined, Name: user?.name?.split(' ')[0] ?? undefined,

View File

@ -1,5 +1,4 @@
import { Flex, HStack, StackProps } from '@chakra-ui/layout' import { CloseButton, Flex, HStack, StackProps } from '@chakra-ui/react'
import { CloseButton } from '@chakra-ui/react'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
type VerifyEmailBannerProps = { id: string } & StackProps type VerifyEmailBannerProps = { id: string } & StackProps

View File

@ -18,6 +18,7 @@ import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
import { Log } from 'db' import { Log } from 'db'
import React, { useMemo, useState } from 'react' import React, { useMemo, useState } from 'react'
import { parseTypebotToPublicTypebot } from 'services/publicTypebot' import { parseTypebotToPublicTypebot } from 'services/publicTypebot'
import { getViewerUrl } from 'utils'
export const PreviewDrawer = () => { export const PreviewDrawer = () => {
const { typebot } = useTypebot() const { typebot } = useTypebot()
@ -100,6 +101,7 @@ export const PreviewDrawer = () => {
pointerEvents={isResizing ? 'none' : 'auto'} pointerEvents={isResizing ? 'none' : 'auto'}
> >
<TypebotViewer <TypebotViewer
apiHost={getViewerUrl({ isBuilder: true })}
typebot={publicTypebot} typebot={publicTypebot}
onNewGroupVisible={setPreviewingEdge} onNewGroupVisible={setPreviewingEdge}
onNewLog={handleNewLog} onNewLog={handleNewLog}

View File

@ -1,10 +1,11 @@
import { chakra, Fade, Button } from '@chakra-ui/react' import { chakra, Fade, Button } from '@chakra-ui/react'
import { Cell as CellProps } from '@tanstack/react-table' import { Cell as CellProps, flexRender } from '@tanstack/react-table'
import { ExpandIcon } from 'assets/icons' import { ExpandIcon } from 'assets/icons'
import { memo } from 'react' import { memo } from 'react'
import { TableData } from 'services/typebots/results'
type Props = { type Props = {
cell: CellProps<any> cell: CellProps<TableData, unknown>
size: number size: number
isExpandButtonVisible: boolean isExpandButtonVisible: boolean
cellIndex: number cellIndex: number
@ -34,7 +35,7 @@ const Cell = ({
maxWidth: size, maxWidth: size,
}} }}
> >
{cell.renderCell()} {flexRender(cell.column.columnDef.cell, cell.getContext())}
<chakra.span <chakra.span
pos="absolute" pos="absolute"
top="0" top="0"

View File

@ -1,10 +1,10 @@
import { Box, BoxProps, chakra } from '@chakra-ui/react' import { Box, BoxProps, chakra } from '@chakra-ui/react'
import { HeaderGroup } from '@tanstack/react-table' import { flexRender, HeaderGroup } from '@tanstack/react-table'
import React from 'react' import React from 'react'
import { TableData } from 'services/typebots/results'
type Props = { type Props = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any headerGroup: HeaderGroup<TableData>
headerGroup: HeaderGroup<any>
} }
export const HeaderRow = ({ headerGroup }: Props) => { export const HeaderRow = ({ headerGroup }: Props) => {
@ -28,7 +28,9 @@ export const HeaderRow = ({ headerGroup }: Props) => {
maxWidth: header.getSize(), maxWidth: header.getSize(),
}} }}
> >
{header.isPlaceholder ? null : header.renderHeader()} {header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
{header.column.getCanResize() && ( {header.column.getCanResize() && (
<ResizeHandle <ResizeHandle
onMouseDown={header.getResizeHandler()} onMouseDown={header.getResizeHandler()}

View File

@ -18,7 +18,7 @@ import {
convertResultsToTableData, convertResultsToTableData,
getAllResults, getAllResults,
deleteResults as deleteFetchResults, deleteResults as deleteFetchResults,
} from 'services/typebots' } from 'services/typebots/results'
type ResultsActionButtonsProps = { type ResultsActionButtonsProps = {
selectedResultsId: string[] selectedResultsId: string[]
@ -93,7 +93,9 @@ export const ResultsActionButtons = ({
const dataToUnparse = isSelectAll const dataToUnparse = isSelectAll
? await getAllTableData() ? await getAllTableData()
: tableData.filter((data) => selectedResultsId.includes(data.id)) : tableData.filter((data) =>
selectedResultsId.includes(data.id.plainText)
)
const csvData = new Blob( const csvData = new Blob(
[ [
unparse({ unparse({

View File

@ -13,10 +13,10 @@ import { ResultHeaderCell, ResultsTablePreferences } from 'models'
import React, { useEffect, useRef, useState } from 'react' import React, { useEffect, useRef, useState } from 'react'
import { LoadingRows } from './LoadingRows' import { LoadingRows } from './LoadingRows'
import { import {
createTable, useReactTable,
useTableInstance,
getCoreRowModel, getCoreRowModel,
ColumnOrderState, ColumnOrderState,
ColumnDef,
} from '@tanstack/react-table' } from '@tanstack/react-table'
import { BlockIcon } from 'components/editor/BlocksSideBar/BlockIcon' import { BlockIcon } from 'components/editor/BlocksSideBar/BlockIcon'
import { ColumnSettingsButton } from './ColumnsSettingsButton' import { ColumnSettingsButton } from './ColumnsSettingsButton'
@ -25,21 +25,11 @@ import { useDebounce } from 'use-debounce'
import { ResultsActionButtons } from './ResultsActionButtons' import { ResultsActionButtons } from './ResultsActionButtons'
import { Row } from './Row' import { Row } from './Row'
import { HeaderRow } from './HeaderRow' import { HeaderRow } from './HeaderRow'
import { CellValueType, TableData } from 'services/typebots/results'
type RowType = {
id: string
[key: string]:
| {
plainText: string
element?: JSX.Element | undefined
}
| string
}
const table = createTable().setRowType<RowType>()
type ResultsTableProps = { type ResultsTableProps = {
resultHeader: ResultHeaderCell[] resultHeader: ResultHeaderCell[]
data: RowType[] data: TableData[]
hasMore?: boolean hasMore?: boolean
preferences?: ResultsTablePreferences preferences?: ResultsTablePreferences
onScrollToBottom: () => void onScrollToBottom: () => void
@ -92,18 +82,18 @@ export const ResultsTable = ({
const bottomElement = useRef<HTMLDivElement | null>(null) const bottomElement = useRef<HTMLDivElement | null>(null)
const tableWrapper = useRef<HTMLDivElement | null>(null) const tableWrapper = useRef<HTMLDivElement | null>(null)
const columns = React.useMemo( const columns = React.useMemo<ColumnDef<TableData>[]>(
() => [ () => [
table.createDisplayColumn({ {
id: 'select', id: 'select',
enableResizing: false, enableResizing: false,
maxSize: 40, maxSize: 40,
header: ({ instance }) => ( header: ({ table }) => (
<IndeterminateCheckbox <IndeterminateCheckbox
{...{ {...{
checked: instance.getIsAllRowsSelected(), checked: table.getIsAllRowsSelected(),
indeterminate: instance.getIsSomeRowsSelected(), indeterminate: table.getIsSomeRowsSelected(),
onChange: instance.getToggleAllRowsSelectedHandler(), onChange: table.getToggleAllRowsSelectedHandler(),
}} }}
/> />
), ),
@ -118,26 +108,24 @@ export const ResultsTable = ({
/> />
</div> </div>
), ),
}), },
...resultHeader.map((header) => ...resultHeader.map<ColumnDef<TableData>>((header) => ({
table.createDataColumn(header.label, { id: header.id,
id: header.id, accessorKey: header.label,
size: header.isLong ? 400 : 200, size: header.isLong ? 400 : 200,
cell: (info) => { header: () => (
const value = info.getValue() <HStack overflow="hidden" data-testid={`${header.label} header`}>
if (!value) return <HeaderIcon header={header} />
if (typeof value === 'string') return '' <Text>{header.label}</Text>
return value.element || value.plainText || '' </HStack>
}, ),
header: () => ( cell: (info) => {
<HStack overflow="hidden" data-testid={`${header.label} header`}> const value = info.getValue() as CellValueType | undefined
<HeaderIcon header={header} /> if (!value) return
<Text>{header.label}</Text> return value.element || value.plainText || ''
</HStack> },
), })),
}) {
),
table.createDisplayColumn({
id: 'logs', id: 'logs',
enableResizing: false, enableResizing: false,
maxSize: 110, maxSize: 110,
@ -152,13 +140,13 @@ export const ResultsTable = ({
See logs See logs
</Button> </Button>
), ),
}), },
], ],
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
[resultHeader] [resultHeader]
) )
const instance = useTableInstance(table, { const instance = useReactTable({
data, data,
columns, columns,
state: { state: {
@ -167,7 +155,7 @@ export const ResultsTable = ({
columnOrder: columnsOrder, columnOrder: columnsOrder,
columnSizing: columnsWidth, columnSizing: columnsWidth,
}, },
getRowId: (row) => row.id, getRowId: (row) => row.id.plainText,
columnResizeMode: 'onChange', columnResizeMode: 'onChange',
onRowSelectionChange: setRowSelection, onRowSelectionChange: setRowSelection,
onColumnVisibilityChange: setColumnsVisibility, onColumnVisibilityChange: setColumnsVisibility,

View File

@ -1,10 +1,10 @@
import React, { useState } from 'react' import React, { useState } from 'react'
import { Row as RowProps } from '@tanstack/react-table' import { Row as RowProps } from '@tanstack/react-table'
import Cell from './Cell' import Cell from './Cell'
import { TableData } from 'services/typebots/results'
type Props = { type Props = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any row: RowProps<TableData>
row: RowProps<any>
isSelected: boolean isSelected: boolean
bottomElement?: React.MutableRefObject<HTMLDivElement | null> bottomElement?: React.MutableRefObject<HTMLDivElement | null>
onExpandButtonClick: () => void onExpandButtonClick: () => void

View File

@ -16,11 +16,10 @@ import { useWorkspace } from 'contexts/WorkspaceContext'
import React from 'react' import React from 'react'
import { parseDefaultPublicId } from 'services/typebots' import { parseDefaultPublicId } from 'services/typebots'
import { isFreePlan } from 'services/workspace' import { isFreePlan } from 'services/workspace'
import { isDefined, isNotDefined } from 'utils' import { getViewerUrl, isDefined, isNotDefined } from 'utils'
import { CustomDomainsDropdown } from './customDomain/CustomDomainsDropdown' import { CustomDomainsDropdown } from './customDomain/CustomDomainsDropdown'
import { EditableUrl } from './EditableUrl' import { EditableUrl } from './EditableUrl'
import { integrationsList } from './integrations/EmbedButton' import { integrationsList } from './integrations/EmbedButton'
import { env } from 'utils'
export const ShareContent = () => { export const ShareContent = () => {
const { workspace } = useWorkspace() const { workspace } = useWorkspace()
@ -59,7 +58,9 @@ export const ShareContent = () => {
</Heading> </Heading>
{typebot && ( {typebot && (
<EditableUrl <EditableUrl
hostname={env('VIEWER_URL') ?? 'https://typebot.io'} hostname={
getViewerUrl({ isBuilder: true }) ?? 'https://typebot.io'
}
pathname={publicId} pathname={publicId}
onPathnameChange={handlePublicIdChange} onPathnameChange={handlePublicIdChange}
/> />

View File

@ -1,11 +1,11 @@
import { FlexProps } from '@chakra-ui/layout'
import prettier from 'prettier/standalone' import prettier from 'prettier/standalone'
import parserHtml from 'prettier/parser-html' import parserHtml from 'prettier/parser-html'
import { BubbleParams } from 'typebot-js' import { BubbleParams } from 'typebot-js'
import { parseInitBubbleCode, typebotJsHtml } from '../params' import { parseInitBubbleCode, typebotJsHtml } from '../params'
import { useTypebot } from 'contexts/TypebotContext' import { useTypebot } from 'contexts/TypebotContext'
import { CodeEditor } from 'components/shared/CodeEditor' import { CodeEditor } from 'components/shared/CodeEditor'
import { env, isEmpty } from 'utils' import { env, getViewerUrl } from 'utils'
import { FlexProps } from '@chakra-ui/react'
type ChatEmbedCodeProps = { type ChatEmbedCodeProps = {
withStarterVariables?: boolean withStarterVariables?: boolean
@ -21,9 +21,7 @@ export const ChatEmbedCode = ({
const snippet = prettier.format( const snippet = prettier.format(
createSnippet({ createSnippet({
url: `${ url: `${
isEmpty(env('VIEWER_INTERNAL_URL')) env('VIEWER_INTERNAL_URL') ?? getViewerUrl({ isBuilder: true })
? env('VIEWER_URL')
: env('VIEWER_INTERNAL_URL')
}/${typebot?.publicId}`, }/${typebot?.publicId}`,
button, button,
proactiveMessage, proactiveMessage,

View File

@ -5,7 +5,7 @@ import { parseInitContainerCode, typebotJsHtml } from '../params'
import { IframeParams } from 'typebot-js' import { IframeParams } from 'typebot-js'
import { useTypebot } from 'contexts/TypebotContext' import { useTypebot } from 'contexts/TypebotContext'
import { CodeEditor } from 'components/shared/CodeEditor' import { CodeEditor } from 'components/shared/CodeEditor'
import { env, isEmpty } from 'utils' import { env, getViewerUrl } from 'utils'
type ContainerEmbedCodeProps = { type ContainerEmbedCodeProps = {
widthLabel: string widthLabel: string
@ -23,9 +23,7 @@ export const ContainerEmbedCode = ({
const snippet = prettier.format( const snippet = prettier.format(
parseSnippet({ parseSnippet({
url: `${ url: `${
isEmpty(env('VIEWER_INTERNAL_URL')) env('VIEWER_INTERNAL_URL') ?? getViewerUrl({ isBuilder: true })
? env('VIEWER_URL')
: env('VIEWER_INTERNAL_URL')
}/${typebot?.publicId}`, }/${typebot?.publicId}`,
heightLabel, heightLabel,
widthLabel, widthLabel,

View File

@ -1,7 +1,7 @@
import { FlexProps } from '@chakra-ui/react' import { FlexProps } from '@chakra-ui/react'
import { CodeEditor } from 'components/shared/CodeEditor' import { CodeEditor } from 'components/shared/CodeEditor'
import { useTypebot } from 'contexts/TypebotContext' import { useTypebot } from 'contexts/TypebotContext'
import { env, isEmpty } from 'utils' import { env, getViewerUrl } from 'utils'
type Props = { type Props = {
widthLabel: string widthLabel: string
@ -14,9 +14,7 @@ export const IframeEmbedCode = ({
}: Props & FlexProps) => { }: Props & FlexProps) => {
const { typebot } = useTypebot() const { typebot } = useTypebot()
const src = `${ const src = `${
isEmpty(env('VIEWER_INTERNAL_URL')) env('VIEWER_INTERNAL_URL') ?? getViewerUrl({ isBuilder: true })
? env('VIEWER_URL')
: env('VIEWER_INTERNAL_URL')
}/${typebot?.publicId}` }/${typebot?.publicId}`
const code = `<iframe src="${src}" width="${widthLabel}" height="${heightLabel}" />` const code = `<iframe src="${src}" width="${widthLabel}" height="${heightLabel}" />`

View File

@ -1,10 +1,10 @@
import { FlexProps } from '@chakra-ui/layout' import { FlexProps } from '@chakra-ui/react'
import { CodeEditor } from 'components/shared/CodeEditor' import { CodeEditor } from 'components/shared/CodeEditor'
import { useTypebot } from 'contexts/TypebotContext' import { useTypebot } from 'contexts/TypebotContext'
import parserHtml from 'prettier/parser-html' import parserHtml from 'prettier/parser-html'
import prettier from 'prettier/standalone' import prettier from 'prettier/standalone'
import { PopupParams } from 'typebot-js' import { PopupParams } from 'typebot-js'
import { env, isEmpty } from 'utils' import { env, getViewerUrl } from 'utils'
import { parseInitPopupCode, typebotJsHtml } from '../params' import { parseInitPopupCode, typebotJsHtml } from '../params'
type PopupEmbedCodeProps = { type PopupEmbedCodeProps = {
@ -18,9 +18,7 @@ export const PopupEmbedCode = ({ delay }: PopupEmbedCodeProps & FlexProps) => {
const snippet = prettier.format( const snippet = prettier.format(
createSnippet({ createSnippet({
url: `${ url: `${
isEmpty(env('VIEWER_INTERNAL_URL')) env('VIEWER_INTERNAL_URL') ?? getViewerUrl({ isBuilder: true })
? env('VIEWER_URL')
: env('VIEWER_INTERNAL_URL')
}/${typebot?.publicId}`, }/${typebot?.publicId}`,
delay, delay,
}), }),

View File

@ -10,7 +10,7 @@ import parserBabel from 'prettier/parser-babel'
import prettier from 'prettier/standalone' import prettier from 'prettier/standalone'
import { CodeEditor } from 'components/shared/CodeEditor' import { CodeEditor } from 'components/shared/CodeEditor'
import { useTypebot } from 'contexts/TypebotContext' import { useTypebot } from 'contexts/TypebotContext'
import { env, isEmpty } from 'utils' import { env, getViewerUrl } from 'utils'
type StandardReactDivProps = { widthLabel: string; heightLabel: string } type StandardReactDivProps = { widthLabel: string; heightLabel: string }
export const StandardReactDiv = ({ export const StandardReactDiv = ({
@ -21,9 +21,7 @@ export const StandardReactDiv = ({
const snippet = prettier.format( const snippet = prettier.format(
parseContainerSnippet({ parseContainerSnippet({
url: `${ url: `${
isEmpty(env('VIEWER_INTERNAL_URL')) env('VIEWER_INTERNAL_URL') ?? getViewerUrl({ isBuilder: true })
? env('VIEWER_URL')
: env('VIEWER_INTERNAL_URL')
}/${typebot?.publicId}`, }/${typebot?.publicId}`,
heightLabel, heightLabel,
widthLabel, widthLabel,
@ -73,9 +71,7 @@ export const PopupReactCode = ({ delay }: PopupEmbedCodeProps & FlexProps) => {
const snippet = prettier.format( const snippet = prettier.format(
parsePopupSnippet({ parsePopupSnippet({
url: `${ url: `${
isEmpty(env('VIEWER_INTERNAL_URL')) env('VIEWER_INTERNAL_URL') ?? getViewerUrl({ isBuilder: true })
? env('VIEWER_URL')
: env('VIEWER_INTERNAL_URL')
}/${typebot?.publicId}`, }/${typebot?.publicId}`,
delay, delay,
}), }),
@ -124,9 +120,7 @@ export const ChatReactCode = ({
const snippet = prettier.format( const snippet = prettier.format(
parseBubbleSnippet({ parseBubbleSnippet({
url: `${ url: `${
isEmpty(env('VIEWER_INTERNAL_URL')) env('VIEWER_INTERNAL_URL') ?? getViewerUrl({ isBuilder: true })
? env('VIEWER_URL')
: env('VIEWER_INTERNAL_URL')
}/${typebot?.publicId}`, }/${typebot?.publicId}`,
button, button,
proactiveMessage, proactiveMessage,

View File

@ -18,7 +18,7 @@ import {
import { useToast } from 'components/shared/hooks/useToast' import { useToast } from 'components/shared/hooks/useToast'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { createCustomDomain } from 'services/user' import { createCustomDomain } from 'services/user'
import { env, isEmpty } from 'utils' import { env, getViewerUrl } from 'utils'
const hostnameRegex = const hostnameRegex =
/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/ /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/
@ -118,9 +118,8 @@ export const CustomDomainModal = ({
<Stack> <Stack>
<Text fontWeight="bold">Value</Text> <Text fontWeight="bold">Value</Text>
<Text> <Text>
{isEmpty(env('VIEWER_INTERNAL_URL')) {env('VIEWER_INTERNAL_URL') ??
? env('VIEWER_URL') getViewerUrl({ isBuilder: true })}
: env('VIEWER_INTERNAL_URL')}
</Text> </Text>
</Stack> </Stack>
</HStack> </HStack>

View File

@ -11,7 +11,7 @@ import { PopupEmbedSettings } from 'components/share/codeSnippets/Popup/EmbedSet
import { CodeEditor } from 'components/shared/CodeEditor' import { CodeEditor } from 'components/shared/CodeEditor'
import { useState } from 'react' import { useState } from 'react'
import { BubbleParams } from 'typebot-js' import { BubbleParams } from 'typebot-js'
import { env, isEmpty } from 'utils' import { env, getViewerUrl } from 'utils'
import { ModalProps } from '../../EmbedButton' import { ModalProps } from '../../EmbedButton'
type GtmInstructionsProps = { type GtmInstructionsProps = {
@ -41,9 +41,7 @@ const StandardInstructions = ({ publicId }: Pick<ModalProps, 'publicId'>) => {
const jsCode = parseInitContainerCode({ const jsCode = parseInitContainerCode({
url: `${ url: `${
isEmpty(env('VIEWER_INTERNAL_URL')) env('VIEWER_INTERNAL_URL') ?? getViewerUrl({ isBuilder: true })
? env('VIEWER_URL')
: env('VIEWER_INTERNAL_URL')
}/${publicId}`, }/${publicId}`,
}) })
const headCode = `${typebotJsHtml} const headCode = `${typebotJsHtml}

View File

@ -16,7 +16,7 @@ import {
} from '@chakra-ui/react' } from '@chakra-ui/react'
import { CopyButton } from 'components/shared/buttons/CopyButton' import { CopyButton } from 'components/shared/buttons/CopyButton'
import { PublishFirstInfo } from 'components/shared/Info' import { PublishFirstInfo } from 'components/shared/Info'
import { env, isEmpty } from 'utils' import { env, getViewerUrl } from 'utils'
import { ModalProps } from '../EmbedButton' import { ModalProps } from '../EmbedButton'
export const NotionModal = ({ export const NotionModal = ({
@ -46,17 +46,15 @@ export const NotionModal = ({
pr="4.5rem" pr="4.5rem"
type={'text'} type={'text'}
defaultValue={`${ defaultValue={`${
isEmpty(env('VIEWER_INTERNAL_URL')) env('VIEWER_INTERNAL_URL') ??
? env('VIEWER_URL') getViewerUrl({ isBuilder: true })
: env('VIEWER_INTERNAL_URL')
}/${publicId}`} }/${publicId}`}
/> />
<InputRightElement width="4.5rem"> <InputRightElement width="4.5rem">
<CopyButton <CopyButton
textToCopy={`${ textToCopy={`${
isEmpty(env('VIEWER_INTERNAL_URL')) env('VIEWER_INTERNAL_URL') ??
? env('VIEWER_URL') getViewerUrl({ isBuilder: true })
: env('VIEWER_INTERNAL_URL')
}/${publicId}`} }/${publicId}`}
/> />
</InputRightElement> </InputRightElement>

View File

@ -14,7 +14,7 @@ import { BubbleParams } from 'typebot-js'
import { ModalProps } from '../../EmbedButton' import { ModalProps } from '../../EmbedButton'
import parserHtml from 'prettier/parser-html' import parserHtml from 'prettier/parser-html'
import prettier from 'prettier/standalone' import prettier from 'prettier/standalone'
import { env, isEmpty } from 'utils' import { env, getViewerUrl } from 'utils'
type ShopifyInstructionsProps = { type ShopifyInstructionsProps = {
type: 'standard' | 'popup' | 'bubble' type: 'standard' | 'popup' | 'bubble'
@ -46,9 +46,7 @@ const StandardInstructions = ({ publicId }: Pick<ModalProps, 'publicId'>) => {
const jsCode = parseInitContainerCode({ const jsCode = parseInitContainerCode({
url: `${ url: `${
isEmpty(env('VIEWER_INTERNAL_URL')) env('VIEWER_INTERNAL_URL') ?? getViewerUrl({ isBuilder: true })
? env('VIEWER_URL')
: env('VIEWER_INTERNAL_URL')
}/${publicId}`, }/${publicId}`,
}) })
const headCode = prettier.format( const headCode = prettier.format(

View File

@ -17,7 +17,7 @@ import {
import { ExternalLinkIcon } from 'assets/icons' import { ExternalLinkIcon } from 'assets/icons'
import { CopyButton } from 'components/shared/buttons/CopyButton' import { CopyButton } from 'components/shared/buttons/CopyButton'
import { PublishFirstInfo } from 'components/shared/Info' import { PublishFirstInfo } from 'components/shared/Info'
import { env, isEmpty } from 'utils' import { env, getViewerUrl } from 'utils'
import { ModalProps } from '../EmbedButton' import { ModalProps } from '../EmbedButton'
export const WordpressModal = ({ export const WordpressModal = ({
@ -55,17 +55,15 @@ export const WordpressModal = ({
pr="4.5rem" pr="4.5rem"
type={'text'} type={'text'}
defaultValue={`${ defaultValue={`${
isEmpty(env('VIEWER_INTERNAL_URL')) env('VIEWER_INTERNAL_URL') ??
? env('VIEWER_URL') getViewerUrl({ isBuilder: true })
: env('VIEWER_INTERNAL_URL')
}/${publicId}`} }/${publicId}`}
/> />
<InputRightElement width="4.5rem"> <InputRightElement width="4.5rem">
<CopyButton <CopyButton
textToCopy={`${ textToCopy={`${
isEmpty(env('VIEWER_INTERNAL_URL')) env('VIEWER_INTERNAL_URL') ??
? env('VIEWER_URL') getViewerUrl({ isBuilder: true })
: env('VIEWER_INTERNAL_URL')
}/${publicId}`} }/${publicId}`}
/> />
</InputRightElement> </InputRightElement>

View File

@ -1,5 +1,6 @@
import { Box, BoxProps, HStack } from '@chakra-ui/react' import { Box, BoxProps, HStack } from '@chakra-ui/react'
import { EditorState, EditorView, basicSetup } from '@codemirror/basic-setup' import { EditorView, basicSetup } from 'codemirror'
import { EditorState } from '@codemirror/state'
import { json, jsonParseLinter } from '@codemirror/lang-json' import { json, jsonParseLinter } from '@codemirror/lang-json'
import { css } from '@codemirror/lang-css' import { css } from '@codemirror/lang-css'
import { javascript } from '@codemirror/lang-javascript' import { javascript } from '@codemirror/lang-javascript'
@ -43,7 +44,7 @@ export const CodeEditor = ({
setPlainTextValue(value) setPlainTextValue(value)
onChange && onChange(value) onChange && onChange(value)
}, },
env('E2E_TEST') === 'enabled' ? 0 : debounceTimeout env('E2E_TEST') === 'true' ? 0 : debounceTimeout
) )
useEffect( useEffect(

View File

@ -1,4 +1,4 @@
import { useEventListener } from '@chakra-ui/hooks' import { useEventListener } from '@chakra-ui/react'
import assert from 'assert' import assert from 'assert'
import { import {
useGraph, useGraph,

View File

@ -1,4 +1,4 @@
import { chakra } from '@chakra-ui/system' import { chakra } from '@chakra-ui/react'
import { colors } from 'libs/theme' import { colors } from 'libs/theme'
import { Edge as EdgeProps } from 'models' import { Edge as EdgeProps } from 'models'
import React from 'react' import React from 'react'

View File

@ -3,7 +3,6 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'
import { import {
Plate, Plate,
selectEditor, selectEditor,
serializeHtml,
TEditor, TEditor,
TElement, TElement,
Value, Value,
@ -16,6 +15,7 @@ import { parseHtmlStringToPlainText } from 'services/utils'
import { defaultTextBubbleContent, TextBubbleContent, Variable } from 'models' import { defaultTextBubbleContent, TextBubbleContent, Variable } from 'models'
import { VariableSearchInput } from 'components/shared/VariableSearchInput' import { VariableSearchInput } from 'components/shared/VariableSearchInput'
import { ReactEditor } from 'slate-react' import { ReactEditor } from 'slate-react'
import { serializeHtml } from '@udecode/plate-serializer-html'
type Props = { type Props = {
initialValue: TElement[] initialValue: TElement[]
@ -98,7 +98,7 @@ export const TextBubbleEditor = ({ initialValue, onClose }: Props) => {
setValue(val) setValue(val)
setIsVariableDropdownOpen(false) setIsVariableDropdownOpen(false)
} }
const handleKeyDown = (e: React.KeyboardEvent) => { const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.shiftKey) return if (e.shiftKey) return
if (e.key === 'Enter') closeEditor() if (e.key === 'Enter') closeEditor()
} }

View File

@ -1,4 +1,4 @@
import { StackProps, HStack, Button } from '@chakra-ui/react' import { StackProps, HStack, Button, IconButton } from '@chakra-ui/react'
import { import {
MARK_BOLD, MARK_BOLD,
MARK_ITALIC, MARK_ITALIC,
@ -7,7 +7,13 @@ import {
import { getPluginType, PlateEditor, Value } from '@udecode/plate-core' import { getPluginType, PlateEditor, Value } from '@udecode/plate-core'
import { LinkToolbarButton } from '@udecode/plate-ui-link' import { LinkToolbarButton } from '@udecode/plate-ui-link'
import { MarkToolbarButton } from '@udecode/plate-ui-toolbar' import { MarkToolbarButton } from '@udecode/plate-ui-toolbar'
import { BoldIcon, ItalicIcon, UnderlineIcon, LinkIcon } from 'assets/icons' import {
BoldIcon,
ItalicIcon,
UnderlineIcon,
LinkIcon,
UserIcon,
} from 'assets/icons'
type Props = { type Props = {
editor: PlateEditor<Value> editor: PlateEditor<Value>
@ -33,9 +39,12 @@ export const ToolBar = ({
borderBottomWidth={1} borderBottomWidth={1}
{...props} {...props}
> >
<Button size="sm" onMouseDown={handleVariablesButtonMouseDown}> <IconButton
Variables aria-label="Insert variable"
</Button> size="sm"
onMouseDown={handleVariablesButtonMouseDown}
icon={<UserIcon />}
/>
<span data-testid="bold-button"> <span data-testid="bold-button">
<MarkToolbarButton <MarkToolbarButton
type={getPluginType(editor, MARK_BOLD)} type={getPluginType(editor, MARK_BOLD)}

View File

@ -1,22 +1,12 @@
import { ChangeEvent, useState } from 'react' import { useState } from 'react'
import { import { Button, Flex, HStack, Stack, Text } from '@chakra-ui/react'
Button,
Flex,
HStack,
Stack,
Text,
Input as ClassicInput,
SimpleGrid,
GridItem,
} from '@chakra-ui/react'
import { SearchContextManager } from '@giphy/react-components' import { SearchContextManager } from '@giphy/react-components'
import { UploadButton } from '../buttons/UploadButton' import { UploadButton } from '../buttons/UploadButton'
import { GiphySearch } from './GiphySearch' import { GiphySearch } from './GiphySearch'
import { useTypebot } from 'contexts/TypebotContext' import { useTypebot } from 'contexts/TypebotContext'
import { BaseEmoji, emojiIndex } from 'emoji-mart'
import { emojis } from './emojis'
import { Input } from '../Textbox/Input' import { Input } from '../Textbox/Input'
import { env, isEmpty } from 'utils' import { env, isEmpty } from 'utils'
import { EmojiSearchableList } from './emoji/EmojiSearchableList'
type Props = { type Props = {
url?: string url?: string
@ -101,7 +91,7 @@ const BodyContent = ({
case 'giphy': case 'giphy':
return <GiphyContent onNewUrl={onSubmit} /> return <GiphyContent onNewUrl={onSubmit} />
case 'emoji': case 'emoji':
return <EmojiContent onEmojiSelected={onSubmit} /> return <EmojiSearchableList onEmojiSelected={onSubmit} />
} }
} }
@ -133,55 +123,6 @@ const EmbedLinkContent = ({ initialUrl, onNewUrl }: ContentProps) => (
</Stack> </Stack>
) )
const EmojiContent = ({
onEmojiSelected,
}: {
onEmojiSelected: (emoji: string) => void
}) => {
const [searchValue, setSearchValue] = useState('')
const [filteredEmojis, setFilteredEmojis] = useState<string[]>(emojis)
const handleEmojiClick = (emoji: string) => () => onEmojiSelected(emoji)
const handleSearchChange = (e: ChangeEvent<HTMLInputElement>) => {
setSearchValue(e.target.value)
setFilteredEmojis(
emojiIndex.search(e.target.value)?.map((o) => (o as BaseEmoji).native) ??
emojis
)
}
return (
<Stack>
<ClassicInput
placeholder="Search..."
value={searchValue}
onChange={handleSearchChange}
/>
<SimpleGrid
maxH="350px"
overflowY="scroll"
overflowX="hidden"
spacing={0}
columns={7}
>
{filteredEmojis.map((emoji) => (
<GridItem key={emoji}>
<Button
onClick={handleEmojiClick(emoji)}
variant="ghost"
size="sm"
fontSize="xl"
>
{emoji}
</Button>
</GridItem>
))}
</SimpleGrid>
</Stack>
)
}
const GiphyContent = ({ onNewUrl }: ContentProps) => { const GiphyContent = ({ onNewUrl }: ContentProps) => {
if (isEmpty(env('GIPHY_API_KEY'))) if (isEmpty(env('GIPHY_API_KEY')))
return <Text>NEXT_PUBLIC_GIPHY_API_KEY is missing in environment</Text> return <Text>NEXT_PUBLIC_GIPHY_API_KEY is missing in environment</Text>

View File

@ -0,0 +1,199 @@
import emojis from './emojiList.json'
import emojiTagsData from 'emojilib'
import {
Stack,
SimpleGrid,
GridItem,
Button,
Input as ClassicInput,
Text,
} from '@chakra-ui/react'
import { useState, ChangeEvent, useEffect, useRef } from 'react'
const emojiTags = emojiTagsData as Record<string, string[]>
const people = emojis['Smileys & Emotion'].concat(emojis['People & Body'])
const nature = emojis['Animals & Nature']
const food = emojis['Food & Drink']
const activities = emojis['Activities']
const travel = emojis['Travel & Places']
const objects = emojis['Objects']
const symbols = emojis['Symbols']
const flags = emojis['Flags']
export const EmojiSearchableList = ({
onEmojiSelected,
}: {
onEmojiSelected: (emoji: string) => void
}) => {
const scrollContainer = useRef<HTMLDivElement>(null)
const bottomElement = useRef<HTMLDivElement>(null)
const [isSearching, setIsSearching] = useState(false)
const [filteredPeople, setFilteredPeople] = useState(people)
const [filteredAnimals, setFilteredAnimals] = useState(nature)
const [filteredFood, setFilteredFood] = useState(food)
const [filteredTravel, setFilteredTravel] = useState(travel)
const [filteredActivities, setFilteredActivities] = useState(activities)
const [filteredObjects, setFilteredObjects] = useState(objects)
const [filteredSymbols, setFilteredSymbols] = useState(symbols)
const [filteredFlags, setFilteredFlags] = useState(flags)
const [totalDisplayedCategories, setTotalDisplayedCategories] = useState(1)
useEffect(() => {
if (!bottomElement.current) return
const observer = new IntersectionObserver(handleObserver, {
root: scrollContainer.current,
})
if (bottomElement.current) observer.observe(bottomElement.current)
return () => {
observer.disconnect()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [bottomElement.current])
const handleObserver = (entities: IntersectionObserverEntry[]) => {
const target = entities[0]
if (target.isIntersecting) setTotalDisplayedCategories((c) => c + 1)
}
const handleSearchChange = async (e: ChangeEvent<HTMLInputElement>) => {
const searchValue = e.target.value
if (searchValue.length <= 2 && isSearching) return resetEmojiList()
setIsSearching(true)
setTotalDisplayedCategories(8)
const byTag = (emoji: string) =>
emojiTags[emoji].find((tag) => tag.includes(searchValue))
setFilteredPeople(people.filter(byTag))
setFilteredAnimals(nature.filter(byTag))
setFilteredFood(food.filter(byTag))
setFilteredTravel(travel.filter(byTag))
setFilteredActivities(activities.filter(byTag))
setFilteredObjects(objects.filter(byTag))
setFilteredSymbols(symbols.filter(byTag))
setFilteredFlags(flags.filter(byTag))
}
const resetEmojiList = () => {
setTotalDisplayedCategories(1)
setIsSearching(false)
setFilteredPeople(people)
setFilteredAnimals(nature)
setFilteredFood(food)
setFilteredTravel(travel)
setFilteredActivities(activities)
setFilteredObjects(objects)
setFilteredSymbols(symbols)
setFilteredFlags(flags)
}
return (
<Stack>
<ClassicInput placeholder="Search..." onChange={handleSearchChange} />
<Stack ref={scrollContainer} overflow="scroll" maxH="350px" spacing={4}>
{filteredPeople.length > 0 && (
<Stack>
<Text fontSize="sm" pl="2">
People
</Text>
<EmojiGrid emojis={filteredPeople} onEmojiClick={onEmojiSelected} />
</Stack>
)}
{filteredAnimals.length > 0 && totalDisplayedCategories >= 2 && (
<Stack>
<Text fontSize="sm" pl="2">
Animals & Nature
</Text>
<EmojiGrid
emojis={filteredAnimals}
onEmojiClick={onEmojiSelected}
/>
</Stack>
)}
{filteredFood.length > 0 && totalDisplayedCategories >= 3 && (
<Stack>
<Text fontSize="sm" pl="2">
Food & Drink
</Text>
<EmojiGrid emojis={filteredFood} onEmojiClick={onEmojiSelected} />
</Stack>
)}
{filteredTravel.length > 0 && totalDisplayedCategories >= 4 && (
<Stack>
<Text fontSize="sm" pl="2">
Travel & Places
</Text>
<EmojiGrid emojis={filteredTravel} onEmojiClick={onEmojiSelected} />
</Stack>
)}
{filteredActivities.length > 0 && totalDisplayedCategories >= 5 && (
<Stack>
<Text fontSize="sm" pl="2">
Activities
</Text>
<EmojiGrid
emojis={filteredActivities}
onEmojiClick={onEmojiSelected}
/>
</Stack>
)}
{filteredObjects.length > 0 && totalDisplayedCategories >= 6 && (
<Stack>
<Text fontSize="sm" pl="2">
Objects
</Text>
<EmojiGrid
emojis={filteredObjects}
onEmojiClick={onEmojiSelected}
/>
</Stack>
)}
{filteredSymbols.length > 0 && totalDisplayedCategories >= 7 && (
<Stack>
<Text fontSize="sm" pl="2">
Symbols
</Text>
<EmojiGrid
emojis={filteredSymbols}
onEmojiClick={onEmojiSelected}
/>
</Stack>
)}
{filteredFlags.length > 0 && totalDisplayedCategories >= 8 && (
<Stack>
<Text fontSize="sm" pl="2">
Flags
</Text>
<EmojiGrid emojis={filteredFlags} onEmojiClick={onEmojiSelected} />
</Stack>
)}
<div ref={bottomElement} />
</Stack>
</Stack>
)
}
const EmojiGrid = ({
emojis,
onEmojiClick,
}: {
emojis: string[]
onEmojiClick: (emoji: string) => void
}) => {
const handleClick = (emoji: string) => () => onEmojiClick(emoji)
return (
<SimpleGrid spacing={0} columns={7}>
{emojis.map((emoji) => (
<GridItem
as={Button}
onClick={handleClick(emoji)}
variant="ghost"
size="sm"
fontSize="xl"
key={emoji}
>
{emoji}
</GridItem>
))}
</SimpleGrid>
)
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -38,7 +38,7 @@ export const SearchableDropdown = ({
const debounced = useDebouncedCallback( const debounced = useDebouncedCallback(
// eslint-disable-next-line @typescript-eslint/no-empty-function // eslint-disable-next-line @typescript-eslint/no-empty-function
onValueChange ? onValueChange : () => {}, onValueChange ? onValueChange : () => {},
env('E2E_TEST') === 'enabled' ? 0 : debounceTimeout env('E2E_TEST') === 'true' ? 0 : debounceTimeout
) )
const [filteredItems, setFilteredItems] = useState([ const [filteredItems, setFilteredItems] = useState([
...items ...items

View File

@ -23,7 +23,7 @@ export const SmartNumberInput = ({
const [currentValue, setCurrentValue] = useState(value?.toString() ?? '') const [currentValue, setCurrentValue] = useState(value?.toString() ?? '')
const debounced = useDebouncedCallback( const debounced = useDebouncedCallback(
onValueChange, onValueChange,
env('E2E_TEST') === 'enabled' ? 0 : debounceTimeout env('E2E_TEST') === 'true' ? 0 : debounceTimeout
) )
useEffect( useEffect(

View File

@ -36,7 +36,7 @@ export const TextBox = ({
(value) => { (value) => {
onChange(value) onChange(value)
}, },
env('E2E_TEST') === 'enabled' ? 0 : debounceTimeout env('E2E_TEST') === 'true' ? 0 : debounceTimeout
) )
useEffect(() => { useEffect(() => {

View File

@ -1,5 +1,9 @@
import { Editable, EditablePreview, EditableInput } from '@chakra-ui/editable' import {
import { Tooltip } from '@chakra-ui/tooltip' Editable,
EditablePreview,
EditableInput,
Tooltip,
} from '@chakra-ui/react'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
type EditableProps = { type EditableProps = {

View File

@ -47,7 +47,7 @@ export const VariableSearchInput = ({
const variable = variables.find((v) => v.name === value) const variable = variables.find((v) => v.name === value)
if (variable) onSelectVariable(variable) if (variable) onSelectVariable(variable)
}, },
env('E2E_TEST') === 'enabled' ? 0 : debounceTimeout env('E2E_TEST') === 'true' ? 0 : debounceTimeout
) )
const [filteredItems, setFilteredItems] = useState<Variable[]>( const [filteredItems, setFilteredItems] = useState<Variable[]>(
variables ?? [] variables ?? []

View File

@ -1,4 +1,4 @@
import { useToast as useChakraToast, UseToastOptions } from '@chakra-ui/toast' import { useToast as useChakraToast, UseToastOptions } from '@chakra-ui/react'
export const useToast = () => { export const useToast = () => {
const toast = useChakraToast() const toast = useChakraToast()

View File

@ -17,7 +17,7 @@ import { useToast } from 'components/shared/hooks/useToast'
import { Typebot } from 'models' import { Typebot } from 'models'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { parseTypebotToPublicTypebot } from 'services/publicTypebot' import { parseTypebotToPublicTypebot } from 'services/publicTypebot'
import { sendRequest } from 'utils' import { getViewerUrl, sendRequest } from 'utils'
import { TemplateProps, templates } from './data' import { TemplateProps, templates } from './data'
type Props = { type Props = {
@ -71,6 +71,7 @@ export const TemplatesModal = ({ isOpen, onClose, onTypebotChoose }: Props) => {
</Heading> </Heading>
{typebot && ( {typebot && (
<TypebotViewer <TypebotViewer
apiHost={getViewerUrl({ isBuilder: true })}
typebot={parseTypebotToPublicTypebot(typebot)} typebot={parseTypebotToPublicTypebot(typebot)}
key={typebot.id} key={typebot.id}
/> />

View File

@ -2,8 +2,9 @@ import { ResultHeaderCell, ResultWithAnswers } from 'models'
import { createContext, ReactNode, useContext, useMemo } from 'react' import { createContext, ReactNode, useContext, useMemo } from 'react'
import { import {
convertResultsToTableData, convertResultsToTableData,
TableData,
useResults as useFetchResults, useResults as useFetchResults,
} from 'services/typebots' } from 'services/typebots/results'
import { KeyedMutator } from 'swr' import { KeyedMutator } from 'swr'
import { isDefined, parseResultHeader } from 'utils' import { isDefined, parseResultHeader } from 'utils'
import { useTypebot } from './TypebotContext' import { useTypebot } from './TypebotContext'
@ -15,10 +16,7 @@ const resultsContext = createContext<{
resultHeader: ResultHeaderCell[] resultHeader: ResultHeaderCell[]
totalResults: number totalResults: number
totalHiddenResults?: number totalHiddenResults?: number
tableData: { tableData: TableData[]
id: string
[key: string]: { plainText: string; element?: JSX.Element } | string
}[]
onDeleteResults: (totalResultsDeleted: number) => void onDeleteResults: (totalResultsDeleted: number) => void
fetchMore: () => void fetchMore: () => void
mutate: KeyedMutator< mutate: KeyedMutator<

View File

@ -150,12 +150,6 @@ export const TypebotContext = ({
new Date(typebot.updatedAt) > new Date(typebot.updatedAt) >
new Date(currentTypebotRef.current.updatedAt) new Date(currentTypebotRef.current.updatedAt)
) { ) {
console.log(
'Incoming typebot',
typebot,
'Current typebot',
currentTypebotRef.current
)
setLocalTypebot({ ...typebot }) setLocalTypebot({ ...typebot })
} }
@ -392,7 +386,7 @@ export const useFetchedTypebot = ({
}, },
Error Error
>(`/api/typebots/${typebotId}`, fetcher, { >(`/api/typebots/${typebotId}`, fetcher, {
dedupingInterval: env('E2E_TEST') === 'enabled' ? 0 : undefined, dedupingInterval: env('E2E_TEST') === 'true' ? 0 : undefined,
}) })
if (error && onError) onError(error) if (error && onError) onError(error)
return { return {

View File

@ -14,14 +14,9 @@ const typebotDndContext = createContext<{
setDraggedTypebot: Dispatch<SetStateAction<TypebotInDashboard | undefined>> setDraggedTypebot: Dispatch<SetStateAction<TypebotInDashboard | undefined>>
mouseOverFolderId?: string | null mouseOverFolderId?: string | null
setMouseOverFolderId: Dispatch<SetStateAction<string | undefined | null>> setMouseOverFolderId: Dispatch<SetStateAction<string | undefined | null>>
}>({ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
setDraggedTypebot: () => { // @ts-ignore
console.log('Not implemented') }>({})
},
setMouseOverFolderId: () => {
console.log('Not implemented')
},
})
export const TypebotDndContext = ({ children }: { children: ReactNode }) => { export const TypebotDndContext = ({ children }: { children: ReactNode }) => {
const [draggedTypebot, setDraggedTypebot] = useState<TypebotInDashboard>() const [draggedTypebot, setDraggedTypebot] = useState<TypebotInDashboard>()

View File

@ -4,6 +4,7 @@ import React, { useMemo } from 'react'
import { TypebotViewer } from 'bot-engine' import { TypebotViewer } from 'bot-engine'
import { parseTypebotToPublicTypebot } from 'services/publicTypebot' import { parseTypebotToPublicTypebot } from 'services/publicTypebot'
import { SettingsSideMenu } from 'components/settings/SettingsSideMenu' import { SettingsSideMenu } from 'components/settings/SettingsSideMenu'
import { getViewerUrl } from 'utils'
export const SettingsContent = () => { export const SettingsContent = () => {
const { typebot } = useTypebot() const { typebot } = useTypebot()
@ -17,7 +18,12 @@ export const SettingsContent = () => {
<Flex h="full" w="full"> <Flex h="full" w="full">
<SettingsSideMenu /> <SettingsSideMenu />
<Flex flex="1"> <Flex flex="1">
{publicTypebot && <TypebotViewer typebot={publicTypebot} />} {publicTypebot && (
<TypebotViewer
apiHost={getViewerUrl({ isBuilder: true })}
typebot={publicTypebot}
/>
)}
</Flex> </Flex>
</Flex> </Flex>
) )

View File

@ -3,6 +3,7 @@ import { TypebotViewer } from 'bot-engine'
import { useTypebot } from 'contexts/TypebotContext/TypebotContext' import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
import React from 'react' import React from 'react'
import { parseTypebotToPublicTypebot } from 'services/publicTypebot' import { parseTypebotToPublicTypebot } from 'services/publicTypebot'
import { getViewerUrl } from 'utils'
import { ThemeSideMenu } from '../../components/theme/ThemeSideMenu' import { ThemeSideMenu } from '../../components/theme/ThemeSideMenu'
export const ThemeContent = () => { export const ThemeContent = () => {
@ -12,7 +13,12 @@ export const ThemeContent = () => {
<Flex h="full" w="full"> <Flex h="full" w="full">
<ThemeSideMenu /> <ThemeSideMenu />
<Flex flex="1"> <Flex flex="1">
{publicTypebot && <TypebotViewer typebot={publicTypebot} />} {publicTypebot && (
<TypebotViewer
apiHost={getViewerUrl({ isBuilder: true })}
typebot={publicTypebot}
/>
)}
</Flex> </Flex>
</Flex> </Flex>
) )

View File

@ -5,6 +5,7 @@ import {
} from '@udecode/plate-basic-marks' } from '@udecode/plate-basic-marks'
import { createPlugins } from '@udecode/plate-core' import { createPlugins } from '@udecode/plate-core'
import { createLinkPlugin, ELEMENT_LINK } from '@udecode/plate-link' import { createLinkPlugin, ELEMENT_LINK } from '@udecode/plate-link'
import { PlateFloatingLink } from '@udecode/plate-ui-link'
export const editorStyle: React.CSSProperties = { export const editorStyle: React.CSSProperties = {
flex: 1, flex: 1,
@ -18,7 +19,9 @@ export const platePlugins = createPlugins(
createBoldPlugin(), createBoldPlugin(),
createItalicPlugin(), createItalicPlugin(),
createUnderlinePlugin(), createUnderlinePlugin(),
createLinkPlugin(), createLinkPlugin({
renderAfterEditable: PlateFloatingLink,
}),
], ],
{ {
components: { components: {

View File

@ -100,4 +100,5 @@ const components = {
}, },
} }
export const customTheme = extendTheme({ colors, fonts, components }) // eslint-disable-next-line @typescript-eslint/no-explicit-any
export const customTheme: any = extendTheme({ colors, fonts, components })

View File

@ -1,30 +0,0 @@
import { rest, setupWorker } from 'msw'
import { setupServer } from 'msw/node'
const handlers = () => [
rest.get('http://localhost:3000/api/auth/session', (req, res, ctx) => {
const authenticatedUser = JSON.parse(
typeof localStorage !== 'undefined'
? (localStorage.getItem('authenticatedUser') as string)
: '{"id":"proUser","name":"Pro user","email":"pro-user@email.com","emailVerified":null,"image":"https://avatars.githubusercontent.com/u/16015833?v=4","plan":"PRO","stripeId":null}'
)
return res(
ctx.json({
user: authenticatedUser,
expires: '2022-03-13T17:02:42.317Z',
})
)
}),
]
export const enableMocks = () => {
if (typeof window === 'undefined') {
const server = setupServer(...handlers())
server.listen()
} else {
const worker = setupWorker(...handlers())
worker.start({
onUnhandledRequest: 'bypass',
})
}
}

View File

@ -1,19 +1,20 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires /* eslint-disable @typescript-eslint/no-var-requires */
const { withSentryConfig } = require('@sentry/nextjs') const { withSentryConfig } = require('@sentry/nextjs')
const path = require('path')
const moduleExports = { /** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
output: 'standalone',
experimental: { experimental: {
outputStandalone: true, outputFileTracingRoot: path.join(__dirname, '../../'),
}, },
optimizeFonts: false,
} }
const sentryWebpackPluginOptions = { const sentryWebpackPluginOptions = {
silent: true, silent: true,
// For all available options, see:
// https://github.com/getsentry/sentry-webpack-plugin#options.
} }
module.exports = process.env.SENTRY_AUTH_TOKEN module.exports = process.env.SENTRY_AUTH_TOKEN
? withSentryConfig(moduleExports, sentryWebpackPluginOptions) ? withSentryConfig(nextConfig, sentryWebpackPluginOptions)
: moduleExports : nextConfig

View File

@ -3,112 +3,112 @@
"version": "0.1.0", "version": "0.1.0",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"scripts": { "scripts": {
"dx": "yarn dev", "dx": "pnpm dev",
"dev": "ENVSH_ENV=.env.local bash ../../env.sh next dev -p 3000", "dev": "ENVSH_ENV=.env.local bash ../../env.sh next dev -p 3000",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "next lint",
"test": "yarn playwright test", "test": "pnpm playwright test",
"test:open": "PWDEBUG=1 yarn playwright test" "test:open": "PWDEBUG=1 pnpm playwright test"
},
"msw": {
"workerDirectory": "public"
}, },
"dependencies": { "dependencies": {
"@chakra-ui/css-reset": "^2.0.0", "@chakra-ui/css-reset": "2.0.3",
"@chakra-ui/react": "^2.0.0", "@chakra-ui/react": "^2.2.6",
"@codemirror/basic-setup": "^0.20.0", "@codemirror/lang-css": "^6.0.0",
"@codemirror/lang-css": "^0.20.0", "@codemirror/lang-html": "^6.1.0",
"@codemirror/lang-html": "^0.20.0", "@codemirror/lang-javascript": "^6.0.2",
"@codemirror/lang-javascript": "^0.20.0", "@codemirror/lang-json": "^6.0.0",
"@codemirror/lang-json": "^0.20.0", "@codemirror/lint": "^6.0.0",
"@codemirror/text": "^0.19.6", "@codemirror/state": "6.1.1",
"@dnd-kit/core": "^6.0.5", "@dnd-kit/core": "^6.0.5",
"@dnd-kit/sortable": "^7.0.1", "@dnd-kit/sortable": "^7.0.1",
"@emotion/react": "^11.9.0", "@dnd-kit/utilities": "^3.2.0",
"@emotion/styled": "^11.8.1", "@emotion/react": "^11.10.0",
"@giphy/js-fetch-api": "^4.1.2", "@emotion/styled": "^11.10.0",
"@giphy/js-types": "^4.1.0", "@giphy/js-fetch-api": "4.4.0",
"@giphy/react-components": "^5.7.0", "@giphy/js-types": "^4.2.1",
"@googleapis/drive": "^2.3.0", "@giphy/react-components": "6.0.0",
"@sentry/nextjs": "^6.19.7", "@googleapis/drive": "^3.0.1",
"@stripe/stripe-js": "^1.29.0", "@sentry/nextjs": "7.9.0",
"@tanstack/react-table": "^8.0.13", "@stripe/stripe-js": "1.35.0",
"@udecode/plate-basic-marks": "^13.1.0", "@tanstack/react-table": "8.5.11",
"@udecode/plate-basic-marks": "16.0.0",
"@udecode/plate-common": "^7.0.2", "@udecode/plate-common": "^7.0.2",
"@udecode/plate-core": "^13.1.0", "@udecode/plate-core": "16.0.0",
"@udecode/plate-link": "^13.1.0", "@udecode/plate-link": "16.0.0",
"@udecode/plate-ui-link": "^13.1.0", "@udecode/plate-serializer-html": "16.0.0",
"@udecode/plate-ui-toolbar": "^13.1.0", "@udecode/plate-ui-link": "16.0.0",
"aws-sdk": "^2.1152.0", "@udecode/plate-ui-toolbar": "16.0.0",
"bot-engine": "*", "aws-sdk": "2.1189.0",
"bot-engine": "workspace:*",
"browser-image-compression": "^2.0.0", "browser-image-compression": "^2.0.0",
"canvas-confetti": "^1.5.1", "canvas-confetti": "^1.5.1",
"codemirror": "^6.0.1",
"cuid": "^2.1.8", "cuid": "^2.1.8",
"db": "*",
"deep-object-diff": "^1.1.7", "deep-object-diff": "^1.1.7",
"dequal": "^2.0.2", "dequal": "^2.0.3",
"emoji-mart": "^3.0.1", "emojilib": "3.0.7",
"focus-visible": "^5.2.0", "focus-visible": "^5.2.0",
"framer-motion": "6.3.3", "framer-motion": "7.0.0",
"google-auth-library": "^8.0.2", "google-auth-library": "^8.1.1",
"google-spreadsheet": "^3.2.0", "google-spreadsheet": "^3.3.0",
"got": "^12.0.4", "got": "12.3.1",
"htmlparser2": "^8.0.1", "htmlparser2": "^8.0.1",
"immer": "^9.0.14", "immer": "^9.0.15",
"js-video-url-parser": "^0.5.1", "js-video-url-parser": "^0.5.1",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"kbar": "^0.1.0-beta.34", "kbar": "^0.1.0-beta.36",
"micro": "^9.3.4", "micro": "9.4.1",
"micro-cors": "^0.1.1", "micro-cors": "^0.1.1",
"minio": "^7.0.28", "minio": "7.0.30",
"models": "*", "next": "12.2.4",
"next": "^12.1.6", "next-auth": "4.10.3",
"next-auth": "4.9.0", "nodemailer": "^6.7.7",
"nodemailer": "^6.7.5",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"papaparse": "^5.3.2", "papaparse": "^5.3.2",
"prettier": "^2.6.2", "prettier": "2.7.1",
"qs": "^6.10.3", "qs": "^6.11.0",
"react": "^18.1.0", "react": "^18.2.0",
"react-dom": "^18.1.0", "react-dom": "^18.2.0",
"react-draggable": "^4.4.5", "react-draggable": "^4.4.5",
"slate": "^0.81.1", "slate": "0.82.0",
"slate-history": "^0.66.0", "slate-history": "^0.66.0",
"slate-hyperscript": "^0.77.0", "slate-hyperscript": "^0.77.0",
"slate-react": "^0.81.0", "slate-react": "0.82.0",
"stripe": "^9.4.0", "stripe": "10.0.0",
"styled-components": "^5.3.5", "styled-components": "^5.3.5",
"svg-round-corners": "^0.3.0", "svg-round-corners": "^0.3.0",
"swr": "^1.3.0", "swr": "^1.3.0",
"tinycolor2": "^1.4.2", "tinycolor2": "^1.4.2",
"typebot-js": "*", "typebot-js": "workspace:*",
"use-debounce": "^8.0.1", "use-debounce": "8.0.3"
"utils": "*"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.22.0", "@babel/core": "7.18.10",
"@types/aws-sdk": "^2.7.0", "@playwright/test": "1.24.2",
"@types/canvas-confetti": "^1.4.2", "@types/canvas-confetti": "^1.4.3",
"@types/emoji-mart": "^3.0.9", "@types/google-spreadsheet": "^3.3.0",
"@types/google-spreadsheet": "^3.2.1",
"@types/jsonwebtoken": "8.5.8", "@types/jsonwebtoken": "8.5.8",
"@types/micro-cors": "^0.1.2", "@types/micro-cors": "^0.1.2",
"@types/minio": "^7.0.13", "@types/minio": "^7.0.13",
"@types/node": "^17.0.33", "@types/node": "18.6.5",
"@types/nodemailer": "^6.4.4", "@types/nodemailer": "6.4.5",
"@types/nprogress": "^0.2.0", "@types/nprogress": "^0.2.0",
"@types/papaparse": "^5.3.2", "@types/papaparse": "5.3.3",
"@types/prettier": "^2.6.1", "@types/prettier": "2.7.0",
"@types/qs": "^6.9.7", "@types/qs": "^6.9.7",
"@types/react": "^18.0.9", "@types/react": "^18.0.17",
"@types/react-table": "^7.7.12", "@types/react-table": "^7.7.12",
"@types/tinycolor2": "^1.4.3", "@types/tinycolor2": "^1.4.3",
"@typescript-eslint/eslint-plugin": "^5.23.0", "@typescript-eslint/eslint-plugin": "5.33.0",
"@typescript-eslint/parser": "5.33.0",
"db": "workspace:*",
"dotenv": "^16.0.1", "dotenv": "^16.0.1",
"eslint": "<8.0.0", "eslint": "8.21.0",
"eslint-config-next": "12.1.6", "eslint-config-next": "12.2.4",
"msw": "^0.43.1", "eslint-plugin-react": "^7.30.1",
"typescript": "^4.6.4" "models": "workspace:*",
"typescript": "^4.7.4",
"utils": "workspace:*"
} }
} }

View File

@ -6,7 +6,6 @@ import { customTheme } from 'libs/theme'
import { useRouterProgressBar } from 'libs/routerProgressBar' import { useRouterProgressBar } from 'libs/routerProgressBar'
import 'assets/styles/routerProgressBar.css' import 'assets/styles/routerProgressBar.css'
import 'assets/styles/plate.css' import 'assets/styles/plate.css'
import 'focus-visible/dist/focus-visible'
import 'assets/styles/submissionsTable.css' import 'assets/styles/submissionsTable.css'
import 'assets/styles/codeMirror.css' import 'assets/styles/codeMirror.css'
import 'assets/styles/custom.css' import 'assets/styles/custom.css'
@ -15,15 +14,12 @@ import { TypebotContext } from 'contexts/TypebotContext'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { KBarProvider } from 'kbar' import { KBarProvider } from 'kbar'
import { actions } from 'libs/kbar' import { actions } from 'libs/kbar'
import { enableMocks } from 'mocks'
import { SupportBubble } from 'components/shared/SupportBubble' import { SupportBubble } from 'components/shared/SupportBubble'
import { WorkspaceContext } from 'contexts/WorkspaceContext' import { WorkspaceContext } from 'contexts/WorkspaceContext'
import { toTitleCase } from 'utils' import { toTitleCase } from 'utils'
const { ToastContainer, toast } = createStandaloneToast(customTheme) const { ToastContainer, toast } = createStandaloneToast(customTheme)
if (process.env.NEXT_PUBLIC_E2E_TEST === 'enabled') enableMocks()
const App = ({ Component, pageProps: { session, ...pageProps } }: AppProps) => { const App = ({ Component, pageProps: { session, ...pageProps } }: AppProps) => {
useRouterProgressBar() useRouterProgressBar()
const { query, pathname, isReady } = useRouter() const { query, pathname, isReady } = useRouter()
@ -70,6 +66,7 @@ const App = ({ Component, pageProps: { session, ...pageProps } }: AppProps) => {
const displayStripeCallbackMessage = ( const displayStripeCallbackMessage = (
status: string | undefined, status: string | undefined,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
toast: any toast: any
) => { ) => {
if (status && ['pro', 'team'].includes(status)) { if (status && ['pro', 'team'].includes(status)) {

View File

@ -15,7 +15,10 @@ import { env, isNotEmpty } from 'utils'
const providers: Provider[] = [] const providers: Provider[] = []
if (isNotEmpty(process.env.GITHUB_CLIENT_ID)) if (
isNotEmpty(process.env.GITHUB_CLIENT_ID) &&
isNotEmpty(process.env.GITHUB_CLIENT_SECRET)
)
providers.push( providers.push(
GitHubProvider({ GitHubProvider({
clientId: process.env.GITHUB_CLIENT_ID, clientId: process.env.GITHUB_CLIENT_ID,

View File

@ -10,7 +10,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const workspaceId = req.query.workspaceId as string | undefined const workspaceId = req.query.workspaceId as string | undefined
if (!workspaceId) return badRequest(res) if (!workspaceId) return badRequest(res)
if (req.method === 'DELETE') { if (req.method === 'DELETE') {
const credentialsId = req.query.credentialsId.toString() const credentialsId = req.query.credentialsId as string | undefined
const credentials = await prisma.credentials.deleteMany({ const credentials = await prisma.credentials.deleteMany({
where: { where: {
id: credentialsId, id: credentialsId,

View File

@ -12,8 +12,10 @@ import { getAuthenticatedUser } from 'services/api/utils'
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)
const state = req.query.state as string | undefined
if (!state) return badRequest(res)
const { redirectUrl, blockId, workspaceId } = JSON.parse( const { redirectUrl, blockId, workspaceId } = JSON.parse(
Buffer.from(req.query.state.toString(), 'base64').toString() Buffer.from(state, 'base64').toString()
) )
if (req.method === 'GET') { if (req.method === 'GET') {
const code = req.query.code as string | undefined const code = req.query.code as string | undefined

View File

@ -11,7 +11,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const workspaceId = req.query.workspaceId as string | undefined const workspaceId = req.query.workspaceId as string | undefined
if (!workspaceId) return badRequest(res) if (!workspaceId) return badRequest(res)
if (req.method === 'DELETE') { if (req.method === 'DELETE') {
const domain = req.query.domain.toString() const domain = req.query.domain as string
try { try {
await deleteDomainOnVercel(domain) await deleteDomainOnVercel(domain)
} catch {} } catch {}

View File

@ -9,7 +9,7 @@ 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)
const id = req.query.id.toString() const id = req.query.id as string
if (req.method === 'GET') { if (req.method === 'GET') {
const folder = await prisma.dashboardFolder.findFirst({ const folder = await prisma.dashboardFolder.findFirst({
where: { where: {

View File

@ -1,6 +1,5 @@
import { NextApiRequest, NextApiResponse } from 'next' import { NextApiRequest, NextApiResponse } from 'next'
import { drive } from '@googleapis/drive' import { drive } from '@googleapis/drive'
import { OAuth2Client } from 'googleapis-common'
import { getAuthenticatedGoogleClient } from 'libs/google-sheets' import { getAuthenticatedGoogleClient } from 'libs/google-sheets'
import { badRequest, methodNotAllowed, notAuthenticated } from 'utils' import { badRequest, methodNotAllowed, notAuthenticated } from 'utils'
import { setUser, withSentry } from '@sentry/nextjs' import { setUser, withSentry } from '@sentry/nextjs'
@ -19,7 +18,9 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
return res.status(404).send("Couldn't find credentials in database") return res.status(404).send("Couldn't find credentials in database")
const response = await drive({ const response = await drive({
version: 'v3', version: 'v3',
auth: auth.client as unknown as OAuth2Client, // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
auth: auth.client,
}).files.list({ }).files.list({
q: "mimeType='application/vnd.google-apps.spreadsheet'", q: "mimeType='application/vnd.google-apps.spreadsheet'",
fields: 'nextPageToken, files(id, name)', fields: 'nextPageToken, files(id, name)',

View File

@ -4,8 +4,8 @@ import { methodNotAllowed } from 'utils'
const handler = (req: NextApiRequest, res: NextApiResponse) => { const handler = (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'POST') { if (req.method === 'POST') {
const firstParam = req.query.firstParam.toString() const firstParam = req.query.firstParam?.toString()
const secondParam = req.query.secondParam.toString() const secondParam = req.query.secondParam?.toString()
const customHeader = req.headers['custom-typebot'] const customHeader = req.headers['custom-typebot']
const { body } = req const { body } = req
if ( if (

View File

@ -1,7 +1,12 @@
import { withSentry } from '@sentry/nextjs' import { withSentry } from '@sentry/nextjs'
import { NextApiRequest, NextApiResponse } from 'next' import { NextApiRequest, NextApiResponse } from 'next'
import { getSession } from 'next-auth/react' import { getAuthenticatedUser } from 'services/api/utils'
import { badRequest, generatePresignedUrl, methodNotAllowed } from 'utils' import {
badRequest,
generatePresignedUrl,
methodNotAllowed,
notAuthenticated,
} from 'utils'
const handler = async ( const handler = async (
req: NextApiRequest, req: NextApiRequest,
@ -9,11 +14,8 @@ const handler = async (
): Promise<void> => { ): Promise<void> => {
res.setHeader('Access-Control-Allow-Origin', '*') res.setHeader('Access-Control-Allow-Origin', '*')
if (req.method === 'GET') { if (req.method === 'GET') {
const session = await getSession({ req }) const user = await getAuthenticatedUser(req)
if (!session) { if (!user) return notAuthenticated(res)
res.status(401)
return
}
if ( if (
!process.env.S3_ENDPOINT || !process.env.S3_ENDPOINT ||

View File

@ -8,7 +8,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (!process.env.STRIPE_SECRET_KEY) if (!process.env.STRIPE_SECRET_KEY)
throw Error('STRIPE_SECRET_KEY var is missing') throw Error('STRIPE_SECRET_KEY var is missing')
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: '2020-08-27', apiVersion: '2022-08-01',
}) })
const { email, currency, plan, workspaceId, href } = const { email, currency, plan, workspaceId, href } =
typeof req.body === 'string' ? JSON.parse(req.body) : req.body typeof req.body === 'string' ? JSON.parse(req.body) : req.body

View File

@ -27,7 +27,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
}) })
if (!workspace?.stripeId) return forbidden(res) if (!workspace?.stripeId) return forbidden(res)
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: '2020-08-27', apiVersion: '2022-08-01',
}) })
const session = await stripe.billingPortal.sessions.create({ const session = await stripe.billingPortal.sessions.create({
customer: workspace.stripeId, customer: workspace.stripeId,

View File

@ -13,7 +13,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (!process.env.STRIPE_SECRET_KEY) if (!process.env.STRIPE_SECRET_KEY)
throw Error('STRIPE_SECRET_KEY var is missing') throw Error('STRIPE_SECRET_KEY var is missing')
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: '2020-08-27', apiVersion: '2022-08-01',
}) })
const subscriptions = await stripe.subscriptions.list({ const subscriptions = await stripe.subscriptions.list({
customer: customerId, customer: customerId,

View File

@ -10,7 +10,7 @@ import { withSentry } from '@sentry/nextjs'
if (!process.env.STRIPE_SECRET_KEY || !process.env.STRIPE_WEBHOOK_SECRET) if (!process.env.STRIPE_SECRET_KEY || !process.env.STRIPE_WEBHOOK_SECRET)
throw new Error('STRIPE_SECRET_KEY or STRIPE_WEBHOOK_SECRET missing') throw new Error('STRIPE_SECRET_KEY or STRIPE_WEBHOOK_SECRET missing')
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: '2020-08-27', apiVersion: '2022-08-01',
}) })
const cors = Cors({ const cors = Cors({

View File

@ -10,7 +10,7 @@ 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)
const typebotId = req.query.typebotId.toString() const typebotId = req.query.typebotId as string
if (req.method === 'GET') { if (req.method === 'GET') {
const typebot = await prisma.typebot.findFirst({ const typebot = await prisma.typebot.findFirst({
where: canReadTypebot(typebotId, user), where: canReadTypebot(typebotId, user),

View File

@ -66,7 +66,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
await prisma.invitation.create({ await prisma.invitation.create({
data: { email: email.toLowerCase().trim(), type, typebotId }, data: { email: email.toLowerCase().trim(), type, typebotId },
}) })
if (env('E2E_TEST') !== 'enabled') if (env('E2E_TEST') !== 'true')
await sendEmailNotification({ await sendEmailNotification({
to: email, to: email,
subject: "You've been invited to collaborate 🤝", subject: "You've been invited to collaborate 🤝",

View File

@ -23,9 +23,9 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
}) })
if (!workspace) return forbidden(res) if (!workspace) return forbidden(res)
if (req.method === 'GET') { if (req.method === 'GET') {
const typebotId = req.query.typebotId.toString() const typebotId = req.query.typebotId as string
const lastResultId = req.query.lastResultId?.toString() const lastResultId = req.query.lastResultId?.toString()
const take = parseInt(req.query.limit?.toString()) const take = Number(req.query.limit?.toString())
const results = await prisma.result.findMany({ const results = await prisma.result.findMany({
take: isNaN(take) ? undefined : take, take: isNaN(take) ? undefined : take,
skip: lastResultId ? 1 : 0, skip: lastResultId ? 1 : 0,

View File

@ -10,7 +10,7 @@ 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 === 'GET') { if (req.method === 'GET') {
const typebotId = req.query.typebotId.toString() const typebotId = req.query.typebotId as string
const typebot = await prisma.typebot.findFirst({ const typebot = await prisma.typebot.findFirst({
where: canReadTypebot(typebotId, user), where: canReadTypebot(typebotId, user),
include: { publishedTypebot: true }, include: { publishedTypebot: true },

View File

@ -10,7 +10,7 @@ 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 === 'GET') { if (req.method === 'GET') {
const typebotId = req.query.typebotId.toString() const typebotId = req.query.typebotId as string
const totalViews = await prisma.result.count({ const totalViews = await prisma.result.count({
where: { where: {

View File

@ -8,7 +8,7 @@ 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)
const id = req.query.userId.toString() const id = req.query.userId as string
if (req.method === 'PUT') { if (req.method === 'PUT') {
const data = typeof req.body === 'string' ? JSON.parse(req.body) : req.body const data = typeof req.body === 'string' ? JSON.parse(req.body) : req.body
const typebots = await prisma.user.update({ const typebots = await prisma.user.update({

View File

@ -9,7 +9,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (!user) return notAuthenticated(res) if (!user) return notAuthenticated(res)
if (req.method === 'DELETE') { if (req.method === 'DELETE') {
const id = req.query.tokenId.toString() const id = req.query.tokenId as string
const apiToken = await prisma.apiToken.delete({ const apiToken = await prisma.apiToken.delete({
where: { id }, where: { id },
}) })

View File

@ -1,11 +1,10 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { Stack, Text, VStack } from '@chakra-ui/layout'
import { DashboardHeader } from 'components/dashboard/DashboardHeader' import { DashboardHeader } from 'components/dashboard/DashboardHeader'
import { Seo } from 'components/Seo' import { Seo } from 'components/Seo'
import { FolderContent } from 'components/dashboard/FolderContent' import { FolderContent } from 'components/dashboard/FolderContent'
import { TypebotDndContext } from 'contexts/TypebotDndContext' import { TypebotDndContext } from 'contexts/TypebotDndContext'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { Spinner } from '@chakra-ui/react' import { Spinner, Stack, Text, VStack } from '@chakra-ui/react'
import { pay } from 'services/stripe' import { pay } from 'services/stripe'
import { useUser } from 'contexts/UserContext' import { useUser } from 'contexts/UserContext'
import { NextPageContext } from 'next/types' import { NextPageContext } from 'next/types'

View File

@ -1,4 +1,3 @@
import { Flex } from '@chakra-ui/layout'
import { Seo } from 'components/Seo' import { Seo } from 'components/Seo'
import { TypebotHeader } from 'components/shared/TypebotHeader' import { TypebotHeader } from 'components/shared/TypebotHeader'
import { import {
@ -16,7 +15,7 @@ import { GraphProvider, GroupsCoordinatesProvider } from 'contexts/GraphContext'
import { GraphDndContext } from 'contexts/GraphDndContext' import { GraphDndContext } from 'contexts/GraphDndContext'
import { useTypebot } from 'contexts/TypebotContext' import { useTypebot } from 'contexts/TypebotContext'
import { GettingStartedModal } from 'components/editor/GettingStartedModal' import { GettingStartedModal } from 'components/editor/GettingStartedModal'
import { Spinner } from '@chakra-ui/react' import { Spinner, Flex } from '@chakra-ui/react'
const TypebotEditPage = () => { const TypebotEditPage = () => {
const { typebot, isReadOnly } = useTypebot() const { typebot, isReadOnly } = useTypebot()

View File

@ -1,8 +1,7 @@
import { Flex, Text } from '@chakra-ui/layout'
import { Seo } from 'components/Seo' import { Seo } from 'components/Seo'
import { TypebotHeader } from 'components/shared/TypebotHeader' import { TypebotHeader } from 'components/shared/TypebotHeader'
import React, { useMemo } from 'react' import React, { useMemo } from 'react'
import { HStack, Button, Tag } from '@chakra-ui/react' import { HStack, Button, Tag, Flex, Text } from '@chakra-ui/react'
import { NextChakraLink } from 'components/nextChakra/NextChakraLink' import { NextChakraLink } from 'components/nextChakra/NextChakraLink'
import { ResultsContent } from 'components/results/ResultsContent' import { ResultsContent } from 'components/results/ResultsContent'
import { useTypebot } from 'contexts/TypebotContext' import { useTypebot } from 'contexts/TypebotContext'
@ -34,7 +33,7 @@ const ResultsPage = () => {
stats: { ...stats, totalStarts: stats.totalStarts - total }, stats: { ...stats, totalStarts: stats.totalStarts - total },
}) })
} }
return ( return (
<Flex overflow="hidden" h="100vh" flexDir="column"> <Flex overflow="hidden" h="100vh" flexDir="column">
<Seo <Seo

View File

@ -1,4 +1,4 @@
import { Flex } from '@chakra-ui/layout' import { Flex } from '@chakra-ui/react'
import { Seo } from 'components/Seo' import { Seo } from 'components/Seo'
import { SettingsContent } from 'layouts/settings/SettingsContent' import { SettingsContent } from 'layouts/settings/SettingsContent'
import { TypebotHeader } from 'components/shared/TypebotHeader' import { TypebotHeader } from 'components/shared/TypebotHeader'

View File

@ -1,4 +1,4 @@
import { Flex } from '@chakra-ui/layout' import { Flex } from '@chakra-ui/react'
import { Seo } from 'components/Seo' import { Seo } from 'components/Seo'
import { ShareContent } from 'components/share/ShareContent' import { ShareContent } from 'components/share/ShareContent'
import { TypebotHeader } from 'components/shared/TypebotHeader' import { TypebotHeader } from 'components/shared/TypebotHeader'

View File

@ -1,4 +1,4 @@
import { Flex } from '@chakra-ui/layout' import { Flex } from '@chakra-ui/react'
import { Seo } from 'components/Seo' import { Seo } from 'components/Seo'
import { TypebotHeader } from 'components/shared/TypebotHeader' import { TypebotHeader } from 'components/shared/TypebotHeader'
import { ThemeContent } from 'layouts/theme/ThemeContent' import { ThemeContent } from 'layouts/theme/ThemeContent'

View File

@ -1,5 +1,5 @@
import React from 'react' import React from 'react'
import { Flex, Stack } from '@chakra-ui/layout' import { Flex, Stack } from '@chakra-ui/react'
import { DashboardHeader } from 'components/dashboard/DashboardHeader' import { DashboardHeader } from 'components/dashboard/DashboardHeader'
import { Seo } from 'components/Seo' import { Seo } from 'components/Seo'
import { FolderContent } from 'components/dashboard/FolderContent' import { FolderContent } from 'components/dashboard/FolderContent'

View File

@ -5,8 +5,6 @@ import path from 'path'
require('dotenv').config({ require('dotenv').config({
path: path.join(__dirname, 'playwright/.env'), path: path.join(__dirname, 'playwright/.env'),
}) })
// eslint-disable-next-line @typescript-eslint/no-var-requires
require('dotenv').config({ path: path.join(__dirname, '.env.local') })
const config: PlaywrightTestConfig = { const config: PlaywrightTestConfig = {
globalSetup: require.resolve(path.join(__dirname, 'playwright/global-setup')), globalSetup: require.resolve(path.join(__dirname, 'playwright/global-setup')),

View File

@ -1,4 +1,6 @@
PLAYWRIGHT_BUILDER_TEST_BASE_URL=http://localhost:3000 PLAYWRIGHT_BUILDER_TEST_BASE_URL=http://localhost:3000
DATABASE_URL=postgresql://postgres:typebot@localhost:5432/typebot
ENCRYPTION_SECRET=SgVkYp2s5v8y/B?E(H+MbQeThWmZq4t6 #256-bits secret (can be generated here: https://www.allkeysgenerator.com/Random/Security-Encryption-Key-Generator.aspx)
# SMTP Credentials (Generated on https://ethereal.email/) # SMTP Credentials (Generated on https://ethereal.email/)
SMTP_HOST=smtp.ethereal.email SMTP_HOST=smtp.ethereal.email

View File

@ -1,5 +1,18 @@
import { Page } from '@playwright/test'
export const refreshUser = async () => { export const refreshUser = async () => {
await fetch('/api/auth/session?update') await fetch('/api/auth/session?update')
const event = new Event('visibilitychange') const event = new Event('visibilitychange')
document.dispatchEvent(event) document.dispatchEvent(event)
} }
export const mockSessionApiCalls = (page: Page) =>
page.route('/api/auth/session', (route) => {
if (route.request().method() === 'GET') {
return route.fulfill({
status: 200,
body: '{"user":{"id":"proUser","name":"Pro user","email":"pro-user@email.com","emailVerified":null,"image":"https://avatars.githubusercontent.com/u/16015833?v=4","stripeId":null,"graphNavigation": "TRACKPAD"}}',
})
}
return route.continue()
})

View File

@ -1,5 +1,8 @@
import test, { expect } from '@playwright/test' import test, { expect } from '@playwright/test'
import path from 'path' import path from 'path'
import { mockSessionApiCalls } from 'playwright/services/browser'
test.beforeEach(({ page }) => mockSessionApiCalls(page))
// Can't test the update features because of the auth mocking. // Can't test the update features because of the auth mocking.
test('should display user info properly', async ({ page }) => { test('should display user info properly', async ({ page }) => {
@ -7,7 +10,7 @@ test('should display user info properly', async ({ page }) => {
await page.click('text=Settings & Members') await page.click('text=Settings & Members')
const saveButton = page.locator('button:has-text("Save")') const saveButton = page.locator('button:has-text("Save")')
await expect(saveButton).toBeHidden() await expect(saveButton).toBeHidden()
await expect( expect(
page.locator('input[type="email"]').getAttribute('disabled') page.locator('input[type="email"]').getAttribute('disabled')
).toBeDefined() ).toBeDefined()
await page.fill('#name', 'John Doe') await page.fill('#name', 'John Doe')

View File

@ -6,11 +6,13 @@ import {
import { BubbleBlockType, defaultEmbedBubbleContent } from 'models' import { BubbleBlockType, defaultEmbedBubbleContent } from 'models'
import { typebotViewer } from '../../services/selectorUtils' import { typebotViewer } from '../../services/selectorUtils'
import cuid from 'cuid' import cuid from 'cuid'
import { mockSessionApiCalls } from 'playwright/services/browser'
const pdfSrc = 'https://www.orimi.com/pdf-test.pdf' const pdfSrc = 'https://www.orimi.com/pdf-test.pdf'
const iframeCode = '<iframe src="https://typebot.io"></iframe>'
const siteSrc = 'https://app.cal.com/baptistearno/15min' const siteSrc = 'https://app.cal.com/baptistearno/15min'
test.beforeEach(({ page }) => mockSessionApiCalls(page))
test.describe.parallel('Embed bubble block', () => { test.describe.parallel('Embed bubble block', () => {
test.describe('Content settings', () => { test.describe('Content settings', () => {
test('should import and parse embed correctly', async ({ page }) => { test('should import and parse embed correctly', async ({ page }) => {

View File

@ -7,10 +7,13 @@ import { BubbleBlockType, defaultImageBubbleContent } from 'models'
import { typebotViewer } from '../../services/selectorUtils' import { typebotViewer } from '../../services/selectorUtils'
import path from 'path' import path from 'path'
import cuid from 'cuid' import cuid from 'cuid'
import { mockSessionApiCalls } from 'playwright/services/browser'
const unsplashImageSrc = const unsplashImageSrc =
'https://images.unsplash.com/photo-1504297050568-910d24c426d3?ixlib=rb-1.2.1&ixid=MnwxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1287&q=80' 'https://images.unsplash.com/photo-1504297050568-910d24c426d3?ixlib=rb-1.2.1&ixid=MnwxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1287&q=80'
test.beforeEach(({ page }) => mockSessionApiCalls(page))
test.describe.parallel('Image bubble block', () => { test.describe.parallel('Image bubble block', () => {
test.describe('Content settings', () => { test.describe('Content settings', () => {
test('should upload image file correctly', async ({ page }) => { test('should upload image file correctly', async ({ page }) => {

View File

@ -6,6 +6,9 @@ import {
import { BubbleBlockType, defaultTextBubbleContent } from 'models' import { BubbleBlockType, defaultTextBubbleContent } from 'models'
import { typebotViewer } from '../../services/selectorUtils' import { typebotViewer } from '../../services/selectorUtils'
import cuid from 'cuid' import cuid from 'cuid'
import { mockSessionApiCalls } from 'playwright/services/browser'
test.beforeEach(({ page }) => mockSessionApiCalls(page))
test.describe('Text bubble block', () => { test.describe('Text bubble block', () => {
test('rich text features should work', async ({ page }) => { test('rich text features should work', async ({ page }) => {
@ -42,15 +45,13 @@ test.describe('Text bubble block', () => {
await page.press('div[role="textbox"]', 'Shift+Enter') await page.press('div[role="textbox"]', 'Shift+Enter')
await page.type('div[role="textbox"]', 'My super link') await page.type('div[role="textbox"]', 'My super link')
await page.waitForTimeout(300)
await page.press('div[role="textbox"]', 'Shift+Meta+ArrowLeft') await page.press('div[role="textbox"]', 'Shift+Meta+ArrowLeft')
await page.waitForTimeout(200)
page.on('dialog', async (dialog) => {
await dialog.accept('https://github.com')
})
await page.click('[data-testid="link-button"]') await page.click('[data-testid="link-button"]')
await page.fill('input[placeholder="Paste link"]', 'https://github.com')
await page.press('input[placeholder="Paste link"]', 'Enter')
await page.press('div[role="textbox"]', 'Shift+Enter') await page.press('div[role="textbox"]', 'Shift+Enter')
await page.click('button >> text=Variables') await page.click('button[aria-label="Insert variable"]')
await page.fill('[data-testid="variables-input"]', 'test') await page.fill('[data-testid="variables-input"]', 'test')
await page.click('text=Create "test"') await page.click('text=Create "test"')

View File

@ -10,12 +10,15 @@ import {
} from 'models' } from 'models'
import { typebotViewer } from '../../services/selectorUtils' import { typebotViewer } from '../../services/selectorUtils'
import cuid from 'cuid' import cuid from 'cuid'
import { mockSessionApiCalls } from 'playwright/services/browser'
const videoSrc = const videoSrc =
'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4' 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4'
const youtubeVideoSrc = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ' const youtubeVideoSrc = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'
const vimeoVideoSrc = 'https://vimeo.com/649301125' const vimeoVideoSrc = 'https://vimeo.com/649301125'
test.beforeEach(({ page }) => mockSessionApiCalls(page))
test.describe.parallel('Video bubble block', () => { test.describe.parallel('Video bubble block', () => {
test.describe('Content settings', () => { test.describe('Content settings', () => {
test('should import video url correctly', async ({ page }) => { test('should import video url correctly', async ({ page }) => {

View File

@ -3,6 +3,7 @@ import cuid from 'cuid'
import { CollaborationType, Plan, WorkspaceRole } from 'db' import { CollaborationType, Plan, WorkspaceRole } from 'db'
import prisma from 'libs/prisma' import prisma from 'libs/prisma'
import { InputBlockType, defaultTextInputOptions } from 'models' import { InputBlockType, defaultTextInputOptions } from 'models'
import { mockSessionApiCalls } from 'playwright/services/browser'
import { import {
createFolder, createFolder,
createResults, createResults,
@ -10,6 +11,8 @@ import {
parseDefaultGroupWithBlock, parseDefaultGroupWithBlock,
} from '../services/database' } from '../services/database'
test.beforeEach(({ page }) => mockSessionApiCalls(page))
test.describe('Typebot owner', () => { test.describe('Typebot owner', () => {
test('Can invite collaborators', async ({ page }) => { test('Can invite collaborators', async ({ page }) => {
const typebotId = cuid() const typebotId = cuid()

View File

@ -7,6 +7,9 @@ import {
} from '../services/database' } from '../services/database'
import path from 'path' import path from 'path'
import cuid from 'cuid' import cuid from 'cuid'
import { mockSessionApiCalls } from 'playwright/services/browser'
test.beforeEach(({ page }) => mockSessionApiCalls(page))
test('should be able to connect custom domain', async ({ page }) => { test('should be able to connect custom domain', async ({ page }) => {
const typebotId = cuid() const typebotId = cuid()

View File

@ -1,9 +1,12 @@
import test, { expect, Page } from '@playwright/test' import test, { expect, Page } from '@playwright/test'
import cuid from 'cuid' import cuid from 'cuid'
import path from 'path' import path from 'path'
import { mockSessionApiCalls } from 'playwright/services/browser'
import { createFolders, createTypebots } from '../services/database' import { createFolders, createTypebots } from '../services/database'
import { deleteButtonInConfirmDialog } from '../services/selectorUtils' import { deleteButtonInConfirmDialog } from '../services/selectorUtils'
test.beforeEach(({ page }) => mockSessionApiCalls(page))
test.describe('Dashboard page', () => { test.describe('Dashboard page', () => {
test('folders navigation should work', async ({ page }) => { test('folders navigation should work', async ({ page }) => {
await page.goto('/typebots') await page.goto('/typebots')

View File

@ -8,6 +8,9 @@ import { defaultTextInputOptions, InputBlockType } from 'models'
import path from 'path' import path from 'path'
import cuid from 'cuid' import cuid from 'cuid'
import { typebotViewer } from '../services/selectorUtils' import { typebotViewer } from '../services/selectorUtils'
import { mockSessionApiCalls } from 'playwright/services/browser'
test.beforeEach(({ page }) => mockSessionApiCalls(page))
test.describe.parallel('Editor', () => { test.describe.parallel('Editor', () => {
test('Edges connection should work', async ({ page }) => { test('Edges connection should work', async ({ page }) => {

View File

@ -8,6 +8,9 @@ import { defaultChoiceInputOptions, InputBlockType, ItemType } from 'models'
import { typebotViewer } from '../../services/selectorUtils' import { typebotViewer } from '../../services/selectorUtils'
import cuid from 'cuid' import cuid from 'cuid'
import path from 'path' import path from 'path'
import { mockSessionApiCalls } from 'playwright/services/browser'
test.beforeEach(({ page }) => mockSessionApiCalls(page))
test.describe.parallel('Buttons input block', () => { test.describe.parallel('Buttons input block', () => {
test('can edit button items', async ({ page }) => { test('can edit button items', async ({ page }) => {

View File

@ -6,6 +6,9 @@ import {
import { defaultDateInputOptions, InputBlockType } from 'models' import { defaultDateInputOptions, InputBlockType } from 'models'
import { typebotViewer } from '../../services/selectorUtils' import { typebotViewer } from '../../services/selectorUtils'
import cuid from 'cuid' import cuid from 'cuid'
import { mockSessionApiCalls } from 'playwright/services/browser'
test.beforeEach(({ page }) => mockSessionApiCalls(page))
test.describe('Date input block', () => { test.describe('Date input block', () => {
test('options should work', async ({ page }) => { test('options should work', async ({ page }) => {

View File

@ -6,6 +6,9 @@ import {
import { defaultEmailInputOptions, InputBlockType } from 'models' import { defaultEmailInputOptions, InputBlockType } from 'models'
import { typebotViewer } from '../../services/selectorUtils' import { typebotViewer } from '../../services/selectorUtils'
import cuid from 'cuid' import cuid from 'cuid'
import { mockSessionApiCalls } from 'playwright/services/browser'
test.beforeEach(({ page }) => mockSessionApiCalls(page))
test.describe('Email input block', () => { test.describe('Email input block', () => {
test('options should work', async ({ page }) => { test('options should work', async ({ page }) => {
@ -31,7 +34,10 @@ test.describe('Email input block', () => {
await expect(typebotViewer(page).locator(`button`)).toBeDisabled() await expect(typebotViewer(page).locator(`button`)).toBeDisabled()
await page.click(`text=${defaultEmailInputOptions.labels.placeholder}`) await page.click(`text=${defaultEmailInputOptions.labels.placeholder}`)
await page.fill('#placeholder', 'Your email...') await page.fill(
`input[value="${defaultEmailInputOptions.labels.placeholder}"]`,
'Your email...'
)
await expect(page.locator('text=Your email...')).toBeVisible() await expect(page.locator('text=Your email...')).toBeVisible()
await page.fill('#button', 'Go') await page.fill('#button', 'Go')
await page.fill( await page.fill(

View File

@ -8,6 +8,9 @@ import { defaultFileInputOptions, InputBlockType } from 'models'
import { typebotViewer } from '../../services/selectorUtils' import { typebotViewer } from '../../services/selectorUtils'
import cuid from 'cuid' import cuid from 'cuid'
import path from 'path' import path from 'path'
import { mockSessionApiCalls } from 'playwright/services/browser'
test.beforeEach(({ page }) => mockSessionApiCalls(page))
test.describe.configure({ mode: 'parallel' }) test.describe.configure({ mode: 'parallel' })

View File

@ -6,6 +6,9 @@ import {
import { defaultNumberInputOptions, InputBlockType } from 'models' import { defaultNumberInputOptions, InputBlockType } from 'models'
import { typebotViewer } from '../../services/selectorUtils' import { typebotViewer } from '../../services/selectorUtils'
import cuid from 'cuid' import cuid from 'cuid'
import { mockSessionApiCalls } from 'playwright/services/browser'
test.beforeEach(({ page }) => mockSessionApiCalls(page))
test.describe('Number input block', () => { test.describe('Number input block', () => {
test('options should work', async ({ page }) => { test('options should work', async ({ page }) => {

View File

@ -6,6 +6,9 @@ import {
import { defaultPaymentInputOptions, InputBlockType } from 'models' import { defaultPaymentInputOptions, InputBlockType } from 'models'
import cuid from 'cuid' import cuid from 'cuid'
import { stripePaymentForm, typebotViewer } from '../../services/selectorUtils' import { stripePaymentForm, typebotViewer } from '../../services/selectorUtils'
import { mockSessionApiCalls } from 'playwright/services/browser'
test.beforeEach(({ page }) => mockSessionApiCalls(page))
test.describe('Payment input block', () => { test.describe('Payment input block', () => {
test('Can configure Stripe account', async ({ page }) => { test('Can configure Stripe account', async ({ page }) => {

Some files were not shown because too many files have changed in this diff Show More