2
0

(dateInput) Add format option and improve parsing

Use date-fns for custom format and make sure dates are timezone independants

Closes #533, closes #592
This commit is contained in:
Baptiste Arnaud
2023-09-05 10:34:56 +02:00
parent 111fb323b1
commit 9e8fa124b5
14 changed files with 100 additions and 86 deletions

View File

@@ -44,7 +44,6 @@ export const UserProvider = ({ children }: { children: ReactNode }) => {
| 'dark' | 'dark'
| null | null
if (currentColorScheme === user.preferredAppAppearance) return if (currentColorScheme === user.preferredAppAppearance) return
console.log('SET')
setColorMode(user.preferredAppAppearance) setColorMode(user.preferredAppAppearance)
}, [setColorMode, user?.preferredAppAppearance]) }, [setColorMode, user?.preferredAppAppearance])

View File

@@ -1,3 +1,4 @@
import { SwitchWithRelatedSettings } from '@/components/SwitchWithRelatedSettings'
import { TextInput } from '@/components/inputs' import { TextInput } from '@/components/inputs'
import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel' import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
import { VariableSearchInput } from '@/components/inputs/VariableSearchInput' import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
@@ -11,49 +12,57 @@ type Props = {
} }
export const DateInputSettings = ({ options, onOptionsChange }: Props) => { export const DateInputSettings = ({ options, onOptionsChange }: Props) => {
const handleFromChange = (from: string) => const updateFromLabel = (from: string) =>
onOptionsChange({ ...options, labels: { ...options?.labels, from } }) onOptionsChange({ ...options, labels: { ...options?.labels, from } })
const handleToChange = (to: string) => const updateToLabel = (to: string) =>
onOptionsChange({ ...options, labels: { ...options?.labels, to } }) onOptionsChange({ ...options, labels: { ...options?.labels, to } })
const handleButtonLabelChange = (button: string) => const updateButtonLabel = (button: string) =>
onOptionsChange({ ...options, labels: { ...options?.labels, button } }) onOptionsChange({ ...options, labels: { ...options?.labels, button } })
const handleIsRangeChange = (isRange: boolean) => const updateIsRange = (isRange: boolean) =>
onOptionsChange({ ...options, isRange }) onOptionsChange({ ...options, isRange })
const handleHasTimeChange = (hasTime: boolean) => const updateHasTime = (hasTime: boolean) =>
onOptionsChange({ ...options, hasTime }) onOptionsChange({ ...options, hasTime })
const handleVariableChange = (variable?: Variable) => const updateVariable = (variable?: Variable) =>
onOptionsChange({ ...options, variableId: variable?.id }) onOptionsChange({ ...options, variableId: variable?.id })
const updateFormat = (format: string) => {
if (format === '') return onOptionsChange({ ...options, format: undefined })
onOptionsChange({ ...options, format })
}
return ( return (
<Stack spacing={4}> <Stack spacing={4}>
<SwitchWithLabel <SwitchWithRelatedSettings
label="Is range?" label="Is range?"
initialValue={options.isRange} initialValue={options.isRange}
onCheckChange={handleIsRangeChange} onCheckChange={updateIsRange}
/> >
<TextInput
label="From label:"
defaultValue={options.labels.from}
onChange={updateFromLabel}
/>
<TextInput
label="To label:"
defaultValue={options.labels.to}
onChange={updateToLabel}
/>
</SwitchWithRelatedSettings>
<SwitchWithLabel <SwitchWithLabel
label="With time?" label="With time?"
initialValue={options.hasTime} initialValue={options.hasTime}
onCheckChange={handleHasTimeChange} onCheckChange={updateHasTime}
/> />
{options.isRange && (
<>
<TextInput
label="From label:"
defaultValue={options.labels.from}
onChange={handleFromChange}
/>
<TextInput
label="To label:"
defaultValue={options.labels.to}
onChange={handleToChange}
/>
</>
)}
<TextInput <TextInput
label="Button label:" label="Button label:"
defaultValue={options.labels.button} defaultValue={options.labels.button}
onChange={handleButtonLabelChange} onChange={updateButtonLabel}
/>
<TextInput
label="Format:"
defaultValue={options.format}
moreInfoTooltip="Popular formats: dd/MM/yyyy, MM/dd/yy, yyyy-MM-dd"
placeholder={options.hasTime ? 'dd/MM/yyyy HH:mm' : 'dd/MM/yyyy'}
onChange={updateFormat}
/> />
<Stack> <Stack>
<FormLabel mb="0" htmlFor="variable"> <FormLabel mb="0" htmlFor="variable">
@@ -61,7 +70,7 @@ export const DateInputSettings = ({ options, onOptionsChange }: Props) => {
</FormLabel> </FormLabel>
<VariableSearchInput <VariableSearchInput
initialVariableId={options.variableId} initialVariableId={options.variableId}
onSelectVariable={handleVariableChange} onSelectVariable={updateVariable}
/> />
</Stack> </Stack>
</Stack> </Stack>

View File

@@ -49,7 +49,17 @@ test.describe('Date input block', () => {
await page.locator('[data-testid="to-date"]').fill('2022-01-01T09:00') await page.locator('[data-testid="to-date"]').fill('2022-01-01T09:00')
await page.getByRole('button', { name: 'Go' }).click() await page.getByRole('button', { name: 'Go' }).click()
await expect( await expect(
page.locator('text="01/01/2021, 11:00 AM to 01/01/2022, 09:00 AM"') page.locator('text="01/01/2021 11:00 to 01/01/2022 09:00"')
).toBeVisible()
await page.click(`text=Pick a date...`)
await page.getByPlaceholder('dd/MM/yyyy HH:mm').fill('dd.MM HH:mm')
await page.click('text=Restart')
await page.locator('[data-testid="from-date"]').fill('2023-01-01T11:00')
await page.locator('[data-testid="to-date"]').fill('2023-02-01T09:00')
await page.getByRole('button', { name: 'Go' }).click()
await expect(
page.locator('text="01.01 11:00 to 01.02 09:00"')
).toBeVisible() ).toBeVisible()
}) })
}) })

