New Timeline (#4936)
Refactored the code to introduce two different concepts: - AuditLogs (immutable, raw data) - TimelineActivities (user-friendly, transformed data) Still some work needed: - Add message, files, calendar events to timeline (~2 hours if done naively) - Refactor repository to try to abstract concept when we can (tbd, wait for Twenty ORM) - Introduce ability to display child timelines on parent timeline with filtering (~2 days) - Improve UI: add links to open note/task, improve diff display, etc (half a day) - Decide the path forward for Task vs Notes: either introduce a new field type "Record Type" and start going into that direction ; or split in two objects? - Trigger updates when a field is changed (will be solved by real-time / websockets: 2 weeks) - Integrate behavioral events (1 day for POC, 1 week for clean/documented) <img width="1248" alt="Screenshot 2024-04-12 at 09 24 49" src="https://github.com/twentyhq/twenty/assets/6399865/9428db1a-ab2b-492c-8b0b-d4d9a36e81fa">
This commit is contained in:
@ -0,0 +1,16 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
|
||||
export const useLinkedObject = (id: string) => {
|
||||
const objectMetadataItems: ObjectMetadataItem[] = useRecoilValue(
|
||||
objectMetadataItemsState,
|
||||
);
|
||||
|
||||
return (
|
||||
objectMetadataItems.find(
|
||||
(objectMetadataItem) => objectMetadataItem.id === id,
|
||||
) ?? null
|
||||
);
|
||||
};
|
||||
@ -1,16 +1,17 @@
|
||||
import { ReactElement } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { EventsGroup } from '@/activities/events/components/EventsGroup';
|
||||
import { Event } from '@/activities/events/types/Event';
|
||||
import { groupEventsByMonth } from '@/activities/events/utils/groupEventsByMonth';
|
||||
import { EventsGroup } from '@/activities/timelineActivities/components/EventsGroup';
|
||||
import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity';
|
||||
import { groupEventsByMonth } from '@/activities/timelineActivities/utils/groupEventsByMonth';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
||||
|
||||
type EventListProps = {
|
||||
targetableObject: ActivityTargetableObject;
|
||||
title: string;
|
||||
events: Event[];
|
||||
events: TimelineActivity[];
|
||||
button?: ReactElement | false;
|
||||
};
|
||||
|
||||
@ -31,12 +32,16 @@ const StyledTimelineContainer = styled.div`
|
||||
export const EventList = ({ events, targetableObject }: EventListProps) => {
|
||||
const groupedEvents = groupEventsByMonth(events);
|
||||
|
||||
const mainObjectMetadataItem = useObjectMetadataItem({
|
||||
objectNameSingular: targetableObject.targetObjectNameSingular,
|
||||
}).objectMetadataItem;
|
||||
|
||||
return (
|
||||
<ScrollWrapper>
|
||||
<StyledTimelineContainer>
|
||||
{groupedEvents.map((group, index) => (
|
||||
<EventsGroup
|
||||
targetableObject={targetableObject}
|
||||
mainObjectMetadataItem={mainObjectMetadataItem}
|
||||
key={group.year.toString() + group.month}
|
||||
group={group}
|
||||
month={new Date(group.items[0].createdAt).toLocaleString(
|
||||
@ -1,15 +1,25 @@
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import styled from '@emotion/styled';
|
||||
import { IconCirclePlus, IconEditCircle, IconFocusCentered } from 'twenty-ui';
|
||||
import {
|
||||
IconCheckbox,
|
||||
IconCirclePlus,
|
||||
IconEditCircle,
|
||||
IconFocusCentered,
|
||||
IconNotes,
|
||||
useIcons,
|
||||
} from 'twenty-ui';
|
||||
|
||||
import { EventUpdateProperty } from '@/activities/events/components/EventUpdateProperty';
|
||||
import { Event } from '@/activities/events/types/Event';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
|
||||
import { useLinkedObject } from '@/activities/timeline/hooks/useLinkedObject';
|
||||
import { EventUpdateProperty } from '@/activities/timelineActivities/components/EventUpdateProperty';
|
||||
import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
import {
|
||||
beautifyExactDateTime,
|
||||
beautifyPastDateRelativeToNow,
|
||||
} from '~/utils/date-utils';
|
||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
|
||||
const StyledIconContainer = styled.div`
|
||||
align-items: center;
|
||||
@ -43,14 +53,6 @@ const StyledItemContainer = styled.div`
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const StyledItemTitleContainer = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-flow: row ${() => (useIsMobile() ? 'wrap' : 'nowrap')};
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const StyledItemAuthorText = styled.span`
|
||||
display: flex;
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
@ -65,6 +67,11 @@ const StyledItemTitle = styled.span`
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const StyledLinkedObject = styled.span`
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
`;
|
||||
|
||||
const StyledItemTitleDate = styled.div`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
@ -117,16 +124,24 @@ const StyledTimelineItemContainer = styled.div<{ isGap?: boolean }>`
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const StyledSummary = styled.summary`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-flow: row ${() => (useIsMobile() ? 'wrap' : 'nowrap')};
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
type EventRowProps = {
|
||||
targetableObject: ActivityTargetableObject;
|
||||
mainObjectMetadataItem: ObjectMetadataItem | null;
|
||||
isLastEvent?: boolean;
|
||||
event: Event;
|
||||
event: TimelineActivity;
|
||||
};
|
||||
|
||||
export const EventRow = ({
|
||||
isLastEvent,
|
||||
event,
|
||||
targetableObject,
|
||||
mainObjectMetadataItem,
|
||||
}: EventRowProps) => {
|
||||
const beautifiedCreatedAt = beautifyPastDateRelativeToNow(event.createdAt);
|
||||
const exactCreatedAt = beautifyExactDateTime(event.createdAt);
|
||||
@ -135,47 +150,108 @@ export const EventRow = ({
|
||||
const diff: Record<string, { before: any; after: any }> = properties?.diff;
|
||||
|
||||
const isEventType = (type: 'created' | 'updated') => {
|
||||
return (
|
||||
event.name === type + '.' + targetableObject.targetObjectNameSingular
|
||||
);
|
||||
if (event.name.includes('.')) {
|
||||
return event.name.split('.')[1] === type;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
const linkedObjectMetadata = useLinkedObject(event.linkedObjectMetadataId);
|
||||
|
||||
const linkedObjectLabel = event.name.includes('note')
|
||||
? 'note'
|
||||
: event.name.includes('task')
|
||||
? 'task'
|
||||
: linkedObjectMetadata?.labelSingular;
|
||||
|
||||
const ActivityIcon = event.linkedObjectMetadataId
|
||||
? event.name.includes('note')
|
||||
? IconNotes
|
||||
: event.name.includes('task')
|
||||
? IconCheckbox
|
||||
: getIcon(linkedObjectMetadata?.icon)
|
||||
: isEventType('created')
|
||||
? IconCirclePlus
|
||||
: isEventType('updated')
|
||||
? IconEditCircle
|
||||
: IconFocusCentered;
|
||||
|
||||
const author =
|
||||
event.workspaceMember?.name.firstName +
|
||||
' ' +
|
||||
event.workspaceMember?.name.lastName;
|
||||
|
||||
const action = isEventType('created')
|
||||
? 'created'
|
||||
: isEventType('updated')
|
||||
? 'updated'
|
||||
: event.name;
|
||||
|
||||
let description;
|
||||
|
||||
if (!isUndefinedOrNull(linkedObjectMetadata)) {
|
||||
description = 'a ' + linkedObjectLabel;
|
||||
} else if (!event.linkedObjectMetadataId && isEventType('created')) {
|
||||
description = `a new ${mainObjectMetadataItem?.labelSingular}`;
|
||||
} else if (isEventType('updated')) {
|
||||
const diffKeys = Object.keys(diff);
|
||||
if (diffKeys.length === 0) {
|
||||
description = `a ${mainObjectMetadataItem?.labelSingular}`;
|
||||
} else if (diffKeys.length === 1) {
|
||||
const [key, value] = Object.entries(diff)[0];
|
||||
description = [
|
||||
<EventUpdateProperty
|
||||
propertyName={key}
|
||||
after={value?.after as string}
|
||||
/>,
|
||||
];
|
||||
} else if (diffKeys.length === 2) {
|
||||
description =
|
||||
mainObjectMetadataItem?.fields.find(
|
||||
(field) => diffKeys[0] === field.name,
|
||||
)?.label +
|
||||
' and ' +
|
||||
mainObjectMetadataItem?.fields.find(
|
||||
(field) => diffKeys[1] === field.name,
|
||||
)?.label;
|
||||
} else if (diffKeys.length > 2) {
|
||||
description =
|
||||
diffKeys[0] + ' and ' + (diffKeys.length - 1) + ' other fields';
|
||||
}
|
||||
} else if (!isEventType('created') && !isEventType('updated')) {
|
||||
description = JSON.stringify(diff);
|
||||
}
|
||||
const details = JSON.stringify(diff);
|
||||
|
||||
const openActivityRightDrawer = useOpenActivityRightDrawer();
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledTimelineItemContainer>
|
||||
<StyledIconContainer>
|
||||
{isEventType('created') && <IconCirclePlus />}
|
||||
{isEventType('updated') && <IconEditCircle />}
|
||||
{!isEventType('created') && !isEventType('updated') && (
|
||||
<IconFocusCentered />
|
||||
)}
|
||||
<ActivityIcon />
|
||||
</StyledIconContainer>
|
||||
<StyledItemContainer>
|
||||
<StyledItemTitleContainer>
|
||||
<StyledItemAuthorText>
|
||||
{event.workspaceMember?.name.firstName}{' '}
|
||||
{event.workspaceMember?.name.lastName}
|
||||
</StyledItemAuthorText>
|
||||
<StyledActionName>
|
||||
{isEventType('created') && 'created'}
|
||||
{isEventType('updated') && 'updated'}
|
||||
{!isEventType('created') && !isEventType('updated') && event.name}
|
||||
</StyledActionName>
|
||||
<StyledItemTitle>
|
||||
{isEventType('created') &&
|
||||
`a new ${targetableObject.targetObjectNameSingular}`}
|
||||
{isEventType('updated') &&
|
||||
Object.entries(diff).map(([key, value]) => (
|
||||
<EventUpdateProperty
|
||||
propertyName={key}
|
||||
after={value?.after}
|
||||
></EventUpdateProperty>
|
||||
))}
|
||||
{!isEventType('created') &&
|
||||
!isEventType('updated') &&
|
||||
JSON.stringify(diff)}
|
||||
</StyledItemTitle>
|
||||
</StyledItemTitleContainer>
|
||||
<details>
|
||||
<StyledSummary>
|
||||
<StyledItemAuthorText>{author}</StyledItemAuthorText>
|
||||
<StyledActionName>{action}</StyledActionName>
|
||||
<StyledItemTitle>{description}</StyledItemTitle>
|
||||
{isUndefinedOrNull(linkedObjectMetadata) ? (
|
||||
<></>
|
||||
) : (
|
||||
<StyledLinkedObject
|
||||
onClick={() => openActivityRightDrawer(event.linkedRecordId)}
|
||||
>
|
||||
{event.linkedRecordCachedName}
|
||||
</StyledLinkedObject>
|
||||
)}
|
||||
</StyledSummary>
|
||||
{details}
|
||||
</details>
|
||||
|
||||
<StyledItemTitleDate id={`id-${event.id}`}>
|
||||
{beautifiedCreatedAt}
|
||||
</StyledItemTitleDate>
|
||||
@ -4,6 +4,7 @@ import { IconArrowRight } from 'twenty-ui';
|
||||
|
||||
type EventUpdatePropertyProps = {
|
||||
propertyName: string;
|
||||
before?: string;
|
||||
after?: string;
|
||||
};
|
||||
|
||||
@ -23,9 +24,9 @@ export const EventUpdateProperty = ({
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledPropertyName>{propertyName}</StyledPropertyName>
|
||||
<StyledPropertyName>{propertyName ?? '(empty)'}</StyledPropertyName>
|
||||
<IconArrowRight size={theme.icon.size.sm} stroke={theme.icon.stroke.sm} />
|
||||
{after}
|
||||
{JSON.stringify(after)}
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
@ -1,14 +1,14 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { EventRow } from '@/activities/events/components/EventRow';
|
||||
import { EventGroup } from '@/activities/events/utils/groupEventsByMonth';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { EventRow } from '@/activities/timelineActivities/components/EventRow';
|
||||
import { EventGroup } from '@/activities/timelineActivities/utils/groupEventsByMonth';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
|
||||
type EventsGroupProps = {
|
||||
group: EventGroup;
|
||||
month: string;
|
||||
year?: number;
|
||||
targetableObject: ActivityTargetableObject;
|
||||
mainObjectMetadataItem: ObjectMetadataItem | null;
|
||||
};
|
||||
|
||||
const StyledActivityGroup = styled.div`
|
||||
@ -57,7 +57,7 @@ export const EventsGroup = ({
|
||||
group,
|
||||
month,
|
||||
year,
|
||||
targetableObject,
|
||||
mainObjectMetadataItem,
|
||||
}: EventsGroupProps) => {
|
||||
return (
|
||||
<StyledActivityGroup>
|
||||
@ -69,7 +69,7 @@ export const EventsGroup = ({
|
||||
<StyledActivityGroupBar />
|
||||
{group.items.map((event, index) => (
|
||||
<EventRow
|
||||
targetableObject={targetableObject}
|
||||
mainObjectMetadataItem={mainObjectMetadataItem}
|
||||
key={event.id}
|
||||
event={event}
|
||||
isLastEvent={index === group.items.length - 1}
|
||||
@ -1,8 +1,9 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { isNonEmptyArray } from '@sniptt/guards';
|
||||
|
||||
import { EventList } from '@/activities/events/components/EventList';
|
||||
import { useEvents } from '@/activities/events/hooks/useEvents';
|
||||
import { TimelineCreateButtonGroup } from '@/activities/timeline/components/TimelineCreateButtonGroup';
|
||||
import { EventList } from '@/activities/timelineActivities/components/EventList';
|
||||
import { useTimelineActivities } from '@/activities/timelineActivities/hooks/useTimelineActivities';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import AnimatedPlaceholder from '@/ui/layout/animated-placeholder/components/AnimatedPlaceholder';
|
||||
import {
|
||||
@ -25,25 +26,26 @@ const StyledMainContainer = styled.div`
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
export const Events = ({
|
||||
export const TimelineActivities = ({
|
||||
targetableObject,
|
||||
}: {
|
||||
targetableObject: ActivityTargetableObject;
|
||||
}) => {
|
||||
const { events } = useEvents(targetableObject);
|
||||
const { timelineActivities } = useTimelineActivities(targetableObject);
|
||||
|
||||
if (!isNonEmptyArray(events)) {
|
||||
if (!isNonEmptyArray(timelineActivities)) {
|
||||
return (
|
||||
<AnimatedPlaceholderEmptyContainer>
|
||||
<AnimatedPlaceholder type="emptyTimeline" />
|
||||
<AnimatedPlaceholderEmptyTextContainer>
|
||||
<AnimatedPlaceholderEmptyTitle>
|
||||
No Events
|
||||
Add your first Activity
|
||||
</AnimatedPlaceholderEmptyTitle>
|
||||
<AnimatedPlaceholderEmptySubTitle>
|
||||
There are no events associated with this record.{' '}
|
||||
There are no activities associated with this record.{' '}
|
||||
</AnimatedPlaceholderEmptySubTitle>
|
||||
</AnimatedPlaceholderEmptyTextContainer>
|
||||
<TimelineCreateButtonGroup targetableObject={targetableObject} />
|
||||
</AnimatedPlaceholderEmptyContainer>
|
||||
);
|
||||
}
|
||||
@ -53,7 +55,7 @@ export const Events = ({
|
||||
<EventList
|
||||
targetableObject={targetableObject}
|
||||
title="All"
|
||||
events={events ?? []}
|
||||
events={timelineActivities ?? []}
|
||||
/>
|
||||
</StyledMainContainer>
|
||||
);
|
||||
@ -1,53 +1,21 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
import { useEvents } from '@/activities/events/hooks/useEvents';
|
||||
import { useTimelineActivities } from '@/activities/timelineActivities/hooks/useTimelineActivities';
|
||||
|
||||
jest.mock('@/object-record/hooks/useFindManyRecords', () => ({
|
||||
useFindManyRecords: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('useEvent', () => {
|
||||
describe('useTimelineActivities', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('fetches events correctly for a given targetableObject', () => {
|
||||
const mockEvents = [
|
||||
const mockedTimelineActivities = [
|
||||
{
|
||||
__typename: 'Event',
|
||||
id: '166ec73f-26b1-4934-bb3b-c86c8513b99b',
|
||||
opportunityId: null,
|
||||
opportunity: null,
|
||||
personId: null,
|
||||
person: null,
|
||||
company: {
|
||||
__typename: 'Company',
|
||||
address: 'Paris',
|
||||
linkedinLink: {
|
||||
__typename: 'Link',
|
||||
label: '',
|
||||
url: '',
|
||||
},
|
||||
xLink: {
|
||||
__typename: 'Link',
|
||||
label: '',
|
||||
url: '',
|
||||
},
|
||||
position: 4,
|
||||
domainName: 'microsoft.com',
|
||||
employees: null,
|
||||
createdAt: '2024-03-21T16:01:41.809Z',
|
||||
annualRecurringRevenue: {
|
||||
__typename: 'Currency',
|
||||
amountMicros: 100000000,
|
||||
currencyCode: 'USD',
|
||||
},
|
||||
idealCustomerProfile: false,
|
||||
accountOwnerId: null,
|
||||
updatedAt: '2024-03-22T08:28:44.812Z',
|
||||
name: 'Microsoft',
|
||||
id: '460b6fb1-ed89-413a-b31a-962986e67bb4',
|
||||
},
|
||||
workspaceMember: {
|
||||
__typename: 'WorkspaceMember',
|
||||
locale: 'en',
|
||||
@ -81,11 +49,13 @@ describe('useEvent', () => {
|
||||
'@/object-record/hooks/useFindManyRecords',
|
||||
);
|
||||
useFindManyRecordsMock.useFindManyRecords.mockReturnValue({
|
||||
records: mockEvents,
|
||||
records: mockedTimelineActivities,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useEvents(mockTargetableObject));
|
||||
const { result } = renderHook(() =>
|
||||
useTimelineActivities(mockTargetableObject),
|
||||
);
|
||||
|
||||
expect(result.current.events).toEqual(mockEvents);
|
||||
expect(result.current.timelineActivities).toEqual(mockedTimelineActivities);
|
||||
});
|
||||
});
|
||||
@ -1,17 +1,19 @@
|
||||
import { Event } from '@/activities/events/types/Event';
|
||||
import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivityTargetObjectFieldIdName';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
|
||||
// do we need to test this?
|
||||
export const useEvents = (targetableObject: ActivityTargetableObject) => {
|
||||
export const useTimelineActivities = (
|
||||
targetableObject: ActivityTargetableObject,
|
||||
) => {
|
||||
const targetableObjectFieldIdName = getActivityTargetObjectFieldIdName({
|
||||
nameSingular: targetableObject.targetObjectNameSingular,
|
||||
});
|
||||
|
||||
const { records: events } = useFindManyRecords({
|
||||
objectNameSingular: CoreObjectNameSingular.Event,
|
||||
const { records: TimelineActivities } = useFindManyRecords({
|
||||
objectNameSingular: CoreObjectNameSingular.TimelineActivity,
|
||||
filter: {
|
||||
[targetableObjectFieldIdName]: {
|
||||
eq: targetableObject.id,
|
||||
@ -20,9 +22,10 @@ export const useEvents = (targetableObject: ActivityTargetableObject) => {
|
||||
orderBy: {
|
||||
createdAt: 'DescNullsFirst',
|
||||
},
|
||||
fetchPolicy: 'network-only',
|
||||
});
|
||||
|
||||
return {
|
||||
events: events as Event[],
|
||||
timelineActivities: TimelineActivities as TimelineActivity[],
|
||||
};
|
||||
};
|
||||
@ -1,15 +1,15 @@
|
||||
import { WorkspaceMember } from '~/generated/graphql';
|
||||
|
||||
export type Event = {
|
||||
export type TimelineActivity = {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
deletedAt: string | null;
|
||||
opportunityId: string | null;
|
||||
companyId: string | null;
|
||||
personId: string | null;
|
||||
workspaceMemberId: string;
|
||||
workspaceMember: WorkspaceMember;
|
||||
properties: any;
|
||||
name: string;
|
||||
linkedRecordCachedName: string;
|
||||
linkedRecordId: string;
|
||||
linkedObjectMetadataId: string;
|
||||
};
|
||||
@ -1,10 +1,10 @@
|
||||
import { mockedEvents } from '~/testing/mock-data/events';
|
||||
import { mockedTimelineActivities } from '~/testing/mock-data/timeline-activities';
|
||||
|
||||
import { groupEventsByMonth } from '../groupEventsByMonth';
|
||||
|
||||
describe('groupEventsByMonth', () => {
|
||||
it('should group activities by month', () => {
|
||||
const grouped = groupEventsByMonth(mockedEvents);
|
||||
const grouped = groupEventsByMonth(mockedTimelineActivities);
|
||||
|
||||
expect(grouped).toHaveLength(2);
|
||||
expect(grouped[0].items).toHaveLength(1);
|
||||
@ -1,13 +1,13 @@
|
||||
import { Event } from '@/activities/events/types/Event';
|
||||
import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export type EventGroup = {
|
||||
month: number;
|
||||
year: number;
|
||||
items: Event[];
|
||||
items: TimelineActivity[];
|
||||
};
|
||||
|
||||
export const groupEventsByMonth = (events: Event[]) => {
|
||||
export const groupEventsByMonth = (events: TimelineActivity[]) => {
|
||||
const acitivityGroups: EventGroup[] = [];
|
||||
|
||||
for (const event of events) {
|
||||
@ -33,7 +33,6 @@ export const objectMetadataItemFamilySelector = selectorFamily<
|
||||
) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
@ -9,7 +9,7 @@ export enum CoreObjectNameSingular {
|
||||
Comment = 'comment',
|
||||
Company = 'company',
|
||||
ConnectedAccount = 'connectedAccount',
|
||||
Event = 'event',
|
||||
TimelineActivity = 'timelineActivity',
|
||||
Favorite = 'favorite',
|
||||
Message = 'message',
|
||||
MessageChannel = 'messageChannel',
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { useQuery, WatchQueryFetchPolicy } from '@apollo/client';
|
||||
import { isNonEmptyArray } from '@apollo/client/utilities';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
@ -32,6 +32,7 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
|
||||
onCompleted,
|
||||
skip,
|
||||
queryFields,
|
||||
fetchPolicy,
|
||||
}: ObjectMetadataItemIdentifier &
|
||||
ObjectRecordQueryVariables & {
|
||||
onCompleted?: (
|
||||
@ -44,6 +45,7 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
|
||||
skip?: boolean;
|
||||
depth?: number;
|
||||
queryFields?: Record<string, any>;
|
||||
fetchPolicy?: WatchQueryFetchPolicy;
|
||||
}) => {
|
||||
const findManyQueryStateIdentifier =
|
||||
objectNameSingular +
|
||||
@ -84,6 +86,7 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
|
||||
limit,
|
||||
orderBy,
|
||||
},
|
||||
fetchPolicy: fetchPolicy,
|
||||
onCompleted: (data) => {
|
||||
if (!isDefined(data)) {
|
||||
onCompleted?.([]);
|
||||
|
||||
@ -11,12 +11,12 @@ import {
|
||||
|
||||
import { Calendar } from '@/activities/calendar/components/Calendar';
|
||||
import { EmailThreads } from '@/activities/emails/components/EmailThreads';
|
||||
import { Events } from '@/activities/events/components/Events';
|
||||
import { Attachments } from '@/activities/files/components/Attachments';
|
||||
import { Notes } from '@/activities/notes/components/Notes';
|
||||
import { ObjectTasks } from '@/activities/tasks/components/ObjectTasks';
|
||||
import { Timeline } from '@/activities/timeline/components/Timeline';
|
||||
import { TimelineQueryEffect } from '@/activities/timeline/components/TimelineQueryEffect';
|
||||
import { TimelineActivities } from '@/activities/timelineActivities/components/TimelineActivities';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { TabList } from '@/ui/layout/tab/components/TabList';
|
||||
@ -144,7 +144,9 @@ export const ShowPageRightContainer = ({
|
||||
{activeTabId === 'calendar' && (
|
||||
<Calendar targetableObject={targetableObject} />
|
||||
)}
|
||||
{activeTabId === 'logs' && <Events targetableObject={targetableObject} />}
|
||||
{activeTabId === 'logs' && (
|
||||
<TimelineActivities targetableObject={targetableObject} />
|
||||
)}
|
||||
</StyledShowPageRightContainer>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user