mirror of
https://github.com/twentyhq/twenty.git
synced 2024-11-22 03:17:40 +03:00
Merge branch 'main' into NavigationFieldTypeSelect
This commit is contained in:
commit
519803167a
@ -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>
|
||||
|
@ -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,
|
||||
},
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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,
|
||||
|
@ -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';
|
||||
};
|
||||
|
@ -0,0 +1,6 @@
|
||||
import { EmailThreadMessage } from '@/activities/emails/types/EmailThreadMessage';
|
||||
import { EmailThreadMessageParticipant } from '@/activities/emails/types/EmailThreadMessageParticipant';
|
||||
|
||||
export type EmailThreadMessageWithSender = EmailThreadMessage & {
|
||||
sender: EmailThreadMessageParticipant;
|
||||
};
|
@ -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',
|
||||
|
@ -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 = {
|
||||
|
@ -45,6 +45,7 @@ export const SettingsAccountsCalendarChannelsContainer = () => {
|
||||
in: accounts.map((account) => account.id),
|
||||
},
|
||||
},
|
||||
skip: !accounts.length,
|
||||
});
|
||||
|
||||
const tabs = [
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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.`));
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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 {}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user