🚸 (variables) Allow null values in variable list
This commit is contained in:
@ -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
|
||||||
|
|
||||||
|
@ -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`)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -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', () => {
|
||||||
|
@ -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")')
|
||||||
|
@ -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 = () => {
|
||||||
|
@ -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',
|
||||||
|
}
|
||||||
|
@ -1471,7 +1471,8 @@
|
|||||||
{
|
{
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "string"
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -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,
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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))
|
||||||
|
@ -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(
|
||||||
|
@ -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
|
||||||
|
@ -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),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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(),
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
|
@ -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),
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Reference in New Issue
Block a user