From 0c39ae41b6e8b191be496f8d9ff74c6f15090c9c Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Tue, 21 Mar 2023 15:42:03 +0100 Subject: [PATCH] :children_crossing: (variables) Allow null values in variable list --- .../billing/components/UsageProgressBars.tsx | 11 ++++++---- .../googleSheets/googleSheets.spec.ts | 2 +- .../customDomains/customDomains.spec.ts | 4 +++- .../src/features/dashboard/dashboard.spec.ts | 6 +++++- .../features/workspace/WorkspaceProvider.tsx | 2 +- apps/builder/src/lib/trpc.ts | 5 +++++ apps/docs/openapi/builder/_spec_.json | 3 ++- apps/docs/openapi/chat/_spec_.json | 9 +++++--- ...injectVariableValuesInButtonsInputBlock.ts | 2 +- .../sendEmail/executeSendEmailBlock.tsx | 15 ++++++++++--- .../integrations/webhook/parseSampleResult.ts | 2 +- .../logic/condition/executeCondition.ts | 21 ++++++++++++------- .../variables/findUniqueVariableValue.ts | 2 +- .../variables/parseGuessedValueType.ts | 2 +- .../src/features/variables/updateVariables.ts | 4 ++-- .../src/features/variables/utils.ts | 2 +- packages/schemas/features/chat.ts | 4 ++-- packages/schemas/features/typebot/variable.ts | 6 ++++-- 18 files changed, 69 insertions(+), 33 deletions(-) diff --git a/apps/builder/src/features/billing/components/UsageProgressBars.tsx b/apps/builder/src/features/billing/components/UsageProgressBars.tsx index 290d8e687..cfe0662b2 100644 --- a/apps/builder/src/features/billing/components/UsageProgressBars.tsx +++ b/apps/builder/src/features/billing/components/UsageProgressBars.tsx @@ -13,7 +13,7 @@ import { Plan, Workspace } from '@typebot.io/prisma' import React from 'react' import { parseNumberWithCommas } from '@typebot.io/lib' import { getChatsLimit, getStorageLimit } from '@typebot.io/lib/pricing' -import { trpc } from '@/lib/trpc' +import { defaultQueryOptions, trpc } from '@/lib/trpc' import { storageToReadable } from '../helpers/storageToReadable' type Props = { @@ -21,9 +21,12 @@ type Props = { } export const UsageProgressBars = ({ workspace }: Props) => { - const { data, isLoading } = trpc.billing.getUsage.useQuery({ - workspaceId: workspace.id, - }) + const { data, isLoading } = trpc.billing.getUsage.useQuery( + { + workspaceId: workspace.id, + }, + defaultQueryOptions + ) const totalChatsUsed = data?.totalChatsUsed ?? 0 const totalStorageUsed = data?.totalStorageUsed ?? 0 diff --git a/apps/builder/src/features/blocks/integrations/googleSheets/googleSheets.spec.ts b/apps/builder/src/features/blocks/integrations/googleSheets/googleSheets.spec.ts index c79d40483..cb46dace1 100644 --- a/apps/builder/src/features/blocks/integrations/googleSheets/googleSheets.spec.ts +++ b/apps/builder/src/features/blocks/integrations/googleSheets/googleSheets.spec.ts @@ -136,7 +136,7 @@ test.describe.parallel('Google sheets integration', () => { .press('Enter') await expect( page.locator('typebot-standard').locator('text=Your name is:') - ).toHaveText(`Your name is: Georges Smith`) + ).toHaveText(`Your name is: Georges2 Smith2`) }) }) diff --git a/apps/builder/src/features/customDomains/customDomains.spec.ts b/apps/builder/src/features/customDomains/customDomains.spec.ts index b11a428d1..d4f50cb57 100644 --- a/apps/builder/src/features/customDomains/customDomains.spec.ts +++ b/apps/builder/src/features/customDomains/customDomains.spec.ts @@ -38,7 +38,9 @@ test('should be able to connect custom domain', async ({ page }) => { await expect(page.locator('text=sub.yolozeeer.com')).toBeHidden() await page.click('button >> text=Add my domain') await page.click('[aria-label="Remove domain"]') - await expect(page.locator('[aria-label="Remove domain"]')).toBeHidden() + await expect(page.locator('[aria-label="Remove domain"]')).toBeHidden({ + timeout: 10000, + }) }) test.describe('Starter workspace', () => { diff --git a/apps/builder/src/features/dashboard/dashboard.spec.ts b/apps/builder/src/features/dashboard/dashboard.spec.ts index 78fe4319e..85b0dd557 100644 --- a/apps/builder/src/features/dashboard/dashboard.spec.ts +++ b/apps/builder/src/features/dashboard/dashboard.spec.ts @@ -34,7 +34,11 @@ test('folders navigation should work', async ({ page }) => { test('folders and typebots should be deletable', async ({ page }) => { await createFolders([{ name: 'Folder #1' }, { name: 'Folder #2' }]) - await createTypebots([{ id: 'deletable-typebot', name: 'Typebot #1' }]) + const tomorrow = new Date() + tomorrow.setDate(tomorrow.getDate() + 1) + await createTypebots([ + { id: 'deletable-typebot', name: 'Typebot #1', createdAt: tomorrow }, + ]) await page.goto('/typebots') await page.click('button[aria-label="Show Folder #1 menu"]') await page.click('li:has-text("Folder #1") >> button:has-text("Delete")') diff --git a/apps/builder/src/features/workspace/WorkspaceProvider.tsx b/apps/builder/src/features/workspace/WorkspaceProvider.tsx index e22f4d1a3..107d5aa01 100644 --- a/apps/builder/src/features/workspace/WorkspaceProvider.tsx +++ b/apps/builder/src/features/workspace/WorkspaceProvider.tsx @@ -92,6 +92,7 @@ export const WorkspaceProvider = ({ onError: (error) => showToast({ description: error.message }), onSuccess: async () => { trpcContext.workspace.listWorkspaces.invalidate() + setWorkspaceId(undefined) }, }) @@ -154,7 +155,6 @@ export const WorkspaceProvider = ({ const deleteCurrentWorkspace = async () => { if (!workspaceId || !workspaces || workspaces.length < 2) return await deleteWorkspaceMutation.mutateAsync({ workspaceId }) - setWorkspaceId(workspaces[0].id) } const refreshWorkspace = () => { diff --git a/apps/builder/src/lib/trpc.ts b/apps/builder/src/lib/trpc.ts index 3615377cd..f36ffeb03 100644 --- a/apps/builder/src/lib/trpc.ts +++ b/apps/builder/src/lib/trpc.ts @@ -2,6 +2,7 @@ import { httpBatchLink, loggerLink } from '@trpc/client' import { createTRPCNext } from '@trpc/next' import type { AppRouter } from '../helpers/server/routers/v1/trpcRouter' import superjson from 'superjson' +import { env } from '@typebot.io/lib' const getBaseUrl = () => typeof window !== 'undefined' ? '' : process.env.NEXTAUTH_URL @@ -24,3 +25,7 @@ export const trpc = createTRPCNext({ }, ssr: false, }) + +export const defaultQueryOptions = { + refetchOnMount: env('E2E_TEST') === 'true', +} diff --git a/apps/docs/openapi/builder/_spec_.json b/apps/docs/openapi/builder/_spec_.json index c68f39185..4ef4d5c8d 100644 --- a/apps/docs/openapi/builder/_spec_.json +++ b/apps/docs/openapi/builder/_spec_.json @@ -1471,7 +1471,8 @@ { "type": "array", "items": { - "type": "string" + "type": "string", + "nullable": true } } ] diff --git a/apps/docs/openapi/chat/_spec_.json b/apps/docs/openapi/chat/_spec_.json index 6f5436ba2..1e4880dc1 100644 --- a/apps/docs/openapi/chat/_spec_.json +++ b/apps/docs/openapi/chat/_spec_.json @@ -2535,7 +2535,8 @@ { "type": "array", "items": { - "type": "string" + "type": "string", + "nullable": true } } ], @@ -3811,7 +3812,8 @@ { "type": "array", "items": { - "type": "string" + "type": "string", + "nullable": true } } ], @@ -3905,7 +3907,8 @@ { "type": "array", "items": { - "type": "string" + "type": "string", + "nullable": true } } ], diff --git a/apps/viewer/src/features/blocks/inputs/buttons/injectVariableValuesInButtonsInputBlock.ts b/apps/viewer/src/features/blocks/inputs/buttons/injectVariableValuesInButtonsInputBlock.ts index af5c990de..03979de24 100644 --- a/apps/viewer/src/features/blocks/inputs/buttons/injectVariableValuesInButtonsInputBlock.ts +++ b/apps/viewer/src/features/blocks/inputs/buttons/injectVariableValuesInButtonsInputBlock.ts @@ -19,7 +19,7 @@ export const injectVariableValuesInButtonsInputBlock = if (!variable || typeof variable.value === 'string') return block return { ...block, - items: variable.value.map((item, idx) => ({ + items: variable.value.filter(isDefined).map((item, idx) => ({ id: idx.toString(), type: ItemType.BUTTON, blockId: block.id, diff --git a/apps/viewer/src/features/blocks/integrations/sendEmail/executeSendEmailBlock.tsx b/apps/viewer/src/features/blocks/integrations/sendEmail/executeSendEmailBlock.tsx index b254f7940..072f0e8f3 100644 --- a/apps/viewer/src/features/blocks/integrations/sendEmail/executeSendEmailBlock.tsx +++ b/apps/viewer/src/features/blocks/integrations/sendEmail/executeSendEmailBlock.tsx @@ -9,10 +9,11 @@ import { SendEmailOptions, SessionState, SmtpCredentials, + Variable, } from '@typebot.io/schemas' import { createTransport } from 'nodemailer' import Mail from 'nodemailer/lib/mailer' -import { byId, isEmpty, isNotDefined, omit } from '@typebot.io/lib' +import { byId, isDefined, isEmpty, isNotDefined, omit } from '@typebot.io/lib' import { parseAnswers } from '@typebot.io/lib/results' import { decrypt } from '@typebot.io/lib/api' import { defaultFrom, defaultTransportOptions } from './constants' @@ -51,8 +52,7 @@ export const executeSendEmailBlock = async ( replyTo: options.replyTo ? parseVariables(variables)(options.replyTo) : undefined, - fileUrls: - variables.find(byId(options.attachmentsVariableId))?.value ?? undefined, + fileUrls: getFileUrls(variables)(options.attachmentsVariableId), isCustomBody: options.isCustomBody, isBodyCode: options.isBodyCode, }) @@ -238,3 +238,12 @@ const parseEmailRecipient = ( email: recipient, } } + +const getFileUrls = + (variables: Variable[]) => + (variableId: string | undefined): string | string[] | undefined => { + const fileUrls = variables.find(byId(variableId))?.value + if (!fileUrls) return + if (typeof fileUrls === 'string') return fileUrls + return fileUrls.filter(isDefined) + } diff --git a/apps/viewer/src/features/blocks/integrations/webhook/parseSampleResult.ts b/apps/viewer/src/features/blocks/integrations/webhook/parseSampleResult.ts index b35e9f6fc..de46c8158 100644 --- a/apps/viewer/src/features/blocks/integrations/webhook/parseSampleResult.ts +++ b/apps/viewer/src/features/blocks/integrations/webhook/parseSampleResult.ts @@ -83,7 +83,7 @@ const parseResultSample = ( headerCells: ResultHeaderCell[], variables: Variable[] ) => - headerCells.reduce>( + headerCells.reduce>( (resultSample, cell) => { const inputBlock = inputBlocks.find((inputBlock) => cell.blocks?.some((block) => block.id === inputBlock.id) diff --git a/apps/viewer/src/features/blocks/logic/condition/executeCondition.ts b/apps/viewer/src/features/blocks/logic/condition/executeCondition.ts index 4af513c0e..5ec6f999b 100644 --- a/apps/viewer/src/features/blocks/logic/condition/executeCondition.ts +++ b/apps/viewer/src/features/blocks/logic/condition/executeCondition.ts @@ -41,8 +41,8 @@ const executeComparison = if (isNotDefined(value)) return false switch (comparison.comparisonOperator) { case ComparisonOperators.CONTAINS: { - const contains = (a: string, b: string) => { - if (b === '') return false + const contains = (a: string | null, b: string | null) => { + if (b === '' || !b || !a) return false return a.toLowerCase().trim().includes(b.toLowerCase().trim()) } return compare(contains, inputValue, value) @@ -55,14 +55,20 @@ const executeComparison = } case ComparisonOperators.GREATER: { return compare( - (a, b) => parseFloat(a) > parseFloat(b), + (a, b) => + isDefined(a) && isDefined(b) + ? parseFloat(a) > parseFloat(b) + : false, inputValue, value ) } case ComparisonOperators.LESS: { return compare( - (a, b) => parseFloat(a) < parseFloat(b), + (a, b) => + isDefined(a) && isDefined(b) + ? parseFloat(a) < parseFloat(b) + : false, inputValue, value ) @@ -74,10 +80,11 @@ const executeComparison = } const compare = ( - func: (a: string, b: string) => boolean, - a: string | string[], - b: string | string[] + func: (a: string | null, b: string | null) => boolean, + a: Variable['value'], + b: Variable['value'] ): boolean => { + if (!a || !b) return false if (typeof a === 'string') { if (typeof b === 'string') return func(a, b) return b.some((b) => func(a, b)) diff --git a/apps/viewer/src/features/variables/findUniqueVariableValue.ts b/apps/viewer/src/features/variables/findUniqueVariableValue.ts index 1e24e574f..6f867c04e 100644 --- a/apps/viewer/src/features/variables/findUniqueVariableValue.ts +++ b/apps/viewer/src/features/variables/findUniqueVariableValue.ts @@ -2,7 +2,7 @@ import { Variable } from '@typebot.io/schemas' export const findUniqueVariableValue = (variables: Variable[]) => - (value: string | undefined): string | string[] | null => { + (value: string | undefined): Variable['value'] => { if (!value || !value.startsWith('{{') || !value.endsWith('}}')) return null const variableName = value.slice(2, -2) const variable = variables.find( diff --git a/apps/viewer/src/features/variables/parseGuessedValueType.ts b/apps/viewer/src/features/variables/parseGuessedValueType.ts index fd10cdb4e..a87f5144a 100644 --- a/apps/viewer/src/features/variables/parseGuessedValueType.ts +++ b/apps/viewer/src/features/variables/parseGuessedValueType.ts @@ -2,7 +2,7 @@ import { Variable } from '@typebot.io/schemas' export const parseGuessedValueType = ( value: Variable['value'] -): string | string[] | boolean | number | null | undefined => { +): string | (string | null)[] | boolean | number | null | undefined => { if (value === null) return null if (value === undefined) return undefined if (typeof value !== 'string') return value diff --git a/apps/viewer/src/features/variables/updateVariables.ts b/apps/viewer/src/features/variables/updateVariables.ts index 27e7f5517..8b927aebf 100644 --- a/apps/viewer/src/features/variables/updateVariables.ts +++ b/apps/viewer/src/features/variables/updateVariables.ts @@ -30,7 +30,7 @@ const updateResultVariables = const serializedNewVariables = newVariables.map((variable) => ({ ...variable, value: Array.isArray(variable.value) - ? variable.value.map(safeStringify).filter(isDefined) + ? variable.value.map(safeStringify) : safeStringify(variable.value), })) @@ -62,7 +62,7 @@ const updateTypebotVariables = const serializedNewVariables = newVariables.map((variable) => ({ ...variable, value: Array.isArray(variable.value) - ? variable.value.map(safeStringify).filter(isDefined) + ? variable.value.map(safeStringify) : safeStringify(variable.value), })) diff --git a/packages/deprecated/bot-engine/src/features/variables/utils.ts b/packages/deprecated/bot-engine/src/features/variables/utils.ts index cc0de1c01..c8c1bdde8 100644 --- a/packages/deprecated/bot-engine/src/features/variables/utils.ts +++ b/packages/deprecated/bot-engine/src/features/variables/utils.ts @@ -48,7 +48,7 @@ export const safeStringify = (val: unknown): string | null => { export const parseCorrectValueType = ( value: Variable['value'] -): string | string[] | boolean | number | null | undefined => { +): string | (string | null)[] | boolean | number | null | undefined => { if (value === null) return null if (value === undefined) return undefined if (Array.isArray(value)) return value diff --git a/packages/schemas/features/chat.ts b/packages/schemas/features/chat.ts index 0d34d3148..6cac9d2d8 100644 --- a/packages/schemas/features/chat.ts +++ b/packages/schemas/features/chat.ts @@ -6,7 +6,7 @@ import { } from './blocks' import { publicTypebotSchema } from './publicTypebot' import { logSchema, resultSchema } from './result' -import { typebotSchema } from './typebot' +import { listVariableValue, typebotSchema } from './typebot' import { textBubbleContentSchema, imageBubbleContentSchema, @@ -123,7 +123,7 @@ const scriptToExecuteSchema = z.object({ .string() .or(z.number()) .or(z.boolean()) - .or(z.array(z.string())) + .or(listVariableValue) .nullish(), }) ), diff --git a/packages/schemas/features/typebot/variable.ts b/packages/schemas/features/typebot/variable.ts index bc9fa2d20..8931848aa 100644 --- a/packages/schemas/features/typebot/variable.ts +++ b/packages/schemas/features/typebot/variable.ts @@ -1,9 +1,11 @@ import { z } from 'zod' +export const listVariableValue = z.array(z.string().nullable()) + export const variableSchema = z.object({ id: z.string(), name: z.string(), - value: z.string().or(z.array(z.string())).nullish(), + value: z.string().or(listVariableValue).nullish(), }) /** @@ -12,7 +14,7 @@ export const variableSchema = z.object({ export const variableWithValueSchema = z.object({ id: z.string(), name: z.string(), - value: z.string().or(z.array(z.string())), + value: z.string().or(listVariableValue), }) /**