(
- (aggregateFilter, targetableObject) => {
- const targetableObjectFieldName = getActivityTargetObjectFieldIdName({
- nameSingular: targetableObject.targetObjectNameSingular,
- });
-
- if (isNonEmptyString(targetableObject.id)) {
- aggregateFilter[targetableObjectFieldName] = {
- eq: targetableObject.id,
- };
- }
-
- return aggregateFilter;
- },
- {},
- );
-
- const { records: activityTargets } = useFindManyRecords({
- objectNameSingular: CoreObjectNameSingular.ActivityTarget,
- filter: targetableObjectsFilter,
- skip: !isTargettingObjectRecords,
- });
-
- const skipRequest = !isNonEmptyArray(activityTargets) && !selectedFilter;
-
- const idFilter = isTargettingObjectRecords
- ? {
- id: {
- in: activityTargets.map(
- (activityTarget) => activityTarget.activityId,
- ),
- },
- }
- : { id: {} };
-
const assigneeIdFilter = selectedFilter
? {
assigneeId: {
@@ -68,32 +29,34 @@ export const useTasks = ({
}
: undefined;
- const { records: completeTasksData } = useFindManyRecords({
- objectNameSingular: CoreObjectNameSingular.Activity,
- skip: skipRequest,
- filter: {
+ const skipActivityTargets = !isNonEmptyArray(targetableObjects);
+
+ const {
+ activities: completeTasksData,
+ initialized: initializedCompleteTasks,
+ } = useActivities({
+ targetableObjects,
+ activitiesFilters: {
completedAt: { is: 'NOT_NULL' },
- ...idFilter,
type: { eq: 'Task' },
...assigneeIdFilter,
},
- orderBy: {
- createdAt: 'DescNullsFirst',
- },
+ activitiesOrderByVariables: FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY,
+ skipActivityTargets,
});
- const { records: incompleteTaskData } = useFindManyRecords({
- objectNameSingular: CoreObjectNameSingular.Activity,
- skip: skipRequest,
- filter: {
+ const {
+ activities: incompleteTaskData,
+ initialized: initializedIncompleteTasks,
+ } = useActivities({
+ targetableObjects,
+ activitiesFilters: {
completedAt: { is: 'NULL' },
- ...idFilter,
type: { eq: 'Task' },
...assigneeIdFilter,
},
- orderBy: {
- createdAt: 'DescNullsFirst',
- },
+ activitiesOrderByVariables: FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY,
+ skipActivityTargets,
});
const todayOrPreviousTasks = incompleteTaskData?.filter((task) => {
@@ -125,5 +88,6 @@ export const useTasks = ({
upcomingTasks: (upcomingTasks ?? []) as Activity[],
unscheduledTasks: (unscheduledTasks ?? []) as Activity[],
completedTasks: (completedTasks ?? []) as Activity[],
+ initialized: initializedCompleteTasks && initializedIncompleteTasks,
};
};
diff --git a/packages/twenty-front/src/modules/activities/timeline/components/Timeline.tsx b/packages/twenty-front/src/modules/activities/timeline/components/Timeline.tsx
index 4e3d085f2..e9c6c27cb 100644
--- a/packages/twenty-front/src/modules/activities/timeline/components/Timeline.tsx
+++ b/packages/twenty-front/src/modules/activities/timeline/components/Timeline.tsx
@@ -1,7 +1,11 @@
+import { useEffect } from 'react';
import styled from '@emotion/styled';
+import { useSetRecoilState } from 'recoil';
+import { useActivities } from '@/activities/hooks/useActivities';
import { TimelineCreateButtonGroup } from '@/activities/timeline/components/TimelineCreateButtonGroup';
-import { useTimelineActivities } from '@/activities/timeline/hooks/useTimelineActivities';
+import { FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY } from '@/activities/timeline/constants/FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY';
+import { timelineTargetableObjectState } from '@/activities/timeline/states/timelineTargetableObjectState';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import AnimatedPlaceholder from '@/ui/layout/animated-placeholder/components/AnimatedPlaceholder';
import {
@@ -11,6 +15,7 @@ import {
AnimatedPlaceholderEmptyTitle,
} from '@/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
+import { isDefined } from '~/utils/isDefined';
import { TimelineItemsContainer } from './TimelineItemsContainer';
@@ -31,11 +36,22 @@ export const Timeline = ({
}: {
targetableObject: ActivityTargetableObject;
}) => {
- const { activities, initialized } = useTimelineActivities({
- targetableObject,
+ const { activities, initialized, noActivities } = useActivities({
+ targetableObjects: [targetableObject],
+ activitiesFilters: {},
+ activitiesOrderByVariables: FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY,
+ skip: !isDefined(targetableObject),
});
- const showEmptyState = initialized && activities.length === 0;
+ const setTimelineTargetableObject = useSetRecoilState(
+ timelineTargetableObjectState,
+ );
+
+ useEffect(() => {
+ setTimelineTargetableObject(targetableObject);
+ }, [targetableObject, setTimelineTargetableObject]);
+
+ const showEmptyState = noActivities;
const showLoadingState = !initialized;
diff --git a/packages/twenty-front/src/modules/activities/timeline/components/TimelineActivity.tsx b/packages/twenty-front/src/modules/activities/timeline/components/TimelineActivity.tsx
index 435aaafd0..fbff3cf8d 100644
--- a/packages/twenty-front/src/modules/activities/timeline/components/TimelineActivity.tsx
+++ b/packages/twenty-front/src/modules/activities/timeline/components/TimelineActivity.tsx
@@ -1,13 +1,14 @@
import { Tooltip } from 'react-tooltip';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
+import { useRecoilValue } from 'recoil';
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
import { Activity } from '@/activities/types/Activity';
+import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { IconCheckbox, IconNotes } from '@/ui/display/icon';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { Avatar } from '@/users/components/Avatar';
-import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
import {
beautifyExactDateTime,
beautifyPastDateRelativeToNow,
@@ -135,19 +136,7 @@ const StyledTimelineItemContainer = styled.div<{ isGap?: boolean }>`
`;
type TimelineActivityProps = {
- activity: Pick<
- Activity,
- | 'id'
- | 'title'
- | 'body'
- | 'createdAt'
- | 'completedAt'
- | 'type'
- | 'comments'
- | 'dueAt'
- > & { author?: Pick
} & {
- assignee?: Pick | null;
- };
+ activity: Activity;
isLastActivity?: boolean;
};
@@ -160,6 +149,8 @@ export const TimelineActivity = ({
const openActivityRightDrawer = useOpenActivityRightDrawer();
const theme = useTheme();
+ const activityFromStore = useRecoilValue(recordStoreFamilyState(activity.id));
+
return (
<>
@@ -191,11 +182,13 @@ export const TimelineActivity = ({
{(activity.type === 'Note' || activity.type === 'Task') && (
openActivityRightDrawer(activity.id)}
+ onClick={() => openActivityRightDrawer(activity)}
>
“
-
- {activity.title ?? '(No Title)'}
+
+ {activityFromStore?.title ?? '(No Title)'}
“
diff --git a/packages/twenty-front/src/modules/activities/timeline/components/TimelineCreateButtonGroup.tsx b/packages/twenty-front/src/modules/activities/timeline/components/TimelineCreateButtonGroup.tsx
index e92623a28..b8542cd19 100644
--- a/packages/twenty-front/src/modules/activities/timeline/components/TimelineCreateButtonGroup.tsx
+++ b/packages/twenty-front/src/modules/activities/timeline/components/TimelineCreateButtonGroup.tsx
@@ -1,7 +1,7 @@
import { useSetRecoilState } from 'recoil';
import { Button, ButtonGroup } from 'tsup.ui.index';
-import { useOpenCreateActivityDrawerV2 } from '@/activities/hooks/useOpenCreateActivityDrawerV2';
+import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import {
IconCheckbox,
@@ -19,7 +19,7 @@ export const TimelineCreateButtonGroup = ({
const { getActiveTabIdState } = useTabList(TAB_LIST_COMPONENT_ID);
const setActiveTabId = useSetRecoilState(getActiveTabIdState());
- const openCreateActivity = useOpenCreateActivityDrawerV2();
+ const openCreateActivity = useOpenCreateActivityDrawer();
return (
@@ -30,7 +30,6 @@ export const TimelineCreateButtonGroup = ({
openCreateActivity({
type: 'Note',
targetableObjects: [targetableObject],
- timelineTargetableObject: targetableObject,
})
}
/>
@@ -41,7 +40,6 @@ export const TimelineCreateButtonGroup = ({
openCreateActivity({
type: 'Task',
targetableObjects: [targetableObject],
- timelineTargetableObject: targetableObject,
})
}
/>
diff --git a/packages/twenty-front/src/modules/activities/timeline/constants/FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY.ts b/packages/twenty-front/src/modules/activities/timeline/constants/FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY.ts
new file mode 100644
index 000000000..47de19c1e
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/timeline/constants/FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY.ts
@@ -0,0 +1,5 @@
+import { OrderByField } from '@/object-metadata/types/OrderByField';
+
+export const FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY: OrderByField = {
+ createdAt: 'DescNullsFirst',
+};
diff --git a/packages/twenty-front/src/modules/activities/timeline/hooks/useInjectIntoTimelineActivitiesQueries.ts b/packages/twenty-front/src/modules/activities/timeline/hooks/useInjectIntoTimelineActivitiesQueries.ts
new file mode 100644
index 000000000..ee637aac1
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/timeline/hooks/useInjectIntoTimelineActivitiesQueries.ts
@@ -0,0 +1,32 @@
+import { useInjectIntoActivitiesQuery } from '@/activities/hooks/useInjectIntoActivitiesQuery';
+import { Activity } from '@/activities/types/Activity';
+import { ActivityTarget } from '@/activities/types/ActivityTarget';
+import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
+
+export const useInjectIntoTimelineActivitiesQueries = () => {
+ const { injectActivitiesQueries } = useInjectIntoActivitiesQuery();
+
+ const injectIntoTimelineActivitiesQueries = ({
+ activityToInject,
+ activityTargetsToInject,
+ timelineTargetableObject,
+ }: {
+ activityToInject: Activity;
+ activityTargetsToInject: ActivityTarget[];
+ timelineTargetableObject: ActivityTargetableObject;
+ }) => {
+ injectActivitiesQueries({
+ activitiesFilters: {},
+ activitiesOrderByVariables: {
+ createdAt: 'DescNullsFirst',
+ },
+ activityTargetsToInject,
+ activityToInject,
+ targetableObjects: [timelineTargetableObject],
+ });
+ };
+
+ return {
+ injectIntoTimelineActivitiesQueries,
+ };
+};
diff --git a/packages/twenty-front/src/modules/activities/timeline/hooks/useRemoveFromTimelineActivitiesQueries.ts b/packages/twenty-front/src/modules/activities/timeline/hooks/useRemoveFromTimelineActivitiesQueries.ts
new file mode 100644
index 000000000..f5572f0f8
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/timeline/hooks/useRemoveFromTimelineActivitiesQueries.ts
@@ -0,0 +1,138 @@
+import { useRecoilValue } from 'recoil';
+
+import { useRemoveFromActivitiesQueries } from '@/activities/hooks/useRemoveFromActivitiesQueries';
+import { FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY } from '@/activities/timeline/constants/FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY';
+import { timelineTargetableObjectState } from '@/activities/timeline/states/timelineTargetableObjectState';
+import { ActivityTarget } from '@/activities/types/ActivityTarget';
+
+export const useRemoveFromTimelineActivitiesQueries = () => {
+ const timelineTargetableObject = useRecoilValue(
+ timelineTargetableObjectState,
+ );
+
+ // const { objectMetadataItem: objectMetadataItemActivity } =
+ // useObjectMetadataItemOnly({
+ // objectNameSingular: CoreObjectNameSingular.Activity,
+ // });
+
+ // const {
+ // upsertFindManyRecordsQueryInCache: overwriteFindManyActivitiesInCache,
+ // } = useUpsertFindManyRecordsQueryInCache({
+ // objectMetadataItem: objectMetadataItemActivity,
+ // });
+
+ // const { objectMetadataItem: objectMetadataItemActivityTarget } =
+ // useObjectMetadataItemOnly({
+ // objectNameSingular: CoreObjectNameSingular.ActivityTarget,
+ // });
+
+ // const {
+ // readFindManyRecordsQueryInCache: readFindManyActivityTargetsQueryInCache,
+ // } = useReadFindManyRecordsQueryInCache({
+ // objectMetadataItem: objectMetadataItemActivityTarget,
+ // });
+
+ // const {
+ // readFindManyRecordsQueryInCache: readFindManyActivitiesQueryInCache,
+ // } = useReadFindManyRecordsQueryInCache({
+ // objectMetadataItem: objectMetadataItemActivity,
+ // });
+
+ // const {
+ // upsertFindManyRecordsQueryInCache:
+ // overwriteFindManyActivityTargetsQueryInCache,
+ // } = useUpsertFindManyRecordsQueryInCache({
+ // objectMetadataItem: objectMetadataItemActivityTarget,
+ // });
+
+ const { removeFromActivitiesQueries } = useRemoveFromActivitiesQueries();
+
+ const removeFromTimelineActivitiesQueries = ({
+ activityIdToRemove,
+ activityTargetsToRemove,
+ }: {
+ activityIdToRemove: string;
+ activityTargetsToRemove: ActivityTarget[];
+ }) => {
+ if (!timelineTargetableObject) {
+ throw new Error('Timeline targetable object is not defined');
+ }
+
+ removeFromActivitiesQueries({
+ activityIdToRemove,
+ activityTargetsToRemove,
+ targetableObjects: [timelineTargetableObject],
+ activitiesFilters: {},
+ activitiesOrderByVariables: FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY,
+ });
+
+ // const targetObjectFieldName = getActivityTargetObjectFieldIdName({
+ // nameSingular: timelineTargetableObject.targetObjectNameSingular,
+ // });
+
+ // const activitiyTargetsForTargetableObjectQueryVariables = {
+ // filter: {
+ // [targetObjectFieldName]: {
+ // eq: timelineTargetableObject.id,
+ // },
+ // },
+ // };
+
+ // const existingActivityTargetsForTargetableObject =
+ // readFindManyActivityTargetsQueryInCache({
+ // queryVariables: activitiyTargetsForTargetableObjectQueryVariables,
+ // });
+
+ // const newActivityTargetsForTargetableObject = isNonEmptyArray(
+ // activityTargetsToRemove,
+ // )
+ // ? existingActivityTargetsForTargetableObject.filter(
+ // (existingActivityTarget) =>
+ // activityTargetsToRemove.some(
+ // (activityTargetToRemove) =>
+ // activityTargetToRemove.id !== existingActivityTarget.id,
+ // ),
+ // )
+ // : existingActivityTargetsForTargetableObject;
+
+ // overwriteFindManyActivityTargetsQueryInCache({
+ // objectRecordsToOverwrite: newActivityTargetsForTargetableObject,
+ // queryVariables: activitiyTargetsForTargetableObjectQueryVariables,
+ // });
+
+ // const existingActivityIds = existingActivityTargetsForTargetableObject
+ // ?.map((activityTarget) => activityTarget.activityId)
+ // .filter(isNonEmptyString);
+
+ // const timelineActivitiesQueryVariablesBeforeDrawerMount =
+ // makeTimelineActivitiesQueryVariables({
+ // activityIds: existingActivityIds,
+ // });
+
+ // const existingActivities = readFindManyActivitiesQueryInCache({
+ // queryVariables: timelineActivitiesQueryVariablesBeforeDrawerMount,
+ // });
+
+ // const activityIdsAfterRemoval = existingActivityIds.filter(
+ // (existingActivityId) => existingActivityId !== activityIdToRemove,
+ // );
+
+ // const timelineActivitiesQueryVariablesAfterRemoval =
+ // makeTimelineActivitiesQueryVariables({
+ // activityIds: activityIdsAfterRemoval,
+ // });
+
+ // const newActivities = existingActivities
+ // .filter((existingActivity) => existingActivity.id !== activityIdToRemove)
+ // .toSorted(sortObjectRecordByDateField('createdAt', 'DescNullsFirst'));
+
+ // overwriteFindManyActivitiesInCache({
+ // objectRecordsToOverwrite: newActivities,
+ // queryVariables: timelineActivitiesQueryVariablesAfterRemoval,
+ // });
+ };
+
+ return {
+ removeFromTimelineActivitiesQueries,
+ };
+};
diff --git a/packages/twenty-front/src/modules/activities/timeline/hooks/useTimelineActivities.ts b/packages/twenty-front/src/modules/activities/timeline/hooks/useTimelineActivities.ts
index ee0df89cb..348f693e0 100644
--- a/packages/twenty-front/src/modules/activities/timeline/hooks/useTimelineActivities.ts
+++ b/packages/twenty-front/src/modules/activities/timeline/hooks/useTimelineActivities.ts
@@ -1,18 +1,37 @@
import { useEffect, useState } from 'react';
import { isNonEmptyArray, isNonEmptyString } from '@sniptt/guards';
+import { useRecoilCallback, useRecoilState } from 'recoil';
import { useActivityTargetsForTargetableObject } from '@/activities/hooks/useActivityTargetsForTargetableObject';
+import { timelineTargetableObjectState } from '@/activities/timeline/states/timelineTargetableObjectState';
import { makeTimelineActivitiesQueryVariables } from '@/activities/timeline/utils/makeTimelineActivitiesQueryVariables';
import { Activity } from '@/activities/types/Activity';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
+import { useActivityConnectionUtils } from '@/activities/utils/useActivityConnectionUtils';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
+import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
+import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
+import { sortByAscString } from '~/utils/array/sortByAscString';
+import { isDefined } from '~/utils/isDefined';
export const useTimelineActivities = ({
targetableObject,
}: {
targetableObject: ActivityTargetableObject;
}) => {
+ const { makeActivityWithoutConnection } = useActivityConnectionUtils();
+
+ const [, setTimelineTargetableObject] = useRecoilState(
+ timelineTargetableObjectState,
+ );
+
+ useEffect(() => {
+ if (isDefined(targetableObject)) {
+ setTimelineTargetableObject(targetableObject);
+ }
+ }, [targetableObject, setTimelineTargetableObject]);
+
const {
activityTargets,
loadingActivityTargets,
@@ -23,9 +42,14 @@ export const useTimelineActivities = ({
const [initialized, setInitialized] = useState(false);
- const activityIds = activityTargets
- ?.map((activityTarget) => activityTarget.activityId)
- .filter(isNonEmptyString);
+ const activityIds = Array.from(
+ new Set(
+ activityTargets
+ ?.map((activityTarget) => activityTarget.activityId)
+ .filter(isNonEmptyString)
+ .toSorted(sortByAscString),
+ ),
+ );
const timelineActivitiesQueryVariables = makeTimelineActivitiesQueryVariables(
{
@@ -33,17 +57,30 @@ export const useTimelineActivities = ({
},
);
- const { records: activities, loading: loadingActivities } =
+ const { records: activitiesWithConnection, loading: loadingActivities } =
useFindManyRecords({
skip: loadingActivityTargets || !isNonEmptyArray(activityTargets),
objectNameSingular: CoreObjectNameSingular.Activity,
filter: timelineActivitiesQueryVariables.filter,
orderBy: timelineActivitiesQueryVariables.orderBy,
- onCompleted: () => {
- if (!initialized) {
- setInitialized(true);
- }
- },
+ onCompleted: useRecoilCallback(
+ ({ set }) =>
+ (data) => {
+ if (!initialized) {
+ setInitialized(true);
+ }
+
+ const activities = getRecordsFromRecordConnection({
+ recordConnection: data,
+ });
+
+ for (const activity of activities) {
+ set(recordStoreFamilyState(activity.id), activity);
+ }
+ },
+ [initialized],
+ ),
+ depth: 3,
});
const noActivityTargets =
@@ -57,6 +94,11 @@ export const useTimelineActivities = ({
const loading = loadingActivities || loadingActivityTargets;
+ const activities = activitiesWithConnection
+ ?.map(makeActivityWithoutConnection as any)
+ .map(({ activity }: any) => activity as any)
+ .filter(isDefined);
+
return {
activities,
loading,
diff --git a/packages/twenty-front/src/modules/activities/timeline/states/timelineTargetableObjectState.ts b/packages/twenty-front/src/modules/activities/timeline/states/timelineTargetableObjectState.ts
new file mode 100644
index 000000000..ad328f150
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/timeline/states/timelineTargetableObjectState.ts
@@ -0,0 +1,9 @@
+import { atom } from 'recoil';
+
+import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
+
+export const timelineTargetableObjectState =
+ atom({
+ key: 'timelineTargetableObjectState',
+ default: null,
+ });
diff --git a/packages/twenty-front/src/modules/activities/timeline/utils/makeTimelineActivitiesQueryVariables.ts b/packages/twenty-front/src/modules/activities/timeline/utils/makeTimelineActivitiesQueryVariables.ts
index ae39da0bd..86af23487 100644
--- a/packages/twenty-front/src/modules/activities/timeline/utils/makeTimelineActivitiesQueryVariables.ts
+++ b/packages/twenty-front/src/modules/activities/timeline/utils/makeTimelineActivitiesQueryVariables.ts
@@ -1,4 +1,5 @@
import { ObjectRecordQueryVariables } from '@/object-record/types/ObjectRecordQueryVariables';
+import { sortByAscString } from '~/utils/array/sortByAscString';
export const makeTimelineActivitiesQueryVariables = ({
activityIds,
@@ -8,7 +9,7 @@ export const makeTimelineActivitiesQueryVariables = ({
return {
filter: {
id: {
- in: activityIds,
+ in: activityIds.toSorted(sortByAscString),
},
},
orderBy: {
diff --git a/packages/twenty-front/src/modules/activities/types/ActivityTargetObject.ts b/packages/twenty-front/src/modules/activities/types/ActivityTargetObject.ts
index d95d4f520..ad119af39 100644
--- a/packages/twenty-front/src/modules/activities/types/ActivityTargetObject.ts
+++ b/packages/twenty-front/src/modules/activities/types/ActivityTargetObject.ts
@@ -1,9 +1,10 @@
+import { ActivityTarget } from '@/activities/types/ActivityTarget';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
-export type ActivityTargetObjectRecord = {
+export type ActivityTargetWithTargetRecord = {
targetObjectMetadataItem: ObjectMetadataItem;
- activityTargetRecord: ObjectRecord;
- targetObjectRecord: ObjectRecord;
+ activityTarget: ActivityTarget;
+ targetObject: ObjectRecord;
targetObjectNameSingular: string;
};
diff --git a/packages/twenty-front/src/modules/activities/utils/getActivityTargetsFilter.ts b/packages/twenty-front/src/modules/activities/utils/getActivityTargetsFilter.ts
new file mode 100644
index 000000000..c6a53c39e
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/utils/getActivityTargetsFilter.ts
@@ -0,0 +1,25 @@
+import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
+import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName';
+
+export const getActivityTargetsFilter = ({
+ targetableObjects,
+}: {
+ targetableObjects: ActivityTargetableObject[];
+}) => {
+ const findManyActivitiyTargetsQueryFilter = Object.fromEntries(
+ targetableObjects.map((targetableObject) => {
+ const targetObjectFieldName = getActivityTargetObjectFieldIdName({
+ nameSingular: targetableObject.targetObjectNameSingular,
+ });
+
+ return [
+ targetObjectFieldName,
+ {
+ eq: targetableObject.id,
+ },
+ ];
+ }),
+ );
+
+ return findManyActivitiyTargetsQueryFilter;
+};
diff --git a/packages/twenty-front/src/modules/activities/utils/useActivityConnectionUtils.ts b/packages/twenty-front/src/modules/activities/utils/useActivityConnectionUtils.ts
index b0c6bacec..55cd0b401 100644
--- a/packages/twenty-front/src/modules/activities/utils/useActivityConnectionUtils.ts
+++ b/packages/twenty-front/src/modules/activities/utils/useActivityConnectionUtils.ts
@@ -13,9 +13,14 @@ import { isDefined } from '~/utils/isDefined';
export const useActivityConnectionUtils = () => {
const mapConnectionToRecords = useMapConnectionToRecords();
- const makeActivityWithoutConnection = (activityWithConnections: any) => {
+ const makeActivityWithoutConnection = (
+ activityWithConnections: Activity & {
+ activityTargets: ObjectRecordConnection;
+ comments: ObjectRecordConnection;
+ },
+ ) => {
if (!isDefined(activityWithConnections)) {
- return { activity: null };
+ throw new Error('Activity with connections is not defined');
}
const hasActivityTargetsConnection = isObjectRecordConnection(
@@ -77,11 +82,13 @@ export const useActivityConnectionUtils = () => {
: [];
const activityTargets = {
+ __typename: 'ActivityTargetConnection',
edges: activityTargetEdges,
pageInfo: getEmptyPageInfo(),
} as ObjectRecordConnection;
const comments = {
+ __typename: 'CommentConnection',
edges: commentEdges,
pageInfo: getEmptyPageInfo(),
} as ObjectRecordConnection;
@@ -90,6 +97,9 @@ export const useActivityConnectionUtils = () => {
...activity,
activityTargets,
comments,
+ } as Activity & {
+ activityTargets: ObjectRecordConnection;
+ comments: ObjectRecordConnection;
};
return { activityWithConnection };
diff --git a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/isObjectRecordConnection.ts b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/isObjectRecordConnection.ts
index 7017d15c0..c069a0a6c 100644
--- a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/isObjectRecordConnection.ts
+++ b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/isObjectRecordConnection.ts
@@ -11,17 +11,19 @@ export const isObjectRecordConnection = (
objectNameSingular,
)}Connection`;
const objectEdgeTypeName = `${capitalize(objectNameSingular)}Edge`;
+
const objectConnectionSchema = z.object({
- __typename: z.literal(objectConnectionTypeName),
+ __typename: z.literal(objectConnectionTypeName).optional(),
edges: z.array(
z.object({
- __typename: z.literal(objectEdgeTypeName),
+ __typename: z.literal(objectEdgeTypeName).optional(),
node: z.object({
id: z.string().uuid(),
}),
}),
),
});
+
const connectionValidation = objectConnectionSchema.safeParse(value);
return connectionValidation.success;
diff --git a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerDetachRelationOptimisticEffect.ts b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerDetachRelationOptimisticEffect.ts
index 7a1196cf4..3d0080526 100644
--- a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerDetachRelationOptimisticEffect.ts
+++ b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerDetachRelationOptimisticEffect.ts
@@ -32,29 +32,25 @@ export const triggerDetachRelationOptimisticEffect = ({
targetRecordFieldValue,
{ isReference, readField },
) => {
- const isRelationTargetFieldAnObjectRecordConnection =
- isCachedObjectRecordConnection(
- sourceObjectNameSingular,
- targetRecordFieldValue,
- );
-
- if (isRelationTargetFieldAnObjectRecordConnection) {
- const relationTargetFieldEdgesWithoutRelationSourceRecordToDetach =
- targetRecordFieldValue.edges.filter(
- ({ node }) => readField('id', node) !== sourceRecordId,
- );
-
- return {
- ...targetRecordFieldValue,
- edges: relationTargetFieldEdgesWithoutRelationSourceRecordToDetach,
- };
- }
-
- const isRelationTargetFieldASingleObjectRecord = isReference(
+ const isRecordConnection = isCachedObjectRecordConnection(
+ sourceObjectNameSingular,
targetRecordFieldValue,
);
- if (isRelationTargetFieldASingleObjectRecord) {
+ if (isRecordConnection) {
+ const nextEdges = targetRecordFieldValue.edges.filter(
+ ({ node }) => readField('id', node) !== sourceRecordId,
+ );
+
+ return {
+ ...targetRecordFieldValue,
+ edges: nextEdges,
+ };
+ }
+
+ const isSingleReference = isReference(targetRecordFieldValue);
+
+ if (isSingleReference) {
return null;
}
diff --git a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect.ts b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect.ts
index 11b69677b..96c5fa0cc 100644
--- a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect.ts
+++ b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect.ts
@@ -45,41 +45,35 @@ export const triggerUpdateRecordOptimisticEffect = ({
rootQueryCachedResponse,
{ DELETE, readField, storeFieldName, toReference },
) => {
- const rootQueryCachedResponseIsNotACachedObjectRecordConnection =
- !isCachedObjectRecordConnection(
- objectMetadataItem.nameSingular,
- rootQueryCachedResponse,
- );
+ const shouldSkip = !isCachedObjectRecordConnection(
+ objectMetadataItem.nameSingular,
+ rootQueryCachedResponse,
+ );
- if (rootQueryCachedResponseIsNotACachedObjectRecordConnection) {
+ if (shouldSkip) {
return rootQueryCachedResponse;
}
- const rootQueryCachedObjectRecordConnection = rootQueryCachedResponse;
+ const rootQueryConnection = rootQueryCachedResponse;
const { fieldArguments: rootQueryVariables } =
parseApolloStoreFieldName(
storeFieldName,
);
- const rootQueryCurrentCachedRecordEdges =
- readField(
- 'edges',
- rootQueryCachedObjectRecordConnection,
- ) ?? [];
+ const rootQueryCurrentEdges =
+ readField('edges', rootQueryConnection) ??
+ [];
- let rootQueryNextCachedRecordEdges = [
- ...rootQueryCurrentCachedRecordEdges,
- ];
+ let rootQueryNextEdges = [...rootQueryCurrentEdges];
const rootQueryFilter = rootQueryVariables?.filter;
const rootQueryOrderBy = rootQueryVariables?.orderBy;
const rootQueryLimit = rootQueryVariables?.first;
- const shouldTestThatUpdatedRecordMatchesThisRootQueryFilter =
- isDefined(rootQueryFilter);
+ const shouldTryToMatchFilter = isDefined(rootQueryFilter);
- if (shouldTestThatUpdatedRecordMatchesThisRootQueryFilter) {
+ if (shouldTryToMatchFilter) {
const updatedRecordMatchesThisRootQueryFilter =
isRecordMatchingFilter({
record: updatedRecord,
@@ -88,24 +82,27 @@ export const triggerUpdateRecordOptimisticEffect = ({
});
const updatedRecordIndexInRootQueryEdges =
- rootQueryCurrentCachedRecordEdges.findIndex(
+ rootQueryCurrentEdges.findIndex(
(cachedEdge) =>
readField('id', cachedEdge.node) === updatedRecord.id,
);
+ const updatedRecordFoundInRootQueryEdges =
+ updatedRecordIndexInRootQueryEdges > -1;
+
const updatedRecordShouldBeAddedToRootQueryEdges =
updatedRecordMatchesThisRootQueryFilter &&
- updatedRecordIndexInRootQueryEdges === -1;
+ !updatedRecordFoundInRootQueryEdges;
const updatedRecordShouldBeRemovedFromRootQueryEdges =
- updatedRecordMatchesThisRootQueryFilter &&
- updatedRecordIndexInRootQueryEdges === -1;
+ !updatedRecordMatchesThisRootQueryFilter &&
+ updatedRecordFoundInRootQueryEdges;
if (updatedRecordShouldBeAddedToRootQueryEdges) {
const updatedRecordNodeReference = toReference(updatedRecord);
if (isDefined(updatedRecordNodeReference)) {
- rootQueryNextCachedRecordEdges.push({
+ rootQueryNextEdges.push({
__typename: objectEdgeTypeName,
node: updatedRecordNodeReference,
cursor: '',
@@ -114,18 +111,15 @@ export const triggerUpdateRecordOptimisticEffect = ({
}
if (updatedRecordShouldBeRemovedFromRootQueryEdges) {
- rootQueryNextCachedRecordEdges.splice(
- updatedRecordIndexInRootQueryEdges,
- 1,
- );
+ rootQueryNextEdges.splice(updatedRecordIndexInRootQueryEdges, 1);
}
}
- const nextRootQueryEdgesShouldBeSorted = isDefined(rootQueryOrderBy);
+ const rootQueryNextEdgesShouldBeSorted = isDefined(rootQueryOrderBy);
- if (nextRootQueryEdgesShouldBeSorted) {
- rootQueryNextCachedRecordEdges = sortCachedObjectEdges({
- edges: rootQueryNextCachedRecordEdges,
+ if (rootQueryNextEdgesShouldBeSorted) {
+ rootQueryNextEdges = sortCachedObjectEdges({
+ edges: rootQueryNextEdges,
orderBy: rootQueryOrderBy,
readCacheField: readField,
});
@@ -158,12 +152,12 @@ export const triggerUpdateRecordOptimisticEffect = ({
// the query's result.
// In this case, invalidate the cache entry so it can be re-fetched.
const rootQueryCurrentCachedRecordEdgesLengthIsAtLimit =
- rootQueryCurrentCachedRecordEdges.length === rootQueryLimit;
+ rootQueryCurrentEdges.length === rootQueryLimit;
// If next edges length is under limit, then we can wait for the network response and merge the result
// then in the merge function we could implement this mechanism to limit the number of edges in the cache
const rootQueryNextCachedRecordEdgesLengthIsUnderLimit =
- rootQueryNextCachedRecordEdges.length < rootQueryLimit;
+ rootQueryNextEdges.length < rootQueryLimit;
const shouldDeleteRootQuerySoItCanBeRefetched =
rootQueryCurrentCachedRecordEdgesLengthIsAtLimit &&
@@ -174,16 +168,16 @@ export const triggerUpdateRecordOptimisticEffect = ({
}
const rootQueryNextCachedRecordEdgesLengthIsAboveRootQueryLimit =
- rootQueryNextCachedRecordEdges.length > rootQueryLimit;
+ rootQueryNextEdges.length > rootQueryLimit;
if (rootQueryNextCachedRecordEdgesLengthIsAboveRootQueryLimit) {
- rootQueryNextCachedRecordEdges.splice(rootQueryLimit);
+ rootQueryNextEdges.splice(rootQueryLimit);
}
}
return {
- ...rootQueryCachedObjectRecordConnection,
- edges: rootQueryNextCachedRecordEdges,
+ ...rootQueryConnection,
+ edges: rootQueryNextEdges,
};
},
},
diff --git a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerUpdateRelationsOptimisticEffect.ts b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerUpdateRelationsOptimisticEffect.ts
index 6610ba302..633fde736 100644
--- a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerUpdateRelationsOptimisticEffect.ts
+++ b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerUpdateRelationsOptimisticEffect.ts
@@ -6,7 +6,7 @@ import { triggerAttachRelationOptimisticEffect } from '@/apollo/optimistic-effec
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect';
import { triggerDetachRelationOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDetachRelationOptimisticEffect';
import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord';
-import { CORE_OBJECT_NAMES_TO_DELETE_ON_TRIGGER_RELATION_DETACH as CORE_OBJECT_NAMES_TO_DELETE_ON_OPTIMISTIC_RELATION_DETACH } from '@/apollo/types/coreObjectNamesToDeleteOnRelationDetach';
+import { CORE_OBJECT_NAMES_TO_DELETE_ON_TRIGGER_RELATION_DETACH } from '@/apollo/types/coreObjectNamesToDeleteOnRelationDetach';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
@@ -74,6 +74,8 @@ export const triggerUpdateRelationsOptimisticEffect = ({
return;
}
+ // TODO: replace this by a relation type check, if it's one to many,
+ // it's an object record connection (we can still check it though as a safeguard)
const currentFieldValueOnSourceRecordIsARecordConnection =
isObjectRecordConnection(
targetObjectMetadataItem.nameSingular,
@@ -104,12 +106,14 @@ export const triggerUpdateRelationsOptimisticEffect = ({
isDefined(currentSourceRecord) && targetRecordsToDetachFrom.length > 0;
if (shouldDetachSourceFromAllTargets) {
- const shouldStartByDeletingRelationTargetRecordsFromCache =
- CORE_OBJECT_NAMES_TO_DELETE_ON_OPTIMISTIC_RELATION_DETACH.includes(
+ // TODO: see if we can de-hardcode this, put cascade delete in relation metadata item
+ // Instead of hardcoding it here
+ const shouldCascadeDeleteTargetRecords =
+ CORE_OBJECT_NAMES_TO_DELETE_ON_TRIGGER_RELATION_DETACH.includes(
targetObjectMetadataItem.nameSingular as CoreObjectNameSingular,
);
- if (shouldStartByDeletingRelationTargetRecordsFromCache) {
+ if (shouldCascadeDeleteTargetRecords) {
triggerDeleteRecordsOptimisticEffect({
cache,
objectMetadataItem: targetObjectMetadataItem,
diff --git a/packages/twenty-front/src/modules/apollo/types/coreObjectNamesToDeleteOnRelationDetach.ts b/packages/twenty-front/src/modules/apollo/types/coreObjectNamesToDeleteOnRelationDetach.ts
index f28caf779..90e91f138 100644
--- a/packages/twenty-front/src/modules/apollo/types/coreObjectNamesToDeleteOnRelationDetach.ts
+++ b/packages/twenty-front/src/modules/apollo/types/coreObjectNamesToDeleteOnRelationDetach.ts
@@ -2,4 +2,6 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
export const CORE_OBJECT_NAMES_TO_DELETE_ON_TRIGGER_RELATION_DETACH = [
CoreObjectNameSingular.Favorite,
+ CoreObjectNameSingular.ActivityTarget,
+ CoreObjectNameSingular.Comment,
];
diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx
index d351e0a03..95a7e2e25 100644
--- a/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx
+++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx
@@ -192,11 +192,11 @@ export const CommandMenu = () => {
const activityCommands = useMemo(
() =>
- activities.map(({ id, title }) => ({
- id,
- label: title ?? '',
+ activities.map((activity) => ({
+ id: activity.id,
+ label: activity.title ?? '',
to: '',
- onCommandClick: () => openActivityRightDrawer(id),
+ onCommandClick: () => openActivityRightDrawer(activity),
})),
[activities, openActivityRightDrawer],
);
@@ -372,7 +372,7 @@ export const CommandMenu = () => {
Icon={IconNotes}
key={activity.id}
label={activity.title ?? ''}
- onClick={() => openActivityRightDrawer(activity.id)}
+ onClick={() => openActivityRightDrawer(activity)}
/>
))}
diff --git a/packages/twenty-front/src/modules/object-record/cache/hooks/useGenerateObjectRecordOptimisticResponse.ts b/packages/twenty-front/src/modules/object-record/cache/hooks/useGenerateObjectRecordOptimisticResponse.ts
index f07a757a2..348c0cc54 100644
--- a/packages/twenty-front/src/modules/object-record/cache/hooks/useGenerateObjectRecordOptimisticResponse.ts
+++ b/packages/twenty-front/src/modules/object-record/cache/hooks/useGenerateObjectRecordOptimisticResponse.ts
@@ -43,12 +43,19 @@ export const useGenerateObjectRecordOptimisticResponse = ({
);
const relationRecordId = result[relationIdFieldName] as string | null;
+ const relationRecord = input[fieldMetadataItem.name] as
+ | ObjectRecord
+ | undefined;
+
return {
...result,
[fieldMetadataItem.name]: relationRecordId
? {
__typename: relationRecordTypeName,
id: relationRecordId,
+ // TODO: there are too many bugs if we don't include the entire relation record
+ // See if we can find a way to work only with the id and typename
+ ...relationRecord,
}
: null,
};
diff --git a/packages/twenty-front/src/modules/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache.ts b/packages/twenty-front/src/modules/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache.ts
index d519428f4..210ecee55 100644
--- a/packages/twenty-front/src/modules/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache.ts
+++ b/packages/twenty-front/src/modules/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache.ts
@@ -1,6 +1,5 @@
import { useApolloClient } from '@apollo/client';
-import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { MAX_QUERY_DEPTH_FOR_CACHE_INJECTION } from '@/object-record/cache/constants/MaxQueryDepthForCacheInjection';
import { getRecordConnectionFromRecords } from '@/object-record/cache/utils/getRecordConnectionFromRecords';
@@ -28,11 +27,11 @@ export const useUpsertFindManyRecordsQueryInCache = ({
}) => {
const findManyRecordsQueryForCacheOverwrite = generateFindManyRecordsQuery({
objectMetadataItem,
- depth: MAX_QUERY_DEPTH_FOR_CACHE_INJECTION,
+ depth: MAX_QUERY_DEPTH_FOR_CACHE_INJECTION, // TODO: fix this
});
const newObjectRecordConnection = getRecordConnectionFromRecords({
- objectNameSingular: CoreObjectNameSingular.ActivityTarget,
+ objectNameSingular: objectMetadataItem.nameSingular,
records: objectRecordsToOverwrite,
});
diff --git a/packages/twenty-front/src/modules/object-record/cache/utils/getRecordConnectionFromRecords.ts b/packages/twenty-front/src/modules/object-record/cache/utils/getRecordConnectionFromRecords.ts
index 18fa0c6de..affe038e9 100644
--- a/packages/twenty-front/src/modules/object-record/cache/utils/getRecordConnectionFromRecords.ts
+++ b/packages/twenty-front/src/modules/object-record/cache/utils/getRecordConnectionFromRecords.ts
@@ -20,5 +20,6 @@ export const getRecordConnectionFromRecords = ({
});
}),
pageInfo: getEmptyPageInfo(),
+ totalCount: records.length,
} as ObjectRecordConnection;
};
diff --git a/packages/twenty-front/src/modules/object-record/hooks/useDeleteManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useDeleteManyRecords.ts
index b055e0dca..b3236cc5d 100644
--- a/packages/twenty-front/src/modules/object-record/hooks/useDeleteManyRecords.ts
+++ b/packages/twenty-front/src/modules/object-record/hooks/useDeleteManyRecords.ts
@@ -12,6 +12,10 @@ type useDeleteOneRecordProps = {
refetchFindManyQuery?: boolean;
};
+type DeleteManyRecordsOptions = {
+ skipOptimisticEffect?: boolean;
+};
+
export const useDeleteManyRecords = ({
objectNameSingular,
}: useDeleteOneRecordProps) => {
@@ -26,34 +30,41 @@ export const useDeleteManyRecords = ({
objectMetadataItem.namePlural,
);
- const deleteManyRecords = async (idsToDelete: string[]) => {
+ const deleteManyRecords = async (
+ idsToDelete: string[],
+ options?: DeleteManyRecordsOptions,
+ ) => {
const deletedRecords = await apolloClient.mutate({
mutation: deleteManyRecordsMutation,
variables: {
filter: { id: { in: idsToDelete } },
},
- optimisticResponse: {
- [mutationResponseField]: idsToDelete.map((idToDelete) => ({
- __typename: capitalize(objectNameSingular),
- id: idToDelete,
- })),
- },
- update: (cache, { data }) => {
- const records = data?.[mutationResponseField];
+ optimisticResponse: options?.skipOptimisticEffect
+ ? undefined
+ : {
+ [mutationResponseField]: idsToDelete.map((idToDelete) => ({
+ __typename: capitalize(objectNameSingular),
+ id: idToDelete,
+ })),
+ },
+ update: options?.skipOptimisticEffect
+ ? undefined
+ : (cache, { data }) => {
+ const records = data?.[mutationResponseField];
- if (!records?.length) return;
+ if (!records?.length) return;
- const cachedRecords = records
- .map((record) => getRecordFromCache(record.id, cache))
- .filter(isDefined);
+ const cachedRecords = records
+ .map((record) => getRecordFromCache(record.id, cache))
+ .filter(isDefined);
- triggerDeleteRecordsOptimisticEffect({
- cache,
- objectMetadataItem,
- recordsToDelete: cachedRecords,
- objectMetadataItems,
- });
- },
+ triggerDeleteRecordsOptimisticEffect({
+ cache,
+ objectMetadataItem,
+ recordsToDelete: cachedRecords,
+ objectMetadataItems,
+ });
+ },
});
return deletedRecords.data?.[mutationResponseField] ?? null;
diff --git a/packages/twenty-front/src/modules/object-record/hooks/useFindOneRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/useFindOneRecord.ts
index 7b8e81d11..ade2c951e 100644
--- a/packages/twenty-front/src/modules/object-record/hooks/useFindOneRecord.ts
+++ b/packages/twenty-front/src/modules/object-record/hooks/useFindOneRecord.ts
@@ -4,6 +4,7 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
+// TODO: fix connection in relation => automatically change to an array
export const useFindOneRecord = ({
objectNameSingular,
objectRecordId = '',
diff --git a/packages/twenty-front/src/modules/object-record/record-filter/types/ObjectRecordQueryFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/types/ObjectRecordQueryFilter.ts
index 19c40d85b..e86ac4da6 100644
--- a/packages/twenty-front/src/modules/object-record/record-filter/types/ObjectRecordQueryFilter.ts
+++ b/packages/twenty-front/src/modules/object-record/record-filter/types/ObjectRecordQueryFilter.ts
@@ -79,7 +79,8 @@ export type LeafFilter =
| CurrencyFilter
| URLFilter
| FullNameFilter
- | BooleanFilter;
+ | BooleanFilter
+ | undefined;
export type AndObjectRecordFilter = {
and?: ObjectRecordQueryFilter[];
diff --git a/packages/twenty-front/src/modules/object-record/utils/sortByObjectRecordId.ts b/packages/twenty-front/src/modules/object-record/utils/sortByObjectRecordId.ts
new file mode 100644
index 000000000..0594de216
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/utils/sortByObjectRecordId.ts
@@ -0,0 +1,5 @@
+import { ObjectRecord } from '@/object-record/types/ObjectRecord';
+
+export const sortByObjectRecordId = (a: ObjectRecord, b: ObjectRecord) => {
+ return a.id.localeCompare(b.id);
+};
diff --git a/packages/twenty-front/src/modules/object-record/utils/sortObjectRecordByDateField.test.ts b/packages/twenty-front/src/modules/object-record/utils/sortObjectRecordByDateField.test.ts
new file mode 100644
index 000000000..46a03aae8
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/utils/sortObjectRecordByDateField.test.ts
@@ -0,0 +1,77 @@
+import { OrderBy } from '@/object-metadata/types/OrderBy';
+
+import { sortObjectRecordByDateField } from './sortObjectRecordByDateField';
+
+describe('sortByObjectRecordByCreatedAt', () => {
+ const recordOldest = { id: '', createdAt: '2022-01-01T00:00:00.000Z' };
+ const recordNewest = { id: '', createdAt: '2022-01-02T00:00:00.000Z' };
+ const recordNull1 = { id: '', createdAt: null };
+ const recordNull2 = { id: '', createdAt: null };
+
+ it('should sort in ascending order with null values first', () => {
+ const sortDirection = 'AscNullsFirst' satisfies OrderBy;
+ const sortedArray = [
+ recordNull2,
+ recordNewest,
+ recordNull1,
+ recordOldest,
+ ].sort(sortObjectRecordByDateField('createdAt', sortDirection));
+
+ expect(sortedArray).toEqual([
+ recordNull1,
+ recordNull2,
+ recordOldest,
+ recordNewest,
+ ]);
+ });
+
+ it('should sort in descending order with null values first', () => {
+ const sortDirection = 'DescNullsFirst' satisfies OrderBy;
+ const sortedArray = [
+ recordNull2,
+ recordOldest,
+ recordNewest,
+ recordNull1,
+ ].sort(sortObjectRecordByDateField('createdAt', sortDirection));
+
+ expect(sortedArray).toEqual([
+ recordNull2,
+ recordNull1,
+ recordNewest,
+ recordOldest,
+ ]);
+ });
+ it('should sort in ascending order with null values last', () => {
+ const sortDirection = 'AscNullsLast' satisfies OrderBy;
+ const sortedArray = [
+ recordOldest,
+ recordNull2,
+ recordNewest,
+ recordNull1,
+ ].sort(sortObjectRecordByDateField('createdAt', sortDirection));
+
+ expect(sortedArray).toEqual([
+ recordOldest,
+ recordNewest,
+ recordNull1,
+ recordNull2,
+ ]);
+ });
+
+ it('should sort in descending order with null values last', () => {
+ const sortDirection = 'DescNullsLast' satisfies OrderBy;
+ const sortedArray = [
+ recordNull1,
+ recordOldest,
+ recordNewest,
+ recordNull2,
+ ].sort(sortObjectRecordByDateField('createdAt', sortDirection));
+
+ expect(sortedArray).toEqual([
+ recordNewest,
+ recordOldest,
+ recordNull1,
+ recordNull2,
+ ]);
+ });
+});
diff --git a/packages/twenty-front/src/modules/object-record/utils/sortObjectRecordByDateField.ts b/packages/twenty-front/src/modules/object-record/utils/sortObjectRecordByDateField.ts
new file mode 100644
index 000000000..10b0b3269
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/utils/sortObjectRecordByDateField.ts
@@ -0,0 +1,68 @@
+import { DateTime } from 'luxon';
+
+import { OrderBy } from '@/object-metadata/types/OrderBy';
+import { ObjectRecord } from '@/object-record/types/ObjectRecord';
+import { isDefined } from '~/utils/isDefined';
+
+const SORT_BEFORE = -1;
+const SORT_AFTER = 1;
+const SORT_EQUAL = 0;
+
+export const sortObjectRecordByDateField =
+ (dateField: keyof T, sortDirection: OrderBy) =>
+ (a: T, b: T) => {
+ const aDate = a[dateField];
+ const bDate = b[dateField];
+
+ if (!isDefined(aDate) && !isDefined(bDate)) {
+ return SORT_EQUAL;
+ }
+
+ if (!isDefined(aDate)) {
+ if (sortDirection === 'AscNullsFirst') {
+ return SORT_BEFORE;
+ } else if (sortDirection === 'DescNullsFirst') {
+ return SORT_BEFORE;
+ } else if (sortDirection === 'AscNullsLast') {
+ return SORT_AFTER;
+ } else if (sortDirection === 'DescNullsLast') {
+ return SORT_AFTER;
+ }
+
+ throw new Error(`Invalid sortDirection: ${sortDirection}`);
+ }
+
+ if (!isDefined(bDate)) {
+ if (sortDirection === 'AscNullsFirst') {
+ return SORT_AFTER;
+ } else if (sortDirection === 'DescNullsFirst') {
+ return SORT_AFTER;
+ } else if (sortDirection === 'AscNullsLast') {
+ return SORT_BEFORE;
+ } else if (sortDirection === 'DescNullsLast') {
+ return SORT_BEFORE;
+ }
+
+ throw new Error(`Invalid sortDirection: ${sortDirection}`);
+ }
+
+ const differenceInMs = DateTime.fromISO(aDate)
+ .diff(DateTime.fromISO(bDate))
+ .as('milliseconds');
+
+ if (differenceInMs === 0) {
+ return SORT_EQUAL;
+ } else if (
+ sortDirection === 'AscNullsFirst' ||
+ sortDirection === 'AscNullsLast'
+ ) {
+ return differenceInMs > 0 ? SORT_AFTER : SORT_BEFORE;
+ } else if (
+ sortDirection === 'DescNullsFirst' ||
+ sortDirection === 'DescNullsLast'
+ ) {
+ return differenceInMs > 0 ? SORT_BEFORE : SORT_AFTER;
+ }
+
+ throw new Error(`Invalid sortDirection: ${sortDirection}`);
+ };
diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/hooks/useRightDrawer.ts b/packages/twenty-front/src/modules/ui/layout/right-drawer/hooks/useRightDrawer.ts
index cb5110960..08cb391cc 100644
--- a/packages/twenty-front/src/modules/ui/layout/right-drawer/hooks/useRightDrawer.ts
+++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/hooks/useRightDrawer.ts
@@ -6,12 +6,15 @@ import { rightDrawerPageState } from '../states/rightDrawerPageState';
import { RightDrawerPages } from '../types/RightDrawerPages';
export const useRightDrawer = () => {
- const [, setIsRightDrawerOpen] = useRecoilState(isRightDrawerOpenState);
+ const [isRightDrawerOpen, setIsRightDrawerOpen] = useRecoilState(
+ isRightDrawerOpenState,
+ );
const [, setIsRightDrawerExpanded] = useRecoilState(
isRightDrawerExpandedState,
);
- const [, setRightDrawerPage] = useRecoilState(rightDrawerPageState);
+ const [rightDrawerPage, setRightDrawerPage] =
+ useRecoilState(rightDrawerPageState);
const openRightDrawer = (rightDrawerPage: RightDrawerPages) => {
setRightDrawerPage(rightDrawerPage);
@@ -25,6 +28,8 @@ export const useRightDrawer = () => {
};
return {
+ rightDrawerPage,
+ isRightDrawerOpen,
openRightDrawer,
closeRightDrawer,
};
diff --git a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageAddButton.tsx b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageAddButton.tsx
index 08812ccbb..1b3f27383 100644
--- a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageAddButton.tsx
+++ b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageAddButton.tsx
@@ -1,6 +1,6 @@
import styled from '@emotion/styled';
-import { useOpenCreateActivityDrawerV2 } from '@/activities/hooks/useOpenCreateActivityDrawerV2';
+import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
import { ActivityType } from '@/activities/types/Activity';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
@@ -24,14 +24,14 @@ export const ShowPageAddButton = ({
activityTargetObject: ActivityTargetableObject;
}) => {
const { closeDropdown, toggleDropdown } = useDropdown('add-show-page');
- const openCreateActivity = useOpenCreateActivityDrawerV2();
+ const openCreateActivity = useOpenCreateActivityDrawer();
const handleSelect = (type: ActivityType) => {
openCreateActivity({
type,
targetableObjects: [activityTargetObject],
- timelineTargetableObject: activityTargetObject,
});
+
closeDropdown();
};
diff --git a/packages/twenty-front/src/pages/tasks/Tasks.tsx b/packages/twenty-front/src/pages/tasks/Tasks.tsx
index 61a550ad6..33b2a4ff1 100644
--- a/packages/twenty-front/src/pages/tasks/Tasks.tsx
+++ b/packages/twenty-front/src/pages/tasks/Tasks.tsx
@@ -52,7 +52,7 @@ export const Tasks = () => {
-
+
diff --git a/packages/twenty-front/src/utils/array/sortByAscString.ts b/packages/twenty-front/src/utils/array/sortByAscString.ts
new file mode 100644
index 000000000..3c4b1dca1
--- /dev/null
+++ b/packages/twenty-front/src/utils/array/sortByAscString.ts
@@ -0,0 +1,3 @@
+export const sortByAscString = (a: string, b: string) => {
+ return a.localeCompare(b);
+};