diff --git a/.env.example b/.env.example index 8b7d5d535d..88c118659e 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ -# Make sure to change this to your own random string of 32 characters (https://docs.typebot.io/self-hosting/docker#2-add-the-required-configuration) +# Make sure to change this to your own random string of 32 characters (https://docs.typebot.io/self-hosting/deploy/docker#2-add-the-required-configuration) ENCRYPTION_SECRET=do+UspMmB/rewbX2K/rskFmtgGSSZ8Ta DATABASE_URL=postgresql://postgres:typebot@typebot-db:5432/typebot @@ -9,4 +9,4 @@ NEXTAUTH_URL= NEXT_PUBLIC_VIEWER_URL= ADMIN_EMAIL= -# For more configuration options check out: https://docs.typebot.io/self-hosting/configuration \ No newline at end of file +# For more configuration options check out: https://docs.typebot.io/self-hosting/configuration diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 3c3629e647..0000000000 --- a/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -node_modules diff --git a/.github/workflows/check-and-report-chats-usage.yml b/.github/workflows/check-and-report-chats-usage.yml index e98eb0afd8..9708fde9d7 100644 --- a/.github/workflows/check-and-report-chats-usage.yml +++ b/.github/workflows/check-and-report-chats-usage.yml @@ -2,7 +2,7 @@ name: Check and report chats usage on: schedule: - - cron: '0 * * * *' + - cron: "0 * * * *" jobs: send: @@ -11,24 +11,24 @@ jobs: run: working-directory: ./packages/scripts env: - DATABASE_URL: '${{ secrets.DATABASE_URL }}' - ENCRYPTION_SECRET: '${{ secrets.ENCRYPTION_SECRET }}' - NEXTAUTH_URL: '${{ secrets.NEXTAUTH_URL }}' - NEXT_PUBLIC_VIEWER_URL: '${{ secrets.NEXT_PUBLIC_VIEWER_URL }}' - NEXT_PUBLIC_POSTHOG_KEY: '${{ secrets.NEXT_PUBLIC_POSTHOG_KEY }}' - NEXT_PUBLIC_POSTHOG_HOST: '${{ secrets.NEXT_PUBLIC_POSTHOG_HOST }}' - SMTP_USERNAME: '${{ secrets.SMTP_USERNAME }}' - SMTP_PASSWORD: '${{ secrets.SMTP_PASSWORD }}' - SMTP_HOST: '${{ secrets.SMTP_HOST }}' - SMTP_PORT: '${{ secrets.SMTP_PORT }}' - NEXT_PUBLIC_SMTP_FROM: '${{ secrets.NEXT_PUBLIC_SMTP_FROM }}' - STRIPE_SECRET_KEY: '${{ secrets.STRIPE_SECRET_KEY }}' - STRIPE_STARTER_PRICE_ID: '${{ secrets.STRIPE_STARTER_PRICE_ID }}' - STRIPE_STARTER_CHATS_PRICE_ID: '${{ secrets.STRIPE_STARTER_CHATS_PRICE_ID }}' - STRIPE_PRO_PRICE_ID: '${{ secrets.STRIPE_PRO_PRICE_ID }}' - STRIPE_PRO_CHATS_PRICE_ID: '${{ secrets.STRIPE_PRO_CHATS_PRICE_ID }}' + DATABASE_URL: "${{ secrets.DATABASE_URL }}" + ENCRYPTION_SECRET: "${{ secrets.ENCRYPTION_SECRET }}" + NEXTAUTH_URL: "${{ secrets.NEXTAUTH_URL }}" + NEXT_PUBLIC_VIEWER_URL: "${{ secrets.NEXT_PUBLIC_VIEWER_URL }}" + NEXT_PUBLIC_POSTHOG_KEY: "${{ secrets.NEXT_PUBLIC_POSTHOG_KEY }}" + NEXT_PUBLIC_POSTHOG_HOST: "${{ secrets.NEXT_PUBLIC_POSTHOG_HOST }}" + SMTP_USERNAME: "${{ secrets.SMTP_USERNAME }}" + SMTP_PASSWORD: "${{ secrets.SMTP_PASSWORD }}" + SMTP_HOST: "${{ secrets.SMTP_HOST }}" + SMTP_PORT: "${{ secrets.SMTP_PORT }}" + NEXT_PUBLIC_SMTP_FROM: "${{ secrets.NEXT_PUBLIC_SMTP_FROM }}" + STRIPE_SECRET_KEY: "${{ secrets.STRIPE_SECRET_KEY }}" + STRIPE_STARTER_PRICE_ID: "${{ secrets.STRIPE_STARTER_PRICE_ID }}" + STRIPE_STARTER_CHATS_PRICE_ID: "${{ secrets.STRIPE_STARTER_CHATS_PRICE_ID }}" + STRIPE_PRO_PRICE_ID: "${{ secrets.STRIPE_PRO_PRICE_ID }}" + STRIPE_PRO_CHATS_PRICE_ID: "${{ secrets.STRIPE_PRO_CHATS_PRICE_ID }}" steps: - uses: actions/checkout@v2 - - uses: pnpm/action-setup@v4 - - run: pnpm i --frozen-lockfile - - run: pnpm turbo run checkAndReportChatsUsage + - uses: oven-sh/setup-bun@v2 + - run: bun install + - run: bunx turbo run checkAndReportChatsUsage diff --git a/.github/workflows/clean-database.yml b/.github/workflows/clean-database.yml index 1707d8b52c..c1981066c8 100644 --- a/.github/workflows/clean-database.yml +++ b/.github/workflows/clean-database.yml @@ -2,7 +2,7 @@ name: Daily database cleanup on: schedule: - - cron: '0 6 * * *' + - cron: "0 6 * * *" jobs: clean: @@ -11,12 +11,12 @@ jobs: run: working-directory: ./packages/scripts env: - DATABASE_URL: '${{ secrets.DATABASE_URL }}' - ENCRYPTION_SECRET: '${{ secrets.ENCRYPTION_SECRET }}' - NEXTAUTH_URL: 'http://localhost:3000' - NEXT_PUBLIC_VIEWER_URL: 'http://localhost:3001' + DATABASE_URL: "${{ secrets.DATABASE_URL }}" + ENCRYPTION_SECRET: "${{ secrets.ENCRYPTION_SECRET }}" + NEXTAUTH_URL: "http://localhost:3000" + NEXT_PUBLIC_VIEWER_URL: "http://localhost:3001" steps: - uses: actions/checkout@v2 - - uses: pnpm/action-setup@v4 - - run: pnpm i --frozen-lockfile - - run: pnpm turbo run db:cleanDatabase + - uses: oven-sh/setup-bun@v2 + - run: bun install + - run: bunx turbo run db:cleanDatabase diff --git a/.github/workflows/publish-lib-to-npm.yml b/.github/workflows/publish-lib-to-npm.yml index c954579ff2..32583297a0 100644 --- a/.github/workflows/publish-lib-to-npm.yml +++ b/.github/workflows/publish-lib-to-npm.yml @@ -3,7 +3,7 @@ name: Publish typebot-js to NPM on: push: tags: - - 'js-lib-v*' + - "js-lib-v*" jobs: publish: @@ -12,12 +12,12 @@ jobs: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} steps: - uses: actions/checkout@v2 - - uses: pnpm/action-setup@v4 - - run: pnpm i --frozen-lockfile - - run: pnpm turbo build --filter=typebot-js... + - uses: oven-sh/setup-bun@v2 + - run: bun install + - run: bunx turbo build --filter=typebot-js... - name: Set NPM_TOKEN in config - run: pnpm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN} + run: bun run npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN} working-directory: ./packages/typebot-js - name: Publish - run: pnpm publish --no-git-checks --access public + run: bun run npm publish --no-git-checks --access public working-directory: ./packages/typebot-js diff --git a/.github/workflows/publish-typebot-js.yml b/.github/workflows/publish-typebot-js.yml index 0fa80e3a79..5d51d564de 100644 --- a/.github/workflows/publish-typebot-js.yml +++ b/.github/workflows/publish-typebot-js.yml @@ -3,7 +3,7 @@ name: Publish @typebot.io/js package to NPM on: push: tags: - - 'js-v*' + - "js-v*" jobs: publish: @@ -12,7 +12,7 @@ jobs: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} steps: - uses: actions/checkout@v2 - - uses: pnpm/action-setup@v4 - - run: pnpm i --frozen-lockfile - - run: pnpm turbo build --filter=@typebot.io/js... - - run: cd packages/embeds/js && pnpm publish --no-git-checks --access public + - uses: oven-sh/setup-bun@v2 + - run: bun install + - run: bunx turbo build --filter=@typebot.io/js... + - run: cd packages/embeds/js && bun run npm publish --no-git-checks --access public diff --git a/.github/workflows/publish-typebot-nextjs.yml b/.github/workflows/publish-typebot-nextjs.yml index 22ab3c78df..69e9a29917 100644 --- a/.github/workflows/publish-typebot-nextjs.yml +++ b/.github/workflows/publish-typebot-nextjs.yml @@ -3,7 +3,7 @@ name: Publish @typebot.io/nextjs package to NPM on: push: tags: - - 'nextjs-v*' + - "nextjs-v*" jobs: publish: @@ -12,7 +12,7 @@ jobs: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} steps: - uses: actions/checkout@v2 - - uses: pnpm/action-setup@v4 - - run: pnpm i --frozen-lockfile - - run: pnpm turbo build --filter=@typebot.io/nextjs... - - run: cd packages/embeds/nextjs && pnpm publish --no-git-checks --access public + - uses: oven-sh/setup-bun@v2 + - run: bun install + - run: bunx turbo build --filter=@typebot.io/nextjs... + - run: cd packages/embeds/nextjs && bun run npm publish --no-git-checks --access public diff --git a/.github/workflows/publish-typebot-react.yml b/.github/workflows/publish-typebot-react.yml index cc2c000b4b..b74ea4b94c 100644 --- a/.github/workflows/publish-typebot-react.yml +++ b/.github/workflows/publish-typebot-react.yml @@ -3,7 +3,7 @@ name: Publish @typebot.io/react package to NPM on: push: tags: - - 'react-v*' + - "react-v*" jobs: publish: @@ -12,7 +12,7 @@ jobs: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} steps: - uses: actions/checkout@v2 - - uses: pnpm/action-setup@v4 - - run: pnpm i --frozen-lockfile - - run: pnpm turbo build --filter=@typebot.io/react... - - run: cd packages/embeds/react && pnpm publish --no-git-checks --access public + - uses: oven-sh/setup-bun@v2 + - run: bun install + - run: bunx turbo build --filter=@typebot.io/react... + - run: cd packages/embeds/react && bun run npm publish --no-git-checks --access public diff --git a/.gitignore b/.gitignore index a873897ace..f2fc961a71 100644 --- a/.gitignore +++ b/.gitignore @@ -40,4 +40,10 @@ snapshots .env .typebot-build -.tolgee \ No newline at end of file +.tolgee + +*.tsbuildinfo + +.tsup + +.env.docker diff --git a/.husky/pre-commit b/.husky/pre-commit old mode 100755 new mode 100644 index ffd1860fff..52c7534c80 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1 @@ -#!/usr/bin/env sh -. "$(dirname -- "$0")/_/husky.sh" - -pnpm lint && pnpm format:check +bun pre-commit diff --git a/.pnpmfile.cjs b/.pnpmfile.cjs deleted file mode 100644 index 9394b843b3..0000000000 --- a/.pnpmfile.cjs +++ /dev/null @@ -1,18 +0,0 @@ -module.exports = { - hooks: { - readPackage(pkg) { - // Solving weird issue: https://github.com/facebook/docusaurus/issues/6724#issuecomment-1188794031 - if (pkg.name != 'docs') { - const deps = [ - '@algolia/client-search', - '@docusaurus/core', - '@docusaurus/preset-classic', - '@docusaurus/theme-common', - '@docusaurus/theme-live-codeblock', - ] - deps.forEach((p) => delete pkg.dependencies[p]) - } - return pkg - }, - }, -} diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index c6fdd0e101..0000000000 --- a/.prettierignore +++ /dev/null @@ -1,4 +0,0 @@ -emojiList.json -iconNames.ts -reporters -.last-run.json diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index fa51da29e7..0000000000 --- a/.prettierrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "trailingComma": "es5", - "tabWidth": 2, - "semi": false, - "singleQuote": true -} diff --git a/.prototools b/.prototools new file mode 100644 index 0000000000..07f53fc654 --- /dev/null +++ b/.prototools @@ -0,0 +1,2 @@ +bun = "1.1.29" +node = "20.17.0" diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 700a6d2508..c0961dfb10 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,10 +1,8 @@ { "recommendations": [ - "esbenp.prettier-vscode", - "dbaeumer.vscode-eslint", "bradlc.vscode-tailwindcss", "unifiedjs.vscode-mdx", - "lokalise.i18n-ally", - "Prisma.prisma" + "Prisma.prisma", + "biomejs.biome" ] } diff --git a/.vscode/i18n-ally-custom-framework.yml b/.vscode/i18n-ally-custom-framework.yml deleted file mode 100644 index d24633e202..0000000000 --- a/.vscode/i18n-ally-custom-framework.yml +++ /dev/null @@ -1,27 +0,0 @@ -# An array of strings which contain Language Ids defined by VS Code -# You can check available language ids here: https://code.visualstudio.com/docs/languages/overview#_language-id -languageIds: - - javascript - - typescript - - javascriptreact - - typescriptreact - -# An array of RegExes to find the key usage. **The key should be captured in the first match group**. -# You should unescape RegEx strings in order to fit in the YAML file -# To help with this, you can use https://www.freeformatter.com/json-escape.html -usageMatchRegex: - # The following example shows how to detect `t("your.i18n.keys")` - # the `{key}` will be placed by a proper keypath matching regex, - # you can ignore it and use your own matching rules as well - - "[^\\w\\d]t\\([\\s\\n]*'({key})'" - - 'keyName="({key})"' - -# An array of strings containing refactor templates. -# The "$1" will be replaced by the keypath specified. -# Optional: uncomment the following two lines to use - -refactorTemplates: - - t("$1") - -# If set to true, only enables this custom framework (will disable all built-in frameworks) -monopoly: true diff --git a/.vscode/settings.json b/.vscode/settings.json index 0bb6069e24..2382dcc7be 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,17 +1,26 @@ { - "i18n-ally.localesPaths": ["apps/builder/src/i18n"], - "i18n-ally.keystyle": "flat", - "i18n-ally.displayLanguage": "en", - "i18n-ally.enabledFrameworks": ["custom"], - "i18n-ally.sortKeys": true, - "editor.codeActionsOnSave": { - "source.fixAll.eslint": "explicit" - }, - "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true, "editor.tabSize": 2, "typescript.updateImportsOnFileMove.enabled": "always", - "[prisma]": { - "editor.defaultFormatter": "Prisma.prisma" - } + "typescript.tsdk": "node_modules/typescript/lib", + "typescript.preferences.autoImportFileExcludePatterns": [ + "next/router.d.ts", + "next/dist/client/router.d.ts" + ], + "editor.defaultFormatter": "biomejs.biome", + "[javascript]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[typescript]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "editor.codeActionsOnSave": { + "quickfix.biome": "explicit", + "source.organizeImports.biome": "explicit" + }, + "eslint.enable": false, + "prettier.enable": false } diff --git a/Dockerfile b/Dockerfile index afcba1dfc1..483bb55929 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,60 +1,116 @@ -FROM node:20-bullseye-slim AS base -WORKDIR /app -ARG SCOPE -ENV SCOPE=${SCOPE} -RUN apt-get -qy update \ - && apt-get -qy --no-install-recommends install \ - openssl \ - && apt-get autoremove -yq \ +# ================= INSTALL BUN =================== +ARG BUN_VERSION=1.1.29 +ARG YARN_PKG_MANAGER="this.packageManager=\"yarn@1.22.22\"" +ARG BUN_PKG_MANAGER="this.packageManager=\"bun@${BUN_VERSION}\"" +FROM debian:bullseye-slim AS build-bun +ARG BUN_VERSION +RUN apt-get update -qq \ + && apt-get install -qq --no-install-recommends \ + ca-certificates \ + curl \ + dirmngr \ + gpg \ + gpg-agent \ + unzip \ && apt-get clean \ - && rm -rf /var/lib/apt/lists/* -RUN npm --global install pnpm@9.5.0 + && rm -rf /var/lib/apt/lists/* \ + && arch="$(dpkg --print-architecture)" \ + && case "${arch##*-}" in \ + amd64) build="x64-baseline";; \ + arm64) build="aarch64";; \ + *) echo "error: unsupported architecture: $arch"; exit 1 ;; \ + esac \ + && version="$BUN_VERSION" \ + && case "$version" in \ + latest | canary | bun-v*) tag="$version"; ;; \ + v*) tag="bun-$version"; ;; \ + *) tag="bun-v$version"; ;; \ + esac \ + && case "$tag" in \ + latest) release="latest/download"; ;; \ + *) release="download/$tag"; ;; \ + esac \ + && curl "https://github.com/oven-sh/bun/releases/$release/bun-linux-$build.zip" \ + -fsSLO \ + --compressed \ + --retry 5 \ + || (echo "error: failed to download: $tag" && exit 1) \ + && for key in \ + "F3DCC08A8572C0749B3E18888EAB4D40A7B22B59" \ + ; do \ + gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys "$key" \ + || gpg --batch --keyserver keyserver.ubuntu.com --recv-keys "$key" ; \ + done \ + && curl "https://github.com/oven-sh/bun/releases/$release/SHASUMS256.txt.asc" \ + -fsSLO \ + --compressed \ + --retry 5 \ + && gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \ + || (echo "error: failed to verify: $tag" && exit 1) \ + && grep " bun-linux-$build.zip\$" SHASUMS256.txt | sha256sum -c - \ + || (echo "error: failed to verify: $tag" && exit 1) \ + && unzip "bun-linux-$build.zip" \ + && mv "bun-linux-$build/bun" /usr/local/bin/bun \ + && rm -f "bun-linux-$build.zip" SHASUMS256.txt.asc SHASUMS256.txt \ + && chmod +x /usr/local/bin/bun \ + && which bun \ + && bun --version -FROM base AS pruner -RUN npm --global install turbo@2.0.5 -WORKDIR /app -COPY . . -RUN turbo prune ${SCOPE} --docker +# ================= ADD BUN IN NODE 20 IMAGE =================== -FROM base AS builder +FROM node:20-bullseye-slim AS bun +ARG BUN_RUNTIME_TRANSPILER_CACHE_PATH=0 +ENV BUN_RUNTIME_TRANSPILER_CACHE_PATH=${BUN_RUNTIME_TRANSPILER_CACHE_PATH} +ARG BUN_INSTALL_BIN=/usr/local/bin +ENV BUN_INSTALL_BIN=${BUN_INSTALL_BIN} +COPY --from=build-bun /usr/local/bin/bun /usr/local/bin/bun +RUN groupadd bun \ + --gid 2000 \ + && useradd bun \ + --uid 2000 \ + --gid bun \ + --shell /bin/sh \ + --create-home \ + && ln -s /usr/local/bin/bun /usr/local/bin/bunx \ + && which bun \ + && which bunx \ + && bun --version RUN apt-get -qy update && apt-get -qy --no-install-recommends install openssl git python3 g++ build-essential WORKDIR /app -COPY .gitignore .gitignore -COPY .npmrc .pnpmfile.cjs ./ -COPY --from=pruner /app/out/json/ . -COPY --from=pruner /app/out/pnpm-lock.yaml ./pnpm-lock.yaml -RUN pnpm install -COPY --from=pruner /app/out/full/ . -COPY turbo.json turbo.json -RUN SKIP_ENV_CHECK=true pnpm turbo run build --filter=${SCOPE}... +# ================= TURBO PRUNE =================== -FROM base AS runner -WORKDIR /app +FROM bun as pruned +ARG YARN_PKG_MANAGER +ARG SCOPE +COPY . . +RUN bunx json -I -f package.json -e ${YARN_PKG_MANAGER} +RUN bunx turbo prune --scope="${SCOPE}" --docker +# =============== INSTALL & BUILD ================= + +FROM bun as builder +ARG BUN_PKG_MANAGER +ARG SCOPE +COPY --from=pruned /app/out/full/ . +RUN bunx json -I -f package.json -e ${BUN_PKG_MANAGER} +RUN SENTRYCLI_SKIP_DOWNLOAD=1 bun install +RUN SKIP_ENV_CHECK=true bunx turbo build --filter="${SCOPE}..." + +# ================== RELEASE ====================== + +FROM bun AS release +ARG SCOPE +ENV SCOPE=${SCOPE} +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/packages/prisma/postgresql ./packages/prisma/postgresql 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 --from=builder --chown=nextjs:nodejs /app/apps/${SCOPE}/public ./apps/${SCOPE}/public -## Copy next-runtime-env and its dependencies for runtime public variable injection -COPY --from=builder /app/node_modules/.pnpm/chalk@4.1.2/node_modules/chalk ./node_modules/chalk -COPY --from=builder /app/node_modules/.pnpm/chalk@4.1.2/node_modules/ansi-styles ./node_modules/ansi-styles -COPY --from=builder /app/node_modules/.pnpm/chalk@4.1.2/node_modules/supports-color ./node_modules/supports-color -COPY --from=builder /app/node_modules/.pnpm/has-flag@4.0.0/node_modules/has-flag ./node_modules/has-flag -COPY --from=builder /app/node_modules/.pnpm/next-runtime-env@1.6.2/node_modules/next-runtime-env/build ./node_modules/next-runtime-env/build - -## Copy prisma package and its dependencies and generate schema -COPY ./packages/prisma/postgresql ./packages/prisma/postgresql -COPY --from=builder /app/node_modules/.pnpm/@prisma+client@5.12.1_prisma@5.12.1/node_modules/@prisma/client ./node_modules/@prisma/client -COPY --from=builder /app/node_modules/.pnpm/@prisma+engines@5.12.1/node_modules/@prisma/engines ./node_modules/@prisma/engines -COPY --from=builder /app/node_modules/.pnpm/@prisma+debug@5.12.1/node_modules/@prisma/debug ./node_modules/@prisma/debug -COPY --from=builder /app/node_modules/.pnpm/@prisma+get-platform@5.12.1/node_modules/@prisma/get-platform ./node_modules/@prisma/get-platform -COPY --from=builder /app/node_modules/.pnpm/@prisma+fetch-engine@5.12.1/node_modules/@prisma/fetch-engine ./node_modules/@prisma/fetch-engine -COPY --from=builder /app/node_modules/.pnpm/@prisma+engines-version@5.12.0-21.473ed3124229e22d881cb7addf559799debae1ab/node_modules/@prisma/engines-version ./node_modules/@prisma/engines-version -COPY --from=builder /app/node_modules/.pnpm/prisma@5.12.1/node_modules/prisma ./node_modules/prisma -COPY --from=builder /app/node_modules/.bin/prisma ./node_modules/.bin/prisma RUN ./node_modules/.bin/prisma generate --schema=packages/prisma/postgresql/schema.prisma; + COPY scripts/${SCOPE}-entrypoint.sh ./ RUN chmod +x ./${SCOPE}-entrypoint.sh ENTRYPOINT ./${SCOPE}-entrypoint.sh diff --git a/LICENSE b/LICENSE index 9656c4ab0a..4f8cf8b335 100644 --- a/LICENSE +++ b/LICENSE @@ -1,669 +1,105 @@ - Copyright (c) 2020-present Typebot - -Portions of this software are licensed as follows: - -- All content that resides under https://github.com/baptisteArno/typebot.io/tree/main/ee directory of this repository is licensed under the license defined in [ee/LICENSE](./ee/LICENSE). -- Content outside of the above mentioned directories or restrictions above is available under the "AGPLv3" license as defined below. - - - GNU AFFERO GENERAL PUBLIC LICENSE - Version 3, 19 November 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU Affero General Public License is a free, copyleft license for -software and other kinds of works, specifically designed to ensure -cooperation with the community in the case of network server software. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -our General Public Licenses are intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - Developers that use our General Public Licenses protect your rights -with two steps: (1) assert copyright on the software, and (2) offer -you this License which gives you legal permission to copy, distribute -and/or modify the software. - - A secondary benefit of defending all users' freedom is that -improvements made in alternate versions of the program, if they -receive widespread use, become available for other developers to -incorporate. Many developers of free software are heartened and -encouraged by the resulting cooperation. However, in the case of -software used on network servers, this result may fail to come about. -The GNU General Public License permits making a modified version and -letting the public access it on a server without ever releasing its -source code to the public. - - The GNU Affero General Public License is designed specifically to -ensure that, in such cases, the modified source code becomes available -to the community. It requires the operator of a network server to -provide the source code of the modified version running there to the -users of that server. Therefore, public use of a modified version, on -a publicly accessible server, gives the public access to the source -code of the modified version. - - An older license, called the Affero General Public License and -published by Affero, was designed to accomplish similar goals. This is -a different license, not a version of the Affero GPL, but Affero has -released a new version of the Affero GPL which permits relicensing under -this license. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU Affero General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Remote Network Interaction; Use with the GNU General Public License. - - Notwithstanding any other provision of this License, if you modify the -Program, your modified version must prominently offer all users -interacting with it remotely through a computer network (if your version -supports such interaction) an opportunity to receive the Corresponding -Source of your version by providing access to the Corresponding Source -from a network server at no charge, through some standard or customary -means of facilitating copying of software. This Corresponding Source -shall include the Corresponding Source for any work covered by version 3 -of the GNU General Public License that is incorporated pursuant to the -following paragraph. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the work with which it is combined will remain governed by version -3 of the GNU General Public License. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU Affero General Public License from time to time. Such new versions -will be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU Affero General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU Affero General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU Affero General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If your software can interact with users remotely through a computer -network, you should also make sure that it provides a way for users to -get its source. For example, if your program is a web application, its -interface could display a "Source" link that leads users to an archive -of the code. There are many ways you could offer source, and different -solutions will be better for different programs; see section 13 for the -specific requirements. - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU AGPL, see -. +# Functional Source License, Version 1.1, Apache 2.0 Future License + +## Abbreviation + +FSL-1.1-Apache-2.0 + +## Notice + +Copyright 2023-2024 Typebot + +## Terms and Conditions + +### Licensor ("We") + +The party offering the Software under these Terms and Conditions. + +### The Software + +The "Software" is each version of the software that we make available under +these Terms and Conditions, as indicated by our inclusion of these Terms and +Conditions with the Software. + +### License Grant + +Subject to your compliance with this License Grant and the Patents, +Redistribution and Trademark clauses below, we hereby grant you the right to +use, copy, modify, create derivative works, publicly perform, publicly display +and redistribute the Software for any Permitted Purpose identified below. + +### Permitted Purpose + +A Permitted Purpose is any purpose other than a Competing Use. A Competing Use +means making the Software available to others in a commercial product or +service that: + +1. substitutes for the Software; + +2. substitutes for any other product or service we offer using the Software + that exists as of the date we make the Software available; or + +3. offers the same or substantially similar functionality as the Software. + +Permitted Purposes specifically include using the Software: + +1. for your internal use and access; + +2. for non-commercial education; + +3. for non-commercial research; and + +4. in connection with professional services that you provide to a licensee + using the Software in accordance with these Terms and Conditions. + +### Patents + +To the extent your use for a Permitted Purpose would necessarily infringe our +patents, the license grant above includes a license under our patents. If you +make a claim against any party that the Software infringes or contributes to +the infringement of any patent, then your patent license to the Software ends +immediately. + +### Redistribution + +The Terms and Conditions apply to all copies, modifications and derivatives of +the Software. + +If you redistribute any copies, modifications or derivatives of the Software, +you must include a copy of or a link to these Terms and Conditions and not +remove any copyright notices provided in or with the Software. + +### Disclaimer + +THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR +PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT. + +IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE +SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES, +EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE. + +### Trademarks + +Except for displaying the License Details and identifying us as the origin of +the Software, you have no right under these Terms and Conditions to use our +trademarks, trade names, service marks or product names. + +## Grant of Future License + +We hereby irrevocably grant you an additional license to use the Software under +the Apache License, Version 2.0 that is effective on the second anniversary of +the date we make the Software available. On or after that date, you may use the +Software under the Apache License, Version 2.0, in which case the following +will apply: + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +this file except in compliance with the License. + +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. diff --git a/README.md b/README.md index 087f3c14c2..5ff3a07b4d 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@

-Typebot is an open-source chatbot builder. It allows you to create advanced chatbots visually, embed them anywhere on your web/mobile apps, and collect results in real-time +Typebot is an Fair Source chatbot builder. It allows you to create advanced chatbots visually, embed them anywhere on your web/mobile apps, and collect results in real-time

@@ -79,7 +79,7 @@ Built for **developers**: The easiest way to get started with Typebot is with [the official managed service in the Cloud](https://app.typebot.io). You'll have high availability, backups, security, and maintenance all managed for you by me, [Baptiste, Typebot's founder](https://twitter.com/baptisteArno). The cloud version can save a substantial amount of developer time and resources. For most sites this ends up being the best value option and the revenue goes to funding the maintenance and further development of Typebot. -So you’ll be supporting open source software and getting a great service! 💙 +So you’ll be supporting fair source software and getting a great service! 💙 ## Support & Community @@ -110,4 +110,4 @@ Made with [contrib.rocks](https://contrib.rocks). ## License -Most of Typebot's code is open-source under the GNU Affero General Public License Version 3 (AGPLv3). You will find more information about the license and how to comply with it [here](https://docs.typebot.io/self-hosting#license-requirements). +Typebot's code is protected under a Functional Source License. You will find more information about the license and how to comply with it [here](https://docs.typebot.io/self-hosting#license-requirements). diff --git a/apps/builder/.eslintignore b/apps/builder/.eslintignore deleted file mode 100644 index ebd9b0fa7c..0000000000 --- a/apps/builder/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -src/test/reporters \ No newline at end of file diff --git a/apps/builder/.eslintrc.js b/apps/builder/.eslintrc.js deleted file mode 100644 index b56159ea9c..0000000000 --- a/apps/builder/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - root: true, - extends: ['custom'], -} diff --git a/apps/builder/next-env.d.ts b/apps/builder/next-env.d.ts index 4f11a03dc6..a4a7b3f5cf 100644 --- a/apps/builder/next-env.d.ts +++ b/apps/builder/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/apps/builder/next.config.mjs b/apps/builder/next.config.mjs index 896f5b5704..f5b5e85585 100644 --- a/apps/builder/next.config.mjs +++ b/apps/builder/next.config.mjs @@ -1,99 +1,97 @@ -import { withSentryConfig } from '@sentry/nextjs' -import { join, dirname } from 'path' -import '@typebot.io/env/dist/env.mjs' -import { configureRuntimeEnv } from 'next-runtime-env/build/configure.js' -import { fileURLToPath } from 'url' +import { dirname, join } from "path"; +import { withSentryConfig } from "@sentry/nextjs"; +import "@typebot.io/env/compiled"; +import { fileURLToPath } from "url"; +import { configureRuntimeEnv } from "next-runtime-env/build/configure.js"; -const __filename = fileURLToPath(import.meta.url) +const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename) +const __dirname = dirname(__filename); const injectViewerUrlIfVercelPreview = (val) => { if ( - (val && typeof val === 'string' && val.length > 0) || - process.env.VERCEL_ENV !== 'preview' || + (val && typeof val === "string" && val.length > 0) || + process.env.VERCEL_ENV !== "preview" || !process.env.VERCEL_BUILDER_PROJECT_NAME || !process.env.NEXT_PUBLIC_VERCEL_VIEWER_PROJECT_NAME ) - return + return; process.env.NEXT_PUBLIC_VIEWER_URL = `https://${process.env.VERCEL_BRANCH_URL}`.replace( process.env.VERCEL_BUILDER_PROJECT_NAME, - process.env.NEXT_PUBLIC_VERCEL_VIEWER_PROJECT_NAME - ) - if (process.env.NEXT_PUBLIC_CHAT_API_URL?.includes('{{pr_id}}')) + process.env.NEXT_PUBLIC_VERCEL_VIEWER_PROJECT_NAME, + ); + if (process.env.NEXT_PUBLIC_CHAT_API_URL?.includes("{{pr_id}}")) process.env.NEXT_PUBLIC_CHAT_API_URL = process.env.NEXT_PUBLIC_CHAT_API_URL.replace( - '{{pr_id}}', - process.env.VERCEL_GIT_PULL_REQUEST_ID - ) -} + "{{pr_id}}", + process.env.VERCEL_GIT_PULL_REQUEST_ID, + ); +}; -injectViewerUrlIfVercelPreview(process.env.NEXT_PUBLIC_VIEWER_URL) +injectViewerUrlIfVercelPreview(process.env.NEXT_PUBLIC_VIEWER_URL); -configureRuntimeEnv() +configureRuntimeEnv(); /** @type {import('next').NextConfig} */ const nextConfig = { + eslint: { + ignoreDuringBuilds: true, + }, + transpilePackages: ["@typebot.io/billing", "@typebot.io/blocks-bubbles"], reactStrictMode: true, - output: 'standalone', - transpilePackages: [ - '@typebot.io/lib', - '@typebot.io/schemas', - '@typebot.io/emails', - '@typebot.io/env', - ], + output: "standalone", i18n: { - defaultLocale: 'en', - locales: ['en', 'fr', 'pt', 'pt-BR', 'de', 'ro', 'es', 'it', 'el'], + defaultLocale: "en", + locales: ["en", "fr", "pt", "pt-BR", "de", "ro", "es", "it", "el"], }, experimental: { - outputFileTracingRoot: join(__dirname, '../../'), - serverComponentsExternalPackages: ['isolated-vm'], + outputFileTracingRoot: join(__dirname, "../../"), + serverComponentsExternalPackages: ["isolated-vm"], }, webpack: (config, { isServer }) => { - if (isServer) return config + if (isServer) return config; - config.resolve.alias['minio'] = false - config.resolve.alias['qrcode'] = false - config.resolve.alias['isolated-vm'] = false - return config + config.resolve.alias["minio"] = false; + config.resolve.alias["qrcode"] = false; + config.resolve.alias["isolated-vm"] = false; + return config; }, headers: async () => { return [ { - source: '/(.*)?', + source: "/(.*)?", headers: [ { - key: 'X-Frame-Options', - value: 'SAMEORIGIN', + key: "X-Frame-Options", + value: "SAMEORIGIN", }, ], }, - ] + ]; }, async rewrites() { return process.env.NEXT_PUBLIC_POSTHOG_KEY ? [ { - source: '/ingest/:path*', + source: "/ingest/:path*", destination: (process.env.NEXT_PUBLIC_POSTHOG_HOST ?? - 'https://app.posthog.com') + '/:path*', + "https://app.posthog.com") + "/:path*", }, { - source: '/healthz', - destination: '/api/health', + source: "/healthz", + destination: "/api/health", }, ] : [ { - source: '/healthz', - destination: '/api/health', + source: "/healthz", + destination: "/api/health", }, - ] + ]; }, -} +}; export default process.env.NEXT_PUBLIC_SENTRY_DSN ? withSentryConfig( @@ -104,7 +102,7 @@ export default process.env.NEXT_PUBLIC_SENTRY_DSN // Suppresses source map uploading logs during build silent: true, - release: process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA + '-builder', + release: process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA + "-builder", org: process.env.SENTRY_ORG, project: process.env.SENTRY_PROJECT, }, @@ -116,13 +114,13 @@ export default process.env.NEXT_PUBLIC_SENTRY_DSN widenClientFileUpload: true, // Routes browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers (increases server load) - tunnelRoute: '/monitoring', + tunnelRoute: "/monitoring", // Hides source maps from generated client bundles hideSourceMaps: true, // Automatically tree-shake Sentry logger statements to reduce bundle size disableLogger: true, - } + }, ) - : nextConfig + : nextConfig; diff --git a/apps/builder/package.json b/apps/builder/package.json index 83d37f0ad1..b804ed5151 100644 --- a/apps/builder/package.json +++ b/apps/builder/package.json @@ -1,16 +1,12 @@ { "name": "builder", - "version": "0.1.0", - "license": "AGPL-3.0-or-later", "scripts": { "dev": "dotenv -e ./.env -e ../../.env -- next dev -p 3000", "build": "dotenv -e ./.env -e ../../.env -- next build", "start": "dotenv -e ./.env -e ../../.env -- next start", - "lint": "dotenv -e ./.env -e ../../.env -- next lint", - "test": "dotenv -e ./.env -e ../../.env -- pnpm playwright test", - "test:show-report": "pnpm playwright show-report src/test/reporters", - "test:ui": "dotenv -e ./.env -e ../../.env -- pnpm playwright test --ui", - "format:check": "prettier --check ./src --ignore-path ../../.prettierignore" + "test": "dotenv -e ./.env -e ../../.env -- playwright test", + "test:show-report": "playwright show-report src/test/reporters", + "test:ui": "dotenv -e ./.env -e ../../.env -- playwright test --ui" }, "dependencies": { "@braintree/sanitize-url": "7.0.1", @@ -22,13 +18,10 @@ "@dnd-kit/utilities": "3.2.1", "@emotion/react": "11.11.4", "@emotion/styled": "11.11.5", - "@faire/mjml-react": "3.3.0", "@giphy/js-fetch-api": "5.0.0", - "@giphy/js-types": "4.4.0", - "@giphy/js-util": "5.0.0", "@giphy/react-components": "7.1.0", "@googleapis/drive": "8.0.0", - "@lilyrose2798/trpc-openapi": "^1.3.9", + "@typebot.io/trpc-openapi": "workspace:*", "@paralleldrive/cuid2": "2.2.1", "@sentry/nextjs": "7.77.0", "@tanstack/react-query": "4.29.19", @@ -39,13 +32,17 @@ "@trpc/next": "10.40.0", "@trpc/react-query": "10.40.0", "@trpc/server": "10.40.0", + "@typebot.io/blocks-core": "workspace:*", + "@typebot.io/blocks-inputs": "workspace:*", + "@typebot.io/blocks-logic": "workspace:*", "@typebot.io/bot-engine": "workspace:*", + "@typebot.io/credentials": "workspace:*", "@typebot.io/emails": "workspace:*", "@typebot.io/env": "workspace:*", - "@typebot.io/js": "workspace:*", "@typebot.io/nextjs": "workspace:*", "@typebot.io/theme": "workspace:*", - "@udecode/cn": "29.0.1", + "@typebot.io/typebot": "workspace:*", + "@typebot.io/whatsapp": "workspace:*", "@udecode/plate-basic-marks": "30.5.3", "@udecode/plate-common": "30.4.5", "@udecode/plate-core": "30.4.5", @@ -57,30 +54,27 @@ "@uiw/react-codemirror": "4.21.24", "@upstash/ratelimit": "0.4.3", "@use-gesture/react": "10.2.27", + "bot-engine": "workspace:*", "browser-image-compression": "2.0.2", "canvas-confetti": "1.6.0", "codemirror": "6.0.1", "deep-object-diff": "1.1.9", "dequal": "2.0.3", "emojilib": "3.0.10", - "focus-visible": "5.2.0", "framer-motion": "11.1.7", "google-auth-library": "8.9.0", "google-spreadsheet": "4.1.1", "immer": "10.0.2", - "ioredis": "^5.4.1", - "isolated-vm": "5.0.1", + "ioredis": "5.4.1", "jsonwebtoken": "9.0.1", "ky": "1.2.4", - "libphonenumber-js": "1.10.37", - "micro": "10.0.1", "micro-cors": "0.1.1", - "next": "14.1.0", + "next": "14.2.13", "next-auth": "4.22.1", "nextjs-cors": "2.1.2", "nodemailer": "6.9.8", "nprogress": "0.2.0", - "openai": "4.47.1", + "openai": "4.52.7", "papaparse": "5.4.1", "pexels": "^1.4.0", "prettier": "2.8.8", @@ -106,7 +100,6 @@ "@typebot.io/forge": "workspace:*", "@typebot.io/forge-repository": "workspace:*", "@typebot.io/lib": "workspace:*", - "@typebot.io/migrations": "workspace:*", "@typebot.io/playwright": "workspace:*", "@typebot.io/prisma": "workspace:*", "@typebot.io/radar": "workspace:*", @@ -128,11 +121,7 @@ "@types/tinycolor2": "1.4.3", "dotenv": "16.4.5", "dotenv-cli": "7.4.1", - "eslint": "8.44.0", - "eslint-config-custom": "workspace:*", "next-runtime-env": "1.6.2", - "superjson": "1.12.4", - "typescript": "5.4.5", - "zod": "3.22.4" + "superjson": "1.12.4" } } diff --git a/apps/builder/playwright.config.ts b/apps/builder/playwright.config.ts index 16b8555560..d116734e01 100644 --- a/apps/builder/playwright.config.ts +++ b/apps/builder/playwright.config.ts @@ -1,8 +1,8 @@ -import { defineConfig, devices } from '@playwright/test' -import { resolve } from 'path' +import { resolve } from "path"; +import { defineConfig, devices } from "@playwright/test"; // eslint-disable-next-line @typescript-eslint/no-var-requires -require('dotenv').config({ path: resolve(__dirname, '../../.env') }) +require("dotenv").config({ path: resolve(__dirname, "../../.env") }); export default defineConfig({ timeout: process.env.CI ? 50 * 1000 : 40 * 1000, @@ -13,44 +13,44 @@ export default defineConfig({ workers: process.env.CI ? 1 : 4, retries: process.env.CI ? 2 : 1, reporter: [ - [process.env.CI ? 'github' : 'list'], - ['html', { outputFolder: 'src/test/reporters' }], + [process.env.CI ? "github" : "list"], + ["html", { outputFolder: "src/test/reporters" }], ], maxFailures: 10, webServer: process.env.CI ? { - command: 'pnpm run start', + command: "bun start", timeout: 60_000, reuseExistingServer: true, port: 3000, } : undefined, - outputDir: './src/test/results', + outputDir: "./src/test/results", use: { - trace: 'on-first-retry', - locale: 'en-US', + trace: "on-first-retry", + locale: "en-US", baseURL: process.env.NEXTAUTH_URL, - storageState: './src/test/storageState.json', - permissions: ['microphone'], + storageState: "./src/test/storageState.json", + permissions: ["microphone"], launchOptions: { args: [ - '--use-fake-ui-for-media-stream', - '--use-fake-device-for-media-stream', + "--use-fake-ui-for-media-stream", + "--use-fake-device-for-media-stream", ], }, }, projects: [ { - name: 'setup db', + name: "setup db", testMatch: /global\.setup\.ts/, }, { - name: 'chromium', + name: "chromium", use: { - ...devices['Desktop Chrome'], + ...devices["Desktop Chrome"], viewport: { width: 1400, height: 1000 }, }, - dependencies: ['setup db'], + dependencies: ["setup db"], }, ], -}) +}); diff --git a/apps/builder/public/templates/quick-carb-calculator.json b/apps/builder/public/templates/quick-carb-calculator.json index 6ee2cb4331..7e35cffe19 100644 --- a/apps/builder/public/templates/quick-carb-calculator.json +++ b/apps/builder/public/templates/quick-carb-calculator.json @@ -66,7 +66,10 @@ { "id": "omopw9oy3srowddb0o3f4xs5", "content": "Swim 🏊‍♂️" }, { "id": "anpwbn8hvolyod2vsh06et68", "content": "Ride 🚴‍♂️" }, { "id": "uto1ghh5c6icwygszercxybz", "content": "Run 🏃" }, - { "id": "sau0amab8nmeqfqyhhzle03v", "content": "Triathlon 🏊‍♂️🚴‍♂️🏃" }, + { + "id": "sau0amab8nmeqfqyhhzle03v", + "content": "Triathlon 🏊‍♂️🚴‍♂️🏃" + }, { "id": "tqvjhk0qmvn43h5hyc9ibdd3", "content": "Swimrun 🏊‍♂️🏃" }, { "id": "mu0m5v7m55vfikbs29lpcm3h", diff --git a/apps/builder/sentry.client.config.ts b/apps/builder/sentry.client.config.ts index 96c8e4c9df..52d3ad2582 100644 --- a/apps/builder/sentry.client.config.ts +++ b/apps/builder/sentry.client.config.ts @@ -1,12 +1,12 @@ -import * as Sentry from '@sentry/nextjs' +import * as Sentry from "@sentry/nextjs"; Sentry.init({ dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, ignoreErrors: [ - 'ResizeObserver loop limit exceeded', - 'ResizeObserver loop completed with undelivered notifications.', - 'ResizeObserver is not defined', + "ResizeObserver loop limit exceeded", + "ResizeObserver loop completed with undelivered notifications.", + "ResizeObserver is not defined", "Can't find variable: ResizeObserver", ], - release: process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA + '-builder', -}) + release: process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA + "-builder", +}); diff --git a/apps/builder/sentry.server.config.ts b/apps/builder/sentry.server.config.ts index a0092a1ae6..019d877eda 100644 --- a/apps/builder/sentry.server.config.ts +++ b/apps/builder/sentry.server.config.ts @@ -1,12 +1,12 @@ -import * as Sentry from '@sentry/nextjs' -import prisma from '@typebot.io/lib/prisma' +import * as Sentry from "@sentry/nextjs"; +import prisma from "@typebot.io/prisma"; Sentry.init({ dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, - release: process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA + '-builder', + release: process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA + "-builder", integrations: [ new Sentry.Integrations.Prisma({ client: prisma, }), ], -}) +}); diff --git a/apps/builder/src/assets/styles/routerProgressBar.css b/apps/builder/src/assets/styles/routerProgressBar.css index 681503a539..91d911eb35 100644 --- a/apps/builder/src/assets/styles/routerProgressBar.css +++ b/apps/builder/src/assets/styles/routerProgressBar.css @@ -22,8 +22,8 @@ right: 0px; width: 100px; height: 100%; - box-shadow: 0 0 10px var(--chakra-colors-blue-500), - 0 0 5px var(--chakra-colors-blue-500); + box-shadow: 0 0 10px var(--chakra-colors-blue-500), 0 0 5px + var(--chakra-colors-blue-500); opacity: 1; -webkit-transform: rotate(3deg) translate(0px, -4px); diff --git a/apps/builder/src/components/AlertInfo.tsx b/apps/builder/src/components/AlertInfo.tsx index 44fcd112c9..a4dfba102c 100644 --- a/apps/builder/src/components/AlertInfo.tsx +++ b/apps/builder/src/components/AlertInfo.tsx @@ -1,8 +1,8 @@ -import { AlertProps, Alert, AlertIcon } from '@chakra-ui/react' +import { Alert, AlertIcon, type AlertProps } from "@chakra-ui/react"; export const AlertInfo = (props: AlertProps) => ( {props.children} -) +); diff --git a/apps/builder/src/components/ColorPicker.tsx b/apps/builder/src/components/ColorPicker.tsx index 9fe01a6b0e..de03834af2 100644 --- a/apps/builder/src/components/ColorPicker.tsx +++ b/apps/builder/src/components/ColorPicker.tsx @@ -1,43 +1,43 @@ import { + Box, + Button, + type ButtonProps, + Center, + Input, Popover, - PopoverTrigger, - PopoverContent, PopoverArrow, + PopoverBody, PopoverCloseButton, + PopoverContent, PopoverHeader, - Center, - PopoverBody, + PopoverTrigger, SimpleGrid, - Input, - Button, Stack, - ButtonProps, - Box, -} from '@chakra-ui/react' -import { useTranslate } from '@tolgee/react' -import React, { useState } from 'react' -import tinyColor from 'tinycolor2' -import { useDebouncedCallback } from 'use-debounce' +} from "@chakra-ui/react"; +import { useTranslate } from "@tolgee/react"; +import React, { useState } from "react"; +import tinyColor from "tinycolor2"; +import { useDebouncedCallback } from "use-debounce"; const colorsSelection: `#${string}`[] = [ - '#666460', - '#FFFFFF', - '#A87964', - '#D09C46', - '#DE8031', - '#598E71', - '#4A8BB2', - '#9B74B7', - '#C75F96', - '#0042DA', -] + "#666460", + "#FFFFFF", + "#A87964", + "#D09C46", + "#DE8031", + "#598E71", + "#4A8BB2", + "#9B74B7", + "#C75F96", + "#0042DA", +]; type Props = { - value?: string - defaultValue?: string - isDisabled?: boolean - onColorChange: (color: string) => void -} + value?: string; + defaultValue?: string; + isDisabled?: boolean; + onColorChange: (color: string) => void; +}; export const ColorPicker = ({ value, @@ -45,25 +45,25 @@ export const ColorPicker = ({ isDisabled, onColorChange, }: Props) => { - const { t } = useTranslate() - const [color, setColor] = useState(defaultValue ?? '') - const displayedValue = value ?? color + const { t } = useTranslate(); + const [color, setColor] = useState(defaultValue ?? ""); + const displayedValue = value ?? color; const handleColorChange = (color: string) => { - setColor(color) - onColorChange(color) - } + setColor(color); + onColorChange(color); + }; const handleClick = (color: string) => () => { - setColor(color) - onColorChange(color) - } + setColor(color); + onColorChange(color); + }; return ( - ) -} + ); +}; diff --git a/apps/builder/src/components/DropdownList.tsx b/apps/builder/src/components/DropdownList.tsx index bab7be1905..72a96a1deb 100644 --- a/apps/builder/src/components/DropdownList.tsx +++ b/apps/builder/src/components/DropdownList.tsx @@ -1,7 +1,7 @@ +import { ChevronLeftIcon } from "@/components/icons"; import { Button, - ButtonProps, - chakra, + type ButtonProps, FormControl, FormHelperText, FormLabel, @@ -12,34 +12,35 @@ import { MenuList, Portal, Stack, -} from '@chakra-ui/react' -import { ChevronLeftIcon } from '@/components/icons' -import React, { ReactNode } from 'react' -import { MoreInfoTooltip } from './MoreInfoTooltip' + chakra, +} from "@chakra-ui/react"; +import type { ReactNode } from "react"; +import React from "react"; +import { MoreInfoTooltip } from "./MoreInfoTooltip"; type Item = | string | number | { - label: string - value: string - } + label: string; + value: string; + }; // eslint-disable-next-line @typescript-eslint/no-explicit-any type Props = { - currentItem: string | number | undefined + currentItem: string | number | undefined; onItemSelect: ( value: T extends string ? T : T extends number ? T : string, - item?: T - ) => void - items: readonly T[] - placeholder?: string - label?: string - isRequired?: boolean - direction?: 'row' | 'column' - helperText?: ReactNode - moreInfoTooltip?: string -} + item?: T, + ) => void; + items: readonly T[]; + placeholder?: string; + label?: string; + isRequired?: boolean; + direction?: "row" | "column"; + helperText?: ReactNode; + moreInfoTooltip?: string; +}; // eslint-disable-next-line @typescript-eslint/no-explicit-any export const DropdownList = ({ @@ -49,31 +50,33 @@ export const DropdownList = ({ placeholder, label, isRequired, - direction = 'column', + direction = "column", helperText, moreInfoTooltip, ...props }: Props & ButtonProps) => { const handleMenuItemClick = (item: T) => () => { - if (typeof item === 'string' || typeof item === 'number') - onItemSelect(item as T extends string ? T : T extends number ? T : string) + if (typeof item === "string" || typeof item === "number") + onItemSelect( + item as T extends string ? T : T extends number ? T : string, + ); else onItemSelect( item.value as T extends string ? T : T extends number ? T : string, - item - ) - } + item, + ); + }; return ( {label && ( - {label}{' '} + {label}{" "} {moreInfoTooltip && ( {moreInfoTooltip} )} @@ -82,7 +85,7 @@ export const DropdownList = ({

} + rightIcon={} colorScheme="gray" justifyContent="space-between" textAlign="left" @@ -93,17 +96,17 @@ export const DropdownList = ({ {currentItem ? getItemLabel( items?.find((item) => - typeof item === 'string' || typeof item === 'number' + typeof item === "string" || typeof item === "number" ? currentItem === item - : currentItem === item.value - ) + : currentItem === item.value, + ), ) - : placeholder ?? 'Select an item'} + : (placeholder ?? "Select an item")} - + {items.map((item) => ( ({ textOverflow="ellipsis" onClick={handleMenuItemClick(item)} > - {typeof item === 'object' ? item.label : item} + {typeof item === "object" ? item.label : item} ))} @@ -122,11 +125,11 @@ export const DropdownList = ({ {helperText && {helperText}} - ) -} + ); +}; const getItemLabel = (item?: Item) => { - if (!item) return '' - if (typeof item === 'object') return item.label - return item -} + if (!item) return ""; + if (typeof item === "object") return item.label; + return item; +}; diff --git a/apps/builder/src/components/EditableEmojiOrImageIcon.tsx b/apps/builder/src/components/EditableEmojiOrImageIcon.tsx index fad061edaf..6b145d631c 100644 --- a/apps/builder/src/components/EditableEmojiOrImageIcon.tsx +++ b/apps/builder/src/components/EditableEmojiOrImageIcon.tsx @@ -1,27 +1,28 @@ +import { useParentModal } from "@/features/graph/providers/ParentModalProvider"; +import type { FilePathUploadProps } from "@/features/upload/api/generateUploadUrl"; import { + Flex, Popover, + PopoverContent, + PopoverTrigger, + Portal, Tooltip, chakra, - PopoverTrigger, - PopoverContent, - Flex, useColorModeValue, - Portal, -} from '@chakra-ui/react' -import React, { RefObject } from 'react' -import { EmojiOrImageIcon } from './EmojiOrImageIcon' -import { ImageUploadContent } from './ImageUploadContent' -import { FilePathUploadProps } from '@/features/upload/api/generateUploadUrl' -import { useTranslate } from '@tolgee/react' -import { useParentModal } from '@/features/graph/providers/ParentModalProvider' +} from "@chakra-ui/react"; +import { useTranslate } from "@tolgee/react"; +import type { RefObject } from "react"; +import React from "react"; +import { EmojiOrImageIcon } from "./EmojiOrImageIcon"; +import { ImageUploadContent } from "./ImageUploadContent"; type Props = { - uploadFileProps: FilePathUploadProps - icon?: string | null - parentModalRef?: RefObject | undefined - onChangeIcon: (icon: string) => void - boxSize?: string -} + uploadFileProps: FilePathUploadProps; + icon?: string | null; + parentModalRef?: RefObject | undefined; + onChangeIcon: (icon: string) => void; + boxSize?: string; +}; export const EditableEmojiOrImageIcon = ({ uploadFileProps, @@ -29,15 +30,15 @@ export const EditableEmojiOrImageIcon = ({ onChangeIcon, boxSize, }: Props) => { - const { t } = useTranslate() - const { ref: parentModalRef } = useParentModal() - const bg = useColorModeValue('gray.100', 'gray.700') + const { t } = useTranslate(); + const { ref: parentModalRef } = useParentModal(); + const bg = useColorModeValue("gray.100", "gray.700"); return ( {({ onClose }: { onClose: () => void }) => ( <> - + )} - ) -} + ); +}; diff --git a/apps/builder/src/components/EmojiOrImageIcon.tsx b/apps/builder/src/components/EmojiOrImageIcon.tsx index f68da2f986..a5c61478dd 100644 --- a/apps/builder/src/components/EmojiOrImageIcon.tsx +++ b/apps/builder/src/components/EmojiOrImageIcon.tsx @@ -1,29 +1,29 @@ -import { ToolIcon } from '@/components/icons' -import React from 'react' -import { chakra, IconProps, Image } from '@chakra-ui/react' -import { isSvgSrc } from '@typebot.io/lib/utils' +import { ToolIcon } from "@/components/icons"; +import { type IconProps, Image, chakra } from "@chakra-ui/react"; +import { isSvgSrc } from "@typebot.io/lib/utils"; +import React from "react"; type Props = { - icon?: string | null - emojiFontSize?: string - boxSize?: string - defaultIcon?: (props: IconProps) => JSX.Element -} + icon?: string | null; + emojiFontSize?: string; + boxSize?: string; + defaultIcon?: (props: IconProps) => JSX.Element; +}; export const EmojiOrImageIcon = ({ icon, - boxSize = '25px', + boxSize = "25px", emojiFontSize, defaultIcon = ToolIcon, }: Props) => { return ( <> {icon ? ( - icon.startsWith('http') || isSvgSrc(icon) ? ( + icon.startsWith("http") || isSvgSrc(icon) ? ( typebot icon @@ -36,5 +36,5 @@ export const EmojiOrImageIcon = ({ defaultIcon({ boxSize }) )} - ) -} + ); +}; diff --git a/apps/builder/src/components/GoogleLogo.tsx b/apps/builder/src/components/GoogleLogo.tsx index 91f2ca0177..40b4d84a7e 100644 --- a/apps/builder/src/components/GoogleLogo.tsx +++ b/apps/builder/src/components/GoogleLogo.tsx @@ -1,4 +1,4 @@ -import { IconProps, Icon } from '@chakra-ui/react' +import { Icon, type IconProps } from "@chakra-ui/react"; export const GoogleLogo = (props: IconProps) => ( @@ -21,4 +21,4 @@ export const GoogleLogo = (props: IconProps) => ( /> -) +); diff --git a/apps/builder/src/components/ImageUploadContent/GiphyPicker.tsx b/apps/builder/src/components/ImageUploadContent/GiphyPicker.tsx index f94eb74ca5..8b00563047 100644 --- a/apps/builder/src/components/ImageUploadContent/GiphyPicker.tsx +++ b/apps/builder/src/components/ImageUploadContent/GiphyPicker.tsx @@ -1,25 +1,25 @@ -import { Flex, Stack, Text } from '@chakra-ui/react' -import { GiphyFetch } from '@giphy/js-fetch-api' -import { Grid } from '@giphy/react-components' -import { GiphyLogo } from '../logos/GiphyLogo' -import React, { useState } from 'react' -import { TextInput } from '../inputs' -import { env } from '@typebot.io/env' +import { Flex, Stack, Text } from "@chakra-ui/react"; +import { GiphyFetch } from "@giphy/js-fetch-api"; +import { Grid } from "@giphy/react-components"; +import { env } from "@typebot.io/env"; +import React, { useState } from "react"; +import { TextInput } from "../inputs"; +import { GiphyLogo } from "../logos/GiphyLogo"; type GiphySearchFormProps = { - onSubmit: (url: string) => void -} + onSubmit: (url: string) => void; +}; -const giphyFetch = new GiphyFetch(env.NEXT_PUBLIC_GIPHY_API_KEY ?? '') +const giphyFetch = new GiphyFetch(env.NEXT_PUBLIC_GIPHY_API_KEY ?? ""); export const GiphyPicker = ({ onSubmit }: GiphySearchFormProps) => { - const [inputValue, setInputValue] = useState('') + const [inputValue, setInputValue] = useState(""); const fetchGifs = (offset: number) => - giphyFetch.search(inputValue, { offset, limit: 10 }) + giphyFetch.search(inputValue, { offset, limit: 10 }); const fetchGifsTrending = (offset: number) => - giphyFetch.trending({ offset, limit: 10 }) + giphyFetch.trending({ offset, limit: 10 }); return !env.NEXT_PUBLIC_GIPHY_API_KEY ? ( NEXT_PUBLIC_GIPHY_API_KEY is missing in environment @@ -39,15 +39,15 @@ export const GiphyPicker = ({ onSubmit }: GiphySearchFormProps) => { { - e.preventDefault() - onSubmit(gif.images.downsized.url) + e.preventDefault(); + onSubmit(gif.images.downsized.url); }} - fetchGifs={inputValue === '' ? fetchGifsTrending : fetchGifs} + fetchGifs={inputValue === "" ? fetchGifsTrending : fetchGifs} width={475} columns={3} className="my-4" /> - ) -} + ); +}; diff --git a/apps/builder/src/components/ImageUploadContent/IconPicker.tsx b/apps/builder/src/components/ImageUploadContent/IconPicker.tsx index a1ed01541c..867dfa698e 100644 --- a/apps/builder/src/components/ImageUploadContent/IconPicker.tsx +++ b/apps/builder/src/components/ImageUploadContent/IconPicker.tsx @@ -1,113 +1,115 @@ import { Button, - Stack, - Image, HStack, - useColorModeValue, + Image, SimpleGrid, + Stack, Text, -} from '@chakra-ui/react' -import { useEffect, useMemo, useRef, useState } from 'react' -import { iconNames } from './iconNames' -import { TextInput } from '../inputs' -import { ColorPicker } from '../ColorPicker' -import { useTranslate } from '@tolgee/react' + useColorModeValue, +} from "@chakra-ui/react"; +import { useTranslate } from "@tolgee/react"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { ColorPicker } from "../ColorPicker"; +import { TextInput } from "../inputs"; +import { iconNames } from "./iconNames"; -const batchSize = 200 +const batchSize = 200; type Props = { - onIconSelected: (url: string) => void -} + onIconSelected: (url: string) => void; +}; -const localStorageRecentIconNamesKey = 'recentIconNames' -const localStorageDefaultIconColorKey = 'defaultIconColor' +const localStorageRecentIconNamesKey = "recentIconNames"; +const localStorageDefaultIconColorKey = "defaultIconColor"; export const IconPicker = ({ onIconSelected }: Props) => { - const initialIconColor = useColorModeValue('#222222', '#ffffff') - const scrollContainer = useRef(null) - const bottomElement = useRef(null) + const initialIconColor = useColorModeValue("#222222", "#ffffff"); + const scrollContainer = useRef(null); + const bottomElement = useRef(null); const [displayedIconNames, setDisplayedIconNames] = useState( - iconNames.slice(0, batchSize) - ) - const searchQuery = useRef('') - const [selectedColor, setSelectedColor] = useState(initialIconColor) + iconNames.slice(0, batchSize), + ); + const searchQuery = useRef(""); + const [selectedColor, setSelectedColor] = useState(initialIconColor); const isWhite = useMemo( () => - initialIconColor === '#222222' && - (selectedColor.toLowerCase() === '#ffffff' || - selectedColor.toLowerCase() === '#fff' || - selectedColor === 'white'), - [initialIconColor, selectedColor] - ) - const [recentIconNames, setRecentIconNames] = useState([]) - const { t } = useTranslate() + initialIconColor === "#222222" && + (selectedColor.toLowerCase() === "#ffffff" || + selectedColor.toLowerCase() === "#fff" || + selectedColor === "white"), + [initialIconColor, selectedColor], + ); + const [recentIconNames, setRecentIconNames] = useState([]); + const { t } = useTranslate(); useEffect(() => { - const recentIconNames = localStorage.getItem(localStorageRecentIconNamesKey) + const recentIconNames = localStorage.getItem( + localStorageRecentIconNamesKey, + ); const defaultIconColor = localStorage.getItem( - localStorageDefaultIconColorKey - ) - if (recentIconNames) setRecentIconNames(JSON.parse(recentIconNames)) - if (defaultIconColor) setSelectedColor(defaultIconColor) - }, []) + localStorageDefaultIconColorKey, + ); + if (recentIconNames) setRecentIconNames(JSON.parse(recentIconNames)); + if (defaultIconColor) setSelectedColor(defaultIconColor); + }, []); useEffect(() => { - if (!bottomElement.current) return + if (!bottomElement.current) return; const observer = new IntersectionObserver(handleObserver, { root: scrollContainer.current, - rootMargin: '200px', - }) - if (bottomElement.current) observer.observe(bottomElement.current) + rootMargin: "200px", + }); + if (bottomElement.current) observer.observe(bottomElement.current); return () => { - observer.disconnect() - } - }, []) + observer.disconnect(); + }; + }, []); const handleObserver = (entities: IntersectionObserverEntry[]) => { - const target = entities[0] + const target = entities[0]; if (target.isIntersecting && searchQuery.current.length <= 2) setDisplayedIconNames((displayedIconNames) => [ ...displayedIconNames, ...iconNames.slice( displayedIconNames.length, - displayedIconNames.length + batchSize + displayedIconNames.length + batchSize, ), - ]) - } + ]); + }; const searchIcon = async (query: string) => { - searchQuery.current = query + searchQuery.current = query; if (query.length <= 2) - return setDisplayedIconNames(iconNames.slice(0, batchSize)) + return setDisplayedIconNames(iconNames.slice(0, batchSize)); const filteredIconNames = iconNames.filter((iconName) => - iconName.toLowerCase().includes(query.toLowerCase()) - ) - setDisplayedIconNames(filteredIconNames) - } + iconName.toLowerCase().includes(query.toLowerCase()), + ); + setDisplayedIconNames(filteredIconNames); + }; const updateColor = (color: string) => { - if (!color.startsWith('#')) return - localStorage.setItem(localStorageDefaultIconColorKey, color) - setSelectedColor(color) - } + if (!color.startsWith("#")) return; + localStorage.setItem(localStorageDefaultIconColorKey, color); + setSelectedColor(color); + }; const selectIcon = async (iconName: string) => { localStorage.setItem( localStorageRecentIconNamesKey, - JSON.stringify([...new Set([iconName, ...recentIconNames].slice(0, 30))]) - ) - const svg = await (await fetch(`/icons/${iconName}.svg`)).text() + JSON.stringify([...new Set([iconName, ...recentIconNames].slice(0, 30))]), + ); + const svg = await (await fetch(`/icons/${iconName}.svg`)).text(); const dataUri = `data:image/svg+xml;utf8,${svg - .replace(' { {recentIconNames.map((iconName) => ( )} - {displayedTabs.includes('upload') && ( + {displayedTabs.includes("upload") && ( )} - {displayedTabs.includes('emoji') && ( + {displayedTabs.includes("emoji") && ( )} - {displayedTabs.includes('giphy') && ( + {displayedTabs.includes("giphy") && ( )} - {displayedTabs.includes('unsplash') && ( + {displayedTabs.includes("unsplash") && ( )} - {displayedTabs.includes('icon') && ( + {displayedTabs.includes("icon") && ( @@ -80,5 +80,5 @@ export const NewVersionPopup = () => { - ) -} + ); +}; diff --git a/apps/builder/src/components/PrimitiveList.tsx b/apps/builder/src/components/PrimitiveList.tsx index e41da46ef5..b4e9debfa6 100644 --- a/apps/builder/src/components/PrimitiveList.tsx +++ b/apps/builder/src/components/PrimitiveList.tsx @@ -1,86 +1,89 @@ -import { Box, Button, Fade, Flex, IconButton, Stack } from '@chakra-ui/react' -import { TrashIcon, PlusIcon } from '@/components/icons' -import React, { useEffect, useState } from 'react' -import { createId } from '@paralleldrive/cuid2' +import { PlusIcon, TrashIcon } from "@/components/icons"; +import { Box, Button, Fade, Flex, IconButton, Stack } from "@chakra-ui/react"; +import { createId } from "@paralleldrive/cuid2"; +import React, { useEffect, useState } from "react"; -type ItemWithId = { id: string; value?: T } +type ItemWithId = { + id: string; + value?: T; +}; export type TableListItemProps = { - item: T - onItemChange: (item: T) => void -} + item: T; + onItemChange: (item: T) => void; +}; type Props = { - initialItems?: T[] - addLabel?: string - newItemDefaultProps?: Partial - hasDefaultItem?: boolean - ComponentBetweenItems?: (props: unknown) => JSX.Element - onItemsChange: (items: T[]) => void - children: (props: TableListItemProps) => JSX.Element -} + initialItems?: T[]; + addLabel?: string; + newItemDefaultProps?: Partial; + hasDefaultItem?: boolean; + ComponentBetweenItems?: (props: unknown) => JSX.Element; + onItemsChange: (items: T[]) => void; + children: (props: TableListItemProps) => JSX.Element; +}; const addIdToItems = ( - items: T[] -): ItemWithId[] => items.map((item) => ({ id: createId(), value: item })) + items: T[], +): ItemWithId[] => items.map((item) => ({ id: createId(), value: item })); const removeIdFromItems = ( - items: ItemWithId[] -): T[] => items.map((item) => item.value as T) + items: ItemWithId[], +): T[] => items.map((item) => item.value as T); export const PrimitiveList = ({ initialItems, - addLabel = 'Add', + addLabel = "Add", hasDefaultItem, children, ComponentBetweenItems, onItemsChange, }: Props) => { - const [items, setItems] = useState[]>() - const [showDeleteIndex, setShowDeleteIndex] = useState(null) + const [items, setItems] = useState[]>(); + const [showDeleteIndex, setShowDeleteIndex] = useState(null); useEffect(() => { - if (items) return + if (items) return; if (initialItems) { - setItems(addIdToItems(initialItems)) + setItems(addIdToItems(initialItems)); } else if (hasDefaultItem) { - setItems(addIdToItems([])) + setItems(addIdToItems([])); } else { - setItems([]) + setItems([]); } - }, [hasDefaultItem, initialItems, items]) + }, [hasDefaultItem, initialItems, items]); const createItem = () => { - if (!items) return - const newItems = [...items, { id: createId() }] - setItems(newItems) - onItemsChange(removeIdFromItems(newItems)) - } + if (!items) return; + const newItems = [...items, { id: createId() }]; + setItems(newItems); + onItemsChange(removeIdFromItems(newItems)); + }; const updateItem = (itemIndex: number, newValue: T) => { - if (!items) return + if (!items) return; const newItems = items.map((item, idx) => - idx === itemIndex ? { ...item, value: newValue } : item - ) - setItems(newItems) - onItemsChange(removeIdFromItems(newItems)) - } + idx === itemIndex ? { ...item, value: newValue } : item, + ); + setItems(newItems); + onItemsChange(removeIdFromItems(newItems)); + }; const deleteItem = (itemIndex: number) => () => { - if (!items) return - const newItems = [...items] - newItems.splice(itemIndex, 1) - setItems([...newItems]) - onItemsChange(removeIdFromItems([...newItems])) - } + if (!items) return; + const newItems = [...items]; + newItems.splice(itemIndex, 1); + setItems([...newItems]); + onItemsChange(removeIdFromItems([...newItems])); + }; const handleMouseEnter = (itemIndex: number) => () => - setShowDeleteIndex(itemIndex) + setShowDeleteIndex(itemIndex); const handleCellChange = (itemIndex: number) => (item: T) => - updateItem(itemIndex, item) + updateItem(itemIndex, item); - const handleMouseLeave = () => setShowDeleteIndex(null) + const handleMouseLeave = () => setShowDeleteIndex(null); return ( @@ -104,9 +107,9 @@ export const PrimitiveList = ({ ({ {addLabel} - ) -} + ); +}; diff --git a/apps/builder/src/components/Seo.tsx b/apps/builder/src/components/Seo.tsx index 34dd9f8edb..99a3f3861e 100644 --- a/apps/builder/src/components/Seo.tsx +++ b/apps/builder/src/components/Seo.tsx @@ -1,25 +1,25 @@ -import { env } from '@typebot.io/env' -import Head from 'next/head' +import { env } from "@typebot.io/env"; +import Head from "next/head"; const getOrigin = () => { - if (typeof window !== 'undefined') { - return window.location.origin + if (typeof window !== "undefined") { + return window.location.origin; } - return env.NEXTAUTH_URL -} + return env.NEXTAUTH_URL; +}; export const Seo = ({ title, - description = 'Create and publish conversational forms that collect 4 times more answers and feel native to your product', + description = "Create and publish conversational forms that collect 4 times more answers and feel native to your product", imagePreviewUrl = `${getOrigin()}/images/og.png`, }: { - title: string - description?: string - currentUrl?: string - imagePreviewUrl?: string + title: string; + description?: string; + currentUrl?: string; + imagePreviewUrl?: string; }) => { - const formattedTitle = `${title} | Typebot` + const formattedTitle = `${title} | Typebot`; return ( @@ -38,5 +38,5 @@ export const Seo = ({ - ) -} + ); +}; diff --git a/apps/builder/src/components/SetVariableLabel.tsx b/apps/builder/src/components/SetVariableLabel.tsx index 4efd273958..36f4b30f74 100644 --- a/apps/builder/src/components/SetVariableLabel.tsx +++ b/apps/builder/src/components/SetVariableLabel.tsx @@ -1,29 +1,29 @@ -import { useColorModeValue, HStack, Tag, Text } from '@chakra-ui/react' -import { useTranslate } from '@tolgee/react' -import { Variable } from '@typebot.io/schemas' +import { HStack, Tag, Text, useColorModeValue } from "@chakra-ui/react"; +import { useTranslate } from "@tolgee/react"; +import type { Variable } from "@typebot.io/variables/schemas"; export const SetVariableLabel = ({ variableId, variables, }: { - variableId: string - variables?: Variable[] + variableId: string; + variables?: Variable[]; }) => { - const { t } = useTranslate() - const textColor = useColorModeValue('gray.600', 'gray.400') + const { t } = useTranslate(); + const textColor = useColorModeValue("gray.600", "gray.400"); const variableName = variables?.find( - (variable) => variable.id === variableId - )?.name + (variable) => variable.id === variableId, + )?.name; - if (!variableName) return null + if (!variableName) return null; return ( - {t('variables.set')} + {t("variables.set")} {variableName} - ) -} + ); +}; diff --git a/apps/builder/src/components/SupportBubble.tsx b/apps/builder/src/components/SupportBubble.tsx index b9b6b84fe3..9539e725f3 100644 --- a/apps/builder/src/components/SupportBubble.tsx +++ b/apps/builder/src/components/SupportBubble.tsx @@ -1,43 +1,44 @@ -import { useTypebot } from '@/features/editor/providers/TypebotProvider' -import { useUser } from '@/features/account/hooks/useUser' -import { useWorkspace } from '@/features/workspace/WorkspaceProvider' -import React, { useEffect, useState } from 'react' -import { Bubble, BubbleProps } from '@typebot.io/nextjs' -import { planToReadable } from '@/features/billing/helpers/planToReadable' -import { Plan } from '@typebot.io/prisma' +import { useUser } from "@/features/account/hooks/useUser"; +import { planToReadable } from "@/features/billing/helpers/planToReadable"; +import { useTypebot } from "@/features/editor/providers/TypebotProvider"; +import { useWorkspace } from "@/features/workspace/WorkspaceProvider"; +import type { BubbleProps } from "@typebot.io/js"; +import { Bubble } from "@typebot.io/nextjs"; +import { Plan } from "@typebot.io/prisma/enum"; +import { useEffect, useState } from "react"; -export const SupportBubble = (props: Omit) => { - const { typebot } = useTypebot() - const { user } = useUser() - const { workspace } = useWorkspace() +export const SupportBubble = (props: Omit) => { + const { typebot } = useTypebot(); + const { user } = useUser(); + const { workspace } = useWorkspace(); - const [lastViewedTypebotId, setLastViewedTypebotId] = useState(typebot?.id) + const [lastViewedTypebotId, setLastViewedTypebotId] = useState(typebot?.id); useEffect(() => { - if (!typebot?.id) return - if (lastViewedTypebotId === typebot?.id) return - setLastViewedTypebotId(typebot?.id) - }, [lastViewedTypebotId, typebot?.id]) + if (!typebot?.id) return; + if (lastViewedTypebotId === typebot?.id) return; + setLastViewedTypebotId(typebot?.id); + }, [lastViewedTypebotId, typebot?.id]); - if (!workspace?.plan || workspace.plan === Plan.FREE) return null + if (!workspace?.plan || workspace.plan === Plan.FREE) return null; return ( - ) -} + ); +}; diff --git a/apps/builder/src/components/SwitchWithRelatedSettings.tsx b/apps/builder/src/components/SwitchWithRelatedSettings.tsx index 9e26394a1e..728723fe71 100644 --- a/apps/builder/src/components/SwitchWithRelatedSettings.tsx +++ b/apps/builder/src/components/SwitchWithRelatedSettings.tsx @@ -1,17 +1,18 @@ -import React from 'react' -import { SwitchWithLabel, SwitchWithLabelProps } from './inputs/SwitchWithLabel' -import { Stack } from '@chakra-ui/react' +import { Stack } from "@chakra-ui/react"; +import React from "react"; +import type { SwitchWithLabelProps } from "./inputs/SwitchWithLabel"; +import { SwitchWithLabel } from "./inputs/SwitchWithLabel"; -type Props = SwitchWithLabelProps +type Props = SwitchWithLabelProps; export const SwitchWithRelatedSettings = ({ children, ...props }: Props) => ( {props.initialValue && children} -) +); diff --git a/apps/builder/src/components/TableList.tsx b/apps/builder/src/components/TableList.tsx index a12716346b..b9a5a74b91 100644 --- a/apps/builder/src/components/TableList.tsx +++ b/apps/builder/src/components/TableList.tsx @@ -1,3 +1,4 @@ +import { PlusIcon, TrashIcon } from "@/components/icons"; import { Box, Button, @@ -6,35 +7,34 @@ import { IconButton, SlideFade, Stack, -} from '@chakra-ui/react' -import { TrashIcon, PlusIcon } from '@/components/icons' -import { createId } from '@paralleldrive/cuid2' -import React, { useEffect, useState } from 'react' +} from "@chakra-ui/react"; +import { createId } from "@paralleldrive/cuid2"; +import React, { useEffect, useState } from "react"; const defaultItem = { id: createId(), -} +}; export type TableListItemProps = { - item: T - onItemChange: (item: T) => void -} + item: T; + onItemChange: (item: T) => void; +}; type Props = { - initialItems?: T[] - isOrdered?: boolean - addLabel?: string - newItemDefaultProps?: Partial - hasDefaultItem?: boolean - ComponentBetweenItems?: (props: unknown) => JSX.Element - onItemsChange: (items: T[]) => void - children: (props: TableListItemProps) => JSX.Element -} + initialItems?: T[]; + isOrdered?: boolean; + addLabel?: string; + newItemDefaultProps?: Partial; + hasDefaultItem?: boolean; + ComponentBetweenItems?: (props: unknown) => JSX.Element; + onItemsChange: (items: T[]) => void; + children: (props: TableListItemProps) => JSX.Element; +}; export const TableList = ({ initialItems, isOrdered, - addLabel = 'Add', + addLabel = "Add", newItemDefaultProps, hasDefaultItem, children, @@ -43,58 +43,58 @@ export const TableList = ({ }: Props) => { const [items, setItems] = useState( addIdsIfMissing(initialItems) ?? - (hasDefaultItem ? ([defaultItem] as T[]) : []) - ) - const [showDeleteIndex, setShowDeleteIndex] = useState(null) + (hasDefaultItem ? ([defaultItem] as T[]) : []), + ); + const [showDeleteIndex, setShowDeleteIndex] = useState(null); useEffect(() => { if (items.length && initialItems && initialItems?.length === 0) - setItems(initialItems) - }, [initialItems, items.length]) + setItems(initialItems); + }, [initialItems, items.length]); const createItem = () => { - const id = createId() - const newItem = { id, ...newItemDefaultProps } as T - setItems([...items, newItem]) - onItemsChange([...items, newItem]) - } + const id = createId(); + const newItem = { id, ...newItemDefaultProps } as T; + setItems([...items, newItem]); + onItemsChange([...items, newItem]); + }; const insertItem = (itemIndex: number) => () => { - const id = createId() - const newItem = { id } as T - const newItems = [...items] - newItems.splice(itemIndex + 1, 0, newItem) - setItems(newItems) - onItemsChange(newItems) - } + const id = createId(); + const newItem = { id } as T; + const newItems = [...items]; + newItems.splice(itemIndex + 1, 0, newItem); + setItems(newItems); + onItemsChange(newItems); + }; const updateItem = (itemIndex: number, updates: Partial) => { const newItems = items.map((item, idx) => - idx === itemIndex ? { ...item, ...updates } : item - ) - setItems(newItems) - onItemsChange(newItems) - } + idx === itemIndex ? { ...item, ...updates } : item, + ); + setItems(newItems); + onItemsChange(newItems); + }; const deleteItem = (itemIndex: number) => () => { - const newItems = [...items] - newItems.splice(itemIndex, 1) - setItems([...newItems]) - onItemsChange([...newItems]) - } + const newItems = [...items]; + newItems.splice(itemIndex, 1); + setItems([...newItems]); + onItemsChange([...newItems]); + }; const handleMouseEnter = (itemIndex: number) => () => - setShowDeleteIndex(itemIndex) + setShowDeleteIndex(itemIndex); const handleCellChange = (itemIndex: number) => (item: T) => - updateItem(itemIndex, item) + updateItem(itemIndex, item); - const handleMouseLeave = () => setShowDeleteIndex(null) + const handleMouseLeave = () => setShowDeleteIndex(null); return ( {items.map((item, itemIndex) => ( - + {itemIndex !== 0 && ComponentBetweenItems && ( )} @@ -110,9 +110,9 @@ export const TableList = ({ @@ -131,8 +131,8 @@ export const TableList = ({ offsetY="-5px" in={showDeleteIndex === itemIndex} style={{ - position: 'absolute', - top: '-15px', + position: "absolute", + top: "-15px", }} unmountOnExit > @@ -150,8 +150,8 @@ export const TableList = ({ offsetY="5px" in={showDeleteIndex === itemIndex} style={{ - position: 'absolute', - bottom: '5px', + position: "absolute", + bottom: "5px", }} unmountOnExit > @@ -180,11 +180,11 @@ export const TableList = ({ )} - ) -} + ); +}; const addIdsIfMissing = (items?: T[]): T[] | undefined => items?.map((item) => ({ id: createId(), ...item, - })) + })); diff --git a/apps/builder/src/components/TagsInput.tsx b/apps/builder/src/components/TagsInput.tsx index c35ac010ae..d803456af8 100644 --- a/apps/builder/src/components/TagsInput.tsx +++ b/apps/builder/src/components/TagsInput.tsx @@ -1,100 +1,100 @@ +import { colors } from "@/lib/theme"; import { HStack, IconButton, - Wrap, + Input, Text, + Wrap, WrapItem, - Input, -} from '@chakra-ui/react' -import { useRef, useState } from 'react' -import { CloseIcon } from './icons' -import { colors } from '@/lib/theme' -import { AnimatePresence, motion } from 'framer-motion' -import { convertStrToList } from '@typebot.io/lib/convertStrToList' -import { isEmpty } from '@typebot.io/lib/utils' +} from "@chakra-ui/react"; +import { convertStrToList } from "@typebot.io/lib/convertStrToList"; +import { isEmpty } from "@typebot.io/lib/utils"; +import { AnimatePresence, motion } from "framer-motion"; +import { useRef, useState } from "react"; +import { CloseIcon } from "./icons"; type Props = { - items?: string[] - placeholder?: string - onChange: (value: string[]) => void -} + items?: string[]; + placeholder?: string; + onChange: (value: string[]) => void; +}; export const TagsInput = ({ items, placeholder, onChange }: Props) => { - const inputRef = useRef(null) - const [inputValue, setInputValue] = useState('') - const [isFocused, setIsFocused] = useState(false) - const [focusedTagIndex, setFocusedTagIndex] = useState() + const inputRef = useRef(null); + const [inputValue, setInputValue] = useState(""); + const [isFocused, setIsFocused] = useState(false); + const [focusedTagIndex, setFocusedTagIndex] = useState(); const handleInputChange = (e: React.ChangeEvent) => { - e.preventDefault() - setFocusedTagIndex(undefined) - setInputValue(e.target.value) + e.preventDefault(); + setFocusedTagIndex(undefined); + setInputValue(e.target.value); if (e.target.value.length - inputValue.length > 0) { - const values = convertStrToList(e.target.value) + const values = convertStrToList(e.target.value); if (values.length > 1) { - onChange([...(items ?? []), ...convertStrToList(e.target.value)]) - setInputValue('') + onChange([...(items ?? []), ...convertStrToList(e.target.value)]); + setInputValue(""); } } - } + }; const handleKeyDown = (e: React.KeyboardEvent) => { - if (!items) return + if (!items) return; - if (e.key === 'Backspace') { + if (e.key === "Backspace") { if (focusedTagIndex !== undefined) { if (focusedTagIndex === items.length - 1) { - setFocusedTagIndex((idx) => idx! - 1) + setFocusedTagIndex((idx) => idx! - 1); } - removeItem(focusedTagIndex) - return + removeItem(focusedTagIndex); + return; } - if (inputValue === '' && focusedTagIndex === undefined) { - setFocusedTagIndex(items?.length - 1) - return + if (inputValue === "" && focusedTagIndex === undefined) { + setFocusedTagIndex(items?.length - 1); + return; } } - if (e.key === 'ArrowLeft') { + if (e.key === "ArrowLeft") { if (focusedTagIndex !== undefined) { - if (focusedTagIndex === 0) return - setFocusedTagIndex(focusedTagIndex - 1) - return + if (focusedTagIndex === 0) return; + setFocusedTagIndex(focusedTagIndex - 1); + return; } if (inputRef.current?.selectionStart === 0 && items) { - setFocusedTagIndex(items.length - 1) - return + setFocusedTagIndex(items.length - 1); + return; } } - if (e.key === 'ArrowRight' && focusedTagIndex !== undefined) { + if (e.key === "ArrowRight" && focusedTagIndex !== undefined) { if (focusedTagIndex === items.length - 1) { - setFocusedTagIndex(undefined) - return + setFocusedTagIndex(undefined); + return; } - setFocusedTagIndex(focusedTagIndex + 1) + setFocusedTagIndex(focusedTagIndex + 1); } - } + }; const removeItem = (index: number) => { - if (!items) return - const newItems = [...items] - newItems.splice(index, 1) - onChange(newItems) - } + if (!items) return; + const newItems = [...items]; + newItems.splice(index, 1); + onChange(newItems); + }; const addItem = (e: React.FormEvent) => { - e.preventDefault() - if (isEmpty(inputValue)) return - setInputValue('') - onChange(items ? [...items, inputValue.trim()] : [inputValue.trim()]) - } + e.preventDefault(); + if (isEmpty(inputValue)) return; + setInputValue(""); + onChange(items ? [...items, inputValue.trim()] : [inputValue.trim()]); + }; return ( { {items?.map((item, index) => ( @@ -138,17 +138,17 @@ export const TagsInput = ({ items, placeholder, onChange }: Props) => { - ) -} + ); +}; const Tag = ({ isFocused, content, onDeleteClick, }: { - isFocused?: boolean - content: string - onDeleteClick: () => void + isFocused?: boolean; + content: string; + onDeleteClick: () => void; }) => ( {content} @@ -170,4 +170,4 @@ const Tag = ({ onClick={onDeleteClick} /> -) +); diff --git a/apps/builder/src/components/TextLink.tsx b/apps/builder/src/components/TextLink.tsx index 5bd28d2254..0db9da1902 100644 --- a/apps/builder/src/components/TextLink.tsx +++ b/apps/builder/src/components/TextLink.tsx @@ -1,9 +1,9 @@ -import Link, { LinkProps } from 'next/link' -import React from 'react' -import { chakra, HStack, TextProps } from '@chakra-ui/react' -import { ExternalLinkIcon } from '@/components/icons' +import { ExternalLinkIcon } from "@/components/icons"; +import { HStack, type TextProps, chakra } from "@chakra-ui/react"; +import Link, { type LinkProps } from "next/link"; +import React from "react"; -type TextLinkProps = LinkProps & TextProps & { isExternal?: boolean } +type TextLinkProps = LinkProps & TextProps & { isExternal?: boolean }; export const TextLink = ({ children, @@ -22,7 +22,7 @@ export const TextLink = ({ replace={replace} scroll={scroll} prefetch={prefetch} - target={isExternal ? '_blank' : undefined} + target={isExternal ? "_blank" : undefined} > {isExternal ? ( @@ -37,4 +37,4 @@ export const TextLink = ({ )} -) +); diff --git a/apps/builder/src/components/TimeSince.tsx b/apps/builder/src/components/TimeSince.tsx index 4dc614dea4..928edfb013 100644 --- a/apps/builder/src/components/TimeSince.tsx +++ b/apps/builder/src/components/TimeSince.tsx @@ -1,46 +1,46 @@ -import { T } from '@tolgee/react' +import { T } from "@tolgee/react"; type Props = { - date: string -} + date: string; +}; export const TimeSince = ({ date }: Props) => { const seconds = Math.floor( - (new Date().getTime() - new Date(date).getTime()) / 1000 - ) + (new Date().getTime() - new Date(date).getTime()) / 1000, + ); - let interval = seconds / 31536000 + let interval = seconds / 31536000; if (interval > 1) { return ( - ) + ); } - interval = seconds / 2592000 + interval = seconds / 2592000; if (interval > 1) { return ( - ) + ); } - interval = seconds / 86400 + interval = seconds / 86400; if (interval > 1) { return ( - ) + ); } - interval = seconds / 3600 + interval = seconds / 3600; if (interval > 1) { return ( - ) + ); } - interval = seconds / 60 + interval = seconds / 60; if (interval > 1) { return ( - ) + ); } return ( - ) -} + ); +}; diff --git a/apps/builder/src/components/Toast.tsx b/apps/builder/src/components/Toast.tsx index 47b1b6dae6..2198a23602 100644 --- a/apps/builder/src/components/Toast.tsx +++ b/apps/builder/src/components/Toast.tsx @@ -10,27 +10,27 @@ import { Stack, Text, useColorModeValue, -} from '@chakra-ui/react' -import { AlertIcon, CloseIcon, InfoIcon, SmileIcon } from './icons' -import { CodeEditor } from './inputs/CodeEditor' -import { LanguageName } from '@uiw/codemirror-extensions-langs' +} from "@chakra-ui/react"; +import type { LanguageName } from "@uiw/codemirror-extensions-langs"; +import { AlertIcon, CloseIcon, InfoIcon, SmileIcon } from "./icons"; +import { CodeEditor } from "./inputs/CodeEditor"; export type ToastProps = { - title?: string - description?: string + title?: string; + description?: string; details?: { - content: string - lang: LanguageName - } - status?: 'info' | 'error' | 'success' - icon?: React.ReactNode - primaryButton?: React.ReactNode - secondaryButton?: React.ReactNode - onClose: () => void -} + content: string; + lang: LanguageName; + }; + status?: "info" | "error" | "success"; + icon?: React.ReactNode; + primaryButton?: React.ReactNode; + secondaryButton?: React.ReactNode; + onClose: () => void; +}; export const Toast = ({ - status = 'error', + status = "error", title, description, details, @@ -39,8 +39,8 @@ export const Toast = ({ secondaryButton, onClose, }: ToastProps) => { - const bgColor = useColorModeValue('white', 'gray.800') - const detailsLabelColor = useColorModeValue('gray.600', 'gray.400') + const bgColor = useColorModeValue("white", "gray.800"); + const detailsLabelColor = useColorModeValue("gray.600", "gray.400"); return ( - {' '} + {" "} {title && {title}} @@ -106,19 +106,19 @@ export const Toast = ({ right={1} /> - ) -} + ); +}; const Icon = ({ customIcon, status, }: { - customIcon?: React.ReactNode - status: ToastProps['status'] + customIcon?: React.ReactNode; + status: ToastProps["status"]; }) => { - const accentColor = useColorModeValue('50', '0') - const color = parseColor(status) - const icon = parseIcon(status, customIcon) + const accentColor = useColorModeValue("50", "0"); + const color = parseColor(status); + const icon = parseIcon(status, customIcon); return ( - ) -} + ); +}; -const parseColor = (status: ToastProps['status']) => { - if (!status) return 'red' +const parseColor = (status: ToastProps["status"]) => { + if (!status) return "red"; switch (status) { - case 'error': - return 'red' - case 'success': - return 'green' - case 'info': - return 'blue' + case "error": + return "red"; + case "success": + return "green"; + case "info": + return "blue"; } -} +}; const parseIcon = ( - status: ToastProps['status'], - customIcon?: React.ReactNode + status: ToastProps["status"], + customIcon?: React.ReactNode, ) => { - if (customIcon) return customIcon + if (customIcon) return customIcon; switch (status) { - case 'error': - return - case 'success': - return - case 'info': - return + case "error": + return ; + case "success": + return ; + case "info": + return ; } -} +}; diff --git a/apps/builder/src/components/Toaster.tsx b/apps/builder/src/components/Toaster.tsx index 4a8d04cc7a..f418efd19a 100644 --- a/apps/builder/src/components/Toaster.tsx +++ b/apps/builder/src/components/Toaster.tsx @@ -1,9 +1,9 @@ -import { colors } from '@/lib/theme' -import { useColorMode, useColorModeValue } from '@chakra-ui/react' -import { Toaster as SonnerToaster } from 'sonner' +import { colors } from "@/lib/theme"; +import { useColorMode, useColorModeValue } from "@chakra-ui/react"; +import { Toaster as SonnerToaster } from "sonner"; export const Toaster = () => { - const { colorMode } = useColorMode() + const { colorMode } = useColorMode(); const theme = useColorModeValue( { bg: undefined, @@ -13,9 +13,9 @@ export const Toaster = () => { { bg: colors.gray[900], actionBg: colors.blue[400], - actionColor: 'white', - } - ) + actionColor: "white", + }, + ); return ( { }, }} /> - ) -} + ); +}; diff --git a/apps/builder/src/components/TypebotLogo.tsx b/apps/builder/src/components/TypebotLogo.tsx index 96a22d0812..b0fef87b7a 100644 --- a/apps/builder/src/components/TypebotLogo.tsx +++ b/apps/builder/src/components/TypebotLogo.tsx @@ -1,4 +1,4 @@ -import { IconProps, Icon } from '@chakra-ui/react' +import { Icon, type IconProps } from "@chakra-ui/react"; export const TypebotLogo = ({ isDark, @@ -9,7 +9,7 @@ export const TypebotLogo = ({ width="800" height="800" rx="80" - fill={isDark ? 'white' : '#0042DA'} + fill={isDark ? "white" : "#0042DA"} /> -) +); diff --git a/apps/builder/src/components/UnlockPlanAlertInfo.tsx b/apps/builder/src/components/UnlockPlanAlertInfo.tsx index 29d726c6e0..7db6c5c65e 100644 --- a/apps/builder/src/components/UnlockPlanAlertInfo.tsx +++ b/apps/builder/src/components/UnlockPlanAlertInfo.tsx @@ -1,23 +1,21 @@ +import type { ChangePlanModalProps } from "@/features/billing/components/ChangePlanModal"; +import { ChangePlanModal } from "@/features/billing/components/ChangePlanModal"; import { Alert, AlertIcon, - AlertProps, + type AlertProps, Button, HStack, Text, useDisclosure, -} from '@chakra-ui/react' -import React from 'react' -import { - ChangePlanModal, - ChangePlanModalProps, -} from '@/features/billing/components/ChangePlanModal' -import { useTranslate } from '@tolgee/react' +} from "@chakra-ui/react"; +import { useTranslate } from "@tolgee/react"; +import React from "react"; type Props = { - buttonLabel?: string + buttonLabel?: string; } & AlertProps & - Pick + Pick; export const UnlockPlanAlertInfo = ({ buttonLabel, @@ -25,8 +23,8 @@ export const UnlockPlanAlertInfo = ({ excludedPlans, ...props }: Props) => { - const { t } = useTranslate() - const { isOpen, onOpen, onClose } = useDisclosure() + const { t } = useTranslate(); + const { isOpen, onOpen, onClose } = useDisclosure(); return ( {props.children} - ) -} + ); +}; diff --git a/apps/builder/src/components/VideoUploadContent/PexelsPicker.tsx b/apps/builder/src/components/VideoUploadContent/PexelsPicker.tsx index a29c85bba4..b95c8b9d49 100644 --- a/apps/builder/src/components/VideoUploadContent/PexelsPicker.tsx +++ b/apps/builder/src/components/VideoUploadContent/PexelsPicker.tsx @@ -1,4 +1,3 @@ -/* eslint-disable react-hooks/exhaustive-deps */ import { Alert, AlertIcon, @@ -12,87 +11,93 @@ import { Spinner, Stack, Text, -} from '@chakra-ui/react' -import { isDefined } from '@typebot.io/lib' -import { useCallback, useEffect, useRef, useState } from 'react' -import { createClient, Video, ErrorResponse, Videos } from 'pexels' -import { TextInput } from '../inputs' -import { TextLink } from '../TextLink' -import { env } from '@typebot.io/env' -import { PexelsLogo } from '../logos/PexelsLogo' +} from "@chakra-ui/react"; +import { env } from "@typebot.io/env"; +import { isDefined } from "@typebot.io/lib/utils"; +import { + type ErrorResponse, + type Video, + type Videos, + createClient, +} from "pexels"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { TextLink } from "../TextLink"; +import { TextInput } from "../inputs"; +import { PexelsLogo } from "../logos/PexelsLogo"; -const client = createClient(env.NEXT_PUBLIC_PEXELS_API_KEY ?? 'dummy') +/* eslint-disable react-hooks/exhaustive-deps */ +const client = createClient(env.NEXT_PUBLIC_PEXELS_API_KEY ?? "dummy"); type Props = { - videoSize: 'large' | 'medium' | 'small' - onVideoSelect: (videoUrl: string) => void -} + videoSize: "large" | "medium" | "small"; + onVideoSelect: (videoUrl: string) => void; +}; export const PexelsPicker = ({ videoSize, onVideoSelect }: Props) => { - const [isFetching, setIsFetching] = useState(false) - const [videos, setVideos] = useState([]) - const [error, setError] = useState(null) - const [searchQuery, setSearchQuery] = useState('') - const scrollContainer = useRef(null) - const bottomAnchor = useRef(null) + const [isFetching, setIsFetching] = useState(false); + const [videos, setVideos] = useState([]); + const [error, setError] = useState(null); + const [searchQuery, setSearchQuery] = useState(""); + const scrollContainer = useRef(null); + const bottomAnchor = useRef(null); - const [nextPage, setNextPage] = useState(0) + const [nextPage, setNextPage] = useState(0); const fetchNewVideos = useCallback(async (query: string, page: number) => { - if (query === '') getInitialVideos() + if (query === "") getInitialVideos(); if (query.length <= 2) { - setNextPage(0) - return + setNextPage(0); + return; } - setError(null) - setIsFetching(true) + setError(null); + setIsFetching(true); try { const result = await client.videos.search({ query, per_page: 24, size: videoSize, page, - }) + }); if ((result as ErrorResponse).error) - setError((result as ErrorResponse).error) + setError((result as ErrorResponse).error); if (isDefined((result as Videos).videos)) { - if (page === 0) setVideos((result as Videos).videos) + if (page === 0) setVideos((result as Videos).videos); else setVideos((videos) => [ ...videos, ...((result as Videos)?.videos ?? []), - ]) - setNextPage((page) => page + 1) + ]); + setNextPage((page) => page + 1); } } catch (err) { - if (err && typeof err === 'object' && 'message' in err) - setError(err.message as string) - setError('Something went wrong') + if (err && typeof err === "object" && "message" in err) + setError(err.message as string); + setError("Something went wrong"); } - setIsFetching(false) - }, []) + setIsFetching(false); + }, []); useEffect(() => { - if (!bottomAnchor.current) return + if (!bottomAnchor.current) return; const observer = new IntersectionObserver( (entities: IntersectionObserverEntry[]) => { - const target = entities[0] - if (target.isIntersecting) fetchNewVideos(searchQuery, nextPage + 1) + const target = entities[0]; + if (target.isIntersecting) fetchNewVideos(searchQuery, nextPage + 1); }, { root: scrollContainer.current, - } - ) + }, + ); if (bottomAnchor.current && nextPage > 0) - observer.observe(bottomAnchor.current) + observer.observe(bottomAnchor.current); return () => { - observer.disconnect() - } - }, [fetchNewVideos, nextPage, searchQuery]) + observer.disconnect(); + }; + }, [fetchNewVideos, nextPage, searchQuery]); const getInitialVideos = async () => { - setError(null) - setIsFetching(true) + setError(null); + setIsFetching(true); client.videos .popular({ per_page: 24, @@ -100,31 +105,31 @@ export const PexelsPicker = ({ videoSize, onVideoSelect }: Props) => { }) .then((res) => { if ((res as ErrorResponse).error) { - setError((res as ErrorResponse).error) + setError((res as ErrorResponse).error); } - setVideos((res as Videos).videos) - setIsFetching(false) + setVideos((res as Videos).videos); + setIsFetching(false); }) .catch((err) => { - if (err && typeof err === 'object' && 'message' in err) - setError(err.message as string) - setError('Something went wrong') - setIsFetching(false) - }) - } + if (err && typeof err === "object" && "message" in err) + setError(err.message as string); + setError("Something went wrong"); + setIsFetching(false); + }); + }; const selectVideo = (video: Video) => { - const videoUrl = video.video_files[0].link - if (isDefined(videoUrl)) onVideoSelect(videoUrl) - } + const videoUrl = video.video_files[0].link; + if (isDefined(videoUrl)) onVideoSelect(videoUrl); + }; useEffect(() => { - if (!env.NEXT_PUBLIC_PEXELS_API_KEY) return - getInitialVideos() - }, []) + if (!env.NEXT_PUBLIC_PEXELS_API_KEY) return; + getInitialVideos(); + }, []); if (!env.NEXT_PUBLIC_PEXELS_API_KEY) - return NEXT_PUBLIC_PEXELS_API_KEY is missing in environment + return NEXT_PUBLIC_PEXELS_API_KEY is missing in environment; return ( @@ -133,8 +138,8 @@ export const PexelsPicker = ({ videoSize, onVideoSelect }: Props) => { autoFocus placeholder="Search..." onChange={(query) => { - setSearchQuery(query) - fetchNewVideos(query, 0) + setSearchQuery(query); + fetchNewVideos(query, 0); }} withVariableButton={false} debounceTimeout={500} @@ -174,41 +179,41 @@ export const PexelsPicker = ({ videoSize, onVideoSelect }: Props) => { )} - ) -} + ); +}; type PexelsVideoProps = { - video: Video - onClick: () => void -} + video: Video; + onClick: () => void; +}; const PexelsVideo = ({ video, onClick }: PexelsVideoProps) => { - const { user, url, video_pictures } = video - const [isImageHovered, setIsImageHovered] = useState(false) + const { user, url, video_pictures } = video; + const [isImageHovered, setIsImageHovered] = useState(false); const [thumbnailImage, setThumbnailImage] = useState( - video_pictures[0].picture - ) - const [imageIndex, setImageIndex] = useState(1) + video_pictures[0].picture, + ); + const [imageIndex, setImageIndex] = useState(1); useEffect(() => { - let interval: NodeJS.Timer + let interval: NodeJS.Timer; if (isImageHovered && video_pictures.length > 0) { interval = setInterval(() => { - setImageIndex((prevIndex) => (prevIndex + 1) % video_pictures.length) - setThumbnailImage(video_pictures[imageIndex].picture) - }, 200) + setImageIndex((prevIndex) => (prevIndex + 1) % video_pictures.length); + setThumbnailImage(video_pictures[imageIndex].picture); + }, 200); } else { - setThumbnailImage(video_pictures[0].picture) - setImageIndex(1) + setThumbnailImage(video_pictures[0].picture); + setImageIndex(1); } return () => { if (interval) { - clearInterval(interval) + clearInterval(interval); } - } - }, [isImageHovered, imageIndex, video_pictures]) + }; + }, [isImageHovered, imageIndex, video_pictures]); return ( { - ) -} + ); +}; diff --git a/apps/builder/src/components/VideoUploadContent/VideoLinkEmbedContent.tsx b/apps/builder/src/components/VideoUploadContent/VideoLinkEmbedContent.tsx index 2dda4fe9b3..d316aa26a3 100644 --- a/apps/builder/src/components/VideoUploadContent/VideoLinkEmbedContent.tsx +++ b/apps/builder/src/components/VideoUploadContent/VideoLinkEmbedContent.tsx @@ -1,38 +1,38 @@ -import { Stack, Text } from '@chakra-ui/react' -import { useTranslate } from '@tolgee/react' -import { VideoBubbleBlock } from '@typebot.io/schemas' -import { TextInput } from '@/components/inputs' -import { defaultVideoBubbleContent } from '@typebot.io/schemas/features/blocks/bubbles/video/constants' -import { SwitchWithLabel } from '../inputs/SwitchWithLabel' +import { TextInput } from "@/components/inputs"; +import { Stack, Text } from "@chakra-ui/react"; +import { useTranslate } from "@tolgee/react"; +import { defaultVideoBubbleContent } from "@typebot.io/blocks-bubbles/video/constants"; +import type { VideoBubbleBlock } from "@typebot.io/blocks-bubbles/video/schema"; +import { SwitchWithLabel } from "../inputs/SwitchWithLabel"; export const VideoLinkEmbedContent = ({ content, updateUrl, onSubmit, }: { - content?: VideoBubbleBlock['content'] - updateUrl: (url: string) => void - onSubmit: (content: VideoBubbleBlock['content']) => void + content?: VideoBubbleBlock["content"]; + updateUrl: (url: string) => void; + onSubmit: (content: VideoBubbleBlock["content"]) => void; }) => { - const { t } = useTranslate() + const { t } = useTranslate(); const updateAspectRatio = (aspectRatio?: string) => { return onSubmit({ ...content, aspectRatio, - }) - } + }); + }; const updateMaxWidth = (maxWidth?: string) => { return onSubmit({ ...content, maxWidth, - }) - } + }); + }; const updateAutoPlay = (isAutoplayEnabled: boolean) => { - return onSubmit({ ...content, isAutoplayEnabled }) - } + return onSubmit({ ...content, isAutoplayEnabled }); + }; const updateControlsDisplay = (areControlsDisplayed: boolean) => { if (areControlsDisplayed === false) { @@ -41,28 +41,28 @@ export const VideoLinkEmbedContent = ({ ...content, isAutoplayEnabled: true, areControlsDisplayed, - }) + }); } - return onSubmit({ ...content, areControlsDisplayed }) - } + return onSubmit({ ...content, areControlsDisplayed }); + }; return ( <> - {t('video.urlInput.helperText')} + {t("video.urlInput.helperText")} {content?.url && ( )} - {content?.url && content?.type === 'url' && ( + {content?.url && content?.type === "url" && ( )} - ) -} + ); +}; diff --git a/apps/builder/src/components/icons.tsx b/apps/builder/src/components/icons.tsx index bad82bc2e3..65137a372e 100644 --- a/apps/builder/src/components/icons.tsx +++ b/apps/builder/src/components/icons.tsx @@ -1,12 +1,12 @@ -import { IconProps, Icon, useColorModeValue } from '@chakra-ui/react' +import { Icon, type IconProps, useColorModeValue } from "@chakra-ui/react"; export const featherIconsBaseProps: IconProps = { - fill: 'none', - stroke: 'currentColor', - strokeWidth: '2px', - strokeLinecap: 'round', - strokeLinejoin: 'round', -} + fill: "none", + stroke: "currentColor", + strokeWidth: "2px", + strokeLinecap: "round", + strokeLinejoin: "round", +}; // 99% of these icons are from Feather icons (https://feathericons.com/) @@ -15,7 +15,7 @@ export const SettingsIcon = (props: IconProps) => ( -) +); export const LogOutIcon = (props: IconProps) => ( @@ -23,32 +23,32 @@ export const LogOutIcon = (props: IconProps) => ( -) +); export const ChevronLeftIcon = (props: IconProps) => ( -) +); export const ChevronRightIcon = (props: IconProps) => ( -) +); export const ChevronDownIcon = (props: IconProps) => ( -) +); export const PlusIcon = (props: IconProps) => ( -) +); export const FolderIcon = (props: IconProps) => ( ( > -) +); export const MoreVerticalIcon = (props: IconProps) => ( @@ -67,7 +67,7 @@ export const MoreVerticalIcon = (props: IconProps) => ( -) +); export const MoreHorizontalIcon = (props: IconProps) => ( @@ -75,7 +75,7 @@ export const MoreHorizontalIcon = (props: IconProps) => ( -) +); export const GlobeIcon = (props: IconProps) => ( @@ -83,13 +83,13 @@ export const GlobeIcon = (props: IconProps) => ( -) +); export const ToolIcon = (props: IconProps) => ( -) +); export const FolderPlusIcon = (props: IconProps) => ( @@ -97,7 +97,7 @@ export const FolderPlusIcon = (props: IconProps) => ( -) +); export const TextIcon = (props: IconProps) => ( @@ -105,7 +105,7 @@ export const TextIcon = (props: IconProps) => ( -) +); export const ImageIcon = (props: IconProps) => ( @@ -113,7 +113,7 @@ export const ImageIcon = (props: IconProps) => ( -) +); export const CalendarIcon = (props: IconProps) => ( @@ -122,21 +122,21 @@ export const CalendarIcon = (props: IconProps) => ( -) +); export const FlagIcon = (props: IconProps) => ( -) +); export const BoldIcon = (props: IconProps) => ( -) +); export const ItalicIcon = (props: IconProps) => ( @@ -144,21 +144,21 @@ export const ItalicIcon = (props: IconProps) => ( -) +); export const UnderlineIcon = (props: IconProps) => ( -) +); export const LinkIcon = (props: IconProps) => ( -) +); export const SaveIcon = (props: IconProps) => ( @@ -166,26 +166,26 @@ export const SaveIcon = (props: IconProps) => ( -) +); export const CheckIcon = (props: IconProps) => ( -) +); export const ChatIcon = (props: IconProps) => ( -) +); export const TrashIcon = (props: IconProps) => ( -) +); export const LayoutIcon = (props: IconProps) => ( @@ -193,20 +193,20 @@ export const LayoutIcon = (props: IconProps) => ( -) +); export const CodeIcon = (props: IconProps) => ( -) +); export const EditIcon = (props: IconProps) => ( -) +); export const UploadIcon = (props: IconProps) => ( @@ -215,7 +215,7 @@ export const UploadIcon = (props: IconProps) => ( -) +); export const DownloadIcon = (props: IconProps) => ( @@ -223,7 +223,7 @@ export const DownloadIcon = (props: IconProps) => ( -) +); export const NumberIcon = (props: IconProps) => ( @@ -232,40 +232,40 @@ export const NumberIcon = (props: IconProps) => ( -) +); export const EmailIcon = (props: IconProps) => ( -) +); export const PhoneIcon = (props: IconProps) => ( -) +); export const CheckSquareIcon = (props: IconProps) => ( -) +); export const FilterIcon = (props: IconProps) => ( -) +); export const UserIcon = (props: IconProps) => ( -) +); export const ExpandIcon = (props: IconProps) => ( @@ -274,7 +274,7 @@ export const ExpandIcon = (props: IconProps) => ( -) +); export const ExternalLinkIcon = (props: IconProps) => ( @@ -282,7 +282,7 @@ export const ExternalLinkIcon = (props: IconProps) => ( -) +); export const FilmIcon = (props: IconProps) => ( @@ -295,13 +295,13 @@ export const FilmIcon = (props: IconProps) => ( -) +); export const ThunderIcon = (props: IconProps) => ( -) +); export const GripIcon = (props: IconProps) => ( @@ -312,56 +312,56 @@ export const GripIcon = (props: IconProps) => ( -) +); export const LockedIcon = (props: IconProps) => ( -) +); export const UnlockedIcon = (props: IconProps) => ( -) +); export const UndoIcon = (props: IconProps) => ( -) +); export const RedoIcon = (props: IconProps) => ( -) +); export const FileIcon = (props: IconProps) => ( -) +); export const EyeIcon = (props: IconProps) => ( -) +); export const SendEmailIcon = (props: IconProps) => ( -) +); export const GithubIcon = (props: IconProps) => ( @@ -369,10 +369,10 @@ export const GithubIcon = (props: IconProps) => ( fillRule="evenodd" clipRule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" - fill={useColorModeValue('#24292f', 'white')} + fill={useColorModeValue("#24292f", "white")} /> -) +); export const UsersIcon = (props: IconProps) => ( @@ -381,7 +381,7 @@ export const UsersIcon = (props: IconProps) => ( -) +); export const AlignLeftTextIcon = (props: IconProps) => ( @@ -390,7 +390,7 @@ export const AlignLeftTextIcon = (props: IconProps) => ( -) +); export const BoxIcon = (props: IconProps) => ( @@ -398,7 +398,7 @@ export const BoxIcon = (props: IconProps) => ( -) +); export const HelpCircleIcon = (props: IconProps) => ( @@ -406,14 +406,14 @@ export const HelpCircleIcon = (props: IconProps) => ( -) +); export const CopyIcon = (props: IconProps) => ( -) +); export const TemplateIcon = (props: IconProps) => ( @@ -422,13 +422,13 @@ export const TemplateIcon = (props: IconProps) => ( -) +); export const MinusIcon = (props: IconProps) => ( -) +); export const LaptopIcon = (props: IconProps) => ( @@ -444,7 +444,7 @@ export const LaptopIcon = (props: IconProps) => ( strokeLinejoin="round" /> -) +); export const MouseIcon = (props: IconProps) => ( @@ -454,7 +454,7 @@ export const MouseIcon = (props: IconProps) => ( strokeLinecap="round" /> -) +); export const HardDriveIcon = (props: IconProps) => ( @@ -463,26 +463,26 @@ export const HardDriveIcon = (props: IconProps) => ( -) +); export const CreditCardIcon = (props: IconProps) => ( -) +); export const PlayIcon = (props: IconProps) => ( -) +); export const StarIcon = (props: IconProps) => ( -) +); export const BuoyIcon = (props: IconProps) => ( @@ -493,14 +493,14 @@ export const BuoyIcon = (props: IconProps) => ( -) +); export const EyeOffIcon = (props: IconProps) => ( -) +); export const AlertIcon = (props: IconProps) => ( @@ -508,14 +508,14 @@ export const AlertIcon = (props: IconProps) => ( -) +); export const CloudOffIcon = (props: IconProps) => ( -) +); export const ListIcon = (props: IconProps) => ( @@ -526,7 +526,7 @@ export const ListIcon = (props: IconProps) => ( -) +); export const PackageIcon = (props: IconProps) => ( @@ -535,14 +535,14 @@ export const PackageIcon = (props: IconProps) => ( -) +); export const CloseIcon = (props: IconProps) => ( -) +); export const NoRadiusIcon = (props: IconProps) => ( @@ -555,7 +555,7 @@ export const NoRadiusIcon = (props: IconProps) => ( mask="url(#path-1-inside-1_1009_3)" /> -) +); export const MediumRadiusIcon = (props: IconProps) => ( @@ -568,7 +568,7 @@ export const MediumRadiusIcon = (props: IconProps) => ( mask="url(#path-1-inside-1_1009_4)" /> -) +); export const LargeRadiusIcon = (props: IconProps) => ( @@ -581,19 +581,19 @@ export const LargeRadiusIcon = (props: IconProps) => ( mask="url(#path-1-inside-1_1009_5)" /> -) +); export const DropletIcon = (props: IconProps) => ( -) +); export const TableIcon = (props: IconProps) => ( -) +); export const ShuffleIcon = (props: IconProps) => ( @@ -603,7 +603,7 @@ export const ShuffleIcon = (props: IconProps) => ( -) +); export const InfoIcon = (props: IconProps) => ( @@ -611,7 +611,7 @@ export const InfoIcon = (props: IconProps) => ( -) +); export const SmileIcon = (props: IconProps) => ( @@ -620,21 +620,21 @@ export const SmileIcon = (props: IconProps) => ( -) +); export const BookIcon = (props: IconProps) => ( -) +); export const ChevronLastIcon = (props: IconProps) => ( -) +); export const XCircleIcon = (props: IconProps) => ( @@ -642,7 +642,7 @@ export const XCircleIcon = (props: IconProps) => ( -) +); export const LightBulbIcon = (props: IconProps) => ( @@ -650,7 +650,7 @@ export const LightBulbIcon = (props: IconProps) => ( -) +); export const UnlinkIcon = (props: IconProps) => ( @@ -661,7 +661,7 @@ export const UnlinkIcon = (props: IconProps) => ( -) +); export const RepeatIcon = (props: IconProps) => ( @@ -670,14 +670,14 @@ export const RepeatIcon = (props: IconProps) => ( -) +); export const BracesIcon = (props: IconProps) => ( -) +); export const VideoPopoverIcon = (props: IconProps) => ( @@ -694,7 +694,7 @@ export const VideoPopoverIcon = (props: IconProps) => ( strokeLinejoin="round" /> -) +); export const WalletIcon = (props: IconProps) => ( @@ -702,4 +702,4 @@ export const WalletIcon = (props: IconProps) => ( -) +); diff --git a/apps/builder/src/components/inputs/AutocompleteInput.tsx b/apps/builder/src/components/inputs/AutocompleteInput.tsx index 7af38e9175..e2b35723ec 100644 --- a/apps/builder/src/components/inputs/AutocompleteInput.tsx +++ b/apps/builder/src/components/inputs/AutocompleteInput.tsx @@ -1,37 +1,37 @@ +import { useParentModal } from "@/features/graph/providers/ParentModalProvider"; +import { VariablesButton } from "@/features/variables/components/VariablesButton"; +import { injectVariableInText } from "@/features/variables/helpers/injectVariableInTextInput"; +import { focusInput } from "@/helpers/focusInput"; +import { useOutsideClick } from "@/hooks/useOutsideClick"; import { - useDisclosure, - Popover, - PopoverContent, Button, - useColorModeValue, + FormControl, + HStack, + Input, + Popover, PopoverAnchor, + PopoverContent, Portal, - Input, - HStack, - FormControl, -} from '@chakra-ui/react' -import { useState, useRef, useEffect } from 'react' -import { useDebouncedCallback } from 'use-debounce' -import { isDefined } from '@typebot.io/lib' -import { useOutsideClick } from '@/hooks/useOutsideClick' -import { useParentModal } from '@/features/graph/providers/ParentModalProvider' -import { VariablesButton } from '@/features/variables/components/VariablesButton' -import { Variable } from '@typebot.io/schemas' -import { injectVariableInText } from '@/features/variables/helpers/injectVariableInTextInput' -import { focusInput } from '@/helpers/focusInput' -import { env } from '@typebot.io/env' + useColorModeValue, + useDisclosure, +} from "@chakra-ui/react"; +import { env } from "@typebot.io/env"; +import { isDefined } from "@typebot.io/lib/utils"; +import type { Variable } from "@typebot.io/variables/schemas"; +import { useEffect, useRef, useState } from "react"; +import { useDebouncedCallback } from "use-debounce"; type Props = { - items: string[] | undefined - value?: string - defaultValue?: string - debounceTimeout?: number - placeholder?: string - withVariableButton?: boolean - moreInfoTooltip?: string - isRequired?: boolean - onChange: (value: string) => void -} + items: string[] | undefined; + value?: string; + defaultValue?: string; + debounceTimeout?: number; + placeholder?: string; + withVariableButton?: boolean; + moreInfoTooltip?: string; + isRequired?: boolean; + onChange: (value: string) => void; +}; export const AutocompleteInput = ({ items, @@ -43,116 +43,116 @@ export const AutocompleteInput = ({ defaultValue, isRequired, }: Props) => { - const bg = useColorModeValue('gray.200', 'gray.700') - const { onOpen, onClose, isOpen } = useDisclosure() - const [isTouched, setIsTouched] = useState(false) - const [inputValue, setInputValue] = useState(defaultValue ?? '') + const bg = useColorModeValue("gray.200", "gray.700"); + const { onOpen, onClose, isOpen } = useDisclosure(); + const [isTouched, setIsTouched] = useState(false); + const [inputValue, setInputValue] = useState(defaultValue ?? ""); const [carretPosition, setCarretPosition] = useState( - inputValue.length ?? 0 - ) + inputValue.length ?? 0, + ); const onChange = useDebouncedCallback( _onChange, - env.NEXT_PUBLIC_E2E_TEST ? 0 : debounceTimeout - ) + env.NEXT_PUBLIC_E2E_TEST ? 0 : debounceTimeout, + ); useEffect(() => { - if (isTouched || inputValue !== '' || !defaultValue || defaultValue === '') - return - setInputValue(defaultValue ?? '') - }, [defaultValue, inputValue, isTouched]) + if (isTouched || inputValue !== "" || !defaultValue || defaultValue === "") + return; + setInputValue(defaultValue ?? ""); + }, [defaultValue, inputValue, isTouched]); const [keyboardFocusIndex, setKeyboardFocusIndex] = useState< number | undefined - >() - const dropdownRef = useRef(null) - const itemsRef = useRef<(HTMLButtonElement | null)[]>([]) - const inputRef = useRef(null) - const { ref: parentModalRef } = useParentModal() + >(); + const dropdownRef = useRef(null); + const itemsRef = useRef<(HTMLButtonElement | null)[]>([]); + const inputRef = useRef(null); + const { ref: parentModalRef } = useParentModal(); const filteredItems = ( - inputValue === '' - ? items ?? [] + inputValue === "" + ? (items ?? []) : [ ...(items ?? []).filter( (item) => - item.toLowerCase().startsWith((inputValue ?? '').toLowerCase()) && - item.toLowerCase() !== inputValue.toLowerCase() + item.toLowerCase().startsWith((inputValue ?? "").toLowerCase()) && + item.toLowerCase() !== inputValue.toLowerCase(), ), ] - ).slice(0, 50) + ).slice(0, 50); useOutsideClick({ ref: dropdownRef, handler: onClose, isEnabled: isOpen, - }) + }); useEffect( () => () => { - onChange.flush() + onChange.flush(); }, - [onChange] - ) + [onChange], + ); const changeValue = (value: string) => { - if (!isTouched) setIsTouched(true) - if (!isOpen) onOpen() - setInputValue(value) - onChange(value) - } + if (!isTouched) setIsTouched(true); + if (!isOpen) onOpen(); + setInputValue(value); + onChange(value); + }; const handleItemClick = (value: string) => () => { - setInputValue(value) - onChange(value) - setKeyboardFocusIndex(undefined) - inputRef.current?.focus() - } + setInputValue(value); + onChange(value); + setKeyboardFocusIndex(undefined); + inputRef.current?.focus(); + }; const updateFocusedDropdownItem = ( - e: React.KeyboardEvent + e: React.KeyboardEvent, ) => { - if (e.key === 'Enter' && isDefined(keyboardFocusIndex)) { - handleItemClick(filteredItems[keyboardFocusIndex])() - return setKeyboardFocusIndex(undefined) + if (e.key === "Enter" && isDefined(keyboardFocusIndex)) { + handleItemClick(filteredItems[keyboardFocusIndex])(); + return setKeyboardFocusIndex(undefined); } - if (e.key === 'ArrowDown') { - if (keyboardFocusIndex === undefined) return setKeyboardFocusIndex(0) + if (e.key === "ArrowDown") { + if (keyboardFocusIndex === undefined) return setKeyboardFocusIndex(0); if (keyboardFocusIndex === filteredItems.length - 1) - return setKeyboardFocusIndex(0) + return setKeyboardFocusIndex(0); itemsRef.current[keyboardFocusIndex + 1]?.scrollIntoView({ - behavior: 'smooth', - block: 'nearest', - }) - return setKeyboardFocusIndex(keyboardFocusIndex + 1) + behavior: "smooth", + block: "nearest", + }); + return setKeyboardFocusIndex(keyboardFocusIndex + 1); } - if (e.key === 'ArrowUp') { + if (e.key === "ArrowUp") { if (keyboardFocusIndex === 0 || keyboardFocusIndex === undefined) - return setKeyboardFocusIndex(filteredItems.length - 1) + return setKeyboardFocusIndex(filteredItems.length - 1); itemsRef.current[keyboardFocusIndex - 1]?.scrollIntoView({ - behavior: 'smooth', - block: 'nearest', - }) - setKeyboardFocusIndex(keyboardFocusIndex - 1) + behavior: "smooth", + block: "nearest", + }); + setKeyboardFocusIndex(keyboardFocusIndex - 1); } - } + }; const handleVariableSelected = (variable?: Variable) => { - if (!variable) return + if (!variable) return; const { text, carretPosition: newCarretPosition } = injectVariableInText({ variable, text: inputValue, at: carretPosition, - }) - changeValue(text) - focusInput({ at: newCarretPosition, input: inputRef.current }) - } + }); + changeValue(text); + focusInput({ at: newCarretPosition, input: inputRef.current }); + }; const updateCarretPosition = (e: React.FocusEvent) => { - const carretPosition = e.target.selectionStart - if (!carretPosition) return - setCarretPosition(carretPosition) - } + const carretPosition = e.target.selectionStart; + if (!carretPosition) return; + setCarretPosition(carretPosition); + }; return ( @@ -173,7 +173,7 @@ export const AutocompleteInput = ({ onFocus={onOpen} onBlur={updateCarretPosition} onKeyDown={updateFocusedDropdownItem} - placeholder={!items ? 'Loading...' : placeholder} + placeholder={!items ? "Loading..." : placeholder} isDisabled={!items} /> @@ -189,29 +189,27 @@ export const AutocompleteInput = ({ onMouseDown={(e) => e.stopPropagation()} onPointerDown={(e) => e.stopPropagation()} > - <> - {filteredItems.map((item, idx) => { - return ( - - ) - })} - + {filteredItems.map((item, idx) => { + return ( + + ); + })} )} @@ -221,5 +219,5 @@ export const AutocompleteInput = ({ )} - ) -} + ); +}; diff --git a/apps/builder/src/components/inputs/CodeEditor.tsx b/apps/builder/src/components/inputs/CodeEditor.tsx index 4b989a5f6e..f5169cfe84 100644 --- a/apps/builder/src/components/inputs/CodeEditor.tsx +++ b/apps/builder/src/components/inputs/CodeEditor.tsx @@ -1,5 +1,6 @@ +import { VariablesButton } from "@/features/variables/components/VariablesButton"; import { - BoxProps, + type BoxProps, Fade, FormControl, FormHelperText, @@ -8,36 +9,39 @@ import { Stack, useColorModeValue, useDisclosure, -} from '@chakra-ui/react' -import React, { ReactNode, useEffect, useRef, useState } from 'react' -import { useDebouncedCallback } from 'use-debounce' -import { VariablesButton } from '@/features/variables/components/VariablesButton' -import { Variable } from '@typebot.io/schemas' -import { env } from '@typebot.io/env' -import CodeMirror, { ReactCodeMirrorRef } from '@uiw/react-codemirror' -import { tokyoNight } from '@uiw/codemirror-theme-tokyo-night' -import { githubLight } from '@uiw/codemirror-theme-github' -import { LanguageName, loadLanguage } from '@uiw/codemirror-extensions-langs' -import { isDefined } from '@udecode/plate-common' -import { CopyButton } from '../CopyButton' -import { MoreInfoTooltip } from '../MoreInfoTooltip' +} from "@chakra-ui/react"; +import { env } from "@typebot.io/env"; +import type { Variable } from "@typebot.io/variables/schemas"; +import { isDefined } from "@udecode/plate-common"; +import { + type LanguageName, + loadLanguage, +} from "@uiw/codemirror-extensions-langs"; +import { githubLight } from "@uiw/codemirror-theme-github"; +import { tokyoNight } from "@uiw/codemirror-theme-tokyo-night"; +import CodeMirror, { type ReactCodeMirrorRef } from "@uiw/react-codemirror"; +import type { ReactNode } from "react"; +import React, { useEffect, useRef, useState } from "react"; +import { useDebouncedCallback } from "use-debounce"; +import { CopyButton } from "../CopyButton"; +import { MoreInfoTooltip } from "../MoreInfoTooltip"; type Props = { - label?: string - value?: string - defaultValue?: string - lang: LanguageName - isReadOnly?: boolean - debounceTimeout?: number - withVariableButton?: boolean - height?: string - maxHeight?: string - minWidth?: string - moreInfoTooltip?: string - helperText?: ReactNode - isRequired?: boolean - onChange?: (value: string) => void -} + label?: string; + value?: string; + defaultValue?: string; + lang: LanguageName; + isReadOnly?: boolean; + debounceTimeout?: number; + withVariableButton?: boolean; + height?: string; + maxHeight?: string; + minWidth?: string; + moreInfoTooltip?: string; + helperText?: ReactNode; + isRequired?: boolean; + onChange?: (value: string) => void; +}; export const CodeEditor = ({ label, defaultValue, @@ -46,57 +50,57 @@ export const CodeEditor = ({ helperText, isRequired, onChange, - height = '250px', - maxHeight = '70vh', + height = "250px", + maxHeight = "70vh", minWidth, withVariableButton = true, isReadOnly = false, debounceTimeout = 1000, ...props -}: Props & Omit) => { - const theme = useColorModeValue(githubLight, tokyoNight) - const codeEditor = useRef(null) - const [carretPosition, setCarretPosition] = useState(0) - const isVariableButtonDisplayed = withVariableButton && !isReadOnly - const [value, _setValue] = useState(defaultValue ?? '') - const { onOpen, onClose, isOpen } = useDisclosure() +}: Props & Omit) => { + const theme = useColorModeValue(githubLight, tokyoNight); + const codeEditor = useRef(null); + const [carretPosition, setCarretPosition] = useState(0); + const isVariableButtonDisplayed = withVariableButton && !isReadOnly; + const [value, _setValue] = useState(defaultValue ?? ""); + const { onOpen, onClose, isOpen } = useDisclosure(); const setValue = useDebouncedCallback( (value) => { - _setValue(value) - onChange && onChange(value) + _setValue(value); + onChange && onChange(value); }, - env.NEXT_PUBLIC_E2E_TEST ? 0 : debounceTimeout - ) + env.NEXT_PUBLIC_E2E_TEST ? 0 : debounceTimeout, + ); - const handleVariableSelected = (variable?: Pick) => { - codeEditor.current?.view?.focus() - const insert = `{{${variable?.name}}}` + const handleVariableSelected = (variable?: Pick) => { + codeEditor.current?.view?.focus(); + const insert = `{{${variable?.name}}}`; codeEditor.current?.view?.dispatch({ changes: { from: carretPosition, insert, }, selection: { anchor: carretPosition + insert.length }, - }) - } + }); + }; const handleChange = (newValue: string) => { - setValue(newValue) - } + setValue(newValue); + }; const rememberCarretPosition = () => { setCarretPosition( - codeEditor.current?.view?.state?.selection.asSingle().main.head ?? 0 - ) - } + codeEditor.current?.view?.state?.selection.asSingle().main.head ?? 0, + ); + }; useEffect( () => () => { - setValue.flush() + setValue.flush(); }, - [setValue] - ) + [setValue], + ); return ( {label && ( - {label}{' '} + {label}{" "} {moreInfoTooltip && ( {moreInfoTooltip} )} @@ -117,9 +121,9 @@ export const CodeEditor = ({ @@ -157,7 +161,7 @@ export const CodeEditor = ({ extensions={[loadLanguage(lang)].filter(isDefined)} editable={!isReadOnly} style={{ - width: isVariableButtonDisplayed ? 'calc(100% - 32px)' : '100%', + width: isVariableButtonDisplayed ? "calc(100% - 32px)" : "100%", }} spellCheck={false} basicSetup={{ @@ -186,5 +190,5 @@ export const CodeEditor = ({ {helperText && {helperText}} - ) -} + ); +}; diff --git a/apps/builder/src/components/inputs/NumberInput.tsx b/apps/builder/src/components/inputs/NumberInput.tsx index 8b7d7027df..fb10a7bbf3 100644 --- a/apps/builder/src/components/inputs/NumberInput.tsx +++ b/apps/builder/src/components/inputs/NumberInput.tsx @@ -1,40 +1,44 @@ -import { VariablesButton } from '@/features/variables/components/VariablesButton' +import { VariablesButton } from "@/features/variables/components/VariablesButton"; import { - NumberInputProps, NumberInput as ChakraNumberInput, - NumberInputField, - NumberInputStepper, - NumberIncrementStepper, - NumberDecrementStepper, - HStack, FormControl, + FormHelperText, FormLabel, + HStack, + NumberDecrementStepper, + NumberIncrementStepper, + NumberInputField, + type NumberInputProps, + NumberInputStepper, Stack, Text, - FormHelperText, -} from '@chakra-ui/react' -import { Variable, VariableString } from '@typebot.io/schemas' -import { ReactNode, useEffect, useState } from 'react' -import { useDebouncedCallback } from 'use-debounce' -import { env } from '@typebot.io/env' -import { MoreInfoTooltip } from '../MoreInfoTooltip' +} from "@chakra-ui/react"; +import { env } from "@typebot.io/env"; +import type { Variable, VariableString } from "@typebot.io/variables/schemas"; +import type { ReactNode } from "react"; +import { useEffect, useState } from "react"; +import { useDebouncedCallback } from "use-debounce"; +import { MoreInfoTooltip } from "../MoreInfoTooltip"; type Value = HasVariable extends true | undefined ? number | VariableString - : number + : number; type Props = { - defaultValue: Value | undefined - debounceTimeout?: number - withVariableButton?: HasVariable - label?: string - moreInfoTooltip?: string - isRequired?: boolean - direction?: 'row' | 'column' - suffix?: string - helperText?: ReactNode - onValueChange: (value?: Value) => void -} & Omit + defaultValue: Value | undefined; + debounceTimeout?: number; + withVariableButton?: HasVariable; + label?: string; + moreInfoTooltip?: string; + isRequired?: boolean; + direction?: "row" | "column"; + suffix?: string; + helperText?: ReactNode; + onValueChange: (value?: Value) => void; +} & Omit< + NumberInputProps, + "defaultValue" | "value" | "onChange" | "isRequired" +>; export const NumberInput = ({ defaultValue, @@ -44,57 +48,57 @@ export const NumberInput = ({ label, moreInfoTooltip, isRequired, - direction = 'column', + direction = "column", suffix, helperText, ...props }: Props) => { - const [isTouched, setIsTouched] = useState(false) - const [value, setValue] = useState(defaultValue?.toString() ?? '') + const [isTouched, setIsTouched] = useState(false); + const [value, setValue] = useState(defaultValue?.toString() ?? ""); const onValueChangeDebounced = useDebouncedCallback( onValueChange, - env.NEXT_PUBLIC_E2E_TEST ? 0 : debounceTimeout - ) + env.NEXT_PUBLIC_E2E_TEST ? 0 : debounceTimeout, + ); useEffect(() => { - if (isTouched || value !== '' || !defaultValue) return - setValue(defaultValue?.toString() ?? '') - }, [defaultValue, isTouched, value]) + if (isTouched || value !== "" || !defaultValue) return; + setValue(defaultValue?.toString() ?? ""); + }, [defaultValue, isTouched, value]); useEffect( () => () => { - onValueChangeDebounced.flush() + onValueChangeDebounced.flush(); }, - [onValueChangeDebounced] - ) + [onValueChangeDebounced], + ); const handleValueChange = (newValue: string) => { - if (!isTouched) setIsTouched(true) - if (value.startsWith('{{') && value.endsWith('}}') && newValue !== '') - return - setValue(newValue) - if (newValue.endsWith('.') || newValue.endsWith(',')) return - if (newValue === '') return onValueChangeDebounced(undefined) + if (!isTouched) setIsTouched(true); + if (value.startsWith("{{") && value.endsWith("}}") && newValue !== "") + return; + setValue(newValue); + if (newValue.endsWith(".") || newValue.endsWith(",")) return; + if (newValue === "") return onValueChangeDebounced(undefined); if ( - newValue.startsWith('{{') && - newValue.endsWith('}}') && + newValue.startsWith("{{") && + newValue.endsWith("}}") && newValue.length > 4 && (withVariableButton ?? true) ) { - onValueChangeDebounced(newValue as Value) - return + onValueChangeDebounced(newValue as Value); + return; } - const numberedValue = parseFloat(newValue) - if (isNaN(numberedValue)) return - onValueChangeDebounced(numberedValue) - } + const numberedValue = Number.parseFloat(newValue); + if (isNaN(numberedValue)) return; + onValueChangeDebounced(numberedValue); + }; const handleVariableSelected = (variable?: Variable) => { - if (!variable) return - const newValue = `{{${variable.name}}}` - handleValueChange(newValue) - } + if (!variable) return; + const newValue = `{{${variable.name}}}`; + handleValueChange(newValue); + }; const Input = ( ({ - ) + ); return ( {label && ( - {label}{' '} + {label}{" "} {moreInfoTooltip && ( {moreInfoTooltip} )} )} - - {withVariableButton ?? true ? ( + + {(withVariableButton ?? true) ? ( {Input} @@ -140,5 +144,5 @@ export const NumberInput = ({ {helperText ? {helperText} : null} - ) -} + ); +}; diff --git a/apps/builder/src/components/inputs/RadioButtons.tsx b/apps/builder/src/components/inputs/RadioButtons.tsx index 30afd23781..5753ad691f 100644 --- a/apps/builder/src/components/inputs/RadioButtons.tsx +++ b/apps/builder/src/components/inputs/RadioButtons.tsx @@ -2,58 +2,58 @@ import { Box, Flex, Stack, + type UseRadioProps, useColorModeValue, useRadio, useRadioGroup, - UseRadioProps, -} from '@chakra-ui/react' -import { ReactNode } from 'react' +} from "@chakra-ui/react"; +import type { ReactNode } from "react"; type Props = { - options: readonly (T | { value: T; label: ReactNode })[] - value?: T - defaultValue?: T - direction?: 'row' | 'column' - size?: 'md' | 'sm' - onSelect: (newValue: T) => void -} + options: readonly (T | { value: T; label: ReactNode })[]; + value?: T; + defaultValue?: T; + direction?: "row" | "column"; + size?: "md" | "sm"; + onSelect: (newValue: T) => void; +}; export const RadioButtons = ({ options, value, defaultValue, - direction = 'row', - size = 'md', + direction = "row", + size = "md", onSelect, }: Props) => { const { getRootProps, getRadioProps } = useRadioGroup({ value, defaultValue, onChange: onSelect, - }) + }); - const group = getRootProps() + const group = getRootProps(); return ( {options.map((item) => { - const radio = getRadioProps({ value: parseValue(item) }) + const radio = getRadioProps({ value: parseValue(item) }); return ( {parseLabel(item)} - ) + ); })} - ) -} + ); +}; export const RadioCard = ( - props: UseRadioProps & { children: ReactNode; size?: 'md' | 'sm' } + props: UseRadioProps & { children: ReactNode; size?: "md" | "sm" }, ) => { - const { getInputProps, getCheckboxProps } = useRadio(props) + const { getInputProps, getCheckboxProps } = useRadio(props); - const input = getInputProps() - const checkbox = getCheckboxProps() + const input = getInputProps(); + const checkbox = getCheckboxProps(); return ( @@ -64,28 +64,28 @@ export const RadioCard = ( borderWidth="2px" borderRadius="md" _checked={{ - borderColor: 'blue.400', + borderColor: "blue.400", }} _hover={{ - bgColor: useColorModeValue('gray.100', 'gray.700'), + bgColor: useColorModeValue("gray.100", "gray.700"), }} _active={{ - bgColor: useColorModeValue('gray.200', 'gray.600'), + bgColor: useColorModeValue("gray.200", "gray.600"), }} - px={props.size === 'sm' ? 3 : 5} - py={props.size === 'sm' ? 1 : 2} + px={props.size === "sm" ? 3 : 5} + py={props.size === "sm" ? 1 : 2} transition="background-color 150ms, color 150ms, border 150ms" justifyContent="center" - fontSize={props.size === 'sm' ? 'sm' : undefined} + fontSize={props.size === "sm" ? "sm" : undefined} > {props.children} - ) -} + ); +}; const parseValue = (item: string | { value: string; label: ReactNode }) => - typeof item === 'string' ? item : item.value + typeof item === "string" ? item : item.value; const parseLabel = (item: string | { value: string; label: ReactNode }) => - typeof item === 'string' ? item : item.label + typeof item === "string" ? item : item.label; diff --git a/apps/builder/src/components/inputs/Select.tsx b/apps/builder/src/components/inputs/Select.tsx index 604829ecf1..0a5fa4fb38 100644 --- a/apps/builder/src/components/inputs/Select.tsx +++ b/apps/builder/src/components/inputs/Select.tsx @@ -1,43 +1,44 @@ +import { useParentModal } from "@/features/graph/providers/ParentModalProvider"; +import { useOutsideClick } from "@/hooks/useOutsideClick"; import { - useDisclosure, + Box, + Button, Flex, - Popover, + HStack, + IconButton, Input, - PopoverContent, - Button, - useColorModeValue, - PopoverAnchor, - Portal, InputGroup, InputRightElement, + Popover, + PopoverAnchor, + PopoverContent, + Portal, Text, - Box, - IconButton, - HStack, -} from '@chakra-ui/react' -import { useState, useRef, ChangeEvent, useEffect } from 'react' -import { isDefined } from '@typebot.io/lib' -import { useOutsideClick } from '@/hooks/useOutsideClick' -import { useParentModal } from '@/features/graph/providers/ParentModalProvider' -import { ChevronDownIcon, CloseIcon } from '../icons' + useColorModeValue, + useDisclosure, +} from "@chakra-ui/react"; +import { isDefined } from "@typebot.io/lib/utils"; +import type { ChangeEvent } from "react"; +import { useEffect, useRef, useState } from "react"; +import { ChevronDownIcon, CloseIcon } from "../icons"; -const dropdownCloseAnimationDuration = 300 +const dropdownCloseAnimationDuration = 300; type RichItem = { - icon?: JSX.Element - label: string - value: string - extras?: Record -} + icon?: JSX.Element; + label: string; + value: string; + extras?: Record; +}; -type Item = string | RichItem +type Item = string | RichItem; type Props = { - selectedItem?: string - items: readonly T[] | undefined - placeholder?: string - onSelect?: (value: string | undefined, item?: T) => void -} + selectedItem?: string; + items: readonly T[] | undefined; + placeholder?: string; + onSelect?: (value: string | undefined, item?: T) => void; +}; export const Select = ({ selectedItem, @@ -45,115 +46,115 @@ export const Select = ({ items, onSelect, }: Props) => { - const focusedItemBgColor = useColorModeValue('gray.200', 'gray.700') - const selectedItemBgColor = useColorModeValue('blue.50', 'blue.400') - const [isTouched, setIsTouched] = useState(false) - const { onOpen, onClose, isOpen } = useDisclosure() + const focusedItemBgColor = useColorModeValue("gray.200", "gray.700"); + const selectedItemBgColor = useColorModeValue("blue.50", "blue.400"); + const [isTouched, setIsTouched] = useState(false); + const { onOpen, onClose, isOpen } = useDisclosure(); const [inputValue, setInputValue] = useState( getItemLabel( items?.find((item) => - typeof item === 'string' + typeof item === "string" ? selectedItem === item - : selectedItem === item.value - ) ?? selectedItem - ) - ) + : selectedItem === item.value, + ) ?? selectedItem, + ), + ); useEffect(() => { - if (!items || typeof items[0] === 'string' || !selectedItem || isTouched) - return + if (!items || typeof items[0] === "string" || !selectedItem || isTouched) + return; setInputValue( getItemLabel( (items as readonly RichItem[]).find( - (item) => selectedItem === item.value - ) ?? selectedItem - ) - ) - }, [isTouched, items, selectedItem]) + (item) => selectedItem === item.value, + ) ?? selectedItem, + ), + ); + }, [isTouched, items, selectedItem]); const [keyboardFocusIndex, setKeyboardFocusIndex] = useState< number | undefined - >() - const dropdownRef = useRef(null) - const itemsRef = useRef<(HTMLButtonElement | null)[]>([]) - const inputRef = useRef(null) - const { ref: parentModalRef } = useParentModal() + >(); + const dropdownRef = useRef(null); + const itemsRef = useRef<(HTMLButtonElement | null)[]>([]); + const inputRef = useRef(null); + const { ref: parentModalRef } = useParentModal(); const filteredItems = isTouched ? [ ...(items ?? []).filter((item) => getItemLabel(item) .toLowerCase() - .includes((inputValue ?? '').toLowerCase()) + .includes((inputValue ?? "").toLowerCase()), ), ] - : items ?? [] + : (items ?? []); const closeDropdown = () => { - onClose() + onClose(); setTimeout(() => { - setIsTouched(false) - }, dropdownCloseAnimationDuration) - } + setIsTouched(false); + }, dropdownCloseAnimationDuration); + }; useOutsideClick({ ref: dropdownRef, handler: closeDropdown, isEnabled: isOpen, - }) + }); const updateInputValue = (e: ChangeEvent) => { - if (!isOpen) onOpen() - if (!isTouched) setIsTouched(true) - setInputValue(e.target.value) - } + if (!isOpen) onOpen(); + if (!isTouched) setIsTouched(true); + setInputValue(e.target.value); + }; const handleItemClick = (item: T) => () => { - if (!isTouched) setIsTouched(true) - setInputValue(getItemLabel(item)) - onSelect?.(getItemValue(item), item) - setKeyboardFocusIndex(undefined) - closeDropdown() - } + if (!isTouched) setIsTouched(true); + setInputValue(getItemLabel(item)); + onSelect?.(getItemValue(item), item); + setKeyboardFocusIndex(undefined); + closeDropdown(); + }; const updateFocusedDropdownItem = ( - e: React.KeyboardEvent + e: React.KeyboardEvent, ) => { - if (e.key === 'Enter' && isDefined(keyboardFocusIndex)) { - e.preventDefault() - handleItemClick(filteredItems[keyboardFocusIndex])() - return setKeyboardFocusIndex(undefined) + if (e.key === "Enter" && isDefined(keyboardFocusIndex)) { + e.preventDefault(); + handleItemClick(filteredItems[keyboardFocusIndex])(); + return setKeyboardFocusIndex(undefined); } - if (e.key === 'ArrowDown') { - e.preventDefault() - if (keyboardFocusIndex === undefined) return setKeyboardFocusIndex(0) + if (e.key === "ArrowDown") { + e.preventDefault(); + if (keyboardFocusIndex === undefined) return setKeyboardFocusIndex(0); if (keyboardFocusIndex === filteredItems.length - 1) - return setKeyboardFocusIndex(0) + return setKeyboardFocusIndex(0); itemsRef.current[keyboardFocusIndex + 1]?.scrollIntoView({ - behavior: 'smooth', - block: 'nearest', - }) - return setKeyboardFocusIndex(keyboardFocusIndex + 1) + behavior: "smooth", + block: "nearest", + }); + return setKeyboardFocusIndex(keyboardFocusIndex + 1); } - if (e.key === 'ArrowUp') { - e.preventDefault() + if (e.key === "ArrowUp") { + e.preventDefault(); if (keyboardFocusIndex === 0 || keyboardFocusIndex === undefined) - return setKeyboardFocusIndex(filteredItems.length - 1) + return setKeyboardFocusIndex(filteredItems.length - 1); itemsRef.current[keyboardFocusIndex - 1]?.scrollIntoView({ - behavior: 'smooth', - block: 'nearest', - }) - setKeyboardFocusIndex(keyboardFocusIndex - 1) + behavior: "smooth", + block: "nearest", + }); + setKeyboardFocusIndex(keyboardFocusIndex - 1); } - } + }; const clearSelection = (e: React.MouseEvent) => { - e.preventDefault() - setInputValue('') - onSelect?.(undefined) - setKeyboardFocusIndex(undefined) - closeDropdown() - } + e.preventDefault(); + setInputValue(""); + onSelect?.(undefined); + setKeyboardFocusIndex(undefined); + closeDropdown(); + }; return ( @@ -186,13 +187,13 @@ export const Select = ({ autoComplete="off" ref={inputRef} className="select-input" - value={isTouched ? inputValue : ''} + value={isTouched ? inputValue : ""} placeholder={ !items - ? 'Loading...' - : !isTouched && inputValue !== '' - ? undefined - : placeholder + ? "Loading..." + : !isTouched && inputValue !== "" + ? undefined + : placeholder } onChange={updateInputValue} onFocus={onOpen} @@ -202,7 +203,7 @@ export const Select = ({ /> @@ -210,7 +211,7 @@ export const Select = ({ } - aria-label={'Clear'} + aria-label={"Clear"} size="sm" variant="ghost" pointerEvents="all" @@ -232,54 +233,49 @@ export const Select = ({ onMouseDown={(e) => e.stopPropagation()} onPointerDown={(e) => e.stopPropagation()} > - {filteredItems.length > 0 && ( - <> - {filteredItems.map((item, idx) => { - return ( - - ) - })} - - )} + : "transparent" + } + justifyContent="flex-start" + transition="none" + leftIcon={typeof item === "object" ? item.icon : undefined} + > + {getItemLabel(item)} + + ); + })} - ) -} + ); +}; const getItemLabel = (item?: Item) => { - if (!item) return '' - if (typeof item === 'object') return item.label - return item -} + if (!item) return ""; + if (typeof item === "object") return item.label; + return item; +}; const getItemValue = (item: Item) => { - if (typeof item === 'object') return item.value - return item -} + if (typeof item === "object") return item.value; + return item; +}; diff --git a/apps/builder/src/components/inputs/SwitchWithLabel.tsx b/apps/builder/src/components/inputs/SwitchWithLabel.tsx index da6cb0c64f..551b7f6a67 100644 --- a/apps/builder/src/components/inputs/SwitchWithLabel.tsx +++ b/apps/builder/src/components/inputs/SwitchWithLabel.tsx @@ -1,42 +1,42 @@ import { FormControl, - FormControlProps, + type FormControlProps, FormLabel, HStack, Switch, - SwitchProps, -} from '@chakra-ui/react' -import React, { useEffect, useState } from 'react' -import { MoreInfoTooltip } from '../MoreInfoTooltip' -import { isDefined } from '@typebot.io/lib' + type SwitchProps, +} from "@chakra-ui/react"; +import { isDefined } from "@typebot.io/lib/utils"; +import React, { useEffect, useState } from "react"; +import { MoreInfoTooltip } from "../MoreInfoTooltip"; export type SwitchWithLabelProps = { - label: string - initialValue: boolean | undefined - moreInfoContent?: string - onCheckChange?: (isChecked: boolean) => void - justifyContent?: FormControlProps['justifyContent'] -} & Omit + label: string; + initialValue: boolean | undefined; + moreInfoContent?: string; + onCheckChange?: (isChecked: boolean) => void; + justifyContent?: FormControlProps["justifyContent"]; +} & Omit; export const SwitchWithLabel = ({ label, initialValue, moreInfoContent, onCheckChange, - justifyContent = 'space-between', + justifyContent = "space-between", ...switchProps }: SwitchWithLabelProps) => { - const [isChecked, setIsChecked] = useState(initialValue) + const [isChecked, setIsChecked] = useState(initialValue); const handleChange = () => { - setIsChecked(!isChecked) - if (onCheckChange) onCheckChange(!isChecked) - } + setIsChecked(!isChecked); + if (onCheckChange) onCheckChange(!isChecked); + }; useEffect(() => { if (isChecked === undefined && isDefined(initialValue)) - setIsChecked(initialValue) - }, [initialValue, isChecked]) + setIsChecked(initialValue); + }, [initialValue, isChecked]); return ( @@ -50,5 +50,5 @@ export const SwitchWithLabel = ({ - ) -} + ); +}; diff --git a/apps/builder/src/components/inputs/TextInput.tsx b/apps/builder/src/components/inputs/TextInput.tsx index 3a3cd34e27..b6566f04bd 100644 --- a/apps/builder/src/components/inputs/TextInput.tsx +++ b/apps/builder/src/components/inputs/TextInput.tsx @@ -1,53 +1,54 @@ -import { VariablesButton } from '@/features/variables/components/VariablesButton' -import { injectVariableInText } from '@/features/variables/helpers/injectVariableInTextInput' -import { focusInput } from '@/helpers/focusInput' +import { VariablesButton } from "@/features/variables/components/VariablesButton"; +import { injectVariableInText } from "@/features/variables/helpers/injectVariableInTextInput"; +import { focusInput } from "@/helpers/focusInput"; import { + Input as ChakraInput, FormControl, FormHelperText, FormLabel, HStack, - Input as ChakraInput, - InputProps, + type InputProps, Stack, -} from '@chakra-ui/react' -import { Variable } from '@typebot.io/schemas' -import React, { +} from "@chakra-ui/react"; +import { env } from "@typebot.io/env"; +import type { Variable } from "@typebot.io/variables/schemas"; +import type { ReactNode } from "react"; +import type React from "react"; +import { forwardRef, - ReactNode, useEffect, useImperativeHandle, useRef, useState, -} from 'react' -import { useDebouncedCallback } from 'use-debounce' -import { env } from '@typebot.io/env' -import { MoreInfoTooltip } from '../MoreInfoTooltip' +} from "react"; +import { useDebouncedCallback } from "use-debounce"; +import { MoreInfoTooltip } from "../MoreInfoTooltip"; export type TextInputProps = { - forceDebounce?: boolean - defaultValue?: string - onChange?: (value: string) => void - debounceTimeout?: number - label?: ReactNode - helperText?: ReactNode - moreInfoTooltip?: string - withVariableButton?: boolean - isRequired?: boolean - placeholder?: string - isDisabled?: boolean - direction?: 'row' | 'column' - width?: 'full' + forceDebounce?: boolean; + defaultValue?: string; + onChange?: (value: string) => void; + debounceTimeout?: number; + label?: ReactNode; + helperText?: ReactNode; + moreInfoTooltip?: string; + withVariableButton?: boolean; + isRequired?: boolean; + placeholder?: string; + isDisabled?: boolean; + direction?: "row" | "column"; + width?: "full"; } & Pick< InputProps, - | 'autoComplete' - | 'onFocus' - | 'onKeyUp' - | 'type' - | 'autoFocus' - | 'size' - | 'maxWidth' - | 'flexShrink' -> + | "autoComplete" + | "onFocus" + | "onKeyUp" + | "type" + | "autoFocus" + | "size" + | "maxWidth" + | "flexShrink" +>; export const TextInput = forwardRef(function TextInput( { @@ -69,60 +70,60 @@ export const TextInput = forwardRef(function TextInput( onKeyUp, size, maxWidth, - direction = 'column', + direction = "column", width, flexShrink, }: TextInputProps, - ref + ref, ) { - const inputRef = useRef(null) - useImperativeHandle(ref, () => inputRef.current) - const [isTouched, setIsTouched] = useState(false) - const [localValue, setLocalValue] = useState(defaultValue ?? '') + const inputRef = useRef(null); + useImperativeHandle(ref, () => inputRef.current); + const [isTouched, setIsTouched] = useState(false); + const [localValue, setLocalValue] = useState(defaultValue ?? ""); const [carretPosition, setCarretPosition] = useState( - localValue.length ?? 0 - ) + localValue.length ?? 0, + ); const onChange = useDebouncedCallback( // eslint-disable-next-line @typescript-eslint/no-empty-function _onChange ?? (() => {}), - env.NEXT_PUBLIC_E2E_TEST && !forceDebounce ? 0 : debounceTimeout - ) + env.NEXT_PUBLIC_E2E_TEST && !forceDebounce ? 0 : debounceTimeout, + ); useEffect(() => { - if (isTouched || localValue !== '' || !defaultValue || defaultValue === '') - return - setLocalValue(defaultValue ?? '') - }, [defaultValue, isTouched, localValue]) + if (isTouched || localValue !== "" || !defaultValue || defaultValue === "") + return; + setLocalValue(defaultValue ?? ""); + }, [defaultValue, isTouched, localValue]); useEffect( () => () => { - onChange.flush() + onChange.flush(); }, - [onChange] - ) + [onChange], + ); const changeValue = (value: string) => { - if (!isTouched) setIsTouched(true) - setLocalValue(value) - onChange(value) - } + if (!isTouched) setIsTouched(true); + setLocalValue(value); + onChange(value); + }; const handleVariableSelected = (variable?: Variable) => { - if (!variable) return + if (!variable) return; const { text, carretPosition: newCarretPosition } = injectVariableInText({ variable, text: localValue, at: carretPosition, - }) - changeValue(text) - focusInput({ at: newCarretPosition, input: inputRef.current }) - } + }); + changeValue(text); + focusInput({ at: newCarretPosition, input: inputRef.current }); + }; const updateCarretPosition = (e: React.FocusEvent) => { - const carretPosition = e.target.selectionStart - if (!carretPosition) return - setCarretPosition(carretPosition) - } + const carretPosition = e.target.selectionStart; + if (!carretPosition) return; + setCarretPosition(carretPosition); + }; const Input = ( - ) + ); return ( {label && ( - {label}{' '} + {label}{" "} {moreInfoTooltip && ( {moreInfoTooltip} )} )} {withVariableButton ? ( - + {Input} @@ -169,5 +170,5 @@ export const TextInput = forwardRef(function TextInput( )} {helperText && {helperText}} - ) -}) + ); +}); diff --git a/apps/builder/src/components/inputs/Textarea.tsx b/apps/builder/src/components/inputs/Textarea.tsx index 666b39689d..9d3cb45fa0 100644 --- a/apps/builder/src/components/inputs/Textarea.tsx +++ b/apps/builder/src/components/inputs/Textarea.tsx @@ -1,34 +1,36 @@ -import { VariablesButton } from '@/features/variables/components/VariablesButton' -import { injectVariableInText } from '@/features/variables/helpers/injectVariableInTextInput' -import { focusInput } from '@/helpers/focusInput' +import { VariablesButton } from "@/features/variables/components/VariablesButton"; +import { injectVariableInText } from "@/features/variables/helpers/injectVariableInTextInput"; +import { focusInput } from "@/helpers/focusInput"; import { + Textarea as ChakraTextarea, FormControl, + FormHelperText, FormLabel, HStack, - Textarea as ChakraTextarea, - TextareaProps, - FormHelperText, Stack, -} from '@chakra-ui/react' -import { Variable } from '@typebot.io/schemas' -import React, { ReactNode, useEffect, useRef, useState } from 'react' -import { useDebouncedCallback } from 'use-debounce' -import { env } from '@typebot.io/env' -import { MoreInfoTooltip } from '../MoreInfoTooltip' + type TextareaProps, +} from "@chakra-ui/react"; +import { env } from "@typebot.io/env"; +import type { Variable } from "@typebot.io/variables/schemas"; +import type { ReactNode } from "react"; +import type React from "react"; +import { useEffect, useRef, useState } from "react"; +import { useDebouncedCallback } from "use-debounce"; +import { MoreInfoTooltip } from "../MoreInfoTooltip"; type Props = { - id?: string - defaultValue?: string - debounceTimeout?: number - label?: string - moreInfoTooltip?: string - withVariableButton?: boolean - isRequired?: boolean - placeholder?: string - helperText?: ReactNode - onChange: (value: string) => void - direction?: 'row' | 'column' -} & Pick + id?: string; + defaultValue?: string; + debounceTimeout?: number; + label?: string; + moreInfoTooltip?: string; + withVariableButton?: boolean; + isRequired?: boolean; + placeholder?: string; + helperText?: ReactNode; + onChange: (value: string) => void; + direction?: "row" | "column"; +} & Pick; export const Textarea = ({ id, @@ -42,55 +44,55 @@ export const Textarea = ({ isRequired, minH, helperText, - direction = 'column', + direction = "column", width, }: Props) => { - const inputRef = useRef(null) - const [isTouched, setIsTouched] = useState(false) - const [localValue, setLocalValue] = useState(defaultValue ?? '') + const inputRef = useRef(null); + const [isTouched, setIsTouched] = useState(false); + const [localValue, setLocalValue] = useState(defaultValue ?? ""); const [carretPosition, setCarretPosition] = useState( - localValue.length ?? 0 - ) + localValue.length ?? 0, + ); const onChange = useDebouncedCallback( _onChange, - env.NEXT_PUBLIC_E2E_TEST ? 0 : debounceTimeout - ) + env.NEXT_PUBLIC_E2E_TEST ? 0 : debounceTimeout, + ); useEffect(() => { - if (isTouched || localValue !== '' || !defaultValue || defaultValue === '') - return - setLocalValue(defaultValue ?? '') - }, [defaultValue, isTouched, localValue]) + if (isTouched || localValue !== "" || !defaultValue || defaultValue === "") + return; + setLocalValue(defaultValue ?? ""); + }, [defaultValue, isTouched, localValue]); useEffect( () => () => { - onChange.flush() + onChange.flush(); }, - [onChange] - ) + [onChange], + ); const changeValue = (value: string) => { - if (!isTouched) setIsTouched(true) - setLocalValue(value) - onChange(value) - } + if (!isTouched) setIsTouched(true); + setLocalValue(value); + onChange(value); + }; const handleVariableSelected = (variable?: Variable) => { - if (!variable) return + if (!variable) return; const { text, carretPosition: newCarretPosition } = injectVariableInText({ variable, text: localValue, at: carretPosition, - }) - changeValue(text) - focusInput({ at: newCarretPosition, input: inputRef.current }) - } + }); + changeValue(text); + focusInput({ at: newCarretPosition, input: inputRef.current }); + }; const updateCarretPosition = (e: React.FocusEvent) => { - const carretPosition = e.target.selectionStart - if (!carretPosition) return - setCarretPosition(carretPosition) - } + const carretPosition = e.target.selectionStart; + if (!carretPosition) return; + setCarretPosition(carretPosition); + }; const Textarea = ( changeValue(e.target.value)} placeholder={placeholder} - minH={minH ?? '150px'} + minH={minH ?? "150px"} /> - ) + ); return ( {label && ( - {label}{' '} + {label}{" "} {moreInfoTooltip && ( {moreInfoTooltip} )} )} {withVariableButton ? ( - + {Textarea} @@ -130,5 +132,5 @@ export const Textarea = ({ )} {helperText && {helperText}} - ) -} + ); +}; diff --git a/apps/builder/src/components/inputs/VariableSearchInput.tsx b/apps/builder/src/components/inputs/VariableSearchInput.tsx index fa669bc318..71fba141a2 100644 --- a/apps/builder/src/components/inputs/VariableSearchInput.tsx +++ b/apps/builder/src/components/inputs/VariableSearchInput.tsx @@ -1,47 +1,49 @@ +import { EditIcon, PlusIcon, TrashIcon } from "@/components/icons"; +import { useTypebot } from "@/features/editor/providers/TypebotProvider"; +import { useParentModal } from "@/features/graph/providers/ParentModalProvider"; +import { useOutsideClick } from "@/hooks/useOutsideClick"; import { - useDisclosure, - Flex, - Popover, - Input, - PopoverContent, Button, - InputProps, - IconButton, + Flex, + FormControl, + FormHelperText, + FormLabel, HStack, - useColorModeValue, + IconButton, + Input, + type InputProps, + Popover, PopoverAnchor, + PopoverContent, Portal, + Stack, Tag, Text, - FormControl, - FormLabel, - FormHelperText, - Stack, -} from '@chakra-ui/react' -import { EditIcon, PlusIcon, TrashIcon } from '@/components/icons' -import { useTypebot } from '@/features/editor/providers/TypebotProvider' -import { createId } from '@paralleldrive/cuid2' -import { Variable } from '@typebot.io/schemas' -import React, { useState, useRef, ChangeEvent, ReactNode } from 'react' -import { byId, isDefined, isNotDefined } from '@typebot.io/lib' -import { useOutsideClick } from '@/hooks/useOutsideClick' -import { useParentModal } from '@/features/graph/providers/ParentModalProvider' -import { MoreInfoTooltip } from '../MoreInfoTooltip' -import { useTranslate } from '@tolgee/react' + useColorModeValue, + useDisclosure, +} from "@chakra-ui/react"; +import { createId } from "@paralleldrive/cuid2"; +import { useTranslate } from "@tolgee/react"; +import { byId, isDefined, isNotDefined } from "@typebot.io/lib/utils"; +import type { Variable } from "@typebot.io/variables/schemas"; +import type { ChangeEvent, ReactNode } from "react"; +import type React from "react"; +import { useRef, useState } from "react"; +import { MoreInfoTooltip } from "../MoreInfoTooltip"; type Props = { - initialVariableId: string | undefined - autoFocus?: boolean + initialVariableId: string | undefined; + autoFocus?: boolean; onSelectVariable: ( - variable: Pick | undefined - ) => void - label?: string - placeholder?: string - helperText?: ReactNode - moreInfoTooltip?: string - direction?: 'row' | 'column' - width?: 'full' -} & Omit + variable: Pick | undefined, + ) => void; + label?: string; + placeholder?: string; + helperText?: ReactNode; + moreInfoTooltip?: string; + direction?: "row" | "column"; + width?: "full"; +} & Omit; export const VariableSearchInput = ({ initialVariableId, @@ -51,154 +53,156 @@ export const VariableSearchInput = ({ label, helperText, moreInfoTooltip, - direction = 'column', + direction = "column", isRequired, width, ...inputProps }: Props) => { - const focusedItemBgColor = useColorModeValue('gray.200', 'gray.700') + const focusedItemBgColor = useColorModeValue("gray.200", "gray.700"); const { onOpen, onClose, isOpen } = useDisclosure({ defaultIsOpen: autoFocus, - }) + }); const { typebot, createVariable, deleteVariable, updateVariable } = - useTypebot() - const variables = typebot?.variables ?? [] + useTypebot(); + const variables = typebot?.variables ?? []; const [inputValue, setInputValue] = useState( - variables.find(byId(initialVariableId))?.name ?? '' - ) + variables.find(byId(initialVariableId))?.name ?? "", + ); const [filteredItems, setFilteredItems] = useState( - variables ?? [] - ) + variables ?? [], + ); const [keyboardFocusIndex, setKeyboardFocusIndex] = useState< number | undefined - >() - const dropdownRef = useRef(null) - const inputRef = useRef(null) - const createVariableItemRef = useRef(null) - const itemsRef = useRef<(HTMLButtonElement | null)[]>([]) - const { ref: parentModalRef } = useParentModal() - const { t } = useTranslate() + >(); + const dropdownRef = useRef(null); + const inputRef = useRef(null); + const createVariableItemRef = useRef(null); + const itemsRef = useRef<(HTMLButtonElement | null)[]>([]); + const { ref: parentModalRef } = useParentModal(); + const { t } = useTranslate(); useOutsideClick({ ref: dropdownRef, handler: () => { - onClose() - setInputValue(variables.find(byId(initialVariableId))?.name ?? '') + onClose(); + setInputValue(variables.find(byId(initialVariableId))?.name ?? ""); }, isEnabled: isOpen, - }) + }); const onInputChange = (e: ChangeEvent) => { - setInputValue(e.target.value) - if (e.target.value === '') { + setInputValue(e.target.value); + if (e.target.value === "") { if (inputValue.length > 0) { - onSelectVariable(undefined) + onSelectVariable(undefined); } - setFilteredItems([...variables.slice(0, 50)]) - return + setFilteredItems([...variables.slice(0, 50)]); + return; } setFilteredItems([ ...variables .filter((item) => - item.name.toLowerCase().includes((e.target.value ?? '').toLowerCase()) + item.name + .toLowerCase() + .includes((e.target.value ?? "").toLowerCase()), ) .slice(0, 50), - ]) - } + ]); + }; const handleVariableNameClick = (variable: Variable) => () => { - setInputValue(variable.name) - onSelectVariable(variable) - setKeyboardFocusIndex(undefined) - inputRef.current?.blur() - onClose() - } + setInputValue(variable.name); + onSelectVariable(variable); + setKeyboardFocusIndex(undefined); + inputRef.current?.blur(); + onClose(); + }; const handleCreateNewVariableClick = () => { - if (!inputValue || inputValue === '') return - const id = 'v' + createId() - onSelectVariable({ id, name: inputValue }) - createVariable({ id, name: inputValue, isSessionVariable: true }) - inputRef.current?.blur() - onClose() - } + if (!inputValue || inputValue === "") return; + const id = "v" + createId(); + onSelectVariable({ id, name: inputValue }); + createVariable({ id, name: inputValue, isSessionVariable: true }); + inputRef.current?.blur(); + onClose(); + }; const handleDeleteVariableClick = (variable: Variable) => (e: React.MouseEvent) => { - e.stopPropagation() - deleteVariable(variable.id) - setFilteredItems(filteredItems.filter((item) => item.id !== variable.id)) + e.stopPropagation(); + deleteVariable(variable.id); + setFilteredItems(filteredItems.filter((item) => item.id !== variable.id)); if (variable.name === inputValue) { - setInputValue('') + setInputValue(""); } - } + }; const handleRenameVariableClick = (variable: Variable) => (e: React.MouseEvent) => { - e.stopPropagation() - const name = prompt(t('variables.rename'), variable.name) - if (!name) return - updateVariable(variable.id, { name }) + e.stopPropagation(); + const name = prompt(t("variables.rename"), variable.name); + if (!name) return; + updateVariable(variable.id, { name }); setFilteredItems( filteredItems.map((item) => - item.id === variable.id ? { ...item, name } : item - ) - ) - } + item.id === variable.id ? { ...item, name } : item, + ), + ); + }; const isCreateVariableButtonDisplayed = (inputValue?.length ?? 0) > 0 && - isNotDefined(variables.find((v) => v.name === inputValue)) + isNotDefined(variables.find((v) => v.name === inputValue)); const handleKeyUp = (e: React.KeyboardEvent) => { - if (e.key === 'Enter' && isDefined(keyboardFocusIndex)) { + if (e.key === "Enter" && isDefined(keyboardFocusIndex)) { if (keyboardFocusIndex === 0 && isCreateVariableButtonDisplayed) - handleCreateNewVariableClick() + handleCreateNewVariableClick(); else handleVariableNameClick( filteredItems[ keyboardFocusIndex - (isCreateVariableButtonDisplayed ? 1 : 0) - ] - )() - return setKeyboardFocusIndex(undefined) + ], + )(); + return setKeyboardFocusIndex(undefined); } - if (e.key === 'ArrowDown') { - if (keyboardFocusIndex === undefined) return setKeyboardFocusIndex(0) - if (keyboardFocusIndex >= filteredItems.length) return + if (e.key === "ArrowDown") { + if (keyboardFocusIndex === undefined) return setKeyboardFocusIndex(0); + if (keyboardFocusIndex >= filteredItems.length) return; itemsRef.current[keyboardFocusIndex + 1]?.scrollIntoView({ - behavior: 'smooth', - block: 'nearest', - }) - return setKeyboardFocusIndex(keyboardFocusIndex + 1) + behavior: "smooth", + block: "nearest", + }); + return setKeyboardFocusIndex(keyboardFocusIndex + 1); } - if (e.key === 'ArrowUp') { - if (keyboardFocusIndex === undefined) return - if (keyboardFocusIndex <= 0) return setKeyboardFocusIndex(undefined) + if (e.key === "ArrowUp") { + if (keyboardFocusIndex === undefined) return; + if (keyboardFocusIndex <= 0) return setKeyboardFocusIndex(undefined); itemsRef.current[keyboardFocusIndex - 1]?.scrollIntoView({ - behavior: 'smooth', - block: 'nearest', - }) - return setKeyboardFocusIndex(keyboardFocusIndex - 1) + behavior: "smooth", + block: "nearest", + }); + return setKeyboardFocusIndex(keyboardFocusIndex - 1); } - return setKeyboardFocusIndex(undefined) - } + return setKeyboardFocusIndex(undefined); + }; const openDropdown = () => { - if (inputValue === '') setFilteredItems(variables) - onOpen() - } + if (inputValue === "") setFilteredItems(variables); + onOpen(); + }; return ( {label && ( - {label}{' '} + {label}{" "} {moreInfoTooltip && ( {moreInfoTooltip} )} @@ -220,7 +224,7 @@ export const VariableSearchInput = ({ onChange={onInputChange} onFocus={openDropdown} onKeyDown={handleKeyUp} - placeholder={placeholder ?? t('variables.select')} + placeholder={placeholder ?? t("variables.select")} autoComplete="off" {...inputProps} /> @@ -254,10 +258,10 @@ export const VariableSearchInput = ({ bgColor={ keyboardFocusIndex === 0 ? focusedItemBgColor - : 'transparent' + : "transparent" } > - {t('create')} + {t("create")} {inputValue} @@ -265,62 +269,59 @@ export const VariableSearchInput = ({ )} - {filteredItems.length > 0 && ( - <> - {filteredItems.map((item, idx) => { - const indexInList = isCreateVariableButtonDisplayed - ? idx + 1 - : idx - return ( - - ) - })} - - )} + + } + aria-label={t("variables.rename")} + size="xs" + onClick={handleRenameVariableClick(item)} + /> + } + aria-label={t("variables.remove")} + size="xs" + onClick={handleDeleteVariableClick(item)} + /> + + + ); + })} {helperText && {helperText}} - ) -} + ); +}; diff --git a/apps/builder/src/components/inputs/index.tsx b/apps/builder/src/components/inputs/index.tsx index 9db702e325..b539242544 100644 --- a/apps/builder/src/components/inputs/index.tsx +++ b/apps/builder/src/components/inputs/index.tsx @@ -1,3 +1,3 @@ -export { TextInput } from './TextInput' -export { Textarea } from './Textarea' -export { NumberInput } from './NumberInput' +export { TextInput } from "./TextInput"; +export { Textarea } from "./Textarea"; +export { NumberInput } from "./NumberInput"; diff --git a/apps/builder/src/components/logos/AzureAdLogo.tsx b/apps/builder/src/components/logos/AzureAdLogo.tsx index f094427d4e..5b001c914e 100644 --- a/apps/builder/src/components/logos/AzureAdLogo.tsx +++ b/apps/builder/src/components/logos/AzureAdLogo.tsx @@ -1,4 +1,4 @@ -import { Icon, IconProps } from '@chakra-ui/react' +import { Icon, type IconProps } from "@chakra-ui/react"; export const AzureAdLogo = (props: IconProps) => { return ( @@ -27,5 +27,5 @@ export const AzureAdLogo = (props: IconProps) => { - ) -} + ); +}; diff --git a/apps/builder/src/components/logos/FacebookLogo.tsx b/apps/builder/src/components/logos/FacebookLogo.tsx index 5e29e1d68e..8931cc9df5 100644 --- a/apps/builder/src/components/logos/FacebookLogo.tsx +++ b/apps/builder/src/components/logos/FacebookLogo.tsx @@ -1,4 +1,4 @@ -import { IconProps, Icon } from '@chakra-ui/react' +import { Icon, type IconProps } from "@chakra-ui/react"; export const FacebookLogo = (props: IconProps) => ( @@ -8,4 +8,4 @@ export const FacebookLogo = (props: IconProps) => ( fill="#fff" /> -) +); diff --git a/apps/builder/src/components/logos/GiphyLogo.tsx b/apps/builder/src/components/logos/GiphyLogo.tsx index 16ece7384c..c3041ccdb9 100644 --- a/apps/builder/src/components/logos/GiphyLogo.tsx +++ b/apps/builder/src/components/logos/GiphyLogo.tsx @@ -1,4 +1,4 @@ -import { IconProps, Icon, useColorModeValue } from '@chakra-ui/react' +import { Icon, type IconProps, useColorModeValue } from "@chakra-ui/react"; export const GiphyLogo = (props: IconProps) => ( @@ -15,9 +15,9 @@ export const GiphyLogo = (props: IconProps) => ( -) +); diff --git a/apps/builder/src/components/logos/GitlabLogo.tsx b/apps/builder/src/components/logos/GitlabLogo.tsx index f5b0e25e03..aad593b992 100644 --- a/apps/builder/src/components/logos/GitlabLogo.tsx +++ b/apps/builder/src/components/logos/GitlabLogo.tsx @@ -1,4 +1,4 @@ -import { IconProps, Icon } from '@chakra-ui/react' +import { Icon, type IconProps } from "@chakra-ui/react"; export const GitlabLogo = (props: IconProps) => ( @@ -31,4 +31,4 @@ export const GitlabLogo = (props: IconProps) => ( fill="#E24329" /> -) +); diff --git a/apps/builder/src/components/logos/KeycloakLogo.tsx b/apps/builder/src/components/logos/KeycloakLogo.tsx index bfa68202f0..73ee7daacc 100644 --- a/apps/builder/src/components/logos/KeycloakLogo.tsx +++ b/apps/builder/src/components/logos/KeycloakLogo.tsx @@ -1,4 +1,4 @@ -import { IconProps, Icon } from '@chakra-ui/react' +import { Icon, type IconProps } from "@chakra-ui/react"; export const KeycloackLogo = (props: IconProps) => ( @@ -8,4 +8,4 @@ export const KeycloackLogo = (props: IconProps) => ( fill="#fff" /> -) +); diff --git a/apps/builder/src/components/logos/PexelsLogo.tsx b/apps/builder/src/components/logos/PexelsLogo.tsx index ddf50d1ea7..d77784a0b0 100644 --- a/apps/builder/src/components/logos/PexelsLogo.tsx +++ b/apps/builder/src/components/logos/PexelsLogo.tsx @@ -1,4 +1,4 @@ -import { IconProps, Icon, useColorModeValue } from '@chakra-ui/react' +import { Icon, type IconProps, useColorModeValue } from "@chakra-ui/react"; export const PexelsLogo = (props: IconProps) => ( @@ -18,8 +18,8 @@ export const PexelsLogo = (props: IconProps) => ( -) +); diff --git a/apps/builder/src/components/logos/StripeLogo.tsx b/apps/builder/src/components/logos/StripeLogo.tsx index 880e69fa39..f9bb4ab9e9 100644 --- a/apps/builder/src/components/logos/StripeLogo.tsx +++ b/apps/builder/src/components/logos/StripeLogo.tsx @@ -1,16 +1,16 @@ -import { Icon, IconProps } from '@chakra-ui/react' +import { Icon, type IconProps } from "@chakra-ui/react"; export const StripeLogo = (props: IconProps) => { return ( - ) -} + ); +}; diff --git a/apps/builder/src/components/logos/UnsplashLogo.tsx b/apps/builder/src/components/logos/UnsplashLogo.tsx index 82571facda..b8fb29a3b8 100644 --- a/apps/builder/src/components/logos/UnsplashLogo.tsx +++ b/apps/builder/src/components/logos/UnsplashLogo.tsx @@ -1,7 +1,7 @@ -import { IconProps, Icon } from '@chakra-ui/react' +import { Icon, type IconProps } from "@chakra-ui/react"; export const UnsplashLogo = (props: IconProps) => ( -) +); diff --git a/apps/builder/src/components/logos/WhatsAppLogo.tsx b/apps/builder/src/components/logos/WhatsAppLogo.tsx index a5b9a7d336..2bf9f2732e 100644 --- a/apps/builder/src/components/logos/WhatsAppLogo.tsx +++ b/apps/builder/src/components/logos/WhatsAppLogo.tsx @@ -1,6 +1,6 @@ -import { IconProps, Icon } from '@chakra-ui/react' +import { Icon, type IconProps } from "@chakra-ui/react"; -export const whatsAppBrandColor = '#25D366' +export const whatsAppBrandColor = "#25D366"; export const WhatsAppLogo = (props: IconProps) => ( @@ -11,4 +11,4 @@ export const WhatsAppLogo = (props: IconProps) => ( fill="currentColor" /> -) +); diff --git a/apps/builder/src/features/account/UserProvider.tsx b/apps/builder/src/features/account/UserProvider.tsx index 3e1fb1ca27..3debc6659f 100644 --- a/apps/builder/src/features/account/UserProvider.tsx +++ b/apps/builder/src/features/account/UserProvider.tsx @@ -1,119 +1,120 @@ -import { signOut, useSession } from 'next-auth/react' -import { useRouter } from 'next/router' -import { createContext, ReactNode, useEffect, useState } from 'react' -import { isDefined, isNotDefined } from '@typebot.io/lib' -import { User } from '@typebot.io/schemas' -import { setUser as setSentryUser } from '@sentry/nextjs' -import { useToast } from '@/hooks/useToast' -import { updateUserQuery } from './queries/updateUserQuery' -import { useDebouncedCallback } from 'use-debounce' -import { env } from '@typebot.io/env' -import { useColorMode } from '@chakra-ui/react' +import { useToast } from "@/hooks/useToast"; +import { useColorMode } from "@chakra-ui/react"; +import { setUser as setSentryUser } from "@sentry/nextjs"; +import { env } from "@typebot.io/env"; +import { isDefined, isNotDefined } from "@typebot.io/lib/utils"; +import type { User } from "@typebot.io/schemas/features/user/schema"; +import { signOut, useSession } from "next-auth/react"; +import { useRouter } from "next/router"; +import type { ReactNode } from "react"; +import { createContext, useEffect, useState } from "react"; +import { useDebouncedCallback } from "use-debounce"; +import { updateUserQuery } from "./queries/updateUserQuery"; export const userContext = createContext<{ - user?: User - isLoading: boolean - currentWorkspaceId?: string - logOut: () => void - updateUser: (newUser: Partial) => void + user?: User; + isLoading: boolean; + currentWorkspaceId?: string; + logOut: () => void; + updateUser: (newUser: Partial) => void; }>({ isLoading: false, logOut: () => {}, updateUser: () => {}, -}) +}); -const debounceTimeout = 1000 +const debounceTimeout = 1000; export const UserProvider = ({ children }: { children: ReactNode }) => { - const router = useRouter() - const { data: session, status } = useSession() - const [user, setUser] = useState() - const { showToast } = useToast() - const [currentWorkspaceId, setCurrentWorkspaceId] = useState() - const { setColorMode } = useColorMode() + const router = useRouter(); + const { data: session, status } = useSession(); + const [user, setUser] = useState(); + const { showToast } = useToast(); + const [currentWorkspaceId, setCurrentWorkspaceId] = useState(); + const { setColorMode } = useColorMode(); useEffect(() => { - const currentColorScheme = localStorage.getItem('chakra-ui-color-mode') as - | 'light' - | 'dark' - | null - if (!currentColorScheme) return - const systemColorScheme = window.matchMedia('(prefers-color-scheme: dark)') + const currentColorScheme = localStorage.getItem("chakra-ui-color-mode") as + | "light" + | "dark" + | null; + if (!currentColorScheme) return; + const systemColorScheme = window.matchMedia("(prefers-color-scheme: dark)") .matches - ? 'dark' - : 'light' + ? "dark" + : "light"; const userPrefersSystemMode = - !user?.preferredAppAppearance || user.preferredAppAppearance === 'system' + !user?.preferredAppAppearance || user.preferredAppAppearance === "system"; const computedColorMode = userPrefersSystemMode ? systemColorScheme - : user?.preferredAppAppearance - if (computedColorMode === currentColorScheme) return - setColorMode(computedColorMode) - }, [setColorMode, user?.preferredAppAppearance]) + : user?.preferredAppAppearance; + if (computedColorMode === currentColorScheme) return; + setColorMode(computedColorMode); + }, [setColorMode, user?.preferredAppAppearance]); useEffect(() => { - if (isDefined(user) || isNotDefined(session)) return + if (isDefined(user) || isNotDefined(session)) return; setCurrentWorkspaceId( - localStorage.getItem('currentWorkspaceId') ?? undefined - ) - const parsedUser = session.user as User - setUser(parsedUser) + localStorage.getItem("currentWorkspaceId") ?? undefined, + ); + const parsedUser = session.user as User; + setUser(parsedUser); if (parsedUser?.id) { - setSentryUser({ id: parsedUser.id }) + setSentryUser({ id: parsedUser.id }); } - }, [session, user]) + }, [session, user]); useEffect(() => { - if (!router.isReady) return - if (status === 'loading') return - const isSignInPath = ['/signin', '/register'].includes(router.pathname) + if (!router.isReady) return; + if (status === "loading") return; + const isSignInPath = ["/signin", "/register"].includes(router.pathname); const isPathPublicFriendly = /\/typebots\/.+\/(edit|theme|settings)/.test( - router.pathname - ) - if (isSignInPath || isPathPublicFriendly) return - if (!user && status === 'unauthenticated') + router.pathname, + ); + if (isSignInPath || isPathPublicFriendly) return; + if (!user && status === "unauthenticated") router.replace({ - pathname: '/signin', + pathname: "/signin", query: { redirectPath: router.asPath, }, - }) - }, [router, status, user]) + }); + }, [router, status, user]); const updateUser = (updates: Partial) => { - if (isNotDefined(user)) return - const newUser = { ...user, ...updates } - setUser(newUser) - saveUser(updates) - } + if (isNotDefined(user)) return; + const newUser = { ...user, ...updates }; + setUser(newUser); + saveUser(updates); + }; const saveUser = useDebouncedCallback( async (updates: Partial) => { - if (isNotDefined(user)) return - const { error } = await updateUserQuery(user.id, updates) - if (error) showToast({ title: error.name, description: error.message }) - await refreshUser() + if (isNotDefined(user)) return; + const { error } = await updateUserQuery(user.id, updates); + if (error) showToast({ title: error.name, description: error.message }); + await refreshUser(); }, - env.NEXT_PUBLIC_E2E_TEST ? 0 : debounceTimeout - ) + env.NEXT_PUBLIC_E2E_TEST ? 0 : debounceTimeout, + ); const logOut = () => { - signOut() - setUser(undefined) - } + signOut(); + setUser(undefined); + }; useEffect(() => { return () => { - saveUser.flush() - } - }, [saveUser]) + saveUser.flush(); + }; + }, [saveUser]); return ( { > {children} - ) -} + ); +}; export const refreshUser = async () => { - await fetch('/api/auth/session?update') - reloadSession() -} + await fetch("/api/auth/session?update"); + reloadSession(); +}; const reloadSession = () => { - const event = new Event('visibilitychange') - document.dispatchEvent(event) -} + const event = new Event("visibilitychange"); + document.dispatchEvent(event); +}; diff --git a/apps/builder/src/features/account/account.spec.ts b/apps/builder/src/features/account/account.spec.ts index 18adb985d4..ed4876a2e5 100644 --- a/apps/builder/src/features/account/account.spec.ts +++ b/apps/builder/src/features/account/account.spec.ts @@ -1,47 +1,47 @@ -import { getTestAsset } from '@/test/utils/playwright' -import test, { expect } from '@playwright/test' -import { env } from '@typebot.io/env' -import { userId } from '@typebot.io/playwright/databaseSetup' +import { getTestAsset } from "@/test/utils/playwright"; +import test, { expect } from "@playwright/test"; +import { env } from "@typebot.io/env"; +import { userId } from "@typebot.io/playwright/databaseSetup"; -test.describe.configure({ mode: 'parallel' }) +test.describe.configure({ mode: "parallel" }); -test('should display user info properly', async ({ page }) => { - await page.goto('/typebots') - await page.click('text=Settings & Members') +test("should display user info properly", async ({ page }) => { + await page.goto("/typebots"); + await page.click("text=Settings & Members"); expect( - page.locator('input[type="email"]').getAttribute('disabled') - ).toBeDefined() - await page.getByRole('textbox', { name: 'Name:' }).fill('John Doe') - await page.setInputFiles('input[type="file"]', getTestAsset('avatar.jpg')) - await expect(page.locator('img >> nth=1')).toHaveAttribute( - 'src', + page.locator('input[type="email"]').getAttribute("disabled"), + ).toBeDefined(); + await page.getByRole("textbox", { name: "Name:" }).fill("John Doe"); + await page.setInputFiles('input[type="file"]', getTestAsset("avatar.jpg")); + await expect(page.locator("img >> nth=1")).toHaveAttribute( + "src", new RegExp( - `${env.S3_ENDPOINT}${env.S3_PORT ? `:${env.S3_PORT}` : ''}/${ + `${env.S3_ENDPOINT}${env.S3_PORT ? `:${env.S3_PORT}` : ""}/${ env.S3_BUCKET }/public/users/${userId}/avatar`, - 'gm' - ) - ) - await page.click('text="Preferences"') - await expect(page.locator('text=Trackpad')).toBeVisible() -}) + "gm", + ), + ); + await page.click('text="Preferences"'); + await expect(page.locator("text=Trackpad")).toBeVisible(); +}); -test('should be able to create and delete api tokens', async ({ page }) => { - await page.goto('/typebots') - await page.click('text=Settings & Members') - await expect(page.locator('text=Github')).toBeVisible() - await page.click('text="Create"') - await expect(page.locator('button >> text="Create token"')).toBeDisabled() - await page.fill('[placeholder="I.e. Zapier, Github, Make.com"]', 'CLI') - await expect(page.locator('button >> text="Create token"')).toBeEnabled() - await page.click('button >> text="Create token"') - await expect(page.locator('text=Please copy your token')).toBeVisible() - await expect(page.locator('button >> text="Copy"')).toBeVisible() - await page.click('button >> text="Done"') - await expect(page.locator('text=CLI')).toBeVisible() - await page.click('text="Delete" >> nth=2') - await expect(page.locator('strong >> text="Github"')).toBeVisible() - await page.click('button >> text="Delete" >> nth=-1') - await expect(page.locator('button >> text="Delete" >> nth=-1')).toBeEnabled() - await expect(page.locator('text="Github"')).toBeHidden() -}) +test("should be able to create and delete api tokens", async ({ page }) => { + await page.goto("/typebots"); + await page.click("text=Settings & Members"); + await expect(page.locator("text=Github")).toBeVisible(); + await page.click('text="Create"'); + await expect(page.locator('button >> text="Create token"')).toBeDisabled(); + await page.fill('[placeholder="I.e. Zapier, Github, Make.com"]', "CLI"); + await expect(page.locator('button >> text="Create token"')).toBeEnabled(); + await page.click('button >> text="Create token"'); + await expect(page.locator("text=Please copy your token")).toBeVisible(); + await expect(page.locator('button >> text="Copy"')).toBeVisible(); + await page.click('button >> text="Done"'); + await expect(page.locator("text=CLI")).toBeVisible(); + await page.click('text="Delete" >> nth=2'); + await expect(page.locator('strong >> text="Github"')).toBeVisible(); + await page.click('button >> text="Delete" >> nth=-1'); + await expect(page.locator('button >> text="Delete" >> nth=-1')).toBeEnabled(); + await expect(page.locator('text="Github"')).toBeHidden(); +}); diff --git a/apps/builder/src/features/account/components/ApiTokensList.tsx b/apps/builder/src/features/account/components/ApiTokensList.tsx index 2ee74794b3..791105a21c 100644 --- a/apps/builder/src/features/account/components/ApiTokensList.tsx +++ b/apps/builder/src/features/account/components/ApiTokensList.tsx @@ -1,67 +1,68 @@ +import { ConfirmModal } from "@/components/ConfirmModal"; +import { TimeSince } from "@/components/TimeSince"; +import { useToast } from "@/hooks/useToast"; import { - TableContainer, - Table, - Thead, - Tr, - Th, - Tbody, - Td, Button, - Text, - Heading, Checkbox, + Flex, + Heading, Skeleton, Stack, - Flex, + Table, + TableContainer, + Tbody, + Td, + Text, + Th, + Thead, + Tr, useDisclosure, -} from '@chakra-ui/react' -import { ConfirmModal } from '@/components/ConfirmModal' -import { useToast } from '@/hooks/useToast' -import { User } from '@typebot.io/prisma' -import React, { useState } from 'react' -import { byId, isDefined } from '@typebot.io/lib' -import { CreateTokenModal } from './CreateTokenModal' -import { useApiTokens } from '../hooks/useApiTokens' -import { ApiTokenFromServer } from '../types' -import { deleteApiTokenQuery } from '../queries/deleteApiTokenQuery' -import { T, useTranslate } from '@tolgee/react' -import { TimeSince } from '@/components/TimeSince' +} from "@chakra-ui/react"; +import { T, useTranslate } from "@tolgee/react"; +import { byId, isDefined } from "@typebot.io/lib/utils"; +import type { Prisma } from "@typebot.io/prisma/types"; +import React, { useState } from "react"; +import { useApiTokens } from "../hooks/useApiTokens"; +import { deleteApiTokenQuery } from "../queries/deleteApiTokenQuery"; +import type { ApiTokenFromServer } from "../types"; +import { CreateTokenModal } from "./CreateTokenModal"; -type Props = { user: User } +type Props = { user: Prisma.User }; export const ApiTokensList = ({ user }: Props) => { - const { t } = useTranslate() - const { showToast } = useToast() + const { t } = useTranslate(); + const { showToast } = useToast(); const { apiTokens, isLoading, mutate } = useApiTokens({ userId: user.id, onError: (e) => - showToast({ title: 'Failed to fetch tokens', description: e.message }), - }) + showToast({ title: "Failed to fetch tokens", description: e.message }), + }); const { isOpen: isCreateOpen, onOpen: onCreateOpen, onClose: onCreateClose, - } = useDisclosure() - const [deletingId, setDeletingId] = useState() + } = useDisclosure(); + const [deletingId, setDeletingId] = useState(); const refreshListWithNewToken = (token: ApiTokenFromServer) => { - if (!apiTokens) return - mutate({ apiTokens: [token, ...apiTokens] }) - } + if (!apiTokens) return; + mutate({ apiTokens: [token, ...apiTokens] }); + }; const deleteToken = async (tokenId?: string) => { - if (!apiTokens || !tokenId) return - const { error } = await deleteApiTokenQuery({ userId: user.id, tokenId }) - if (!error) mutate({ apiTokens: apiTokens.filter((t) => t.id !== tokenId) }) - } + if (!apiTokens || !tokenId) return; + const { error } = await deleteApiTokenQuery({ userId: user.id, tokenId }); + if (!error) + mutate({ apiTokens: apiTokens.filter((t) => t.id !== tokenId) }); + }; return ( - {t('account.apiTokens.heading')} - {t('account.apiTokens.description')} + {t("account.apiTokens.heading")} + {t("account.apiTokens.description")} { - - + + @@ -94,7 +95,7 @@ export const ApiTokensList = ({ user }: Props) => { variant="outline" onClick={() => setDeletingId(token.id)} > - {t('account.apiTokens.deleteButton.label')} + {t("account.apiTokens.deleteButton.label")} @@ -132,8 +133,8 @@ export const ApiTokensList = ({ user }: Props) => { /> } - confirmButtonLabel={t('account.apiTokens.deleteButton.label')} + confirmButtonLabel={t("account.apiTokens.deleteButton.label")} /> - ) -} + ); +}; diff --git a/apps/builder/src/features/account/components/ApiTokensModal.tsx b/apps/builder/src/features/account/components/ApiTokensModal.tsx index 1073d3c6a8..f117c8874d 100644 --- a/apps/builder/src/features/account/components/ApiTokensModal.tsx +++ b/apps/builder/src/features/account/components/ApiTokensModal.tsx @@ -5,18 +5,18 @@ import { ModalFooter, ModalHeader, ModalOverlay, -} from '@chakra-ui/react' -import { ApiTokensList } from './ApiTokensList' -import { useUser } from '../hooks/useUser' +} from "@chakra-ui/react"; +import { useUser } from "../hooks/useUser"; +import { ApiTokensList } from "./ApiTokensList"; type Props = { - isOpen: boolean - onClose: () => void -} + isOpen: boolean; + onClose: () => void; +}; export const ApiTokensModal = ({ isOpen, onClose }: Props) => { - const { user } = useUser() + const { user } = useUser(); - if (!user) return + if (!user) return; return ( @@ -28,5 +28,5 @@ export const ApiTokensModal = ({ isOpen, onClose }: Props) => { - ) -} + ); +}; diff --git a/apps/builder/src/features/account/components/AppearanceRadioGroup.tsx b/apps/builder/src/features/account/components/AppearanceRadioGroup.tsx index ea7b604876..d15f2ad9f0 100644 --- a/apps/builder/src/features/account/components/AppearanceRadioGroup.tsx +++ b/apps/builder/src/features/account/components/AppearanceRadioGroup.tsx @@ -1,39 +1,39 @@ import { - RadioGroup, HStack, - VStack, - Stack, + Image, Radio, + RadioGroup, + Stack, Text, - Image, -} from '@chakra-ui/react' -import { useTranslate } from '@tolgee/react' + VStack, +} from "@chakra-ui/react"; +import { useTranslate } from "@tolgee/react"; type Props = { - defaultValue: string - onChange: (value: string) => void -} + defaultValue: string; + onChange: (value: string) => void; +}; export const AppearanceRadioGroup = ({ defaultValue, onChange }: Props) => { - const { t } = useTranslate() + const { t } = useTranslate(); const appearanceData = [ { - value: 'light', - label: t('account.preferences.appearance.lightLabel'), - image: '/images/light-mode.png', + value: "light", + label: t("account.preferences.appearance.lightLabel"), + image: "/images/light-mode.png", }, { - value: 'dark', - label: t('account.preferences.appearance.darkLabel'), - image: '/images/dark-mode.png', + value: "dark", + label: t("account.preferences.appearance.darkLabel"), + image: "/images/dark-mode.png", }, { - value: 'system', - label: t('account.preferences.appearance.systemLabel'), - image: '/images/system-mode.png', + value: "system", + label: t("account.preferences.appearance.systemLabel"), + image: "/images/system-mode.png", }, - ] + ]; return ( @@ -54,7 +54,7 @@ export const AppearanceRadioGroup = ({ defaultValue, onChange }: Props) => { @@ -67,5 +67,5 @@ export const AppearanceRadioGroup = ({ defaultValue, onChange }: Props) => { ))} - ) -} + ); +}; diff --git a/apps/builder/src/features/account/components/CreateTokenModal.tsx b/apps/builder/src/features/account/components/CreateTokenModal.tsx index 4c1c631330..118f0d0e5b 100644 --- a/apps/builder/src/features/account/components/CreateTokenModal.tsx +++ b/apps/builder/src/features/account/components/CreateTokenModal.tsx @@ -1,30 +1,31 @@ -import { CopyButton } from '@/components/CopyButton' -import { useTranslate } from '@tolgee/react' +import { CopyButton } from "@/components/CopyButton"; import { + Button, + Input, + InputGroup, + InputRightElement, Modal, - ModalOverlay, + ModalBody, + ModalCloseButton, ModalContent, + ModalFooter, ModalHeader, - ModalCloseButton, - ModalBody, + ModalOverlay, Stack, - Input, - ModalFooter, - Button, Text, - InputGroup, - InputRightElement, -} from '@chakra-ui/react' -import React, { FormEvent, useRef, useState } from 'react' -import { createApiTokenQuery } from '../queries/createApiTokenQuery' -import { ApiTokenFromServer } from '../types' +} from "@chakra-ui/react"; +import { useTranslate } from "@tolgee/react"; +import type { FormEvent } from "react"; +import React, { useRef, useState } from "react"; +import { createApiTokenQuery } from "../queries/createApiTokenQuery"; +import type { ApiTokenFromServer } from "../types"; type Props = { - userId: string - isOpen: boolean - onNewToken: (token: ApiTokenFromServer) => void - onClose: () => void -} + userId: string; + isOpen: boolean; + onNewToken: (token: ApiTokenFromServer) => void; + onClose: () => void; +}; export const CreateTokenModal = ({ userId, @@ -32,22 +33,22 @@ export const CreateTokenModal = ({ onClose, onNewToken, }: Props) => { - const inputRef = useRef(null) - const { t } = useTranslate() - const [name, setName] = useState('') - const [isSubmitting, setIsSubmitting] = useState(false) - const [newTokenValue, setNewTokenValue] = useState() + const inputRef = useRef(null); + const { t } = useTranslate(); + const [name, setName] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); + const [newTokenValue, setNewTokenValue] = useState(); const createToken = async (e: FormEvent) => { - e.preventDefault() - setIsSubmitting(true) - const { data } = await createApiTokenQuery(userId, { name }) + e.preventDefault(); + setIsSubmitting(true); + const { data } = await createApiTokenQuery(userId, { name }); if (data?.apiToken) { - setNewTokenValue(data.apiToken.token) - onNewToken(data.apiToken) + setNewTokenValue(data.apiToken.token); + onNewToken(data.apiToken); } - setIsSubmitting(false) - } + setIsSubmitting(false); + }; return ( @@ -55,16 +56,16 @@ export const CreateTokenModal = ({ {newTokenValue - ? t('account.apiTokens.createModal.createdHeading') - : t('account.apiTokens.createModal.createHeading')} + ? t("account.apiTokens.createModal.createdHeading") + : t("account.apiTokens.createModal.createHeading")} {newTokenValue ? ( - {t('account.apiTokens.createModal.copyInstruction')}{' '} + {t("account.apiTokens.createModal.copyInstruction")}{" "} - {t('account.apiTokens.createModal.securityWarning')} + {t("account.apiTokens.createModal.securityWarning")} @@ -77,12 +78,12 @@ export const CreateTokenModal = ({ ) : ( - {t('account.apiTokens.createModal.nameInput.label')} + {t("account.apiTokens.createModal.nameInput.label")} setName(e.target.value)} /> @@ -92,7 +93,7 @@ export const CreateTokenModal = ({ {newTokenValue ? ( ) : ( )} - ) -} + ); +}; diff --git a/apps/builder/src/features/account/components/GraphNavigationRadioGroup.tsx b/apps/builder/src/features/account/components/GraphNavigationRadioGroup.tsx index e66205032b..e73246cad1 100644 --- a/apps/builder/src/features/account/components/GraphNavigationRadioGroup.tsx +++ b/apps/builder/src/features/account/components/GraphNavigationRadioGroup.tsx @@ -1,40 +1,40 @@ -import { MouseIcon, LaptopIcon } from '@/components/icons' -import { useTranslate } from '@tolgee/react' +import { LaptopIcon, MouseIcon } from "@/components/icons"; import { HStack, Radio, RadioGroup, Stack, - VStack, Text, -} from '@chakra-ui/react' -import { GraphNavigation } from '@typebot.io/prisma' + VStack, +} from "@chakra-ui/react"; +import { useTranslate } from "@tolgee/react"; +import { GraphNavigation } from "@typebot.io/prisma/enum"; type Props = { - defaultValue: string - onChange: (value: string) => void -} + defaultValue: string; + onChange: (value: string) => void; +}; export const GraphNavigationRadioGroup = ({ defaultValue, onChange, }: Props) => { - const { t } = useTranslate() + const { t } = useTranslate(); const graphNavigationData = [ { value: GraphNavigation.MOUSE, - label: t('account.preferences.graphNavigation.mouse.label'), - description: t('account.preferences.graphNavigation.mouse.description'), + label: t("account.preferences.graphNavigation.mouse.label"), + description: t("account.preferences.graphNavigation.mouse.description"), icon: , }, { value: GraphNavigation.TRACKPAD, - label: t('account.preferences.graphNavigation.trackpad.label'), + label: t("account.preferences.graphNavigation.trackpad.label"), description: t( - 'account.preferences.graphNavigation.trackpad.description' + "account.preferences.graphNavigation.trackpad.description", ), icon: , }, - ] + ]; return ( @@ -64,5 +64,5 @@ export const GraphNavigationRadioGroup = ({ ))} - ) -} + ); +}; diff --git a/apps/builder/src/features/account/components/MyAccountForm.tsx b/apps/builder/src/features/account/components/MyAccountForm.tsx index bd86629685..792ff552c1 100644 --- a/apps/builder/src/features/account/components/MyAccountForm.tsx +++ b/apps/builder/src/features/account/components/MyAccountForm.tsx @@ -1,31 +1,31 @@ -import { Stack, HStack, Avatar, Text, Tooltip } from '@chakra-ui/react' -import { UploadIcon } from '@/components/icons' -import React, { useState } from 'react' -import { ApiTokensList } from './ApiTokensList' -import { UploadButton } from '@/components/ImageUploadContent/UploadButton' -import { useUser } from '../hooks/useUser' -import { TextInput } from '@/components/inputs/TextInput' -import { useTranslate } from '@tolgee/react' +import { UploadButton } from "@/components/ImageUploadContent/UploadButton"; +import { UploadIcon } from "@/components/icons"; +import { TextInput } from "@/components/inputs/TextInput"; +import { Avatar, HStack, Stack, Text, Tooltip } from "@chakra-ui/react"; +import { useTranslate } from "@tolgee/react"; +import React, { useState } from "react"; +import { useUser } from "../hooks/useUser"; +import { ApiTokensList } from "./ApiTokensList"; export const MyAccountForm = () => { - const { t } = useTranslate() - const { user, updateUser } = useUser() - const [name, setName] = useState(user?.name ?? '') - const [email, setEmail] = useState(user?.email ?? '') + const { t } = useTranslate(); + const { user, updateUser } = useUser(); + const [name, setName] = useState(user?.name ?? ""); + const [email, setEmail] = useState(user?.email ?? ""); const handleFileUploaded = async (url: string) => { - updateUser({ image: url }) - } + updateUser({ image: url }); + }; const handleNameChange = (newName: string) => { - setName(newName) - updateUser({ name: newName }) - } + setName(newName); + updateUser({ name: newName }); + }; const handleEmailChange = (newEmail: string) => { - setEmail(newEmail) - updateUser({ email: newEmail }) - } + setEmail(newEmail); + updateUser({ email: newEmail }); + }; return ( @@ -42,16 +42,16 @@ export const MyAccountForm = () => { fileType="image" filePathProps={{ userId: user.id, - fileName: 'avatar', + fileName: "avatar", }} leftIcon={} onFileUploaded={handleFileUploaded} > - {t('account.myAccount.changePhotoButton.label')} + {t("account.myAccount.changePhotoButton.label")} )} - {t('account.myAccount.changePhotoButton.specification')} + {t("account.myAccount.changePhotoButton.specification")} @@ -59,17 +59,17 @@ export const MyAccountForm = () => { - + { {user && } - ) -} + ); +}; diff --git a/apps/builder/src/features/account/components/UserPreferencesForm.tsx b/apps/builder/src/features/account/components/UserPreferencesForm.tsx index 61eb6f9827..2b8224591c 100644 --- a/apps/builder/src/features/account/components/UserPreferencesForm.tsx +++ b/apps/builder/src/features/account/components/UserPreferencesForm.tsx @@ -1,85 +1,85 @@ +import { MoreInfoTooltip } from "@/components/MoreInfoTooltip"; +import { ChevronDownIcon } from "@/components/icons"; import { - Stack, + Button, + HStack, Heading, Menu, MenuButton, - MenuList, MenuItem, - Button, - HStack, -} from '@chakra-ui/react' -import { GraphNavigation } from '@typebot.io/prisma' -import React, { useEffect } from 'react' -import { AppearanceRadioGroup } from './AppearanceRadioGroup' -import { useUser } from '../hooks/useUser' -import { ChevronDownIcon } from '@/components/icons' -import { MoreInfoTooltip } from '@/components/MoreInfoTooltip' -import { useTranslate, useTolgee } from '@tolgee/react' -import { useRouter } from 'next/router' -import { GraphNavigationRadioGroup } from './GraphNavigationRadioGroup' + MenuList, + Stack, +} from "@chakra-ui/react"; +import { useTolgee, useTranslate } from "@tolgee/react"; +import { GraphNavigation } from "@typebot.io/prisma/enum"; +import { useRouter } from "next/router"; +import React, { useEffect } from "react"; +import { useUser } from "../hooks/useUser"; +import { AppearanceRadioGroup } from "./AppearanceRadioGroup"; +import { GraphNavigationRadioGroup } from "./GraphNavigationRadioGroup"; const localeHumanReadable = { - en: 'English', - fr: 'Français', - de: 'Deutsch', - pt: 'Português', - 'pt-BR': 'Português (BR)', - ro: 'Română', - es: 'Español', - it: 'Italiano', -} as const + en: "English", + fr: "Français", + de: "Deutsch", + pt: "Português", + "pt-BR": "Português (BR)", + ro: "Română", + es: "Español", + it: "Italiano", +} as const; export const UserPreferencesForm = () => { - const { getLanguage } = useTolgee() - const router = useRouter() - const { t } = useTranslate() - const { user, updateUser } = useUser() + const { getLanguage } = useTolgee(); + const router = useRouter(); + const { t } = useTranslate(); + const { user, updateUser } = useUser(); useEffect(() => { if (!user?.graphNavigation) - updateUser({ graphNavigation: GraphNavigation.MOUSE }) - }, [updateUser, user?.graphNavigation]) + updateUser({ graphNavigation: GraphNavigation.MOUSE }); + }, [updateUser, user?.graphNavigation]); const changeAppearance = async (value: string) => { - updateUser({ preferredAppAppearance: value }) - } + updateUser({ preferredAppAppearance: value }); + }; const updateLocale = (locale: keyof typeof localeHumanReadable) => () => { - document.cookie = `NEXT_LOCALE=${locale}; path=/; max-age=31536000` + document.cookie = `NEXT_LOCALE=${locale}; path=/; max-age=31536000`; router.replace( { pathname: router.pathname, query: router.query, }, undefined, - { locale } - ) - } + { locale }, + ); + }; const changeGraphNavigation = async (value: string) => { - updateUser({ graphNavigation: value as GraphNavigation }) - } + updateUser({ graphNavigation: value as GraphNavigation }); + }; - const currentLanguage = getLanguage() + const currentLanguage = getLanguage(); return ( - {t('account.preferences.language.heading')} + {t("account.preferences.language.heading")} }> {currentLanguage ? localeHumanReadable[ currentLanguage as keyof typeof localeHumanReadable ] - : 'Loading...'} + : "Loading..."} {Object.keys(localeHumanReadable).map((locale) => ( { @@ -91,15 +91,15 @@ export const UserPreferencesForm = () => { ))} - {currentLanguage !== 'en' && ( + {currentLanguage !== "en" && ( - {t('account.preferences.language.tooltip')} + {t("account.preferences.language.tooltip")} )} - {t('account.preferences.graphNavigation.heading')} + {t("account.preferences.graphNavigation.heading")} { - {t('account.preferences.appearance.heading')} + {t("account.preferences.appearance.heading")} - ) -} + ); +}; diff --git a/apps/builder/src/features/account/hooks/useApiTokens.ts b/apps/builder/src/features/account/hooks/useApiTokens.ts index d38aafd373..69238a26ae 100644 --- a/apps/builder/src/features/account/hooks/useApiTokens.ts +++ b/apps/builder/src/features/account/hooks/useApiTokens.ts @@ -1,30 +1,30 @@ -import { fetcher } from '@/helpers/fetcher' -import useSWR from 'swr' -import { env } from '@typebot.io/env' -import { ApiTokenFromServer } from '../types' +import { fetcher } from "@/helpers/fetcher"; +import { env } from "@typebot.io/env"; +import useSWR from "swr"; +import type { ApiTokenFromServer } from "../types"; type ServerResponse = { - apiTokens: ApiTokenFromServer[] -} + apiTokens: ApiTokenFromServer[]; +}; export const useApiTokens = ({ userId, onError, }: { - userId?: string - onError: (error: Error) => void + userId?: string; + onError: (error: Error) => void; }) => { const { data, error, mutate } = useSWR( userId ? `/api/users/${userId}/api-tokens` : null, fetcher, { dedupingInterval: env.NEXT_PUBLIC_E2E_TEST ? 0 : undefined, - } - ) - if (error) onError(error) + }, + ); + if (error) onError(error); return { apiTokens: data?.apiTokens, isLoading: !error && !data, mutate, - } -} + }; +}; diff --git a/apps/builder/src/features/account/hooks/useUser.ts b/apps/builder/src/features/account/hooks/useUser.ts index c484569725..08f8069f1e 100644 --- a/apps/builder/src/features/account/hooks/useUser.ts +++ b/apps/builder/src/features/account/hooks/useUser.ts @@ -1,4 +1,4 @@ -import { useContext } from 'react' -import { userContext } from '../UserProvider' +import { useContext } from "react"; +import { userContext } from "../UserProvider"; -export const useUser = () => useContext(userContext) +export const useUser = () => useContext(userContext); diff --git a/apps/builder/src/features/account/queries/createApiTokenQuery.ts b/apps/builder/src/features/account/queries/createApiTokenQuery.ts index fba8971fa9..1dddb07326 100644 --- a/apps/builder/src/features/account/queries/createApiTokenQuery.ts +++ b/apps/builder/src/features/account/queries/createApiTokenQuery.ts @@ -1,14 +1,14 @@ -import { sendRequest } from '@typebot.io/lib' -import { ApiTokenFromServer } from '../types' +import { sendRequest } from "@typebot.io/lib/utils"; +import type { ApiTokenFromServer } from "../types"; export const createApiTokenQuery = ( userId: string, - { name }: { name: string } + { name }: { name: string }, ) => sendRequest<{ apiToken: ApiTokenFromServer & { token: string } }>({ url: `/api/users/${userId}/api-tokens`, - method: 'POST', + method: "POST", body: { name, }, - }) + }); diff --git a/apps/builder/src/features/account/queries/deleteApiTokenQuery.ts b/apps/builder/src/features/account/queries/deleteApiTokenQuery.ts index 0de20aff91..34148f1be4 100644 --- a/apps/builder/src/features/account/queries/deleteApiTokenQuery.ts +++ b/apps/builder/src/features/account/queries/deleteApiTokenQuery.ts @@ -1,14 +1,14 @@ -import { ApiToken } from '@typebot.io/prisma' -import { sendRequest } from '@typebot.io/lib' +import { sendRequest } from "@typebot.io/lib/utils"; +import type { Prisma } from "@typebot.io/prisma/types"; export const deleteApiTokenQuery = ({ userId, tokenId, }: { - userId: string - tokenId: string + userId: string; + tokenId: string; }) => - sendRequest<{ apiToken: ApiToken }>({ + sendRequest<{ apiToken: Prisma.ApiToken }>({ url: `/api/users/${userId}/api-tokens/${tokenId}`, - method: 'DELETE', - }) + method: "DELETE", + }); diff --git a/apps/builder/src/features/account/queries/updateUserQuery.ts b/apps/builder/src/features/account/queries/updateUserQuery.ts index 2ddb2a0927..214f02c48e 100644 --- a/apps/builder/src/features/account/queries/updateUserQuery.ts +++ b/apps/builder/src/features/account/queries/updateUserQuery.ts @@ -1,9 +1,9 @@ -import { sendRequest } from '@typebot.io/lib' -import { User } from '@typebot.io/schemas' +import { sendRequest } from "@typebot.io/lib/utils"; +import type { User } from "@typebot.io/schemas/features/user/schema"; export const updateUserQuery = async (id: string, user: Partial) => sendRequest({ url: `/api/users/${id}`, - method: 'PATCH', + method: "PATCH", body: user, - }) + }); diff --git a/apps/builder/src/features/account/types.ts b/apps/builder/src/features/account/types.ts index 4daf9a623e..9eff39ae56 100644 --- a/apps/builder/src/features/account/types.ts +++ b/apps/builder/src/features/account/types.ts @@ -1 +1,5 @@ -export type ApiTokenFromServer = { id: string; name: string; createdAt: string } +export type ApiTokenFromServer = { + id: string; + name: string; + createdAt: string; +}; diff --git a/apps/builder/src/features/analytics/analytics.spec.ts b/apps/builder/src/features/analytics/analytics.spec.ts index 7352dfe613..86109835f7 100644 --- a/apps/builder/src/features/analytics/analytics.spec.ts +++ b/apps/builder/src/features/analytics/analytics.spec.ts @@ -1,32 +1,32 @@ -import { getTestAsset } from '@/test/utils/playwright' -import test, { expect } from '@playwright/test' -import { createId } from '@paralleldrive/cuid2' +import { getTestAsset } from "@/test/utils/playwright"; +import { createId } from "@paralleldrive/cuid2"; +import test, { expect } from "@playwright/test"; import { importTypebotInDatabase, injectFakeResults, -} from '@typebot.io/playwright/databaseActions' -import { starterWorkspaceId } from '@typebot.io/playwright/databaseSetup' +} from "@typebot.io/playwright/databaseActions"; +import { starterWorkspaceId } from "@typebot.io/playwright/databaseSetup"; -test('analytics are not available for non-pro workspaces', async ({ page }) => { - const typebotId = createId() +test("analytics are not available for non-pro workspaces", async ({ page }) => { + const typebotId = createId(); await importTypebotInDatabase( - getTestAsset('typebots/results/submissionHeader.json'), + getTestAsset("typebots/results/submissionHeader.json"), { id: typebotId, workspaceId: starterWorkspaceId, - } - ) - await injectFakeResults({ typebotId, count: 10 }) - await page.goto(`/typebots/${typebotId}/results/analytics`) - const firstDropoffBox = page.locator('text="%" >> nth=0') - await firstDropoffBox.hover() + }, + ); + await injectFakeResults({ typebotId, count: 10 }); + await page.goto(`/typebots/${typebotId}/results/analytics`); + const firstDropoffBox = page.locator('text="%" >> nth=0'); + await firstDropoffBox.hover(); await expect( - page.locator('text="Upgrade your plan to PRO to reveal drop-off rate."') - ).toBeVisible() - await firstDropoffBox.click() + page.locator('text="Upgrade your plan to PRO to reveal drop-off rate."'), + ).toBeVisible(); + await firstDropoffBox.click(); await expect( page.locator( - 'text="You need to upgrade your plan in order to unlock in-depth analytics"' - ) - ).toBeVisible() -}) + 'text="You need to upgrade your plan in order to unlock in-depth analytics"', + ), + ).toBeVisible(); +}); diff --git a/apps/builder/src/features/analytics/api/getInDepthAnalyticsData.ts b/apps/builder/src/features/analytics/api/getInDepthAnalyticsData.ts index 280ffa9171..2e7e2d0713 100644 --- a/apps/builder/src/features/analytics/api/getInDepthAnalyticsData.ts +++ b/apps/builder/src/features/analytics/api/getInDepthAnalyticsData.ts @@ -1,26 +1,26 @@ -import prisma from '@typebot.io/lib/prisma' -import { authenticatedProcedure } from '@/helpers/server/trpc' -import { TRPCError } from '@trpc/server' -import { z } from 'zod' -import { canReadTypebots } from '@/helpers/databaseRules' -import { totalAnswersSchema } from '@typebot.io/schemas/features/analytics' -import { parseGroups } from '@typebot.io/schemas' -import { isInputBlock } from '@typebot.io/schemas/helpers' -import { defaultTimeFilter, timeFilterValues } from '../constants' +import { canReadTypebots } from "@/helpers/databaseRules"; +import { authenticatedProcedure } from "@/helpers/server/trpc"; +import { TRPCError } from "@trpc/server"; +import { isInputBlock } from "@typebot.io/blocks-core/helpers"; +import { parseGroups } from "@typebot.io/groups/schemas"; +import prisma from "@typebot.io/prisma"; +import { totalAnswersSchema } from "@typebot.io/schemas/features/analytics"; +import { z } from "@typebot.io/zod"; +import { defaultTimeFilter, timeFilterValues } from "../constants"; import { parseFromDateFromTimeFilter, parseToDateFromTimeFilter, -} from '../helpers/parseDateFromTimeFilter' +} from "../helpers/parseDateFromTimeFilter"; export const getInDepthAnalyticsData = authenticatedProcedure .meta({ openapi: { - method: 'GET', - path: '/v1/typebots/{typebotId}/analytics/inDepthData', + method: "GET", + path: "/v1/typebots/{typebotId}/analytics/inDepthData", protect: true, summary: - 'List total answers in blocks and off-default paths visited edges', - tags: ['Analytics'], + "List total answers in blocks and off-default paths visited edges", + tags: ["Analytics"], }, }) .input( @@ -28,33 +28,33 @@ export const getInDepthAnalyticsData = authenticatedProcedure typebotId: z.string(), timeFilter: z.enum(timeFilterValues).default(defaultTimeFilter), timeZone: z.string().optional(), - }) + }), ) .output( z.object({ totalAnswers: z.array(totalAnswersSchema), offDefaultPathVisitedEdges: z.array( - z.object({ edgeId: z.string(), total: z.number() }) + z.object({ edgeId: z.string(), total: z.number() }), ), - }) + }), ) .query( async ({ input: { typebotId, timeFilter, timeZone }, ctx: { user } }) => { const typebot = await prisma.typebot.findFirst({ where: canReadTypebots(typebotId, user), select: { publishedTypebot: true }, - }) + }); if (!typebot?.publishedTypebot) throw new TRPCError({ - code: 'NOT_FOUND', - message: 'Published typebot not found', - }) + code: "NOT_FOUND", + message: "Published typebot not found", + }); - const fromDate = parseFromDateFromTimeFilter(timeFilter, timeZone) - const toDate = parseToDateFromTimeFilter(timeFilter, timeZone) + const fromDate = parseFromDateFromTimeFilter(timeFilter, timeZone); + const toDate = parseToDateFromTimeFilter(timeFilter, timeZone); const totalAnswersPerBlock = await prisma.answer.groupBy({ - by: ['blockId', 'resultId'], + by: ["blockId", "resultId"], where: { result: { typebotId: typebot.publishedTypebot.typebotId, @@ -69,14 +69,14 @@ export const getInDepthAnalyticsData = authenticatedProcedure in: parseGroups(typebot.publishedTypebot.groups, { typebotVersion: typebot.publishedTypebot.version, }).flatMap((group) => - group.blocks.filter(isInputBlock).map((block) => block.id) + group.blocks.filter(isInputBlock).map((block) => block.id), ), }, }, - }) + }); const totalAnswersV2PerBlock = await prisma.answerV2.groupBy({ - by: ['blockId', 'resultId'], + by: ["blockId", "resultId"], where: { result: { typebotId: typebot.publishedTypebot.typebotId, @@ -91,24 +91,24 @@ export const getInDepthAnalyticsData = authenticatedProcedure in: parseGroups(typebot.publishedTypebot.groups, { typebotVersion: typebot.publishedTypebot.version, }).flatMap((group) => - group.blocks.filter(isInputBlock).map((block) => block.id) + group.blocks.filter(isInputBlock).map((block) => block.id), ), }, }, - }) + }); const uniqueCounts = totalAnswersPerBlock .concat(totalAnswersV2PerBlock) .reduce<{ - [key: string]: Set + [key: string]: Set; }>((acc, { blockId, resultId }) => { - acc[blockId] = acc[blockId] || new Set() - acc[blockId].add(resultId) - return acc - }, {}) + acc[blockId] = acc[blockId] || new Set(); + acc[blockId].add(resultId); + return acc; + }, {}); const offDefaultPathVisitedEdges = await prisma.visitedEdge.groupBy({ - by: ['edgeId'], + by: ["edgeId"], where: { result: { typebotId: typebot.publishedTypebot.typebotId, @@ -121,7 +121,7 @@ export const getInDepthAnalyticsData = authenticatedProcedure }, }, _count: { resultId: true }, - }) + }); return { totalAnswers: Object.keys(uniqueCounts).map((blockId) => ({ @@ -132,6 +132,6 @@ export const getInDepthAnalyticsData = authenticatedProcedure edgeId: e.edgeId, total: e._count.resultId, })), - } - } - ) + }; + }, + ); diff --git a/apps/builder/src/features/analytics/api/getStats.ts b/apps/builder/src/features/analytics/api/getStats.ts index e264307a0c..9ba37dee19 100644 --- a/apps/builder/src/features/analytics/api/getStats.ts +++ b/apps/builder/src/features/analytics/api/getStats.ts @@ -1,23 +1,23 @@ -import prisma from '@typebot.io/lib/prisma' -import { authenticatedProcedure } from '@/helpers/server/trpc' -import { TRPCError } from '@trpc/server' -import { z } from 'zod' -import { canReadTypebots } from '@/helpers/databaseRules' -import { Stats, statsSchema } from '@typebot.io/schemas' -import { defaultTimeFilter, timeFilterValues } from '../constants' +import { canReadTypebots } from "@/helpers/databaseRules"; +import { authenticatedProcedure } from "@/helpers/server/trpc"; +import { TRPCError } from "@trpc/server"; +import prisma from "@typebot.io/prisma"; +import { type Stats, statsSchema } from "@typebot.io/results/schemas/answers"; +import { z } from "@typebot.io/zod"; +import { defaultTimeFilter, timeFilterValues } from "../constants"; import { parseFromDateFromTimeFilter, parseToDateFromTimeFilter, -} from '../helpers/parseDateFromTimeFilter' +} from "../helpers/parseDateFromTimeFilter"; export const getStats = authenticatedProcedure .meta({ openapi: { - method: 'GET', - path: '/v1/typebots/{typebotId}/analytics/stats', + method: "GET", + path: "/v1/typebots/{typebotId}/analytics/stats", protect: true, - summary: 'Get results stats', - tags: ['Analytics'], + summary: "Get results stats", + tags: ["Analytics"], }, }) .input( @@ -25,7 +25,7 @@ export const getStats = authenticatedProcedure typebotId: z.string(), timeFilter: z.enum(timeFilterValues).default(defaultTimeFilter), timeZone: z.string().optional(), - }) + }), ) .output(z.object({ stats: statsSchema })) .query( @@ -33,15 +33,15 @@ export const getStats = authenticatedProcedure const typebot = await prisma.typebot.findFirst({ where: canReadTypebots(typebotId, user), select: { publishedTypebot: true, id: true }, - }) + }); if (!typebot?.publishedTypebot) throw new TRPCError({ - code: 'NOT_FOUND', - message: 'Published typebot not found', - }) + code: "NOT_FOUND", + message: "Published typebot not found", + }); - const fromDate = parseFromDateFromTimeFilter(timeFilter, timeZone) - const toDate = parseToDateFromTimeFilter(timeFilter, timeZone) + const fromDate = parseFromDateFromTimeFilter(timeFilter, timeZone); + const toDate = parseToDateFromTimeFilter(timeFilter, timeZone); const [totalViews, totalStarts, totalCompleted] = await prisma.$transaction([ @@ -83,16 +83,16 @@ export const getStats = authenticatedProcedure : undefined, }, }), - ]) + ]); const stats: Stats = { totalViews, totalStarts, totalCompleted, - } + }; return { stats, - } - } - ) + }; + }, + ); diff --git a/apps/builder/src/features/analytics/api/getTotalVisitedEdges.ts b/apps/builder/src/features/analytics/api/getTotalVisitedEdges.ts index b083aa21ec..0953bd2dd9 100644 --- a/apps/builder/src/features/analytics/api/getTotalVisitedEdges.ts +++ b/apps/builder/src/features/analytics/api/getTotalVisitedEdges.ts @@ -1,23 +1,23 @@ -import prisma from '@typebot.io/lib/prisma' -import { authenticatedProcedure } from '@/helpers/server/trpc' -import { TRPCError } from '@trpc/server' -import { z } from 'zod' -import { canReadTypebots } from '@/helpers/databaseRules' -import { totalVisitedEdgesSchema } from '@typebot.io/schemas' -import { defaultTimeFilter, timeFilterValues } from '../constants' +import { canReadTypebots } from "@/helpers/databaseRules"; +import { authenticatedProcedure } from "@/helpers/server/trpc"; +import { TRPCError } from "@trpc/server"; +import prisma from "@typebot.io/prisma"; +import { totalVisitedEdgesSchema } from "@typebot.io/schemas/features/analytics"; +import { z } from "@typebot.io/zod"; +import { defaultTimeFilter, timeFilterValues } from "../constants"; import { parseFromDateFromTimeFilter, parseToDateFromTimeFilter, -} from '../helpers/parseDateFromTimeFilter' +} from "../helpers/parseDateFromTimeFilter"; export const getTotalVisitedEdges = authenticatedProcedure .meta({ openapi: { - method: 'GET', - path: '/v1/typebots/{typebotId}/analytics/totalVisitedEdges', + method: "GET", + path: "/v1/typebots/{typebotId}/analytics/totalVisitedEdges", protect: true, - summary: 'List total edges used in results', - tags: ['Analytics'], + summary: "List total edges used in results", + tags: ["Analytics"], }, }) .input( @@ -25,30 +25,30 @@ export const getTotalVisitedEdges = authenticatedProcedure typebotId: z.string(), timeFilter: z.enum(timeFilterValues).default(defaultTimeFilter), timeZone: z.string().optional(), - }) + }), ) .output( z.object({ totalVisitedEdges: z.array(totalVisitedEdgesSchema), - }) + }), ) .query( async ({ input: { typebotId, timeFilter, timeZone }, ctx: { user } }) => { const typebot = await prisma.typebot.findFirst({ where: canReadTypebots(typebotId, user), select: { id: true }, - }) + }); if (!typebot?.id) throw new TRPCError({ - code: 'NOT_FOUND', - message: 'Published typebot not found', - }) + code: "NOT_FOUND", + message: "Published typebot not found", + }); - const fromDate = parseFromDateFromTimeFilter(timeFilter, timeZone) - const toDate = parseToDateFromTimeFilter(timeFilter, timeZone) + const fromDate = parseFromDateFromTimeFilter(timeFilter, timeZone); + const toDate = parseToDateFromTimeFilter(timeFilter, timeZone); const edges = await prisma.visitedEdge.groupBy({ - by: ['edgeId'], + by: ["edgeId"], where: { result: { typebotId: typebot.id, @@ -61,13 +61,13 @@ export const getTotalVisitedEdges = authenticatedProcedure }, }, _count: { resultId: true }, - }) + }); return { totalVisitedEdges: edges.map((e) => ({ edgeId: e.edgeId, total: e._count.resultId, })), - } - } - ) + }; + }, + ); diff --git a/apps/builder/src/features/analytics/api/router.ts b/apps/builder/src/features/analytics/api/router.ts index 8b1252ef0b..e58241b8ba 100644 --- a/apps/builder/src/features/analytics/api/router.ts +++ b/apps/builder/src/features/analytics/api/router.ts @@ -1,8 +1,8 @@ -import { router } from '@/helpers/server/trpc' -import { getStats } from './getStats' -import { getInDepthAnalyticsData } from './getInDepthAnalyticsData' +import { router } from "@/helpers/server/trpc"; +import { getInDepthAnalyticsData } from "./getInDepthAnalyticsData"; +import { getStats } from "./getStats"; export const analyticsRouter = router({ getInDepthAnalyticsData, getStats, -}) +}); diff --git a/apps/builder/src/features/analytics/components/AnalyticsGraphContainer.tsx b/apps/builder/src/features/analytics/components/AnalyticsGraphContainer.tsx index 00957e20a3..2cfba55907 100644 --- a/apps/builder/src/features/analytics/components/AnalyticsGraphContainer.tsx +++ b/apps/builder/src/features/analytics/components/AnalyticsGraphContainer.tsx @@ -1,53 +1,53 @@ +import { ChangePlanModal } from "@/features/billing/components/ChangePlanModal"; +import { useTypebot } from "@/features/editor/providers/TypebotProvider"; +import { Graph } from "@/features/graph/components/Graph"; +import { EventsCoordinatesProvider } from "@/features/graph/providers/EventsCoordinateProvider"; +import { GraphProvider } from "@/features/graph/providers/GraphProvider"; +import { trpc } from "@/lib/trpc"; import { Flex, Spinner, useColorModeValue, useDisclosure, -} from '@chakra-ui/react' -import { useTypebot } from '@/features/editor/providers/TypebotProvider' -import { - Edge, - GroupV6, - Stats, +} from "@chakra-ui/react"; +import { useTranslate } from "@tolgee/react"; +import { blockHasItems, isInputBlock } from "@typebot.io/blocks-core/helpers"; +import type { GroupV6 } from "@typebot.io/groups/schemas"; +import { isDefined } from "@typebot.io/lib/utils"; +import type { Stats } from "@typebot.io/results/schemas/answers"; +import type { TotalAnswers, TotalVisitedEdges, -} from '@typebot.io/schemas' -import React, { useMemo } from 'react' -import { StatsCards } from './StatsCards' -import { ChangePlanModal } from '@/features/billing/components/ChangePlanModal' -import { Graph } from '@/features/graph/components/Graph' -import { GraphProvider } from '@/features/graph/providers/GraphProvider' -import { useTranslate } from '@tolgee/react' -import { trpc } from '@/lib/trpc' -import { isDefined } from '@typebot.io/lib' -import { EventsCoordinatesProvider } from '@/features/graph/providers/EventsCoordinateProvider' -import { timeFilterValues } from '../constants' -import { blockHasItems, isInputBlock } from '@typebot.io/schemas/helpers' +} from "@typebot.io/schemas/features/analytics"; +import type { Edge } from "@typebot.io/typebot/schemas/edge"; +import React, { useMemo } from "react"; +import type { timeFilterValues } from "../constants"; +import { StatsCards } from "./StatsCards"; -const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone +const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; type Props = { - timeFilter: (typeof timeFilterValues)[number] - onTimeFilterChange: (timeFilter: (typeof timeFilterValues)[number]) => void - stats?: Stats -} + timeFilter: (typeof timeFilterValues)[number]; + onTimeFilterChange: (timeFilter: (typeof timeFilterValues)[number]) => void; + stats?: Stats; +}; export const AnalyticsGraphContainer = ({ timeFilter, onTimeFilterChange, stats, }: Props) => { - const { t } = useTranslate() - const { isOpen, onOpen, onClose } = useDisclosure() - const { typebot, publishedTypebot } = useTypebot() + const { t } = useTranslate(); + const { isOpen, onOpen, onClose } = useDisclosure(); + const { typebot, publishedTypebot } = useTypebot(); const { data } = trpc.analytics.getInDepthAnalyticsData.useQuery( { typebotId: typebot?.id as string, timeFilter, timeZone, }, - { enabled: isDefined(publishedTypebot) } - ) + { enabled: isDefined(publishedTypebot) }, + ); const totalVisitedEdges = useMemo(() => { if ( @@ -57,9 +57,9 @@ export const AnalyticsGraphContainer = ({ !data?.totalAnswers || !stats?.totalViews ) - return - const firstEdgeId = publishedTypebot.events[0].outgoingEdgeId - if (!firstEdgeId) return + return; + const firstEdgeId = publishedTypebot.events[0].outgoingEdgeId; + if (!firstEdgeId) return; return populateEdgesWithVisitData({ edgeId: firstEdgeId, edges: publishedTypebot.edges, @@ -70,7 +70,7 @@ export const AnalyticsGraphContainer = ({ : [], totalAnswers: data.totalAnswers, edgeVisitHistory: [], - }) + }); }, [ data?.offDefaultPathVisitedEdges, data?.totalAnswers, @@ -78,16 +78,16 @@ export const AnalyticsGraphContainer = ({ publishedTypebot?.groups, publishedTypebot?.events, stats?.totalViews, - ]) + ]); return ( - ) -} + ); +}; const populateEdgesWithVisitData = ({ edgeId, @@ -141,27 +141,27 @@ const populateEdgesWithVisitData = ({ totalAnswers, edgeVisitHistory, }: { - edgeId: string - edges: Edge[] - groups: GroupV6[] - currentTotalUsers: number - totalVisitedEdges: TotalVisitedEdges[] - totalAnswers: TotalAnswers[] - edgeVisitHistory: string[] + edgeId: string; + edges: Edge[]; + groups: GroupV6[]; + currentTotalUsers: number; + totalVisitedEdges: TotalVisitedEdges[]; + totalAnswers: TotalAnswers[]; + edgeVisitHistory: string[]; }): TotalVisitedEdges[] => { - if (edgeVisitHistory.find((e) => e === edgeId)) return totalVisitedEdges + if (edgeVisitHistory.find((e) => e === edgeId)) return totalVisitedEdges; totalVisitedEdges.push({ edgeId, total: currentTotalUsers, - }) - edgeVisitHistory.push(edgeId) - const edge = edges.find((edge) => edge.id === edgeId) - if (!edge) return totalVisitedEdges - const group = groups.find((group) => edge?.to.groupId === group.id) - if (!group) return totalVisitedEdges + }); + edgeVisitHistory.push(edgeId); + const edge = edges.find((edge) => edge.id === edgeId); + if (!edge) return totalVisitedEdges; + const group = groups.find((group) => edge?.to.groupId === group.id); + if (!group) return totalVisitedEdges; for (const block of edge.to.blockId ? group.blocks.slice( - group.blocks.findIndex((b) => b.id === edge.to.blockId) + group.blocks.findIndex((b) => b.id === edge.to.blockId), ) : group.blocks) { if (blockHasItems(block)) { @@ -173,19 +173,19 @@ const populateEdgesWithVisitData = ({ groups, currentTotalUsers: totalVisitedEdges.find( - (tve) => tve.edgeId === item.outgoingEdgeId + (tve) => tve.edgeId === item.outgoingEdgeId, )?.total ?? 0, totalVisitedEdges, totalAnswers, edgeVisitHistory, - }) + }); } } } if (block.outgoingEdgeId) { const totalUsers = isInputBlock(block) ? totalAnswers.find((a) => a.blockId === block.id)?.total - : currentTotalUsers + : currentTotalUsers; totalVisitedEdges = populateEdgesWithVisitData({ edgeId: block.outgoingEdgeId, edges, @@ -194,9 +194,9 @@ const populateEdgesWithVisitData = ({ totalVisitedEdges, totalAnswers, edgeVisitHistory, - }) + }); } } - return totalVisitedEdges -} + return totalVisitedEdges; +}; diff --git a/apps/builder/src/features/analytics/components/StatsCards.tsx b/apps/builder/src/features/analytics/components/StatsCards.tsx index f0bcf571c6..f4ed1c3b18 100644 --- a/apps/builder/src/features/analytics/components/StatsCards.tsx +++ b/apps/builder/src/features/analytics/components/StatsCards.tsx @@ -1,24 +1,24 @@ -import { useTranslate } from '@tolgee/react' import { - GridProps, + type GridProps, SimpleGrid, Skeleton, Stat, StatLabel, StatNumber, useColorModeValue, -} from '@chakra-ui/react' -import { Stats } from '@typebot.io/schemas' -import React from 'react' -import { timeFilterValues } from '../constants' -import { TimeFilterDropdown } from './TimeFilterDropdown' +} from "@chakra-ui/react"; +import { useTranslate } from "@tolgee/react"; +import type { Stats } from "@typebot.io/results/schemas/answers"; +import React from "react"; +import type { timeFilterValues } from "../constants"; +import { TimeFilterDropdown } from "./TimeFilterDropdown"; const computeCompletionRate = (notAvailableLabel: string) => (totalCompleted: number, totalStarts: number): string => { - if (totalStarts === 0) return notAvailableLabel - return `${Math.round((totalCompleted / totalStarts) * 100)}%` - } + if (totalStarts === 0) return notAvailableLabel; + return `${Math.round((totalCompleted / totalStarts) * 100)}%`; + }; export const StatsCards = ({ stats, @@ -26,12 +26,12 @@ export const StatsCards = ({ onTimeFilterChange, ...props }: { - stats?: Stats - timeFilter: (typeof timeFilterValues)[number] - onTimeFilterChange: (timeFilter: (typeof timeFilterValues)[number]) => void + stats?: Stats; + timeFilter: (typeof timeFilterValues)[number]; + onTimeFilterChange: (timeFilter: (typeof timeFilterValues)[number]) => void; } & GridProps) => { - const { t } = useTranslate() - const bg = useColorModeValue('white', 'gray.900') + const { t } = useTranslate(); + const bg = useColorModeValue("white", "gray.900"); return ( - {t('analytics.viewsLabel')} + {t("analytics.viewsLabel")} {stats ? ( {stats.totalViews} ) : ( @@ -49,7 +49,7 @@ export const StatsCards = ({ )} - {t('analytics.startsLabel')} + {t("analytics.startsLabel")} {stats ? ( {stats.totalStarts} ) : ( @@ -57,12 +57,12 @@ export const StatsCards = ({ )} - {t('analytics.completionRateLabel')} + {t("analytics.completionRateLabel")} {stats ? ( - {computeCompletionRate(t('analytics.notAvailableLabel'))( + {computeCompletionRate(t("analytics.notAvailableLabel"))( stats.totalCompleted, - stats.totalStarts + stats.totalStarts, )} ) : ( @@ -76,5 +76,5 @@ export const StatsCards = ({ boxShadow="md" /> - ) -} + ); +}; diff --git a/apps/builder/src/features/analytics/components/TimeFilterDropdown.tsx b/apps/builder/src/features/analytics/components/TimeFilterDropdown.tsx index 6ba70f0359..5ead414cc6 100644 --- a/apps/builder/src/features/analytics/components/TimeFilterDropdown.tsx +++ b/apps/builder/src/features/analytics/components/TimeFilterDropdown.tsx @@ -1,11 +1,11 @@ -import { DropdownList } from '@/components/DropdownList' -import { timeFilterLabels, timeFilterValues } from '../constants' -import { ButtonProps } from '@chakra-ui/react' +import { DropdownList } from "@/components/DropdownList"; +import type { ButtonProps } from "@chakra-ui/react"; +import { timeFilterLabels, type timeFilterValues } from "../constants"; type Props = { - timeFilter: (typeof timeFilterValues)[number] - onTimeFilterChange: (timeFilter: (typeof timeFilterValues)[number]) => void -} & ButtonProps + timeFilter: (typeof timeFilterValues)[number]; + onTimeFilterChange: (timeFilter: (typeof timeFilterValues)[number]) => void; +} & ButtonProps; export const TimeFilterDropdown = ({ timeFilter, @@ -23,4 +23,4 @@ export const TimeFilterDropdown = ({ } {...props} /> -) +); diff --git a/apps/builder/src/features/analytics/constants.ts b/apps/builder/src/features/analytics/constants.ts index ebba096afa..fe0b95606e 100644 --- a/apps/builder/src/features/analytics/constants.ts +++ b/apps/builder/src/features/analytics/constants.ts @@ -1,24 +1,24 @@ export const timeFilterValues = [ - 'today', - 'last7Days', - 'last30Days', - 'monthToDate', - 'lastMonth', - 'yearToDate', - 'allTime', -] as const + "today", + "last7Days", + "last30Days", + "monthToDate", + "lastMonth", + "yearToDate", + "allTime", +] as const; export const timeFilterLabels: Record< (typeof timeFilterValues)[number], string > = { - today: 'Today', - last7Days: 'Last 7 days', - last30Days: 'Last 30 days', - monthToDate: 'Month to date', - lastMonth: 'Last month', - yearToDate: 'Year to date', - allTime: 'All time', -} + today: "Today", + last7Days: "Last 7 days", + last30Days: "Last 30 days", + monthToDate: "Month to date", + lastMonth: "Last month", + yearToDate: "Year to date", + allTime: "All time", +}; -export const defaultTimeFilter = 'last7Days' as const +export const defaultTimeFilter = "last7Days" as const; diff --git a/apps/builder/src/features/analytics/helpers/computeTotalUsersAtBlock.ts b/apps/builder/src/features/analytics/helpers/computeTotalUsersAtBlock.ts index 65304b6b26..5fc372f3b6 100644 --- a/apps/builder/src/features/analytics/helpers/computeTotalUsersAtBlock.ts +++ b/apps/builder/src/features/analytics/helpers/computeTotalUsersAtBlock.ts @@ -1,10 +1,10 @@ -import { isNotDefined } from '@typebot.io/lib' -import { PublicTypebotV6 } from '@typebot.io/schemas' -import { isInputBlock } from '@typebot.io/schemas/helpers' -import { +import { isInputBlock } from "@typebot.io/blocks-core/helpers"; +import { isNotDefined } from "@typebot.io/lib/utils"; +import type { TotalAnswers, TotalVisitedEdges, -} from '@typebot.io/schemas/features/analytics' +} from "@typebot.io/schemas/features/analytics"; +import type { PublicTypebotV6 } from "@typebot.io/typebot/schemas/publicTypebot"; export const computeTotalUsersAtBlock = ( currentBlockId: string, @@ -13,49 +13,49 @@ export const computeTotalUsersAtBlock = ( totalVisitedEdges, totalAnswers, }: { - publishedTypebot: PublicTypebotV6 - totalVisitedEdges: TotalVisitedEdges[] - totalAnswers: TotalAnswers[] - } + publishedTypebot: PublicTypebotV6; + totalVisitedEdges: TotalVisitedEdges[]; + totalAnswers: TotalAnswers[]; + }, ): number => { - let totalUsers = 0 + let totalUsers = 0; const currentGroup = publishedTypebot.groups.find((group) => - group.blocks.find((block) => block.id === currentBlockId) - ) - if (!currentGroup) return 0 + group.blocks.find((block) => block.id === currentBlockId), + ); + if (!currentGroup) return 0; const currentBlockIndex = currentGroup.blocks.findIndex( - (block) => block.id === currentBlockId - ) - const previousBlocks = currentGroup.blocks.slice(0, currentBlockIndex + 1) + (block) => block.id === currentBlockId, + ); + const previousBlocks = currentGroup.blocks.slice(0, currentBlockIndex + 1); for (const block of previousBlocks.reverse()) { if (currentBlockId !== block.id && isInputBlock(block)) - return totalAnswers.find((t) => t.blockId === block.id)?.total ?? 0 + return totalAnswers.find((t) => t.blockId === block.id)?.total ?? 0; const incomingEdges = publishedTypebot.edges.filter( - (edge) => edge.to.blockId === block.id - ) - if (!incomingEdges.length) continue + (edge) => edge.to.blockId === block.id, + ); + if (!incomingEdges.length) continue; totalUsers += incomingEdges.reduce( (acc, incomingEdge) => acc + (totalVisitedEdges.find( - (totalEdge) => totalEdge.edgeId === incomingEdge.id + (totalEdge) => totalEdge.edgeId === incomingEdge.id, )?.total ?? 0), - 0 - ) + 0, + ); } const edgesConnectedToGroup = publishedTypebot.edges.filter( (edge) => - edge.to.groupId === currentGroup.id && isNotDefined(edge.to.blockId) - ) + edge.to.groupId === currentGroup.id && isNotDefined(edge.to.blockId), + ); totalUsers += edgesConnectedToGroup.reduce( (acc, connectedEdge) => acc + (totalVisitedEdges.find( - (totalEdge) => totalEdge.edgeId === connectedEdge.id + (totalEdge) => totalEdge.edgeId === connectedEdge.id, )?.total ?? 0), - 0 - ) + 0, + ); - return totalUsers -} + return totalUsers; +}; diff --git a/apps/builder/src/features/analytics/helpers/getTotalAnswersAtBlock.ts b/apps/builder/src/features/analytics/helpers/getTotalAnswersAtBlock.ts index c17769e707..40cc9be18b 100644 --- a/apps/builder/src/features/analytics/helpers/getTotalAnswersAtBlock.ts +++ b/apps/builder/src/features/analytics/helpers/getTotalAnswersAtBlock.ts @@ -1,6 +1,6 @@ -import { byId } from '@typebot.io/lib' -import { PublicTypebotV6 } from '@typebot.io/schemas' -import { TotalAnswers } from '@typebot.io/schemas/features/analytics' +import { byId } from "@typebot.io/lib/utils"; +import type { TotalAnswers } from "@typebot.io/schemas/features/analytics"; +import type { PublicTypebotV6 } from "@typebot.io/typebot/schemas/publicTypebot"; export const getTotalAnswersAtBlock = ( currentBlockId: string, @@ -8,13 +8,13 @@ export const getTotalAnswersAtBlock = ( publishedTypebot, totalAnswers, }: { - publishedTypebot: PublicTypebotV6 - totalAnswers: TotalAnswers[] - } + publishedTypebot: PublicTypebotV6; + totalAnswers: TotalAnswers[]; + }, ): number => { const block = publishedTypebot.groups .flatMap((g) => g.blocks) - .find(byId(currentBlockId)) - if (!block) throw new Error(`Block ${currentBlockId} not found`) - return totalAnswers.find((t) => t.blockId === block.id)?.total ?? 0 -} + .find(byId(currentBlockId)); + if (!block) throw new Error(`Block ${currentBlockId} not found`); + return totalAnswers.find((t) => t.blockId === block.id)?.total ?? 0; +}; diff --git a/apps/builder/src/features/analytics/helpers/parseDateFromTimeFilter.ts b/apps/builder/src/features/analytics/helpers/parseDateFromTimeFilter.ts index 783012c421..d9fd60fd85 100644 --- a/apps/builder/src/features/analytics/helpers/parseDateFromTimeFilter.ts +++ b/apps/builder/src/features/analytics/helpers/parseDateFromTimeFilter.ts @@ -1,72 +1,72 @@ -import { timeFilterValues } from '../constants' import { + endOfMonth, startOfDay, - subDays, - startOfYear, startOfMonth, - endOfMonth, + startOfYear, + subDays, subMonths, -} from 'date-fns' -import { utcToZonedTime, zonedTimeToUtc } from 'date-fns-tz' +} from "date-fns"; +import { utcToZonedTime, zonedTimeToUtc } from "date-fns-tz"; +import type { timeFilterValues } from "../constants"; export const parseFromDateFromTimeFilter = ( timeFilter: (typeof timeFilterValues)[number], - userTimezone: string = 'UTC' + userTimezone = "UTC", ): Date | null => { - const nowInUserTimezone = utcToZonedTime(new Date(), userTimezone) + const nowInUserTimezone = utcToZonedTime(new Date(), userTimezone); switch (timeFilter) { - case 'today': { - return zonedTimeToUtc(startOfDay(nowInUserTimezone), userTimezone) + case "today": { + return zonedTimeToUtc(startOfDay(nowInUserTimezone), userTimezone); } - case 'last7Days': { + case "last7Days": { return zonedTimeToUtc( subDays(startOfDay(nowInUserTimezone), 6), - userTimezone - ) + userTimezone, + ); } - case 'last30Days': { + case "last30Days": { return zonedTimeToUtc( subDays(startOfDay(nowInUserTimezone), 29), - userTimezone - ) + userTimezone, + ); } - case 'lastMonth': { + case "lastMonth": { return zonedTimeToUtc( subMonths(startOfMonth(nowInUserTimezone), 1), - userTimezone - ) + userTimezone, + ); } - case 'monthToDate': { - return zonedTimeToUtc(startOfMonth(nowInUserTimezone), userTimezone) + case "monthToDate": { + return zonedTimeToUtc(startOfMonth(nowInUserTimezone), userTimezone); } - case 'yearToDate': { - return zonedTimeToUtc(startOfYear(nowInUserTimezone), userTimezone) + case "yearToDate": { + return zonedTimeToUtc(startOfYear(nowInUserTimezone), userTimezone); } - case 'allTime': - return null + case "allTime": + return null; } -} +}; export const parseToDateFromTimeFilter = ( timeFilter: (typeof timeFilterValues)[number], - userTimezone: string = 'UTC' + userTimezone = "UTC", ): Date | null => { - const nowInUserTimezone = utcToZonedTime(new Date(), userTimezone) + const nowInUserTimezone = utcToZonedTime(new Date(), userTimezone); switch (timeFilter) { - case 'lastMonth': { + case "lastMonth": { return zonedTimeToUtc( subMonths(endOfMonth(nowInUserTimezone), 1), - userTimezone - ) + userTimezone, + ); } - case 'last30Days': - case 'last7Days': - case 'today': - case 'monthToDate': - case 'yearToDate': - case 'allTime': - return null + case "last30Days": + case "last7Days": + case "today": + case "monthToDate": + case "yearToDate": + case "allTime": + return null; } -} +}; diff --git a/apps/builder/src/features/auth/api/customAdapter.ts b/apps/builder/src/features/auth/api/customAdapter.ts index 891de5be31..10d069b746 100644 --- a/apps/builder/src/features/auth/api/customAdapter.ts +++ b/apps/builder/src/features/auth/api/customAdapter.ts @@ -1,49 +1,129 @@ +import { convertInvitationsToCollaborations } from "@/features/auth/helpers/convertInvitationsToCollaborations"; +import { getNewUserInvitations } from "@/features/auth/helpers/getNewUserInvitations"; +import { joinWorkspaces } from "@/features/auth/helpers/joinWorkspaces"; +import { parseWorkspaceDefaultPlan } from "@/features/workspace/helpers/parseWorkspaceDefaultPlan"; +import { createId } from "@paralleldrive/cuid2"; +import { env } from "@typebot.io/env"; +import { generateId } from "@typebot.io/lib/utils"; +import { WorkspaceRole } from "@typebot.io/prisma/enum"; +import type { Prisma } from "@typebot.io/prisma/types"; +import type { TelemetryEvent } from "@typebot.io/telemetry/schemas"; +import { trackEvents } from "@typebot.io/telemetry/trackEvents"; +import type { Account, Awaitable, User } from "next-auth"; + // Forked from https://github.com/nextauthjs/adapters/blob/main/packages/prisma/src/index.ts -import { - PrismaClient, - Prisma, - WorkspaceRole, - Session, -} from '@typebot.io/prisma' -import type { Adapter, AdapterUser } from 'next-auth/adapters' -import { createId } from '@paralleldrive/cuid2' -import { generateId } from '@typebot.io/lib' -import { TelemetryEvent } from '@typebot.io/schemas/features/telemetry' -import { convertInvitationsToCollaborations } from '@/features/auth/helpers/convertInvitationsToCollaborations' -import { getNewUserInvitations } from '@/features/auth/helpers/getNewUserInvitations' -import { joinWorkspaces } from '@/features/auth/helpers/joinWorkspaces' -import { parseWorkspaceDefaultPlan } from '@/features/workspace/helpers/parseWorkspaceDefaultPlan' -import { env } from '@typebot.io/env' -import { trackEvents } from '@typebot.io/telemetry/trackEvents' -export function customAdapter(p: PrismaClient): Adapter { +interface AdapterUser extends User { + id: string; + email: string; + emailVerified: Date | null; +} + +interface AdapterAccount extends Account { + userId: string; +} +interface AdapterSession { + sessionToken: string; + userId: string; + expires: Date; +} +interface VerificationToken { + identifier: string; + expires: Date; + token: string; +} + +type Adapter = DefaultAdapter & + (WithVerificationToken extends true + ? { + createVerificationToken: ( + verificationToken: VerificationToken, + ) => Awaitable; + /** + * Return verification token from the database + * and delete it so it cannot be used again. + */ + useVerificationToken: (params: { + identifier: string; + token: string; + }) => Awaitable; + } + : {}); +interface DefaultAdapter { + createUser: (user: Omit) => Awaitable; + getUser: (id: string) => Awaitable; + getUserByEmail: (email: string) => Awaitable; + /** Using the provider id and the id of the user for a specific account, get the user. */ + getUserByAccount: ( + providerAccountId: Pick, + ) => Awaitable; + updateUser: ( + user: Partial & Pick, + ) => Awaitable; + /** @todo Implement */ + deleteUser?: ( + userId: string, + ) => Promise | Awaitable; + linkAccount: ( + account: AdapterAccount, + ) => Promise | Awaitable; + /** @todo Implement */ + unlinkAccount?: ( + providerAccountId: Pick, + ) => Promise | Awaitable; + /** Creates a session for the user and returns it. */ + createSession: (session: { + sessionToken: string; + userId: string; + expires: Date; + }) => Awaitable; + getSessionAndUser: (sessionToken: string) => Awaitable<{ + session: AdapterSession; + user: AdapterUser; + } | null>; + updateSession: ( + session: Partial & Pick, + ) => Awaitable; + deleteSession: ( + sessionToken: string, + ) => Promise | Awaitable; + createVerificationToken?: ( + verificationToken: VerificationToken, + ) => Awaitable; + useVerificationToken?: (params: { + identifier: string; + token: string; + }) => Awaitable; +} + +export function customAdapter(p: Prisma.PrismaClient): Adapter { return { - createUser: async (data: Omit) => { + createUser: async (data) => { if (!data.email) - throw Error('Provider did not forward email but it is required') - const user = { id: createId(), email: data.email as string } + throw Error("Provider did not forward email but it is required"); + const user = { id: createId(), email: data.email as string }; const { invitations, workspaceInvitations } = await getNewUserInvitations( p, - user.email - ) + user.email, + ); if ( env.DISABLE_SIGNUP && env.ADMIN_EMAIL?.every((email) => email !== user.email) && invitations.length === 0 && workspaceInvitations.length === 0 ) - throw Error('New users are forbidden') + throw Error("New users are forbidden"); const newWorkspaceData = { name: data.name ? `${data.name}'s workspace` : `My workspace`, plan: parseWorkspaceDefaultPlan(data.email), - } + }; const createdUser = await p.user.create({ data: { ...data, id: user.id, apiTokens: { - create: { name: 'Default', token: generateId(24) }, + create: { name: "Default", token: generateId(24) }, }, workspaces: workspaceInvitations.length > 0 @@ -61,31 +141,31 @@ export function customAdapter(p: PrismaClient): Adapter { include: { workspaces: { select: { workspaceId: true } }, }, - }) - const newWorkspaceId = createdUser.workspaces.pop()?.workspaceId - const events: TelemetryEvent[] = [] + }); + const newWorkspaceId = createdUser.workspaces.pop()?.workspaceId; + const events: TelemetryEvent[] = []; if (newWorkspaceId) { events.push({ - name: 'Workspace created', + name: "Workspace created", workspaceId: newWorkspaceId, userId: createdUser.id, data: newWorkspaceData, - }) + }); } events.push({ - name: 'User created', + name: "User created", userId: createdUser.id, data: { email: data.email, - name: data.name ? (data.name as string).split(' ')[0] : undefined, + name: data.name ? (data.name as string).split(" ")[0] : undefined, }, - }) - await trackEvents(events) + }); + await trackEvents(events); if (invitations.length > 0) - await convertInvitationsToCollaborations(p, user, invitations) + await convertInvitationsToCollaborations(p, user, invitations); if (workspaceInvitations.length > 0) - await joinWorkspaces(p, user, workspaceInvitations) - return createdUser as AdapterUser + await joinWorkspaces(p, user, workspaceInvitations); + return createdUser as AdapterUser; }, getUser: async (id) => (await p.user.findUnique({ where: { id } })) as AdapterUser, @@ -95,8 +175,8 @@ export function customAdapter(p: PrismaClient): Adapter { const account = await p.account.findUnique({ where: { provider_providerAccountId }, select: { user: true }, - }) - return (account?.user ?? null) as AdapterUser | null + }); + return (account?.user ?? null) as AdapterUser | null; }, updateUser: async (data) => (await p.user.update({ where: { id: data.id }, data })) as AdapterUser, @@ -120,19 +200,22 @@ export function customAdapter(p: PrismaClient): Adapter { oauth_token: data.oauth_token as string, refresh_token_expires_in: data.refresh_token_expires_in as number, }, - }) + }); }, unlinkAccount: async (provider_providerAccountId) => { - await p.account.delete({ where: { provider_providerAccountId } }) + await p.account.delete({ where: { provider_providerAccountId } }); }, async getSessionAndUser(sessionToken) { const userAndSession = await p.session.findUnique({ where: { sessionToken }, include: { user: true }, - }) - if (!userAndSession) return null - const { user, ...session } = userAndSession - return { user, session } as { user: AdapterUser; session: Session } + }); + if (!userAndSession) return null; + const { user, ...session } = userAndSession; + return { user, session } as { + user: AdapterUser; + session: AdapterSession; + }; }, createSession: (data) => p.session.create({ data }), updateSession: (data) => @@ -142,12 +225,17 @@ export function customAdapter(p: PrismaClient): Adapter { createVerificationToken: (data) => p.verificationToken.create({ data }), async useVerificationToken(identifier_token) { try { - return await p.verificationToken.delete({ where: { identifier_token } }) + return await p.verificationToken.delete({ + where: { identifier_token }, + }); } catch (error) { - if ((error as Prisma.PrismaClientKnownRequestError).code === 'P2025') - return null - throw error + if ( + (error as Prisma.Prisma.PrismaClientKnownRequestError).code === + "P2025" + ) + return null; + throw error; } }, - } + }; } diff --git a/apps/builder/src/features/auth/components/DividerWithText.tsx b/apps/builder/src/features/auth/components/DividerWithText.tsx index c6e9c17445..7931205f0d 100644 --- a/apps/builder/src/features/auth/components/DividerWithText.tsx +++ b/apps/builder/src/features/auth/components/DividerWithText.tsx @@ -1,15 +1,15 @@ import { - FlexProps, - Flex, Box, Divider, + Flex, + type FlexProps, Text, useColorModeValue, -} from '@chakra-ui/react' -import React from 'react' +} from "@chakra-ui/react"; +import React from "react"; export const DividerWithText = (props: FlexProps) => { - const { children, ...flexProps } = props + const { children, ...flexProps } = props; return ( @@ -18,7 +18,7 @@ export const DividerWithText = (props: FlexProps) => { {children} @@ -27,5 +27,5 @@ export const DividerWithText = (props: FlexProps) => { - ) -} + ); +}; diff --git a/apps/builder/src/features/auth/components/SignInError.tsx b/apps/builder/src/features/auth/components/SignInError.tsx index 1f57b11495..4579814bd4 100644 --- a/apps/builder/src/features/auth/components/SignInError.tsx +++ b/apps/builder/src/features/auth/components/SignInError.tsx @@ -1,26 +1,26 @@ -import { useTranslate } from '@tolgee/react' -import { Alert } from '@chakra-ui/react' +import { Alert } from "@chakra-ui/react"; +import { useTranslate } from "@tolgee/react"; type Props = { - error: string -} + error: string; +}; export const SignInError = ({ error }: Props) => { - const { t } = useTranslate() + const { t } = useTranslate(); const errors: Record = { - Signin: t('auth.error.default'), - OAuthSignin: t('auth.error.default'), - OAuthCallback: t('auth.error.default'), - OAuthCreateAccount: t('auth.error.email'), - EmailCreateAccount: t('auth.error.default'), - Callback: t('auth.error.default'), - OAuthAccountNotLinked: t('auth.error.oauthNotLinked'), - default: t('auth.error.unknown'), - } - if (!errors[error]) return null + Signin: t("auth.error.default"), + OAuthSignin: t("auth.error.default"), + OAuthCallback: t("auth.error.default"), + OAuthCreateAccount: t("auth.error.email"), + EmailCreateAccount: t("auth.error.default"), + Callback: t("auth.error.default"), + OAuthAccountNotLinked: t("auth.error.oauthNotLinked"), + default: t("auth.error.unknown"), + }; + if (!errors[error]) return null; return ( {errors[error]} - ) -} + ); +}; diff --git a/apps/builder/src/features/auth/components/SignInForm.tsx b/apps/builder/src/features/auth/components/SignInForm.tsx index 85689f16de..3b60045ea8 100644 --- a/apps/builder/src/features/auth/components/SignInForm.tsx +++ b/apps/builder/src/features/auth/components/SignInForm.tsx @@ -1,141 +1,141 @@ +import { TextLink } from "@/components/TextLink"; +import { useToast } from "@/hooks/useToast"; +import { sanitizeUrl } from "@braintree/sanitize-url"; import { + Alert, + AlertIcon, Button, - HTMLChakraProps, + Flex, + HStack, + type HTMLChakraProps, Input, + SlideFade, + Spinner, Stack, - HStack, Text, - Spinner, - Alert, - Flex, - AlertIcon, - SlideFade, -} from '@chakra-ui/react' -import React, { ChangeEvent, FormEvent, useEffect } from 'react' -import { useState } from 'react' +} from "@chakra-ui/react"; +import { useTranslate } from "@tolgee/react"; +import type { BuiltInProviderType } from "next-auth/providers/index"; import { - ClientSafeProvider, + type ClientSafeProvider, + type LiteralUnion, getProviders, - LiteralUnion, signIn, useSession, -} from 'next-auth/react' -import { DividerWithText } from './DividerWithText' -import { SocialLoginButtons } from './SocialLoginButtons' -import { useRouter } from 'next/router' -import { BuiltInProviderType } from 'next-auth/providers' -import { useToast } from '@/hooks/useToast' -import { TextLink } from '@/components/TextLink' -import { SignInError } from './SignInError' -import { useTranslate } from '@tolgee/react' -import { sanitizeUrl } from '@braintree/sanitize-url' +} from "next-auth/react"; +import { useRouter } from "next/router"; +import type { ChangeEvent, FormEvent } from "react"; +import React, { useEffect, useState } from "react"; +import { DividerWithText } from "./DividerWithText"; +import { SignInError } from "./SignInError"; +import { SocialLoginButtons } from "./SocialLoginButtons"; type Props = { - defaultEmail?: string -} + defaultEmail?: string; +}; export const SignInForm = ({ defaultEmail, -}: Props & HTMLChakraProps<'form'>) => { - const { t } = useTranslate() - const router = useRouter() - const { status } = useSession() - const [authLoading, setAuthLoading] = useState(false) - const [isLoadingProviders, setIsLoadingProviders] = useState(true) +}: Props & HTMLChakraProps<"form">) => { + const { t } = useTranslate(); + const router = useRouter(); + const { status } = useSession(); + const [authLoading, setAuthLoading] = useState(false); + const [isLoadingProviders, setIsLoadingProviders] = useState(true); - const [emailValue, setEmailValue] = useState(defaultEmail ?? '') - const [isMagicLinkSent, setIsMagicLinkSent] = useState(false) + const [emailValue, setEmailValue] = useState(defaultEmail ?? ""); + const [isMagicLinkSent, setIsMagicLinkSent] = useState(false); - const { showToast } = useToast() + const { showToast } = useToast(); const [providers, setProviders] = useState< Record, ClientSafeProvider> - >() + >(); const hasNoAuthProvider = - !isLoadingProviders && Object.keys(providers ?? {}).length === 0 + !isLoadingProviders && Object.keys(providers ?? {}).length === 0; useEffect(() => { - if (status === 'authenticated') { - const redirectPath = router.query.redirectPath?.toString() - router.replace(redirectPath ? sanitizeUrl(redirectPath) : '/typebots') - return + if (status === "authenticated") { + const redirectPath = router.query.redirectPath?.toString(); + router.replace(redirectPath ? sanitizeUrl(redirectPath) : "/typebots"); + return; } - ;(async () => { - const providers = await getProviders() - setProviders(providers ?? undefined) - setIsLoadingProviders(false) - })() - }, [status, router]) + (async () => { + const providers = await getProviders(); + setProviders(providers ?? undefined); + setIsLoadingProviders(false); + })(); + }, [status, router]); useEffect(() => { - if (!router.isReady) return - if (router.query.error === 'ip-banned') { + if (!router.isReady) return; + if (router.query.error === "ip-banned") { showToast({ - status: 'info', + status: "info", description: - 'Your account has suspicious activity and is being reviewed by our team. Feel free to contact us.', - }) + "Your account has suspicious activity and is being reviewed by our team. Feel free to contact us.", + }); } - }, [router.isReady, router.query.error, showToast]) + }, [router.isReady, router.query.error, showToast]); const handleEmailChange = (e: ChangeEvent) => - setEmailValue(e.target.value) + setEmailValue(e.target.value); const handleEmailSubmit = async (e: FormEvent) => { - e.preventDefault() - if (isMagicLinkSent) return - setAuthLoading(true) + e.preventDefault(); + if (isMagicLinkSent) return; + setAuthLoading(true); try { - const response = await signIn('email', { + const response = await signIn("email", { email: emailValue, redirect: false, - }) + }); if (response?.error) { - if (response.error.includes('rate-limited')) + if (response.error.includes("rate-limited")) showToast({ - status: 'info', - description: t('auth.signinErrorToast.tooManyRequests'), - }) - else if (response.error.includes('sign-up-disabled')) + status: "info", + description: t("auth.signinErrorToast.tooManyRequests"), + }); + else if (response.error.includes("sign-up-disabled")) showToast({ - title: t('auth.signinErrorToast.title'), - description: t('auth.signinErrorToast.description'), - }) + title: t("auth.signinErrorToast.title"), + description: t("auth.signinErrorToast.description"), + }); else showToast({ - status: 'info', - description: t('errorMessage'), + status: "info", + description: t("errorMessage"), details: { - content: 'Check server logs to see relevent error message.', - lang: 'json', + content: "Check server logs to see relevent error message.", + lang: "json", }, - }) + }); } else { - setIsMagicLinkSent(true) + setIsMagicLinkSent(true); } } catch (e) { showToast({ - status: 'info', - description: 'An error occured while signing in', - }) + status: "info", + description: "An error occured while signing in", + }); } - setAuthLoading(false) - } + setAuthLoading(false); + }; - if (isLoadingProviders) return + if (isLoadingProviders) return ; if (hasNoAuthProvider) return ( - {t('auth.noProvider.preLink')}{' '} + {t("auth.noProvider.preLink")}{" "} - {t('auth.noProvider.link')} + {t("auth.noProvider.link")} - ) + ); return ( {!isMagicLinkSent && ( @@ -143,7 +143,7 @@ export const SignInForm = ({ {providers?.email && ( <> - {t('auth.orEmailLabel')} + {t("auth.orEmailLabel")} - {t('auth.emailSubmitButton.label')} + {t("auth.emailSubmitButton.label")} @@ -177,13 +177,13 @@ export const SignInForm = ({ - {t('auth.magicLink.title')} - {t('auth.magicLink.description')} + {t("auth.magicLink.title")} + {t("auth.magicLink.description")} - ) -} + ); +}; diff --git a/apps/builder/src/features/auth/components/SignInPage.tsx b/apps/builder/src/features/auth/components/SignInPage.tsx index c05f9eb8a9..7fd56c71e9 100644 --- a/apps/builder/src/features/auth/components/SignInPage.tsx +++ b/apps/builder/src/features/auth/components/SignInPage.tsx @@ -1,66 +1,66 @@ -import { Seo } from '@/components/Seo' -import { TextLink } from '@/components/TextLink' -import { T, useTranslate } from '@tolgee/react' -import { VStack, Heading, Text } from '@chakra-ui/react' -import { useRouter } from 'next/router' -import { SignInForm } from './SignInForm' +import { Seo } from "@/components/Seo"; +import { TextLink } from "@/components/TextLink"; +import { Heading, Text, VStack } from "@chakra-ui/react"; +import { T, useTranslate } from "@tolgee/react"; +import { useRouter } from "next/router"; +import { SignInForm } from "./SignInForm"; type Props = { - type: 'signin' | 'signup' - defaultEmail?: string -} + type: "signin" | "signup"; + defaultEmail?: string; +}; export const SignInPage = ({ type }: Props) => { - const { t } = useTranslate() - const { query } = useRouter() + const { t } = useTranslate(); + const { query } = useRouter(); return ( { - throw new Error('Sentry is working') + throw new Error("Sentry is working"); }} > - {type === 'signin' - ? t('auth.signin.heading') - : t('auth.register.heading')} + {type === "signin" + ? t("auth.signin.heading") + : t("auth.register.heading")} - {type === 'signin' ? ( + {type === "signin" ? ( - {t('auth.signin.noAccountLabel.preLink')}{' '} + {t("auth.signin.noAccountLabel.preLink")}{" "} - {t('auth.signin.noAccountLabel.link')} + {t("auth.signin.noAccountLabel.link")} ) : ( - {t('auth.register.alreadyHaveAccountLabel.preLink')}{' '} + {t("auth.register.alreadyHaveAccountLabel.preLink")}{" "} - {t('auth.register.alreadyHaveAccountLabel.link')} + {t("auth.register.alreadyHaveAccountLabel.link")} )} - {type === 'signup' ? ( + {type === "signup" ? ( , + terms: , privacy: ( - + ), }} /> ) : null} - ) -} + ); +}; diff --git a/apps/builder/src/features/auth/components/SocialLoginButtons.tsx b/apps/builder/src/features/auth/components/SocialLoginButtons.tsx index db627552a8..e5065cb636 100644 --- a/apps/builder/src/features/auth/components/SocialLoginButtons.tsx +++ b/apps/builder/src/features/auth/components/SocialLoginButtons.tsx @@ -1,59 +1,59 @@ -import { Stack, Button } from '@chakra-ui/react' -import { GithubIcon } from '@/components/icons' +import { GoogleLogo } from "@/components/GoogleLogo"; +import { GithubIcon } from "@/components/icons"; +import { AzureAdLogo } from "@/components/logos/AzureAdLogo"; +import { FacebookLogo } from "@/components/logos/FacebookLogo"; +import { GitlabLogo } from "@/components/logos/GitlabLogo"; +import { KeycloackLogo } from "@/components/logos/KeycloakLogo"; +import { Button, Stack } from "@chakra-ui/react"; +import { useTranslate } from "@tolgee/react"; +import { omit } from "@typebot.io/lib/utils"; +import type { BuiltInProviderType } from "next-auth/providers/index"; import { - ClientSafeProvider, - LiteralUnion, + type ClientSafeProvider, + type LiteralUnion, signIn, useSession, -} from 'next-auth/react' -import { useRouter } from 'next/router' -import React, { useState } from 'react' -import { stringify } from 'qs' -import { BuiltInProviderType } from 'next-auth/providers' -import { GoogleLogo } from '@/components/GoogleLogo' -import { omit } from '@typebot.io/lib' -import { AzureAdLogo } from '@/components/logos/AzureAdLogo' -import { FacebookLogo } from '@/components/logos/FacebookLogo' -import { GitlabLogo } from '@/components/logos/GitlabLogo' -import { useTranslate } from '@tolgee/react' -import { KeycloackLogo } from '@/components/logos/KeycloakLogo' +} from "next-auth/react"; +import { useRouter } from "next/router"; +import { stringify } from "qs"; +import React, { useState } from "react"; type Props = { providers: | Record, ClientSafeProvider> - | undefined -} + | undefined; +}; export const SocialLoginButtons = ({ providers }: Props) => { - const { t } = useTranslate() - const { query } = useRouter() - const { status } = useSession() + const { t } = useTranslate(); + const { query } = useRouter(); + const { status } = useSession(); const [authLoading, setAuthLoading] = - useState>() + useState>(); const handleSignIn = async (provider: string) => { - setAuthLoading(provider) + setAuthLoading(provider); await signIn(provider, { callbackUrl: query.callbackUrl?.toString() ?? - `/typebots?${stringify(omit(query, 'error', 'callbackUrl'))}`, - }) - setTimeout(() => setAuthLoading(undefined), 3000) - } + `/typebots?${stringify(omit(query, "error", "callbackUrl"))}`, + }); + setTimeout(() => setAuthLoading(undefined), 3000); + }; - const handleGitHubClick = () => handleSignIn('github') + const handleGitHubClick = () => handleSignIn("github"); - const handleGoogleClick = () => handleSignIn('google') + const handleGoogleClick = () => handleSignIn("google"); - const handleFacebookClick = () => handleSignIn('facebook') + const handleFacebookClick = () => handleSignIn("facebook"); - const handleGitlabClick = () => handleSignIn('gitlab') + const handleGitlabClick = () => handleSignIn("gitlab"); - const handleAzureAdClick = () => handleSignIn('azure-ad') + const handleAzureAdClick = () => handleSignIn("azure-ad"); - const handleCustomOAuthClick = () => handleSignIn('custom-oauth') + const handleCustomOAuthClick = () => handleSignIn("custom-oauth"); - const handleKeyCloackClick = () => handleSignIn('keycloak') + const handleKeyCloackClick = () => handleSignIn("keycloak"); return ( @@ -63,12 +63,12 @@ export const SocialLoginButtons = ({ providers }: Props) => { onClick={handleGitHubClick} data-testid="github" isLoading={ - ['loading', 'authenticated'].includes(status) || - authLoading === 'github' + ["loading", "authenticated"].includes(status) || + authLoading === "github" } variant="outline" > - {t('auth.socialLogin.githubButton.label')} + {t("auth.socialLogin.githubButton.label")} )} {providers?.google && ( @@ -77,12 +77,12 @@ export const SocialLoginButtons = ({ providers }: Props) => { onClick={handleGoogleClick} data-testid="google" isLoading={ - ['loading', 'authenticated'].includes(status) || - authLoading === 'google' + ["loading", "authenticated"].includes(status) || + authLoading === "google" } variant="outline" > - {t('auth.socialLogin.googleButton.label')} + {t("auth.socialLogin.googleButton.label")} )} {providers?.facebook && ( @@ -91,12 +91,12 @@ export const SocialLoginButtons = ({ providers }: Props) => { onClick={handleFacebookClick} data-testid="facebook" isLoading={ - ['loading', 'authenticated'].includes(status) || - authLoading === 'facebook' + ["loading", "authenticated"].includes(status) || + authLoading === "facebook" } variant="outline" > - {t('auth.socialLogin.facebookButton.label')} + {t("auth.socialLogin.facebookButton.label")} )} {providers?.gitlab && ( @@ -105,43 +105,43 @@ export const SocialLoginButtons = ({ providers }: Props) => { onClick={handleGitlabClick} data-testid="gitlab" isLoading={ - ['loading', 'authenticated'].includes(status) || - authLoading === 'gitlab' + ["loading", "authenticated"].includes(status) || + authLoading === "gitlab" } variant="outline" > - {t('auth.socialLogin.gitlabButton.label', { + {t("auth.socialLogin.gitlabButton.label", { gitlabProviderName: providers.gitlab.name, })} )} - {providers?.['azure-ad'] && ( + {providers?.["azure-ad"] && ( )} - {providers?.['custom-oauth'] && ( + {providers?.["custom-oauth"] && ( )} @@ -151,14 +151,14 @@ export const SocialLoginButtons = ({ providers }: Props) => { onClick={handleKeyCloackClick} data-testid="keycloak" isLoading={ - ['loading', 'authenticated'].includes(status) || - authLoading === 'keycloak' + ["loading", "authenticated"].includes(status) || + authLoading === "keycloak" } variant="outline" > - {t('auth.socialLogin.keycloakButton.label')} + {t("auth.socialLogin.keycloakButton.label")} )} - ) -} + ); +}; diff --git a/apps/builder/src/features/auth/helpers/convertInvitationsToCollaborations.ts b/apps/builder/src/features/auth/helpers/convertInvitationsToCollaborations.ts index f90c08ab37..669a85f12d 100644 --- a/apps/builder/src/features/auth/helpers/convertInvitationsToCollaborations.ts +++ b/apps/builder/src/features/auth/helpers/convertInvitationsToCollaborations.ts @@ -1,15 +1,16 @@ -import { Invitation, PrismaClient, WorkspaceRole } from '@typebot.io/prisma' +import { WorkspaceRole } from "@typebot.io/prisma/enum"; +import type { Prisma } from "@typebot.io/prisma/types"; -export type InvitationWithWorkspaceId = Invitation & { +export type InvitationWithWorkspaceId = Prisma.Invitation & { typebot: { - workspaceId: string | null - } -} + workspaceId: string | null; + }; +}; export const convertInvitationsToCollaborations = async ( - p: PrismaClient, + p: Prisma.PrismaClient, { id, email }: { id: string; email: string }, - invitations: InvitationWithWorkspaceId[] + invitations: InvitationWithWorkspaceId[], ) => { await p.collaboratorsOnTypebots.createMany({ data: invitations.map((invitation) => ({ @@ -17,18 +18,18 @@ export const convertInvitationsToCollaborations = async ( type: invitation.type, userId: id, })), - }) + }); const workspaceInvitations = invitations.reduce( (acc, invitation) => acc.some( - (inv) => inv.typebot.workspaceId === invitation.typebot.workspaceId + (inv) => inv.typebot.workspaceId === invitation.typebot.workspaceId, ) ? acc : [...acc, invitation], - [] - ) + [], + ); for (const invitation of workspaceInvitations) { - if (!invitation.typebot.workspaceId) continue + if (!invitation.typebot.workspaceId) continue; await p.memberInWorkspace.createMany({ data: [ { @@ -38,11 +39,11 @@ export const convertInvitationsToCollaborations = async ( }, ], skipDuplicates: true, - }) + }); } return p.invitation.deleteMany({ where: { email, }, - }) -} + }); +}; diff --git a/apps/builder/src/features/auth/helpers/getAuthenticatedUser.ts b/apps/builder/src/features/auth/helpers/getAuthenticatedUser.ts index 38b456a051..a3eb82e9e5 100644 --- a/apps/builder/src/features/auth/helpers/getAuthenticatedUser.ts +++ b/apps/builder/src/features/auth/helpers/getAuthenticatedUser.ts @@ -1,38 +1,38 @@ -import prisma from '@typebot.io/lib/prisma' -import { getAuthOptions } from '@/pages/api/auth/[...nextauth]' -import * as Sentry from '@sentry/nextjs' -import { User } from '@typebot.io/prisma' -import { NextApiRequest, NextApiResponse } from 'next' -import { getServerSession } from 'next-auth' -import { env } from '@typebot.io/env' -import { mockedUser } from '@typebot.io/lib/mockedUser' +import { getAuthOptions } from "@/pages/api/auth/[...nextauth]"; +import * as Sentry from "@sentry/nextjs"; +import { env } from "@typebot.io/env"; +import { mockedUser } from "@typebot.io/lib/mockedUser"; +import prisma from "@typebot.io/prisma"; +import type { Prisma } from "@typebot.io/prisma/types"; +import type { NextApiRequest, NextApiResponse } from "next"; +import { getServerSession } from "next-auth"; export const getAuthenticatedUser = async ( req: NextApiRequest, - res: NextApiResponse -): Promise => { - const bearerToken = extractBearerToken(req) - if (bearerToken) return authenticateByToken(bearerToken) + res: NextApiResponse, +): Promise => { + const bearerToken = extractBearerToken(req); + if (bearerToken) return authenticateByToken(bearerToken); const user = env.NEXT_PUBLIC_E2E_TEST ? mockedUser : ((await getServerSession(req, res, getAuthOptions({})))?.user as - | User - | undefined) - if (!user || !('id' in user)) return - Sentry.setUser({ id: user.id }) - return user -} + | Prisma.User + | undefined); + if (!user || !("id" in user)) return; + Sentry.setUser({ id: user.id }); + return user; +}; const authenticateByToken = async ( - apiToken: string -): Promise => { - if (typeof window !== 'undefined') return + apiToken: string, +): Promise => { + if (typeof window !== "undefined") return; const user = (await prisma.user.findFirst({ where: { apiTokens: { some: { token: apiToken } } }, - })) as User - Sentry.setUser({ id: user.id }) - return user -} + })) as Prisma.User; + Sentry.setUser({ id: user.id }); + return user; +}; const extractBearerToken = (req: NextApiRequest) => - req.headers['authorization']?.slice(7) + req.headers["authorization"]?.slice(7); diff --git a/apps/builder/src/features/auth/helpers/getNewUserInvitations.ts b/apps/builder/src/features/auth/helpers/getNewUserInvitations.ts index 014d1c48f5..bf5885cfd4 100644 --- a/apps/builder/src/features/auth/helpers/getNewUserInvitations.ts +++ b/apps/builder/src/features/auth/helpers/getNewUserInvitations.ts @@ -1,12 +1,12 @@ -import { PrismaClient, WorkspaceInvitation } from '@typebot.io/prisma' -import { InvitationWithWorkspaceId } from './convertInvitationsToCollaborations' +import type { Prisma } from "@typebot.io/prisma/types"; +import type { InvitationWithWorkspaceId } from "./convertInvitationsToCollaborations"; export const getNewUserInvitations = async ( - p: PrismaClient, - email: string + p: Prisma.PrismaClient, + email: string, ): Promise<{ - invitations: InvitationWithWorkspaceId[] - workspaceInvitations: WorkspaceInvitation[] + invitations: InvitationWithWorkspaceId[]; + workspaceInvitations: Prisma.WorkspaceInvitation[]; }> => { const [invitations, workspaceInvitations] = await p.$transaction([ p.invitation.findMany({ @@ -16,7 +16,7 @@ export const getNewUserInvitations = async ( p.workspaceInvitation.findMany({ where: { email }, }), - ]) + ]); - return { invitations, workspaceInvitations } -} + return { invitations, workspaceInvitations }; +}; diff --git a/apps/builder/src/features/auth/helpers/joinWorkspaces.ts b/apps/builder/src/features/auth/helpers/joinWorkspaces.ts index 0332260bb4..064f095050 100644 --- a/apps/builder/src/features/auth/helpers/joinWorkspaces.ts +++ b/apps/builder/src/features/auth/helpers/joinWorkspaces.ts @@ -1,9 +1,9 @@ -import { PrismaClient, WorkspaceInvitation } from '@typebot.io/prisma' +import type { Prisma } from "@typebot.io/prisma/types"; export const joinWorkspaces = async ( - p: PrismaClient, + p: Prisma.PrismaClient, { id, email }: { id: string; email: string }, - invitations: WorkspaceInvitation[] + invitations: Prisma.WorkspaceInvitation[], ) => { await p.$transaction([ p.memberInWorkspace.createMany({ @@ -19,5 +19,5 @@ export const joinWorkspaces = async ( email, }, }), - ]) -} + ]); +}; diff --git a/apps/builder/src/features/auth/helpers/sendVerificationRequest.ts b/apps/builder/src/features/auth/helpers/sendVerificationRequest.ts index 1cf579b0ea..10f2140df8 100644 --- a/apps/builder/src/features/auth/helpers/sendVerificationRequest.ts +++ b/apps/builder/src/features/auth/helpers/sendVerificationRequest.ts @@ -1,15 +1,15 @@ -import { sendMagicLinkEmail } from '@typebot.io/emails' +import { sendMagicLinkEmail } from "@typebot.io/emails/emails/MagicLinkEmail"; type Props = { - identifier: string - url: string -} + identifier: string; + url: string; +}; export const sendVerificationRequest = async ({ identifier, url }: Props) => { try { - await sendMagicLinkEmail({ url, to: identifier }) + await sendMagicLinkEmail({ url, to: identifier }); } catch (err) { - console.error(err) - throw new Error(`Magic link email could not be sent. See error above.`) + console.error(err); + throw new Error(`Magic link email could not be sent. See error above.`); } -} +}; diff --git a/apps/builder/src/features/billing/api/createCheckoutSession.ts b/apps/builder/src/features/billing/api/createCheckoutSession.ts index 7d15cced98..b874aebcf1 100644 --- a/apps/builder/src/features/billing/api/createCheckoutSession.ts +++ b/apps/builder/src/features/billing/api/createCheckoutSession.ts @@ -1,16 +1,16 @@ -import { authenticatedProcedure } from '@/helpers/server/trpc' -import { Plan } from '@typebot.io/prisma' -import { z } from 'zod' -import { createCheckoutSession as createCheckoutSessionHandler } from '@typebot.io/billing/api/createCheckoutSession' +import { authenticatedProcedure } from "@/helpers/server/trpc"; +import { createCheckoutSession as createCheckoutSessionHandler } from "@typebot.io/billing/api/createCheckoutSession"; +import { Plan } from "@typebot.io/prisma/enum"; +import { z } from "@typebot.io/zod"; export const createCheckoutSession = authenticatedProcedure .meta({ openapi: { - method: 'POST', - path: '/v1/billing/subscription/checkout', + method: "POST", + path: "/v1/billing/subscription/checkout", protect: true, - summary: 'Create checkout session to create a new subscription', - tags: ['Billing'], + summary: "Create checkout session to create a new subscription", + tags: ["Billing"], }, }) .input( @@ -18,7 +18,7 @@ export const createCheckoutSession = authenticatedProcedure email: z.string(), company: z.string(), workspaceId: z.string(), - currency: z.enum(['usd', 'eur']), + currency: z.enum(["usd", "eur"]), plan: z.enum([Plan.STARTER, Plan.PRO]), returnUrl: z.string(), vat: z @@ -27,13 +27,13 @@ export const createCheckoutSession = authenticatedProcedure value: z.string(), }) .optional(), - }) + }), ) .output( z.object({ checkoutUrl: z.string(), - }) + }), ) .mutation(async ({ input, ctx: { user } }) => - createCheckoutSessionHandler({ ...input, user }) - ) + createCheckoutSessionHandler({ ...input, user }), + ); diff --git a/apps/builder/src/features/billing/api/createCustomCheckoutSession.ts b/apps/builder/src/features/billing/api/createCustomCheckoutSession.ts index a397434f95..d9fafc7b6f 100644 --- a/apps/builder/src/features/billing/api/createCustomCheckoutSession.ts +++ b/apps/builder/src/features/billing/api/createCustomCheckoutSession.ts @@ -1,16 +1,16 @@ -import { authenticatedProcedure } from '@/helpers/server/trpc' -import { z } from 'zod' -import { createCustomCheckoutSession as createCustomCheckoutSessionHandler } from '@typebot.io/billing/api/createCustomCheckoutSession' +import { authenticatedProcedure } from "@/helpers/server/trpc"; +import { createCustomCheckoutSession as createCustomCheckoutSessionHandler } from "@typebot.io/billing/api/createCustomCheckoutSession"; +import { z } from "@typebot.io/zod"; export const createCustomCheckoutSession = authenticatedProcedure .meta({ openapi: { - method: 'POST', - path: '/v1/billing/subscription/custom-checkout', + method: "POST", + path: "/v1/billing/subscription/custom-checkout", protect: true, summary: - 'Create custom checkout session to make a workspace pay for a custom plan', - tags: ['Billing'], + "Create custom checkout session to make a workspace pay for a custom plan", + tags: ["Billing"], }, }) .input( @@ -18,16 +18,16 @@ export const createCustomCheckoutSession = authenticatedProcedure email: z.string(), workspaceId: z.string(), returnUrl: z.string(), - }) + }), ) .output( z.object({ checkoutUrl: z.string(), - }) + }), ) .mutation(async ({ input, ctx: { user } }) => createCustomCheckoutSessionHandler({ ...input, user, - }) - ) + }), + ); diff --git a/apps/builder/src/features/billing/api/getBillingPortalUrl.ts b/apps/builder/src/features/billing/api/getBillingPortalUrl.ts index 7b1b3e8ab0..46a4d5e2fc 100644 --- a/apps/builder/src/features/billing/api/getBillingPortalUrl.ts +++ b/apps/builder/src/features/billing/api/getBillingPortalUrl.ts @@ -1,27 +1,27 @@ -import { authenticatedProcedure } from '@/helpers/server/trpc' -import { z } from 'zod' -import { getBillingPortalUrl as getBillingPortalUrlHandler } from '@typebot.io/billing/api/getBillingPortalUrl' +import { authenticatedProcedure } from "@/helpers/server/trpc"; +import { getBillingPortalUrl as getBillingPortalUrlHandler } from "@typebot.io/billing/api/getBillingPortalUrl"; +import { z } from "@typebot.io/zod"; export const getBillingPortalUrl = authenticatedProcedure .meta({ openapi: { - method: 'GET', - path: '/v1/billing/subscription/portal', + method: "GET", + path: "/v1/billing/subscription/portal", protect: true, - summary: 'Get Stripe billing portal URL', - tags: ['Billing'], + summary: "Get Stripe billing portal URL", + tags: ["Billing"], }, }) .input( z.object({ workspaceId: z.string(), - }) + }), ) .output( z.object({ billingPortalUrl: z.string(), - }) + }), ) .query(async ({ input: { workspaceId }, ctx: { user } }) => - getBillingPortalUrlHandler({ workspaceId, user }) - ) + getBillingPortalUrlHandler({ workspaceId, user }), + ); diff --git a/apps/builder/src/features/billing/api/getSubscription.ts b/apps/builder/src/features/billing/api/getSubscription.ts index 3c17e52823..4ddb4781b2 100644 --- a/apps/builder/src/features/billing/api/getSubscription.ts +++ b/apps/builder/src/features/billing/api/getSubscription.ts @@ -1,28 +1,28 @@ -import { authenticatedProcedure } from '@/helpers/server/trpc' -import { z } from 'zod' -import { subscriptionSchema } from '@typebot.io/schemas/features/billing/subscription' -import { getSubscription as getSubscriptionHandler } from '@typebot.io/billing/api/getSubscription' +import { authenticatedProcedure } from "@/helpers/server/trpc"; +import { getSubscription as getSubscriptionHandler } from "@typebot.io/billing/api/getSubscription"; +import { subscriptionSchema } from "@typebot.io/billing/schemas/subscription"; +import { z } from "@typebot.io/zod"; export const getSubscription = authenticatedProcedure .meta({ openapi: { - method: 'GET', - path: '/v1/billing/subscription', + method: "GET", + path: "/v1/billing/subscription", protect: true, - summary: 'List invoices', - tags: ['Billing'], + summary: "List invoices", + tags: ["Billing"], }, }) .input( z.object({ workspaceId: z.string(), - }) + }), ) .output( z.object({ - subscription: subscriptionSchema.or(z.null().openapi({ type: 'string' })), - }) + subscription: subscriptionSchema.or(z.null().openapi({ type: "string" })), + }), ) .query(async ({ input: { workspaceId }, ctx: { user } }) => - getSubscriptionHandler({ workspaceId, user }) - ) + getSubscriptionHandler({ workspaceId, user }), + ); diff --git a/apps/builder/src/features/billing/api/getUsage.ts b/apps/builder/src/features/billing/api/getUsage.ts index 2e006019a9..0a446ad96a 100644 --- a/apps/builder/src/features/billing/api/getUsage.ts +++ b/apps/builder/src/features/billing/api/getUsage.ts @@ -1,15 +1,15 @@ -import { authenticatedProcedure } from '@/helpers/server/trpc' -import { z } from 'zod' -import { getUsage as getUsageHandler } from '@typebot.io/billing/api/getUsage' +import { authenticatedProcedure } from "@/helpers/server/trpc"; +import { getUsage as getUsageHandler } from "@typebot.io/billing/api/getUsage"; +import { z } from "@typebot.io/zod"; export const getUsage = authenticatedProcedure .meta({ openapi: { - method: 'GET', - path: '/v1/billing/usage', + method: "GET", + path: "/v1/billing/usage", protect: true, - summary: 'Get current plan usage', - tags: ['Billing'], + summary: "Get current plan usage", + tags: ["Billing"], }, }) .input( @@ -17,11 +17,11 @@ export const getUsage = authenticatedProcedure workspaceId: z .string() .describe( - '[Where to find my workspace ID?](../how-to#how-to-find-my-workspaceid)' + "[Where to find my workspace ID?](../how-to#how-to-find-my-workspaceid)", ), - }) + }), ) .output(z.object({ totalChatsUsed: z.number(), resetsAt: z.date() })) .query(async ({ input: { workspaceId }, ctx: { user } }) => - getUsageHandler({ workspaceId, user }) - ) + getUsageHandler({ workspaceId, user }), + ); diff --git a/apps/builder/src/features/billing/api/listInvoices.ts b/apps/builder/src/features/billing/api/listInvoices.ts index 074b521e76..b4accf0c34 100644 --- a/apps/builder/src/features/billing/api/listInvoices.ts +++ b/apps/builder/src/features/billing/api/listInvoices.ts @@ -1,16 +1,16 @@ -import { authenticatedProcedure } from '@/helpers/server/trpc' -import { z } from 'zod' -import { invoiceSchema } from '@typebot.io/schemas/features/billing/invoice' -import { listInvoices as listInvoicesHandler } from '@typebot.io/billing/api/listInvoices' +import { authenticatedProcedure } from "@/helpers/server/trpc"; +import { listInvoices as listInvoicesHandler } from "@typebot.io/billing/api/listInvoices"; +import { invoiceSchema } from "@typebot.io/billing/schemas/invoice"; +import { z } from "@typebot.io/zod"; export const listInvoices = authenticatedProcedure .meta({ openapi: { - method: 'GET', - path: '/v1/billing/invoices', + method: "GET", + path: "/v1/billing/invoices", protect: true, - summary: 'List invoices', - tags: ['Billing'], + summary: "List invoices", + tags: ["Billing"], }, }) .input( @@ -18,15 +18,15 @@ export const listInvoices = authenticatedProcedure workspaceId: z .string() .describe( - '[Where to find my workspace ID?](../how-to#how-to-find-my-workspaceid)' + "[Where to find my workspace ID?](../how-to#how-to-find-my-workspaceid)", ), - }) + }), ) .output( z.object({ invoices: z.array(invoiceSchema), - }) + }), ) .query(async ({ input: { workspaceId }, ctx: { user } }) => - listInvoicesHandler({ workspaceId, user }) - ) + listInvoicesHandler({ workspaceId, user }), + ); diff --git a/apps/builder/src/features/billing/api/router.ts b/apps/builder/src/features/billing/api/router.ts index de4cd5317c..2997e5d9e5 100644 --- a/apps/builder/src/features/billing/api/router.ts +++ b/apps/builder/src/features/billing/api/router.ts @@ -1,11 +1,11 @@ -import { router } from '@/helpers/server/trpc' -import { createCheckoutSession } from './createCheckoutSession' -import { getBillingPortalUrl } from './getBillingPortalUrl' -import { getSubscription } from './getSubscription' -import { getUsage } from './getUsage' -import { listInvoices } from './listInvoices' -import { updateSubscription } from './updateSubscription' -import { createCustomCheckoutSession } from './createCustomCheckoutSession' +import { router } from "@/helpers/server/trpc"; +import { createCheckoutSession } from "./createCheckoutSession"; +import { createCustomCheckoutSession } from "./createCustomCheckoutSession"; +import { getBillingPortalUrl } from "./getBillingPortalUrl"; +import { getSubscription } from "./getSubscription"; +import { getUsage } from "./getUsage"; +import { listInvoices } from "./listInvoices"; +import { updateSubscription } from "./updateSubscription"; export const billingRouter = router({ getBillingPortalUrl, @@ -15,4 +15,4 @@ export const billingRouter = router({ getSubscription, getUsage, createCustomCheckoutSession, -}) +}); diff --git a/apps/builder/src/features/billing/api/updateSubscription.ts b/apps/builder/src/features/billing/api/updateSubscription.ts index c83b582b9d..9f7db0c057 100644 --- a/apps/builder/src/features/billing/api/updateSubscription.ts +++ b/apps/builder/src/features/billing/api/updateSubscription.ts @@ -1,17 +1,17 @@ -import { authenticatedProcedure } from '@/helpers/server/trpc' -import { Plan } from '@typebot.io/prisma' -import { workspaceSchema } from '@typebot.io/schemas' -import { z } from 'zod' -import { updateSubscription as updateSubscriptionHandler } from '@typebot.io/billing/api/updateSubscription' +import { authenticatedProcedure } from "@/helpers/server/trpc"; +import { updateSubscription as updateSubscriptionHandler } from "@typebot.io/billing/api/updateSubscription"; +import { Plan } from "@typebot.io/prisma/enum"; +import { workspaceSchema } from "@typebot.io/workspaces/schemas"; +import { z } from "@typebot.io/zod"; export const updateSubscription = authenticatedProcedure .meta({ openapi: { - method: 'PATCH', - path: '/v1/billing/subscription', + method: "PATCH", + path: "/v1/billing/subscription", protect: true, - summary: 'Update subscription', - tags: ['Billing'], + summary: "Update subscription", + tags: ["Billing"], }, }) .input( @@ -19,18 +19,18 @@ export const updateSubscription = authenticatedProcedure returnUrl: z.string(), workspaceId: z.string(), plan: z.enum([Plan.STARTER, Plan.PRO]), - currency: z.enum(['usd', 'eur']), - }) + currency: z.enum(["usd", "eur"]), + }), ) .output( z.object({ workspace: workspaceSchema.nullish(), checkoutUrl: z.string().nullish(), - }) + }), ) .mutation(async ({ input, ctx: { user } }) => updateSubscriptionHandler({ ...input, user, - }) - ) + }), + ); diff --git a/apps/builder/src/features/billing/billing.spec.ts b/apps/builder/src/features/billing/billing.spec.ts index 0391eeb69f..492477d9da 100644 --- a/apps/builder/src/features/billing/billing.spec.ts +++ b/apps/builder/src/features/billing/billing.spec.ts @@ -2,127 +2,127 @@ import { addSubscriptionToWorkspace, cancelSubscription, createClaimableCustomPlan, -} from '@/test/utils/databaseActions' -import test, { expect } from '@playwright/test' -import { createId } from '@paralleldrive/cuid2' -import { Plan } from '@typebot.io/prisma' +} from "@/test/utils/databaseActions"; +import { createId } from "@paralleldrive/cuid2"; +import test, { expect } from "@playwright/test"; +import { env } from "@typebot.io/env"; import { createTypebots, createWorkspaces, deleteWorkspaces, injectFakeResults, -} from '@typebot.io/playwright/databaseActions' -import { env } from '@typebot.io/env' +} from "@typebot.io/playwright/databaseActions"; +import { Plan } from "@typebot.io/prisma/enum"; -const usageWorkspaceId = createId() -const usageTypebotId = createId() -const planChangeWorkspaceId = createId() -const enterpriseWorkspaceId = createId() +const usageWorkspaceId = createId(); +const usageTypebotId = createId(); +const planChangeWorkspaceId = createId(); +const enterpriseWorkspaceId = createId(); test.beforeAll(async () => { await createWorkspaces([ { id: usageWorkspaceId, - name: 'Usage Workspace', + name: "Usage Workspace", plan: Plan.STARTER, }, { id: planChangeWorkspaceId, - name: 'Plan Change Workspace', + name: "Plan Change Workspace", }, { id: enterpriseWorkspaceId, - name: 'Enterprise Workspace', + name: "Enterprise Workspace", }, - ]) - await createTypebots([{ id: usageTypebotId, workspaceId: usageWorkspaceId }]) -}) + ]); + await createTypebots([{ id: usageTypebotId, workspaceId: usageWorkspaceId }]); +}); test.afterAll(async () => { await deleteWorkspaces([ usageWorkspaceId, planChangeWorkspaceId, enterpriseWorkspaceId, - ]) -}) - -test('should display valid usage', async ({ page }) => { - await page.goto('/typebots') - await page.click('text=Settings & Members') - await page.click('text=Billing & Usage') - await expect(page.locator('text="/ 10,000"')).toBeVisible() - await page.getByText('Members', { exact: true }).click() + ]); +}); + +test("should display valid usage", async ({ page }) => { + await page.goto("/typebots"); + await page.click("text=Settings & Members"); + await page.click("text=Billing & Usage"); + await expect(page.locator('text="/ 10,000"')).toBeVisible(); + await page.getByText("Members", { exact: true }).click(); await expect( - page.getByRole('heading', { name: 'Members (1/5)' }) - ).toBeVisible() - await page.click('text=Pro workspace', { force: true }) - - await page.click('text=Pro workspace') - await page.click('text="Custom workspace"') - await page.click('text=Settings & Members') - await page.click('text=Billing & Usage') - await expect(page.locator('text="/ 100,000"')).toBeVisible() - await expect(page.getByText('Upgrade to Starter')).toBeHidden() - await expect(page.getByText('Upgrade to Pro')).toBeHidden() - await expect(page.getByText('Need custom limits?')).toBeHidden() - await page.getByText('Members', { exact: true }).click() + page.getByRole("heading", { name: "Members (1/5)" }), + ).toBeVisible(); + await page.click("text=Pro workspace", { force: true }); + + await page.click("text=Pro workspace"); + await page.click('text="Custom workspace"'); + await page.click("text=Settings & Members"); + await page.click("text=Billing & Usage"); + await expect(page.locator('text="/ 100,000"')).toBeVisible(); + await expect(page.getByText("Upgrade to Starter")).toBeHidden(); + await expect(page.getByText("Upgrade to Pro")).toBeHidden(); + await expect(page.getByText("Need custom limits?")).toBeHidden(); + await page.getByText("Members", { exact: true }).click(); await expect( - page.getByRole('heading', { name: 'Members (1/20)' }) - ).toBeVisible() - await page.click('text=Custom workspace', { force: true }) - - await page.click('text=Custom workspace') - await page.click('text="Free workspace"') - await page.click('text=Settings & Members') - await page.click('text=Billing & Usage') - await expect(page.locator('text="/ 200"')).toBeVisible() - await page.getByText('Members', { exact: true }).click() + page.getByRole("heading", { name: "Members (1/20)" }), + ).toBeVisible(); + await page.click("text=Custom workspace", { force: true }); + + await page.click("text=Custom workspace"); + await page.click('text="Free workspace"'); + await page.click("text=Settings & Members"); + await page.click("text=Billing & Usage"); + await expect(page.locator('text="/ 200"')).toBeVisible(); + await page.getByText("Members", { exact: true }).click(); await expect( - page.getByRole('heading', { name: 'Members (1/1)' }) - ).toBeVisible() - await page.click('text=Free workspace', { force: true }) + page.getByRole("heading", { name: "Members (1/1)" }), + ).toBeVisible(); + await page.click("text=Free workspace", { force: true }); await injectFakeResults({ count: 10, typebotId: usageTypebotId, - }) - await page.click('text=Free workspace') - await page.click('text="Usage Workspace"') - await page.click('text=Settings & Members') - await page.click('text=Billing & Usage') - await expect(page.locator('text="/ 2,000"')).toBeVisible() - await expect(page.locator('text="10" >> nth=0')).toBeVisible() + }); + await page.click("text=Free workspace"); + await page.click('text="Usage Workspace"'); + await page.click("text=Settings & Members"); + await page.click("text=Billing & Usage"); + await expect(page.locator('text="/ 2,000"')).toBeVisible(); + await expect(page.locator('text="10" >> nth=0')).toBeVisible(); await expect(page.locator('[role="progressbar"] >> nth=0')).toHaveAttribute( - 'aria-valuenow', - '1' - ) + "aria-valuenow", + "1", + ); await injectFakeResults({ typebotId: usageTypebotId, count: 1090, - }) - await page.click('text="Settings"') - await page.click('text="Billing & Usage"') - await expect(page.locator('text="/ 2,000"')).toBeVisible() - await expect(page.locator('text="1,100"')).toBeVisible() - await expect(page.locator('[aria-valuenow="55"]')).toBeVisible() -}) - -test('plan changes should work', async ({ page }) => { - test.setTimeout(80000) + }); + await page.click('text="Settings"'); + await page.click('text="Billing & Usage"'); + await expect(page.locator('text="/ 2,000"')).toBeVisible(); + await expect(page.locator('text="1,100"')).toBeVisible(); + await expect(page.locator('[aria-valuenow="55"]')).toBeVisible(); +}); + +test("plan changes should work", async ({ page }) => { + test.setTimeout(80000); // Upgrade to STARTER - await page.goto('/typebots') - await page.click('text=Pro workspace') - await page.click('text=Plan Change Workspace') - await page.click('text=Settings & Members') - await page.click('text=Billing & Usage') - await expect(page.locator('text="$39"')).toBeVisible() - await page.click('button >> text=Upgrade >> nth=0') - await page.getByLabel('Company name').fill('Company LLC') - await page.getByRole('button', { name: 'Go to checkout' }).click() - await page.waitForNavigation() - expect(page.url()).toContain('https://checkout.stripe.com') - await expect(page.locator('text=$39 >> nth=0')).toBeVisible() + await page.goto("/typebots"); + await page.click("text=Pro workspace"); + await page.click("text=Plan Change Workspace"); + await page.click("text=Settings & Members"); + await page.click("text=Billing & Usage"); + await expect(page.locator('text="$39"')).toBeVisible(); + await page.click("button >> text=Upgrade >> nth=0"); + await page.getByLabel("Company name").fill("Company LLC"); + await page.getByRole("button", { name: "Go to checkout" }).click(); + await page.waitForNavigation(); + expect(page.url()).toContain("https://checkout.stripe.com"); + await expect(page.locator("text=$39 >> nth=0")).toBeVisible(); const stripeId = await addSubscriptionToWorkspace( planChangeWorkspaceId, [ @@ -134,86 +134,86 @@ test('plan changes should work', async ({ page }) => { price: env.STRIPE_STARTER_CHATS_PRICE_ID, }, ], - { plan: Plan.STARTER } - ) + { plan: Plan.STARTER }, + ); // Update plan with additional quotas - await page.goto('/typebots') - await page.click('text=Settings & Members') - await page.click('text=Billing & Usage') - await expect(page.locator('text="/ 2,000"')).toBeVisible() - await expect(page.getByText('/ 2,000')).toBeVisible() + await page.goto("/typebots"); + await page.click("text=Settings & Members"); + await page.click("text=Billing & Usage"); + await expect(page.locator('text="/ 2,000"')).toBeVisible(); + await expect(page.getByText("/ 2,000")).toBeVisible(); // Upgrade to PRO - await expect(page.locator('text="$89"')).toBeVisible() - await page.click('button >> text=Upgrade') + await expect(page.locator('text="$89"')).toBeVisible(); + await page.click("button >> text=Upgrade"); await expect( - page.locator('text="Workspace PRO plan successfully updated" >> nth=0') - ).toBeVisible() + page.locator('text="Workspace PRO plan successfully updated" >> nth=0'), + ).toBeVisible(); // Go to customer portal await Promise.all([ page.waitForNavigation(), page.click('text="Billing portal"'), - ]) - await expect(page.getByText('$39.00')).toBeVisible({ + ]); + await expect(page.getByText("$39.00")).toBeVisible({ timeout: 10000, - }) - await expect(page.getByText('$50.00')).toBeVisible({ + }); + await expect(page.getByText("$50.00")).toBeVisible({ timeout: 10000, - }) - await expect(page.locator('text="Add payment method"')).toBeVisible() - await cancelSubscription(stripeId) + }); + await expect(page.locator('text="Add payment method"')).toBeVisible(); + await cancelSubscription(stripeId); // Cancel subscription - await page.goto('/typebots') - await page.click('text=Settings & Members') - await page.click('text=Billing & Usage') + await page.goto("/typebots"); + await page.click("text=Settings & Members"); + await page.click("text=Billing & Usage"); await expect( - page.getByTestId('current-subscription').getByTestId('pro-plan-tag') - ).toBeVisible() - await expect(page.getByText('Will be cancelled on')).toBeVisible() -}) - -test('should display invoices', async ({ page }) => { - await page.goto('/typebots') - await page.click('text=Settings & Members') - await page.click('text=Billing & Usage') - await expect(page.locator('text="Invoices"')).toBeHidden() - await page.click('text=Pro workspace', { force: true }) - - await page.click('text=Pro workspace') - await page.click('text=Plan Change Workspace') - await page.click('text=Settings & Members') - await page.click('text=Billing & Usage') - await expect(page.locator('text="Invoices"')).toBeVisible() - await expect(page.locator('tr')).toHaveCount(4) - await expect(page.getByText('$39.00')).toBeVisible() - await expect(page.getByText('$50.00')).toBeVisible() -}) - -test('custom plans should work', async ({ page }) => { - await page.goto('/typebots') - await page.click('text=Pro workspace') - await page.click('text=Enterprise Workspace') - await page.click('text=Settings & Members') - await page.click('text=Billing & Usage') - await expect(page.getByTestId('current-subscription')).toHaveText( - 'Current workspace subscription: Free' - ) + page.getByTestId("current-subscription").getByTestId("pro-plan-tag"), + ).toBeVisible(); + await expect(page.getByText("Will be cancelled on")).toBeVisible(); +}); + +test("should display invoices", async ({ page }) => { + await page.goto("/typebots"); + await page.click("text=Settings & Members"); + await page.click("text=Billing & Usage"); + await expect(page.locator('text="Invoices"')).toBeHidden(); + await page.click("text=Pro workspace", { force: true }); + + await page.click("text=Pro workspace"); + await page.click("text=Plan Change Workspace"); + await page.click("text=Settings & Members"); + await page.click("text=Billing & Usage"); + await expect(page.locator('text="Invoices"')).toBeVisible(); + await expect(page.locator("tr")).toHaveCount(4); + await expect(page.getByText("$39.00")).toBeVisible(); + await expect(page.getByText("$50.00")).toBeVisible(); +}); + +test("custom plans should work", async ({ page }) => { + await page.goto("/typebots"); + await page.click("text=Pro workspace"); + await page.click("text=Enterprise Workspace"); + await page.click("text=Settings & Members"); + await page.click("text=Billing & Usage"); + await expect(page.getByTestId("current-subscription")).toHaveText( + "Current workspace subscription: Free", + ); await createClaimableCustomPlan({ - currency: 'usd', + currency: "usd", price: 239, workspaceId: enterpriseWorkspaceId, chatsLimit: 100000, storageLimit: 50, seatsLimit: 10, - name: 'Acme custom plan', - description: 'Description of the deal', - }) + name: "Acme custom plan", + description: "Description of the deal", + }); - await page.goto('/typebots?claimCustomPlan=true') + await page.goto("/typebots?claimCustomPlan=true"); - await expect(page.getByRole('list').getByText('$239.00')).toBeVisible() - await expect(page.getByText('Subscribe to Acme custom plan')).toBeVisible() -}) + await expect(page.getByRole("list").getByText("$239.00")).toBeVisible(); + await expect(page.getByText("Subscribe to Acme custom plan")).toBeVisible(); +}); diff --git a/apps/builder/src/features/billing/components/BillingPortalButton.tsx b/apps/builder/src/features/billing/components/BillingPortalButton.tsx index 747dc4e922..6519dc12d4 100644 --- a/apps/builder/src/features/billing/components/BillingPortalButton.tsx +++ b/apps/builder/src/features/billing/components/BillingPortalButton.tsx @@ -1,15 +1,15 @@ -import { useToast } from '@/hooks/useToast' -import { trpc } from '@/lib/trpc' -import { useTranslate } from '@tolgee/react' -import { Button, ButtonProps, Link } from '@chakra-ui/react' +import { useToast } from "@/hooks/useToast"; +import { trpc } from "@/lib/trpc"; +import { Button, type ButtonProps, Link } from "@chakra-ui/react"; +import { useTranslate } from "@tolgee/react"; type Props = { - workspaceId: string -} & Pick + workspaceId: string; +} & Pick; export const BillingPortalButton = ({ workspaceId, colorScheme }: Props) => { - const { t } = useTranslate() - const { showToast } = useToast() + const { t } = useTranslate(); + const { showToast } = useToast(); const { data } = trpc.billing.getBillingPortalUrl.useQuery( { workspaceId, @@ -18,10 +18,10 @@ export const BillingPortalButton = ({ workspaceId, colorScheme }: Props) => { onError: (error) => { showToast({ description: error.message, - }) + }); }, - } - ) + }, + ); return ( - ) -} + ); +}; diff --git a/apps/builder/src/features/billing/components/BillingSettingsLayout.tsx b/apps/builder/src/features/billing/components/BillingSettingsLayout.tsx index 755950eeb2..f30e63fb73 100644 --- a/apps/builder/src/features/billing/components/BillingSettingsLayout.tsx +++ b/apps/builder/src/features/billing/components/BillingSettingsLayout.tsx @@ -1,15 +1,15 @@ -import { Stack } from '@chakra-ui/react' -import { useWorkspace } from '@/features/workspace/WorkspaceProvider' -import React from 'react' -import { InvoicesList } from './InvoicesList' -import { ChangePlanForm } from './ChangePlanForm' -import { UsageProgressBars } from './UsageProgressBars' -import { CurrentSubscriptionSummary } from './CurrentSubscriptionSummary' +import { useWorkspace } from "@/features/workspace/WorkspaceProvider"; +import { Stack } from "@chakra-ui/react"; +import React from "react"; +import { ChangePlanForm } from "./ChangePlanForm"; +import { CurrentSubscriptionSummary } from "./CurrentSubscriptionSummary"; +import { InvoicesList } from "./InvoicesList"; +import { UsageProgressBars } from "./UsageProgressBars"; export const BillingSettingsLayout = () => { - const { workspace, currentRole } = useWorkspace() + const { workspace, currentRole } = useWorkspace(); - if (!workspace) return null + if (!workspace) return null; return ( @@ -20,5 +20,5 @@ export const BillingSettingsLayout = () => { {workspace.stripeId && } - ) -} + ); +}; diff --git a/apps/builder/src/features/billing/components/ChangePlanForm.tsx b/apps/builder/src/features/billing/components/ChangePlanForm.tsx index 5524a130ff..8bcb8af7d5 100644 --- a/apps/builder/src/features/billing/components/ChangePlanForm.tsx +++ b/apps/builder/src/features/billing/components/ChangePlanForm.tsx @@ -1,97 +1,98 @@ -import { Stack, HStack, Text } from '@chakra-ui/react' -import { Plan, WorkspaceRole } from '@typebot.io/prisma' -import { TextLink } from '@/components/TextLink' -import { useToast } from '@/hooks/useToast' -import { trpc } from '@/lib/trpc' -import { PreCheckoutModal, PreCheckoutModalProps } from './PreCheckoutModal' -import { useState } from 'react' -import { ParentModalProvider } from '@/features/graph/providers/ParentModalProvider' -import { useUser } from '@/features/account/hooks/useUser' -import { StarterPlanPricingCard } from './StarterPlanPricingCard' -import { ProPlanPricingCard } from './ProPlanPricingCard' -import { useTranslate } from '@tolgee/react' -import { StripeClimateLogo } from './StripeClimateLogo' -import { guessIfUserIsEuropean } from '@typebot.io/billing/helpers/guessIfUserIsEuropean' -import { WorkspaceInApp } from '@/features/workspace/WorkspaceProvider' +import { TextLink } from "@/components/TextLink"; +import { useUser } from "@/features/account/hooks/useUser"; +import { ParentModalProvider } from "@/features/graph/providers/ParentModalProvider"; +import type { WorkspaceInApp } from "@/features/workspace/WorkspaceProvider"; +import { useToast } from "@/hooks/useToast"; +import { trpc } from "@/lib/trpc"; +import { HStack, Stack, Text } from "@chakra-ui/react"; +import { useTranslate } from "@tolgee/react"; +import { guessIfUserIsEuropean } from "@typebot.io/billing/helpers/guessIfUserIsEuropean"; +import { Plan, WorkspaceRole } from "@typebot.io/prisma/enum"; +import { useState } from "react"; +import type { PreCheckoutModalProps } from "./PreCheckoutModal"; +import { PreCheckoutModal } from "./PreCheckoutModal"; +import { ProPlanPricingCard } from "./ProPlanPricingCard"; +import { StarterPlanPricingCard } from "./StarterPlanPricingCard"; +import { StripeClimateLogo } from "./StripeClimateLogo"; type Props = { - workspace: WorkspaceInApp - currentRole?: WorkspaceRole - excludedPlans?: ('STARTER' | 'PRO')[] -} + workspace: WorkspaceInApp; + currentRole?: WorkspaceRole; + excludedPlans?: ("STARTER" | "PRO")[]; +}; export const ChangePlanForm = ({ workspace, currentRole, excludedPlans, }: Props) => { - const { t } = useTranslate() + const { t } = useTranslate(); - const { user } = useUser() - const { showToast } = useToast() + const { user } = useUser(); + const { showToast } = useToast(); const [preCheckoutPlan, setPreCheckoutPlan] = - useState() + useState(); - const trpcContext = trpc.useContext() + const trpcContext = trpc.useContext(); const { data, refetch } = trpc.billing.getSubscription.useQuery({ workspaceId: workspace.id, - }) + }); const { mutate: updateSubscription, isLoading: isUpdatingSubscription } = trpc.billing.updateSubscription.useMutation({ onError: (error) => { showToast({ description: error.message, - }) + }); }, onSuccess: ({ workspace, checkoutUrl }) => { if (checkoutUrl) { - window.location.href = checkoutUrl - return + window.location.href = checkoutUrl; + return; } - refetch() - trpcContext.workspace.getWorkspace.invalidate() + refetch(); + trpcContext.workspace.getWorkspace.invalidate(); showToast({ - status: 'success', - description: t('billing.updateSuccessToast.description', { + status: "success", + description: t("billing.updateSuccessToast.description", { plan: workspace?.plan, }), - }) + }); }, - }) + }); - const handlePayClick = async (plan: 'STARTER' | 'PRO') => { - if (!user) return + const handlePayClick = async (plan: "STARTER" | "PRO") => { + if (!user) return; const newSubscription = { plan, workspaceId: workspace.id, currency: data?.subscription?.currency ?? - (guessIfUserIsEuropean() ? 'eur' : 'usd'), - } as const + (guessIfUserIsEuropean() ? "eur" : "usd"), + } as const; if (workspace.stripeId) { updateSubscription({ ...newSubscription, returnUrl: window.location.href, - }) + }); } else { - setPreCheckoutPlan(newSubscription) + setPreCheckoutPlan(newSubscription); } - } + }; if ( data?.subscription?.cancelDate || - data?.subscription?.status === 'past_due' + data?.subscription?.status === "past_due" ) - return null + return null; const isSubscribed = (workspace.plan === Plan.STARTER || workspace.plan === Plan.PRO) && - workspace.stripeId + workspace.stripeId; - if (workspace.plan !== Plan.FREE && !isSubscribed) return null + if (workspace.plan !== Plan.FREE && !isSubscribed) return null; if (currentRole !== WorkspaceRole.ADMIN) return ( @@ -99,16 +100,16 @@ export const ChangePlanForm = ({ Only workspace admins can change the subscription plan. Contact your workspace admin to change the plan. - ) + ); return ( - {t('billing.contribution.preLink')}{' '} + {t("billing.contribution.preLink")}{" "} - {t('billing.contribution.link')} + {t("billing.contribution.link")} @@ -125,7 +126,7 @@ export const ChangePlanForm = ({ {data && ( - {excludedPlans?.includes('STARTER') ? null : ( + {excludedPlans?.includes("STARTER") ? null : ( handlePayClick(Plan.STARTER)} @@ -134,7 +135,7 @@ export const ChangePlanForm = ({ /> )} - {excludedPlans?.includes('PRO') ? null : ( + {excludedPlans?.includes("PRO") ? null : ( handlePayClick(Plan.PRO)} @@ -147,11 +148,11 @@ export const ChangePlanForm = ({ )} - {t('billing.customLimit.preLink')}{' '} - - {t('billing.customLimit.link')} + {t("billing.customLimit.preLink")}{" "} + + {t("billing.customLimit.link")} - ) -} + ); +}; diff --git a/apps/builder/src/features/billing/components/ChangePlanModal.tsx b/apps/builder/src/features/billing/components/ChangePlanModal.tsx index 9e53d85937..da3928c8a0 100644 --- a/apps/builder/src/features/billing/components/ChangePlanModal.tsx +++ b/apps/builder/src/features/billing/components/ChangePlanModal.tsx @@ -1,24 +1,24 @@ -import { AlertInfo } from '@/components/AlertInfo' -import { useWorkspace } from '@/features/workspace/WorkspaceProvider' -import { useTranslate } from '@tolgee/react' +import { AlertInfo } from "@/components/AlertInfo"; +import { useWorkspace } from "@/features/workspace/WorkspaceProvider"; import { + Button, + HStack, Modal, ModalBody, ModalContent, ModalFooter, ModalOverlay, Stack, - Button, - HStack, -} from '@chakra-ui/react' -import { ChangePlanForm } from './ChangePlanForm' +} from "@chakra-ui/react"; +import { useTranslate } from "@tolgee/react"; +import { ChangePlanForm } from "./ChangePlanForm"; export type ChangePlanModalProps = { - type?: string - isOpen: boolean - excludedPlans?: ('STARTER' | 'PRO')[] - onClose: () => void -} + type?: string; + isOpen: boolean; + excludedPlans?: ("STARTER" | "PRO")[]; + onClose: () => void; +}; export const ChangePlanModal = ({ onClose, @@ -26,21 +26,21 @@ export const ChangePlanModal = ({ type, excludedPlans, }: ChangePlanModalProps) => { - const { t } = useTranslate() - const { workspace, currentRole } = useWorkspace() + const { t } = useTranslate(); + const { workspace, currentRole } = useWorkspace(); return ( {type && ( - {t('billing.upgradeLimitLabel', { type: type })} + {t("billing.upgradeLimitLabel", { type: type })} )} {workspace && ( @@ -55,11 +55,11 @@ export const ChangePlanModal = ({ - ) -} + ); +}; diff --git a/apps/builder/src/features/billing/components/ChatsProTiersModal.tsx b/apps/builder/src/features/billing/components/ChatsProTiersModal.tsx index b6206fb3bb..8bfb1865e3 100644 --- a/apps/builder/src/features/billing/components/ChatsProTiersModal.tsx +++ b/apps/builder/src/features/billing/components/ChatsProTiersModal.tsx @@ -1,13 +1,13 @@ import { + Heading, Modal, - ModalOverlay, + ModalBody, + ModalCloseButton, ModalContent, + ModalFooter, ModalHeader, - ModalCloseButton, - ModalBody, + ModalOverlay, Stack, - ModalFooter, - Heading, Table, TableContainer, Tbody, @@ -15,25 +15,25 @@ import { Th, Thead, Tr, -} from '@chakra-ui/react' -import { useTranslate } from '@tolgee/react' -import { proChatTiers } from '@typebot.io/billing/constants' -import { formatPrice } from '@typebot.io/billing/helpers/formatPrice' +} from "@chakra-ui/react"; +import { useTranslate } from "@tolgee/react"; +import { proChatTiers } from "@typebot.io/billing/constants"; +import { formatPrice } from "@typebot.io/billing/helpers/formatPrice"; type Props = { - isOpen: boolean - onClose: () => void -} + isOpen: boolean; + onClose: () => void; +}; export const ChatsProTiersModal = ({ isOpen, onClose }: Props) => { - const { t } = useTranslate() + const { t } = useTranslate(); return ( - {t('billing.tiersModal.heading')} + {t("billing.tiersModal.heading")} @@ -51,33 +51,33 @@ export const ChatsProTiersModal = ({ isOpen, onClose }: Props) => { const pricePerMonth = (tier.flat_amount ?? proChatTiers.at(-2)?.flat_amount ?? - 0) / 100 + 0) / 100; return ( - ) + ); })}
{t('account.apiTokens.table.nameHeader')}{t('account.apiTokens.table.createdHeader')}{t("account.apiTokens.table.nameHeader")}{t("account.apiTokens.table.createdHeader")}
- {tier.up_to === 'inf' - ? '2,000,000+' + {tier.up_to === "inf" + ? "2,000,000+" : tier.up_to.toLocaleString()} - {index === 0 ? 'included' : formatPrice(pricePerMonth)} + {index === 0 ? "included" : formatPrice(pricePerMonth)} {index === proChatTiers.length - 1 ? formatPrice(4.42, { maxFractionDigits: 2 }) : index === 0 - ? 'included' - : formatPrice( - (((pricePerMonth * 100) / - ((tier.up_to as number) - - (proChatTiers.at(0)?.up_to as number))) * - 1000) / - 100, - { maxFractionDigits: 2 } - )} + ? "included" + : formatPrice( + (((pricePerMonth * 100) / + ((tier.up_to as number) - + (proChatTiers.at(0)?.up_to as number))) * + 1000) / + 100, + { maxFractionDigits: 2 }, + )}
@@ -86,5 +86,5 @@ export const ChatsProTiersModal = ({ isOpen, onClose }: Props) => { - ) -} + ); +}; diff --git a/apps/builder/src/features/billing/components/CurrentSubscriptionSummary.tsx b/apps/builder/src/features/billing/components/CurrentSubscriptionSummary.tsx index 13dedd0de0..9a91a86b12 100644 --- a/apps/builder/src/features/billing/components/CurrentSubscriptionSummary.tsx +++ b/apps/builder/src/features/billing/components/CurrentSubscriptionSummary.tsx @@ -1,53 +1,53 @@ +import { trpc } from "@/lib/trpc"; import { - Text, - HStack, - Stack, - Heading, Alert, AlertIcon, -} from '@chakra-ui/react' -import { Plan } from '@typebot.io/prisma' -import React from 'react' -import { PlanTag } from './PlanTag' -import { BillingPortalButton } from './BillingPortalButton' -import { trpc } from '@/lib/trpc' -import { Workspace } from '@typebot.io/schemas' -import { useTranslate } from '@tolgee/react' + HStack, + Heading, + Stack, + Text, +} from "@chakra-ui/react"; +import { useTranslate } from "@tolgee/react"; +import { Plan } from "@typebot.io/prisma/enum"; +import type { Workspace } from "@typebot.io/workspaces/schemas"; +import React from "react"; +import { BillingPortalButton } from "./BillingPortalButton"; +import { PlanTag } from "./PlanTag"; type Props = { - workspace: Pick -} + workspace: Pick; +}; export const CurrentSubscriptionSummary = ({ workspace }: Props) => { - const { t } = useTranslate() + const { t } = useTranslate(); const { data } = trpc.billing.getSubscription.useQuery({ workspaceId: workspace.id, - }) + }); const isSubscribed = (workspace.plan === Plan.STARTER || workspace.plan === Plan.PRO) && - workspace.stripeId + workspace.stripeId; return ( - {t('billing.currentSubscription.heading')} + {t("billing.currentSubscription.heading")} - {t('billing.currentSubscription.subheading')} + {t("billing.currentSubscription.subheading")} {data?.subscription?.cancelDate && ( - ({t('billing.currentSubscription.cancelDate')}{' '} + ({t("billing.currentSubscription.cancelDate")}{" "} {data.subscription.cancelDate.toDateString()}) )} - {data?.subscription?.status === 'past_due' && ( + {data?.subscription?.status === "past_due" && ( - {t('billing.currentSubscription.pastDueAlert')} + {t("billing.currentSubscription.pastDueAlert")} )} @@ -55,10 +55,10 @@ export const CurrentSubscriptionSummary = ({ workspace }: Props) => { )} - ) -} + ); +}; diff --git a/apps/builder/src/features/billing/components/FeaturesList.tsx b/apps/builder/src/features/billing/components/FeaturesList.tsx index 11e2015d71..da4906a9e1 100644 --- a/apps/builder/src/features/billing/components/FeaturesList.tsx +++ b/apps/builder/src/features/billing/components/FeaturesList.tsx @@ -1,13 +1,13 @@ +import { CheckIcon } from "@/components/icons"; import { - ListProps, - UnorderedList, Flex, - ListItem, ListIcon, -} from '@chakra-ui/react' -import { CheckIcon } from '@/components/icons' + ListItem, + type ListProps, + UnorderedList, +} from "@chakra-ui/react"; -type FeaturesListProps = { features: (string | JSX.Element)[] } & ListProps +type FeaturesListProps = { features: (string | JSX.Element)[] } & ListProps; export const FeaturesList = ({ features, ...props }: FeaturesListProps) => ( @@ -18,4 +18,4 @@ export const FeaturesList = ({ features, ...props }: FeaturesListProps) => (
))} -) +); diff --git a/apps/builder/src/features/billing/components/InvoicesList.tsx b/apps/builder/src/features/billing/components/InvoicesList.tsx index 9c1e003c15..287e76742b 100644 --- a/apps/builder/src/features/billing/components/InvoicesList.tsx +++ b/apps/builder/src/features/billing/components/InvoicesList.tsx @@ -1,48 +1,48 @@ +import { DownloadIcon, FileIcon } from "@/components/icons"; +import { useToast } from "@/hooks/useToast"; +import { trpc } from "@/lib/trpc"; import { - Stack, - Heading, Checkbox, + Heading, + IconButton, Skeleton, + Stack, Table, TableContainer, Tbody, Td, + Text, Th, Thead, Tr, - IconButton, - Text, -} from '@chakra-ui/react' -import { DownloadIcon, FileIcon } from '@/components/icons' -import Link from 'next/link' -import React from 'react' -import { trpc } from '@/lib/trpc' -import { useToast } from '@/hooks/useToast' -import { useTranslate } from '@tolgee/react' +} from "@chakra-ui/react"; +import { useTranslate } from "@tolgee/react"; +import Link from "next/link"; +import React from "react"; type Props = { - workspaceId: string -} + workspaceId: string; +}; export const InvoicesList = ({ workspaceId }: Props) => { - const { t } = useTranslate() - const { showToast } = useToast() + const { t } = useTranslate(); + const { showToast } = useToast(); const { data, status } = trpc.billing.listInvoices.useQuery( { workspaceId, }, { onError: (error) => { - showToast({ description: error.message }) + showToast({ description: error.message }); }, - } - ) + }, + ); return ( - {t('billing.invoices.heading')} - {data?.invoices.length === 0 && status !== 'loading' ? ( - {t('billing.invoices.empty')} + {t("billing.invoices.heading")} + {data?.invoices.length === 0 && status !== "loading" ? ( + {t("billing.invoices.empty")} ) : ( @@ -50,8 +50,8 @@ export const InvoicesList = ({ workspaceId }: Props) => { - - + + @@ -65,7 +65,7 @@ export const InvoicesList = ({ workspaceId }: Props) => { ))} - {status === 'loading' && + {status === "loading" && Array.from({ length: 3 }).map((_, idx) => (
#{t('billing.invoices.paidAt')}{t('billing.invoices.subtotal')}{t("billing.invoices.paidAt")}{t("billing.invoices.subtotal")}
{invoice.date ? new Date(invoice.date * 1000).toDateString() - : ''} + : ""} {getFormattedPrice(invoice.amount, invoice.currency)} @@ -77,13 +77,13 @@ export const InvoicesList = ({ workspaceId }: Props) => { variant="outline" href={invoice.url} target="_blank" - aria-label={'Download invoice'} + aria-label={"Download invoice"} /> )}
@@ -102,14 +102,14 @@ export const InvoicesList = ({ workspaceId }: Props) => { )} - ) -} + ); +}; const getFormattedPrice = (amount: number, currency: string) => { - const formatter = new Intl.NumberFormat('en-US', { - style: 'currency', + const formatter = new Intl.NumberFormat("en-US", { + style: "currency", currency, - }) + }); - return formatter.format(amount / 100) -} + return formatter.format(amount / 100); +}; diff --git a/apps/builder/src/features/billing/components/LockTag.tsx b/apps/builder/src/features/billing/components/LockTag.tsx index 8b6a1711da..39c858bb9f 100644 --- a/apps/builder/src/features/billing/components/LockTag.tsx +++ b/apps/builder/src/features/billing/components/LockTag.tsx @@ -1,14 +1,14 @@ -import { Tag, TagProps } from '@chakra-ui/react' -import { LockedIcon } from '@/components/icons' -import { Plan } from '@typebot.io/prisma' -import { planColorSchemes } from './PlanTag' +import { LockedIcon } from "@/components/icons"; +import { Tag, type TagProps } from "@chakra-ui/react"; +import type { Plan } from "@typebot.io/prisma/enum"; +import { planColorSchemes } from "./PlanTag"; export const LockTag = ({ plan, ...props }: { plan?: Plan } & TagProps) => ( -) +); diff --git a/apps/builder/src/features/billing/components/PlanTag.tsx b/apps/builder/src/features/billing/components/PlanTag.tsx index c4e4f171da..fbcd4fab37 100644 --- a/apps/builder/src/features/billing/components/PlanTag.tsx +++ b/apps/builder/src/features/billing/components/PlanTag.tsx @@ -1,15 +1,15 @@ -import { Tag, TagProps, ThemeTypings } from '@chakra-ui/react' -import { Plan } from '@typebot.io/prisma' +import { Tag, type TagProps, type ThemeTypings } from "@chakra-ui/react"; +import { Plan } from "@typebot.io/prisma/enum"; -export const planColorSchemes: Record = { - [Plan.LIFETIME]: 'purple', - [Plan.PRO]: 'blue', - [Plan.OFFERED]: 'orange', - [Plan.STARTER]: 'orange', - [Plan.FREE]: 'gray', - [Plan.CUSTOM]: 'yellow', - [Plan.UNLIMITED]: 'yellow', -} +export const planColorSchemes: Record = { + [Plan.LIFETIME]: "purple", + [Plan.PRO]: "blue", + [Plan.OFFERED]: "orange", + [Plan.STARTER]: "orange", + [Plan.FREE]: "gray", + [Plan.CUSTOM]: "yellow", + [Plan.UNLIMITED]: "yellow", +}; export const PlanTag = ({ plan, @@ -25,7 +25,7 @@ export const PlanTag = ({ > Lifetime - ) + ); } case Plan.PRO: { return ( @@ -36,7 +36,7 @@ export const PlanTag = ({ > Pro - ) + ); } case Plan.OFFERED: case Plan.STARTER: { @@ -48,7 +48,7 @@ export const PlanTag = ({ > Starter - ) + ); } case Plan.FREE: { return ( @@ -59,7 +59,7 @@ export const PlanTag = ({ > Free - ) + ); } case Plan.CUSTOM: { return ( @@ -70,7 +70,7 @@ export const PlanTag = ({ > Custom - ) + ); } case Plan.UNLIMITED: { return ( @@ -81,7 +81,7 @@ export const PlanTag = ({ > Unlimited - ) + ); } } -} +}; diff --git a/apps/builder/src/features/billing/components/PreCheckoutModal.tsx b/apps/builder/src/features/billing/components/PreCheckoutModal.tsx index 5b4c550c0b..7b974ad728 100644 --- a/apps/builder/src/features/billing/components/PreCheckoutModal.tsx +++ b/apps/builder/src/features/billing/components/PreCheckoutModal.tsx @@ -1,8 +1,8 @@ -import { TextInput } from '@/components/inputs' -import { Select } from '@/components/inputs/Select' -import { useParentModal } from '@/features/graph/providers/ParentModalProvider' -import { useToast } from '@/hooks/useToast' -import { trpc } from '@/lib/trpc' +import { TextInput } from "@/components/inputs"; +import { Select } from "@/components/inputs/Select"; +import { useParentModal } from "@/features/graph/providers/ParentModalProvider"; +import { useToast } from "@/hooks/useToast"; +import { trpc } from "@/lib/trpc"; import { Button, FormControl, @@ -13,25 +13,26 @@ import { ModalContent, ModalOverlay, Stack, -} from '@chakra-ui/react' -import { useRouter } from 'next/router' -import React, { FormEvent, useState } from 'react' -import { isDefined } from '@typebot.io/lib' -import { useTranslate } from '@tolgee/react' -import { taxIdTypes } from '@typebot.io/billing/taxIdTypes' +} from "@chakra-ui/react"; +import { useTranslate } from "@tolgee/react"; +import { taxIdTypes } from "@typebot.io/billing/taxIdTypes"; +import { isDefined } from "@typebot.io/lib/utils"; +import { useRouter } from "next/router"; +import type { FormEvent } from "react"; +import React, { useState } from "react"; export type PreCheckoutModalProps = { selectedSubscription: | { - plan: 'STARTER' | 'PRO' - workspaceId: string - currency: 'eur' | 'usd' + plan: "STARTER" | "PRO"; + workspaceId: string; + currency: "eur" | "usd"; } - | undefined - existingCompany?: string - existingEmail?: string - onClose: () => void -} + | undefined; + existingCompany?: string; + existingEmail?: string; + onClose: () => void; +}; const vatCodeLabels = taxIdTypes.map((taxIdType) => ({ label: `${taxIdType.emoji} ${taxIdType.name} (${taxIdType.code})`, @@ -39,7 +40,7 @@ const vatCodeLabels = taxIdTypes.map((taxIdType) => ({ extras: { placeholder: taxIdType.placeholder, }, -})) +})); export const PreCheckoutModal = ({ selectedSubscription, @@ -47,44 +48,44 @@ export const PreCheckoutModal = ({ existingEmail, onClose, }: PreCheckoutModalProps) => { - const { t } = useTranslate() - const { ref } = useParentModal() - const vatValueInputRef = React.useRef(null) - const router = useRouter() - const { showToast } = useToast() + const { t } = useTranslate(); + const { ref } = useParentModal(); + const vatValueInputRef = React.useRef(null); + const router = useRouter(); + const { showToast } = useToast(); const { mutate: createCheckoutSession, isLoading: isCreatingCheckout } = trpc.billing.createCheckoutSession.useMutation({ onError: (error) => { showToast({ description: error.message, - }) + }); }, onSuccess: ({ checkoutUrl }) => { - router.push(checkoutUrl) + router.push(checkoutUrl); }, - }) + }); const [customer, setCustomer] = useState({ - company: existingCompany ?? '', - email: existingEmail ?? '', + company: existingCompany ?? "", + email: existingEmail ?? "", vat: { type: undefined as string | undefined, - value: '', + value: "", }, - }) - const [vatValuePlaceholder, setVatValuePlaceholder] = useState('') + }); + const [vatValuePlaceholder, setVatValuePlaceholder] = useState(""); const updateCustomerCompany = (company: string) => { - setCustomer((customer) => ({ ...customer, company })) - } + setCustomer((customer) => ({ ...customer, company })); + }; const updateCustomerEmail = (email: string) => { - setCustomer((customer) => ({ ...customer, email })) - } + setCustomer((customer) => ({ ...customer, email })); + }; const updateVatType = ( type: string | undefined, - vatCode?: (typeof vatCodeLabels)[number] + vatCode?: (typeof vatCodeLabels)[number], ) => { setCustomer((customer) => ({ ...customer, @@ -92,10 +93,10 @@ export const PreCheckoutModal = ({ ...customer.vat, type, }, - })) - setVatValuePlaceholder(vatCode?.extras?.placeholder ?? '') - vatValueInputRef.current?.focus() - } + })); + setVatValuePlaceholder(vatCode?.extras?.placeholder ?? ""); + vatValueInputRef.current?.focus(); + }; const updateVatValue = (value: string) => { setCustomer((customer) => ({ @@ -104,13 +105,13 @@ export const PreCheckoutModal = ({ ...customer.vat, value, }, - })) - } + })); + }; const goToCheckout = (e: FormEvent) => { - e.preventDefault() - if (!selectedSubscription) return - const { email, company, vat } = customer + e.preventDefault(); + if (!selectedSubscription) return; + const { email, company, vat } = customer; createCheckoutSession({ ...selectedSubscription, email, @@ -120,8 +121,8 @@ export const PreCheckoutModal = ({ vat.value && vat.type ? { type: vat.type, value: vat.value } : undefined, - }) - } + }); + }; return ( @@ -131,7 +132,7 @@ export const PreCheckoutModal = ({ - {t('billing.preCheckoutModal.taxId.label')} + {t("billing.preCheckoutModal.taxId.label")} { { - {t('blocks.inputs.payment.settings.additionalInformation.label')} + {t("blocks.inputs.payment.settings.additionalInformation.label")} { /> { /> { onNewCredentials={updateCredentials} /> - ) -} + ); +}; diff --git a/apps/builder/src/features/blocks/inputs/payment/components/StripeConfigModal.tsx b/apps/builder/src/features/blocks/inputs/payment/components/StripeConfigModal.tsx index b283f8ea65..f6779e0d88 100644 --- a/apps/builder/src/features/blocks/inputs/payment/components/StripeConfigModal.tsx +++ b/apps/builder/src/features/blocks/inputs/payment/components/StripeConfigModal.tsx @@ -1,35 +1,36 @@ +import { MoreInfoTooltip } from "@/components/MoreInfoTooltip"; +import { TextLink } from "@/components/TextLink"; +import { TextInput } from "@/components/inputs"; +import { useUser } from "@/features/account/hooks/useUser"; +import { useWorkspace } from "@/features/workspace/WorkspaceProvider"; +import { useToast } from "@/hooks/useToast"; +import { trpc } from "@/lib/trpc"; import { - Modal, - ModalOverlay, - ModalContent, - ModalHeader, - ModalCloseButton, - ModalBody, - ModalFooter, Button, FormControl, FormLabel, + HStack, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, Stack, Text, - HStack, -} from '@chakra-ui/react' -import React, { useState } from 'react' -import { useWorkspace } from '@/features/workspace/WorkspaceProvider' -import { useToast } from '@/hooks/useToast' -import { TextInput } from '@/components/inputs' -import { MoreInfoTooltip } from '@/components/MoreInfoTooltip' -import { TextLink } from '@/components/TextLink' -import { StripeCredentials } from '@typebot.io/schemas' -import { trpc } from '@/lib/trpc' -import { isNotEmpty } from '@typebot.io/lib' -import { useUser } from '@/features/account/hooks/useUser' -import { useTranslate } from '@tolgee/react' +} from "@chakra-ui/react"; +import { useTranslate } from "@tolgee/react"; +import type { StripeCredentials } from "@typebot.io/blocks-inputs/payment/schema"; +import { isNotEmpty } from "@typebot.io/lib/utils"; +import type React from "react"; +import { useState } from "react"; type Props = { - isOpen: boolean - onClose: () => void - onNewCredentials: (id: string) => void -} + isOpen: boolean; + onClose: () => void; + onNewCredentials: (id: string) => void; +}; export const StripeConfigModal = ({ isOpen, @@ -44,79 +45,79 @@ export const StripeConfigModal = ({ onClose={onClose} /> - ) -} + ); +}; export const StripeCreateModalContent = ({ onNewCredentials, onClose, -}: Pick) => { - const { t } = useTranslate() - const { user } = useUser() - const { workspace } = useWorkspace() - const [isCreating, setIsCreating] = useState(false) - const { showToast } = useToast() +}: Pick) => { + const { t } = useTranslate(); + const { user } = useUser(); + const { workspace } = useWorkspace(); + const [isCreating, setIsCreating] = useState(false); + const { showToast } = useToast(); const [stripeConfig, setStripeConfig] = useState< - StripeCredentials['data'] & { name: string } + StripeCredentials["data"] & { name: string } >({ - name: '', - live: { publicKey: '', secretKey: '' }, - test: { publicKey: '', secretKey: '' }, - }) + name: "", + live: { publicKey: "", secretKey: "" }, + test: { publicKey: "", secretKey: "" }, + }); const { credentials: { listCredentials: { refetch: refetchCredentials }, }, - } = trpc.useContext() + } = trpc.useContext(); const { mutate } = trpc.credentials.createCredentials.useMutation({ onMutate: () => setIsCreating(true), onSettled: () => setIsCreating(false), onError: (err) => { showToast({ description: err.message, - status: 'error', - }) + status: "error", + }); }, onSuccess: (data) => { - refetchCredentials() - onNewCredentials(data.credentialsId) - onClose() + refetchCredentials(); + onNewCredentials(data.credentialsId); + onClose(); }, - }) + }); const handleNameChange = (name: string) => setStripeConfig({ ...stripeConfig, name, - }) + }); const handlePublicKeyChange = (publicKey: string) => setStripeConfig({ ...stripeConfig, live: { ...stripeConfig.live, publicKey }, - }) + }); const handleSecretKeyChange = (secretKey: string) => setStripeConfig({ ...stripeConfig, live: { ...stripeConfig.live, secretKey }, - }) + }); const handleTestPublicKeyChange = (publicKey: string) => setStripeConfig({ ...stripeConfig, test: { ...stripeConfig.test, publicKey }, - }) + }); const handleTestSecretKeyChange = (secretKey: string) => setStripeConfig({ ...stripeConfig, test: { ...stripeConfig.test, secretKey }, - }) + }); const createCredentials = async (e: React.FormEvent) => { - e.preventDefault() - if (!user?.email || !workspace?.id) return + e.preventDefault(); + if (!user?.email || !workspace?.id) return; mutate({ credentials: { data: { @@ -131,16 +132,16 @@ export const StripeCreateModalContent = ({ }, }, name: stripeConfig.name, - type: 'stripe', + type: "stripe", workspaceId: workspace.id, }, - }) - } + }); + }; return ( - {t('blocks.inputs.payment.settings.stripeConfig.title.label')} + {t("blocks.inputs.payment.settings.stripeConfig.title.label")}
@@ -149,7 +150,7 @@ export const StripeCreateModalContent = ({ {t( - 'blocks.inputs.payment.settings.stripeConfig.testKeys.label' - )}{' '} + "blocks.inputs.payment.settings.stripeConfig.testKeys.label", + )}{" "} {t( - 'blocks.inputs.payment.settings.stripeConfig.testKeys.infoText.label' + "blocks.inputs.payment.settings.stripeConfig.testKeys.infoText.label", )} @@ -186,7 +187,7 @@ export const StripeCreateModalContent = ({ {t( - 'blocks.inputs.payment.settings.stripeConfig.liveKeys.label' + "blocks.inputs.payment.settings.stripeConfig.liveKeys.label", )} @@ -211,10 +212,10 @@ export const StripeCreateModalContent = ({ - ({t('blocks.inputs.payment.settings.stripeConfig.findKeys.label')}{' '} + ({t("blocks.inputs.payment.settings.stripeConfig.findKeys.label")}{" "} {t( - 'blocks.inputs.payment.settings.stripeConfig.findKeys.here.label' + "blocks.inputs.payment.settings.stripeConfig.findKeys.here.label", )} ) @@ -227,16 +228,16 @@ export const StripeCreateModalContent = ({ type="submit" colorScheme="blue" isDisabled={ - stripeConfig.live.publicKey === '' || - stripeConfig.name === '' || - stripeConfig.live.secretKey === '' + stripeConfig.live.publicKey === "" || + stripeConfig.name === "" || + stripeConfig.live.secretKey === "" } isLoading={isCreating} > - {t('connect')} + {t("connect")}
- ) -} + ); +}; diff --git a/apps/builder/src/features/blocks/inputs/payment/components/UpdateStripeCredentialsModalContent.tsx b/apps/builder/src/features/blocks/inputs/payment/components/UpdateStripeCredentialsModalContent.tsx index 425d065f95..8e41de903d 100644 --- a/apps/builder/src/features/blocks/inputs/payment/components/UpdateStripeCredentialsModalContent.tsx +++ b/apps/builder/src/features/blocks/inputs/payment/components/UpdateStripeCredentialsModalContent.tsx @@ -1,43 +1,43 @@ -import { TextInput } from '@/components/inputs' -import { MoreInfoTooltip } from '@/components/MoreInfoTooltip' -import { TextLink } from '@/components/TextLink' -import { useUser } from '@/features/account/hooks/useUser' -import { useWorkspace } from '@/features/workspace/WorkspaceProvider' -import { trpc } from '@/lib/trpc' +import { MoreInfoTooltip } from "@/components/MoreInfoTooltip"; +import { TextLink } from "@/components/TextLink"; +import { TextInput } from "@/components/inputs"; +import { useUser } from "@/features/account/hooks/useUser"; +import { useWorkspace } from "@/features/workspace/WorkspaceProvider"; +import { trpc } from "@/lib/trpc"; import { - Text, - ModalContent, - ModalHeader, - ModalCloseButton, - ModalBody, - Stack, + Button, + FormControl, FormLabel, HStack, - FormControl, + ModalBody, + ModalCloseButton, + ModalContent, ModalFooter, - Button, -} from '@chakra-ui/react' -import { useTranslate } from '@tolgee/react' -import { isNotEmpty } from '@typebot.io/lib' -import { StripeCredentials } from '@typebot.io/schemas' -import { useEffect, useState } from 'react' + ModalHeader, + Stack, + Text, +} from "@chakra-ui/react"; +import { useTranslate } from "@tolgee/react"; +import type { StripeCredentials } from "@typebot.io/blocks-inputs/payment/schema"; +import { isNotEmpty } from "@typebot.io/lib/utils"; +import { useEffect, useState } from "react"; type Props = { - credentialsId: string - onUpdate: () => void -} + credentialsId: string; + onUpdate: () => void; +}; export const UpdateStripeCredentialsModalContent = ({ credentialsId, onUpdate, }: Props) => { - const { t } = useTranslate() - const { user } = useUser() - const { workspace } = useWorkspace() - const [isCreating, setIsCreating] = useState(false) + const { t } = useTranslate(); + const { user } = useUser(); + const { workspace } = useWorkspace(); + const [isCreating, setIsCreating] = useState(false); const [stripeConfig, setStripeConfig] = useState< - StripeCredentials['data'] & { name: string } - >() + StripeCredentials["data"] & { name: string } + >(); const { data: existingCredentials } = trpc.credentials.getCredentials.useQuery( @@ -47,64 +47,64 @@ export const UpdateStripeCredentialsModalContent = ({ }, { enabled: !!workspace?.id, - } - ) + }, + ); useEffect(() => { - if (!existingCredentials || stripeConfig) return + if (!existingCredentials || stripeConfig) return; setStripeConfig({ name: existingCredentials.name, live: existingCredentials.data.live, test: existingCredentials.data.test, - }) - }, [existingCredentials, stripeConfig]) + }); + }, [existingCredentials, stripeConfig]); const { mutate } = trpc.credentials.updateCredentials.useMutation({ onMutate: () => setIsCreating(true), onSettled: () => setIsCreating(false), onSuccess: () => { - onUpdate() + onUpdate(); }, - }) + }); const handleNameChange = (name: string) => stripeConfig && setStripeConfig({ ...stripeConfig, name, - }) + }); const handlePublicKeyChange = (publicKey: string) => stripeConfig && setStripeConfig({ ...stripeConfig, live: { ...stripeConfig.live, publicKey }, - }) + }); const handleSecretKeyChange = (secretKey: string) => stripeConfig && setStripeConfig({ ...stripeConfig, live: { ...stripeConfig.live, secretKey }, - }) + }); const handleTestPublicKeyChange = (publicKey: string) => stripeConfig && setStripeConfig({ ...stripeConfig, test: { ...stripeConfig.test, publicKey }, - }) + }); const handleTestSecretKeyChange = (secretKey: string) => stripeConfig && setStripeConfig({ ...stripeConfig, test: { ...stripeConfig.test, secretKey }, - }) + }); const updateCreds = async (e: React.FormEvent) => { - e.preventDefault() - if (!user?.email || !workspace?.id || !stripeConfig) return + e.preventDefault(); + if (!user?.email || !workspace?.id || !stripeConfig) return; mutate({ credentialsId, credentials: { @@ -120,16 +120,16 @@ export const UpdateStripeCredentialsModalContent = ({ }, }, name: stripeConfig.name, - type: 'stripe', + type: "stripe", workspaceId: workspace.id, }, - }) - } + }); + }; return ( - {t('blocks.inputs.payment.settings.stripeConfig.title.label')} + {t("blocks.inputs.payment.settings.stripeConfig.title.label")}
@@ -138,7 +138,7 @@ export const UpdateStripeCredentialsModalContent = ({ {t( - 'blocks.inputs.payment.settings.stripeConfig.testKeys.label' - )}{' '} + "blocks.inputs.payment.settings.stripeConfig.testKeys.label", + )}{" "} {t( - 'blocks.inputs.payment.settings.stripeConfig.testKeys.infoText.label' + "blocks.inputs.payment.settings.stripeConfig.testKeys.infoText.label", )} @@ -178,7 +178,7 @@ export const UpdateStripeCredentialsModalContent = ({ {t( - 'blocks.inputs.payment.settings.stripeConfig.liveKeys.label' + "blocks.inputs.payment.settings.stripeConfig.liveKeys.label", )} @@ -205,10 +205,10 @@ export const UpdateStripeCredentialsModalContent = ({ - ({t('blocks.inputs.payment.settings.stripeConfig.findKeys.label')}{' '} + ({t("blocks.inputs.payment.settings.stripeConfig.findKeys.label")}{" "} {t( - 'blocks.inputs.payment.settings.stripeConfig.findKeys.here.label' + "blocks.inputs.payment.settings.stripeConfig.findKeys.here.label", )} ) @@ -221,16 +221,16 @@ export const UpdateStripeCredentialsModalContent = ({ type="submit" colorScheme="blue" isDisabled={ - stripeConfig?.live.publicKey === '' || - stripeConfig?.name === '' || - stripeConfig?.live.secretKey === '' + stripeConfig?.live.publicKey === "" || + stripeConfig?.name === "" || + stripeConfig?.live.secretKey === "" } isLoading={isCreating} > - {t('connect')} + {t("connect")}
- ) -} + ); +}; diff --git a/apps/builder/src/features/blocks/inputs/payment/currencies.tsx b/apps/builder/src/features/blocks/inputs/payment/currencies.tsx index c185183d79..001c426502 100644 --- a/apps/builder/src/features/blocks/inputs/payment/currencies.tsx +++ b/apps/builder/src/features/blocks/inputs/payment/currencies.tsx @@ -3,543 +3,543 @@ export const currencies = [ { - code: 'AED', - description: 'United Arab Emirates Dirham', + code: "AED", + description: "United Arab Emirates Dirham", }, { - code: 'AFN', - description: 'Afghan Afghani**', + code: "AFN", + description: "Afghan Afghani**", }, { - code: 'ALL', - description: 'Albanian Lek', + code: "ALL", + description: "Albanian Lek", }, { - code: 'AMD', - description: 'Armenian Dram', + code: "AMD", + description: "Armenian Dram", }, { - code: 'ANG', - description: 'Netherlands Antillean Gulden', + code: "ANG", + description: "Netherlands Antillean Gulden", }, { - code: 'AOA', - description: 'Angolan Kwanza**', + code: "AOA", + description: "Angolan Kwanza**", }, { - code: 'ARS', - description: 'Argentine Peso**', + code: "ARS", + description: "Argentine Peso**", }, { - code: 'AUD', - description: 'Australian Dollar', + code: "AUD", + description: "Australian Dollar", }, { - code: 'AWG', - description: 'Aruban Florin', + code: "AWG", + description: "Aruban Florin", }, { - code: 'AZN', - description: 'Azerbaijani Manat', + code: "AZN", + description: "Azerbaijani Manat", }, { - code: 'BAM', - description: 'Bosnia & Herzegovina Convertible Mark', + code: "BAM", + description: "Bosnia & Herzegovina Convertible Mark", }, { - code: 'BBD', - description: 'Barbadian Dollar', + code: "BBD", + description: "Barbadian Dollar", }, { - code: 'BDT', - description: 'Bangladeshi Taka', + code: "BDT", + description: "Bangladeshi Taka", }, { - code: 'BGN', - description: 'Bulgarian Lev', + code: "BGN", + description: "Bulgarian Lev", }, { - code: 'BIF', - description: 'Burundian Franc', + code: "BIF", + description: "Burundian Franc", }, { - code: 'BMD', - description: 'Bermudian Dollar', + code: "BMD", + description: "Bermudian Dollar", }, { - code: 'BND', - description: 'Brunei Dollar', + code: "BND", + description: "Brunei Dollar", }, { - code: 'BOB', - description: 'Bolivian Boliviano**', + code: "BOB", + description: "Bolivian Boliviano**", }, { - code: 'BRL', - description: 'Brazilian Real**', + code: "BRL", + description: "Brazilian Real**", }, { - code: 'BSD', - description: 'Bahamian Dollar', + code: "BSD", + description: "Bahamian Dollar", }, { - code: 'BWP', - description: 'Botswana Pula', + code: "BWP", + description: "Botswana Pula", }, { - code: 'BZD', - description: 'Belize Dollar', + code: "BZD", + description: "Belize Dollar", }, { - code: 'CAD', - description: 'Canadian Dollar', + code: "CAD", + description: "Canadian Dollar", }, { - code: 'CDF', - description: 'Congolese Franc', + code: "CDF", + description: "Congolese Franc", }, { - code: 'CHF', - description: 'Swiss Franc', + code: "CHF", + description: "Swiss Franc", }, { - code: 'CLP', - description: 'Chilean Peso**', + code: "CLP", + description: "Chilean Peso**", }, { - code: 'CNY', - description: 'Chinese Renminbi Yuan', + code: "CNY", + description: "Chinese Renminbi Yuan", }, { - code: 'COP', - description: 'Colombian Peso**', + code: "COP", + description: "Colombian Peso**", }, { - code: 'CRC', - description: 'Costa Rican Colón**', + code: "CRC", + description: "Costa Rican Colón**", }, { - code: 'CVE', - description: 'Cape Verdean Escudo**', + code: "CVE", + description: "Cape Verdean Escudo**", }, { - code: 'CZK', - description: 'Czech Koruna**', + code: "CZK", + description: "Czech Koruna**", }, { - code: 'DJF', - description: 'Djiboutian Franc**', + code: "DJF", + description: "Djiboutian Franc**", }, { - code: 'DKK', - description: 'Danish Krone', + code: "DKK", + description: "Danish Krone", }, { - code: 'DOP', - description: 'Dominican Peso', + code: "DOP", + description: "Dominican Peso", }, { - code: 'DZD', - description: 'Algerian Dinar', + code: "DZD", + description: "Algerian Dinar", }, { - code: 'EGP', - description: 'Egyptian Pound', + code: "EGP", + description: "Egyptian Pound", }, { - code: 'ETB', - description: 'Ethiopian Birr', + code: "ETB", + description: "Ethiopian Birr", }, { - code: 'EUR', - description: 'Euro', + code: "EUR", + description: "Euro", }, { - code: 'FJD', - description: 'Fijian Dollar', + code: "FJD", + description: "Fijian Dollar", }, { - code: 'FKP', - description: 'Falkland Islands Pound**', + code: "FKP", + description: "Falkland Islands Pound**", }, { - code: 'GBP', - description: 'British Pound', + code: "GBP", + description: "British Pound", }, { - code: 'GEL', - description: 'Georgian Lari', + code: "GEL", + description: "Georgian Lari", }, { - code: 'GIP', - description: 'Gibraltar Pound', + code: "GIP", + description: "Gibraltar Pound", }, { - code: 'GMD', - description: 'Gambian Dalasi', + code: "GMD", + description: "Gambian Dalasi", }, { - code: 'GNF', - description: 'Guinean Franc**', + code: "GNF", + description: "Guinean Franc**", }, { - code: 'GTQ', - description: 'Guatemalan Quetzal**', + code: "GTQ", + description: "Guatemalan Quetzal**", }, { - code: 'GYD', - description: 'Guyanese Dollar', + code: "GYD", + description: "Guyanese Dollar", }, { - code: 'HKD', - description: 'Hong Kong Dollar', + code: "HKD", + description: "Hong Kong Dollar", }, { - code: 'HNL', - description: 'Honduran Lempira**', + code: "HNL", + description: "Honduran Lempira**", }, { - code: 'HRK', - description: 'Croatian Kuna', + code: "HRK", + description: "Croatian Kuna", }, { - code: 'HTG', - description: 'Haitian Gourde', + code: "HTG", + description: "Haitian Gourde", }, { - code: 'HUF', - description: 'Hungarian Forint**', + code: "HUF", + description: "Hungarian Forint**", }, { - code: 'IDR', - description: 'Indonesian Rupiah', + code: "IDR", + description: "Indonesian Rupiah", }, { - code: 'ILS', - description: 'Israeli New Sheqel', + code: "ILS", + description: "Israeli New Sheqel", }, { - code: 'INR', - description: 'Indian Rupee**', + code: "INR", + description: "Indian Rupee**", }, { - code: 'ISK', - description: 'Icelandic Króna', + code: "ISK", + description: "Icelandic Króna", }, { - code: 'JMD', - description: 'Jamaican Dollar', + code: "JMD", + description: "Jamaican Dollar", }, { - code: 'JPY', - description: 'Japanese Yen', + code: "JPY", + description: "Japanese Yen", }, { - code: 'KES', - description: 'Kenyan Shilling', + code: "KES", + description: "Kenyan Shilling", }, { - code: 'KGS', - description: 'Kyrgyzstani Som', + code: "KGS", + description: "Kyrgyzstani Som", }, { - code: 'KHR', - description: 'Cambodian Riel', + code: "KHR", + description: "Cambodian Riel", }, { - code: 'KMF', - description: 'Comorian Franc', + code: "KMF", + description: "Comorian Franc", }, { - code: 'KRW', - description: 'South Korean Won', + code: "KRW", + description: "South Korean Won", }, { - code: 'KYD', - description: 'Cayman Islands Dollar', + code: "KYD", + description: "Cayman Islands Dollar", }, { - code: 'KZT', - description: 'Kazakhstani Tenge', + code: "KZT", + description: "Kazakhstani Tenge", }, { - code: 'LAK', - description: 'Lao Kip**', + code: "LAK", + description: "Lao Kip**", }, { - code: 'LBP', - description: 'Lebanese Pound', + code: "LBP", + description: "Lebanese Pound", }, { - code: 'LKR', - description: 'Sri Lankan Rupee', + code: "LKR", + description: "Sri Lankan Rupee", }, { - code: 'LRD', - description: 'Liberian Dollar', + code: "LRD", + description: "Liberian Dollar", }, { - code: 'LSL', - description: 'Lesotho Loti', + code: "LSL", + description: "Lesotho Loti", }, { - code: 'MAD', - description: 'Moroccan Dirham', + code: "MAD", + description: "Moroccan Dirham", }, { - code: 'MDL', - description: 'Moldovan Leu', + code: "MDL", + description: "Moldovan Leu", }, { - code: 'MGA', - description: 'Malagasy Ariary', + code: "MGA", + description: "Malagasy Ariary", }, { - code: 'MKD', - description: 'Macedonian Denar', + code: "MKD", + description: "Macedonian Denar", }, { - code: 'MNT', - description: 'Mongolian Tögrög', + code: "MNT", + description: "Mongolian Tögrög", }, { - code: 'MOP', - description: 'Macanese Pataca', + code: "MOP", + description: "Macanese Pataca", }, { - code: 'MRO', - description: 'Mauritanian Ouguiya', + code: "MRO", + description: "Mauritanian Ouguiya", }, { - code: 'MUR', - description: 'Mauritian Rupee**', + code: "MUR", + description: "Mauritian Rupee**", }, { - code: 'MVR', - description: 'Maldivian Rufiyaa', + code: "MVR", + description: "Maldivian Rufiyaa", }, { - code: 'MWK', - description: 'Malawian Kwacha', + code: "MWK", + description: "Malawian Kwacha", }, { - code: 'MXN', - description: 'Mexican Peso**', + code: "MXN", + description: "Mexican Peso**", }, { - code: 'MYR', - description: 'Malaysian Ringgit', + code: "MYR", + description: "Malaysian Ringgit", }, { - code: 'MZN', - description: 'Mozambican Metical', + code: "MZN", + description: "Mozambican Metical", }, { - code: 'NAD', - description: 'Namibian Dollar', + code: "NAD", + description: "Namibian Dollar", }, { - code: 'NGN', - description: 'Nigerian Naira', + code: "NGN", + description: "Nigerian Naira", }, { - code: 'NIO', - description: 'Nicaraguan Córdoba**', + code: "NIO", + description: "Nicaraguan Córdoba**", }, { - code: 'NOK', - description: 'Norwegian Krone', + code: "NOK", + description: "Norwegian Krone", }, { - code: 'NPR', - description: 'Nepalese Rupee', + code: "NPR", + description: "Nepalese Rupee", }, { - code: 'NZD', - description: 'New Zealand Dollar', + code: "NZD", + description: "New Zealand Dollar", }, { - code: 'PAB', - description: 'Panamanian Balboa**', + code: "PAB", + description: "Panamanian Balboa**", }, { - code: 'PEN', - description: 'Peruvian Nuevo Sol**', + code: "PEN", + description: "Peruvian Nuevo Sol**", }, { - code: 'PGK', - description: 'Papua New Guinean Kina', + code: "PGK", + description: "Papua New Guinean Kina", }, { - code: 'PHP', - description: 'Philippine Peso', + code: "PHP", + description: "Philippine Peso", }, { - code: 'PKR', - description: 'Pakistani Rupee', + code: "PKR", + description: "Pakistani Rupee", }, { - code: 'PLN', - description: 'Polish Złoty', + code: "PLN", + description: "Polish Złoty", }, { - code: 'PYG', - description: 'Paraguayan Guaraní**', + code: "PYG", + description: "Paraguayan Guaraní**", }, { - code: 'QAR', - description: 'Qatari Riyal', + code: "QAR", + description: "Qatari Riyal", }, { - code: 'RON', - description: 'Romanian Leu', + code: "RON", + description: "Romanian Leu", }, { - code: 'RSD', - description: 'Serbian Dinar', + code: "RSD", + description: "Serbian Dinar", }, { - code: 'RUB', - description: 'Russian Ruble', + code: "RUB", + description: "Russian Ruble", }, { - code: 'RWF', - description: 'Rwandan Franc', + code: "RWF", + description: "Rwandan Franc", }, { - code: 'SAR', - description: 'Saudi Riyal', + code: "SAR", + description: "Saudi Riyal", }, { - code: 'SBD', - description: 'Solomon Islands Dollar', + code: "SBD", + description: "Solomon Islands Dollar", }, { - code: 'SCR', - description: 'Seychellois Rupee', + code: "SCR", + description: "Seychellois Rupee", }, { - code: 'SEK', - description: 'Swedish Krona', + code: "SEK", + description: "Swedish Krona", }, { - code: 'SGD', - description: 'Singapore Dollar', + code: "SGD", + description: "Singapore Dollar", }, { - code: 'SHP', - description: 'Saint Helenian Pound**', + code: "SHP", + description: "Saint Helenian Pound**", }, { - code: 'SLL', - description: 'Sierra Leonean Leone', + code: "SLL", + description: "Sierra Leonean Leone", }, { - code: 'SOS', - description: 'Somali Shilling', + code: "SOS", + description: "Somali Shilling", }, { - code: 'SRD', - description: 'Surinamese Dollar**', + code: "SRD", + description: "Surinamese Dollar**", }, { - code: 'STD', - description: 'São Tomé and Príncipe Dobra', + code: "STD", + description: "São Tomé and Príncipe Dobra", }, { - code: 'SVC', - description: 'Salvadoran Colón**', + code: "SVC", + description: "Salvadoran Colón**", }, { - code: 'SZL', - description: 'Swazi Lilangeni', + code: "SZL", + description: "Swazi Lilangeni", }, { - code: 'THB', - description: 'Thai Baht', + code: "THB", + description: "Thai Baht", }, { - code: 'TJS', - description: 'Tajikistani Somoni', + code: "TJS", + description: "Tajikistani Somoni", }, { - code: 'TOP', - description: 'Tongan Paʻanga', + code: "TOP", + description: "Tongan Paʻanga", }, { - code: 'TRY', - description: 'Turkish Lira', + code: "TRY", + description: "Turkish Lira", }, { - code: 'TTD', - description: 'Trinidad and Tobago Dollar', + code: "TTD", + description: "Trinidad and Tobago Dollar", }, { - code: 'TWD', - description: 'New Taiwan Dollar', + code: "TWD", + description: "New Taiwan Dollar", }, { - code: 'TZS', - description: 'Tanzanian Shilling', + code: "TZS", + description: "Tanzanian Shilling", }, { - code: 'UAH', - description: 'Ukrainian Hryvnia', + code: "UAH", + description: "Ukrainian Hryvnia", }, { - code: 'UGX', - description: 'Ugandan Shilling', + code: "UGX", + description: "Ugandan Shilling", }, { - code: 'USD', - description: 'United States Dollar', + code: "USD", + description: "United States Dollar", }, { - code: 'UYU', - description: 'Uruguayan Peso**', + code: "UYU", + description: "Uruguayan Peso**", }, { - code: 'UZS', - description: 'Uzbekistani Som', + code: "UZS", + description: "Uzbekistani Som", }, { - code: 'VND', - description: 'Vietnamese Đồng', + code: "VND", + description: "Vietnamese Đồng", }, { - code: 'VUV', - description: 'Vanuatu Vatu', + code: "VUV", + description: "Vanuatu Vatu", }, { - code: 'WST', - description: 'Samoan Tala', + code: "WST", + description: "Samoan Tala", }, { - code: 'XAF', - description: 'Central African Cfa Franc', + code: "XAF", + description: "Central African Cfa Franc", }, { - code: 'XCD', - description: 'East Caribbean Dollar', + code: "XCD", + description: "East Caribbean Dollar", }, { - code: 'XOF', - description: 'West African Cfa Franc**', + code: "XOF", + description: "West African Cfa Franc**", }, { - code: 'XPF', - description: 'Cfp Franc**', + code: "XPF", + description: "Cfp Franc**", }, { - code: 'YER', - description: 'Yemeni Rial', + code: "YER", + description: "Yemeni Rial", }, { - code: 'ZAR', - description: 'South African Rand', + code: "ZAR", + description: "South African Rand", }, { - code: 'ZMW', - description: 'Zambian Kwacha', + code: "ZMW", + description: "Zambian Kwacha", }, -] +]; diff --git a/apps/builder/src/features/blocks/inputs/payment/payment.spec.ts b/apps/builder/src/features/blocks/inputs/payment/payment.spec.ts index 49a8cc2a7b..3e6c8ec083 100644 --- a/apps/builder/src/features/blocks/inputs/payment/payment.spec.ts +++ b/apps/builder/src/features/blocks/inputs/payment/payment.spec.ts @@ -1,14 +1,14 @@ -import test, { expect } from '@playwright/test' -import { createTypebots } from '@typebot.io/playwright/databaseActions' -import { parseDefaultGroupWithBlock } from '@typebot.io/playwright/databaseHelpers' -import { createId } from '@paralleldrive/cuid2' -import { stripePaymentForm } from '@/test/utils/selectorUtils' -import { env } from '@typebot.io/env' -import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants' +import { stripePaymentForm } from "@/test/utils/selectorUtils"; +import { createId } from "@paralleldrive/cuid2"; +import test, { expect } from "@playwright/test"; +import { InputBlockType } from "@typebot.io/blocks-inputs/constants"; +import { env } from "@typebot.io/env"; +import { createTypebots } from "@typebot.io/playwright/databaseActions"; +import { parseDefaultGroupWithBlock } from "@typebot.io/playwright/databaseHelpers"; -test.describe('Payment input block', () => { - test('Can configure Stripe account', async ({ page }) => { - const typebotId = createId() +test.describe("Payment input block", () => { + test("Can configure Stripe account", async ({ page }) => { + const typebotId = createId(); await createTypebots([ { id: typebotId, @@ -16,53 +16,53 @@ test.describe('Payment input block', () => { type: InputBlockType.PAYMENT, }), }, - ]) + ]); - await page.goto(`/typebots/${typebotId}/edit`) - await page.click('text=Configure...') - await page.getByRole('button', { name: 'Select Stripe account' }).click() - await page.getByRole('menuitem', { name: 'Connect new' }).click() - await page.fill('[placeholder="Typebot"]', 'My Stripe Account') - await page.fill('[placeholder="sk_test_..."]', env.STRIPE_SECRET_KEY ?? '') - await page.fill('[placeholder="sk_live_..."]', env.STRIPE_SECRET_KEY ?? '') + await page.goto(`/typebots/${typebotId}/edit`); + await page.click("text=Configure..."); + await page.getByRole("button", { name: "Select Stripe account" }).click(); + await page.getByRole("menuitem", { name: "Connect new" }).click(); + await page.fill('[placeholder="Typebot"]', "My Stripe Account"); + await page.fill('[placeholder="sk_test_..."]', env.STRIPE_SECRET_KEY ?? ""); + await page.fill('[placeholder="sk_live_..."]', env.STRIPE_SECRET_KEY ?? ""); await page.fill( '[placeholder="pk_test_..."]', - env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY ?? '' - ) + env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY ?? "", + ); await page.fill( '[placeholder="pk_live_..."]', - env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY ?? '' - ) - await expect(page.locator('button >> text="Connect"')).toBeEnabled() - await page.click('button >> text="Connect"') - await expect(page.locator('text="Secret test key:"')).toBeHidden() - await expect(page.locator('text="My Stripe Account"')).toBeVisible() - await page.fill('[placeholder="30.00"] >> nth=-1', '30.00') - await page.selectOption('select', 'EUR') - await page.click('text=Additional information') - await page.fill('[placeholder="John Smith"]', 'Baptiste') - await page.fill('[placeholder="john@gmail.com"]', 'test@typebot.io') - await expect(page.locator('text="Phone number:"')).toBeVisible() + env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY ?? "", + ); + await expect(page.locator('button >> text="Connect"')).toBeEnabled(); + await page.click('button >> text="Connect"'); + await expect(page.locator('text="Secret test key:"')).toBeHidden(); + await expect(page.locator('text="My Stripe Account"')).toBeVisible(); + await page.fill('[placeholder="30.00"] >> nth=-1', "30.00"); + await page.selectOption("select", "EUR"); + await page.click("text=Additional information"); + await page.fill('[placeholder="John Smith"]', "Baptiste"); + await page.fill('[placeholder="john@gmail.com"]', "test@typebot.io"); + await expect(page.locator('text="Phone number:"')).toBeVisible(); - await page.click('text=Test') + await page.click("text=Test"); await stripePaymentForm(page) .locator(`[placeholder="1234 1234 1234 1234"]`) - .fill('4000000000000002') + .fill("4000000000000002"); await stripePaymentForm(page) .locator(`[placeholder="MM / YY"]`) - .fill('12 / 25') - await stripePaymentForm(page).locator(`[placeholder="CVC"]`).fill('240') - await page.getByRole('button', { name: 'Pay 30,00 €' }).click() + .fill("12 / 25"); + await stripePaymentForm(page).locator(`[placeholder="CVC"]`).fill("240"); + await page.getByRole("button", { name: "Pay 30,00 €" }).click(); await expect( - page.locator(`text="Your card has been declined."`) - ).toBeVisible() + page.locator(`text="Your card has been declined."`), + ).toBeVisible(); await stripePaymentForm(page) .locator(`[placeholder="1234 1234 1234 1234"]`) - .fill('4242424242424242') - const zipInput = stripePaymentForm(page).getByPlaceholder('90210') - const isZipInputVisible = await zipInput.isVisible() - if (isZipInputVisible) await zipInput.fill('12345') - await page.getByRole('button', { name: 'Pay 30,00 €' }).click() - await expect(page.locator(`text="Success"`)).toBeVisible() - }) -}) + .fill("4242424242424242"); + const zipInput = stripePaymentForm(page).getByPlaceholder("90210"); + const isZipInputVisible = await zipInput.isVisible(); + if (isZipInputVisible) await zipInput.fill("12345"); + await page.getByRole("button", { name: "Pay 30,00 €" }).click(); + await expect(page.locator(`text="Success"`)).toBeVisible(); + }); +}); diff --git a/apps/builder/src/features/blocks/inputs/phone/components/CountryCodeSelect.tsx b/apps/builder/src/features/blocks/inputs/phone/components/CountryCodeSelect.tsx index 60a3695430..4c15a83d5a 100644 --- a/apps/builder/src/features/blocks/inputs/phone/components/CountryCodeSelect.tsx +++ b/apps/builder/src/features/blocks/inputs/phone/components/CountryCodeSelect.tsx @@ -1,21 +1,21 @@ -import { Select } from '@chakra-ui/react' -import { useTranslate } from '@tolgee/react' -import React, { ChangeEvent } from 'react' +import { Select } from "@chakra-ui/react"; +import { useTranslate } from "@tolgee/react"; +import React, { type ChangeEvent } from "react"; type Props = { - countryCode?: string - onSelect: (countryCode: string) => void -} + countryCode?: string; + onSelect: (countryCode: string) => void; +}; export const CountryCodeSelect = ({ countryCode, onSelect }: Props) => { - const { t } = useTranslate() + const { t } = useTranslate(); const handleOnChange = (e: ChangeEvent) => { - onSelect(e.target.value) - } + onSelect(e.target.value); + }; return ( - ) -} + ); +}; diff --git a/apps/builder/src/features/blocks/inputs/phone/components/PhoneInputIcon.tsx b/apps/builder/src/features/blocks/inputs/phone/components/PhoneInputIcon.tsx index 342a74605a..f82b28c5ec 100644 --- a/apps/builder/src/features/blocks/inputs/phone/components/PhoneInputIcon.tsx +++ b/apps/builder/src/features/blocks/inputs/phone/components/PhoneInputIcon.tsx @@ -1,7 +1,7 @@ -import { PhoneIcon } from '@/components/icons' -import { IconProps } from '@chakra-ui/react' -import React from 'react' +import { PhoneIcon } from "@/components/icons"; +import type { IconProps } from "@chakra-ui/react"; +import React from "react"; export const PhoneInputIcon = (props: IconProps) => ( -) +); diff --git a/apps/builder/src/features/blocks/inputs/phone/components/PhoneInputSettings.tsx b/apps/builder/src/features/blocks/inputs/phone/components/PhoneInputSettings.tsx index c45a297850..e50f5453c0 100644 --- a/apps/builder/src/features/blocks/inputs/phone/components/PhoneInputSettings.tsx +++ b/apps/builder/src/features/blocks/inputs/phone/components/PhoneInputSettings.tsx @@ -1,34 +1,38 @@ -import { TextInput } from '@/components/inputs' -import { VariableSearchInput } from '@/components/inputs/VariableSearchInput' -import { FormLabel, Stack } from '@chakra-ui/react' -import { PhoneNumberInputBlock, Variable } from '@typebot.io/schemas' -import React from 'react' -import { CountryCodeSelect } from './CountryCodeSelect' -import { useTranslate } from '@tolgee/react' -import { defaultPhoneInputOptions } from '@typebot.io/schemas/features/blocks/inputs/phone/constants' +import { TextInput } from "@/components/inputs"; +import { VariableSearchInput } from "@/components/inputs/VariableSearchInput"; +import { FormLabel, Stack } from "@chakra-ui/react"; +import { useTranslate } from "@tolgee/react"; +import { defaultPhoneInputOptions } from "@typebot.io/blocks-inputs/phone/constants"; +import type { PhoneNumberInputBlock } from "@typebot.io/blocks-inputs/phone/schema"; +import type { Variable } from "@typebot.io/variables/schemas"; +import React from "react"; +import { CountryCodeSelect } from "./CountryCodeSelect"; type Props = { - options: PhoneNumberInputBlock['options'] - onOptionsChange: (options: PhoneNumberInputBlock['options']) => void -} + options: PhoneNumberInputBlock["options"]; + onOptionsChange: (options: PhoneNumberInputBlock["options"]) => void; +}; export const PhoneInputSettings = ({ options, onOptionsChange }: Props) => { - const { t } = useTranslate() + const { t } = useTranslate(); const handlePlaceholderChange = (placeholder: string) => - onOptionsChange({ ...options, labels: { ...options?.labels, placeholder } }) + onOptionsChange({ + ...options, + labels: { ...options?.labels, placeholder }, + }); const handleButtonLabelChange = (button: string) => - onOptionsChange({ ...options, labels: { ...options?.labels, button } }) + onOptionsChange({ ...options, labels: { ...options?.labels, button } }); const handleVariableChange = (variable?: Variable) => - onOptionsChange({ ...options, variableId: variable?.id }) + onOptionsChange({ ...options, variableId: variable?.id }); const handleRetryMessageChange = (retryMessageContent: string) => - onOptionsChange({ ...options, retryMessageContent }) + onOptionsChange({ ...options, retryMessageContent }); const handleDefaultCountryChange = (defaultCountryCode: string) => - onOptionsChange({ ...options, defaultCountryCode }) + onOptionsChange({ ...options, defaultCountryCode }); return ( { onChange={handlePlaceholderChange} /> { /> - {t('blocks.inputs.phone.settings.defaultCountry.label')} + {t("blocks.inputs.phone.settings.defaultCountry.label")} { /> { /> - {t('blocks.inputs.settings.saveAnswer.label')} + {t("blocks.inputs.settings.saveAnswer.label")} { /> - ) -} + ); +}; diff --git a/apps/builder/src/features/blocks/inputs/phone/components/PhoneNodeContent.tsx b/apps/builder/src/features/blocks/inputs/phone/components/PhoneNodeContent.tsx index 178721e73f..9f39985d81 100644 --- a/apps/builder/src/features/blocks/inputs/phone/components/PhoneNodeContent.tsx +++ b/apps/builder/src/features/blocks/inputs/phone/components/PhoneNodeContent.tsx @@ -1,12 +1,12 @@ -import React from 'react' -import { Text } from '@chakra-ui/react' -import { WithVariableContent } from '@/features/graph/components/nodes/block/WithVariableContent' -import { PhoneNumberInputBlock } from '@typebot.io/schemas' -import { defaultPhoneInputOptions } from '@typebot.io/schemas/features/blocks/inputs/phone/constants' +import { WithVariableContent } from "@/features/graph/components/nodes/block/WithVariableContent"; +import { Text } from "@chakra-ui/react"; +import { defaultPhoneInputOptions } from "@typebot.io/blocks-inputs/phone/constants"; +import type { PhoneNumberInputBlock } from "@typebot.io/blocks-inputs/phone/schema"; +import React from "react"; type Props = { - options: PhoneNumberInputBlock['options'] -} + options: PhoneNumberInputBlock["options"]; +}; export const PhoneNodeContent = ({ options: { variableId, labels } = {}, @@ -14,7 +14,7 @@ export const PhoneNodeContent = ({ variableId ? ( ) : ( - + {labels?.placeholder ?? defaultPhoneInputOptions.labels.placeholder} - ) + ); diff --git a/apps/builder/src/features/blocks/inputs/phone/phone.spec.ts b/apps/builder/src/features/blocks/inputs/phone/phone.spec.ts index 0f522830ac..d2f8456f4a 100644 --- a/apps/builder/src/features/blocks/inputs/phone/phone.spec.ts +++ b/apps/builder/src/features/blocks/inputs/phone/phone.spec.ts @@ -1,13 +1,13 @@ -import test, { expect } from '@playwright/test' -import { createTypebots } from '@typebot.io/playwright/databaseActions' -import { parseDefaultGroupWithBlock } from '@typebot.io/playwright/databaseHelpers' -import { createId } from '@paralleldrive/cuid2' -import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants' -import { defaultPhoneInputOptions } from '@typebot.io/schemas/features/blocks/inputs/phone/constants' +import { createId } from "@paralleldrive/cuid2"; +import test, { expect } from "@playwright/test"; +import { InputBlockType } from "@typebot.io/blocks-inputs/constants"; +import { defaultPhoneInputOptions } from "@typebot.io/blocks-inputs/phone/constants"; +import { createTypebots } from "@typebot.io/playwright/databaseActions"; +import { parseDefaultGroupWithBlock } from "@typebot.io/playwright/databaseHelpers"; -test.describe('Phone input block', () => { - test('options should work', async ({ page }) => { - const typebotId = createId() +test.describe("Phone input block", () => { + test("options should work", async ({ page }) => { + const typebotId = createId(); await createTypebots([ { id: typebotId, @@ -15,34 +15,34 @@ test.describe('Phone input block', () => { type: InputBlockType.PHONE, }), }, - ]) + ]); - await page.goto(`/typebots/${typebotId}/edit`) + await page.goto(`/typebots/${typebotId}/edit`); - await page.click('text=Test') + await page.click("text=Test"); await expect( page.locator( - `input[placeholder="${defaultPhoneInputOptions.labels.placeholder}"]` - ) - ).toHaveAttribute('type', 'tel') + `input[placeholder="${defaultPhoneInputOptions.labels.placeholder}"]`, + ), + ).toHaveAttribute("type", "tel"); - await page.click(`text=${defaultPhoneInputOptions.labels.placeholder}`) - await page.getByLabel('Placeholder:').fill('+33 XX XX XX XX') - await page.getByLabel('Button label:').fill('Go') + await page.click(`text=${defaultPhoneInputOptions.labels.placeholder}`); + await page.getByLabel("Placeholder:").fill("+33 XX XX XX XX"); + await page.getByLabel("Button label:").fill("Go"); await page.fill( `input[value="${defaultPhoneInputOptions.retryMessageContent}"]`, - 'Try again bro' - ) + "Try again bro", + ); - await page.click('text=Restart') - await page.locator(`input[placeholder="+33 XX XX XX XX"]`).type('+33 6 73') - await expect(page.getByText('🇫🇷')).toBeVisible() - await page.locator('button >> text="Go"').click() - await expect(page.locator('text=Try again bro')).toBeVisible() + await page.click("text=Restart"); + await page.locator(`input[placeholder="+33 XX XX XX XX"]`).type("+33 6 73"); + await expect(page.getByText("🇫🇷")).toBeVisible(); + await page.locator('button >> text="Go"').click(); + await expect(page.locator("text=Try again bro")).toBeVisible(); await page .locator(`input[placeholder="+33 XX XX XX XX"]`) - .fill('+33 6 73 54 45 67') - await page.locator('button >> text="Go"').click() - await expect(page.locator('text=+33 6 73 54 45 67')).toBeVisible() - }) -}) + .fill("+33 6 73 54 45 67"); + await page.locator('button >> text="Go"').click(); + await expect(page.locator("text=+33 6 73 54 45 67")).toBeVisible(); + }); +}); diff --git a/apps/builder/src/features/blocks/inputs/pictureChoice/components/PictureChoiceIcon.tsx b/apps/builder/src/features/blocks/inputs/pictureChoice/components/PictureChoiceIcon.tsx index d9a4d34660..6276cc040d 100644 --- a/apps/builder/src/features/blocks/inputs/pictureChoice/components/PictureChoiceIcon.tsx +++ b/apps/builder/src/features/blocks/inputs/pictureChoice/components/PictureChoiceIcon.tsx @@ -1,7 +1,7 @@ -import { ImageIcon } from '@/components/icons' -import { IconProps } from '@chakra-ui/react' -import React from 'react' +import { ImageIcon } from "@/components/icons"; +import type { IconProps } from "@chakra-ui/react"; +import React from "react"; export const PictureChoiceIcon = (props: IconProps) => ( -) +); diff --git a/apps/builder/src/features/blocks/inputs/pictureChoice/components/PictureChoiceItemNode.tsx b/apps/builder/src/features/blocks/inputs/pictureChoice/components/PictureChoiceItemNode.tsx index 0ae32b7cc8..41f1717d29 100644 --- a/apps/builder/src/features/blocks/inputs/pictureChoice/components/PictureChoiceItemNode.tsx +++ b/apps/builder/src/features/blocks/inputs/pictureChoice/components/PictureChoiceItemNode.tsx @@ -1,66 +1,67 @@ +import { ImageIcon, PlusIcon } from "@/components/icons"; +import { useTypebot } from "@/features/editor/providers/TypebotProvider"; +import { useGraph } from "@/features/graph/providers/GraphProvider"; import { Fade, - IconButton, Flex, + IconButton, Image, Popover, - Portal, - PopoverContent, + PopoverAnchor, PopoverArrow, PopoverBody, - PopoverAnchor, - useEventListener, + PopoverContent, + Portal, useColorModeValue, -} from '@chakra-ui/react' -import { ImageIcon, PlusIcon } from '@/components/icons' -import { useTypebot } from '@/features/editor/providers/TypebotProvider' -import { ItemIndices } from '@typebot.io/schemas' -import React, { useRef } from 'react' -import { PictureChoiceItem } from '@typebot.io/schemas/features/blocks/inputs/pictureChoice' -import { useGraph } from '@/features/graph/providers/GraphProvider' -import { PictureChoiceItemSettings } from './PictureChoiceItemSettings' -import { isSvgSrc } from '@typebot.io/lib' + useEventListener, +} from "@chakra-ui/react"; +import type { ItemIndices } from "@typebot.io/blocks-core/schemas/items/types"; +import type { PictureChoiceItem } from "@typebot.io/blocks-inputs/pictureChoice/schema"; +import { isSvgSrc } from "@typebot.io/lib/utils"; +import type React from "react"; +import { useRef } from "react"; +import { PictureChoiceItemSettings } from "./PictureChoiceItemSettings"; type Props = { - item: PictureChoiceItem - indices: ItemIndices - isMouseOver: boolean -} + item: PictureChoiceItem; + indices: ItemIndices; + isMouseOver: boolean; +}; export const PictureChoiceItemNode = ({ item, indices, isMouseOver, }: Props) => { - const emptyImageBgColor = useColorModeValue('gray.100', 'gray.700') - const { openedItemId, setOpenedItemId } = useGraph() - const { updateItem, createItem, typebot } = useTypebot() - const ref = useRef(null) + const emptyImageBgColor = useColorModeValue("gray.100", "gray.700"); + const { openedItemId, setOpenedItemId } = useGraph(); + const { updateItem, createItem, typebot } = useTypebot(); + const ref = useRef(null); const handlePlusClick = (e: React.MouseEvent) => { - e.stopPropagation() - const itemIndex = indices.itemIndex + 1 - createItem({}, { ...indices, itemIndex }) - } + e.stopPropagation(); + const itemIndex = indices.itemIndex + 1; + createItem({}, { ...indices, itemIndex }); + }; - const handleMouseDown = (e: React.MouseEvent) => e.stopPropagation() + const handleMouseDown = (e: React.MouseEvent) => e.stopPropagation(); const openPopover = () => { - setOpenedItemId(item.id) - } + setOpenedItemId(item.id); + }; const handleItemChange = (updates: Partial) => { - updateItem(indices, { ...item, ...updates }) - } + updateItem(indices, { ...item, ...updates }); + }; const handleMouseWheel = (e: WheelEvent) => { - e.stopPropagation() - } - useEventListener('wheel', handleMouseWheel, ref.current) + e.stopPropagation(); + }; + useEventListener("wheel", handleMouseWheel, ref.current); const blockId = typebot ? typebot.groups.at(indices.groupIndex)?.blocks?.at(indices.blockIndex)?.id - : undefined + : undefined; return ( @@ -107,10 +108,10 @@ export const PictureChoiceItemNode = ({ @@ -149,5 +150,5 @@ export const PictureChoiceItemNode = ({ - ) -} + ); +}; diff --git a/apps/builder/src/features/blocks/inputs/pictureChoice/components/PictureChoiceItemSettings.tsx b/apps/builder/src/features/blocks/inputs/pictureChoice/components/PictureChoiceItemSettings.tsx index f48e2df78a..869f3854c5 100644 --- a/apps/builder/src/features/blocks/inputs/pictureChoice/components/PictureChoiceItemSettings.tsx +++ b/apps/builder/src/features/blocks/inputs/pictureChoice/components/PictureChoiceItemSettings.tsx @@ -1,6 +1,7 @@ -import React from 'react' -import { TextInput, Textarea } from '@/components/inputs' -import { PictureChoiceItem } from '@typebot.io/schemas/features/blocks/inputs/pictureChoice' +import { ImageUploadContent } from "@/components/ImageUploadContent"; +import { SwitchWithRelatedSettings } from "@/components/SwitchWithRelatedSettings"; +import { TextInput, Textarea } from "@/components/inputs"; +import { ConditionForm } from "@/features/blocks/logic/condition/components/ConditionForm"; import { Button, HStack, @@ -9,21 +10,20 @@ import { PopoverTrigger, Stack, Text, -} from '@chakra-ui/react' -import { ImageUploadContent } from '@/components/ImageUploadContent' -import { SwitchWithRelatedSettings } from '@/components/SwitchWithRelatedSettings' -import { ConditionForm } from '@/features/blocks/logic/condition/components/ConditionForm' -import { Condition } from '@typebot.io/schemas' -import { LogicalOperator } from '@typebot.io/schemas/features/blocks/logic/condition/constants' -import { useTranslate } from '@tolgee/react' +} from "@chakra-ui/react"; +import { useTranslate } from "@tolgee/react"; +import type { PictureChoiceItem } from "@typebot.io/blocks-inputs/pictureChoice/schema"; +import { LogicalOperator } from "@typebot.io/conditions/constants"; +import type { Condition } from "@typebot.io/conditions/schemas"; +import React from "react"; type Props = { - workspaceId: string - typebotId: string - blockId: string - item: PictureChoiceItem - onItemChange: (updates: Partial) => void -} + workspaceId: string; + typebotId: string; + blockId: string; + item: PictureChoiceItem; + onItemChange: (updates: Partial) => void; +}; export const PictureChoiceItemSettings = ({ workspaceId, @@ -32,16 +32,16 @@ export const PictureChoiceItemSettings = ({ item, onItemChange, }: Props) => { - const { t } = useTranslate() + const { t } = useTranslate(); - const updateTitle = (title: string) => onItemChange({ ...item, title }) + const updateTitle = (title: string) => onItemChange({ ...item, title }); const updateImage = (pictureSrc: string) => { - onItemChange({ ...item, pictureSrc }) - } + onItemChange({ ...item, pictureSrc }); + }; const updateDescription = (description: string) => - onItemChange({ ...item, description }) + onItemChange({ ...item, description }); const updateIsDisplayConditionEnabled = (isEnabled: boolean) => onItemChange({ @@ -50,7 +50,7 @@ export const PictureChoiceItemSettings = ({ ...item.displayCondition, isEnabled, }, - }) + }); const updateDisplayCondition = (condition: Condition) => onItemChange({ @@ -59,13 +59,13 @@ export const PictureChoiceItemSettings = ({ ...item.displayCondition, condition, }, - }) + }); return ( - {t('blocks.inputs.picture.itemSettings.image.label')} + {t("blocks.inputs.picture.itemSettings.image.label")} {({ onClose }) => ( @@ -73,8 +73,8 @@ export const PictureChoiceItemSettings = ({ @@ -87,10 +87,10 @@ export const PictureChoiceItemSettings = ({ }} defaultUrl={item.pictureSrc} onSubmit={(url) => { - updateImage(url) - onClose() + updateImage(url); + onClose(); }} - excludedTabs={['emoji']} + excludedTabs={["emoji"]} /> @@ -98,17 +98,17 @@ export const PictureChoiceItemSettings = ({