Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add date form field #9021

Merged
merged 24 commits into from
Dec 17, 2024
Merged

Add date form field #9021

merged 24 commits into from
Dec 17, 2024

Conversation

Devessier
Copy link
Contributor

@Devessier Devessier commented Dec 11, 2024

  • Add an option to hide the input from the packages/twenty-front/src/modules/ui/input/components/internal/date/components/AbsoluteDatePickerHeader.tsx component
  • Create a form field for dates
    • Integrate the picker
    • Create an input without a mask to let the user types a date and validate it by pressing the Enter key
  • Extract some utils to parse and format dates

@Devessier Devessier force-pushed the add-date-form-field branch 2 times, most recently from 1ce4d95 to 83abb94 Compare December 12, 2024 13:11
@Devessier Devessier marked this pull request as ready for review December 12, 2024 16:41
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Summary

This PR adds comprehensive date form field functionality with both manual text input and date picker UI, including timezone-aware date handling and validation.

  • Added new FormDateFieldInput component in /packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormDateFieldInput.tsx with unmasked date input validation and picker integration
  • Extracted date parsing logic into utility functions /packages/twenty-front/src/modules/ui/input/components/internal/date/utils/parseDateToString.ts and parseStringToDate.ts for consistent handling
  • Added US-centric date format constants (MM/dd/yyyy) in /packages/twenty-front/src/modules/ui/input/components/internal/date/constants/DateParserFormat.ts which may need internationalization
  • Moved state management up from DateInput to parent components for better control flow
  • Added comprehensive test coverage in FormDateFieldInput.stories.tsx including edge cases and validation scenarios

13 file(s) reviewed, 19 comment(s)
Edit PR Review Bot Settings | Greptile

) : isFieldDate(field) ? (
<FormDateFieldInput
label={field.label}
defaultValue={defaultValue as string | undefined}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: defaultValue should be typed as Date | string | undefined since dates can be passed as Date objects

expect(args.onPersist).toHaveBeenCalledWith('2024-12-08T00:00:00.000Z');
});

expect(dialog).toBeVisible();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Dialog should be removed after successful date entry - currently remains visible

