diff --git a/packages/twenty-front/src/modules/object-record/record-field/components/FormFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/components/FormFieldInput.tsx index a3fa74cc5..bfa0a901c 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/components/FormFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/components/FormFieldInput.tsx @@ -1,6 +1,6 @@ import { FormAddressFieldInput } from '@/object-record/record-field/form-types/components/FormAddressFieldInput'; import { FormBooleanFieldInput } from '@/object-record/record-field/form-types/components/FormBooleanFieldInput'; -import { FormDateTimeFieldInputBase } from '@/object-record/record-field/form-types/components/FormDateTimeFieldInputBase'; +import { FormDateFieldInput } from '@/object-record/record-field/form-types/components/FormDateFieldInput'; import { FormEmailsFieldInput } from '@/object-record/record-field/form-types/components/FormEmailsFieldInput'; import { FormFullNameFieldInput } from '@/object-record/record-field/form-types/components/FormFullNameFieldInput'; import { FormLinksFieldInput } from '@/object-record/record-field/form-types/components/FormLinksFieldInput'; @@ -34,6 +34,7 @@ import { isFieldSelect } from '@/object-record/record-field/types/guards/isField import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText'; import { isFieldUuid } from '@/object-record/record-field/types/guards/isFieldUuid'; import { JsonValue } from 'type-fest'; +import { FormDateTimeFieldInput } from '@/object-record/record-field/form-types/components/FormDateTimeFieldInput'; type FormFieldInputProps = { field: FieldDefinition; @@ -109,16 +110,14 @@ export const FormFieldInput = ({ VariablePicker={VariablePicker} /> ) : isFieldDate(field) ? ( - ) : isFieldDateTime(field) ? ( - void; + VariablePicker?: VariablePickerComponent; +}; + +export const FormDateFieldInput = ({ + label, + defaultValue, + onPersist, + VariablePicker, +}: FormDateFieldInputProps) => { + return ( + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormDateTimeFieldInputBase.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormDateTimeFieldInput.tsx similarity index 95% rename from packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormDateTimeFieldInputBase.tsx rename to packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormDateTimeFieldInput.tsx index fb45ea4f9..62b61ebe7 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormDateTimeFieldInputBase.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormDateTimeFieldInput.tsx @@ -69,27 +69,26 @@ type DraftValue = value: string; }; -type FormDateTimeFieldInputBaseProps = { - mode: 'date' | 'datetime'; +type FormDateTimeFieldInputProps = { + dateOnly?: boolean; label?: string; + placeholder?: string; defaultValue: string | undefined; onPersist: (value: string | null) => void; VariablePicker?: VariablePickerComponent; }; -export const FormDateTimeFieldInputBase = ({ - mode, +export const FormDateTimeFieldInput = ({ + dateOnly, label, defaultValue, onPersist, VariablePicker, -}: FormDateTimeFieldInputBaseProps) => { +}: FormDateTimeFieldInputProps) => { const { timeZone } = useContext(UserContext); const inputId = useId(); - const placeholder = mode === 'date' ? 'mm/dd/yyyy' : 'mm/dd/yyyy hh:mm'; - const [draftValue, setDraftValue] = useState( isStandaloneVariableString(defaultValue) ? { @@ -116,7 +115,7 @@ export const FormDateTimeFieldInputBase = ({ isDefined(draftValueAsDate) && !isStandaloneVariableString(defaultValue) ? parseDateToString({ date: draftValueAsDate, - isDateTimeInput: mode === 'datetime', + isDateTimeInput: !dateOnly, userTimezone: timeZone, }) : '', @@ -143,6 +142,8 @@ export const FormDateTimeFieldInputBase = ({ const displayDatePicker = draftValue.type === 'static' && draftValue.mode === 'edit'; + const placeholder = dateOnly ? 'mm/dd/yyyy' : 'mm/dd/yyyy hh:mm'; + useListenClickOutside({ refs: [datePickerWrapperRef], listenerId: 'FormDateTimeFieldInputBase', @@ -168,7 +169,7 @@ export const FormDateTimeFieldInputBase = ({ isDefined(newDate) ? parseDateToString({ date: newDate, - isDateTimeInput: mode === 'datetime', + isDateTimeInput: !dateOnly, userTimezone: timeZone, }) : '', @@ -226,7 +227,7 @@ export const FormDateTimeFieldInputBase = ({ isDefined(newDate) ? parseDateToString({ date: newDate, - isDateTimeInput: mode === 'datetime', + isDateTimeInput: !dateOnly, userTimezone: timeZone, }) : '', @@ -262,7 +263,7 @@ export const FormDateTimeFieldInputBase = ({ const parsedInputDateTime = parseStringToDate({ dateAsString: inputDateTimeTrimmed, - isDateTimeInput: mode === 'datetime', + isDateTimeInput: !dateOnly, userTimezone: timeZone, }); @@ -288,7 +289,7 @@ export const FormDateTimeFieldInputBase = ({ setInputDateTime( parseDateToString({ date: validatedDate, - isDateTimeInput: mode === 'datetime', + isDateTimeInput: !dateOnly, userTimezone: timeZone, }), ); diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormDateFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormDateFieldInput.stories.tsx new file mode 100644 index 000000000..39a313fb8 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormDateFieldInput.stories.tsx @@ -0,0 +1,370 @@ +import { MAX_DATE } from '@/ui/input/components/internal/date/constants/MaxDate'; +import { MIN_DATE } from '@/ui/input/components/internal/date/constants/MinDate'; +import { parseDateToString } from '@/ui/input/components/internal/date/utils/parseDateToString'; +import { expect } from '@storybook/jest'; +import { Meta, StoryObj } from '@storybook/react'; +import { + fn, + userEvent, + waitFor, + waitForElementToBeRemoved, + within, +} from '@storybook/test'; +import { DateTime } from 'luxon'; +import { FormDateFieldInput } from '../FormDateFieldInput'; + +const meta: Meta = { + title: 'UI/Data/Field/Form/Input/FormDateFieldInput', + component: FormDateFieldInput, + args: {}, + argTypes: {}, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + label: 'Created At', + defaultValue: '2024-12-09T13:20:19.631Z', + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + await canvas.findByText('Created At'); + await canvas.findByDisplayValue('12/09/2024'); + }, +}; + +export const WithDefaultEmptyValue: Story = { + args: { + label: 'Created At', + defaultValue: undefined, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + await canvas.findByText('Created At'); + await canvas.findByDisplayValue(''); + await canvas.findByPlaceholderText('mm/dd/yyyy'); + }, +}; + +export const SetsDateWithInput: Story = { + args: { + label: 'Created At', + defaultValue: undefined, + onPersist: fn(), + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + const input = await canvas.findByPlaceholderText('mm/dd/yyyy'); + + await userEvent.click(input); + + const dialog = await canvas.findByRole('dialog'); + expect(dialog).toBeVisible(); + + await userEvent.type(input, '12/08/2024{enter}'); + + await waitFor(() => { + expect(args.onPersist).toHaveBeenCalledWith('2024-12-08T00:00:00.000Z'); + }); + + expect(dialog).toBeVisible(); + }, +}; + +export const SetsDateWithDatePicker: Story = { + args: { + label: 'Created At', + defaultValue: undefined, + onPersist: fn(), + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + const input = await canvas.findByPlaceholderText('mm/dd/yyyy'); + expect(input).toBeVisible(); + + await userEvent.click(input); + + const datePicker = await canvas.findByRole('dialog'); + expect(datePicker).toBeVisible(); + + const dayToChoose = await within(datePicker).findByRole('option', { + name: 'Choose Saturday, December 7th, 2024', + }); + + await Promise.all([ + userEvent.click(dayToChoose), + + waitForElementToBeRemoved(datePicker), + waitFor(() => { + expect(args.onPersist).toHaveBeenCalledWith( + expect.stringMatching(/^2024-12-07/), + ); + }), + waitFor(() => { + expect(canvas.getByDisplayValue('12/07/2024')).toBeVisible(); + }), + ]); + }, +}; + +export const ResetsDateByClickingButton: Story = { + args: { + label: 'Created At', + defaultValue: '2024-12-09T13:20:19.631Z', + onPersist: fn(), + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + const input = await canvas.findByPlaceholderText('mm/dd/yyyy'); + expect(input).toBeVisible(); + + await userEvent.click(input); + + const datePicker = await canvas.findByRole('dialog'); + expect(datePicker).toBeVisible(); + + const clearButton = await canvas.findByText('Clear'); + + await Promise.all([ + userEvent.click(clearButton), + + waitForElementToBeRemoved(datePicker), + waitFor(() => { + expect(args.onPersist).toHaveBeenCalledWith(null); + }), + waitFor(() => { + expect(input).toHaveDisplayValue(''); + }), + ]); + }, +}; + +export const ResetsDateByErasingInputContent: Story = { + args: { + label: 'Created At', + defaultValue: '2024-12-09T13:20:19.631Z', + onPersist: fn(), + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + const input = await canvas.findByPlaceholderText('mm/dd/yyyy'); + expect(input).toBeVisible(); + + expect(input).toHaveDisplayValue('12/09/2024'); + + await userEvent.clear(input); + + await Promise.all([ + userEvent.type(input, '{Enter}'), + + waitForElementToBeRemoved(() => canvas.queryByRole('dialog')), + waitFor(() => { + expect(args.onPersist).toHaveBeenCalledWith(null); + }), + waitFor(() => { + expect(input).toHaveDisplayValue(''); + }), + ]); + }, +}; + +export const DefaultsToMinValueWhenTypingReallyOldDate: Story = { + args: { + label: 'Created At', + defaultValue: undefined, + onPersist: fn(), + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + const input = await canvas.findByPlaceholderText('mm/dd/yyyy'); + expect(input).toBeVisible(); + + await userEvent.click(input); + + const datePicker = await canvas.findByRole('dialog'); + expect(datePicker).toBeVisible(); + + await Promise.all([ + userEvent.type(input, '02/02/1500{Enter}'), + + waitFor(() => { + expect(args.onPersist).toHaveBeenCalledWith(MIN_DATE.toISOString()); + }), + waitFor(() => { + expect(input).toHaveDisplayValue( + parseDateToString({ + date: MIN_DATE, + isDateTimeInput: false, + userTimezone: undefined, + }), + ); + }), + waitFor(() => { + const expectedDate = DateTime.fromJSDate(MIN_DATE) + .toLocal() + .set({ + day: MIN_DATE.getUTCDate(), + month: MIN_DATE.getUTCMonth() + 1, + year: MIN_DATE.getUTCFullYear(), + hour: 0, + minute: 0, + second: 0, + millisecond: 0, + }); + + const selectedDay = within(datePicker).getByRole('option', { + selected: true, + name: (accessibleName) => { + // The name looks like "Choose Sunday, December 31st, 1899" + return accessibleName.includes(expectedDate.toFormat('yyyy')); + }, + }); + expect(selectedDay).toBeVisible(); + }), + ]); + }, +}; + +export const DefaultsToMaxValueWhenTypingReallyFarDate: Story = { + args: { + label: 'Created At', + defaultValue: undefined, + onPersist: fn(), + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + const input = await canvas.findByPlaceholderText('mm/dd/yyyy'); + expect(input).toBeVisible(); + + await userEvent.click(input); + + const datePicker = await canvas.findByRole('dialog'); + expect(datePicker).toBeVisible(); + + await Promise.all([ + userEvent.type(input, '02/02/2500{Enter}'), + + waitFor(() => { + expect(args.onPersist).toHaveBeenCalledWith(MAX_DATE.toISOString()); + }), + waitFor(() => { + expect(input).toHaveDisplayValue( + parseDateToString({ + date: MAX_DATE, + isDateTimeInput: false, + userTimezone: undefined, + }), + ); + }), + waitFor(() => { + const expectedDate = DateTime.fromJSDate(MAX_DATE) + .toLocal() + .set({ + day: MAX_DATE.getUTCDate(), + month: MAX_DATE.getUTCMonth() + 1, + year: MAX_DATE.getUTCFullYear(), + hour: 0, + minute: 0, + second: 0, + millisecond: 0, + }); + + const selectedDay = within(datePicker).getByRole('option', { + selected: true, + name: (accessibleName) => { + // The name looks like "Choose Thursday, December 30th, 2100" + return accessibleName.includes(expectedDate.toFormat('yyyy')); + }, + }); + expect(selectedDay).toBeVisible(); + }), + ]); + }, +}; + +export const SwitchesToStandaloneVariable: Story = { + args: { + label: 'Created At', + defaultValue: undefined, + onPersist: fn(), + VariablePicker: ({ onVariableSelect }) => { + return ( + + ); + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const addVariableButton = await canvas.findByText('Add variable'); + await userEvent.click(addVariableButton); + + const variableTag = await canvas.findByText('test'); + expect(variableTag).toBeVisible(); + + const removeVariableButton = canvas.getByTestId(/^remove-icon/); + + await Promise.all([ + userEvent.click(removeVariableButton), + + waitForElementToBeRemoved(variableTag), + waitFor(() => { + const input = canvas.getByPlaceholderText('mm/dd/yyyy'); + expect(input).toBeVisible(); + }), + ]); + }, +}; + +export const ClickingOutsideDoesNotResetInputState: Story = { + args: { + label: 'Created At', + defaultValue: '2024-12-09T13:20:19.631Z', + onPersist: fn(), + }, + play: async ({ canvasElement, args }) => { + const defaultValueAsDisplayString = parseDateToString({ + date: new Date(args.defaultValue!), + isDateTimeInput: false, + userTimezone: undefined, + }); + + const canvas = within(canvasElement); + + const input = await canvas.findByPlaceholderText('mm/dd/yyyy'); + expect(input).toBeVisible(); + expect(input).toHaveDisplayValue(defaultValueAsDisplayString); + + await userEvent.type(input, '{Backspace}{Backspace}'); + + const datePicker = await canvas.findByRole('dialog'); + expect(datePicker).toBeVisible(); + + await Promise.all([ + userEvent.click(canvasElement), + + waitForElementToBeRemoved(datePicker), + ]); + + expect(args.onPersist).not.toHaveBeenCalled(); + + expect(input).toHaveDisplayValue(defaultValueAsDisplayString.slice(0, -2)); + }, +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormDateTimeFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormDateTimeFieldInput.stories.tsx new file mode 100644 index 000000000..d3dffde5d --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormDateTimeFieldInput.stories.tsx @@ -0,0 +1,397 @@ +import { MAX_DATE } from '@/ui/input/components/internal/date/constants/MaxDate'; +import { MIN_DATE } from '@/ui/input/components/internal/date/constants/MinDate'; +import { parseDateToString } from '@/ui/input/components/internal/date/utils/parseDateToString'; +import { expect } from '@storybook/jest'; +import { Meta, StoryObj } from '@storybook/react'; +import { + fn, + userEvent, + waitFor, + waitForElementToBeRemoved, + within, +} from '@storybook/test'; +import { DateTime } from 'luxon'; +import { FormDateTimeFieldInput } from '@/object-record/record-field/form-types/components/FormDateTimeFieldInput'; + +const meta: Meta = { + title: 'UI/Data/Field/Form/Input/FormDateTimeFieldInput', + component: FormDateTimeFieldInput, + args: {}, + argTypes: {}, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + label: 'Created At', + defaultValue: '2024-12-09T13:20:19.631Z', + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + await canvas.findByText('Created At'); + await canvas.findByDisplayValue(/12\/09\/2024 \d{2}:20/); + }, +}; + +export const WithDefaultEmptyValue: Story = { + args: { + label: 'Created At', + defaultValue: undefined, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + await canvas.findByText('Created At'); + await canvas.findByDisplayValue(''); + await canvas.findByPlaceholderText('mm/dd/yyyy hh:mm'); + }, +}; + +export const SetsDateTimeWithInput: Story = { + args: { + label: 'Created At', + defaultValue: undefined, + onPersist: fn(), + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + const input = await canvas.findByPlaceholderText('mm/dd/yyyy hh:mm'); + + await userEvent.click(input); + + const dialog = await canvas.findByRole('dialog'); + expect(dialog).toBeVisible(); + + await userEvent.type(input, '12/08/2024 12:10{enter}'); + + await waitFor(() => { + expect(args.onPersist).toHaveBeenCalledWith( + expect.stringMatching(/2024-12-08T\d{2}:10:00.000Z/), + ); + }); + + expect(dialog).toBeVisible(); + }, +}; + +export const DoesNotSetDateWithoutTime: Story = { + args: { + label: 'Created At', + defaultValue: undefined, + onPersist: fn(), + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + const input = await canvas.findByPlaceholderText('mm/dd/yyyy hh:mm'); + + await userEvent.click(input); + + const dialog = await canvas.findByRole('dialog'); + expect(dialog).toBeVisible(); + + await userEvent.type(input, '12/08/2024{enter}'); + + expect(args.onPersist).not.toHaveBeenCalled(); + expect(dialog).toBeVisible(); + }, +}; + +export const SetsDateTimeWithDatePicker: Story = { + args: { + label: 'Created At', + defaultValue: undefined, + onPersist: fn(), + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + const input = await canvas.findByPlaceholderText('mm/dd/yyyy hh:mm'); + expect(input).toBeVisible(); + + await userEvent.click(input); + + const datePicker = await canvas.findByRole('dialog'); + expect(datePicker).toBeVisible(); + + const dayToChoose = await within(datePicker).findByRole('option', { + name: 'Choose Saturday, December 7th, 2024', + }); + + await Promise.all([ + userEvent.click(dayToChoose), + + waitForElementToBeRemoved(datePicker), + waitFor(() => { + expect(args.onPersist).toHaveBeenCalledWith( + expect.stringMatching(/^2024-12-07/), + ); + }), + waitFor(() => { + expect( + canvas.getByDisplayValue(/12\/07\/2024 \d{2}:\d{2}/), + ).toBeVisible(); + }), + ]); + }, +}; + +export const ResetsDateByClickingButton: Story = { + args: { + label: 'Created At', + defaultValue: '2024-12-09T13:20:19.631Z', + onPersist: fn(), + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + const input = await canvas.findByPlaceholderText('mm/dd/yyyy hh:mm'); + expect(input).toBeVisible(); + + await userEvent.click(input); + + const datePicker = await canvas.findByRole('dialog'); + expect(datePicker).toBeVisible(); + + const clearButton = await canvas.findByText('Clear'); + + await Promise.all([ + userEvent.click(clearButton), + + waitForElementToBeRemoved(datePicker), + waitFor(() => { + expect(args.onPersist).toHaveBeenCalledWith(null); + }), + waitFor(() => { + expect(input).toHaveDisplayValue(''); + }), + ]); + }, +}; + +export const ResetsDateByErasingInputContent: Story = { + args: { + label: 'Created At', + defaultValue: '2024-12-09T13:20:19.631Z', + onPersist: fn(), + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + const input = await canvas.findByPlaceholderText('mm/dd/yyyy hh:mm'); + expect(input).toBeVisible(); + + expect(input).toHaveDisplayValue(/12\/09\/2024 \d{2}:\d{2}/); + + await userEvent.clear(input); + + await Promise.all([ + userEvent.type(input, '{Enter}'), + + waitForElementToBeRemoved(() => canvas.queryByRole('dialog')), + waitFor(() => { + expect(args.onPersist).toHaveBeenCalledWith(null); + }), + waitFor(() => { + expect(input).toHaveDisplayValue(''); + }), + ]); + }, +}; + +export const DefaultsToMinValueWhenTypingReallyOldDate: Story = { + args: { + label: 'Created At', + defaultValue: undefined, + onPersist: fn(), + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + const input = await canvas.findByPlaceholderText('mm/dd/yyyy hh:mm'); + expect(input).toBeVisible(); + + await userEvent.click(input); + + const datePicker = await canvas.findByRole('dialog'); + expect(datePicker).toBeVisible(); + + await Promise.all([ + userEvent.type(input, '02/02/1500 10:10{Enter}'), + + waitFor(() => { + expect(args.onPersist).toHaveBeenCalledWith(MIN_DATE.toISOString()); + }), + waitFor(() => { + expect(input).toHaveDisplayValue( + parseDateToString({ + date: MIN_DATE, + isDateTimeInput: true, + userTimezone: undefined, + }), + ); + }), + waitFor(() => { + const expectedDate = DateTime.fromJSDate(MIN_DATE) + .toLocal() + .set({ + day: MIN_DATE.getUTCDate(), + month: MIN_DATE.getUTCMonth() + 1, + year: MIN_DATE.getUTCFullYear(), + hour: 0, + minute: 0, + second: 0, + millisecond: 0, + }); + + const selectedDay = within(datePicker).getByRole('option', { + selected: true, + name: (accessibleName) => { + // The name looks like "Choose Sunday, December 31st, 1899" + return accessibleName.includes(expectedDate.toFormat('yyyy')); + }, + }); + expect(selectedDay).toBeVisible(); + }), + ]); + }, +}; + +export const DefaultsToMaxValueWhenTypingReallyFarDate: Story = { + args: { + label: 'Created At', + defaultValue: undefined, + onPersist: fn(), + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + const input = await canvas.findByPlaceholderText('mm/dd/yyyy hh:mm'); + expect(input).toBeVisible(); + + await userEvent.click(input); + + const datePicker = await canvas.findByRole('dialog'); + expect(datePicker).toBeVisible(); + + await Promise.all([ + userEvent.type(input, '02/02/2500 10:10{Enter}'), + + waitFor(() => { + expect(args.onPersist).toHaveBeenCalledWith(MAX_DATE.toISOString()); + }), + waitFor(() => { + expect(input).toHaveDisplayValue( + parseDateToString({ + date: MAX_DATE, + isDateTimeInput: true, + userTimezone: undefined, + }), + ); + }), + waitFor(() => { + const expectedDate = DateTime.fromJSDate(MAX_DATE) + .toLocal() + .set({ + day: MAX_DATE.getUTCDate(), + month: MAX_DATE.getUTCMonth() + 1, + year: MAX_DATE.getUTCFullYear(), + hour: 0, + minute: 0, + second: 0, + millisecond: 0, + }); + + const selectedDay = within(datePicker).getByRole('option', { + selected: true, + name: (accessibleName) => { + // The name looks like "Choose Thursday, December 30th, 2100" + return accessibleName.includes(expectedDate.toFormat('yyyy')); + }, + }); + expect(selectedDay).toBeVisible(); + }), + ]); + }, +}; + +export const SwitchesToStandaloneVariable: Story = { + args: { + label: 'Created At', + defaultValue: undefined, + onPersist: fn(), + VariablePicker: ({ onVariableSelect }) => { + return ( + + ); + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const addVariableButton = await canvas.findByText('Add variable'); + await userEvent.click(addVariableButton); + + const variableTag = await canvas.findByText('test'); + expect(variableTag).toBeVisible(); + + const removeVariableButton = canvas.getByTestId(/^remove-icon/); + + await Promise.all([ + userEvent.click(removeVariableButton), + + waitForElementToBeRemoved(variableTag), + waitFor(() => { + const input = canvas.getByPlaceholderText('mm/dd/yyyy hh:mm'); + expect(input).toBeVisible(); + }), + ]); + }, +}; + +export const ClickingOutsideDoesNotResetInputState: Story = { + args: { + label: 'Created At', + defaultValue: '2024-12-09T13:20:19.631Z', + onPersist: fn(), + }, + play: async ({ canvasElement, args }) => { + const defaultValueAsDisplayString = parseDateToString({ + date: new Date(args.defaultValue!), + isDateTimeInput: true, + userTimezone: undefined, + }); + + const canvas = within(canvasElement); + + const input = await canvas.findByPlaceholderText('mm/dd/yyyy hh:mm'); + expect(input).toBeVisible(); + expect(input).toHaveDisplayValue(defaultValueAsDisplayString); + + await userEvent.type(input, '{Backspace}{Backspace}'); + + const datePicker = await canvas.findByRole('dialog'); + expect(datePicker).toBeVisible(); + + await Promise.all([ + userEvent.click(canvasElement), + + waitForElementToBeRemoved(datePicker), + ]); + + expect(args.onPersist).not.toHaveBeenCalled(); + + expect(input).toHaveDisplayValue(defaultValueAsDisplayString.slice(0, -2)); + }, +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormDateTimeFieldInputBase.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormDateTimeFieldInputBase.stories.tsx deleted file mode 100644 index cad93289e..000000000 --- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormDateTimeFieldInputBase.stories.tsx +++ /dev/null @@ -1,765 +0,0 @@ -import { MAX_DATE } from '@/ui/input/components/internal/date/constants/MaxDate'; -import { MIN_DATE } from '@/ui/input/components/internal/date/constants/MinDate'; -import { parseDateToString } from '@/ui/input/components/internal/date/utils/parseDateToString'; -import { expect } from '@storybook/jest'; -import { Meta, StoryObj } from '@storybook/react'; -import { - fn, - userEvent, - waitFor, - waitForElementToBeRemoved, - within, -} from '@storybook/test'; -import { DateTime } from 'luxon'; -import { FormDateTimeFieldInputBase } from '../FormDateTimeFieldInputBase'; - -const meta: Meta = { - title: 'UI/Data/Field/Form/Input/FormDateTimeFieldInputBase', - component: FormDateTimeFieldInputBase, - args: {}, - argTypes: {}, -}; - -export default meta; - -type Story = StoryObj; - -export const DateDefault: Story = { - args: { - mode: 'date', - label: 'Created At', - defaultValue: '2024-12-09T13:20:19.631Z', - }, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - await canvas.findByText('Created At'); - await canvas.findByDisplayValue('12/09/2024'); - }, -}; - -export const DateWithDefaultEmptyValue: Story = { - args: { - mode: 'date', - label: 'Created At', - defaultValue: undefined, - }, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - await canvas.findByText('Created At'); - await canvas.findByDisplayValue(''); - await canvas.findByPlaceholderText('mm/dd/yyyy'); - }, -}; - -export const DateSetsDateWithInput: Story = { - args: { - mode: 'date', - label: 'Created At', - defaultValue: undefined, - onPersist: fn(), - }, - play: async ({ canvasElement, args }) => { - const canvas = within(canvasElement); - - const input = await canvas.findByPlaceholderText('mm/dd/yyyy'); - - await userEvent.click(input); - - const dialog = await canvas.findByRole('dialog'); - expect(dialog).toBeVisible(); - - await userEvent.type(input, '12/08/2024{enter}'); - - await waitFor(() => { - expect(args.onPersist).toHaveBeenCalledWith('2024-12-08T00:00:00.000Z'); - }); - - expect(dialog).toBeVisible(); - }, -}; - -export const DateSetsDateWithDatePicker: Story = { - args: { - mode: 'date', - label: 'Created At', - defaultValue: undefined, - onPersist: fn(), - }, - play: async ({ canvasElement, args }) => { - const canvas = within(canvasElement); - - const input = await canvas.findByPlaceholderText('mm/dd/yyyy'); - expect(input).toBeVisible(); - - await userEvent.click(input); - - const datePicker = await canvas.findByRole('dialog'); - expect(datePicker).toBeVisible(); - - const dayToChoose = await within(datePicker).findByRole('option', { - name: 'Choose Saturday, December 7th, 2024', - }); - - await Promise.all([ - userEvent.click(dayToChoose), - - waitForElementToBeRemoved(datePicker), - waitFor(() => { - expect(args.onPersist).toHaveBeenCalledWith( - expect.stringMatching(/^2024-12-07/), - ); - }), - waitFor(() => { - expect(canvas.getByDisplayValue('12/07/2024')).toBeVisible(); - }), - ]); - }, -}; - -export const DateResetsDateByClickingButton: Story = { - args: { - mode: 'date', - label: 'Created At', - defaultValue: '2024-12-09T13:20:19.631Z', - onPersist: fn(), - }, - play: async ({ canvasElement, args }) => { - const canvas = within(canvasElement); - - const input = await canvas.findByPlaceholderText('mm/dd/yyyy'); - expect(input).toBeVisible(); - - await userEvent.click(input); - - const datePicker = await canvas.findByRole('dialog'); - expect(datePicker).toBeVisible(); - - const clearButton = await canvas.findByText('Clear'); - - await Promise.all([ - userEvent.click(clearButton), - - waitForElementToBeRemoved(datePicker), - waitFor(() => { - expect(args.onPersist).toHaveBeenCalledWith(null); - }), - waitFor(() => { - expect(input).toHaveDisplayValue(''); - }), - ]); - }, -}; - -export const DateResetsDateByErasingInputContent: Story = { - args: { - mode: 'date', - label: 'Created At', - defaultValue: '2024-12-09T13:20:19.631Z', - onPersist: fn(), - }, - play: async ({ canvasElement, args }) => { - const canvas = within(canvasElement); - - const input = await canvas.findByPlaceholderText('mm/dd/yyyy'); - expect(input).toBeVisible(); - - expect(input).toHaveDisplayValue('12/09/2024'); - - await userEvent.clear(input); - - await Promise.all([ - userEvent.type(input, '{Enter}'), - - waitForElementToBeRemoved(() => canvas.queryByRole('dialog')), - waitFor(() => { - expect(args.onPersist).toHaveBeenCalledWith(null); - }), - waitFor(() => { - expect(input).toHaveDisplayValue(''); - }), - ]); - }, -}; - -export const DateDefaultsToMinValueWhenTypingReallyOldDate: Story = { - args: { - mode: 'date', - label: 'Created At', - defaultValue: undefined, - onPersist: fn(), - }, - play: async ({ canvasElement, args }) => { - const canvas = within(canvasElement); - - const input = await canvas.findByPlaceholderText('mm/dd/yyyy'); - expect(input).toBeVisible(); - - await userEvent.click(input); - - const datePicker = await canvas.findByRole('dialog'); - expect(datePicker).toBeVisible(); - - await Promise.all([ - userEvent.type(input, '02/02/1500{Enter}'), - - waitFor(() => { - expect(args.onPersist).toHaveBeenCalledWith(MIN_DATE.toISOString()); - }), - waitFor(() => { - expect(input).toHaveDisplayValue( - parseDateToString({ - date: MIN_DATE, - isDateTimeInput: false, - userTimezone: undefined, - }), - ); - }), - waitFor(() => { - const expectedDate = DateTime.fromJSDate(MIN_DATE) - .toLocal() - .set({ - day: MIN_DATE.getUTCDate(), - month: MIN_DATE.getUTCMonth() + 1, - year: MIN_DATE.getUTCFullYear(), - hour: 0, - minute: 0, - second: 0, - millisecond: 0, - }); - - const selectedDay = within(datePicker).getByRole('option', { - selected: true, - name: (accessibleName) => { - // The name looks like "Choose Sunday, December 31st, 1899" - return accessibleName.includes(expectedDate.toFormat('yyyy')); - }, - }); - expect(selectedDay).toBeVisible(); - }), - ]); - }, -}; - -export const DateDefaultsToMaxValueWhenTypingReallyFarDate: Story = { - args: { - mode: 'date', - label: 'Created At', - defaultValue: undefined, - onPersist: fn(), - }, - play: async ({ canvasElement, args }) => { - const canvas = within(canvasElement); - - const input = await canvas.findByPlaceholderText('mm/dd/yyyy'); - expect(input).toBeVisible(); - - await userEvent.click(input); - - const datePicker = await canvas.findByRole('dialog'); - expect(datePicker).toBeVisible(); - - await Promise.all([ - userEvent.type(input, '02/02/2500{Enter}'), - - waitFor(() => { - expect(args.onPersist).toHaveBeenCalledWith(MAX_DATE.toISOString()); - }), - waitFor(() => { - expect(input).toHaveDisplayValue( - parseDateToString({ - date: MAX_DATE, - isDateTimeInput: false, - userTimezone: undefined, - }), - ); - }), - waitFor(() => { - const expectedDate = DateTime.fromJSDate(MAX_DATE) - .toLocal() - .set({ - day: MAX_DATE.getUTCDate(), - month: MAX_DATE.getUTCMonth() + 1, - year: MAX_DATE.getUTCFullYear(), - hour: 0, - minute: 0, - second: 0, - millisecond: 0, - }); - - const selectedDay = within(datePicker).getByRole('option', { - selected: true, - name: (accessibleName) => { - // The name looks like "Choose Thursday, December 30th, 2100" - return accessibleName.includes(expectedDate.toFormat('yyyy')); - }, - }); - expect(selectedDay).toBeVisible(); - }), - ]); - }, -}; - -export const DateSwitchesToStandaloneVariable: Story = { - args: { - mode: 'date', - label: 'Created At', - defaultValue: undefined, - onPersist: fn(), - VariablePicker: ({ onVariableSelect }) => { - return ( - - ); - }, - }, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - const addVariableButton = await canvas.findByText('Add variable'); - await userEvent.click(addVariableButton); - - const variableTag = await canvas.findByText('test'); - expect(variableTag).toBeVisible(); - - const removeVariableButton = canvas.getByTestId(/^remove-icon/); - - await Promise.all([ - userEvent.click(removeVariableButton), - - waitForElementToBeRemoved(variableTag), - waitFor(() => { - const input = canvas.getByPlaceholderText('mm/dd/yyyy'); - expect(input).toBeVisible(); - }), - ]); - }, -}; - -export const DateClickingOutsideDoesNotResetInputState: Story = { - args: { - mode: 'date', - label: 'Created At', - defaultValue: '2024-12-09T13:20:19.631Z', - onPersist: fn(), - }, - play: async ({ canvasElement, args }) => { - const defaultValueAsDisplayString = parseDateToString({ - date: new Date(args.defaultValue!), - isDateTimeInput: false, - userTimezone: undefined, - }); - - const canvas = within(canvasElement); - - const input = await canvas.findByPlaceholderText('mm/dd/yyyy'); - expect(input).toBeVisible(); - expect(input).toHaveDisplayValue(defaultValueAsDisplayString); - - await userEvent.type(input, '{Backspace}{Backspace}'); - - const datePicker = await canvas.findByRole('dialog'); - expect(datePicker).toBeVisible(); - - await Promise.all([ - userEvent.click(canvasElement), - - waitForElementToBeRemoved(datePicker), - ]); - - expect(args.onPersist).not.toHaveBeenCalled(); - - expect(input).toHaveDisplayValue(defaultValueAsDisplayString.slice(0, -2)); - }, -}; - -// ---- - -export const DateTimeDefault: Story = { - args: { - mode: 'datetime', - label: 'Created At', - defaultValue: '2024-12-09T13:20:19.631Z', - }, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - await canvas.findByText('Created At'); - await canvas.findByDisplayValue(/12\/09\/2024 \d{2}:20/); - }, -}; - -export const DateTimeWithDefaultEmptyValue: Story = { - args: { - mode: 'datetime', - label: 'Created At', - defaultValue: undefined, - }, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - await canvas.findByText('Created At'); - await canvas.findByDisplayValue(''); - await canvas.findByPlaceholderText('mm/dd/yyyy hh:mm'); - }, -}; - -export const DateTimeSetsDateTimeWithInput: Story = { - args: { - mode: 'datetime', - label: 'Created At', - defaultValue: undefined, - onPersist: fn(), - }, - play: async ({ canvasElement, args }) => { - const canvas = within(canvasElement); - - const input = await canvas.findByPlaceholderText('mm/dd/yyyy hh:mm'); - - await userEvent.click(input); - - const dialog = await canvas.findByRole('dialog'); - expect(dialog).toBeVisible(); - - await userEvent.type(input, '12/08/2024 12:10{enter}'); - - await waitFor(() => { - expect(args.onPersist).toHaveBeenCalledWith( - expect.stringMatching(/2024-12-08T\d{2}:10:00.000Z/), - ); - }); - - expect(dialog).toBeVisible(); - }, -}; - -export const DateTimeDoesNotSetDateWithoutTime: Story = { - args: { - mode: 'datetime', - label: 'Created At', - defaultValue: undefined, - onPersist: fn(), - }, - play: async ({ canvasElement, args }) => { - const canvas = within(canvasElement); - - const input = await canvas.findByPlaceholderText('mm/dd/yyyy hh:mm'); - - await userEvent.click(input); - - const dialog = await canvas.findByRole('dialog'); - expect(dialog).toBeVisible(); - - await userEvent.type(input, '12/08/2024{enter}'); - - expect(args.onPersist).not.toHaveBeenCalled(); - expect(dialog).toBeVisible(); - }, -}; - -export const DateTimeSetsDateTimeWithDatePicker: Story = { - args: { - mode: 'datetime', - label: 'Created At', - defaultValue: undefined, - onPersist: fn(), - }, - play: async ({ canvasElement, args }) => { - const canvas = within(canvasElement); - - const input = await canvas.findByPlaceholderText('mm/dd/yyyy hh:mm'); - expect(input).toBeVisible(); - - await userEvent.click(input); - - const datePicker = await canvas.findByRole('dialog'); - expect(datePicker).toBeVisible(); - - const dayToChoose = await within(datePicker).findByRole('option', { - name: 'Choose Saturday, December 7th, 2024', - }); - - await Promise.all([ - userEvent.click(dayToChoose), - - waitForElementToBeRemoved(datePicker), - waitFor(() => { - expect(args.onPersist).toHaveBeenCalledWith( - expect.stringMatching(/^2024-12-07/), - ); - }), - waitFor(() => { - expect( - canvas.getByDisplayValue(/12\/07\/2024 \d{2}:\d{2}/), - ).toBeVisible(); - }), - ]); - }, -}; - -export const DateTimeResetsDateByClickingButton: Story = { - args: { - mode: 'datetime', - label: 'Created At', - defaultValue: '2024-12-09T13:20:19.631Z', - onPersist: fn(), - }, - play: async ({ canvasElement, args }) => { - const canvas = within(canvasElement); - - const input = await canvas.findByPlaceholderText('mm/dd/yyyy hh:mm'); - expect(input).toBeVisible(); - - await userEvent.click(input); - - const datePicker = await canvas.findByRole('dialog'); - expect(datePicker).toBeVisible(); - - const clearButton = await canvas.findByText('Clear'); - - await Promise.all([ - userEvent.click(clearButton), - - waitForElementToBeRemoved(datePicker), - waitFor(() => { - expect(args.onPersist).toHaveBeenCalledWith(null); - }), - waitFor(() => { - expect(input).toHaveDisplayValue(''); - }), - ]); - }, -}; - -export const DateTimeResetsDateByErasingInputContent: Story = { - args: { - mode: 'datetime', - label: 'Created At', - defaultValue: '2024-12-09T13:20:19.631Z', - onPersist: fn(), - }, - play: async ({ canvasElement, args }) => { - const canvas = within(canvasElement); - - const input = await canvas.findByPlaceholderText('mm/dd/yyyy hh:mm'); - expect(input).toBeVisible(); - - expect(input).toHaveDisplayValue(/12\/09\/2024 \d{2}:\d{2}/); - - await userEvent.clear(input); - - await Promise.all([ - userEvent.type(input, '{Enter}'), - - waitForElementToBeRemoved(() => canvas.queryByRole('dialog')), - waitFor(() => { - expect(args.onPersist).toHaveBeenCalledWith(null); - }), - waitFor(() => { - expect(input).toHaveDisplayValue(''); - }), - ]); - }, -}; - -export const DateTimeDefaultsToMinValueWhenTypingReallyOldDate: Story = { - args: { - mode: 'datetime', - label: 'Created At', - defaultValue: undefined, - onPersist: fn(), - }, - play: async ({ canvasElement, args }) => { - const canvas = within(canvasElement); - - const input = await canvas.findByPlaceholderText('mm/dd/yyyy hh:mm'); - expect(input).toBeVisible(); - - await userEvent.click(input); - - const datePicker = await canvas.findByRole('dialog'); - expect(datePicker).toBeVisible(); - - await Promise.all([ - userEvent.type(input, '02/02/1500 10:10{Enter}'), - - waitFor(() => { - expect(args.onPersist).toHaveBeenCalledWith(MIN_DATE.toISOString()); - }), - waitFor(() => { - expect(input).toHaveDisplayValue( - parseDateToString({ - date: MIN_DATE, - isDateTimeInput: true, - userTimezone: undefined, - }), - ); - }), - waitFor(() => { - const expectedDate = DateTime.fromJSDate(MIN_DATE) - .toLocal() - .set({ - day: MIN_DATE.getUTCDate(), - month: MIN_DATE.getUTCMonth() + 1, - year: MIN_DATE.getUTCFullYear(), - hour: 0, - minute: 0, - second: 0, - millisecond: 0, - }); - - const selectedDay = within(datePicker).getByRole('option', { - selected: true, - name: (accessibleName) => { - // The name looks like "Choose Sunday, December 31st, 1899" - return accessibleName.includes(expectedDate.toFormat('yyyy')); - }, - }); - expect(selectedDay).toBeVisible(); - }), - ]); - }, -}; - -export const DateTimeDefaultsToMaxValueWhenTypingReallyFarDate: Story = { - args: { - mode: 'datetime', - label: 'Created At', - defaultValue: undefined, - onPersist: fn(), - }, - play: async ({ canvasElement, args }) => { - const canvas = within(canvasElement); - - const input = await canvas.findByPlaceholderText('mm/dd/yyyy hh:mm'); - expect(input).toBeVisible(); - - await userEvent.click(input); - - const datePicker = await canvas.findByRole('dialog'); - expect(datePicker).toBeVisible(); - - await Promise.all([ - userEvent.type(input, '02/02/2500 10:10{Enter}'), - - waitFor(() => { - expect(args.onPersist).toHaveBeenCalledWith(MAX_DATE.toISOString()); - }), - waitFor(() => { - expect(input).toHaveDisplayValue( - parseDateToString({ - date: MAX_DATE, - isDateTimeInput: true, - userTimezone: undefined, - }), - ); - }), - waitFor(() => { - const expectedDate = DateTime.fromJSDate(MAX_DATE) - .toLocal() - .set({ - day: MAX_DATE.getUTCDate(), - month: MAX_DATE.getUTCMonth() + 1, - year: MAX_DATE.getUTCFullYear(), - hour: 0, - minute: 0, - second: 0, - millisecond: 0, - }); - - const selectedDay = within(datePicker).getByRole('option', { - selected: true, - name: (accessibleName) => { - // The name looks like "Choose Thursday, December 30th, 2100" - return accessibleName.includes(expectedDate.toFormat('yyyy')); - }, - }); - expect(selectedDay).toBeVisible(); - }), - ]); - }, -}; - -export const DateTimeSwitchesToStandaloneVariable: Story = { - args: { - mode: 'datetime', - label: 'Created At', - defaultValue: undefined, - onPersist: fn(), - VariablePicker: ({ onVariableSelect }) => { - return ( - - ); - }, - }, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - const addVariableButton = await canvas.findByText('Add variable'); - await userEvent.click(addVariableButton); - - const variableTag = await canvas.findByText('test'); - expect(variableTag).toBeVisible(); - - const removeVariableButton = canvas.getByTestId(/^remove-icon/); - - await Promise.all([ - userEvent.click(removeVariableButton), - - waitForElementToBeRemoved(variableTag), - waitFor(() => { - const input = canvas.getByPlaceholderText('mm/dd/yyyy hh:mm'); - expect(input).toBeVisible(); - }), - ]); - }, -}; - -export const DateTimeClickingOutsideDoesNotResetInputState: Story = { - args: { - mode: 'datetime', - label: 'Created At', - defaultValue: '2024-12-09T13:20:19.631Z', - onPersist: fn(), - }, - play: async ({ canvasElement, args }) => { - const defaultValueAsDisplayString = parseDateToString({ - date: new Date(args.defaultValue!), - isDateTimeInput: true, - userTimezone: undefined, - }); - - const canvas = within(canvasElement); - - const input = await canvas.findByPlaceholderText('mm/dd/yyyy hh:mm'); - expect(input).toBeVisible(); - expect(input).toHaveDisplayValue(defaultValueAsDisplayString); - - await userEvent.type(input, '{Backspace}{Backspace}'); - - const datePicker = await canvas.findByRole('dialog'); - expect(datePicker).toBeVisible(); - - await Promise.all([ - userEvent.click(canvasElement), - - waitForElementToBeRemoved(datePicker), - ]); - - expect(args.onPersist).not.toHaveBeenCalled(); - - expect(input).toHaveDisplayValue(defaultValueAsDisplayString.slice(0, -2)); - }, -};