Activity timeline refactoring followup (#5835)
Following https://github.com/twentyhq/twenty/pull/5697, addressing review
This commit is contained in:
@ -8,7 +8,7 @@ import { CalendarContext } from '@/activities/calendar/contexts/CalendarContext'
|
|||||||
import { useCalendarEvents } from '@/activities/calendar/hooks/useCalendarEvents';
|
import { useCalendarEvents } from '@/activities/calendar/hooks/useCalendarEvents';
|
||||||
import { getTimelineCalendarEventsFromCompanyId } from '@/activities/calendar/queries/getTimelineCalendarEventsFromCompanyId';
|
import { getTimelineCalendarEventsFromCompanyId } from '@/activities/calendar/queries/getTimelineCalendarEventsFromCompanyId';
|
||||||
import { getTimelineCalendarEventsFromPersonId } from '@/activities/calendar/queries/getTimelineCalendarEventsFromPersonId';
|
import { getTimelineCalendarEventsFromPersonId } from '@/activities/calendar/queries/getTimelineCalendarEventsFromPersonId';
|
||||||
import { FetchMoreLoader } from '@/activities/components/CustomResolverFetchMoreLoader';
|
import { CustomResolverFetchMoreLoader } from '@/activities/components/CustomResolverFetchMoreLoader';
|
||||||
import { useCustomResolver } from '@/activities/hooks/useCustomResolver';
|
import { useCustomResolver } from '@/activities/hooks/useCustomResolver';
|
||||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
@ -128,7 +128,7 @@ export const Calendar = ({
|
|||||||
</Section>
|
</Section>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<FetchMoreLoader
|
<CustomResolverFetchMoreLoader
|
||||||
loading={isFetchingMore || firstQueryLoading}
|
loading={isFetchingMore || firstQueryLoading}
|
||||||
onLastRowVisible={fetchMoreRecords}
|
onLastRowVisible={fetchMoreRecords}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { useInView } from 'react-intersection-observer';
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { GRAY_SCALE } from 'twenty-ui';
|
import { GRAY_SCALE } from 'twenty-ui';
|
||||||
|
|
||||||
type FetchMoreLoaderProps = {
|
type CustomResolverFetchMoreLoaderProps = {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
onLastRowVisible: (...args: any[]) => any;
|
onLastRowVisible: (...args: any[]) => any;
|
||||||
};
|
};
|
||||||
@ -17,10 +17,10 @@ const StyledText = styled.div`
|
|||||||
padding-left: ${({ theme }) => theme.spacing(2)};
|
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const FetchMoreLoader = ({
|
export const CustomResolverFetchMoreLoader = ({
|
||||||
loading,
|
loading,
|
||||||
onLastRowVisible,
|
onLastRowVisible,
|
||||||
}: FetchMoreLoaderProps) => {
|
}: CustomResolverFetchMoreLoaderProps) => {
|
||||||
const { ref: tbodyRef } = useInView({
|
const { ref: tbodyRef } = useInView({
|
||||||
onChange: onLastRowVisible,
|
onChange: onLastRowVisible,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { H1Title, H1TitleFontColor } from 'twenty-ui';
|
import { H1Title, H1TitleFontColor } from 'twenty-ui';
|
||||||
|
|
||||||
import { FetchMoreLoader } from '@/activities/components/CustomResolverFetchMoreLoader';
|
import { CustomResolverFetchMoreLoader } from '@/activities/components/CustomResolverFetchMoreLoader';
|
||||||
import { EmailLoader } from '@/activities/emails/components/EmailLoader';
|
import { EmailLoader } from '@/activities/emails/components/EmailLoader';
|
||||||
import { EmailThreadPreview } from '@/activities/emails/components/EmailThreadPreview';
|
import { EmailThreadPreview } from '@/activities/emails/components/EmailThreadPreview';
|
||||||
import { TIMELINE_THREADS_DEFAULT_PAGE_SIZE } from '@/activities/emails/constants/Messaging';
|
import { TIMELINE_THREADS_DEFAULT_PAGE_SIZE } from '@/activities/emails/constants/Messaging';
|
||||||
@ -102,7 +102,7 @@ export const EmailThreads = ({
|
|||||||
))}
|
))}
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
<FetchMoreLoader
|
<CustomResolverFetchMoreLoader
|
||||||
loading={isFetchingMore || firstQueryLoading}
|
loading={isFetchingMore || firstQueryLoading}
|
||||||
onLastRowVisible={fetchMoreRecords}
|
onLastRowVisible={fetchMoreRecords}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useRecoilCallback } from 'recoil';
|
import { useRecoilCallback } from 'recoil';
|
||||||
|
|
||||||
import { FetchMoreLoader } from '@/activities/components/CustomResolverFetchMoreLoader';
|
import { CustomResolverFetchMoreLoader } from '@/activities/components/CustomResolverFetchMoreLoader';
|
||||||
import { EmailLoader } from '@/activities/emails/components/EmailLoader';
|
import { EmailLoader } from '@/activities/emails/components/EmailLoader';
|
||||||
import { EmailThreadHeader } from '@/activities/emails/components/EmailThreadHeader';
|
import { EmailThreadHeader } from '@/activities/emails/components/EmailThreadHeader';
|
||||||
import { EmailThreadMessage } from '@/activities/emails/components/EmailThreadMessage';
|
import { EmailThreadMessage } from '@/activities/emails/components/EmailThreadMessage';
|
||||||
@ -98,7 +98,7 @@ export const RightDrawerEmailThread = () => {
|
|||||||
sentAt={lastMessage.receivedAt}
|
sentAt={lastMessage.receivedAt}
|
||||||
isExpanded
|
isExpanded
|
||||||
/>
|
/>
|
||||||
<FetchMoreLoader
|
<CustomResolverFetchMoreLoader
|
||||||
loading={loading}
|
loading={loading}
|
||||||
onLastRowVisible={fetchMoreMessages}
|
onLastRowVisible={fetchMoreMessages}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -4,19 +4,14 @@ import { useRecoilValue } from 'recoil';
|
|||||||
|
|
||||||
import { useLinkedObject } from '@/activities/timeline/hooks/useLinkedObject';
|
import { useLinkedObject } from '@/activities/timeline/hooks/useLinkedObject';
|
||||||
import { TimelineActivityContext } from '@/activities/timelineActivities/contexts/TimelineActivityContext';
|
import { TimelineActivityContext } from '@/activities/timelineActivities/contexts/TimelineActivityContext';
|
||||||
import {
|
import { EventIconDynamicComponent } from '@/activities/timelineActivities/rows/components/EventIconDynamicComponent';
|
||||||
EventIconDynamicComponent,
|
import { EventRowDynamicComponent } from '@/activities/timelineActivities/rows/components/EventRowDynamicComponent';
|
||||||
EventRowDynamicComponent,
|
|
||||||
} from '@/activities/timelineActivities/rows/components/EventRowDynamicComponent';
|
|
||||||
import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity';
|
import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity';
|
||||||
import {
|
import { getTimelineActivityAuthorFullName } from '@/activities/timelineActivities/utils/getTimelineActivityAuthorFullName';
|
||||||
CurrentWorkspaceMember,
|
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||||
currentWorkspaceMemberState,
|
|
||||||
} from '@/auth/states/currentWorkspaceMemberState';
|
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||||
import { beautifyPastDateRelativeToNow } from '~/utils/date-utils';
|
import { beautifyPastDateRelativeToNow } from '~/utils/date-utils';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
|
||||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||||
|
|
||||||
const StyledIconContainer = styled.div`
|
const StyledIconContainer = styled.div`
|
||||||
@ -93,18 +88,6 @@ type EventRowProps = {
|
|||||||
event: TimelineActivity;
|
event: TimelineActivity;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAuthorFullName = (
|
|
||||||
event: TimelineActivity,
|
|
||||||
currentWorkspaceMember: CurrentWorkspaceMember,
|
|
||||||
) => {
|
|
||||||
if (isDefined(event.workspaceMember)) {
|
|
||||||
return currentWorkspaceMember.id === event.workspaceMember.id
|
|
||||||
? 'You'
|
|
||||||
: `${event.workspaceMember?.name.firstName} ${event.workspaceMember?.name.lastName}`;
|
|
||||||
}
|
|
||||||
return 'Twenty';
|
|
||||||
};
|
|
||||||
|
|
||||||
export const EventRow = ({
|
export const EventRow = ({
|
||||||
isLastEvent,
|
isLastEvent,
|
||||||
event,
|
event,
|
||||||
@ -122,10 +105,13 @@ export const EventRow = ({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const authorFullName = getAuthorFullName(event, currentWorkspaceMember);
|
const authorFullName = getTimelineActivityAuthorFullName(
|
||||||
|
event,
|
||||||
|
currentWorkspaceMember,
|
||||||
|
);
|
||||||
|
|
||||||
if (isUndefinedOrNull(mainObjectMetadataItem)) {
|
if (isUndefinedOrNull(mainObjectMetadataItem)) {
|
||||||
return null;
|
throw new Error('mainObjectMetadataItem is required');
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { isNonEmptyArray } from '@sniptt/guards';
|
import { isNonEmptyArray } from '@sniptt/guards';
|
||||||
|
|
||||||
import { FetchMoreLoader } from '@/activities/components/CustomResolverFetchMoreLoader';
|
import { CustomResolverFetchMoreLoader } from '@/activities/components/CustomResolverFetchMoreLoader';
|
||||||
import { TimelineCreateButtonGroup } from '@/activities/timeline/components/TimelineCreateButtonGroup';
|
import { TimelineCreateButtonGroup } from '@/activities/timeline/components/TimelineCreateButtonGroup';
|
||||||
import { EventList } from '@/activities/timelineActivities/components/EventList';
|
import { EventList } from '@/activities/timelineActivities/components/EventList';
|
||||||
import { useTimelineActivities } from '@/activities/timelineActivities/hooks/useTimelineActivities';
|
import { useTimelineActivities } from '@/activities/timelineActivities/hooks/useTimelineActivities';
|
||||||
@ -64,7 +64,10 @@ export const TimelineActivities = ({
|
|||||||
title="All"
|
title="All"
|
||||||
events={timelineActivities ?? []}
|
events={timelineActivities ?? []}
|
||||||
/>
|
/>
|
||||||
<FetchMoreLoader loading={loading} onLastRowVisible={fetchMoreRecords} />
|
<CustomResolverFetchMoreLoader
|
||||||
|
loading={loading}
|
||||||
|
onLastRowVisible={fetchMoreRecords}
|
||||||
|
/>
|
||||||
</StyledMainContainer>
|
</StyledMainContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -3,8 +3,8 @@ import styled from '@emotion/styled';
|
|||||||
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
|
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
|
||||||
import {
|
import {
|
||||||
EventRowDynamicComponentProps,
|
EventRowDynamicComponentProps,
|
||||||
StyledItemAction,
|
StyledEventRowItemAction,
|
||||||
StyledItemAuthorText,
|
StyledEventRowItemColumn,
|
||||||
} from '@/activities/timelineActivities/rows/components/EventRowDynamicComponent';
|
} from '@/activities/timelineActivities/rows/components/EventRowDynamicComponent';
|
||||||
|
|
||||||
type EventRowActivityProps = EventRowDynamicComponentProps;
|
type EventRowActivityProps = EventRowDynamicComponentProps;
|
||||||
@ -14,7 +14,7 @@ const StyledLinkedActivity = styled.span`
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const EventRowActivity: React.FC<EventRowActivityProps> = ({
|
export const EventRowActivity = ({
|
||||||
event,
|
event,
|
||||||
authorFullName,
|
authorFullName,
|
||||||
}: EventRowActivityProps) => {
|
}: EventRowActivityProps) => {
|
||||||
@ -28,8 +28,8 @@ export const EventRowActivity: React.FC<EventRowActivityProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StyledItemAuthorText>{authorFullName}</StyledItemAuthorText>
|
<StyledEventRowItemColumn>{authorFullName}</StyledEventRowItemColumn>
|
||||||
<StyledItemAction>{eventAction}</StyledItemAction>
|
<StyledEventRowItemAction>{eventAction}</StyledEventRowItemAction>
|
||||||
<StyledLinkedActivity
|
<StyledLinkedActivity
|
||||||
onClick={() => openActivityRightDrawer(event.linkedRecordId)}
|
onClick={() => openActivityRightDrawer(event.linkedRecordId)}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import {
|
|||||||
formatToHumanReadableDay,
|
formatToHumanReadableDay,
|
||||||
formatToHumanReadableMonth,
|
formatToHumanReadableMonth,
|
||||||
formatToHumanReadableTime,
|
formatToHumanReadableTime,
|
||||||
} from '~/utils';
|
} from '~/utils/format/formatDate';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||||
|
|
||||||
@ -121,7 +121,7 @@ export const EventCardCalendarEvent = ({
|
|||||||
return <div>Calendar event not found</div>;
|
return <div>Calendar event not found</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div>Error loading message</div>;
|
return <div>Error loading calendar event</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading || isUndefined(calendarEvent)) {
|
if (loading || isUndefined(calendarEvent)) {
|
||||||
|
|||||||
@ -2,14 +2,12 @@ import { useState } from 'react';
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
import { EventCardCalendarEvent } from '@/activities/timelineActivities/rows/calendar/components/EventCardCalendarEvent';
|
import { EventCardCalendarEvent } from '@/activities/timelineActivities/rows/calendar/components/EventCardCalendarEvent';
|
||||||
import {
|
import { EventCard } from '@/activities/timelineActivities/rows/components/EventCard';
|
||||||
EventCard,
|
import { EventCardToggleButton } from '@/activities/timelineActivities/rows/components/EventCardToggleButton';
|
||||||
EventCardToggleButton,
|
|
||||||
} from '@/activities/timelineActivities/rows/components/EventCard';
|
|
||||||
import {
|
import {
|
||||||
EventRowDynamicComponentProps,
|
EventRowDynamicComponentProps,
|
||||||
StyledItemAction,
|
StyledEventRowItemAction,
|
||||||
StyledItemAuthorText,
|
StyledEventRowItemColumn,
|
||||||
} from '@/activities/timelineActivities/rows/components/EventRowDynamicComponent';
|
} from '@/activities/timelineActivities/rows/components/EventRowDynamicComponent';
|
||||||
|
|
||||||
type EventRowCalendarEventProps = EventRowDynamicComponentProps;
|
type EventRowCalendarEventProps = EventRowDynamicComponentProps;
|
||||||
@ -26,7 +24,7 @@ const StyledRowContainer = styled.div`
|
|||||||
gap: ${({ theme }) => theme.spacing(1)};
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const EventRowCalendarEvent: React.FC<EventRowCalendarEventProps> = ({
|
export const EventRowCalendarEvent = ({
|
||||||
event,
|
event,
|
||||||
authorFullName,
|
authorFullName,
|
||||||
labelIdentifierValue,
|
labelIdentifierValue,
|
||||||
@ -34,24 +32,17 @@ export const EventRowCalendarEvent: React.FC<EventRowCalendarEventProps> = ({
|
|||||||
const [, eventAction] = event.name.split('.');
|
const [, eventAction] = event.name.split('.');
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
const renderRow = () => {
|
if (['linked'].includes(eventAction) === false) {
|
||||||
switch (eventAction) {
|
throw new Error('Invalid event action for calendarEvent event type.');
|
||||||
case 'linked': {
|
}
|
||||||
return (
|
|
||||||
<StyledItemAction>
|
|
||||||
linked a calendar event with {labelIdentifierValue}
|
|
||||||
</StyledItemAction>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
throw new Error('Invalid event action for calendarEvent event type.');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<StyledEventRowCalendarEventContainer>
|
<StyledEventRowCalendarEventContainer>
|
||||||
<StyledRowContainer>
|
<StyledRowContainer>
|
||||||
<StyledItemAuthorText>{authorFullName}</StyledItemAuthorText>
|
<StyledEventRowItemColumn>{authorFullName}</StyledEventRowItemColumn>
|
||||||
{renderRow()}
|
<StyledEventRowItemAction>
|
||||||
|
linked a calendar event with {labelIdentifierValue}
|
||||||
|
</StyledEventRowItemAction>
|
||||||
<EventCardToggleButton isOpen={isOpen} setIsOpen={setIsOpen} />
|
<EventCardToggleButton isOpen={isOpen} setIsOpen={setIsOpen} />
|
||||||
</StyledRowContainer>
|
</StyledRowContainer>
|
||||||
<EventCard isOpen={isOpen}>
|
<EventCard isOpen={isOpen}>
|
||||||
|
|||||||
@ -0,0 +1,68 @@
|
|||||||
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { graphql, HttpResponse } from 'msw';
|
||||||
|
import { ComponentDecorator } from 'twenty-ui';
|
||||||
|
|
||||||
|
import { EventCardCalendarEvent } from '@/activities/timelineActivities/rows/calendar/components/EventCardCalendarEvent';
|
||||||
|
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
|
||||||
|
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
||||||
|
|
||||||
|
const meta: Meta<typeof EventCardCalendarEvent> = {
|
||||||
|
title: 'Modules/TimelineActivities/Rows/CalendarEvent/EventCardCalendarEvent',
|
||||||
|
component: EventCardCalendarEvent,
|
||||||
|
decorators: [
|
||||||
|
ComponentDecorator,
|
||||||
|
ObjectMetadataItemsDecorator,
|
||||||
|
SnackBarDecorator,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof EventCardCalendarEvent>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
calendarEventId: '1',
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
msw: {
|
||||||
|
handlers: [
|
||||||
|
graphql.query('FindOneCalendarEvent', () => {
|
||||||
|
return HttpResponse.json({
|
||||||
|
data: {
|
||||||
|
calendarEvent: {
|
||||||
|
id: '1',
|
||||||
|
title: 'Mock title',
|
||||||
|
startsAt: '2022-01-01T00:00:00Z',
|
||||||
|
endsAt: '2022-01-01T01:00:00Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NotShared: Story = {
|
||||||
|
args: {
|
||||||
|
calendarEventId: '1',
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
msw: {
|
||||||
|
handlers: [
|
||||||
|
graphql.query('FindOneCalendarEvent', () => {
|
||||||
|
return HttpResponse.json({
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
message: 'Forbidden',
|
||||||
|
extensions: {
|
||||||
|
code: 'FORBIDDEN',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -1,7 +1,5 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { IconChevronDown, IconChevronUp } from 'twenty-ui';
|
|
||||||
|
|
||||||
import { IconButton } from '@/ui/input/button/components/IconButton';
|
|
||||||
import { Card } from '@/ui/layout/card/components/Card';
|
import { Card } from '@/ui/layout/card/components/Card';
|
||||||
|
|
||||||
type EventCardProps = {
|
type EventCardProps = {
|
||||||
@ -9,15 +7,6 @@ type EventCardProps = {
|
|||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type EventCardToggleButtonProps = {
|
|
||||||
isOpen: boolean;
|
|
||||||
setIsOpen: (isOpen: boolean) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const StyledButtonContainer = styled.div`
|
|
||||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledCardContainer = styled.div`
|
const StyledCardContainer = styled.div`
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -52,19 +41,3 @@ export const EventCard = ({ children, isOpen }: EventCardProps) => {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EventCardToggleButton = ({
|
|
||||||
isOpen,
|
|
||||||
setIsOpen,
|
|
||||||
}: EventCardToggleButtonProps) => {
|
|
||||||
return (
|
|
||||||
<StyledButtonContainer>
|
|
||||||
<IconButton
|
|
||||||
Icon={isOpen ? IconChevronUp : IconChevronDown}
|
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
|
||||||
size="small"
|
|
||||||
variant="secondary"
|
|
||||||
/>
|
|
||||||
</StyledButtonContainer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|||||||
@ -0,0 +1,29 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { IconChevronDown, IconChevronUp } from 'twenty-ui';
|
||||||
|
|
||||||
|
import { IconButton } from '@/ui/input/button/components/IconButton';
|
||||||
|
|
||||||
|
type EventCardToggleButtonProps = {
|
||||||
|
isOpen: boolean;
|
||||||
|
setIsOpen: (isOpen: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledButtonContainer = styled.div`
|
||||||
|
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const EventCardToggleButton = ({
|
||||||
|
isOpen,
|
||||||
|
setIsOpen,
|
||||||
|
}: EventCardToggleButtonProps) => {
|
||||||
|
return (
|
||||||
|
<StyledButtonContainer>
|
||||||
|
<IconButton
|
||||||
|
Icon={isOpen ? IconChevronUp : IconChevronDown}
|
||||||
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
|
size="small"
|
||||||
|
variant="secondary"
|
||||||
|
/>
|
||||||
|
</StyledButtonContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
import { IconCirclePlus, IconEditCircle, useIcons } from 'twenty-ui';
|
||||||
|
|
||||||
|
import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity';
|
||||||
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
|
||||||
|
export const EventIconDynamicComponent = ({
|
||||||
|
event,
|
||||||
|
linkedObjectMetadataItem,
|
||||||
|
}: {
|
||||||
|
event: TimelineActivity;
|
||||||
|
linkedObjectMetadataItem: ObjectMetadataItem | null;
|
||||||
|
}) => {
|
||||||
|
const { getIcon } = useIcons();
|
||||||
|
const [, eventAction] = event.name.split('.');
|
||||||
|
|
||||||
|
if (eventAction === 'created') {
|
||||||
|
return <IconCirclePlus />;
|
||||||
|
}
|
||||||
|
if (eventAction === 'updated') {
|
||||||
|
return <IconEditCircle />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const IconComponent = getIcon(linkedObjectMetadataItem?.icon);
|
||||||
|
|
||||||
|
return <IconComponent />;
|
||||||
|
};
|
||||||
@ -1,13 +1,11 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { IconCirclePlus, IconEditCircle, useIcons } from 'twenty-ui';
|
|
||||||
|
|
||||||
import { EventRowActivity } from '@/activities/timelineActivities/rows/activity/components/EventRowActivity';
|
import { EventRowActivity } from '@/activities/timelineActivities/rows/activity/components/EventRowActivity';
|
||||||
import { EventRowCalendarEvent } from '@/activities/timelineActivities/rows/calendar/components/EventRowCalendarEvent';
|
import { EventRowCalendarEvent } from '@/activities/timelineActivities/rows/calendar/components/EventRowCalendarEvent';
|
||||||
import { EventRowMainObject } from '@/activities/timelineActivities/rows/mainObject/components/EventRowMainObject';
|
import { EventRowMainObject } from '@/activities/timelineActivities/rows/main-object/components/EventRowMainObject';
|
||||||
import { EventRowMessage } from '@/activities/timelineActivities/rows/message/components/EventRowMessage';
|
import { EventRowMessage } from '@/activities/timelineActivities/rows/message/components/EventRowMessage';
|
||||||
import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity';
|
import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity';
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
|
||||||
|
|
||||||
export interface EventRowDynamicComponentProps {
|
export interface EventRowDynamicComponentProps {
|
||||||
labelIdentifierValue: string;
|
labelIdentifierValue: string;
|
||||||
@ -17,7 +15,7 @@ export interface EventRowDynamicComponentProps {
|
|||||||
authorFullName: string;
|
authorFullName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledItemColumn = styled.div`
|
export const StyledEventRowItemColumn = styled.div`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: ${({ theme }) => theme.font.color.primary};
|
color: ${({ theme }) => theme.font.color.primary};
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -25,23 +23,10 @@ const StyledItemColumn = styled.div`
|
|||||||
gap: ${({ theme }) => theme.spacing(1)};
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const StyledItemAuthorText = styled(StyledItemColumn)``;
|
export const StyledEventRowItemAction = styled(StyledEventRowItemColumn)`
|
||||||
|
|
||||||
export const StyledItemLabelIdentifier = styled(StyledItemColumn)``;
|
|
||||||
|
|
||||||
export const StyledItemAction = styled(StyledItemColumn)`
|
|
||||||
color: ${({ theme }) => theme.font.color.secondary};
|
color: ${({ theme }) => theme.font.color.secondary};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const eventRowComponentMap: {
|
|
||||||
[key: string]: React.FC<EventRowDynamicComponentProps>;
|
|
||||||
} = {
|
|
||||||
calendarEvent: EventRowCalendarEvent,
|
|
||||||
message: EventRowMessage,
|
|
||||||
task: EventRowActivity,
|
|
||||||
note: EventRowActivity,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const EventRowDynamicComponent = ({
|
export const EventRowDynamicComponent = ({
|
||||||
labelIdentifierValue,
|
labelIdentifierValue,
|
||||||
event,
|
event,
|
||||||
@ -50,53 +35,52 @@ export const EventRowDynamicComponent = ({
|
|||||||
authorFullName,
|
authorFullName,
|
||||||
}: EventRowDynamicComponentProps) => {
|
}: EventRowDynamicComponentProps) => {
|
||||||
const [eventName] = event.name.split('.');
|
const [eventName] = event.name.split('.');
|
||||||
const EventRowComponent = eventRowComponentMap[eventName];
|
|
||||||
|
|
||||||
if (isDefined(EventRowComponent)) {
|
switch (eventName) {
|
||||||
return (
|
case 'calendarEvent':
|
||||||
<EventRowComponent
|
return (
|
||||||
labelIdentifierValue={labelIdentifierValue}
|
<EventRowCalendarEvent
|
||||||
event={event}
|
labelIdentifierValue={labelIdentifierValue}
|
||||||
mainObjectMetadataItem={mainObjectMetadataItem}
|
event={event}
|
||||||
linkedObjectMetadataItem={linkedObjectMetadataItem}
|
mainObjectMetadataItem={mainObjectMetadataItem}
|
||||||
authorFullName={authorFullName}
|
linkedObjectMetadataItem={linkedObjectMetadataItem}
|
||||||
/>
|
authorFullName={authorFullName}
|
||||||
);
|
/>
|
||||||
|
);
|
||||||
|
case 'message':
|
||||||
|
return (
|
||||||
|
<EventRowMessage
|
||||||
|
labelIdentifierValue={labelIdentifierValue}
|
||||||
|
event={event}
|
||||||
|
mainObjectMetadataItem={mainObjectMetadataItem}
|
||||||
|
linkedObjectMetadataItem={linkedObjectMetadataItem}
|
||||||
|
authorFullName={authorFullName}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'task':
|
||||||
|
case 'note':
|
||||||
|
return (
|
||||||
|
<EventRowActivity
|
||||||
|
labelIdentifierValue={labelIdentifierValue}
|
||||||
|
event={event}
|
||||||
|
mainObjectMetadataItem={mainObjectMetadataItem}
|
||||||
|
linkedObjectMetadataItem={linkedObjectMetadataItem}
|
||||||
|
authorFullName={authorFullName}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case mainObjectMetadataItem?.nameSingular:
|
||||||
|
return (
|
||||||
|
<EventRowMainObject
|
||||||
|
labelIdentifierValue={labelIdentifierValue}
|
||||||
|
event={event}
|
||||||
|
mainObjectMetadataItem={mainObjectMetadataItem}
|
||||||
|
linkedObjectMetadataItem={linkedObjectMetadataItem}
|
||||||
|
authorFullName={authorFullName}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
throw new Error(
|
||||||
|
`Cannot find event component for event name ${eventName}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (eventName === mainObjectMetadataItem?.nameSingular) {
|
|
||||||
return (
|
|
||||||
<EventRowMainObject
|
|
||||||
labelIdentifierValue={labelIdentifierValue}
|
|
||||||
event={event}
|
|
||||||
mainObjectMetadataItem={mainObjectMetadataItem}
|
|
||||||
linkedObjectMetadataItem={linkedObjectMetadataItem}
|
|
||||||
authorFullName={authorFullName}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`Cannot find event component for event name ${eventName}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const EventIconDynamicComponent = ({
|
|
||||||
event,
|
|
||||||
linkedObjectMetadataItem,
|
|
||||||
}: {
|
|
||||||
event: TimelineActivity;
|
|
||||||
linkedObjectMetadataItem: ObjectMetadataItem | null;
|
|
||||||
}) => {
|
|
||||||
const { getIcon } = useIcons();
|
|
||||||
const [, eventAction] = event.name.split('.');
|
|
||||||
|
|
||||||
if (eventAction === 'created') {
|
|
||||||
return <IconCirclePlus />;
|
|
||||||
}
|
|
||||||
if (eventAction === 'updated') {
|
|
||||||
return <IconEditCircle />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const IconComponent = getIcon(linkedObjectMetadataItem?.icon);
|
|
||||||
|
|
||||||
return <IconComponent />;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
import { EventFieldDiffLabel } from '@/activities/timelineActivities/rows/mainObject/components/EventFieldDiffLabel';
|
import { EventFieldDiffLabel } from '@/activities/timelineActivities/rows/main-object/components/EventFieldDiffLabel';
|
||||||
import { EventFieldDiffValue } from '@/activities/timelineActivities/rows/mainObject/components/EventFieldDiffValue';
|
import { EventFieldDiffValue } from '@/activities/timelineActivities/rows/main-object/components/EventFieldDiffValue';
|
||||||
import { EventFieldDiffValueEffect } from '@/activities/timelineActivities/rows/mainObject/components/EventFieldDiffValueEffect';
|
import { EventFieldDiffValueEffect } from '@/activities/timelineActivities/rows/main-object/components/EventFieldDiffValueEffect';
|
||||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
|
||||||
@ -10,7 +10,7 @@ type EventFieldDiffProps = {
|
|||||||
diffRecord: Record<string, any>;
|
diffRecord: Record<string, any>;
|
||||||
mainObjectMetadataItem: ObjectMetadataItem;
|
mainObjectMetadataItem: ObjectMetadataItem;
|
||||||
fieldMetadataItem: FieldMetadataItem | undefined;
|
fieldMetadataItem: FieldMetadataItem | undefined;
|
||||||
forgedRecordId: string;
|
diffArtificialRecordStoreId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StyledEventFieldDiffContainer = styled.div`
|
const StyledEventFieldDiffContainer = styled.div`
|
||||||
@ -26,23 +26,23 @@ export const EventFieldDiff = ({
|
|||||||
diffRecord,
|
diffRecord,
|
||||||
mainObjectMetadataItem,
|
mainObjectMetadataItem,
|
||||||
fieldMetadataItem,
|
fieldMetadataItem,
|
||||||
forgedRecordId,
|
diffArtificialRecordStoreId,
|
||||||
}: EventFieldDiffProps) => {
|
}: EventFieldDiffProps) => {
|
||||||
if (!fieldMetadataItem) {
|
if (!fieldMetadataItem) {
|
||||||
return null;
|
throw new Error('fieldMetadataItem is required');
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledEventFieldDiffContainer>
|
<StyledEventFieldDiffContainer>
|
||||||
<EventFieldDiffLabel fieldMetadataItem={fieldMetadataItem} />→
|
<EventFieldDiffLabel fieldMetadataItem={fieldMetadataItem} />→
|
||||||
<EventFieldDiffValueEffect
|
<EventFieldDiffValueEffect
|
||||||
forgedRecordId={forgedRecordId}
|
diffArtificialRecordStoreId={diffArtificialRecordStoreId}
|
||||||
mainObjectMetadataItem={mainObjectMetadataItem}
|
mainObjectMetadataItem={mainObjectMetadataItem}
|
||||||
fieldMetadataItem={fieldMetadataItem}
|
fieldMetadataItem={fieldMetadataItem}
|
||||||
diffRecord={diffRecord}
|
diffRecord={diffRecord}
|
||||||
/>
|
/>
|
||||||
<EventFieldDiffValue
|
<EventFieldDiffValue
|
||||||
forgedRecordId={forgedRecordId}
|
diffArtificialRecordStoreId={diffArtificialRecordStoreId}
|
||||||
mainObjectMetadataItem={mainObjectMetadataItem}
|
mainObjectMetadataItem={mainObjectMetadataItem}
|
||||||
fieldMetadataItem={fieldMetadataItem}
|
fieldMetadataItem={fieldMetadataItem}
|
||||||
/>
|
/>
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
import { EventFieldDiff } from '@/activities/timelineActivities/rows/main-object/components/EventFieldDiff';
|
||||||
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
|
||||||
|
type EventFieldDiffContainerProps = {
|
||||||
|
mainObjectMetadataItem: ObjectMetadataItem;
|
||||||
|
diffKey: string;
|
||||||
|
diffValue: any;
|
||||||
|
eventId: string;
|
||||||
|
fieldMetadataItemMap: Record<string, FieldMetadataItem>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EventFieldDiffContainer = ({
|
||||||
|
mainObjectMetadataItem,
|
||||||
|
diffKey,
|
||||||
|
diffValue,
|
||||||
|
eventId,
|
||||||
|
fieldMetadataItemMap,
|
||||||
|
}: EventFieldDiffContainerProps) => {
|
||||||
|
const fieldMetadataItem = fieldMetadataItemMap[diffKey];
|
||||||
|
|
||||||
|
if (!fieldMetadataItem) {
|
||||||
|
throw new Error(
|
||||||
|
`Cannot find field metadata item for field name ${diffKey} on object ${mainObjectMetadataItem.nameSingular}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const diffArtificialRecordStoreId = eventId + '--' + fieldMetadataItem.id;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EventFieldDiff
|
||||||
|
key={diffArtificialRecordStoreId}
|
||||||
|
diffRecord={diffValue}
|
||||||
|
fieldMetadataItem={fieldMetadataItem}
|
||||||
|
mainObjectMetadataItem={mainObjectMetadataItem}
|
||||||
|
diffArtificialRecordStoreId={diffArtificialRecordStoreId}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -22,8 +22,6 @@ const StyledUpdatedFieldIconContainer = styled.div`
|
|||||||
width: 14px;
|
width: 14px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledUpdatedFieldLabel = styled.div``;
|
|
||||||
|
|
||||||
export const EventFieldDiffLabel = ({
|
export const EventFieldDiffLabel = ({
|
||||||
fieldMetadataItem,
|
fieldMetadataItem,
|
||||||
}: EventFieldDiffLabelProps) => {
|
}: EventFieldDiffLabelProps) => {
|
||||||
@ -38,9 +36,7 @@ export const EventFieldDiffLabel = ({
|
|||||||
<StyledUpdatedFieldIconContainer>
|
<StyledUpdatedFieldIconContainer>
|
||||||
<IconComponent />
|
<IconComponent />
|
||||||
</StyledUpdatedFieldIconContainer>
|
</StyledUpdatedFieldIconContainer>
|
||||||
<StyledUpdatedFieldLabel>
|
{fieldMetadataItem.label}
|
||||||
{fieldMetadataItem.label}
|
|
||||||
</StyledUpdatedFieldLabel>
|
|
||||||
</StyledUpdatedFieldContainer>
|
</StyledUpdatedFieldContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -7,7 +7,7 @@ import { FieldDisplay } from '@/object-record/record-field/components/FieldDispl
|
|||||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||||
|
|
||||||
type EventFieldDiffValueProps = {
|
type EventFieldDiffValueProps = {
|
||||||
forgedRecordId: string;
|
diffArtificialRecordStoreId: string;
|
||||||
mainObjectMetadataItem: ObjectMetadataItem;
|
mainObjectMetadataItem: ObjectMetadataItem;
|
||||||
fieldMetadataItem: FieldMetadataItem;
|
fieldMetadataItem: FieldMetadataItem;
|
||||||
};
|
};
|
||||||
@ -18,7 +18,7 @@ const StyledEventFieldDiffValue = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const EventFieldDiffValue = ({
|
export const EventFieldDiffValue = ({
|
||||||
forgedRecordId,
|
diffArtificialRecordStoreId,
|
||||||
mainObjectMetadataItem,
|
mainObjectMetadataItem,
|
||||||
fieldMetadataItem,
|
fieldMetadataItem,
|
||||||
}: EventFieldDiffValueProps) => {
|
}: EventFieldDiffValueProps) => {
|
||||||
@ -26,7 +26,7 @@ export const EventFieldDiffValue = ({
|
|||||||
<StyledEventFieldDiffValue>
|
<StyledEventFieldDiffValue>
|
||||||
<FieldContext.Provider
|
<FieldContext.Provider
|
||||||
value={{
|
value={{
|
||||||
entityId: forgedRecordId,
|
entityId: diffArtificialRecordStoreId,
|
||||||
isLabelIdentifier: isLabelIdentifierField({
|
isLabelIdentifier: isLabelIdentifierField({
|
||||||
fieldMetadataItem,
|
fieldMetadataItem,
|
||||||
objectMetadataItem: mainObjectMetadataItem,
|
objectMetadataItem: mainObjectMetadataItem,
|
||||||
@ -6,18 +6,18 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
|||||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||||
|
|
||||||
export const EventFieldDiffValueEffect = ({
|
export const EventFieldDiffValueEffect = ({
|
||||||
forgedRecordId,
|
diffArtificialRecordStoreId,
|
||||||
diffRecord,
|
diffRecord,
|
||||||
mainObjectMetadataItem,
|
mainObjectMetadataItem,
|
||||||
fieldMetadataItem,
|
fieldMetadataItem,
|
||||||
}: {
|
}: {
|
||||||
forgedRecordId: string;
|
diffArtificialRecordStoreId: string;
|
||||||
diffRecord: Record<string, any> | undefined;
|
diffRecord: Record<string, any> | undefined;
|
||||||
mainObjectMetadataItem: ObjectMetadataItem;
|
mainObjectMetadataItem: ObjectMetadataItem;
|
||||||
fieldMetadataItem: FieldMetadataItem;
|
fieldMetadataItem: FieldMetadataItem;
|
||||||
}) => {
|
}) => {
|
||||||
const setEntityFields = useSetRecoilState(
|
const setEntityFields = useSetRecoilState(
|
||||||
recordStoreFamilyState(forgedRecordId),
|
recordStoreFamilyState(diffArtificialRecordStoreId),
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -25,14 +25,14 @@ export const EventFieldDiffValueEffect = ({
|
|||||||
|
|
||||||
const forgedObjectRecord = {
|
const forgedObjectRecord = {
|
||||||
__typename: mainObjectMetadataItem.nameSingular,
|
__typename: mainObjectMetadataItem.nameSingular,
|
||||||
id: forgedRecordId,
|
id: diffArtificialRecordStoreId,
|
||||||
[fieldMetadataItem.name]: diffRecord,
|
[fieldMetadataItem.name]: diffRecord,
|
||||||
};
|
};
|
||||||
|
|
||||||
setEntityFields(forgedObjectRecord);
|
setEntityFields(forgedObjectRecord);
|
||||||
}, [
|
}, [
|
||||||
diffRecord,
|
diffRecord,
|
||||||
forgedRecordId,
|
diffArtificialRecordStoreId,
|
||||||
fieldMetadataItem.name,
|
fieldMetadataItem.name,
|
||||||
mainObjectMetadataItem.nameSingular,
|
mainObjectMetadataItem.nameSingular,
|
||||||
setEntityFields,
|
setEntityFields,
|
||||||
@ -2,11 +2,10 @@ import styled from '@emotion/styled';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
EventRowDynamicComponentProps,
|
EventRowDynamicComponentProps,
|
||||||
StyledItemAction,
|
StyledEventRowItemAction,
|
||||||
StyledItemAuthorText,
|
StyledEventRowItemColumn,
|
||||||
StyledItemLabelIdentifier,
|
|
||||||
} from '@/activities/timelineActivities/rows/components/EventRowDynamicComponent';
|
} from '@/activities/timelineActivities/rows/components/EventRowDynamicComponent';
|
||||||
import { EventRowMainObjectUpdated } from '@/activities/timelineActivities/rows/mainObject/components/EventRowMainObjectUpdated';
|
import { EventRowMainObjectUpdated } from '@/activities/timelineActivities/rows/main-object/components/EventRowMainObjectUpdated';
|
||||||
|
|
||||||
type EventRowMainObjectProps = EventRowDynamicComponentProps;
|
type EventRowMainObjectProps = EventRowDynamicComponentProps;
|
||||||
|
|
||||||
@ -28,11 +27,11 @@ export const EventRowMainObject = ({
|
|||||||
case 'created': {
|
case 'created': {
|
||||||
return (
|
return (
|
||||||
<StyledMainContainer>
|
<StyledMainContainer>
|
||||||
<StyledItemLabelIdentifier>
|
<StyledEventRowItemColumn>
|
||||||
{labelIdentifierValue}
|
{labelIdentifierValue}
|
||||||
</StyledItemLabelIdentifier>
|
</StyledEventRowItemColumn>
|
||||||
<StyledItemAction>was created by</StyledItemAction>
|
<StyledEventRowItemAction>was created by</StyledEventRowItemAction>
|
||||||
<StyledItemAuthorText>{authorFullName}</StyledItemAuthorText>
|
<StyledEventRowItemColumn>{authorFullName}</StyledEventRowItemColumn>
|
||||||
</StyledMainContainer>
|
</StyledMainContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1,15 +1,13 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
import { EventCard } from '@/activities/timelineActivities/rows/components/EventCard';
|
||||||
|
import { EventCardToggleButton } from '@/activities/timelineActivities/rows/components/EventCardToggleButton';
|
||||||
import {
|
import {
|
||||||
EventCard,
|
StyledEventRowItemAction,
|
||||||
EventCardToggleButton,
|
StyledEventRowItemColumn,
|
||||||
} from '@/activities/timelineActivities/rows/components/EventCard';
|
|
||||||
import {
|
|
||||||
StyledItemAction,
|
|
||||||
StyledItemAuthorText,
|
|
||||||
} from '@/activities/timelineActivities/rows/components/EventRowDynamicComponent';
|
} from '@/activities/timelineActivities/rows/components/EventRowDynamicComponent';
|
||||||
import { EventFieldDiff } from '@/activities/timelineActivities/rows/mainObject/components/EventFieldDiff';
|
import { EventFieldDiffContainer } from '@/activities/timelineActivities/rows/main-object/components/EventFieldDiffContainer';
|
||||||
import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity';
|
import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity';
|
||||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
@ -33,34 +31,6 @@ const StyledEventRowMainObjectUpdatedContainer = styled.div`
|
|||||||
gap: ${({ theme }) => theme.spacing(1)};
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const renderUpdateDescription = (
|
|
||||||
mainObjectMetadataItem: ObjectMetadataItem,
|
|
||||||
diffKey: string,
|
|
||||||
diffValue: any,
|
|
||||||
eventId: string,
|
|
||||||
fieldMetadataItemMap: Record<string, FieldMetadataItem>,
|
|
||||||
) => {
|
|
||||||
const fieldMetadataItem = fieldMetadataItemMap[diffKey];
|
|
||||||
|
|
||||||
if (!fieldMetadataItem) {
|
|
||||||
throw new Error(
|
|
||||||
`Cannot find field metadata item for field name ${diffKey} on object ${mainObjectMetadataItem.nameSingular}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const forgedRecordId = eventId + '--' + fieldMetadataItem.id;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EventFieldDiff
|
|
||||||
key={forgedRecordId}
|
|
||||||
diffRecord={diffValue}
|
|
||||||
fieldMetadataItem={fieldMetadataItem}
|
|
||||||
mainObjectMetadataItem={mainObjectMetadataItem}
|
|
||||||
forgedRecordId={forgedRecordId}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const EventRowMainObjectUpdated = ({
|
export const EventRowMainObjectUpdated = ({
|
||||||
authorFullName,
|
authorFullName,
|
||||||
labelIdentifierValue,
|
labelIdentifierValue,
|
||||||
@ -86,17 +56,18 @@ export const EventRowMainObjectUpdated = ({
|
|||||||
return (
|
return (
|
||||||
<StyledEventRowMainObjectUpdatedContainer>
|
<StyledEventRowMainObjectUpdatedContainer>
|
||||||
<StyledRowContainer>
|
<StyledRowContainer>
|
||||||
<StyledItemAuthorText>{authorFullName}</StyledItemAuthorText>
|
<StyledEventRowItemColumn>{authorFullName}</StyledEventRowItemColumn>
|
||||||
<StyledItemAction>
|
<StyledEventRowItemAction>
|
||||||
updated
|
updated
|
||||||
{diffEntries.length === 1 &&
|
{diffEntries.length === 1 && (
|
||||||
renderUpdateDescription(
|
<EventFieldDiffContainer
|
||||||
mainObjectMetadataItem,
|
mainObjectMetadataItem={mainObjectMetadataItem}
|
||||||
diffEntries[0][0],
|
diffKey={diffEntries[0][0]}
|
||||||
diffEntries[0][1].after,
|
diffValue={diffEntries[0][1].after}
|
||||||
event.id,
|
eventId={event.id}
|
||||||
fieldMetadataItemMap,
|
fieldMetadataItemMap={fieldMetadataItemMap}
|
||||||
)}
|
/>
|
||||||
|
)}
|
||||||
{diffEntries.length > 1 && (
|
{diffEntries.length > 1 && (
|
||||||
<>
|
<>
|
||||||
<span>
|
<span>
|
||||||
@ -105,19 +76,20 @@ export const EventRowMainObjectUpdated = ({
|
|||||||
<EventCardToggleButton isOpen={isOpen} setIsOpen={setIsOpen} />
|
<EventCardToggleButton isOpen={isOpen} setIsOpen={setIsOpen} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</StyledItemAction>
|
</StyledEventRowItemAction>
|
||||||
</StyledRowContainer>
|
</StyledRowContainer>
|
||||||
{diffEntries.length > 1 && (
|
{diffEntries.length > 1 && (
|
||||||
<EventCard isOpen={isOpen}>
|
<EventCard isOpen={isOpen}>
|
||||||
{diffEntries.map(([diffKey, diffValue]) =>
|
{diffEntries.map(([diffKey, diffValue]) => (
|
||||||
renderUpdateDescription(
|
<EventFieldDiffContainer
|
||||||
mainObjectMetadataItem,
|
key={diffKey}
|
||||||
diffKey,
|
mainObjectMetadataItem={mainObjectMetadataItem}
|
||||||
diffValue.after,
|
diffKey={diffKey}
|
||||||
event.id,
|
diffValue={diffValue.after}
|
||||||
fieldMetadataItemMap,
|
eventId={event.id}
|
||||||
),
|
fieldMetadataItemMap={fieldMetadataItemMap}
|
||||||
)}
|
/>
|
||||||
|
))}
|
||||||
</EventCard>
|
</EventCard>
|
||||||
)}
|
)}
|
||||||
</StyledEventRowMainObjectUpdatedContainer>
|
</StyledEventRowMainObjectUpdatedContainer>
|
||||||
@ -0,0 +1,51 @@
|
|||||||
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { ComponentDecorator, RouterDecorator } from 'twenty-ui';
|
||||||
|
|
||||||
|
import { EventRowMainObjectUpdated } from '@/activities/timelineActivities/rows/main-object/components/EventRowMainObjectUpdated';
|
||||||
|
import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity';
|
||||||
|
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
|
||||||
|
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
||||||
|
import { mockedPersonObjectMetadataItem } from '~/testing/mock-data/metadata';
|
||||||
|
|
||||||
|
const meta: Meta<typeof EventRowMainObjectUpdated> = {
|
||||||
|
title: 'Modules/TimelineActivities/Rows/MainObject/EventRowMainObjectUpdated',
|
||||||
|
component: EventRowMainObjectUpdated,
|
||||||
|
args: {
|
||||||
|
authorFullName: 'John Doe',
|
||||||
|
labelIdentifierValue: 'Mock',
|
||||||
|
event: {
|
||||||
|
id: '1',
|
||||||
|
name: 'mock.updated',
|
||||||
|
properties: {
|
||||||
|
diff: {
|
||||||
|
jobTitle: {
|
||||||
|
after: 'mock job title',
|
||||||
|
before: '',
|
||||||
|
},
|
||||||
|
linkedinLink: {
|
||||||
|
after: {
|
||||||
|
url: 'mock.linkedin',
|
||||||
|
label: 'mock linkedin url',
|
||||||
|
},
|
||||||
|
before: {
|
||||||
|
url: '',
|
||||||
|
label: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as TimelineActivity,
|
||||||
|
mainObjectMetadataItem: mockedPersonObjectMetadataItem,
|
||||||
|
},
|
||||||
|
decorators: [
|
||||||
|
ComponentDecorator,
|
||||||
|
ObjectMetadataItemsDecorator,
|
||||||
|
SnackBarDecorator,
|
||||||
|
RouterDecorator,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof EventRowMainObjectUpdated>;
|
||||||
|
|
||||||
|
export const Default: Story = {};
|
||||||
@ -101,7 +101,7 @@ export const EventCardMessage = ({
|
|||||||
return <div>Loading...</div>;
|
return <div>Loading...</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const messageParticipantHandles = message?.messageParticipants
|
const messageParticipantHandles = message.messageParticipants
|
||||||
.map((participant) => participant.handle)
|
.map((participant) => participant.handle)
|
||||||
.join(', ');
|
.join(', ');
|
||||||
|
|
||||||
|
|||||||
@ -1,15 +1,12 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
import {
|
import { EventCard } from '@/activities/timelineActivities/rows/components/EventCard';
|
||||||
EventCard,
|
import { EventCardToggleButton } from '@/activities/timelineActivities/rows/components/EventCardToggleButton';
|
||||||
EventCardToggleButton,
|
|
||||||
} from '@/activities/timelineActivities/rows/components/EventCard';
|
|
||||||
import {
|
import {
|
||||||
EventRowDynamicComponentProps,
|
EventRowDynamicComponentProps,
|
||||||
StyledItemAction,
|
StyledEventRowItemAction,
|
||||||
StyledItemAuthorText,
|
StyledEventRowItemColumn,
|
||||||
StyledItemLabelIdentifier,
|
|
||||||
} from '@/activities/timelineActivities/rows/components/EventRowDynamicComponent';
|
} from '@/activities/timelineActivities/rows/components/EventRowDynamicComponent';
|
||||||
import { EventCardMessage } from '@/activities/timelineActivities/rows/message/components/EventCardMessage';
|
import { EventCardMessage } from '@/activities/timelineActivities/rows/message/components/EventCardMessage';
|
||||||
|
|
||||||
@ -27,36 +24,28 @@ const StyledRowContainer = styled.div`
|
|||||||
gap: ${({ theme }) => theme.spacing(1)};
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const EventRowMessage: React.FC<EventRowMessageProps> = ({
|
export const EventRowMessage = ({
|
||||||
labelIdentifierValue,
|
|
||||||
event,
|
event,
|
||||||
authorFullName,
|
authorFullName,
|
||||||
|
labelIdentifierValue,
|
||||||
}: EventRowMessageProps) => {
|
}: EventRowMessageProps) => {
|
||||||
const [, eventAction] = event.name.split('.');
|
const [, eventAction] = event.name.split('.');
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
const renderRow = () => {
|
if (['linked'].includes(eventAction) === false) {
|
||||||
switch (eventAction) {
|
throw new Error('Invalid event action for message event type.');
|
||||||
case 'linked': {
|
}
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<StyledItemAuthorText>{authorFullName}</StyledItemAuthorText>
|
|
||||||
<StyledItemAction>linked an email with</StyledItemAction>
|
|
||||||
<StyledItemLabelIdentifier>
|
|
||||||
{labelIdentifierValue}
|
|
||||||
</StyledItemLabelIdentifier>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
throw new Error('Invalid event action for message event type.');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledEventRowMessageContainer>
|
<StyledEventRowMessageContainer>
|
||||||
<StyledRowContainer>
|
<StyledRowContainer>
|
||||||
{renderRow()}
|
<StyledEventRowItemColumn>{authorFullName}</StyledEventRowItemColumn>
|
||||||
|
<StyledEventRowItemAction>
|
||||||
|
linked an email with
|
||||||
|
</StyledEventRowItemAction>
|
||||||
|
<StyledEventRowItemColumn>
|
||||||
|
{labelIdentifierValue}
|
||||||
|
</StyledEventRowItemColumn>
|
||||||
<EventCardToggleButton isOpen={isOpen} setIsOpen={setIsOpen} />
|
<EventCardToggleButton isOpen={isOpen} setIsOpen={setIsOpen} />
|
||||||
</StyledRowContainer>
|
</StyledRowContainer>
|
||||||
<EventCard isOpen={isOpen}>
|
<EventCard isOpen={isOpen}>
|
||||||
|
|||||||
@ -0,0 +1,82 @@
|
|||||||
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { graphql, HttpResponse } from 'msw';
|
||||||
|
import { ComponentDecorator } from 'twenty-ui';
|
||||||
|
|
||||||
|
import { TimelineActivityContext } from '@/activities/timelineActivities/contexts/TimelineActivityContext';
|
||||||
|
import { EventCardMessage } from '@/activities/timelineActivities/rows/message/components/EventCardMessage';
|
||||||
|
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
|
||||||
|
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
||||||
|
|
||||||
|
const meta: Meta<typeof EventCardMessage> = {
|
||||||
|
title: 'Modules/TimelineActivities/Rows/Message/EventCardMessage',
|
||||||
|
component: EventCardMessage,
|
||||||
|
decorators: [
|
||||||
|
ComponentDecorator,
|
||||||
|
ObjectMetadataItemsDecorator,
|
||||||
|
SnackBarDecorator,
|
||||||
|
(Story) => {
|
||||||
|
return (
|
||||||
|
<TimelineActivityContext.Provider
|
||||||
|
value={{
|
||||||
|
labelIdentifierValue: 'Mock',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Story />
|
||||||
|
</TimelineActivityContext.Provider>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof EventCardMessage>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
messageId: '1',
|
||||||
|
authorFullName: 'John Doe',
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
msw: {
|
||||||
|
handlers: [
|
||||||
|
graphql.query('FindOneMessage', () => {
|
||||||
|
return HttpResponse.json({
|
||||||
|
data: {
|
||||||
|
message: {
|
||||||
|
id: '1',
|
||||||
|
subject: 'Mock title',
|
||||||
|
text: 'Mock body',
|
||||||
|
messageParticipants: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NotShared: Story = {
|
||||||
|
args: {
|
||||||
|
messageId: '1',
|
||||||
|
authorFullName: 'John Doe',
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
msw: {
|
||||||
|
handlers: [
|
||||||
|
graphql.query('FindOneMessage', () => {
|
||||||
|
return HttpResponse.json({
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
message: 'Forbidden',
|
||||||
|
extensions: {
|
||||||
|
code: 'FORBIDDEN',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -0,0 +1,63 @@
|
|||||||
|
import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity';
|
||||||
|
import { getTimelineActivityAuthorFullName } from '@/activities/timelineActivities/utils/getTimelineActivityAuthorFullName';
|
||||||
|
import { CurrentWorkspaceMember } from '@/auth/states/currentWorkspaceMemberState';
|
||||||
|
|
||||||
|
describe('getTimelineActivityAuthorFullName', () => {
|
||||||
|
it('should return "You" if the current workspace member is the author', () => {
|
||||||
|
const event = {
|
||||||
|
workspaceMember: {
|
||||||
|
id: '123',
|
||||||
|
name: {
|
||||||
|
firstName: 'John',
|
||||||
|
lastName: 'Doe',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const currentWorkspaceMember = {
|
||||||
|
id: '123',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getTimelineActivityAuthorFullName(
|
||||||
|
event as TimelineActivity,
|
||||||
|
currentWorkspaceMember as CurrentWorkspaceMember,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBe('You');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the full name of the workspace member if they are not the current workspace member', () => {
|
||||||
|
const event = {
|
||||||
|
workspaceMember: {
|
||||||
|
id: '456',
|
||||||
|
name: {
|
||||||
|
firstName: 'Jane',
|
||||||
|
lastName: 'Smith',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const currentWorkspaceMember = {
|
||||||
|
id: '123',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getTimelineActivityAuthorFullName(
|
||||||
|
event as TimelineActivity,
|
||||||
|
currentWorkspaceMember as CurrentWorkspaceMember,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBe('Jane Smith');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return "Twenty" if the workspace member is not defined', () => {
|
||||||
|
const event = {};
|
||||||
|
const currentWorkspaceMember = {
|
||||||
|
id: '123',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getTimelineActivityAuthorFullName(
|
||||||
|
event as TimelineActivity,
|
||||||
|
currentWorkspaceMember as CurrentWorkspaceMember,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBe('Twenty');
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity';
|
||||||
|
import { CurrentWorkspaceMember } from '@/auth/states/currentWorkspaceMemberState';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
|
export const getTimelineActivityAuthorFullName = (
|
||||||
|
event: TimelineActivity,
|
||||||
|
currentWorkspaceMember: CurrentWorkspaceMember,
|
||||||
|
) => {
|
||||||
|
if (isDefined(event.workspaceMember)) {
|
||||||
|
return currentWorkspaceMember.id === event.workspaceMember.id
|
||||||
|
? 'You'
|
||||||
|
: `${event.workspaceMember?.name.firstName} ${event.workspaceMember?.name.lastName}`;
|
||||||
|
}
|
||||||
|
return 'Twenty';
|
||||||
|
};
|
||||||
@ -3638,7 +3638,7 @@ export const getObjectMetadataItemsMock = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
__typename: 'object',
|
__typename: 'object',
|
||||||
id: '20202020-049d-4d0c-9e7c-e74fee3f88b2',
|
id: '20202020-6736-4337-b5c4-8b39fae325a5',
|
||||||
dataSourceId: '20202020-7f63-47a9-b1b3-6c7290ca9fb1',
|
dataSourceId: '20202020-7f63-47a9-b1b3-6c7290ca9fb1',
|
||||||
nameSingular: 'timelineActivity',
|
nameSingular: 'timelineActivity',
|
||||||
namePlural: 'timelineActivities',
|
namePlural: 'timelineActivities',
|
||||||
@ -3654,6 +3654,24 @@ export const getObjectMetadataItemsMock = () => {
|
|||||||
updatedAt: '2023-11-30T11:13:15.206Z',
|
updatedAt: '2023-11-30T11:13:15.206Z',
|
||||||
fields: [],
|
fields: [],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
__typename: 'object',
|
||||||
|
id: '20202020-3f6b-4425-80ab-e468899ab4b2',
|
||||||
|
dataSourceId: '20202020-7f63-47a9-b1b3-6c7290ca9fb1',
|
||||||
|
nameSingular: 'message',
|
||||||
|
namePlural: 'messages',
|
||||||
|
labelSingular: 'Message',
|
||||||
|
labelPlural: 'Messages',
|
||||||
|
description: 'A message',
|
||||||
|
icon: 'IconMessage',
|
||||||
|
isCustom: false,
|
||||||
|
isRemote: false,
|
||||||
|
isActive: true,
|
||||||
|
isSystem: true,
|
||||||
|
createdAt: '2023-11-30T11:13:15.206Z',
|
||||||
|
updatedAt: '2023-11-30T11:13:15.206Z',
|
||||||
|
fields: [],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// Todo fix typing here (the backend is not in sync with the frontend)
|
// Todo fix typing here (the backend is not in sync with the frontend)
|
||||||
|
|||||||
@ -0,0 +1,29 @@
|
|||||||
|
import {
|
||||||
|
formatToHumanReadableDay,
|
||||||
|
formatToHumanReadableMonth,
|
||||||
|
formatToHumanReadableTime,
|
||||||
|
} from '../formatDate';
|
||||||
|
|
||||||
|
describe('formatToHumanReadableMonth', () => {
|
||||||
|
it('should format the date to a human-readable month', () => {
|
||||||
|
const date = new Date('2022-01-01');
|
||||||
|
const result = formatToHumanReadableMonth(date);
|
||||||
|
expect(result).toBe('Jan');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('formatToHumanReadableDay', () => {
|
||||||
|
it('should format the date to a human-readable day', () => {
|
||||||
|
const date = new Date('2022-01-01');
|
||||||
|
const result = formatToHumanReadableDay(date);
|
||||||
|
expect(result).toBe('1');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('formatToHumanReadableTime', () => {
|
||||||
|
it('should format the date to a human-readable time', () => {
|
||||||
|
const date = new Date('2022-01-01T12:30:00');
|
||||||
|
const result = formatToHumanReadableTime(date);
|
||||||
|
expect(result).toBe('12:30 PM');
|
||||||
|
});
|
||||||
|
});
|
||||||
26
packages/twenty-front/src/utils/format/formatDate.ts
Normal file
26
packages/twenty-front/src/utils/format/formatDate.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { parseDate } from '~/utils/date-utils';
|
||||||
|
|
||||||
|
export const formatToHumanReadableMonth = (date: Date | string) => {
|
||||||
|
const parsedJSDate = parseDate(date).toJSDate();
|
||||||
|
|
||||||
|
return new Intl.DateTimeFormat(undefined, {
|
||||||
|
month: 'short',
|
||||||
|
}).format(parsedJSDate);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatToHumanReadableDay = (date: Date | string) => {
|
||||||
|
const parsedJSDate = parseDate(date).toJSDate();
|
||||||
|
|
||||||
|
return new Intl.DateTimeFormat(undefined, {
|
||||||
|
day: 'numeric',
|
||||||
|
}).format(parsedJSDate);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatToHumanReadableTime = (date: Date | string) => {
|
||||||
|
const parsedJSDate = parseDate(date).toJSDate();
|
||||||
|
|
||||||
|
return new Intl.DateTimeFormat(undefined, {
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: 'numeric',
|
||||||
|
}).format(parsedJSDate);
|
||||||
|
};
|
||||||
@ -22,31 +22,6 @@ export const formatToHumanReadableDateTime = (date: Date | string) => {
|
|||||||
}).format(parsedJSDate);
|
}).format(parsedJSDate);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const formatToHumanReadableMonth = (date: Date | string) => {
|
|
||||||
const parsedJSDate = parseDate(date).toJSDate();
|
|
||||||
|
|
||||||
return new Intl.DateTimeFormat(undefined, {
|
|
||||||
month: 'short',
|
|
||||||
}).format(parsedJSDate);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const formatToHumanReadableDay = (date: Date | string) => {
|
|
||||||
const parsedJSDate = parseDate(date).toJSDate();
|
|
||||||
|
|
||||||
return new Intl.DateTimeFormat(undefined, {
|
|
||||||
day: 'numeric',
|
|
||||||
}).format(parsedJSDate);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const formatToHumanReadableTime = (date: Date | string) => {
|
|
||||||
const parsedJSDate = parseDate(date).toJSDate();
|
|
||||||
|
|
||||||
return new Intl.DateTimeFormat(undefined, {
|
|
||||||
hour: 'numeric',
|
|
||||||
minute: 'numeric',
|
|
||||||
}).format(parsedJSDate);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const sanitizeURL = (link: string | null | undefined) => {
|
export const sanitizeURL = (link: string | null | undefined) => {
|
||||||
return link
|
return link
|
||||||
? link.replace(/(https?:\/\/)|(www\.)/g, '').replace(/\/$/, '')
|
? link.replace(/(https?:\/\/)|(www\.)/g, '').replace(/\/$/, '')
|
||||||
|
|||||||
Reference in New Issue
Block a user