feat(engine): ✨ Add Rating input
This commit is contained in:
@ -457,3 +457,9 @@ export const PlayIcon = (props: IconProps) => (
|
||||
<polygon points="5 3 19 12 5 21 5 3"></polygon>
|
||||
</Icon>
|
||||
)
|
||||
|
||||
export const StarIcon = (props: IconProps) => (
|
||||
<Icon viewBox="0 0 24 24" {...featherIconsBaseProps} {...props}>
|
||||
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
|
||||
</Icon>
|
||||
)
|
||||
|
@ -18,6 +18,7 @@ import {
|
||||
NumberIcon,
|
||||
PhoneIcon,
|
||||
SendEmailIcon,
|
||||
StarIcon,
|
||||
TextIcon,
|
||||
WebhookIcon,
|
||||
} from 'assets/icons'
|
||||
@ -65,6 +66,8 @@ export const StepIcon = ({ type, ...props }: StepIconProps) => {
|
||||
return <CheckSquareIcon color="orange.500" {...props} />
|
||||
case InputStepType.PAYMENT:
|
||||
return <CreditCardIcon color="orange.500" {...props} />
|
||||
case InputStepType.RATING:
|
||||
return <StarIcon color="orange.500" {...props} />
|
||||
case LogicStepType.SET_VARIABLE:
|
||||
return <EditIcon color="purple.500" {...props} />
|
||||
case LogicStepType.CONDITION:
|
||||
@ -91,7 +94,5 @@ export const StepIcon = ({ type, ...props }: StepIconProps) => {
|
||||
return <SendEmailIcon {...props} />
|
||||
case 'start':
|
||||
return <FlagIcon {...props} />
|
||||
default:
|
||||
return <></>
|
||||
}
|
||||
}
|
||||
|
@ -10,8 +10,10 @@ import React from 'react'
|
||||
|
||||
type Props = { type: StepType }
|
||||
|
||||
export const StepTypeLabel = ({ type }: Props) => {
|
||||
export const StepTypeLabel = ({ type }: Props): JSX.Element => {
|
||||
switch (type) {
|
||||
case 'start':
|
||||
return <Text>Start</Text>
|
||||
case BubbleStepType.TEXT:
|
||||
case InputStepType.TEXT:
|
||||
return <Text>Text</Text>
|
||||
@ -39,6 +41,8 @@ export const StepTypeLabel = ({ type }: Props) => {
|
||||
return <Text>Button</Text>
|
||||
case InputStepType.PAYMENT:
|
||||
return <Text>Payment</Text>
|
||||
case InputStepType.RATING:
|
||||
return <Text>Rating</Text>
|
||||
case LogicStepType.SET_VARIABLE:
|
||||
return <Text>Set variable</Text>
|
||||
case LogicStepType.CONDITION:
|
||||
@ -79,7 +83,5 @@ export const StepTypeLabel = ({ type }: Props) => {
|
||||
return <Text>Pabbly</Text>
|
||||
case IntegrationStepType.EMAIL:
|
||||
return <Text>Email</Text>
|
||||
default:
|
||||
return <></>
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ import {
|
||||
LogicStepType,
|
||||
Step,
|
||||
StepOptions,
|
||||
TextBubbleStep,
|
||||
StepWithOptions,
|
||||
Webhook,
|
||||
} from 'models'
|
||||
import { useRef } from 'react'
|
||||
@ -33,6 +33,7 @@ import { GoogleAnalyticsSettings } from './bodies/GoogleAnalyticsSettings'
|
||||
import { GoogleSheetsSettingsBody } from './bodies/GoogleSheetsSettingsBody'
|
||||
import { PaymentSettings } from './bodies/PaymentSettings'
|
||||
import { PhoneNumberSettingsBody } from './bodies/PhoneNumberSettingsBody'
|
||||
import { RatingInputSettings } from './bodies/RatingInputSettingsBody'
|
||||
import { RedirectSettings } from './bodies/RedirectSettings'
|
||||
import { SendEmailSettings } from './bodies/SendEmailSettings'
|
||||
import { SetVariableSettings } from './bodies/SetVariableSettings'
|
||||
@ -41,7 +42,7 @@ import { WebhookSettings } from './bodies/WebhookSettings'
|
||||
import { ZapierSettings } from './bodies/ZapierSettings'
|
||||
|
||||
type Props = {
|
||||
step: Exclude<Step, TextBubbleStep>
|
||||
step: StepWithOptions | ConditionStep
|
||||
webhook?: Webhook
|
||||
onExpandClick: () => void
|
||||
onStepChange: (updates: Partial<Step>) => void
|
||||
@ -87,10 +88,10 @@ export const StepSettings = ({
|
||||
step,
|
||||
onStepChange,
|
||||
}: {
|
||||
step: Step
|
||||
step: StepWithOptions | ConditionStep
|
||||
webhook?: Webhook
|
||||
onStepChange: (step: Partial<Step>) => void
|
||||
}) => {
|
||||
}): JSX.Element => {
|
||||
const handleOptionsChange = (options: StepOptions) => {
|
||||
onStepChange({ options } as Partial<Step>)
|
||||
}
|
||||
@ -164,6 +165,14 @@ export const StepSettings = ({
|
||||
/>
|
||||
)
|
||||
}
|
||||
case InputStepType.RATING: {
|
||||
return (
|
||||
<RatingInputSettings
|
||||
options={step.options}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case LogicStepType.SET_VARIABLE: {
|
||||
return (
|
||||
<SetVariableSettings
|
||||
@ -258,8 +267,5 @@ export const StepSettings = ({
|
||||
/>
|
||||
)
|
||||
}
|
||||
default: {
|
||||
return <></>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,149 @@
|
||||
import { FormLabel, Stack } from '@chakra-ui/react'
|
||||
import { DropdownList } from 'components/shared/DropdownList'
|
||||
import { SwitchWithLabel } from 'components/shared/SwitchWithLabel'
|
||||
import { Input } from 'components/shared/Textbox'
|
||||
import { VariableSearchInput } from 'components/shared/VariableSearchInput'
|
||||
import { RatingInputOptions, Variable } from 'models'
|
||||
import React from 'react'
|
||||
|
||||
type RatingInputSettingsProps = {
|
||||
options: RatingInputOptions
|
||||
onOptionsChange: (options: RatingInputOptions) => void
|
||||
}
|
||||
|
||||
export const RatingInputSettings = ({
|
||||
options,
|
||||
onOptionsChange,
|
||||
}: RatingInputSettingsProps) => {
|
||||
const handleLengthChange = (length: number) =>
|
||||
onOptionsChange({ ...options, length })
|
||||
|
||||
const handleTypeChange = (buttonType: 'Icons' | 'Numbers') =>
|
||||
onOptionsChange({ ...options, buttonType })
|
||||
|
||||
const handleCustomIconCheck = (isEnabled: boolean) =>
|
||||
onOptionsChange({
|
||||
...options,
|
||||
customIcon: { ...options.customIcon, isEnabled },
|
||||
})
|
||||
|
||||
const handleIconSvgChange = (svg: string) =>
|
||||
onOptionsChange({ ...options, customIcon: { ...options.customIcon, svg } })
|
||||
|
||||
const handleLeftLabelChange = (left: string) =>
|
||||
onOptionsChange({ ...options, labels: { ...options.labels, left } })
|
||||
|
||||
const handleMiddleLabelChange = (middle: string) =>
|
||||
onOptionsChange({ ...options, labels: { ...options.labels, middle } })
|
||||
|
||||
const handleRightLabelChange = (right: string) =>
|
||||
onOptionsChange({ ...options, labels: { ...options.labels, right } })
|
||||
|
||||
const handleButtonLabelChange = (button: string) =>
|
||||
onOptionsChange({ ...options, labels: { ...options.labels, button } })
|
||||
|
||||
const handleVariableChange = (variable?: Variable) =>
|
||||
onOptionsChange({ ...options, variableId: variable?.id })
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="button">
|
||||
Maximum:
|
||||
</FormLabel>
|
||||
<DropdownList
|
||||
onItemSelect={handleLengthChange}
|
||||
items={[3, 4, 5, 6, 7, 8, 9, 10]}
|
||||
currentItem={options.length}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="button">
|
||||
Type:
|
||||
</FormLabel>
|
||||
<DropdownList
|
||||
onItemSelect={handleTypeChange}
|
||||
items={['Icons', 'Numbers']}
|
||||
currentItem={options.buttonType}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
{options.buttonType === 'Icons' && (
|
||||
<SwitchWithLabel
|
||||
id="switch"
|
||||
label="Custom icon?"
|
||||
initialValue={options.customIcon.isEnabled}
|
||||
onCheckChange={handleCustomIconCheck}
|
||||
/>
|
||||
)}
|
||||
{options.buttonType === 'Icons' && options.customIcon.isEnabled && (
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="svg">
|
||||
Icon SVG:
|
||||
</FormLabel>
|
||||
<Input
|
||||
id="svg"
|
||||
defaultValue={options.customIcon.svg}
|
||||
onChange={handleIconSvgChange}
|
||||
placeholder="<svg>...</svg>"
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="button">
|
||||
1 label:
|
||||
</FormLabel>
|
||||
<Input
|
||||
id="button"
|
||||
defaultValue={options.labels.left}
|
||||
onChange={handleLeftLabelChange}
|
||||
placeholder="Not likely at all"
|
||||
/>
|
||||
</Stack>
|
||||
{options.length >= 4 && (
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="button">
|
||||
{Math.floor(options.length / 2)} label:
|
||||
</FormLabel>
|
||||
<Input
|
||||
id="button"
|
||||
defaultValue={options.labels.middle}
|
||||
onChange={handleMiddleLabelChange}
|
||||
placeholder="Neutral"
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="button">
|
||||
{options.length} label:
|
||||
</FormLabel>
|
||||
<Input
|
||||
id="button"
|
||||
defaultValue={options.labels.right}
|
||||
onChange={handleRightLabelChange}
|
||||
placeholder="Extremely likely"
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="button">
|
||||
Button label:
|
||||
</FormLabel>
|
||||
<Input
|
||||
id="button"
|
||||
defaultValue={options.labels.button}
|
||||
onChange={handleButtonLabelChange}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="variable">
|
||||
Save answer in a variable:
|
||||
</FormLabel>
|
||||
<VariableSearchInput
|
||||
initialVariableId={options.variableId}
|
||||
onSelectVariable={handleVariableChange}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -9,8 +9,10 @@ import React, { useEffect, useRef, useState } from 'react'
|
||||
import {
|
||||
BubbleStep,
|
||||
BubbleStepContent,
|
||||
ConditionStep,
|
||||
DraggableStep,
|
||||
Step,
|
||||
StepWithOptions,
|
||||
TextBubbleContent,
|
||||
TextBubbleStep,
|
||||
} from 'models'
|
||||
@ -210,11 +212,16 @@ export const StepNode = ({
|
||||
</Flex>
|
||||
</PopoverTrigger>
|
||||
{hasSettingsPopover(step) && (
|
||||
<SettingsPopoverContent
|
||||
step={step}
|
||||
onExpandClick={handleExpandClick}
|
||||
onStepChange={handleStepUpdate}
|
||||
/>
|
||||
<>
|
||||
<SettingsPopoverContent
|
||||
step={step}
|
||||
onExpandClick={handleExpandClick}
|
||||
onStepChange={handleStepUpdate}
|
||||
/>
|
||||
<SettingsModal isOpen={isModalOpen} onClose={handleModalClose}>
|
||||
<StepSettings step={step} onStepChange={handleStepUpdate} />
|
||||
</SettingsModal>
|
||||
</>
|
||||
)}
|
||||
{isMediaBubbleStep(step) && (
|
||||
<MediaBubblePopoverContent
|
||||
@ -222,17 +229,15 @@ export const StepNode = ({
|
||||
onContentChange={handleContentChange}
|
||||
/>
|
||||
)}
|
||||
<SettingsModal isOpen={isModalOpen} onClose={handleModalClose}>
|
||||
<StepSettings step={step} onStepChange={handleStepUpdate} />
|
||||
</SettingsModal>
|
||||
</Popover>
|
||||
)}
|
||||
</ContextMenu>
|
||||
)
|
||||
}
|
||||
|
||||
const hasSettingsPopover = (step: Step): step is Exclude<Step, BubbleStep> =>
|
||||
!isBubbleStep(step)
|
||||
const hasSettingsPopover = (
|
||||
step: Step
|
||||
): step is StepWithOptions | ConditionStep => !isBubbleStep(step)
|
||||
|
||||
const isMediaBubbleStep = (
|
||||
step: Step
|
||||
|
@ -22,6 +22,7 @@ import { ConfigureContent } from './contents/ConfigureContent'
|
||||
import { ImageBubbleContent } from './contents/ImageBubbleContent'
|
||||
import { PaymentInputContent } from './contents/PaymentInputContent'
|
||||
import { PlaceholderContent } from './contents/PlaceholderContent'
|
||||
import { RatingInputContent } from './contents/RatingInputContent'
|
||||
import { SendEmailContent } from './contents/SendEmailContent'
|
||||
import { TypebotLinkContent } from './contents/TypebotLinkContent'
|
||||
import { ProviderWebhookContent } from './contents/ZapierContent'
|
||||
@ -30,7 +31,7 @@ type Props = {
|
||||
step: Step | StartStep
|
||||
indices: StepIndices
|
||||
}
|
||||
export const StepNodeContent = ({ step, indices }: Props) => {
|
||||
export const StepNodeContent = ({ step, indices }: Props): JSX.Element => {
|
||||
if (isInputStep(step) && !isChoiceInput(step) && step.options.variableId) {
|
||||
return <WithVariableContent step={step} />
|
||||
}
|
||||
@ -72,6 +73,9 @@ export const StepNodeContent = ({ step, indices }: Props) => {
|
||||
case InputStepType.PAYMENT: {
|
||||
return <PaymentInputContent step={step} />
|
||||
}
|
||||
case InputStepType.RATING: {
|
||||
return <RatingInputContent step={step} />
|
||||
}
|
||||
case LogicStepType.SET_VARIABLE: {
|
||||
return <SetVariableContent step={step} />
|
||||
}
|
||||
@ -144,8 +148,5 @@ export const StepNodeContent = ({ step, indices }: Props) => {
|
||||
case 'start': {
|
||||
return <Text>Start</Text>
|
||||
}
|
||||
default: {
|
||||
return <Text>No input</Text>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,12 @@
|
||||
import { Text } from '@chakra-ui/react'
|
||||
import { RatingInputStep } from 'models'
|
||||
|
||||
type Props = {
|
||||
step: RatingInputStep
|
||||
}
|
||||
|
||||
export const RatingInputContent = ({ step }: Props) => (
|
||||
<Text noOfLines={0} pr="6">
|
||||
Rate from 1 to {step.options.length}
|
||||
</Text>
|
||||
)
|
65
apps/builder/playwright/tests/inputs/rating.spec.ts
Normal file
65
apps/builder/playwright/tests/inputs/rating.spec.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import test, { expect } from '@playwright/test'
|
||||
import {
|
||||
createTypebots,
|
||||
parseDefaultBlockWithStep,
|
||||
} from '../../services/database'
|
||||
import { defaultRatingInputOptions, InputStepType } from 'models'
|
||||
import { typebotViewer } from '../../services/selectorUtils'
|
||||
import cuid from 'cuid'
|
||||
|
||||
const boxSvg = `<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path>
|
||||
<polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline>
|
||||
<line x1="12" y1="22.08" x2="12" y2="12"></line>
|
||||
</svg>`
|
||||
|
||||
test('options should work', async ({ page }) => {
|
||||
const typebotId = cuid()
|
||||
await createTypebots([
|
||||
{
|
||||
id: typebotId,
|
||||
...parseDefaultBlockWithStep({
|
||||
type: InputStepType.RATING,
|
||||
options: defaultRatingInputOptions,
|
||||
}),
|
||||
},
|
||||
])
|
||||
|
||||
await page.goto(`/typebots/${typebotId}/edit`)
|
||||
|
||||
await page.click('text=Preview')
|
||||
await expect(typebotViewer(page).locator(`text=Send`)).toBeHidden()
|
||||
await typebotViewer(page).locator(`text=8`).click()
|
||||
await typebotViewer(page).locator(`text=Send`).click()
|
||||
await expect(typebotViewer(page).locator(`text=8`)).toBeVisible()
|
||||
await page.click('text=Rate from 1 to 10')
|
||||
await page.click('text=10')
|
||||
await page.click('text=5')
|
||||
await page.click('text=Numbers')
|
||||
await page.click('text=Icons')
|
||||
await page.click('text="Custom icon?"')
|
||||
await page.fill('[placeholder="<svg>...</svg>"]', boxSvg)
|
||||
await page.fill('[placeholder="Not likely at all"]', 'Not likely at all')
|
||||
await page.fill('[placeholder="Neutral"]', 'Neutral')
|
||||
await page.fill('[placeholder="Extremely likely"]', 'Extremely likely')
|
||||
await page.click('text="Restart"')
|
||||
await expect(typebotViewer(page).locator(`text=8`)).toBeHidden()
|
||||
await expect(typebotViewer(page).locator(`text=4`)).toBeHidden()
|
||||
await expect(
|
||||
typebotViewer(page).locator(`text=Not likely at all`)
|
||||
).toBeVisible()
|
||||
await expect(typebotViewer(page).locator(`text=Neutral`)).toBeVisible()
|
||||
await expect(
|
||||
typebotViewer(page).locator(`text=Extremely likely`)
|
||||
).toBeVisible()
|
||||
await typebotViewer(page).locator(`svg >> nth=4`).click()
|
||||
await typebotViewer(page).locator(`text=Send`).click()
|
||||
await expect(typebotViewer(page).locator(`text=5`)).toBeVisible()
|
||||
})
|
@ -38,6 +38,7 @@ import {
|
||||
ChoiceInputStep,
|
||||
ConditionStep,
|
||||
defaultPaymentInputOptions,
|
||||
defaultRatingInputOptions,
|
||||
} from 'models'
|
||||
import { Typebot } from 'models'
|
||||
import useSWR from 'swr'
|
||||
@ -326,6 +327,8 @@ const parseDefaultStepOptions = (type: StepWithOptionsType): StepOptions => {
|
||||
return defaultChoiceInputOptions
|
||||
case InputStepType.PAYMENT:
|
||||
return defaultPaymentInputOptions
|
||||
case InputStepType.RATING:
|
||||
return defaultRatingInputOptions
|
||||
case LogicStepType.SET_VARIABLE:
|
||||
return defaultSetVariablesOptions
|
||||
case LogicStepType.REDIRECT:
|
||||
|
@ -23,7 +23,7 @@ It means you can apply operations on existing variables:
|
||||
But also set the variable to the current date for example:
|
||||
|
||||
```js
|
||||
new Date()
|
||||
new Date().toISOString()
|
||||
```
|
||||
|
||||
## Random ID
|
||||
|
@ -198,3 +198,23 @@ textarea {
|
||||
.ping span {
|
||||
background-color: var(--typebot-button-bg-color);
|
||||
}
|
||||
|
||||
.rating-icon-container svg {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
stroke: var(--typebot-button-bg-color);
|
||||
fill: var(--typebot-host-bubble-bg-color);
|
||||
transition: fill 100ms ease-out;
|
||||
}
|
||||
|
||||
.rating-icon-container.selected svg {
|
||||
fill: var(--typebot-button-bg-color);
|
||||
}
|
||||
|
||||
.rating-icon-container:hover svg {
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
|
||||
.rating-icon-container:active svg {
|
||||
filter: brightness(0.75);
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { parseVariables } from '../../../services/variable'
|
||||
import { isInputValid } from 'services/inputs'
|
||||
import { PaymentForm } from './inputs/PaymentForm'
|
||||
import { RatingForm } from './inputs/RatingForm'
|
||||
|
||||
export const InputChatStep = ({
|
||||
step,
|
||||
@ -115,5 +116,7 @@ const Input = ({
|
||||
onSuccess={() => onSubmit(step.options.labels.success ?? 'Success')}
|
||||
/>
|
||||
)
|
||||
case InputStepType.RATING:
|
||||
return <RatingForm step={step} onSubmit={onSubmit} />
|
||||
}
|
||||
}
|
||||
|
@ -69,7 +69,7 @@ export const ChoiceForm = ({ step, onSubmit }: ChoiceFormProps) => {
|
||||
</div>
|
||||
<div className="flex">
|
||||
{selectedIndices.length > 0 && (
|
||||
<SendButton label={step.options?.buttonLabel ?? 'Send'} />
|
||||
<SendButton label={step.options?.buttonLabel ?? 'Send'} disableIcon />
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
|
@ -0,0 +1,108 @@
|
||||
import { RatingInputOptions, RatingInputStep } from 'models'
|
||||
import React, { FormEvent, useRef, useState } from 'react'
|
||||
import { isDefined, isEmpty, isNotDefined } from 'utils'
|
||||
import { SendButton } from './SendButton'
|
||||
|
||||
type Props = {
|
||||
step: RatingInputStep
|
||||
onSubmit: (value: string) => void
|
||||
}
|
||||
|
||||
export const RatingForm = ({ step, onSubmit }: Props) => {
|
||||
const [rating, setRating] = useState<number>()
|
||||
const scaleElement = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
const handleSubmit = (e: FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (isNotDefined(rating)) return
|
||||
onSubmit(rating.toString())
|
||||
}
|
||||
|
||||
const handleClick = (rating: number) => setRating(rating)
|
||||
|
||||
return (
|
||||
<form className="flex flex-col" onSubmit={handleSubmit}>
|
||||
<div className="flex flex-col" ref={scaleElement}>
|
||||
<div className="flex">
|
||||
{Array.from(Array(step.options.length)).map((_, idx) => (
|
||||
<RatingButton
|
||||
{...step.options}
|
||||
key={idx}
|
||||
rating={rating}
|
||||
idx={idx + 1}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between mr-2 mb-2">
|
||||
{<span className="text-sm w-full ">{step.options.labels.left}</span>}
|
||||
{!isEmpty(step.options.labels.middle) && (
|
||||
<span className="text-sm w-full text-center">
|
||||
{step.options.labels.middle}
|
||||
</span>
|
||||
)}
|
||||
{
|
||||
<span className="text-sm w-full text-right">
|
||||
{step.options.labels.right}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end mr-2">
|
||||
{isDefined(rating) && (
|
||||
<SendButton
|
||||
label={step.options?.labels.button ?? 'Send'}
|
||||
disableIcon
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
const RatingButton = ({
|
||||
rating,
|
||||
idx,
|
||||
buttonType,
|
||||
customIcon,
|
||||
onClick,
|
||||
}: Pick<RatingInputOptions, 'buttonType' | 'customIcon'> & {
|
||||
rating: number | undefined
|
||||
idx: number
|
||||
onClick: (idx: number) => void
|
||||
}) => {
|
||||
if (buttonType === 'Numbers')
|
||||
return (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
onClick(idx)
|
||||
}}
|
||||
className={
|
||||
'py-2 px-4 mr-2 mb-2 text-left font-semibold rounded-md transition-all filter hover:brightness-90 active:brightness-75 duration-100 focus:outline-none typebot-button ' +
|
||||
(isDefined(rating) && idx <= rating ? '' : 'selectable')
|
||||
}
|
||||
>
|
||||
{idx}
|
||||
</button>
|
||||
)
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
'flex justify-center items-center rating-icon-container cursor-pointer mr-2 mb-2 ' +
|
||||
(isDefined(rating) && idx <= rating ? 'selected' : '')
|
||||
}
|
||||
onClick={() => onClick(idx)}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html:
|
||||
customIcon.isEnabled && !isEmpty(customIcon.svg)
|
||||
? customIcon.svg
|
||||
: defaultIcon,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const defaultIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-star"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon></svg>`
|
@ -7,3 +7,4 @@ export * from './date'
|
||||
export * from './choice'
|
||||
export * from './payment'
|
||||
export * from './phone'
|
||||
export * from './rating'
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
phoneNumberInputOptionsSchema,
|
||||
phoneNumberInputStepSchema,
|
||||
} from './phone'
|
||||
import { ratingInputOptionsSchema, ratingInputStepSchema } from './rating'
|
||||
import { textInputOptionsSchema, textInputSchema } from './text'
|
||||
import { urlInputOptionsSchema, urlInputSchema } from './url'
|
||||
|
||||
@ -22,6 +23,7 @@ export const inputStepOptionsSchema = textInputOptionsSchema
|
||||
.or(phoneNumberInputOptionsSchema)
|
||||
.or(dateInputOptionsSchema)
|
||||
.or(paymentInputOptionsSchema)
|
||||
.or(ratingInputOptionsSchema)
|
||||
|
||||
export const inputStepSchema = textInputSchema
|
||||
.or(numberInputSchema)
|
||||
@ -31,6 +33,7 @@ export const inputStepSchema = textInputSchema
|
||||
.or(phoneNumberInputStepSchema)
|
||||
.or(choiceInputSchema)
|
||||
.or(paymentInputSchema)
|
||||
.or(ratingInputStepSchema)
|
||||
|
||||
export type InputStep = z.infer<typeof inputStepSchema>
|
||||
export type InputStepOptions = z.infer<typeof inputStepOptionsSchema>
|
||||
|
41
packages/models/src/typebot/steps/input/rating.ts
Normal file
41
packages/models/src/typebot/steps/input/rating.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
defaultButtonLabel,
|
||||
InputStepType,
|
||||
optionBaseSchema,
|
||||
stepBaseSchema,
|
||||
} from '../shared'
|
||||
|
||||
export const defaultRatingInputOptions: RatingInputOptions = {
|
||||
buttonType: 'Numbers',
|
||||
length: 10,
|
||||
labels: { button: defaultButtonLabel },
|
||||
customIcon: { isEnabled: false },
|
||||
}
|
||||
|
||||
export const ratingInputOptionsSchema = optionBaseSchema.and(
|
||||
z.object({
|
||||
buttonType: z.literal('Icons').or(z.literal('Numbers')),
|
||||
length: z.number(),
|
||||
labels: z.object({
|
||||
left: z.string().optional(),
|
||||
right: z.string().optional(),
|
||||
middle: z.string().optional(),
|
||||
button: z.string(),
|
||||
}),
|
||||
customIcon: z.object({
|
||||
isEnabled: z.boolean(),
|
||||
svg: z.string().optional(),
|
||||
}),
|
||||
})
|
||||
)
|
||||
|
||||
export const ratingInputStepSchema = stepBaseSchema.and(
|
||||
z.object({
|
||||
type: z.literal(InputStepType.RATING),
|
||||
options: ratingInputOptionsSchema,
|
||||
})
|
||||
)
|
||||
|
||||
export type RatingInputStep = z.infer<typeof ratingInputStepSchema>
|
||||
export type RatingInputOptions = z.infer<typeof ratingInputOptionsSchema>
|
@ -39,6 +39,7 @@ export enum InputStepType {
|
||||
PHONE = 'phone number input',
|
||||
CHOICE = 'choice input',
|
||||
PAYMENT = 'payment input',
|
||||
RATING = 'rating input',
|
||||
}
|
||||
|
||||
export enum LogicStepType {
|
||||
|
Reference in New Issue
Block a user