feat(bot): ⚡️ Add required option on file upload input
This commit is contained in:
@ -17,19 +17,27 @@ export const FileInputSettings = ({ options, onOptionsChange }: Props) => {
|
|||||||
onOptionsChange({ ...options, labels: { ...options.labels, button } })
|
onOptionsChange({ ...options, labels: { ...options.labels, button } })
|
||||||
const handlePlaceholderLabelChange = (placeholder: string) =>
|
const handlePlaceholderLabelChange = (placeholder: string) =>
|
||||||
onOptionsChange({ ...options, labels: { ...options.labels, placeholder } })
|
onOptionsChange({ ...options, labels: { ...options.labels, placeholder } })
|
||||||
const handleLongChange = (isMultipleAllowed: boolean) =>
|
const handleMultipleFilesChange = (isMultipleAllowed: boolean) =>
|
||||||
onOptionsChange({ ...options, isMultipleAllowed })
|
onOptionsChange({ ...options, isMultipleAllowed })
|
||||||
const handleVariableChange = (variable?: Variable) =>
|
const handleVariableChange = (variable?: Variable) =>
|
||||||
onOptionsChange({ ...options, variableId: variable?.id })
|
onOptionsChange({ ...options, variableId: variable?.id })
|
||||||
const handleSizeLimitChange = (sizeLimit?: number) =>
|
const handleSizeLimitChange = (sizeLimit?: number) =>
|
||||||
onOptionsChange({ ...options, sizeLimit })
|
onOptionsChange({ ...options, sizeLimit })
|
||||||
|
const handleRequiredChange = (isRequired: boolean) =>
|
||||||
|
onOptionsChange({ ...options, isRequired })
|
||||||
return (
|
return (
|
||||||
<Stack spacing={4}>
|
<Stack spacing={4}>
|
||||||
|
<SwitchWithLabel
|
||||||
|
id="required"
|
||||||
|
label="Required?"
|
||||||
|
initialValue={options.isRequired ?? true}
|
||||||
|
onCheckChange={handleRequiredChange}
|
||||||
|
/>
|
||||||
<SwitchWithLabel
|
<SwitchWithLabel
|
||||||
id="switch"
|
id="switch"
|
||||||
label="Allow multiple files?"
|
label="Allow multiple files?"
|
||||||
initialValue={options.isMultipleAllowed}
|
initialValue={options.isMultipleAllowed}
|
||||||
onCheckChange={handleLongChange}
|
onCheckChange={handleMultipleFilesChange}
|
||||||
/>
|
/>
|
||||||
<Stack>
|
<Stack>
|
||||||
<FormLabel mb="0" htmlFor="limit">
|
<FormLabel mb="0" htmlFor="limit">
|
||||||
|
@ -29,16 +29,19 @@ test('options should work', async ({ page }) => {
|
|||||||
await expect(
|
await expect(
|
||||||
typebotViewer(page).locator(`text=Click to upload`)
|
typebotViewer(page).locator(`text=Click to upload`)
|
||||||
).toBeVisible()
|
).toBeVisible()
|
||||||
|
await expect(typebotViewer(page).locator(`text="Skip"`)).toBeHidden()
|
||||||
await typebotViewer(page)
|
await typebotViewer(page)
|
||||||
.locator(`input[type="file"]`)
|
.locator(`input[type="file"]`)
|
||||||
.setInputFiles([path.join(__dirname, '../../fixtures/avatar.jpg')])
|
.setInputFiles([path.join(__dirname, '../../fixtures/avatar.jpg')])
|
||||||
await expect(typebotViewer(page).locator(`text=File uploaded`)).toBeVisible()
|
await expect(typebotViewer(page).locator(`text=File uploaded`)).toBeVisible()
|
||||||
await page.click('text="Collect file"')
|
await page.click('text="Collect file"')
|
||||||
|
await page.click('text="Required?"')
|
||||||
await page.click('text="Allow multiple files?"')
|
await page.click('text="Allow multiple files?"')
|
||||||
await page.fill('div[contenteditable=true]', '<strong>Upload now!!</strong>')
|
await page.fill('div[contenteditable=true]', '<strong>Upload now!!</strong>')
|
||||||
await page.fill('[value="Upload"]', 'Go')
|
await page.fill('[value="Upload"]', 'Go')
|
||||||
await page.fill('input[value="10"]', '20')
|
await page.fill('input[value="10"]', '20')
|
||||||
await page.click('text="Restart"')
|
await page.click('text="Restart"')
|
||||||
|
await expect(typebotViewer(page).locator(`text="Skip"`)).toBeVisible()
|
||||||
await expect(typebotViewer(page).locator(`text="Upload now!!"`)).toBeVisible()
|
await expect(typebotViewer(page).locator(`text="Upload now!!"`)).toBeVisible()
|
||||||
await typebotViewer(page)
|
await typebotViewer(page)
|
||||||
.locator(`input[type="file"]`)
|
.locator(`input[type="file"]`)
|
||||||
|
@ -5,9 +5,8 @@ import { parse } from 'papaparse'
|
|||||||
import { typebotViewer } from '../services/selectorUtils'
|
import { typebotViewer } from '../services/selectorUtils'
|
||||||
import { importTypebotInDatabase } from '../services/database'
|
import { importTypebotInDatabase } from '../services/database'
|
||||||
import { readFileSync } from 'fs'
|
import { readFileSync } from 'fs'
|
||||||
import { isDefined } from 'utils'
|
|
||||||
|
|
||||||
test('should work as expected', async ({ page, browser }) => {
|
test('should work as expected', async ({ page }) => {
|
||||||
const typebotId = cuid()
|
const typebotId = cuid()
|
||||||
await importTypebotInDatabase(
|
await importTypebotInDatabase(
|
||||||
path.join(__dirname, '../fixtures/typebots/fileUpload.json'),
|
path.join(__dirname, '../fixtures/typebots/fileUpload.json'),
|
||||||
|
@ -24,6 +24,7 @@ export const InputChatBlock = ({
|
|||||||
hasAvatar,
|
hasAvatar,
|
||||||
hasGuestAvatar,
|
hasGuestAvatar,
|
||||||
onTransitionEnd,
|
onTransitionEnd,
|
||||||
|
onSkip,
|
||||||
}: {
|
}: {
|
||||||
block: InputBlock
|
block: InputBlock
|
||||||
hasGuestAvatar: boolean
|
hasGuestAvatar: boolean
|
||||||
@ -32,6 +33,7 @@ export const InputChatBlock = ({
|
|||||||
answerContent?: InputSubmitContent,
|
answerContent?: InputSubmitContent,
|
||||||
isRetry?: boolean
|
isRetry?: boolean
|
||||||
) => void
|
) => void
|
||||||
|
onSkip: () => void
|
||||||
}) => {
|
}) => {
|
||||||
const { typebot } = useTypebot()
|
const { typebot } = useTypebot()
|
||||||
const { addAnswer } = useAnswers()
|
const { addAnswer } = useAnswers()
|
||||||
@ -84,6 +86,7 @@ export const InputChatBlock = ({
|
|||||||
<Input
|
<Input
|
||||||
block={block}
|
block={block}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
|
onSkip={onSkip}
|
||||||
defaultValue={defaultValue?.toString()}
|
defaultValue={defaultValue?.toString()}
|
||||||
hasGuestAvatar={hasGuestAvatar}
|
hasGuestAvatar={hasGuestAvatar}
|
||||||
/>
|
/>
|
||||||
@ -94,11 +97,13 @@ export const InputChatBlock = ({
|
|||||||
const Input = ({
|
const Input = ({
|
||||||
block,
|
block,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
|
onSkip,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
hasGuestAvatar,
|
hasGuestAvatar,
|
||||||
}: {
|
}: {
|
||||||
block: InputBlock
|
block: InputBlock
|
||||||
onSubmit: (value: InputSubmitContent) => void
|
onSubmit: (value: InputSubmitContent) => void
|
||||||
|
onSkip: () => void
|
||||||
defaultValue?: string
|
defaultValue?: string
|
||||||
hasGuestAvatar: boolean
|
hasGuestAvatar: boolean
|
||||||
}) => {
|
}) => {
|
||||||
@ -132,6 +137,8 @@ const Input = ({
|
|||||||
case InputBlockType.RATING:
|
case InputBlockType.RATING:
|
||||||
return <RatingForm block={block} onSubmit={onSubmit} />
|
return <RatingForm block={block} onSubmit={onSubmit} />
|
||||||
case InputBlockType.FILE:
|
case InputBlockType.FILE:
|
||||||
return <FileUploadForm block={block} onSubmit={onSubmit} />
|
return (
|
||||||
|
<FileUploadForm block={block} onSubmit={onSubmit} onSkip={onSkip} />
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,14 +9,16 @@ import { SendButton, Spinner } from './SendButton'
|
|||||||
type Props = {
|
type Props = {
|
||||||
block: FileInputBlock
|
block: FileInputBlock
|
||||||
onSubmit: (url: InputSubmitContent) => void
|
onSubmit: (url: InputSubmitContent) => void
|
||||||
|
onSkip: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FileUploadForm = ({
|
export const FileUploadForm = ({
|
||||||
block: {
|
block: {
|
||||||
id,
|
id,
|
||||||
options: { isMultipleAllowed, labels, sizeLimit },
|
options: { isMultipleAllowed, labels, sizeLimit, isRequired },
|
||||||
},
|
},
|
||||||
onSubmit,
|
onSubmit,
|
||||||
|
onSkip,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const {
|
const {
|
||||||
isPreview,
|
isPreview,
|
||||||
@ -169,6 +171,18 @@ export const FileUploadForm = ({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</label>
|
</label>
|
||||||
|
{selectedFiles.length === 0 && isRequired === false && (
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<button
|
||||||
|
className={
|
||||||
|
'py-2 px-4 justify-center font-semibold rounded-md text-white focus:outline-none flex items-center disabled:opacity-50 disabled:cursor-not-allowed disabled:brightness-100 transition-all filter hover:brightness-90 active:brightness-75 typebot-button '
|
||||||
|
}
|
||||||
|
onClick={onSkip}
|
||||||
|
>
|
||||||
|
Skip
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{isMultipleAllowed && selectedFiles.length > 0 && !isUploading && (
|
{isMultipleAllowed && selectedFiles.length > 0 && !isUploading && (
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
|
@ -243,6 +243,8 @@ const ChatChunks = ({
|
|||||||
keepShowingHostAvatar,
|
keepShowingHostAvatar,
|
||||||
onDisplayNextBlock,
|
onDisplayNextBlock,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
const [isSkipped, setIsSkipped] = useState(false)
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const avatarSideContainerRef = useRef<any>()
|
const avatarSideContainerRef = useRef<any>()
|
||||||
|
|
||||||
@ -250,6 +252,11 @@ const ChatChunks = ({
|
|||||||
refreshTopOffset()
|
refreshTopOffset()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const skipInput = () => {
|
||||||
|
onDisplayNextBlock()
|
||||||
|
setIsSkipped(true)
|
||||||
|
}
|
||||||
|
|
||||||
const refreshTopOffset = () =>
|
const refreshTopOffset = () =>
|
||||||
avatarSideContainerRef.current?.refreshTopOffset()
|
avatarSideContainerRef.current?.refreshTopOffset()
|
||||||
|
|
||||||
@ -260,7 +267,9 @@ const ChatChunks = ({
|
|||||||
<AvatarSideContainer
|
<AvatarSideContainer
|
||||||
ref={avatarSideContainerRef}
|
ref={avatarSideContainerRef}
|
||||||
hostAvatarSrc={hostAvatar.src}
|
hostAvatarSrc={hostAvatar.src}
|
||||||
keepShowing={keepShowingHostAvatar || isDefined(input)}
|
keepShowing={
|
||||||
|
(keepShowingHostAvatar || isDefined(input)) && !isSkipped
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
@ -287,21 +296,24 @@ const ChatChunks = ({
|
|||||||
</TransitionGroup>
|
</TransitionGroup>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<CSSTransition
|
{!isSkipped && (
|
||||||
classNames="bubble"
|
<CSSTransition
|
||||||
timeout={500}
|
classNames="bubble"
|
||||||
unmountOnExit
|
timeout={500}
|
||||||
in={isDefined(input)}
|
unmountOnExit
|
||||||
>
|
in={isDefined(input)}
|
||||||
{input && (
|
>
|
||||||
<InputChatBlock
|
{input && (
|
||||||
block={input}
|
<InputChatBlock
|
||||||
onTransitionEnd={onDisplayNextBlock}
|
block={input}
|
||||||
hasAvatar={hostAvatar.isEnabled}
|
onTransitionEnd={onDisplayNextBlock}
|
||||||
hasGuestAvatar={hasGuestAvatar}
|
onSkip={skipInput}
|
||||||
/>
|
hasAvatar={hostAvatar.isEnabled}
|
||||||
)}
|
hasGuestAvatar={hasGuestAvatar}
|
||||||
</CSSTransition>
|
/>
|
||||||
|
)}
|
||||||
|
</CSSTransition>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ import { InputBlockType, optionBaseSchema, blockBaseSchema } from '../shared'
|
|||||||
|
|
||||||
export const fileInputOptionsSchema = optionBaseSchema.and(
|
export const fileInputOptionsSchema = optionBaseSchema.and(
|
||||||
z.object({
|
z.object({
|
||||||
|
isRequired: z.boolean().optional(),
|
||||||
isMultipleAllowed: z.boolean(),
|
isMultipleAllowed: z.boolean(),
|
||||||
labels: z.object({
|
labels: z.object({
|
||||||
placeholder: z.string(),
|
placeholder: z.string(),
|
||||||
@ -20,6 +21,7 @@ export const fileInputStepSchema = blockBaseSchema.and(
|
|||||||
)
|
)
|
||||||
|
|
||||||
export const defaultFileInputOptions: FileInputOptions = {
|
export const defaultFileInputOptions: FileInputOptions = {
|
||||||
|
isRequired: true,
|
||||||
isMultipleAllowed: false,
|
isMultipleAllowed: false,
|
||||||
labels: {
|
labels: {
|
||||||
placeholder: `<strong>
|
placeholder: `<strong>
|
||||||
|
Reference in New Issue
Block a user