2
0

🚸 (variables) Allow null values in variable list

This commit is contained in:
Baptiste Arnaud
2023-03-21 15:42:03 +01:00
parent c52a284013
commit 0c39ae41b6
18 changed files with 69 additions and 33 deletions

View File

@ -13,7 +13,7 @@ import { Plan, Workspace } from '@typebot.io/prisma'
import React from 'react' import React from 'react'
import { parseNumberWithCommas } from '@typebot.io/lib' import { parseNumberWithCommas } from '@typebot.io/lib'
import { getChatsLimit, getStorageLimit } from '@typebot.io/lib/pricing' import { getChatsLimit, getStorageLimit } from '@typebot.io/lib/pricing'
import { trpc } from '@/lib/trpc' import { defaultQueryOptions, trpc } from '@/lib/trpc'
import { storageToReadable } from '../helpers/storageToReadable' import { storageToReadable } from '../helpers/storageToReadable'
type Props = { type Props = {
@ -21,9 +21,12 @@ type Props = {
} }
export const UsageProgressBars = ({ workspace }: Props) => { export const UsageProgressBars = ({ workspace }: Props) => {
const { data, isLoading } = trpc.billing.getUsage.useQuery({ const { data, isLoading } = trpc.billing.getUsage.useQuery(
workspaceId: workspace.id, {
}) workspaceId: workspace.id,
},
defaultQueryOptions
)
const totalChatsUsed = data?.totalChatsUsed ?? 0 const totalChatsUsed = data?.totalChatsUsed ?? 0
const totalStorageUsed = data?.totalStorageUsed ?? 0 const totalStorageUsed = data?.totalStorageUsed ?? 0

View File

@ -136,7 +136,7 @@ test.describe.parallel('Google sheets integration', () => {
.press('Enter') .press('Enter')
await expect( await expect(
page.locator('typebot-standard').locator('text=Your name is:') page.locator('typebot-standard').locator('text=Your name is:')
).toHaveText(`Your name is: Georges Smith`) ).toHaveText(`Your name is: Georges2 Smith2`)
}) })
}) })

View File