View File

@@ -35,3 +35,17 @@ The input will use the native date picker depending on the device and browser us
style={{ maxWidth: '500px' }} style={{ maxWidth: '500px' }}
alt="Date native picker" alt="Date native picker"
/> />
## Format
The `Format` setting lets you customize the picked date format. Under the hood, it is done using the [date-fns](https://date-fns.org/) library. You can use any of the [formatting tokens](https://date-fns.org/docs/format) supported by the library.
Here are some examples:
```text
yyyy-MM-dd
yyyy-MM-dd HH:mm:ss
dd/MM/yy
dd/MM/yyyy HH:mm:ss
d.MM.yy
```

View File

@@ -16,11 +16,12 @@
"@trpc/server": "10.34.0", "@trpc/server": "10.34.0",
"@typebot.io/nextjs": "workspace:*", "@typebot.io/nextjs": "workspace:*",
"@typebot.io/prisma": "workspace:*", "@typebot.io/prisma": "workspace:*",
"ai": "2.1.32",
"@udecode/plate-common": "^21.1.5", "@udecode/plate-common": "^21.1.5",
"ai": "2.1.32",
"bot-engine": "workspace:*", "bot-engine": "workspace:*",
"chrono-node": "^2.6.4", "chrono-node": "2.6.6",
"cors": "2.8.5", "cors": "2.8.5",
"date-fns": "^2.30.0",
"eventsource-parser": "^1.0.0", "eventsource-parser": "^1.0.0",
"google-spreadsheet": "4.0.2", "google-spreadsheet": "4.0.2",
"got": "12.6.0", "got": "12.6.0",
@@ -54,10 +55,10 @@
"@types/react": "18.2.15", "@types/react": "18.2.15",
"@types/sanitize-html": "2.9.0", "@types/sanitize-html": "2.9.0",
"dotenv-cli": "^7.2.1", "dotenv-cli": "^7.2.1",
"next-runtime-env": "^1.6.2",
"eslint": "8.44.0", "eslint": "8.44.0",
"eslint-config-custom": "workspace:*", "eslint-config-custom": "workspace:*",
"google-auth-library": "8.9.0", "google-auth-library": "8.9.0",
"next-runtime-env": "^1.6.2",
"node-fetch": "3.3.1", "node-fetch": "3.3.1",
"papaparse": "5.4.1", "papaparse": "5.4.1",
"superjson": "1.12.4", "superjson": "1.12.4",

View File

@@ -1,6 +1,7 @@
import { ParsedReply } from '@/features/chat/types' import { ParsedReply } from '@/features/chat/types'
import { DateInputBlock } from '@typebot.io/schemas' import { DateInputBlock } from '@typebot.io/schemas'
import { parse as chronoParse } from 'chrono-node' import { parse as chronoParse } from 'chrono-node'
import { format } from 'date-fns'
export const parseDateReply = ( export const parseDateReply = (
reply: string, reply: string,
@@ -8,21 +9,29 @@ export const parseDateReply = (
): ParsedReply => { ): ParsedReply => {
const parsedDate = chronoParse(reply) const parsedDate = chronoParse(reply)
if (parsedDate.length === 0) return { status: 'fail' } if (parsedDate.length === 0) return { status: 'fail' }
const formatOptions: Intl.DateTimeFormatOptions = { const formatString =
day: '2-digit', block.options.format ??
month: '2-digit', (block.options.hasTime ? 'dd/MM/yyyy HH:mm' : 'dd/MM/yyyy')
year: 'numeric',
hour: block.options.hasTime ? '2-digit' : undefined, const detectedStartDate = parseDateWithNeutralTimezone(
minute: block.options.hasTime ? '2-digit' : undefined, parsedDate[0].start.date()
} )
const startDate = parsedDate[0].start const startDate = format(detectedStartDate, formatString)
.date()
.toLocaleString(undefined, formatOptions) const detectedEndDate = parsedDate[0].end?.date()
const endDate = parsedDate[0].end ? parseDateWithNeutralTimezone(parsedDate[0].end?.date())
?.date() : undefined
.toLocaleString(undefined, formatOptions) const endDate = detectedEndDate
? format(detectedEndDate, formatString)
: undefined
if (block.options.isRange && !endDate) return { status: 'fail' }
return { return {
status: 'success', status: 'success',
reply: block.options.isRange ? `${startDate} to ${endDate}` : startDate, reply: block.options.isRange ? `${startDate} to ${endDate}` : startDate,
} }
} }
const parseDateWithNeutralTimezone = (date: Date) =>
new Date(date.valueOf() + date.getTimezoneOffset() * 60 * 1000)

View File

@@ -142,7 +142,6 @@ test('API chat execution should work on published bot', async ({ request }) => {
data: { message: '8', sessionId: chatSessionId }, data: { message: '8', sessionId: chatSessionId },
}) })
).json() ).json()
console.log(messages, input)
expect(messages[0].content.richText).toStrictEqual([ expect(messages[0].content.richText).toStrictEqual([
{ {
children: [{ text: "I'm gonna shoot multiple inputs now..." }], children: [{ text: "I'm gonna shoot multiple inputs now..." }],

View File

@@ -1,6 +1,6 @@
{ {
"name": "@typebot.io/js", "name": "@typebot.io/js",
"version": "0.1.23", "version": "0.1.24",
"description": "Javascript library to display typebots on your website", "description": "Javascript library to display typebots on your website",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",

View File

@@ -2,7 +2,6 @@ import { SendButton } from '@/components/SendButton'
import { InputSubmitContent } from '@/types' import { InputSubmitContent } from '@/types'
import type { DateInputOptions } from '@typebot.io/schemas' import type { DateInputOptions } from '@typebot.io/schemas'
import { createSignal } from 'solid-js' import { createSignal } from 'solid-js'
import { parseReadableDate } from '../utils/parseReadableDate'
type Props = { type Props = {
onSubmit: (inputValue: InputSubmitContent) => void onSubmit: (inputValue: InputSubmitContent) => void
@@ -24,11 +23,9 @@ export const DateForm = (props: Props) => {
if (inputValues().from === '' && inputValues().to === '') return if (inputValues().from === '' && inputValues().to === '') return
e.preventDefault() e.preventDefault()
props.onSubmit({ props.onSubmit({
value: parseReadableDate({ value: `${inputValues().from}${
...inputValues(), props.options?.isRange ? ` to ${inputValues().to}` : ''
hasTime: props.options?.hasTime, }`,
isRange: props.options?.isRange,
}),
}) })
}} }}
> >

View File

@@ -1,27 +0,0 @@
export const parseReadableDate = ({
from,
to,
hasTime,
isRange,
}: {
from: string
to: string
hasTime?: boolean
isRange?: boolean
}) => {
const currentLocale = window.navigator.language
const formatOptions: Intl.DateTimeFormatOptions = {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: hasTime ? '2-digit' : undefined,
minute: hasTime ? '2-digit' : undefined,
}
const fromReadable = new Date(
hasTime ? from : from.replace(/-/g, '/')
).toLocaleString(currentLocale, formatOptions)
const toReadable = new Date(
hasTime ? to : to.replace(/-/g, '/')
).toLocaleString(currentLocale, formatOptions)
return `${fromReadable}${isRange ? ` to ${toReadable}` : ''}`
}

View File

@@ -1,6 +1,6 @@
{ {
"name": "@typebot.io/nextjs", "name": "@typebot.io/nextjs",
"version": "0.1.23", "version": "0.1.24",
"description": "Convenient library to display typebots on your Next.js website", "description": "Convenient library to display typebots on your Next.js website",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@typebot.io/react", "name": "@typebot.io/react",
"version": "0.1.23", "version": "0.1.24",
"description": "Convenient library to display typebots on your React app", "description": "Convenient library to display typebots on your React app",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",

View File

@@ -12,6 +12,7 @@ export const dateInputOptionsSchema = optionBaseSchema.merge(
}), }),
hasTime: z.boolean(), hasTime: z.boolean(),
isRange: z.boolean(), isRange: z.boolean(),
format: z.string().optional(),
}) })
) )

