New field type: DATE (#4876)

### Description
New field type: DATE

### Refs
https://github.com/twentyhq/twenty/issues/4377

### Demo

https://jam.dev/c/d0b59883-593c-4ca3-966b-c12d5d2e1c32

Fixes #4377

---------

Co-authored-by: gitstart-twenty <gitstart-twenty@users.noreply.github.com>
Co-authored-by: v1b3m <vibenjamin6@gmail.com>
Co-authored-by: Toledodev <rafael.toledo@engenharia.ufjf.br>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
gitstart-app[bot]
2024-04-11 17:29:29 +02:00
committed by GitHub
parent ca9cc86742
commit 7211730570
49 changed files with 354 additions and 62 deletions

View File

@ -3,6 +3,7 @@ export type FilterType =
| 'PHONE'
| 'EMAIL'
| 'DATE_TIME'
| 'DATE'
| 'NUMBER'
| 'CURRENCY'
| 'FULL_NAME'

View File

@ -15,6 +15,7 @@ export const getOperandsForFilterType = (
case 'CURRENCY':
case 'NUMBER':
case 'DATE_TIME':
case 'DATE':
return [ViewFilterOperand.GreaterThan, ViewFilterOperand.LessThan];
case 'RELATION':
case 'SELECT':

View File

@ -5,6 +5,7 @@ import { AddressFieldDisplay } from '../meta-types/display/components/AddressFie
import { ChipFieldDisplay } from '../meta-types/display/components/ChipFieldDisplay';
import { CurrencyFieldDisplay } from '../meta-types/display/components/CurrencyFieldDisplay';
import { DateFieldDisplay } from '../meta-types/display/components/DateFieldDisplay';
import { DateTimeFieldDisplay } from '../meta-types/display/components/DateTimeFieldDisplay';
import { EmailFieldDisplay } from '../meta-types/display/components/EmailFieldDisplay';
import { FullNameFieldDisplay } from '../meta-types/display/components/FullNameFieldDisplay';
import { JsonFieldDisplay } from '../meta-types/display/components/JsonFieldDisplay';
@ -18,6 +19,7 @@ import { TextFieldDisplay } from '../meta-types/display/components/TextFieldDisp
import { UuidFieldDisplay } from '../meta-types/display/components/UuidFieldDisplay';
import { isFieldAddress } from '../types/guards/isFieldAddress';
import { isFieldCurrency } from '../types/guards/isFieldCurrency';
import { isFieldDate } from '../types/guards/isFieldDate';
import { isFieldDateTime } from '../types/guards/isFieldDateTime';
import { isFieldEmail } from '../types/guards/isFieldEmail';
import { isFieldFullName } from '../types/guards/isFieldFullName';
@ -53,6 +55,8 @@ export const FieldDisplay = () => {
) : isFieldEmail(fieldDefinition) ? (
<EmailFieldDisplay />
) : isFieldDateTime(fieldDefinition) ? (
<DateTimeFieldDisplay />
) : isFieldDate(fieldDefinition) ? (
<DateFieldDisplay />
) : isFieldNumber(fieldDefinition) ? (
<NumberFieldDisplay />

View File

@ -1,11 +1,13 @@
import { useContext } from 'react';
import { AddressFieldInput } from '@/object-record/record-field/meta-types/input/components/AddressFieldInput';
import { DateFieldInput } from '@/object-record/record-field/meta-types/input/components/DateFieldInput';
import { FullNameFieldInput } from '@/object-record/record-field/meta-types/input/components/FullNameFieldInput';
import { MultiSelectFieldInput } from '@/object-record/record-field/meta-types/input/components/MultiSelectFieldInput.tsx';
import { RawJsonFieldInput } from '@/object-record/record-field/meta-types/input/components/RawJsonFieldInput';
import { SelectFieldInput } from '@/object-record/record-field/meta-types/input/components/SelectFieldInput';
import { RecordFieldInputScope } from '@/object-record/record-field/scopes/RecordFieldInputScope';
import { isFieldDate } from '@/object-record/record-field/types/guards/isFieldDate';
import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName';
import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect.ts';
import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson';
@ -15,7 +17,7 @@ import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/get
import { FieldContext } from '../contexts/FieldContext';
import { BooleanFieldInput } from '../meta-types/input/components/BooleanFieldInput';
import { CurrencyFieldInput } from '../meta-types/input/components/CurrencyFieldInput';
import { DateFieldInput } from '../meta-types/input/components/DateFieldInput';
import { DateTimeFieldInput } from '../meta-types/input/components/DateTimeFieldInput';
import { EmailFieldInput } from '../meta-types/input/components/EmailFieldInput';
import { LinkFieldInput } from '../meta-types/input/components/LinkFieldInput';
import { NumberFieldInput } from '../meta-types/input/components/NumberFieldInput';
@ -98,6 +100,12 @@ export const FieldInput = ({
onShiftTab={onShiftTab}
/>
) : isFieldDateTime(fieldDefinition) ? (
<DateTimeFieldInput
onEnter={onEnter}
onEscape={onEscape}
onClickOutside={onClickOutside}
/>
) : isFieldDate(fieldDefinition) ? (
<DateFieldInput
onEnter={onEnter}
onEscape={onEscape}

View File

@ -3,6 +3,8 @@ import { useRecoilCallback } from 'recoil';
import { isFieldAddress } from '@/object-record/record-field/types/guards/isFieldAddress';
import { isFieldAddressValue } from '@/object-record/record-field/types/guards/isFieldAddressValue';
import { isFieldDate } from '@/object-record/record-field/types/guards/isFieldDate';
import { isFieldDateValue } from '@/object-record/record-field/types/guards/isFieldDateValue';
import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName';
import { isFieldFullNameValue } from '@/object-record/record-field/types/guards/isFieldFullNameValue';
import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect.ts';
@ -61,6 +63,9 @@ export const usePersistField = () => {
isFieldDateTime(fieldDefinition) &&
isFieldDateTimeValue(valueToPersist);
const fieldIsDate =
isFieldDate(fieldDefinition) && isFieldDateValue(valueToPersist);
const fieldIsLink =
isFieldLink(fieldDefinition) && isFieldLinkValue(valueToPersist);
@ -108,6 +113,7 @@ export const usePersistField = () => {
fieldIsProbability ||
fieldIsNumber ||
fieldIsDateTime ||
fieldIsDate ||
fieldIsPhone ||
fieldIsLink ||
fieldIsCurrency ||

View File

@ -1,9 +1,9 @@
import { DateDisplay } from '@/ui/field/display/components/DateDisplay';
import { useDateTimeField } from '../../hooks/useDateTimeField';
import { useDateField } from '../../hooks/useDateField';
export const DateFieldDisplay = () => {
const { fieldValue } = useDateTimeField();
const { fieldValue } = useDateField();
return <DateDisplay value={fieldValue} />;
};

View File

@ -0,0 +1,9 @@
import { DateDisplay } from '@/ui/field/display/components/DateDisplay';
import { useDateTimeField } from '../../hooks/useDateTimeField';
export const DateTimeFieldDisplay = () => {
const { fieldValue } = useDateTimeField();
return <DateDisplay value={fieldValue} />;
};

View File

@ -6,7 +6,7 @@ import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { FieldContext } from '../../../../contexts/FieldContext';
import { useDateTimeField } from '../../../hooks/useDateTimeField';
import { DateFieldDisplay } from '../DateFieldDisplay';
import { DateTimeFieldDisplay } from '../DateTimeFieldDisplay';
const formattedDate = new Date('2023-04-01');
@ -47,7 +47,7 @@ const meta: Meta = {
),
ComponentDecorator,
],
component: DateFieldDisplay,
component: DateTimeFieldDisplay,
argTypes: { value: { control: 'date' } },
args: {
value: formattedDate,
@ -56,7 +56,7 @@ const meta: Meta = {
export default meta;
type Story = StoryObj<typeof DateFieldDisplay>;
type Story = StoryObj<typeof DateTimeFieldDisplay>;
export const Default: Story = {};

View File

@ -0,0 +1,40 @@
import { useContext } from 'react';
import { useRecoilState } from 'recoil';
import { useRecordFieldInput } from '@/object-record/record-field/hooks/useRecordFieldInput';
import { FieldDateValue } from '@/object-record/record-field/types/FieldMetadata';
import { isFieldDate } from '@/object-record/record-field/types/guards/isFieldDate';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { FieldContext } from '../../contexts/FieldContext';
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
export const useDateField = () => {
const { entityId, fieldDefinition, hotkeyScope, clearable } =
useContext(FieldContext);
assertFieldMetadata(FieldMetadataType.Date, isFieldDate, fieldDefinition);
const fieldName = fieldDefinition.metadata.fieldName;
const [fieldValue, setFieldValue] = useRecoilState<string>(
recordStoreFamilySelector({
recordId: entityId,
fieldName: fieldName,
}),
);
const { setDraftValue } = useRecordFieldInput<FieldDateValue>(
`${entityId}-${fieldName}`,
);
return {
fieldDefinition,
fieldValue,
setDraftValue,
setFieldValue,
hotkeyScope,
clearable,
};
};

View File

@ -3,7 +3,7 @@ import { BooleanInput } from '@/ui/field/input/components/BooleanInput';
import { usePersistField } from '../../../hooks/usePersistField';
import { useBooleanField } from '../../hooks/useBooleanField';
import { FieldInputEvent } from './DateFieldInput';
import { FieldInputEvent } from './DateTimeFieldInput';
export type BooleanFieldInputProps = {
onSubmit?: FieldInputEvent;

View File

@ -5,7 +5,7 @@ import { CurrencyInput } from '@/ui/field/input/components/CurrencyInput';
import { FieldInputOverlay } from '../../../../../ui/field/input/components/FieldInputOverlay';
import { useCurrencyField } from '../../hooks/useCurrencyField';
import { FieldInputEvent } from './DateFieldInput';
import { FieldInputEvent } from './DateTimeFieldInput';
export type CurrencyFieldInputProps = {
onClickOutside?: FieldInputEvent;

View File

@ -1,8 +1,8 @@
import { useDateField } from '@/object-record/record-field/meta-types/hooks/useDateField';
import { DateInput } from '@/ui/field/input/components/DateInput';
import { Nullable } from '~/types/Nullable';
import { usePersistField } from '../../../hooks/usePersistField';
import { useDateTimeField } from '../../hooks/useDateTimeField';
export type FieldInputEvent = (persist: () => void) => void;
@ -17,8 +17,7 @@ export const DateFieldInput = ({
onEscape,
onClickOutside,
}: DateFieldInputProps) => {
const { fieldValue, hotkeyScope, clearable, setDraftValue } =
useDateTimeField();
const { fieldValue, hotkeyScope, setDraftValue } = useDateField();
const persistField = usePersistField();
@ -33,6 +32,7 @@ export const DateFieldInput = ({
};
const handleEnter = (newDate: Nullable<Date>) => {
console.log('newDate enter', newDate);
onEnter?.(() => persistDate(newDate));
};
@ -60,7 +60,7 @@ export const DateFieldInput = ({
onEnter={handleEnter}
onEscape={handleEscape}
value={dateValue}
clearable={clearable}
clearable
onChange={handleChange}
/>
);

View File

@ -0,0 +1,68 @@
import { DateInput } from '@/ui/field/input/components/DateInput';
import { Nullable } from '~/types/Nullable';
import { usePersistField } from '../../../hooks/usePersistField';
import { useDateTimeField } from '../../hooks/useDateTimeField';
export type FieldInputEvent = (persist: () => void) => void;
export type DateTimeFieldInputProps = {
onClickOutside?: FieldInputEvent;
onEnter?: FieldInputEvent;
onEscape?: FieldInputEvent;
};
export const DateTimeFieldInput = ({
onEnter,
onEscape,
onClickOutside,
}: DateTimeFieldInputProps) => {
const { fieldValue, hotkeyScope, clearable, setDraftValue } =
useDateTimeField();
const persistField = usePersistField();
const persistDate = (newDate: Nullable<Date>) => {
if (!newDate) {
persistField(null);
} else {
const newDateISO = newDate?.toISOString();
persistField(newDateISO);
}
};
const handleEnter = (newDate: Nullable<Date>) => {
onEnter?.(() => persistDate(newDate));
};
const handleEscape = (newDate: Nullable<Date>) => {
onEscape?.(() => persistDate(newDate));
};
const handleClickOutside = (
_event: MouseEvent | TouchEvent,
newDate: Nullable<Date>,
) => {
onClickOutside?.(() => persistDate(newDate));
};
const handleChange = (newDate: Nullable<Date>) => {
setDraftValue(newDate?.toDateString() ?? '');
};
const dateValue = fieldValue ? new Date(fieldValue) : null;
return (
<DateInput
hotkeyScope={hotkeyScope}
onClickOutside={handleClickOutside}
onEnter={handleEnter}
onEscape={handleEscape}
value={dateValue}
clearable={clearable}
onChange={handleChange}
isDateTimeInput
/>
);
};

View File

@ -4,7 +4,7 @@ import { FieldInputOverlay } from '../../../../../ui/field/input/components/Fiel
import { usePersistField } from '../../../hooks/usePersistField';
import { useEmailField } from '../../hooks/useEmailField';
import { FieldInputEvent } from './DateFieldInput';
import { FieldInputEvent } from './DateTimeFieldInput';
export type EmailFieldInputProps = {
onClickOutside?: FieldInputEvent;

View File

@ -5,7 +5,7 @@ import { FieldInputOverlay } from '@/ui/field/input/components/FieldInputOverlay
import { usePersistField } from '../../../hooks/usePersistField';
import { FieldInputEvent } from './DateFieldInput';
import { FieldInputEvent } from './DateTimeFieldInput';
const FIRST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS =
'First name';

View File

@ -3,7 +3,7 @@ import { TextInput } from '@/ui/field/input/components/TextInput';
import { FieldInputOverlay } from '../../../../../ui/field/input/components/FieldInputOverlay';
import { useLinkField } from '../../hooks/useLinkField';
import { FieldInputEvent } from './DateFieldInput';
import { FieldInputEvent } from './DateTimeFieldInput';
export type LinkFieldInputProps = {
onClickOutside?: FieldInputEvent;

View File

@ -3,7 +3,7 @@ import { PhoneInput } from '@/ui/field/input/components/PhoneInput';
import { FieldInputOverlay } from '../../../../../ui/field/input/components/FieldInputOverlay';
import { usePhoneField } from '../../hooks/usePhoneField';
import { FieldInputEvent } from './DateFieldInput';
import { FieldInputEvent } from './DateTimeFieldInput';
export type PhoneFieldInputProps = {
onClickOutside?: FieldInputEvent;

View File

@ -4,7 +4,7 @@ import { RatingInput } from '@/ui/field/input/components/RatingInput';
import { usePersistField } from '../../../hooks/usePersistField';
import { useRatingField } from '../../hooks/useRatingField';
import { FieldInputEvent } from './DateFieldInput';
import { FieldInputEvent } from './DateTimeFieldInput';
export type RatingFieldInputProps = {
onSubmit?: FieldInputEvent;

View File

@ -6,7 +6,7 @@ import { EntityForSelect } from '@/object-record/relation-picker/types/EntityFor
import { usePersistField } from '../../../hooks/usePersistField';
import { useRelationField } from '../../hooks/useRelationField';
import { FieldInputEvent } from './DateFieldInput';
import { FieldInputEvent } from './DateTimeFieldInput';
const StyledRelationPickerContainer = styled.div`
left: -1px;

View File

@ -4,7 +4,7 @@ import { TextAreaInput } from '@/ui/field/input/components/TextAreaInput';
import { usePersistField } from '../../../hooks/usePersistField';
import { useTextField } from '../../hooks/useTextField';
import { FieldInputEvent } from './DateFieldInput';
import { FieldInputEvent } from './DateTimeFieldInput';
export type TextFieldInputProps = {
onClickOutside?: FieldInputEvent;

View File

@ -7,7 +7,10 @@ import { FieldMetadataType } from '~/generated/graphql';
import { FieldContextProvider } from '../../../__stories__/FieldContextProvider';
import { useDateTimeField } from '../../../hooks/useDateTimeField';
import { DateFieldInput, DateFieldInputProps } from '../DateFieldInput';
import {
DateTimeFieldInput,
DateTimeFieldInputProps,
} from '../DateTimeFieldInput';
const formattedDate = new Date(2022, 1, 1);
@ -21,7 +24,7 @@ const DateFieldValueSetterEffect = ({ value }: { value: Date }) => {
return <></>;
};
type DateFieldInputWithContextProps = DateFieldInputProps & {
type DateFieldInputWithContextProps = DateTimeFieldInputProps & {
value: Date;
entityId?: string;
};
@ -55,7 +58,7 @@ const DateFieldInputWithContext = ({
entityId={entityId}
>
<DateFieldValueSetterEffect value={value} />
<DateFieldInput
<DateTimeFieldInput
onEscape={onEscape}
onEnter={onEnter}
onClickOutside={onClickOutside}

View File

@ -26,6 +26,12 @@ export type FieldDateTimeMetadata = {
fieldName: string;
};
export type FieldDateMetadata = {
objectMetadataNameSingular?: string;
placeHolder: string;
fieldName: string;
};
export type FieldNumberMetadata = {
objectMetadataNameSingular?: string;
fieldName: string;
@ -113,6 +119,7 @@ export type FieldMetadata =
| FieldBooleanMetadata
| FieldCurrencyMetadata
| FieldDateTimeMetadata
| FieldDateMetadata
| FieldEmailMetadata
| FieldFullNameMetadata
| FieldLinkMetadata
@ -129,6 +136,7 @@ export type FieldMetadata =
export type FieldTextValue = string;
export type FieldUUidValue = string;
export type FieldDateTimeValue = string | null;
export type FieldDateValue = string | null;
export type FieldNumberValue = number | null;
export type FieldBooleanValue = boolean;

View File

@ -5,6 +5,7 @@ import {
FieldAddressMetadata,
FieldBooleanMetadata,
FieldCurrencyMetadata,
FieldDateMetadata,
FieldDateTimeMetadata,
FieldEmailMetadata,
FieldFullNameMetadata,
@ -31,33 +32,35 @@ type AssertFieldMetadataFunction = <
? FieldFullNameMetadata
: E extends 'DATE_TIME'
? FieldDateTimeMetadata
: E extends 'EMAIL'
? FieldEmailMetadata
: E extends 'SELECT'
? FieldSelectMetadata
: E extends 'MULTI_SELECT'
? FieldMultiSelectMetadata
: E extends 'RATING'
? FieldRatingMetadata
: E extends 'LINK'
? FieldLinkMetadata
: E extends 'NUMBER'
? FieldNumberMetadata
: E extends 'PHONE'
? FieldPhoneMetadata
: E extends 'PROBABILITY'
? FieldRatingMetadata
: E extends 'RELATION'
? FieldRelationMetadata
: E extends 'TEXT'
? FieldTextMetadata
: E extends 'UUID'
? FieldUuidMetadata
: E extends 'ADDRESS'
? FieldAddressMetadata
: E extends 'RAW_JSON'
? FieldRawJsonMetadata
: never,
: E extends 'DATE'
? FieldDateMetadata
: E extends 'EMAIL'
? FieldEmailMetadata
: E extends 'SELECT'
? FieldSelectMetadata
: E extends 'MULTI_SELECT'
? FieldMultiSelectMetadata
: E extends 'RATING'
? FieldRatingMetadata
: E extends 'LINK'
? FieldLinkMetadata
: E extends 'NUMBER'
? FieldNumberMetadata
: E extends 'PHONE'
? FieldPhoneMetadata
: E extends 'PROBABILITY'
? FieldRatingMetadata
: E extends 'RELATION'
? FieldRelationMetadata
: E extends 'TEXT'
? FieldTextMetadata
: E extends 'UUID'
? FieldUuidMetadata
: E extends 'ADDRESS'
? FieldAddressMetadata
: E extends 'RAW_JSON'
? FieldRawJsonMetadata
: never,
>(
fieldType: E,
fieldTypeGuard: (

View File

@ -0,0 +1,6 @@
import { FieldDefinition } from '../FieldDefinition';
import { FieldDateMetadata, FieldMetadata } from '../FieldMetadata';
export const isFieldDate = (
field: Pick<FieldDefinition<FieldMetadata>, 'type'>,
): field is FieldDefinition<FieldDateMetadata> => field.type === 'DATE';

View File

@ -0,0 +1,10 @@
import { isNull, isString } from '@sniptt/guards';
import { FieldDateValue } from '@/object-record/record-field/types/FieldMetadata';
// TODO: add zod
export const isFieldDateValue = (
fieldValue: unknown,
): fieldValue is FieldDateValue =>
(isString(fieldValue) && !isNaN(Date.parse(fieldValue))) ||
isNull(fieldValue);

View File

@ -5,6 +5,7 @@ import { isFieldAddressValue } from '@/object-record/record-field/types/guards/i
import { isFieldBoolean } from '@/object-record/record-field/types/guards/isFieldBoolean';
import { isFieldCurrency } from '@/object-record/record-field/types/guards/isFieldCurrency';
import { isFieldCurrencyValue } from '@/object-record/record-field/types/guards/isFieldCurrencyValue';
import { isFieldDate } from '@/object-record/record-field/types/guards/isFieldDate';
import { isFieldDateTime } from '@/object-record/record-field/types/guards/isFieldDateTime';
import { isFieldEmail } from '@/object-record/record-field/types/guards/isFieldEmail';
import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName';
@ -38,6 +39,7 @@ export const isFieldValueEmpty = ({
isFieldUuid(fieldDefinition) ||
isFieldText(fieldDefinition) ||
isFieldDateTime(fieldDefinition) ||
isFieldDate(fieldDefinition) ||
isFieldNumber(fieldDefinition) ||
isFieldRating(fieldDefinition) ||
isFieldEmail(fieldDefinition) ||

View File

@ -39,6 +39,9 @@ export const generateEmptyFieldValue = (
case FieldMetadataType.DateTime: {
return null;
}
case FieldMetadataType.Date: {
return null;
}
case FieldMetadataType.Number:
case FieldMetadataType.Rating:
case FieldMetadataType.Position: