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
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a0b56dc
feat: make a first implementation for the date field
Devessier Dec 5, 2024
4f10a97
feat: remove mask and use input's value as the new date whe pressing …
Devessier Dec 10, 2024
5ae1b2a
refactor: remove useDateTimeInput hook and extract parseDateToString …
Devessier Dec 11, 2024
6a3e9c2
fix: reset input date time when selecting a variable and default to a…
Devessier Dec 11, 2024
40813d3
refactor: move handlers to dedicated functions
Devessier Dec 11, 2024
2492029
refactor: move where React states are declared and remove useMemo
Devessier Dec 11, 2024
e0dc88b
feat: ensure the input's date doesn't go beyond date limits
Devessier Dec 11, 2024
3bbf078
fix: get user timezone from UserContext
Devessier Dec 11, 2024
4972aa7
fix: ensure we set the input date time in reaction for every date pic…
Devessier Dec 11, 2024
92684b7
refactor: rename variable
Devessier Dec 11, 2024
d8b3e79
refactor: prefer a local override
Devessier Dec 11, 2024
26b591a
refactor: extract parser formats in constants
Devessier Dec 11, 2024
fd2a5e4
refactor: simplify code
Devessier Dec 12, 2024
83743c0
test: write tests for form date field input
Devessier Dec 12, 2024
e2c575f
test: try to make date related tests pass on CI
Devessier Dec 12, 2024
0bed965
refactor: simplify condition
Devessier Dec 13, 2024
70640f4
refactor: use a smaller zIndex and delete comment
Devessier Dec 13, 2024
e8a9856
refactor: change editingMode to mode
Devessier Dec 13, 2024
9e9cd55
refactor: default parameter + simplify jsx condition
Devessier Dec 13, 2024
a8e8ef1
fix: keep the input state when clicking outside of the picker or pres…
Devessier Dec 13, 2024
f35ff7e
fix: use renamed components
Devessier Dec 13, 2024
00d6ccc
revert: revert updates on the DateInput component
Devessier Dec 16, 2024
246a4fb
refactor: directly use the InternalDatePicker instead of trying to ma…
Devessier Dec 16, 2024
2d707ea
revert: go back to previous version with untouched DateInput component
Devessier Dec 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { FormBooleanFieldInput } from '@/object-record/record-field/form-types/c
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';
import { FormDateFieldInput } from '@/object-record/record-field/form-types/components/FormDateFieldInput';
import { FormNumberFieldInput } from '@/object-record/record-field/form-types/components/FormNumberFieldInput';
import { FormSelectFieldInput } from '@/object-record/record-field/form-types/components/FormSelectFieldInput';
import { FormTextFieldInput } from '@/object-record/record-field/form-types/components/FormTextFieldInput';
Expand All @@ -20,6 +21,7 @@ import { isFieldBoolean } from '@/object-record/record-field/types/guards/isFiel
import { isFieldEmails } from '@/object-record/record-field/types/guards/isFieldEmails';
import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName';
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
import { isFieldDate } from '@/object-record/record-field/types/guards/isFieldDate';
import { isFieldNumber } from '@/object-record/record-field/types/guards/isFieldNumber';
import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect';
import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText';
Expand Down Expand Up @@ -98,5 +100,12 @@ export const FormFieldInput = ({
onPersist={onPersist}
VariablePicker={VariablePicker}
/>
) : 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

onPersist={onPersist}
VariablePicker={VariablePicker}
/>
) : null;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,341 @@
import { FormFieldInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputContainer';
import { FormFieldInputInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInputContainer';
import { FormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/FormFieldInputRowContainer';
import { VariableChip } from '@/object-record/record-field/form-types/components/VariableChip';
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
import { DateInput } from '@/ui/field/input/components/DateInput';
import { InputLabel } from '@/ui/input/components/InputLabel';
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 { parseStringToDate } from '@/ui/input/components/internal/date/utils/parseStringToDate';
import { UserContext } from '@/users/contexts/UserContext';
import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
import { css } from '@emotion/react';
import styled from '@emotion/styled';
import {
ChangeEvent,
KeyboardEvent,
useContext,
useId,
useRef,
useState,
} from 'react';
import { isDefined, Nullable, TEXT_INPUT_STYLE } from 'twenty-ui';

const StyledInputContainer = styled(FormFieldInputInputContainer)`
display: grid;
grid-template-columns: 1fr;
grid-template-rows: 1fr 0px;
overflow: visible;
position: relative;
`;

const StyledDateInputAbsoluteContainer = styled.div`
position: absolute;
`;

const StyledDateInput = styled.input<{ hasError?: boolean }>`
${TEXT_INPUT_STYLE}

${({ hasError, theme }) =>
hasError &&
css`
color: ${theme.color.red};
`};
`;

const StyledDateInputContainer = styled.div`
position: relative;
z-index: 1;
`;

type DraftValue =
| {
type: 'static';
value: string | null;
mode: 'view' | 'edit';
}
| {
type: 'variable';
value: string;
};

type FormDateFieldInputProps = {
label?: string;
defaultValue: string | undefined;
onPersist: (value: string | null) => void;
VariablePicker?: VariablePickerComponent;
};

export const FormDateFieldInput = ({
label,
defaultValue,
onPersist,
VariablePicker,
}: FormDateFieldInputProps) => {
const { timeZone } = useContext(UserContext);

const inputId = useId();

const [draftValue, setDraftValue] = useState<DraftValue>(
isStandaloneVariableString(defaultValue)
? {
type: 'variable',
value: defaultValue,
}
: {
type: 'static',
value: defaultValue ?? null,
mode: 'view',
},
);

const draftValueAsDate = isDefined(draftValue.value)
? new Date(draftValue.value)
: null;
Comment on lines +102 to +104
Copy link
Contributor

Choose a reason for hiding this comment

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

style: direct Date constructor usage with strings can be unreliable across browsers - consider using parseStringToDate here instead


const datePickerWrapperRef = useRef<HTMLDivElement>(null);

const [temporaryValue, setTemporaryValue] =
useState<Nullable<Date>>(draftValueAsDate);

const [inputDateTime, setInputDateTime] = useState(
isDefined(draftValueAsDate) && !isStandaloneVariableString(defaultValue)
? parseDateToString({
date: draftValueAsDate,
isDateTimeInput: false,
userTimezone: timeZone,
})
: '',
);

const persistDate = (newDate: Nullable<Date>) => {
if (!isDefined(newDate)) {
onPersist(null);
} else {
const newDateISO = newDate.toISOString();

onPersist(newDateISO);
}
};

const handlePickerChange = (newDate: Nullable<Date>) => {
setDraftValue({
type: 'static',
mode: 'edit',
value: newDate?.toDateString() ?? null,
});

setInputDateTime(
isDefined(newDate)
? parseDateToString({
date: newDate,
isDateTimeInput: false,
userTimezone: timeZone,
})
: '',
);

persistDate(newDate);
};

const handlePickerEnter = () => {};
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: empty handlePickerEnter function could lead to unexpected behavior when user presses enter in the picker


const handlePickerEscape = () => {
// FIXME: Escape key is not handled properly by the underlying DateInput component. We need to solve that.

setDraftValue({
type: 'static',
value: draftValue.value,
mode: 'view',
});
};

const handlePickerClickOutside = () => {
setDraftValue({
type: 'static',
value: draftValue.value,
mode: 'view',
});
};

const handlePickerClear = () => {
setDraftValue({
type: 'static',
value: null,
mode: 'view',
});

setTemporaryValue(null);

setInputDateTime('');

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

// 2
setDraftValue({
type: 'static',
value: newDate?.toDateString() ?? null,
mode: 'view',
});

setTemporaryValue(newDate);

setInputDateTime(
isDefined(newDate)
? parseDateToString({
date: newDate,
isDateTimeInput: false,
userTimezone: timeZone,
})
: '',
);

persistDate(newDate);
};

const handleInputFocus = () => {
setDraftValue({
type: 'static',
mode: 'edit',
value: draftValue.value,
});
};

const handleInputChange = (event: ChangeEvent<HTMLInputElement>) => {
setInputDateTime(event.target.value);
};

const handleInputKeydown = (event: KeyboardEvent<HTMLInputElement>) => {
if (event.key !== 'Enter') {
return;
}

const inputDateTimeTrimmed = inputDateTime.trim();

if (inputDateTimeTrimmed === '') {
handlePickerClear();

return;
}

const parsedInputDateTime = parseStringToDate({
dateAsString: inputDateTimeTrimmed,
isDateTimeInput: false,
userTimezone: timeZone,
});

if (!isDefined(parsedInputDateTime)) {
return;
}
Comment on lines +265 to +267
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 show error state when date parsing fails instead of silently returning


let validatedDate = parsedInputDateTime;
if (parsedInputDateTime < MIN_DATE) {
validatedDate = MIN_DATE;
} else if (parsedInputDateTime > MAX_DATE) {
validatedDate = MAX_DATE;
}

setDraftValue({
type: 'static',
value: validatedDate.toDateString(),
mode: 'edit',
});

setTemporaryValue(validatedDate);

setInputDateTime(
parseDateToString({
date: validatedDate,
isDateTimeInput: false,
userTimezone: timeZone,
}),
);

persistDate(validatedDate);
};

const handleVariableTagInsert = (variableName: string) => {
setDraftValue({
type: 'variable',
value: variableName,
});

setInputDateTime('');

onPersist(variableName);
};

const handleUnlinkVariable = () => {
setDraftValue({
type: 'static',
value: null,
mode: 'view',
});

setTemporaryValue(null);

onPersist(null);
};

return (
<FormFieldInputContainer>
{label ? <InputLabel>{label}</InputLabel> : null}

<FormFieldInputRowContainer>
<StyledInputContainer
ref={datePickerWrapperRef}
hasRightElement={isDefined(VariablePicker)}
>
{draftValue.type === 'static' ? (
<>
<StyledDateInput
type="text"
placeholder="mm/dd/yyyy"
value={inputDateTime}
onFocus={handleInputFocus}
onChange={handleInputChange}
onKeyDown={handleInputKeydown}
/>

{draftValue.mode === 'edit' ? (
<StyledDateInputContainer>
<StyledDateInputAbsoluteContainer>
<DateInput
clearable
onChange={handlePickerChange}
onEscape={handlePickerEscape}
onClickOutside={handlePickerClickOutside}
onEnter={handlePickerEnter}
onClear={handlePickerClear}
onSubmit={handlePickerSubmit}
hideHeaderInput
wrapperRef={datePickerWrapperRef}
temporaryValue={temporaryValue}
setTemporaryValue={setTemporaryValue}
/>
</StyledDateInputAbsoluteContainer>
</StyledDateInputContainer>
) : null}
</>
) : (
<VariableChip
rawVariableName={draftValue.value}
onRemove={handleUnlinkVariable}
/>
)}
</StyledInputContainer>

{VariablePicker ? (
<VariablePicker
inputId={inputId}
onVariableSelect={handleVariableTagInsert}
/>
) : null}
</FormFieldInputRowContainer>
</FormFieldInputContainer>
);
};
Loading
Loading