@ -38,7 +38,9 @@ test('should be able to connect custom domain', async ({ page }) => {
await expect(page.locator('text=sub.yolozeeer.com')).toBeHidden() await expect(page.locator('text=sub.yolozeeer.com')).toBeHidden()
await page.click('button >> text=Add my domain') await page.click('button >> text=Add my domain')
await page.click('[aria-label="Remove 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', () => { test.describe('Starter workspace', () => {

View File

@ -34,7 +34,11 @@ test('folders navigation should work', async ({ page }) => {
test('folders and typebots should be deletable', async ({ page }) => { test('folders and typebots should be deletable', async ({ page }) => {
await createFolders([{ name: 'Folder #1' }, { name: 'Folder #2' }]) 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.goto('/typebots')
await page.click('button[aria-label="Show Folder #1 menu"]') await page.click('button[aria-label="Show Folder #1 menu"]')
await page.click('li:has-text("Folder #1") >> button:has-text("Delete")') await page.click('li:has-text("Folder #1") >> button:has-text("Delete")')

View File

@ -92,6 +92,7 @@ export const WorkspaceProvider = ({
onError: (error) => showToast({ description: error.message }), onError: (error) => showToast({ description: error.message }),
onSuccess: async () => { onSuccess: async () => {
trpcContext.workspace.listWorkspaces.invalidate() trpcContext.workspace.listWorkspaces.invalidate()
setWorkspaceId(undefined)
}, },
}) })
@ -154,7 +155,6 @@ export const WorkspaceProvider = ({
const deleteCurrentWorkspace = async () => { const deleteCurrentWorkspace = async () => {
if (!workspaceId || !workspaces || workspaces.length < 2) return if (!workspaceId || !workspaces || workspaces.length < 2) return
await deleteWorkspaceMutation.mutateAsync({ workspaceId }) await deleteWorkspaceMutation.mutateAsync({ workspaceId })
setWorkspaceId(workspaces[0].id)
} }
const refreshWorkspace = () => { const refreshWorkspace = () => {

View File

@ -2,6 +2,7 @@ import { httpBatchLink, loggerLink } from '@trpc/client'
import { createTRPCNext } from '@trpc/next' import { createTRPCNext } from '@trpc/next'
import type { AppRouter } from '../helpers/server/routers/v1/trpcRouter' import type { AppRouter } from '../helpers/server/routers/v1/trpcRouter'
import superjson from 'superjson' import superjson from 'superjson'
import { env } from '@typebot.io/lib'
const getBaseUrl = () => const getBaseUrl = () =>
typeof window !== 'undefined' ? '' : process.env.NEXTAUTH_URL typeof window !== 'undefined' ? '' : process.env.NEXTAUTH_URL
@ -24,3 +25,7 @@ export const trpc = createTRPCNext<AppRouter>({
}, },
ssr: false, ssr: false,
}) })
export const defaultQueryOptions = {
refetchOnMount: env('E2E_TEST') === 'true',
}

View File

@ -1471,7 +1471,8 @@
{ {
"type": "array", "type": "array",
"items": { "items": {
"type": "string" "type": "string",
"nullable": true
} }
} }
] ]

View File

@ -2535,7 +2535,8 @@
{ {
"type": "array", "type": "array",
"items": { "items": {
"type": "string" "type": "string",
"nullable": true
} }
} }
], ],
@ -3811,7 +3812,8 @@
{ {
"type": "array", "type": "array",
"items": { "items": {
"type": "string" "type": "string",
"nullable": true
} }
} }
], ],
@ -3905,7 +3907,8 @@
{ {
"type": "array", "type": "array",
"items": { "items": {
"type": "string" "type": "string",
"nullable": true
} }
} }
], ],

View File

@ -19,7 +19,7 @@ export const injectVariableValuesInButtonsInputBlock =
if (!variable || typeof variable.value === 'string') return block if (!variable || typeof variable.value === 'string') return block
return { return {
...block, ...block,
items: variable.value.map((item, idx) => ({ items: variable.value.filter(isDefined).map((item, idx) => ({
id: idx.toString(), id: idx.toString(),
type: ItemType.BUTTON, type: ItemType.BUTTON,
blockId: block.id, blockId: block.id,

View File

@ -9,10 +9,11 @@ import {
SendEmailOptions, SendEmailOptions,
SessionState, SessionState,
SmtpCredentials, SmtpCredentials,
Variable,
} from '@typebot.io/schemas' } from '@typebot.io/schemas'
import { createTransport } from 'nodemailer' import { createTransport } from 'nodemailer'
import Mail from 'nodemailer/lib/mailer' 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 { parseAnswers } from '@typebot.io/lib/results'
import { decrypt } from '@typebot.io/lib/api' import { decrypt } from '@typebot.io/lib/api'
import { defaultFrom, defaultTransportOptions } from './constants' import { defaultFrom, defaultTransportOptions } from './constants'
@ -51,8 +52,7 @@ export const executeSendEmailBlock = async (
replyTo: options.replyTo replyTo: options.replyTo
? parseVariables(variables)(options.replyTo) ? parseVariables(variables)(options.replyTo)
: undefined, : undefined,
fileUrls: fileUrls: getFileUrls(variables)(options.attachmentsVariableId),
variables.find(byId(options.attachmentsVariableId))?.value ?? undefined,
isCustomBody: options.isCustomBody, isCustomBody: options.isCustomBody,
isBodyCode: options.isBodyCode, isBodyCode: options.isBodyCode,
}) })
@ -238,3 +238,12 @@ const parseEmailRecipient = (
email: recipient, 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)
}

View File

@ -83,7 +83,7 @@ const parseResultSample = (
headerCells: ResultHeaderCell[], headerCells: ResultHeaderCell[],
variables: Variable[] variables: Variable[]
) => ) =>
headerCells.reduce<Record<string, string | string[] | undefined>>( headerCells.reduce<Record<string, string | (string | null)[] | undefined>>(
(resultSample, cell) => { (resultSample, cell) => {
const inputBlock = inputBlocks.find((inputBlock) => const inputBlock = inputBlocks.find((inputBlock) =>
cell.blocks?.some((block) => block.id === inputBlock.id) cell.blocks?.some((block) => block.id === inputBlock.id)

View File

@ -41,8 +41,8 @@ const executeComparison =
if (isNotDefined(value)) return false if (isNotDefined(value)) return false
switch (comparison.comparisonOperator) { switch (comparison.comparisonOperator) {
case ComparisonOperators.CONTAINS: { case ComparisonOperators.CONTAINS: {
const contains = (a: string, b: string) => { const contains = (a: string | null, b: string | null) => {
if (b === '') return false if (b === '' || !b || !a) return false
return a.toLowerCase().trim().includes(b.toLowerCase().trim()) return a.toLowerCase().trim().includes(b.toLowerCase().trim())
} }
return compare(contains, inputValue, value) return compare(contains, inputValue, value)
@ -55,14 +55,20 @@ const executeComparison =
} }
case ComparisonOperators.GREATER: { case ComparisonOperators.GREATER: {
return compare( return compare(
(a, b) => parseFloat(a) > parseFloat(b), (a, b) =>
isDefined(a) && isDefined(b)
? parseFloat(a) > parseFloat(b)
: false,
inputValue, inputValue,
value value
) )
} }
case ComparisonOperators.LESS: { case ComparisonOperators.LESS: {
return compare( return compare(
(a, b) => parseFloat(a) < parseFloat(b), (a, b) =>
isDefined(a) && isDefined(b)
? parseFloat(a) < parseFloat(b)
: false,
inputValue, inputValue,
value value
) )
@ -74,10 +80,11 @@ const executeComparison =
} }
const compare = ( const compare = (
func: (a: string, b: string) => boolean, func: (a: string | null, b: string | null) => boolean,
a: string | string[], a: Variable['value'],
b: string | string[] b: Variable['value']
): boolean => { ): boolean => {
if (!a || !b) return false
if (typeof a === 'string') { if (typeof a === 'string') {
if (typeof b === 'string') return func(a, b) if (typeof b === 'string') return func(a, b)
return b.some((b) => func(a, b)) return b.some((b) => func(a, b))

View File

@ -2,7 +2,7 @@ import { Variable } from '@typebot.io/schemas'
export const findUniqueVariableValue = export const findUniqueVariableValue =
(variables: Variable[]) => (variables: Variable[]) =>
(value: string | undefined): string | string[] | null => { (value: string | undefined): Variable['value'] => {
if (!value || !value.startsWith('{{') || !value.endsWith('}}')) return null if (!value || !value.startsWith('{{') || !value.endsWith('}}')) return null
const variableName = value.slice(2, -2) const variableName = value.slice(2, -2)
const variable = variables.find( const variable = variables.find(

View File

@ -2,7 +2,7 @@ import { Variable } from '@typebot.io/schemas'
export const parseGuessedValueType = ( export const parseGuessedValueType = (
value: Variable['value'] value: Variable['value']
): string | string[] | boolean | number | null | undefined => { ): string | (string | null)[] | boolean | number | null | undefined => {
if (value === null) return null if (value === null) return null
if (value === undefined) return undefined if (value === undefined) return undefined
if (typeof value !== 'string') return value if (typeof value !== 'string') return value

View File

@ -30,7 +30,7 @@ const updateResultVariables =
const serializedNewVariables = newVariables.map((variable) => ({ const serializedNewVariables = newVariables.map((variable) => ({
...variable, ...variable,
value: Array.isArray(variable.value) value: Array.isArray(variable.value)
? variable.value.map(safeStringify).filter(isDefined) ? variable.value.map(safeStringify)
: safeStringify(variable.value), : safeStringify(variable.value),
})) }))
@ -62,7 +62,7 @@ const updateTypebotVariables =
const serializedNewVariables = newVariables.map((variable) => ({ const serializedNewVariables = newVariables.map((variable) => ({
...variable, ...variable,
value: Array.isArray(variable.value) value: Array.isArray(variable.value)
? variable.value.map(safeStringify).filter(isDefined) ? variable.value.map(safeStringify)
: safeStringify(variable.value), : safeStringify(variable.value),
})) }))

View File

@ -48,7 +48,7 @@ export const safeStringify = (val: unknown): string | null => {
export const parseCorrectValueType = ( export const parseCorrectValueType = (
value: Variable['value'] value: Variable['value']
): string | string[] | boolean | number | null | undefined => { ): string | (string | null)[] | boolean | number | null | undefined => {
if (value === null) return null if (value === null) return null
if (value === undefined) return undefined if (value === undefined) return undefined
if (Array.isArray(value)) return value if (Array.isArray(value)) return value

View File

@ -6,7 +6,7 @@ import {
} from './blocks' } from './blocks'
import { publicTypebotSchema } from './publicTypebot' import { publicTypebotSchema } from './publicTypebot'
import { logSchema, resultSchema } from './result' import { logSchema, resultSchema } from './result'
import { typebotSchema } from './typebot' import { listVariableValue, typebotSchema } from './typebot'
import { import {
textBubbleContentSchema, textBubbleContentSchema,
imageBubbleContentSchema, imageBubbleContentSchema,
@ -123,7 +123,7 @@ const scriptToExecuteSchema = z.object({
.string() .string()
.or(z.number()) .or(z.number())
.or(z.boolean()) .or(z.boolean())
.or(z.array(z.string())) .or(listVariableValue)
.nullish(), .nullish(),
}) })
), ),

View File

@ -1,9 +1,11 @@
import { z } from 'zod' import { z } from 'zod'
export const listVariableValue = z.array(z.string().nullable())
export const variableSchema = z.object({ export const variableSchema = z.object({
id: z.string(), id: z.string(),
name: 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({ export const variableWithValueSchema = z.object({
id: z.string(), id: z.string(),
name: z.string(), name: z.string(),
value: z.string().or(z.array(z.string())), value: z.string().or(listVariableValue),
}) })
/** /**