Merge branch 'main' into NavigationFieldTypeSelect

This commit is contained in:
bosiraphael 2024-09-25 16:56:38 +02:00
commit 519803167a
47 changed files with 672 additions and 80 deletions

View File

@ -1,5 +1,5 @@
import { useState } from 'react';
import styled from '@emotion/styled';
import { useState } from 'react';
import { EmailThreadMessageBody } from '@/activities/emails/components/EmailThreadMessageBody';
import { EmailThreadMessageBodyPreview } from '@/activities/emails/components/EmailThreadMessageBodyPreview';
@ -30,6 +30,7 @@ const StyledThreadMessageBody = styled.div`
type EmailThreadMessageProps = {
body: string;
sentAt: string;
sender: EmailThreadMessageParticipant;
participants: EmailThreadMessageParticipant[];
isExpanded?: boolean;
};
@ -37,17 +38,17 @@ type EmailThreadMessageProps = {
export const EmailThreadMessage = ({
body,
sentAt,
sender,
participants,
isExpanded = false,
}: EmailThreadMessageProps) => {
const [isOpen, setIsOpen] = useState(isExpanded);
const from = participants.find((participant) => participant.role === 'from');
const receivers = participants.filter(
(participant) => participant.role !== 'from',
);
if (!from || receivers.length === 0) {
if (!sender || receivers.length === 0) {
return null;
}
@ -57,7 +58,7 @@ export const EmailThreadMessage = ({
style={{ cursor: isOpen ? 'auto' : 'pointer' }}
>
<StyledThreadMessageHeader onClick={() => isOpen && setIsOpen(false)}>
<EmailThreadMessageSender sender={from} sentAt={sentAt} />
<EmailThreadMessageSender sender={sender} sentAt={sentAt} />
{isOpen && <EmailThreadMessageReceivers receivers={receivers} />}
</StyledThreadMessageHeader>
<StyledThreadMessageBody>

View File

@ -47,11 +47,7 @@ export const fetchAllThreadMessagesOperationSignatureFactory: RecordGqlOperation
id: true,
role: true,
displayName: true,
participant: {
id: true,
email: true,
name: true,
},
handle: true,
person: true,
workspaceMember: true,
},

View File

@ -1,9 +1,9 @@
import { useState } from 'react';
import styled from '@emotion/styled';
import { useState } from 'react';
import { IconArrowsVertical } from 'twenty-ui';
import { EmailThreadMessage } from '@/activities/emails/components/EmailThreadMessage';
import { EmailThreadMessage as EmailThreadMessageType } from '@/activities/emails/types/EmailThreadMessage';
import { EmailThreadMessageWithSender } from '@/activities/emails/types/EmailThreadMessageWithSender';
import { Button } from '@/ui/input/button/components/Button';
const StyledButtonContainer = styled.div`
@ -14,7 +14,7 @@ const StyledButtonContainer = styled.div`
export const IntermediaryMessages = ({
messages,
}: {
messages: EmailThreadMessageType[];
messages: EmailThreadMessageWithSender[];
}) => {
const [areMessagesOpen, setAreMessagesOpen] = useState(false);
@ -26,6 +26,7 @@ export const IntermediaryMessages = ({
messages.map((message) => (
<EmailThreadMessage
key={message.id}
sender={message.sender}
participants={message.messageParticipants}
body={message.text}
sentAt={message.receivedAt}

View File

@ -55,23 +55,11 @@ export const RightDrawerEmailThread = () => {
messageChannelLoading,
} = useRightDrawerEmailThread();
const visibleMessages = useMemo(() => {
return messages.filter(({ messageParticipants }) => {
const from = messageParticipants.find(
(participant) => participant.role === 'from',
);
const receivers = messageParticipants.filter(
(participant) => participant.role !== 'from',
);
return from && receivers.length > 0;
});
}, [messages]);
useEffect(() => {
if (!visibleMessages[0]?.messageThread) {
if (!messages[0]?.messageThread) {
return;
}
setMessageThread(visibleMessages[0]?.messageThread);
setMessageThread(messages[0]?.messageThread);
});
const { useRegisterClickOutsideListenerCallback } = useClickOutsideListener(
@ -93,17 +81,17 @@ export const RightDrawerEmailThread = () => {
),
});
const visibleMessagesCount = visibleMessages.length;
const is5OrMoreMessages = visibleMessagesCount >= 5;
const firstMessages = visibleMessages.slice(
const messagesCount = messages.length;
const is5OrMoreMessages = messagesCount >= 5;
const firstMessages = messages.slice(
0,
is5OrMoreMessages ? 2 : visibleMessagesCount - 1,
is5OrMoreMessages ? 2 : messagesCount - 1,
);
const intermediaryMessages = is5OrMoreMessages
? visibleMessages.slice(2, visibleMessagesCount - 1)
? messages.slice(2, messagesCount - 1)
: [];
const lastMessage = visibleMessages[visibleMessagesCount - 1];
const subject = visibleMessages[0]?.subject;
const lastMessage = messages[messagesCount - 1];
const subject = messages[0]?.subject;
const canReply = useMemo(() => {
return (
@ -119,7 +107,7 @@ export const RightDrawerEmailThread = () => {
const url = `https://mail.google.com/mail/?authuser=${connectedAccountHandle}#all/${messageThreadExternalId}`;
window.open(url, '_blank');
};
if (!thread) {
if (!thread || !messages.length) {
return null;
}
return (
@ -136,6 +124,7 @@ export const RightDrawerEmailThread = () => {
{firstMessages.map((message) => (
<EmailThreadMessage
key={message.id}
sender={message.sender}
participants={message.messageParticipants}
body={message.text}
sentAt={message.receivedAt}
@ -144,6 +133,7 @@ export const RightDrawerEmailThread = () => {
<IntermediaryMessages messages={intermediaryMessages} />
<EmailThreadMessage
key={lastMessage.id}
sender={lastMessage.sender}
participants={lastMessage.messageParticipants}
body={lastMessage.text}
sentAt={lastMessage.receivedAt}

View File

@ -6,6 +6,8 @@ import { EmailThread } from '@/activities/emails/types/EmailThread';
import { EmailThreadMessage } from '@/activities/emails/types/EmailThreadMessage';
import { MessageChannel } from '@/accounts/types/MessageChannel';
import { EmailThreadMessageParticipant } from '@/activities/emails/types/EmailThreadMessageParticipant';
import { EmailThreadMessageWithSender } from '@/activities/emails/types/EmailThreadMessageWithSender';
import { MessageChannelMessageAssociation } from '@/activities/emails/types/MessageChannelMessageAssociation';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
@ -13,6 +15,7 @@ import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { isDefined } from 'twenty-ui';
export const useRightDrawerEmailThread = () => {
const viewableRecordId = useRecoilValue(viewableRecordIdState);
@ -74,6 +77,30 @@ export const useRightDrawerEmailThread = () => {
}
}, [messages, isMessagesFetchComplete]);
// TODO: introduce nested filters so we can retrieve the message sender directly from the message query
const { records: messageSenders } =
useFindManyRecords<EmailThreadMessageParticipant>({
filter: {
messageId: {
in: messages.map(({ id }) => id),
},
role: {
eq: 'from',
},
},
objectNameSingular: CoreObjectNameSingular.MessageParticipant,
recordGqlFields: {
id: true,
role: true,
displayName: true,
messageId: true,
handle: true,
person: true,
workspaceMember: true,
},
skip: messages.length === 0,
});
const { records: messageChannelMessageAssociationData } =
useFindManyRecords<MessageChannelMessageAssociation>({
filter: {
@ -123,9 +150,24 @@ export const useRightDrawerEmailThread = () => {
const connectedAccountHandle =
messageChannelData.length > 0 ? messageChannelData[0].handle : null;
const messagesWithSender: EmailThreadMessageWithSender[] = messages
.map((message) => {
const sender = messageSenders.find(
(messageSender) => messageSender.messageId === message.id,
);
if (!sender) {
return null;
}
return {
...message,
sender,
};
})
.filter(isDefined);
return {
thread,
messages,
messages: messagesWithSender,
messageThreadExternalId,
connectedAccountHandle,
threadLoading: messagesLoading,

View File

@ -3,9 +3,12 @@ import { Person } from '@/people/types/Person';
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
export type EmailThreadMessageParticipant = {
id: string;
displayName: string;
handle: string;
role: EmailParticipantRole;
messageId: string;
person: Person;
workspaceMember: WorkspaceMember;
__typename: 'EmailThreadMessageParticipant';
};

View File

@ -0,0 +1,6 @@
import { EmailThreadMessage } from '@/activities/emails/types/EmailThreadMessage';
import { EmailThreadMessageParticipant } from '@/activities/emails/types/EmailThreadMessageParticipant';
export type EmailThreadMessageWithSender = EmailThreadMessage & {
sender: EmailThreadMessageParticipant;
};

View File

@ -4,9 +4,12 @@ import { getDisplayNameFromParticipant } from '../getDisplayNameFromParticipant'
describe('getDisplayNameFromParticipant', () => {
const participantWithName: EmailThreadMessageParticipant = {
id: '2cac0ba7-0e60-46c6-86e7-e5b0bc55b7cf',
__typename: 'EmailThreadMessageParticipant',
displayName: '',
handle: '',
role: 'from',
messageId: '638f52d1-fd55-4a2b-b0f3-9858ea3b2e91',
person: {
__typename: 'Person',
id: '1',

View File

@ -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 });
};

View File

@ -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
}
}
`;

View File

@ -41,6 +41,7 @@ export const FIND_MANY_OBJECT_METADATA_ITEMS = gql`
updatedAt
defaultValue
options
settings
relationDefinition {
relationId
direction

View File

@ -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 = {

View File

@ -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;
},

View File

@ -36,4 +36,7 @@ export type FieldMetadataItem = Omit<
'id' | 'nameSingular' | 'namePlural'
>;
} | null;
settings?: {
displayAsRelativeDate?: boolean;
};
};

View File

@ -37,6 +37,7 @@ export const formatFieldMetadataItemAsFieldDefinition = ({
targetFieldMetadataName:
field.relationDefinition?.targetFieldMetadata?.name ?? '',
options: field.options,
settings: field.settings,
isNullable: field.isNullable,
};

View File

@ -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,
};
};

View File

@ -35,6 +35,7 @@ export const fieldMetadataItemSchema = (existingLabels?: string[]) => {
)
.nullable()
.optional(),
settings: z.any().optional(),
relationDefinition: z
.object({
__typename: z.literal('RelationDefinition').optional(),

View File

@ -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}
/>
);
};

View File

@ -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}
/>
);
};

View File

@ -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,

View File

@ -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,

View File

@ -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 = {

View File

@ -45,6 +45,7 @@ export const SettingsAccountsCalendarChannelsContainer = () => {
in: accounts.map((account) => account.id),
},
},
skip: !accounts.length,
});
const tabs = [

View File

@ -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>}

View File

@ -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)};
`;

View File

@ -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,
});

View File

@ -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}
/>

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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}
/>
}
/>
);
};

View File

@ -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,
};
};

View File

@ -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);

View File

@ -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,
},

View File

@ -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>;

View File

@ -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>;

View File

@ -229,7 +229,6 @@ export const SettingsObjectFieldEdit = () => {
<Section>
<H2Title title="Values" description="The values of this field" />
<SettingsDataModelFieldSettingsFormCard
disableCurrencyForm
fieldMetadataItem={fieldMetadataItem}
objectMetadataItem={objectMetadataItem}
/>

View File

@ -0,0 +1,128 @@
import { InjectRepository } from '@nestjs/typeorm';
import chalk from 'chalk';
import { Command } from 'nest-commander';
import { In, Repository } from 'typeorm';
import {
ActiveWorkspacesCommandOptions,
ActiveWorkspacesCommandRunner,
} from 'src/database/commands/active-workspaces.command';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
@Command({
name: 'upgrade-0.31:add-index-key-to-tasks-and-notes-views',
description: 'Add index key to tasks and notes views',
})
export class AddIndexKeyToTasksAndNotesViewsCommand extends ActiveWorkspacesCommandRunner {
constructor(
@InjectRepository(Workspace, 'core')
protected readonly workspaceRepository: Repository<Workspace>,
@InjectRepository(ObjectMetadataEntity, 'metadata')
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {
super(workspaceRepository);
}
async executeActiveWorkspacesCommand(
_passedParam: string[],
_options: ActiveWorkspacesCommandOptions,
workspaceIds: string[],
): Promise<void> {
this.logger.log('Running command to fix migration');
for (const workspaceId of workspaceIds) {
this.logger.log(`Running command for workspace ${workspaceId}`);
try {
this.logger.log(chalk.green(`Cleaning views of ${workspaceId}.`));
await this.addIndexKeyToTasksAndNotesViews(
workspaceId,
_options.dryRun ?? false,
);
await this.twentyORMGlobalManager.destroyDataSourceForWorkspace(
workspaceId,
);
} catch (error) {
this.logger.log(
chalk.red(
`Running command on workspace ${workspaceId} failed with error: ${error}`,
),
);
continue;
} finally {
this.logger.log(
chalk.green(`Finished running command for workspace ${workspaceId}.`),
);
}
this.logger.log(chalk.green(`Command completed!`));
}
}
private async addIndexKeyToTasksAndNotesViews(
workspaceId: string,
dryRun: boolean,
): Promise<void> {
const viewRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewWorkspaceEntity>(
workspaceId,
'view',
false,
);
const allViews = await viewRepository.find();
const viewObjectMetadataIds = allViews.map((view) => view.objectMetadataId);
const objectMetadataEntities = await this.objectMetadataRepository.find({
where: {
id: In(viewObjectMetadataIds),
},
});
const tasksAndNotesObjectMetadataIds = objectMetadataEntities.filter(
(entity) =>
entity.standardId === STANDARD_OBJECT_IDS.task ||
entity.standardId === STANDARD_OBJECT_IDS.note,
);
const viewsToUpdate = allViews.filter(
(view) =>
tasksAndNotesObjectMetadataIds.some(
(entity) => entity.id === view.objectMetadataId,
) &&
['All Tasks', 'All Notes'].includes(view.name) &&
view.key === null,
);
if (dryRun) {
this.logger.log(
chalk.green(
`Found ${viewsToUpdate.length} views to update in workspace ${workspaceId}.`,
),
);
}
if (viewsToUpdate.length > 0 && !dryRun) {
await viewRepository.update(
viewsToUpdate.map((view) => view.id),
{
key: 'INDEX',
},
);
this.logger.log(chalk.green(`Updating ${viewsToUpdate.length} views.`));
}
if (viewsToUpdate.length === 0 && !dryRun) {
this.logger.log(chalk.green(`No views to update.`));
}
}
}

View File

@ -4,6 +4,7 @@ import { Command } from 'nest-commander';
import { Repository } from 'typeorm';
import { ActiveWorkspacesCommandRunner } from 'src/database/commands/active-workspaces.command';
import { AddIndexKeyToTasksAndNotesViewsCommand } from 'src/database/commands/upgrade-version/0-31/0-31-add-index-key-to-tasks-and-notes-views.command';
import { BackfillWorkspaceFavoritesCommand } from 'src/database/commands/upgrade-version/0-31/0-31-backfill-workspace-favorites.command';
import { CleanViewsWithDeletedObjectMetadataCommand } from 'src/database/commands/upgrade-version/0-31/0-31-clean-views-with-deleted-object-metadata.command';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
@ -24,6 +25,7 @@ export class UpgradeTo0_31Command extends ActiveWorkspacesCommandRunner {
private readonly syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand,
private readonly backfillWorkspaceFavoritesCommand: BackfillWorkspaceFavoritesCommand,
private readonly cleanViewsWithDeletedObjectMetadataCommand: CleanViewsWithDeletedObjectMetadataCommand,
private readonly addIndexKeyToTasksAndNotesViewsCommand: AddIndexKeyToTasksAndNotesViewsCommand,
) {
super(workspaceRepository);
}
@ -46,6 +48,11 @@ export class UpgradeTo0_31Command extends ActiveWorkspacesCommandRunner {
options,
workspaceIds,
);
await this.addIndexKeyToTasksAndNotesViewsCommand.executeActiveWorkspacesCommand(
passedParam,
options,
workspaceIds,
);
await this.backfillWorkspaceFavoritesCommand.executeActiveWorkspacesCommand(
passedParam,
options,

View File

@ -1,6 +1,7 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AddIndexKeyToTasksAndNotesViewsCommand } from 'src/database/commands/upgrade-version/0-31/0-31-add-index-key-to-tasks-and-notes-views.command';
import { BackfillWorkspaceFavoritesCommand } from 'src/database/commands/upgrade-version/0-31/0-31-backfill-workspace-favorites.command';
import { CleanViewsWithDeletedObjectMetadataCommand } from 'src/database/commands/upgrade-version/0-31/0-31-clean-views-with-deleted-object-metadata.command';
import { UpgradeTo0_31Command } from 'src/database/commands/upgrade-version/0-31/0-31-upgrade-version.command';
@ -18,6 +19,7 @@ import { WorkspaceSyncMetadataCommandsModule } from 'src/engine/workspace-manage
UpgradeTo0_31Command,
BackfillWorkspaceFavoritesCommand,
CleanViewsWithDeletedObjectMetadataCommand,
AddIndexKeyToTasksAndNotesViewsCommand,
],
})
export class UpgradeTo0_31CommandModule {}

View File

@ -153,21 +153,25 @@ export class MigratePhoneFieldsToPhonesCommand extends ActiveWorkspacesCommandRu
const workspaceId = standardPersonPhoneField.workspaceId;
try {
let standardPersonPhonesFieldType =
let standardPersonPhonesField =
await this.fieldMetadataRepository.findOneBy({
workspaceId,
standardId: PERSON_STANDARD_FIELD_IDS.phones,
});
if (!standardPersonPhonesFieldType) {
standardPersonPhonesFieldType =
await this.fieldMetadataService.createOne({
...deprecatedPhoneFieldWithoutId,
label: 'Phones',
type: FieldMetadataType.PHONES,
defaultValue: null,
name: 'phones',
} satisfies CreateFieldInput);
if (!standardPersonPhonesField) {
standardPersonPhonesField = await this.fieldMetadataService.createOne({
...deprecatedPhoneFieldWithoutId,
label: 'Phones',
type: FieldMetadataType.PHONES,
defaultValue: null,
name: 'phones',
} satisfies CreateFieldInput);
await this.viewService.removeFieldFromViews({
workspaceId: workspaceId,
fieldId: standardPersonPhonesField.id,
});
}
// Copy phone data from Text type to Phones type
@ -176,6 +180,12 @@ export class MigratePhoneFieldsToPhonesCommand extends ActiveWorkspacesCommandRu
workspaceSchemaName,
});
// Add (deprecated) to Phone field label
await this.metadataDataSource.query(
`UPDATE "metadata"."fieldMetadata" SET "label" = $1 where "id"=$2`,
['Phone (deprecated)', standardPersonPhoneField.id],
);
// Add new phones field to views and hide deprecated phone field
const viewFieldRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewFieldWorkspaceEntity>(
@ -193,7 +203,7 @@ export class MigratePhoneFieldsToPhonesCommand extends ActiveWorkspacesCommandRu
await this.viewService.addFieldToViews({
workspaceId: workspaceId,
fieldId: standardPersonPhonesFieldType.id,
fieldId: standardPersonPhonesField.id,
viewsIds: viewFieldsWithDeprecatedPhoneField
.filter((viewField) => viewField.viewId !== null)
.map((viewField) => viewField.viewId as string),
@ -208,6 +218,7 @@ export class MigratePhoneFieldsToPhonesCommand extends ActiveWorkspacesCommandRu
},
[],
),
size: 150,
});
await this.viewService.removeFieldFromViews({
@ -243,6 +254,12 @@ export class MigratePhoneFieldsToPhonesCommand extends ActiveWorkspacesCommandRu
workspaceId,
);
}
// Revert Phone field label (remove (deprecated))
await this.metadataDataSource.query(
`UPDATE "metadata"."fieldMetadata" SET "label" = $1 where "id"=$2`,
['Phone', standardPersonPhoneField.id],
);
} finally {
await workspaceQueryRunner.release();
}

View File

@ -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'> =

View File

@ -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;

View File

@ -69,6 +69,7 @@ export function WorkspaceField<T extends FieldMetadataType>(
icon: options.icon,
defaultValue,
options: options.options,
settings: options.settings,
isPrimary,
isNullable,
isSystem,

View File

@ -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.
*/

View File

@ -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[] = [];

View File

@ -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,

View File

@ -48,6 +48,7 @@ export {
IconCircleX,
IconClick,
IconClockHour8,
IconClockShare,
IconCode,
IconCoins,
IconColorSwatch,