12
pnpm-lock.yaml generated
View File

@@ -543,11 +543,14 @@ importers:
specifier: workspace:* specifier: workspace:*
version: link:../../packages/deprecated/bot-engine version: link:../../packages/deprecated/bot-engine
chrono-node: chrono-node:
specifier: ^2.6.4 specifier: 2.6.6
version: 2.6.4 version: 2.6.6
cors: cors:
specifier: 2.8.5 specifier: 2.8.5
version: 2.8.5 version: 2.8.5
date-fns:
specifier: ^2.30.0
version: 2.30.0
eventsource-parser: eventsource-parser:
specifier: ^1.0.0 specifier: ^1.0.0
version: 1.0.0 version: 1.0.0
@@ -11206,8 +11209,8 @@ packages:
resolution: {integrity: sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==} resolution: {integrity: sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==}
engines: {node: '>=6.0'} engines: {node: '>=6.0'}
/chrono-node@2.6.4: /chrono-node@2.6.6:
resolution: {integrity: sha512-weCpfagfISvUMleIIqCi12AL9iQYn1ybX/6RB9qolynvHNvYlfdJete51uyB8TmwDTgEeKFEq0I5p/SHhOfhsw==} resolution: {integrity: sha512-RObSvo49wRL/ek6U4lMuZjmCi//gLM2GsHBMauIw/50fBbP6To3F99vn88IRL9w4qC39tFRnJZc6uiGrOi1oGw==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
dependencies: dependencies:
dayjs: 1.11.9 dayjs: 1.11.9
@@ -12063,7 +12066,6 @@ packages:
engines: {node: '>=0.11'} engines: {node: '>=0.11'}
dependencies: dependencies:
'@babel/runtime': 7.22.15 '@babel/runtime': 7.22.15
dev: true
/dayjs@1.11.9: /dayjs@1.11.9:
resolution: {integrity: sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA==} resolution: {integrity: sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA==}