⚡ (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:
@@ -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])
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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
|
||||||
|
```
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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..." }],
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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,
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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}` : ''}`
|
|
||||||
}
|
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
12
pnpm-lock.yaml
generated
@@ -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==}
|
||||||
|
|||||||
Reference in New Issue
Block a user