diff --git a/apps/builder/package.json b/apps/builder/package.json index bcf9be87e1..376021bc80 100644 --- a/apps/builder/package.json +++ b/apps/builder/package.json @@ -56,7 +56,6 @@ "@uiw/codemirror-theme-tokyo-night": "4.21.24", "@uiw/react-codemirror": "4.21.24", "@upstash/ratelimit": "0.4.3", - "@upstash/redis": "1.22.0", "@use-gesture/react": "10.2.27", "browser-image-compression": "2.0.2", "canvas-confetti": "1.6.0", @@ -69,6 +68,7 @@ "google-auth-library": "8.9.0", "google-spreadsheet": "4.1.1", "immer": "10.0.2", + "ioredis": "^5.4.1", "isolated-vm": "4.7.2", "jsonwebtoken": "9.0.1", "ky": "1.2.4", diff --git a/apps/builder/src/components/EditableEmojiOrImageIcon.tsx b/apps/builder/src/components/EditableEmojiOrImageIcon.tsx index 5f49038f68..fad061edaf 100644 --- a/apps/builder/src/components/EditableEmojiOrImageIcon.tsx +++ b/apps/builder/src/components/EditableEmojiOrImageIcon.tsx @@ -8,15 +8,17 @@ import { useColorModeValue, Portal, } from '@chakra-ui/react' -import React from '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' type Props = { uploadFileProps: FilePathUploadProps icon?: string | null + parentModalRef?: RefObject | undefined onChangeIcon: (icon: string) => void boxSize?: string } @@ -28,6 +30,7 @@ export const EditableEmojiOrImageIcon = ({ boxSize, }: Props) => { const { t } = useTranslate() + const { ref: parentModalRef } = useParentModal() const bg = useColorModeValue('gray.100', 'gray.700') return ( @@ -56,7 +59,7 @@ export const EditableEmojiOrImageIcon = ({ - + { 'date' ) await page.locator('[data-testid="from-date"]').fill('2021-01-01') - await page.locator('form').getByRole('button').click() + await page.getByLabel('Send').click() await expect(page.locator('text="01/01/2021"')).toBeVisible() await page.click(`text=Pick a date`) diff --git a/apps/builder/src/features/blocks/inputs/fileUpload/fileUpload.spec.ts b/apps/builder/src/features/blocks/inputs/fileUpload/fileUpload.spec.ts index 9bdb69903e..26ed08f239 100644 --- a/apps/builder/src/features/blocks/inputs/fileUpload/fileUpload.spec.ts +++ b/apps/builder/src/features/blocks/inputs/fileUpload/fileUpload.spec.ts @@ -27,7 +27,9 @@ test('options should work', async ({ page }) => { await page .locator(`input[type="file"]`) .setInputFiles([getTestAsset('avatar.jpg')]) - await expect(page.locator(`text=File uploaded`)).toBeVisible() + await expect( + page.getByRole('img', { name: 'Attached image 1' }) + ).toBeVisible() await page.click('text="Collect file"') await page.click('text="Required?"') await page.click('text="Allow multiple files?"') @@ -46,9 +48,11 @@ test('options should work', async ({ page }) => { getTestAsset('avatar.jpg'), getTestAsset('avatar.jpg'), ]) - await expect(page.locator(`text="3"`)).toBeVisible() + await expect(page.getByRole('img', { name: 'avatar.jpg' })).toHaveCount(3) await page.locator('text="Go"').click() - await expect(page.locator(`text="3 files uploaded"`)).toBeVisible() + await expect( + page.getByRole('img', { name: 'Attached image 1' }) + ).toBeVisible() }) test.describe('Free workspace', () => { diff --git a/apps/builder/src/features/blocks/inputs/textInput/components/TextInputNodeContent.tsx b/apps/builder/src/features/blocks/inputs/textInput/components/TextInputNodeContent.tsx index 20ad882243..9ba9d005ae 100644 --- a/apps/builder/src/features/blocks/inputs/textInput/components/TextInputNodeContent.tsx +++ b/apps/builder/src/features/blocks/inputs/textInput/components/TextInputNodeContent.tsx @@ -1,25 +1,48 @@ import React from 'react' -import { Text } from '@chakra-ui/react' +import { Stack, Text } from '@chakra-ui/react' import { WithVariableContent } from '@/features/graph/components/nodes/block/WithVariableContent' import { TextInputBlock } from '@typebot.io/schemas' import { defaultTextInputOptions } from '@typebot.io/schemas/features/blocks/inputs/text/constants' +import { useTypebot } from '@/features/editor/providers/TypebotProvider' +import { SetVariableLabel } from '@/components/SetVariableLabel' type Props = { options: TextInputBlock['options'] } export const TextInputNodeContent = ({ options }: Props) => { + const { typebot } = useTypebot() + const attachmentVariableId = + typebot && + options?.attachments?.isEnabled && + options?.attachments.saveVariableId if (options?.variableId) return ( - + + + {attachmentVariableId && ( + + )} + ) return ( - - {options?.labels?.placeholder ?? - defaultTextInputOptions.labels.placeholder} - + + + {options?.labels?.placeholder ?? + defaultTextInputOptions.labels.placeholder} + + {attachmentVariableId && ( + + )} + ) } diff --git a/apps/builder/src/features/blocks/inputs/textInput/components/TextInputSettings.tsx b/apps/builder/src/features/blocks/inputs/textInput/components/TextInputSettings.tsx index 73615e3ade..121c50c7a0 100644 --- a/apps/builder/src/features/blocks/inputs/textInput/components/TextInputSettings.tsx +++ b/apps/builder/src/features/blocks/inputs/textInput/components/TextInputSettings.tsx @@ -1,9 +1,12 @@ +import { DropdownList } from '@/components/DropdownList' +import { SwitchWithRelatedSettings } from '@/components/SwitchWithRelatedSettings' import { TextInput } from '@/components/inputs' import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel' import { VariableSearchInput } from '@/components/inputs/VariableSearchInput' import { FormLabel, Stack } from '@chakra-ui/react' import { useTranslate } from '@tolgee/react' import { TextInputBlock, Variable } from '@typebot.io/schemas' +import { fileVisibilityOptions } from '@typebot.io/schemas/features/blocks/inputs/file/constants' import { defaultTextInputOptions } from '@typebot.io/schemas/features/blocks/inputs/text/constants' import React from 'react' @@ -14,21 +17,44 @@ type Props = { export const TextInputSettings = ({ options, onOptionsChange }: Props) => { const { t } = useTranslate() - const handlePlaceholderChange = (placeholder: string) => + const updatePlaceholder = (placeholder: string) => onOptionsChange({ ...options, labels: { ...options?.labels, placeholder } }) - const handleButtonLabelChange = (button: string) => + + const updateButtonLabel = (button: string) => onOptionsChange({ ...options, labels: { ...options?.labels, button } }) - const handleLongChange = (isLong: boolean) => + + const updateIsLong = (isLong: boolean) => onOptionsChange({ ...options, isLong }) - const handleVariableChange = (variable?: Variable) => + + const updateVariableId = (variable?: Variable) => onOptionsChange({ ...options, variableId: variable?.id }) + const updateAttachmentsEnabled = (isEnabled: boolean) => + onOptionsChange({ + ...options, + attachments: { ...options?.attachments, isEnabled }, + }) + + const updateAttachmentsSaveVariableId = (variable?: Pick) => + onOptionsChange({ + ...options, + attachments: { ...options?.attachments, saveVariableId: variable?.id }, + }) + + const updateVisibility = ( + visibility: (typeof fileVisibilityOptions)[number] + ) => + onOptionsChange({ + ...options, + attachments: { ...options?.attachments, visibility }, + }) + return ( { options?.labels?.placeholder ?? defaultTextInputOptions.labels.placeholder } - onChange={handlePlaceholderChange} + onChange={updatePlaceholder} /> + + + + Save the URLs in a variable: + + + + + {t('blocks.inputs.settings.saveAnswer.label')} diff --git a/apps/builder/src/features/blocks/inputs/textInput/textInput.spec.ts b/apps/builder/src/features/blocks/inputs/textInput/textInput.spec.ts index 0b5edb44e9..ace3ed22e3 100644 --- a/apps/builder/src/features/blocks/inputs/textInput/textInput.spec.ts +++ b/apps/builder/src/features/blocks/inputs/textInput/textInput.spec.ts @@ -4,6 +4,7 @@ import { parseDefaultGroupWithBlock } from '@typebot.io/playwright/databaseHelpe import { createId } from '@paralleldrive/cuid2' import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants' import { defaultTextInputOptions } from '@typebot.io/schemas/features/blocks/inputs/text/constants' +import { getTestAsset } from '@/test/utils/playwright' test.describe.parallel('Text input block', () => { test('options should work', async ({ page }) => { @@ -37,4 +38,48 @@ test.describe.parallel('Text input block', () => { ).toBeVisible() await expect(page.getByRole('button', { name: 'Go' })).toBeVisible() }) + + test('hey boy', async ({ page }) => { + const typebotId = createId() + await createTypebots([ + { + id: typebotId, + ...parseDefaultGroupWithBlock({ + type: InputBlockType.TEXT, + }), + }, + ]) + + await page.goto(`/typebots/${typebotId}/edit`) + + await page.click(`text=${defaultTextInputOptions.labels.placeholder}`) + await page.getByText('Allow attachments').click() + await page.locator('[data-testid="variables-input"]').first().click() + await page.getByText('var1').click() + await page.getByRole('button', { name: 'Test' }).click() + await page + .getByPlaceholder('Type your answer...') + .fill('Help me with these') + await page.getByLabel('Add attachments').click() + await expect(page.getByRole('menuitem', { name: 'Document' })).toBeVisible() + await expect( + page.getByRole('menuitem', { name: 'Photos & videos' }) + ).toBeVisible() + await page + .locator('#document-upload') + .setInputFiles(getTestAsset('typebots/theme.json')) + await expect(page.getByText('theme.json')).toBeVisible() + await page + .locator('#photos-upload') + .setInputFiles([getTestAsset('avatar.jpg'), getTestAsset('avatar.jpg')]) + await expect(page.getByRole('img', { name: 'avatar.jpg' })).toHaveCount(2) + await page.getByRole('img', { name: 'avatar.jpg' }).first().hover() + await page.getByLabel('Remove attachment').first().click() + await expect(page.getByRole('img', { name: 'avatar.jpg' })).toHaveCount(1) + await page.getByLabel('Send').click() + await expect( + page.getByRole('img', { name: 'Attached image 1' }) + ).toBeVisible() + await expect(page.getByText('Help me with these')).toBeVisible() + }) }) 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 420304589a..b0bb7fcc04 100644 --- a/apps/builder/src/features/blocks/integrations/googleSheets/googleSheets.spec.ts +++ b/apps/builder/src/features/blocks/integrations/googleSheets/googleSheets.spec.ts @@ -19,9 +19,9 @@ test.describe.parallel('Google sheets integration', () => { await page.click('text=Add a value') await page.click('text=Select a column') - await page.click('button >> text="Email"') + await page.getByRole('menuitem', { name: 'Email' }).click() await page.click('[aria-label="Insert a variable"]') - await page.click('button >> text="Email" >> nth=1') + await page.getByRole('menuitem', { name: 'Email' }).last().click() await page.click('text=Add a value') await page.click('text=Select a column') @@ -61,11 +61,11 @@ test.describe.parallel('Google sheets integration', () => { await page.getByRole('button', { name: 'Row(s) to update' }).click() await page.getByRole('button', { name: 'Add filter rule' }).click() await page.click('text=Select a column') - await page.click('button >> text="Email"') + await page.getByRole('menuitem', { name: 'Email' }).click() await page.getByRole('button', { name: 'Select an operator' }).click() await page.getByRole('menuitem', { name: 'Equal to' }).click() await page.click('[aria-label="Insert a variable"]') - await page.click('button >> text="Email" >> nth=1') + await page.getByRole('menuitem', { name: 'Email' }).last().click() await page.getByRole('button', { name: 'Cells to update' }).click() await page.click('text=Add a value') @@ -106,11 +106,11 @@ test.describe.parallel('Google sheets integration', () => { await page.getByRole('button', { name: 'Select row(s)' }).click() await page.getByRole('button', { name: 'Add filter rule' }).click() await page.click('text=Select a column') - await page.click('button >> text="Email"') + await page.getByRole('menuitem', { name: 'Email' }).click() await page.getByRole('button', { name: 'Select an operator' }).click() await page.getByRole('menuitem', { name: 'Equal to' }).click() await page.click('[aria-label="Insert a variable"]') - await page.click('button >> text="Email" >> nth=1') + await page.getByRole('menuitem', { name: 'Email' }).last().click() await page.getByRole('button', { name: 'Add filter rule' }).click() await page.getByRole('button', { name: 'AND', exact: true }).click() diff --git a/apps/builder/src/features/blocks/logic/condition/condition.spec.ts b/apps/builder/src/features/blocks/logic/condition/condition.spec.ts index 0963c8fe86..a5989ba04f 100644 --- a/apps/builder/src/features/blocks/logic/condition/condition.spec.ts +++ b/apps/builder/src/features/blocks/logic/condition/condition.spec.ts @@ -20,7 +20,7 @@ test.describe('Condition block', () => { 'input[placeholder="Search for a variable"] >> nth=-1', 'Age' ) - await page.click('button:has-text("Age")') + await page.getByRole('menuitem', { name: 'Age' }).click() await page.click('button:has-text("Select an operator")') await page.click('button:has-text("Greater than")', { force: true }) await page.fill('input[placeholder="Type a number..."]', '80') @@ -31,7 +31,7 @@ test.describe('Condition block', () => { ':nth-match(input[placeholder="Search for a variable"], 2)', 'Age' ) - await page.click('button:has-text("Age")') + await page.getByRole('menuitem', { name: 'Age' }).click() await page.click('button:has-text("Select an operator")') await page.click('button:has-text("Less than")', { force: true }) await page.fill( @@ -44,7 +44,7 @@ test.describe('Condition block', () => { 'input[placeholder="Search for a variable"] >> nth=-1', 'Age' ) - await page.click('button:has-text("Age")') + await page.getByRole('menuitem', { name: 'Age' }).click() await page.click('button:has-text("Select an operator")') await page.click('button:has-text("Greater than")', { force: true }) await page.fill('input[placeholder="Type a number..."]', '20') diff --git a/apps/builder/src/features/blocks/logic/setVariable/setVariable.spec.ts b/apps/builder/src/features/blocks/logic/setVariable/setVariable.spec.ts index e2a63de662..40dcc6f2c7 100644 --- a/apps/builder/src/features/blocks/logic/setVariable/setVariable.spec.ts +++ b/apps/builder/src/features/blocks/logic/setVariable/setVariable.spec.ts @@ -23,10 +23,7 @@ test.describe('Set variable block', () => { await page.click('text=Click to edit... >> nth = 0') await page.fill('input[placeholder="Select a variable"] >> nth=-1', 'Total') await page.getByRole('menuitem', { name: 'Create Total' }).click() - await page - .getByTestId('code-editor') - .getByRole('textbox') - .fill('1000 * {{Num}}') + await page.locator('textarea').fill('1000 * {{Num}}') await page.click('text=Click to edit...', { force: true }) await expect(page.getByText('Save in results?')).toBeHidden() @@ -39,10 +36,7 @@ test.describe('Set variable block', () => { await expect( page.getByRole('group').nth(1).locator('.chakra-switch') ).not.toHaveAttribute('data-checked') - await page - .getByTestId('code-editor') - .getByRole('textbox') - .fill('Custom value') + await page.locator('textarea').fill('Custom value') await page.click('text=Click to edit...', { force: true }) await page.fill( @@ -50,10 +44,7 @@ test.describe('Set variable block', () => { 'Addition' ) await page.getByRole('menuitem', { name: 'Create Addition' }).click() - await page - .getByTestId('code-editor') - .getByRole('textbox') - .fill('1000 + {{Total}}') + await page.locator('textarea').fill('1000 + {{Total}}') await page.click('text=Test') await page @@ -94,14 +85,14 @@ test.describe('Set variable block', () => { await page.getByRole('button', { name: 'Test' }).click() await page.getByRole('button', { name: 'There is a bug 🐛' }).click() await page.getByTestId('textarea').fill('Hello!!') - await page.getByTestId('input').getByRole('button').click() + await page.getByLabel('Send').click() await page .locator('typebot-standard') .getByRole('button', { name: 'Restart' }) .click() await page.getByRole('button', { name: 'I have a question 💭' }).click() await page.getByTestId('textarea').fill('How are you?') - await page.getByTestId('input').getByRole('button').click() + await page.getByLabel('Send').click() await page.getByRole('button', { name: 'Transcription' }).click() await expect( diff --git a/apps/builder/src/features/blocks/logic/typebotLink/typebotLink.spec.ts b/apps/builder/src/features/blocks/logic/typebotLink/typebotLink.spec.ts index 366d79a02b..132b147d91 100644 --- a/apps/builder/src/features/blocks/logic/typebotLink/typebotLink.spec.ts +++ b/apps/builder/src/features/blocks/logic/typebotLink/typebotLink.spec.ts @@ -46,8 +46,8 @@ test('should be configurable', async ({ page }) => { await page.getByLabel('Clear').click() await page.click('text=Test') - await page.locator('typebot-standard').locator('input').fill('Hello there!') - await page.locator('typebot-standard').locator('input').press('Enter') + await page.getByPlaceholder('Type your answer...').fill('Hello there!') + await page.getByPlaceholder('Type your answer...').press('Enter') await expect( page.locator('typebot-standard').locator('text=Hello there!') ).toBeVisible() diff --git a/apps/builder/src/features/dashboard/components/DashboardHeader.tsx b/apps/builder/src/features/dashboard/components/DashboardHeader.tsx index ce78662f92..6f21a88b3a 100644 --- a/apps/builder/src/features/dashboard/components/DashboardHeader.tsx +++ b/apps/builder/src/features/dashboard/components/DashboardHeader.tsx @@ -9,6 +9,7 @@ import { useTranslate } from '@tolgee/react' import { useWorkspace } from '@/features/workspace/WorkspaceProvider' import { WorkspaceDropdown } from '@/features/workspace/components/WorkspaceDropdown' import { WorkspaceSettingsModal } from '@/features/workspace/components/WorkspaceSettingsModal' +import { ParentModalProvider } from '@/features/graph/providers/ParentModalProvider' export const DashboardHeader = () => { const { t } = useTranslate() @@ -38,12 +39,14 @@ export const DashboardHeader = () => { {user && workspace && !workspace.isPastDue && ( - + + + )} {!workspace?.isPastDue && (