Implements #5398. --------- Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com> Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
@ -0,0 +1,25 @@
|
||||
import {
|
||||
differenceInDays,
|
||||
formatDistance,
|
||||
isToday,
|
||||
startOfDay,
|
||||
} from 'date-fns';
|
||||
|
||||
export const formatDateISOStringToRelativeDate = (
|
||||
isoDate: string,
|
||||
isDayMaximumPrecision = false,
|
||||
) => {
|
||||
const now = new Date();
|
||||
const targetDate = new Date(isoDate);
|
||||
|
||||
if (isDayMaximumPrecision && isToday(targetDate)) return 'Today';
|
||||
|
||||
const isWithin24h = Math.abs(differenceInDays(targetDate, now)) < 1;
|
||||
|
||||
if (isDayMaximumPrecision || !isWithin24h)
|
||||
return formatDistance(startOfDay(targetDate), startOfDay(now), {
|
||||
addSuffix: true,
|
||||
});
|
||||
|
||||
return formatDistance(targetDate, now, { addSuffix: true });
|
||||
};
|
||||
@ -35,6 +35,7 @@ export const CREATE_ONE_FIELD_METADATA_ITEM = gql`
|
||||
isNullable
|
||||
createdAt
|
||||
updatedAt
|
||||
settings
|
||||
defaultValue
|
||||
options
|
||||
}
|
||||
@ -73,6 +74,7 @@ export const UPDATE_ONE_FIELD_METADATA_ITEM = gql`
|
||||
isNullable
|
||||
createdAt
|
||||
updatedAt
|
||||
settings
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -136,6 +138,7 @@ export const DELETE_ONE_FIELD_METADATA_ITEM = gql`
|
||||
isNullable
|
||||
createdAt
|
||||
updatedAt
|
||||
settings
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@ -41,6 +41,7 @@ export const FIND_MANY_OBJECT_METADATA_ITEMS = gql`
|
||||
updatedAt
|
||||
defaultValue
|
||||
options
|
||||
settings
|
||||
relationDefinition {
|
||||
relationId
|
||||
direction
|
||||
|
||||
@ -17,6 +17,7 @@ const baseFields = `
|
||||
isNullable
|
||||
createdAt
|
||||
updatedAt
|
||||
settings
|
||||
`;
|
||||
|
||||
export const queries = {
|
||||
@ -73,6 +74,7 @@ export const variables = {
|
||||
label: 'fieldLabel',
|
||||
name: 'fieldLabel',
|
||||
options: undefined,
|
||||
settings: undefined,
|
||||
objectMetadataId,
|
||||
type: 'TEXT',
|
||||
},
|
||||
@ -96,6 +98,7 @@ const defaultResponseData = {
|
||||
isNullable: false,
|
||||
createdAt: '1977-09-28T13:56:55.157Z',
|
||||
updatedAt: '1996-10-10T08:27:57.117Z',
|
||||
settings: undefined,
|
||||
};
|
||||
|
||||
const fieldRelationResponseData = {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useDeleteOneRelationMetadataItem } from '@/object-metadata/hooks/useDeleteOneRelationMetadataItem';
|
||||
import { Field } from '~/generated/graphql';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { Field } from '~/generated/graphql';
|
||||
|
||||
import { FieldMetadataItem } from '../types/FieldMetadataItem';
|
||||
import { formatFieldMetadataItemInput } from '../utils/formatFieldMetadataItemInput';
|
||||
@ -18,7 +18,13 @@ export const useFieldMetadataItem = () => {
|
||||
const createMetadataField = (
|
||||
input: Pick<
|
||||
Field,
|
||||
'label' | 'icon' | 'description' | 'defaultValue' | 'type' | 'options'
|
||||
| 'label'
|
||||
| 'icon'
|
||||
| 'description'
|
||||
| 'defaultValue'
|
||||
| 'type'
|
||||
| 'options'
|
||||
| 'settings'
|
||||
> & {
|
||||
objectMetadataId: string;
|
||||
},
|
||||
|
||||
@ -36,4 +36,7 @@ export type FieldMetadataItem = Omit<
|
||||
'id' | 'nameSingular' | 'namePlural'
|
||||
>;
|
||||
} | null;
|
||||
settings?: {
|
||||
displayAsRelativeDate?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
@ -37,6 +37,7 @@ export const formatFieldMetadataItemAsFieldDefinition = ({
|
||||
targetFieldMetadataName:
|
||||
field.relationDefinition?.targetFieldMetadata?.name ?? '',
|
||||
options: field.options,
|
||||
settings: field.settings,
|
||||
isNullable: field.isNullable,
|
||||
};
|
||||
|
||||
|
||||
@ -5,7 +5,13 @@ export const formatFieldMetadataItemInput = (
|
||||
input: Partial<
|
||||
Pick<
|
||||
FieldMetadataItem,
|
||||
'type' | 'label' | 'defaultValue' | 'icon' | 'description' | 'options'
|
||||
| 'type'
|
||||
| 'label'
|
||||
| 'defaultValue'
|
||||
| 'icon'
|
||||
| 'description'
|
||||
| 'options'
|
||||
| 'settings'
|
||||
>
|
||||
>,
|
||||
) => {
|
||||
@ -18,5 +24,6 @@ export const formatFieldMetadataItemInput = (
|
||||
label,
|
||||
name: label ? computeMetadataNameFromLabelOrThrow(label) : undefined,
|
||||
options: input.options,
|
||||
settings: input.settings,
|
||||
};
|
||||
};
|
||||
|
||||
@ -35,6 +35,7 @@ export const fieldMetadataItemSchema = (existingLabels?: string[]) => {
|
||||
)
|
||||
.nullable()
|
||||
.optional(),
|
||||
settings: z.any().optional(),
|
||||
relationDefinition: z
|
||||
.object({
|
||||
__typename: z.literal('RelationDefinition').optional(),
|
||||
|
||||
@ -2,7 +2,15 @@ import { useDateFieldDisplay } from '@/object-record/record-field/meta-types/hoo
|
||||
import { DateDisplay } from '@/ui/field/display/components/DateDisplay';
|
||||
|
||||
export const DateFieldDisplay = () => {
|
||||
const { fieldValue } = useDateFieldDisplay();
|
||||
const { fieldValue, fieldDefinition } = useDateFieldDisplay();
|
||||
|
||||
return <DateDisplay value={fieldValue} />;
|
||||
const displayAsRelativeDate =
|
||||
fieldDefinition.metadata?.settings?.displayAsRelativeDate;
|
||||
|
||||
return (
|
||||
<DateDisplay
|
||||
value={fieldValue}
|
||||
displayAsRelativeDate={displayAsRelativeDate}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -2,7 +2,15 @@ import { useDateTimeFieldDisplay } from '@/object-record/record-field/meta-types
|
||||
import { DateTimeDisplay } from '@/ui/field/display/components/DateTimeDisplay';
|
||||
|
||||
export const DateTimeFieldDisplay = () => {
|
||||
const { fieldValue } = useDateTimeFieldDisplay();
|
||||
const { fieldValue, fieldDefinition } = useDateTimeFieldDisplay();
|
||||
|
||||
return <DateTimeDisplay value={fieldValue} />;
|
||||
const displayAsRelativeDate =
|
||||
fieldDefinition.metadata?.settings?.displayAsRelativeDate;
|
||||
|
||||
return (
|
||||
<DateTimeDisplay
|
||||
value={fieldValue}
|
||||
displayAsRelativeDate={displayAsRelativeDate}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -2,6 +2,8 @@ import { useContext } from 'react';
|
||||
|
||||
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
|
||||
|
||||
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
|
||||
import { FieldDateMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { FieldContext } from '../../contexts/FieldContext';
|
||||
|
||||
export const useDateFieldDisplay = () => {
|
||||
@ -16,7 +18,10 @@ export const useDateFieldDisplay = () => {
|
||||
);
|
||||
|
||||
return {
|
||||
fieldDefinition,
|
||||
// TODO: we have to use this because we removed the assertion that would have otherwise narrowed the type because
|
||||
// it impacts performance. We should find a way to assert the type in a way that doesn't impact performance.
|
||||
// Maybe a level above ?
|
||||
fieldDefinition: fieldDefinition as FieldDefinition<FieldDateMetadata>,
|
||||
fieldValue,
|
||||
hotkeyScope,
|
||||
clearable,
|
||||
|
||||
@ -2,6 +2,8 @@ import { useContext } from 'react';
|
||||
|
||||
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
|
||||
|
||||
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
|
||||
import { FieldDateTimeMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { FieldContext } from '../../contexts/FieldContext';
|
||||
|
||||
export const useDateTimeFieldDisplay = () => {
|
||||
@ -16,7 +18,7 @@ export const useDateTimeFieldDisplay = () => {
|
||||
);
|
||||
|
||||
return {
|
||||
fieldDefinition,
|
||||
fieldDefinition: fieldDefinition as FieldDefinition<FieldDateTimeMetadata>,
|
||||
fieldValue,
|
||||
hotkeyScope,
|
||||
clearable,
|
||||
|
||||
@ -27,12 +27,18 @@ export type FieldDateTimeMetadata = {
|
||||
objectMetadataNameSingular?: string;
|
||||
placeHolder: string;
|
||||
fieldName: string;
|
||||
settings?: {
|
||||
displayAsRelativeDate?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type FieldDateMetadata = {
|
||||
objectMetadataNameSingular?: string;
|
||||
placeHolder: string;
|
||||
fieldName: string;
|
||||
settings?: {
|
||||
displayAsRelativeDate?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type FieldNumberMetadata = {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { ReactNode } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { StyledFormCardTitle } from '@/settings/data-model/fields/components/StyledFormCardTitle';
|
||||
import { Card } from '@/ui/layout/card/components/Card';
|
||||
import { CardContent } from '@/ui/layout/card/components/CardContent';
|
||||
|
||||
@ -14,14 +15,6 @@ const StyledPreviewContainer = styled(CardContent)`
|
||||
background-color: ${({ theme }) => theme.background.transparent.lighter};
|
||||
`;
|
||||
|
||||
const StyledTitle = styled.h3`
|
||||
color: ${({ theme }) => theme.font.color.extraLight};
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
margin: 0;
|
||||
margin-bottom: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
const StyledFormContainer = styled(CardContent)`
|
||||
padding: 0;
|
||||
`;
|
||||
@ -33,7 +26,7 @@ export const SettingsDataModelPreviewFormCard = ({
|
||||
}: SettingsDataModelPreviewFormCardProps) => (
|
||||
<Card className={className} fullWidth>
|
||||
<StyledPreviewContainer divider={!!form}>
|
||||
<StyledTitle>Preview</StyledTitle>
|
||||
<StyledFormCardTitle>Preview</StyledFormCardTitle>
|
||||
{preview}
|
||||
</StyledPreviewContainer>
|
||||
{!!form && <StyledFormContainer>{form}</StyledFormContainer>}
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
export const StyledFormCardTitle = styled.h3`
|
||||
color: ${({ theme }) => theme.font.color.extraLight};
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
margin: 0;
|
||||
margin-bottom: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
@ -9,9 +9,9 @@ import { IconPicker } from '@/ui/input/components/IconPicker';
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
|
||||
export const settingsDataModelFieldIconLabelFormSchema = (
|
||||
existingLabels?: string[],
|
||||
existingOtherLabels: string[] = [],
|
||||
) => {
|
||||
return fieldMetadataItemSchema(existingLabels || []).pick({
|
||||
return fieldMetadataItemSchema(existingOtherLabels).pick({
|
||||
icon: true,
|
||||
label: true,
|
||||
});
|
||||
|
||||
@ -9,6 +9,8 @@ import { settingsDataModelFieldBooleanFormSchema } from '@/settings/data-model/f
|
||||
import { SettingsDataModelFieldBooleanSettingsFormCard } from '@/settings/data-model/fields/forms/boolean/components/SettingsDataModelFieldBooleanSettingsFormCard';
|
||||
import { settingsDataModelFieldCurrencyFormSchema } from '@/settings/data-model/fields/forms/currency/components/SettingsDataModelFieldCurrencyForm';
|
||||
import { SettingsDataModelFieldCurrencySettingsFormCard } from '@/settings/data-model/fields/forms/currency/components/SettingsDataModelFieldCurrencySettingsFormCard';
|
||||
import { settingsDataModelFieldDateFormSchema } from '@/settings/data-model/fields/forms/date/components/SettingsDataModelFieldDateForm';
|
||||
import { SettingsDataModelFieldDateSettingsFormCard } from '@/settings/data-model/fields/forms/date/components/SettingsDataModelFieldDateSettingsFormCard';
|
||||
import { settingsDataModelFieldRelationFormSchema } from '@/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationForm';
|
||||
import { SettingsDataModelFieldRelationSettingsFormCard } from '@/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationSettingsFormCard';
|
||||
import {
|
||||
@ -30,6 +32,14 @@ const currencyFieldFormSchema = z
|
||||
.object({ type: z.literal(FieldMetadataType.Currency) })
|
||||
.merge(settingsDataModelFieldCurrencyFormSchema);
|
||||
|
||||
const dateFieldFormSchema = z
|
||||
.object({ type: z.literal(FieldMetadataType.Date) })
|
||||
.merge(settingsDataModelFieldDateFormSchema);
|
||||
|
||||
const dateTimeFieldFormSchema = z
|
||||
.object({ type: z.literal(FieldMetadataType.DateTime) })
|
||||
.merge(settingsDataModelFieldDateFormSchema);
|
||||
|
||||
const relationFieldFormSchema = z
|
||||
.object({ type: z.literal(FieldMetadataType.Relation) })
|
||||
.merge(settingsDataModelFieldRelationFormSchema);
|
||||
@ -51,6 +61,8 @@ const otherFieldsFormSchema = z.object({
|
||||
FieldMetadataType.Relation,
|
||||
FieldMetadataType.Select,
|
||||
FieldMetadataType.MultiSelect,
|
||||
FieldMetadataType.Date,
|
||||
FieldMetadataType.DateTime,
|
||||
]),
|
||||
) as [FieldMetadataType, ...FieldMetadataType[]],
|
||||
),
|
||||
@ -61,6 +73,8 @@ export const settingsDataModelFieldSettingsFormSchema = z.discriminatedUnion(
|
||||
[
|
||||
booleanFieldFormSchema,
|
||||
currencyFieldFormSchema,
|
||||
dateFieldFormSchema,
|
||||
dateTimeFieldFormSchema,
|
||||
relationFieldFormSchema,
|
||||
selectFieldFormSchema,
|
||||
multiSelectFieldFormSchema,
|
||||
@ -69,7 +83,7 @@ export const settingsDataModelFieldSettingsFormSchema = z.discriminatedUnion(
|
||||
);
|
||||
|
||||
type SettingsDataModelFieldSettingsFormCardProps = {
|
||||
disableCurrencyForm?: boolean;
|
||||
isCreatingField?: boolean;
|
||||
fieldMetadataItem: Pick<FieldMetadataItem, 'icon' | 'label' | 'type'> &
|
||||
Partial<Omit<FieldMetadataItem, 'icon' | 'label' | 'type'>>;
|
||||
} & Pick<SettingsDataModelFieldPreviewCardProps, 'objectMetadataItem'>;
|
||||
@ -102,7 +116,7 @@ const previewableTypes = [
|
||||
];
|
||||
|
||||
export const SettingsDataModelFieldSettingsFormCard = ({
|
||||
disableCurrencyForm,
|
||||
isCreatingField,
|
||||
fieldMetadataItem,
|
||||
objectMetadataItem,
|
||||
}: SettingsDataModelFieldSettingsFormCardProps) => {
|
||||
@ -120,7 +134,20 @@ export const SettingsDataModelFieldSettingsFormCard = ({
|
||||
if (fieldMetadataItem.type === FieldMetadataType.Currency) {
|
||||
return (
|
||||
<SettingsDataModelFieldCurrencySettingsFormCard
|
||||
disabled={disableCurrencyForm}
|
||||
disabled={!isCreatingField}
|
||||
fieldMetadataItem={fieldMetadataItem}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
fieldMetadataItem.type === FieldMetadataType.Date ||
|
||||
fieldMetadataItem.type === FieldMetadataType.DateTime
|
||||
) {
|
||||
return (
|
||||
<SettingsDataModelFieldDateSettingsFormCard
|
||||
disabled={!isCreatingField}
|
||||
fieldMetadataItem={fieldMetadataItem}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
/>
|
||||
|
||||
@ -0,0 +1,91 @@
|
||||
import { Toggle } from '@/ui/input/components/Toggle';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { createPortal } from 'react-dom';
|
||||
import {
|
||||
AppTooltip,
|
||||
IconComponent,
|
||||
IconInfoCircle,
|
||||
TooltipDelay,
|
||||
} from 'twenty-ui';
|
||||
|
||||
const StyledContainer = styled.div<{ disabled?: boolean }>`
|
||||
align-items: center;
|
||||
background-color: ${({ theme }) => theme.background.transparent.lighter};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
box-sizing: border-box;
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
color: ${({ disabled, theme }) =>
|
||||
disabled ? theme.font.color.tertiary : theme.font.color.primary};
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
height: ${({ theme }) => theme.spacing(8)};
|
||||
justify-content: space-between;
|
||||
padding: 0 ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledGroup = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
interface SettingsDataModelFieldToggleProps {
|
||||
disabled?: boolean;
|
||||
Icon?: IconComponent;
|
||||
label: string;
|
||||
tooltip?: string;
|
||||
value?: boolean;
|
||||
onChange: (value: boolean) => void;
|
||||
}
|
||||
|
||||
export const SettingsDataModelFieldToggle = ({
|
||||
disabled,
|
||||
Icon,
|
||||
label,
|
||||
tooltip,
|
||||
value,
|
||||
onChange,
|
||||
}: SettingsDataModelFieldToggleProps) => {
|
||||
const theme = useTheme();
|
||||
const infoCircleElementId = `info-circle-id-${Math.random().toString(36).slice(2)}`;
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledGroup>
|
||||
{Icon && (
|
||||
<Icon color={theme.font.color.tertiary} size={theme.icon.size.md} />
|
||||
)}
|
||||
{label}
|
||||
</StyledGroup>
|
||||
<StyledGroup>
|
||||
{tooltip && (
|
||||
<IconInfoCircle
|
||||
id={infoCircleElementId}
|
||||
size={theme.icon.size.md}
|
||||
color={theme.font.color.tertiary}
|
||||
/>
|
||||
)}
|
||||
{tooltip &&
|
||||
createPortal(
|
||||
<AppTooltip
|
||||
anchorSelect={`#${infoCircleElementId}`}
|
||||
content={tooltip}
|
||||
offset={5}
|
||||
noArrow
|
||||
place="bottom"
|
||||
positionStrategy="absolute"
|
||||
delay={TooltipDelay.shortDelay}
|
||||
/>,
|
||||
document.body,
|
||||
)}
|
||||
<Toggle
|
||||
disabled={disabled}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
toggleSize="small"
|
||||
/>
|
||||
</StyledGroup>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,63 @@
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { StyledFormCardTitle } from '@/settings/data-model/fields/components/StyledFormCardTitle';
|
||||
import { SettingsDataModelFieldToggle } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldToggle';
|
||||
import { useDateSettingsFormInitialValues } from '@/settings/data-model/fields/forms/date/hooks/useDateSettingsFormInitialValues';
|
||||
import { CardContent } from '@/ui/layout/card/components/CardContent';
|
||||
import { IconClockShare } from 'twenty-ui';
|
||||
|
||||
export const settingsDataModelFieldDateFormSchema = z.object({
|
||||
settings: z
|
||||
.object({
|
||||
displayAsRelativeDate: z.boolean().optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export type SettingsDataModelFieldDateFormValues = z.infer<
|
||||
typeof settingsDataModelFieldDateFormSchema
|
||||
>;
|
||||
|
||||
type SettingsDataModelFieldDateFormProps = {
|
||||
disabled?: boolean;
|
||||
fieldMetadataItem: Pick<FieldMetadataItem, 'settings'>;
|
||||
};
|
||||
|
||||
export const SettingsDataModelFieldDateForm = ({
|
||||
disabled,
|
||||
fieldMetadataItem,
|
||||
}: SettingsDataModelFieldDateFormProps) => {
|
||||
const { control } = useFormContext<SettingsDataModelFieldDateFormValues>();
|
||||
|
||||
const { initialDisplayAsRelativeDateValue } =
|
||||
useDateSettingsFormInitialValues({
|
||||
fieldMetadataItem,
|
||||
});
|
||||
|
||||
return (
|
||||
<CardContent>
|
||||
<Controller
|
||||
name="settings.displayAsRelativeDate"
|
||||
control={control}
|
||||
defaultValue={initialDisplayAsRelativeDateValue}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<>
|
||||
<StyledFormCardTitle>Options</StyledFormCardTitle>
|
||||
<SettingsDataModelFieldToggle
|
||||
label="Display as relative date"
|
||||
Icon={IconClockShare}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
tooltip={
|
||||
'Show dates in a human-friendly format. Example: "13 mins ago" instead of "Jul 30, 2024 7:11pm"'
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,66 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { SettingsDataModelPreviewFormCard } from '@/settings/data-model/components/SettingsDataModelPreviewFormCard';
|
||||
import {
|
||||
SettingsDataModelFieldDateForm,
|
||||
SettingsDataModelFieldDateFormValues,
|
||||
} from '@/settings/data-model/fields/forms/date/components/SettingsDataModelFieldDateForm';
|
||||
import { useDateSettingsFormInitialValues } from '@/settings/data-model/fields/forms/date/hooks/useDateSettingsFormInitialValues';
|
||||
import {
|
||||
SettingsDataModelFieldPreviewCard,
|
||||
SettingsDataModelFieldPreviewCardProps,
|
||||
} from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard';
|
||||
|
||||
type SettingsDataModelFieldDateSettingsFormCardProps = {
|
||||
disabled?: boolean;
|
||||
fieldMetadataItem: Pick<
|
||||
FieldMetadataItem,
|
||||
'icon' | 'label' | 'type' | 'settings'
|
||||
>;
|
||||
} & Pick<SettingsDataModelFieldPreviewCardProps, 'objectMetadataItem'>;
|
||||
|
||||
const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)`
|
||||
display: grid;
|
||||
flex: 1 1 100%;
|
||||
`;
|
||||
|
||||
export const SettingsDataModelFieldDateSettingsFormCard = ({
|
||||
disabled,
|
||||
fieldMetadataItem,
|
||||
objectMetadataItem,
|
||||
}: SettingsDataModelFieldDateSettingsFormCardProps) => {
|
||||
const { initialDisplayAsRelativeDateValue } =
|
||||
useDateSettingsFormInitialValues({
|
||||
fieldMetadataItem,
|
||||
});
|
||||
|
||||
const { watch: watchFormValue } =
|
||||
useFormContext<SettingsDataModelFieldDateFormValues>();
|
||||
|
||||
return (
|
||||
<SettingsDataModelPreviewFormCard
|
||||
preview={
|
||||
<StyledFieldPreviewCard
|
||||
fieldMetadataItem={{
|
||||
...fieldMetadataItem,
|
||||
settings: {
|
||||
displayAsRelativeDate: watchFormValue(
|
||||
'settings.displayAsRelativeDate',
|
||||
initialDisplayAsRelativeDateValue,
|
||||
),
|
||||
},
|
||||
}}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
/>
|
||||
}
|
||||
form={
|
||||
<SettingsDataModelFieldDateForm
|
||||
disabled={disabled}
|
||||
fieldMetadataItem={fieldMetadataItem}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,25 @@
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { SettingsDataModelFieldDateFormValues } from '@/settings/data-model/fields/forms/date/components/SettingsDataModelFieldDateForm';
|
||||
|
||||
export const useDateSettingsFormInitialValues = ({
|
||||
fieldMetadataItem,
|
||||
}: {
|
||||
fieldMetadataItem?: Pick<FieldMetadataItem, 'settings'>;
|
||||
}) => {
|
||||
const initialDisplayAsRelativeDateValue =
|
||||
fieldMetadataItem?.settings?.displayAsRelativeDate;
|
||||
|
||||
const { resetField } = useFormContext<SettingsDataModelFieldDateFormValues>();
|
||||
|
||||
const resetDefaultValueField = () =>
|
||||
resetField('settings.displayAsRelativeDate', {
|
||||
defaultValue: initialDisplayAsRelativeDateValue,
|
||||
});
|
||||
|
||||
return {
|
||||
initialDisplayAsRelativeDateValue,
|
||||
resetDefaultValueField,
|
||||
};
|
||||
};
|
||||
@ -5,10 +5,10 @@ import { settingsDataModelFieldIconLabelFormSchema } from '@/settings/data-model
|
||||
import { settingsDataModelFieldSettingsFormSchema } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard';
|
||||
import { settingsDataModelFieldTypeFormSchema } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldTypeSelect';
|
||||
|
||||
export const settingsFieldFormSchema = (existingLabels?: string[]) => {
|
||||
export const settingsFieldFormSchema = (existingOtherLabels?: string[]) => {
|
||||
return z
|
||||
.object({})
|
||||
.merge(settingsDataModelFieldIconLabelFormSchema(existingLabels))
|
||||
.merge(settingsDataModelFieldIconLabelFormSchema(existingOtherLabels))
|
||||
.merge(settingsDataModelFieldDescriptionFormSchema())
|
||||
.merge(settingsDataModelFieldTypeFormSchema)
|
||||
.and(settingsDataModelFieldSettingsFormSchema);
|
||||
|
||||
@ -18,7 +18,7 @@ import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
export type SettingsDataModelFieldPreviewProps = {
|
||||
fieldMetadataItem: Pick<
|
||||
FieldMetadataItem,
|
||||
'icon' | 'label' | 'type' | 'defaultValue' | 'options'
|
||||
'icon' | 'label' | 'type' | 'defaultValue' | 'options' | 'settings'
|
||||
> & {
|
||||
id?: string;
|
||||
name?: string;
|
||||
@ -132,6 +132,7 @@ export const SettingsDataModelFieldPreview = ({
|
||||
relationObjectMetadataNameSingular:
|
||||
relationObjectMetadataItem?.nameSingular,
|
||||
options: fieldMetadataItem.options ?? [],
|
||||
settings: fieldMetadataItem.settings,
|
||||
},
|
||||
defaultValue: fieldMetadataItem.defaultValue,
|
||||
},
|
||||
|
||||
@ -1,17 +1,24 @@
|
||||
import { formatDateISOStringToDate } from '@/localization/utils/formatDateISOStringToDate';
|
||||
import { formatDateISOStringToRelativeDate } from '@/localization/utils/formatDateISOStringToRelativeDate';
|
||||
import { UserContext } from '@/users/contexts/UserContext';
|
||||
import { useContext } from 'react';
|
||||
import { EllipsisDisplay } from './EllipsisDisplay';
|
||||
|
||||
type DateDisplayProps = {
|
||||
value: string | null | undefined;
|
||||
displayAsRelativeDate?: boolean;
|
||||
};
|
||||
|
||||
export const DateDisplay = ({ value }: DateDisplayProps) => {
|
||||
export const DateDisplay = ({
|
||||
value,
|
||||
displayAsRelativeDate,
|
||||
}: DateDisplayProps) => {
|
||||
const { dateFormat, timeZone } = useContext(UserContext);
|
||||
|
||||
const formattedDate = value
|
||||
? formatDateISOStringToDate(value, timeZone, dateFormat)
|
||||
? displayAsRelativeDate
|
||||
? formatDateISOStringToRelativeDate(value, true)
|
||||
: formatDateISOStringToDate(value, timeZone, dateFormat)
|
||||
: '';
|
||||
|
||||
return <EllipsisDisplay>{formattedDate}</EllipsisDisplay>;
|
||||
|
||||
@ -1,17 +1,24 @@
|
||||
import { formatDateISOStringToDateTime } from '@/localization/utils/formatDateISOStringToDateTime';
|
||||
import { formatDateISOStringToRelativeDate } from '@/localization/utils/formatDateISOStringToRelativeDate';
|
||||
import { UserContext } from '@/users/contexts/UserContext';
|
||||
import { useContext } from 'react';
|
||||
import { EllipsisDisplay } from './EllipsisDisplay';
|
||||
|
||||
type DateTimeDisplayProps = {
|
||||
value: string | null | undefined;
|
||||
displayAsRelativeDate?: boolean;
|
||||
};
|
||||
|
||||
export const DateTimeDisplay = ({ value }: DateTimeDisplayProps) => {
|
||||
export const DateTimeDisplay = ({
|
||||
value,
|
||||
displayAsRelativeDate,
|
||||
}: DateTimeDisplayProps) => {
|
||||
const { dateFormat, timeFormat, timeZone } = useContext(UserContext);
|
||||
|
||||
const formattedDate = value
|
||||
? formatDateISOStringToDateTime(value, timeZone, dateFormat, timeFormat)
|
||||
? displayAsRelativeDate
|
||||
? formatDateISOStringToRelativeDate(value)
|
||||
: formatDateISOStringToDateTime(value, timeZone, dateFormat, timeFormat)
|
||||
: '';
|
||||
|
||||
return <EllipsisDisplay>{formattedDate}</EllipsisDisplay>;
|
||||
|
||||
@ -229,7 +229,6 @@ export const SettingsObjectFieldEdit = () => {
|
||||
<Section>
|
||||
<H2Title title="Values" description="The values of this field" />
|
||||
<SettingsDataModelFieldSettingsFormCard
|
||||
disableCurrencyForm
|
||||
fieldMetadataItem={fieldMetadataItem}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
/>
|
||||
|
||||
@ -248,6 +248,7 @@ export const SettingsObjectNewFieldStep2 = () => {
|
||||
/>
|
||||
|
||||
<SettingsDataModelFieldSettingsFormCard
|
||||
isCreatingField
|
||||
fieldMetadataItem={{
|
||||
icon: formConfig.watch('icon'),
|
||||
label: formConfig.watch('label') || 'Employees',
|
||||
|
||||
@ -14,8 +14,18 @@ type FieldMetadataNumberSettings = {
|
||||
dataType: NumberDataType;
|
||||
};
|
||||
|
||||
type FieldMetadataDateSettings = {
|
||||
displayAsRelativeDate?: boolean;
|
||||
};
|
||||
|
||||
type FieldMetadataDateTimeSettings = {
|
||||
displayAsRelativeDate?: boolean;
|
||||
};
|
||||
|
||||
type FieldMetadataSettingsMapping = {
|
||||
[FieldMetadataType.NUMBER]: FieldMetadataNumberSettings;
|
||||
[FieldMetadataType.DATE]: FieldMetadataDateSettings;
|
||||
[FieldMetadataType.DATE_TIME]: FieldMetadataDateTimeSettings;
|
||||
};
|
||||
|
||||
type SettingsByFieldMetadata<T extends FieldMetadataType | 'default'> =
|
||||
|
||||
@ -25,6 +25,9 @@ export abstract class BaseWorkspaceEntity {
|
||||
description: 'Creation date',
|
||||
icon: 'IconCalendar',
|
||||
defaultValue: 'now',
|
||||
settings: {
|
||||
displayAsRelativeDate: true,
|
||||
},
|
||||
})
|
||||
createdAt: string;
|
||||
|
||||
@ -35,6 +38,9 @@ export abstract class BaseWorkspaceEntity {
|
||||
description: 'Last time the record was changed',
|
||||
icon: 'IconCalendarClock',
|
||||
defaultValue: 'now',
|
||||
settings: {
|
||||
displayAsRelativeDate: true,
|
||||
},
|
||||
})
|
||||
updatedAt: string;
|
||||
|
||||
@ -44,6 +50,9 @@ export abstract class BaseWorkspaceEntity {
|
||||
label: 'Deleted at',
|
||||
description: 'Date when the record was deleted',
|
||||
icon: 'IconCalendarMinus',
|
||||
settings: {
|
||||
displayAsRelativeDate: true,
|
||||
},
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
deletedAt?: string | null;
|
||||
|
||||
@ -69,6 +69,7 @@ export function WorkspaceField<T extends FieldMetadataType>(
|
||||
icon: options.icon,
|
||||
defaultValue,
|
||||
options: options.options,
|
||||
settings: options.settings,
|
||||
isPrimary,
|
||||
isNullable,
|
||||
isSystem,
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
|
||||
import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface';
|
||||
import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
|
||||
import { Gate } from 'src/engine/twenty-orm/interfaces/gate.interface';
|
||||
|
||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
@ -54,6 +55,11 @@ export interface WorkspaceFieldMetadataArgs {
|
||||
*/
|
||||
readonly options?: FieldMetadataOptions;
|
||||
|
||||
/**
|
||||
* Field settings.
|
||||
*/
|
||||
readonly settings?: FieldMetadataSettings;
|
||||
|
||||
/**
|
||||
* Is primary field.
|
||||
*/
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
/* eslint-disable @typescript-eslint/ban-types */
|
||||
|
||||
import { WorkspaceDynamicRelationMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-dynamic-relation-metadata-args.interface';
|
||||
import { WorkspaceFieldMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-field-metadata-args.interface';
|
||||
import { WorkspaceEntityMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-entity-metadata-args.interface';
|
||||
import { WorkspaceRelationMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-relation-metadata-args.interface';
|
||||
import { WorkspaceExtendedEntityMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-extended-entity-metadata-args.interface';
|
||||
import { WorkspaceFieldMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-field-metadata-args.interface';
|
||||
import { WorkspaceIndexMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-index-metadata-args.interface';
|
||||
import { WorkspaceJoinColumnsMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-join-columns-metadata-args.interface';
|
||||
import { WorkspaceRelationMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-relation-metadata-args.interface';
|
||||
|
||||
export class MetadataArgsStorage {
|
||||
private readonly entities: WorkspaceEntityMetadataArgs[] = [];
|
||||
|
||||
@ -160,6 +160,7 @@ export class StandardFieldFactory {
|
||||
description: workspaceFieldMetadataArgs.description,
|
||||
defaultValue: workspaceFieldMetadataArgs.defaultValue,
|
||||
options: workspaceFieldMetadataArgs.options,
|
||||
settings: workspaceFieldMetadataArgs.settings,
|
||||
workspaceId: context.workspaceId,
|
||||
isNullable: workspaceFieldMetadataArgs.isNullable,
|
||||
isCustom: workspaceFieldMetadataArgs.isDeprecated ? true : false,
|
||||
|
||||
@ -48,6 +48,7 @@ export {
|
||||
IconCircleX,
|
||||
IconClick,
|
||||
IconClockHour8,
|
||||
IconClockShare,
|
||||
IconCode,
|
||||
IconCoins,
|
||||
IconColorSwatch,
|
||||
|
||||
Reference in New Issue
Block a user