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

@ -1391,6 +1391,7 @@ export type FieldDeleteResponse = {
export enum FieldMetadataType {
Boolean = 'BOOLEAN',
Currency = 'CURRENCY',
Date = 'DATE',
DateTime = 'DATE_TIME',
Email = 'EMAIL',
FullName = 'FULL_NAME',

View File

@ -284,6 +284,7 @@ export enum FieldMetadataType {
Address = 'ADDRESS',
Boolean = 'BOOLEAN',
Currency = 'CURRENCY',
Date = 'DATE',
DateTime = 'DATE_TIME',
Email = 'EMAIL',
FullName = 'FULL_NAME',

View File

@ -194,6 +194,7 @@ export enum FieldMetadataType {
Address = 'ADDRESS',
Boolean = 'BOOLEAN',
Currency = 'CURRENCY',
Date = 'DATE',
DateTime = 'DATE_TIME',
Email = 'EMAIL',
FullName = 'FULL_NAME',

View File

@ -61,6 +61,8 @@ export const getFilterTypeFromFieldType = (fieldType: FieldMetadataType) => {
switch (fieldType) {
case FieldMetadataType.DateTime:
return 'DATE_TIME';
case FieldMetadataType.Date:
return 'DATE';
case FieldMetadataType.Link:
return 'LINK';
case FieldMetadataType.FullName:

View File

@ -12,6 +12,7 @@ export const formatFieldMetadataItemsAsSortDefinitions = ({
if (
![
FieldMetadataType.DateTime,
FieldMetadataType.Date,
FieldMetadataType.Number,
FieldMetadataType.Text,
FieldMetadataType.Boolean,

View File

@ -31,6 +31,7 @@ export const mapFieldMetadataToGraphQLQuery = ({
'TEXT',
'PHONE',
'DATE_TIME',
'DATE',
'EMAIL',
'NUMBER',
'BOOLEAN',

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:

View File

@ -71,6 +71,11 @@ export const SETTINGS_FIELD_TYPE_CONFIGS: Record<
Icon: IconCalendarEvent,
defaultValue: DEFAULT_DATE_VALUE.toISOString(),
},
[FieldMetadataType.Date]: {
label: 'Date',
Icon: IconCalendarEvent,
defaultValue: DEFAULT_DATE_VALUE.toISOString(),
},
[FieldMetadataType.Select]: {
label: 'Select',
Icon: IconTag,

View File

@ -63,6 +63,7 @@ const previewableTypes = [
FieldMetadataType.Boolean,
FieldMetadataType.Currency,
FieldMetadataType.DateTime,
FieldMetadataType.Date,
FieldMetadataType.Select,
FieldMetadataType.MultiSelect,
FieldMetadataType.Link,

View File

@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from 'react';
import { useRef, useState } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { flip, offset, useFloating } from '@floating-ui/react';
@ -14,8 +14,6 @@ const StyledCalendarContainer = styled.div`
border-radius: ${({ theme }) => theme.border.radius.md};
box-shadow: ${({ theme }) => theme.boxShadow.strong};
margin-top: 1px;
position: absolute;
z-index: 1;
@ -38,6 +36,7 @@ export type DateInputProps = {
hotkeyScope: string;
clearable?: boolean;
onChange?: (newDate: Nullable<Date>) => void;
isDateTimeInput?: boolean;
};
export const DateInput = ({
@ -48,6 +47,7 @@ export const DateInput = ({
onClickOutside,
clearable,
onChange,
isDateTimeInput,
}: DateInputProps) => {
const theme = useTheme();
@ -60,7 +60,7 @@ export const DateInput = ({
middleware: [
flip(),
offset({
mainAxis: theme.spacingMultiplicator * 2,
mainAxis: theme.spacingMultiplicator * -6,
}),
],
});
@ -70,10 +70,6 @@ export const DateInput = ({
onChange?.(newDate);
};
useEffect(() => {
setInternalValue(value);
}, [value]);
useRegisterInputEvents({
inputRef: wrapperRef,
inputValue: internalValue,
@ -99,6 +95,7 @@ export const DateInput = ({
onEnter(newDate);
}}
clearable={clearable ? clearable : false}
isDateTimeInput={isDateTimeInput}
/>
</StyledCalendarContainer>
</div>

View File

@ -1,6 +1,7 @@
import React from 'react';
import { useState } from 'react';
import ReactDatePicker from 'react-datepicker';
import styled from '@emotion/styled';
import { DateTime } from 'luxon';
import { IconCalendarX } from 'twenty-ui';
import { MenuItemLeftContent } from '@/ui/navigation/menu-item/internals/components/MenuItemLeftContent';
@ -235,7 +236,7 @@ const StyledButtonContainer = styled(StyledHoverableMenuItemBase)`
`;
const StyledButton = styled(MenuItemLeftContent)`
justify-content: center;
justify-content: start;
`;
export type InternalDatePickerProps = {
@ -243,24 +244,100 @@ export type InternalDatePickerProps = {
onMouseSelect?: (date: Date | null) => void;
onChange?: (date: Date) => void;
clearable?: boolean;
isDateTimeInput?: boolean;
};
const StyledInputContainer = styled.div`
width: 100%;
display: flex;
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
height: ${({ theme }) => theme.spacing(8)};
`;
const StyledInput = styled.input`
background: ${({ theme }) => theme.background.secondary};
border: none;
color: ${({ theme }) => theme.font.color.primary};
outline: none;
padding: 8px;
font-weight: 500;
font-size: ${({ theme }) => theme.font.size.md};
width: 100%;
`;
const PICKER_DATE_FORMAT = 'MM/dd/yyyy';
export const InternalDatePicker = ({
date,
onChange,
onMouseSelect,
clearable = true,
isDateTimeInput,
}: InternalDatePickerProps) => {
const handleClear = () => {
onMouseSelect?.(null);
};
const initialDate = date
? DateTime.fromJSDate(date).toFormat(PICKER_DATE_FORMAT)
: DateTime.now().toFormat(PICKER_DATE_FORMAT);
const [dateValue, setDateValue] = useState(initialDate);
const dateValueAsJSDate = DateTime.fromFormat(dateValue, PICKER_DATE_FORMAT)
.isValid
? DateTime.fromFormat(dateValue, PICKER_DATE_FORMAT).toJSDate()
: null;
return (
<StyledContainer>
<div className={clearable ? 'clearable ' : ''}>
<StyledInputContainer>
<StyledInput
type="text"
placeholder={`Type date${
isDateTimeInput ? ' and time' : ' (mm/dd/yyyy)'
}`}
inputMode="numeric"
value={dateValue}
onChange={(e) => {
const inputValue = e.target.value;
setDateValue(inputValue);
if (!isDateTimeInput) {
const parsedInputDate = DateTime.fromFormat(
inputValue,
PICKER_DATE_FORMAT,
{ zone: 'utc' },
);
const isValid = parsedInputDate.isValid;
if (isValid) {
onChange?.(parsedInputDate.toJSDate());
}
} else {
// TODO: implement time also
const parsedInputDate = DateTime.fromFormat(
inputValue,
PICKER_DATE_FORMAT,
{ zone: 'utc' },
);
const isValid = parsedInputDate.isValid;
if (isValid) {
onChange?.(parsedInputDate.toJSDate());
}
}
}}
/>
</StyledInputContainer>
<ReactDatePicker
open={true}
selected={date}
selected={dateValueAsJSDate}
value={dateValue}
showMonthDropdown
showYearDropdown
onChange={() => {
@ -268,10 +345,18 @@ export const InternalDatePicker = ({
}}
customInput={<></>}
onSelect={(date: Date, event) => {
// Setting the time to midnight might sometimes return the previous day
// We set to 21:00 to avoid any timezone issues
const dateForDateField = new Date(date.setHours(21, 0, 0, 0));
setDateValue(
DateTime.fromJSDate(date).toFormat(PICKER_DATE_FORMAT),
);
if (event?.type === 'click') {
onMouseSelect?.(date);
onMouseSelect?.(isDateTimeInput ? date : dateForDateField);
} else {
onChange?.(date);
onChange?.(isDateTimeInput ? date : dateForDateField);
}
}}
></ReactDatePicker>

View File

@ -65,6 +65,7 @@ export class TypeMapperService {
[FieldMetadataType.PHONE, GraphQLString],
[FieldMetadataType.EMAIL, GraphQLString],
[FieldMetadataType.DATE_TIME, dateScalar],
[FieldMetadataType.DATE, dateScalar],
[FieldMetadataType.BOOLEAN, GraphQLBoolean],
[FieldMetadataType.NUMBER, numberScalar],
[FieldMetadataType.NUMERIC, BigFloatScalarType],
@ -96,6 +97,7 @@ export class TypeMapperService {
[FieldMetadataType.PHONE, StringFilterType],
[FieldMetadataType.EMAIL, StringFilterType],
[FieldMetadataType.DATE_TIME, dateFilter],
[FieldMetadataType.DATE, DateFilterType],
[FieldMetadataType.BOOLEAN, BooleanFilterType],
[FieldMetadataType.NUMBER, numberScalar],
[FieldMetadataType.NUMERIC, BigFloatFilterType],
@ -117,6 +119,7 @@ export class TypeMapperService {
[FieldMetadataType.PHONE, OrderByDirectionType],
[FieldMetadataType.EMAIL, OrderByDirectionType],
[FieldMetadataType.DATE_TIME, OrderByDirectionType],
[FieldMetadataType.DATE, OrderByDirectionType],
[FieldMetadataType.BOOLEAN, OrderByDirectionType],
[FieldMetadataType.NUMBER, OrderByDirectionType],
[FieldMetadataType.NUMERIC, OrderByDirectionType],

View File

@ -20,6 +20,7 @@ export const mapFieldMetadataToGraphqlQuery = (
FieldMetadataType.TEXT,
FieldMetadataType.PHONE,
FieldMetadataType.DATE_TIME,
FieldMetadataType.DATE,
FieldMetadataType.EMAIL,
FieldMetadataType.NUMBER,
FieldMetadataType.SELECT,

View File

@ -31,6 +31,7 @@ const getSchemaComponentsProperties = (
case FieldMetadataType.PHONE:
case FieldMetadataType.EMAIL:
case FieldMetadataType.DATE_TIME:
case FieldMetadataType.DATE:
itemProperty.type = 'string';
break;
case FieldMetadataType.NUMBER:

View File

@ -58,6 +58,12 @@ export class FieldMetadataDefaultValueDateTime {
value: Date | null;
}
export class FieldMetadataDefaultValueDate {
@ValidateIf((object, value) => value !== null)
@IsDate()
value: Date | null;
}
export class FieldMetadataDefaultValueLink {
@ValidateIf((object, value) => value !== null)
@IsQuotedString()

View File

@ -23,6 +23,7 @@ export enum FieldMetadataType {
PHONE = 'PHONE',
EMAIL = 'EMAIL',
DATE_TIME = 'DATE_TIME',
DATE = 'DATE',
BOOLEAN = 'BOOLEAN',
NUMBER = 'NUMBER',
NUMERIC = 'NUMERIC',

View File

@ -27,6 +27,9 @@ type FieldMetadataDefaultValueMapping = {
[FieldMetadataType.DATE_TIME]:
| FieldMetadataDefaultValueDateTime
| FieldMetadataDefaultValueNowFunction;
[FieldMetadataType.DATE]:
| FieldMetadataDefaultValueDateTime
| FieldMetadataDefaultValueNowFunction;
[FieldMetadataType.BOOLEAN]: FieldMetadataDefaultValueBoolean;
[FieldMetadataType.NUMBER]: FieldMetadataDefaultValueNumber;
[FieldMetadataType.POSITION]: FieldMetadataDefaultValueNumber;

View File

@ -20,6 +20,7 @@ import {
FieldMetadataDefaultValueStringArray,
FieldMetadataDefaultValueNowFunction,
FieldMetadataDefaultValueUuidFunction,
FieldMetadataDefaultValueDate,
} from 'src/engine/metadata-modules/field-metadata/dtos/default-value.input';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
@ -35,6 +36,7 @@ export const defaultValueValidatorsMap = {
FieldMetadataDefaultValueDateTime,
FieldMetadataDefaultValueNowFunction,
],
[FieldMetadataType.DATE]: [FieldMetadataDefaultValueDate],
[FieldMetadataType.BOOLEAN]: [FieldMetadataDefaultValueBoolean],
[FieldMetadataType.NUMBER]: [FieldMetadataDefaultValueNumber],
[FieldMetadataType.NUMERIC]: [FieldMetadataDefaultValueString],

View File

@ -25,6 +25,7 @@ export type BasicFieldMetadataType =
| FieldMetadataType.BOOLEAN
| FieldMetadataType.POSITION
| FieldMetadataType.DATE_TIME
| FieldMetadataType.DATE
| FieldMetadataType.POSITION;
@Injectable()

View File

@ -25,6 +25,8 @@ export const fieldMetadataTypeToColumnType = <Type extends FieldMetadataType>(
return 'boolean';
case FieldMetadataType.DATE_TIME:
return 'timestamptz';
case FieldMetadataType.DATE:
return 'date';
case FieldMetadataType.RATING:
case FieldMetadataType.SELECT:
case FieldMetadataType.MULTI_SELECT:

View File

@ -74,6 +74,7 @@ export class WorkspaceMigrationFactory {
],
[FieldMetadataType.BOOLEAN, { factory: this.basicColumnActionFactory }],
[FieldMetadataType.DATE_TIME, { factory: this.basicColumnActionFactory }],
[FieldMetadataType.DATE, { factory: this.basicColumnActionFactory }],
[FieldMetadataType.RATING, { factory: this.enumColumnActionFactory }],
[FieldMetadataType.SELECT, { factory: this.enumColumnActionFactory }],
[

View File

@ -18,6 +18,8 @@ const getTypeFromFieldMetadataType = (
return 'string';
case FieldMetadataType.DATE_TIME:
return 'datetime';
case FieldMetadataType.DATE:
return 'date';
case FieldMetadataType.BOOLEAN:
return 'boolean';
case FieldMetadataType.NUMBER:
@ -177,6 +179,7 @@ export const computeInputFields = (
case FieldMetadataType.PHONE:
case FieldMetadataType.EMAIL:
case FieldMetadataType.DATE_TIME:
case FieldMetadataType.DATE:
case FieldMetadataType.BOOLEAN:
case FieldMetadataType.NUMBER:
case FieldMetadataType.NUMERIC:

View File

@ -36,6 +36,7 @@ export enum FieldMetadataType {
PHONE = 'PHONE',
EMAIL = 'EMAIL',
DATE_TIME = 'DATE_TIME',
DATE = 'DATE',
BOOLEAN = 'BOOLEAN',
NUMBER = 'NUMBER',
NUMERIC = 'NUMERIC',