Activity timeline refactoring followup (#5835)

Following https://github.com/twentyhq/twenty/pull/5697, addressing
review
This commit is contained in:
Weiko
2024-06-12 16:21:30 +02:00
committed by GitHub
parent bd22bfce2e
commit ad6547948b
31 changed files with 607 additions and 293 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@ -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(/\/$/, '')