Comment on lines +162 to +168
await Promise.all([
userEvent.type(input, '{Enter}'),

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Race condition possible between clear and Enter key - should verify input is actually empty before sending Enter

Comment on lines 73 to 74
const [temporaryValue, setTemporaryValue] =
useState<Nullable<Date>>(dateValue);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: temporaryValue state is initialized with dateValue but not updated when fieldValue changes. Add useEffect to sync or document why this is intentional

date={temporaryValue ?? new Date()}
onChange={handleChange}
onMouseSelect={handleMouseSelect}
clearable={clearable ? clearable : false}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: redundant ternary - can be simplified to clearable || false

? DATE_TIME_PARSER_FORMAT
: DATE_PARSER_FORMAT;

const dateParsed = DateTime.fromJSDate(date, { zone: userTimezone });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: potential timezone issue - if userTimezone is undefined, Luxon will use local timezone which may cause inconsistencies

Comment on lines +22 to +32
const dateWithoutTime = DateTime.fromJSDate(date)
.toLocal()
.set({
day: date.getUTCDate(),
month: date.getUTCMonth() + 1,
year: date.getUTCFullYear(),
hour: 0,
minute: 0,
second: 0,
millisecond: 0,
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: mixing UTC methods with toLocal() can lead to incorrect date conversions - consider using Luxon's built-in UTC handling instead of manual UTC getters

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why we did that. Handling dates is really hard, and I experienced several bugs related to dates in the app. I think we'll have to work on that topic at some point.

Comment on lines +21 to +22
? DateTime.fromFormat(dateAsString, parsingFormat, { zone: userTimezone })
: DateTime.fromFormat(dateAsString, parsingFormat, { zone: 'utc' });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: inconsistent timezone handling between date/datetime could cause issues - dates without times should probably use the user's timezone too to avoid off-by-one errors near midnight

Comment on lines +26 to +28
if (!isValid) {
return null;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: should validate dateAsString is not empty/undefined before parsing to avoid unnecessary Luxon operations

Comment on lines +20 to +22
const parsedDate = isDateTimeInput
? DateTime.fromFormat(dateAsString, parsingFormat, { zone: userTimezone })
: DateTime.fromFormat(dateAsString, parsingFormat, { zone: 'utc' });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: consider adding try/catch around DateTime.fromFormat() as it can throw exceptions for malformed inputs

Copy link
Contributor

@martmull martmull left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice code! Some comments

hideHeaderInput?: boolean;
temporaryValue: Nullable<Date>;
setTemporaryValue: (newValue: Nullable<Date>) => void;
wrapperRef: React.RefObject<HTMLElement>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you make temporaryValue, setTemporaryValue and wrapperRef arguments optional? Then you can leave the instanciations if props if they are undefined. It will avoid to update packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/DateTimeFieldInput.tsx

Copy link
Contributor Author

@Devessier Devessier Dec 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@charlesBochet wasn't satisfied with this code, and I asked him to take a look as he had an idea of the ideal API. Thanks for your suggestion, I know this part can be improved.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not convinced by the name temporary value, but I think it's not a bad idea to let parents provide temporaryValue, setTemporaryValue, and wrapperRef. It makes the component more easily customizable. I find it easier than making the arguments optional; I think it would make the child component less understandable—I don't think having an "optional" useState is common.

I can change my mind if we agree there is no better solution!

@@ -0,0 +1 @@
export const DATE_PARSER_FORMAT = 'MM/dd/yyyy';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can find the information in the database workspaceMember.dateFormat. The info is persisted in the recoil state dateTimeFormatState (see packages/twenty-front/src/modules/users/components/UserProviderEffect.tsx)

@@ -0,0 +1 @@
export const DATE_TIME_PARSER_FORMAT = 'MM/dd/yyyy HH:mm';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same for timeFormat

// Inspired by the StyledInlineCellInput component.
const StyledDateInputContainer = styled.div`
position: relative;
z-index: 1000;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would put 998 :D

Didn't is z-index: 1 enough?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't put much thought into it. As my comment says, I copied some part of the code of another component 😄 I will try to minimize the zIndex to 998 or 1.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried one, and it worked. That's less cool than 1000, though 😭

persistDate(newDate);
};

const handlePickerClickOutside = (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

content should be common with handlePickerEscape

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wrote the same code three times because I had taken inspiration from the other date pickers. In our case, the behavior is sometimes different. In the case of the form field date input, the input's content mustn't be reset when the user clicks outside the picker or presses Escape.

Please note that pressing Escape doesn't work at all. I created an issue as this problem should be solved in the InternalDatePicker and also exists for the date picker used on the RecordTable. See #9056

persistDate(null);
};

const handlePickerSubmit = (newDate: Nullable<Date>) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

@@ -32,10 +30,13 @@ export type DateInputProps = {
isDateTimeInput?: boolean;
onClear?: () => void;
onSubmit?: (newDate: Nullable<Date>) => void;
hideHeaderInput?: boolean;
temporaryValue: Nullable<Date>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really like temporaryValue as naming. It may make sense for your component but not for this one neither the other parents. Let's keep value or internalValue

Copy link
Contributor Author

@Devessier Devessier Dec 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with you that this could be better. I kept my v1 as Charles said he knew about a better API.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically speaking, the internalValue is, in fact, a temporary value every time the DateInput component is used.

On the Record Table, the final value is set when the user clicks outside or press enter.

In my case, I must control this internal value from the outside, and it doesn't make much sense to call it internalValue if its parent provides it 🤔

As a consequence, I need clarification on the best API. @charlesBochet said he had a better solution in mind.

Be sure to check edge cases. Previously, the component was unmounted when the field was saved. Now, we keep it displayed. We need to figure out how to solve the involved edge cases.
…and parseStringToDate functions to new files
…n empty string when the default value is a variable
@@ -32,10 +30,13 @@ export type DateInputProps = {
isDateTimeInput?: boolean;
onClear?: () => void;
onSubmit?: (newDate: Nullable<Date>) => void;
hideHeaderInput?: boolean;
temporaryValue: Nullable<Date>;
setTemporaryValue: (newValue: Nullable<Date>) => void;
Copy link
Contributor

@lucasbordeau lucasbordeau Dec 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Passing a setter as props is a code smell, it means either there is an inversion of layer of abstraction (child handling parent logic for example) or that we should instead use a Recoil state.

}: DateInputProps) => {
const [internalValue, setInternalValue] = useState(value);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should stay here

@Devessier Devessier merged commit 5bd73e0 into main Dec 17, 2024
19 checks passed
@Devessier Devessier deleted the add-date-form-field branch December 17, 2024 10:11
Copy link

Thanks @Devessier for your contribution!
This marks your 35th PR on the repo. You're top 1% of all our contributors 🎉
See contributor page - Share on LinkedIn - Share on Twitter

Contributions

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants