Skip to content

Commit

Permalink
Add relations to notes/tasks list view (#6971)
Browse files Browse the repository at this point in the history
<img width="664" alt="Screenshot 2024-09-10 at 17 00 11"
src="https://github.com/user-attachments/assets/37132805-ff67-4d28-b664-b03da680e166">

---------

Co-authored-by: Lucas Bordeau <[email protected]>
  • Loading branch information
FelixMalfait and lucasbordeau authored Sep 12, 2024
1 parent 725ee83 commit f8e5b33
Show file tree
Hide file tree
Showing 12 changed files with 135 additions and 80 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { RecoilRoot, useSetRecoilState } from 'recoil';
import { useActivityTargetObjectRecords } from '@/activities/hooks/useActivityTargetObjectRecords';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
import { JestObjectMetadataItemSetter } from '~/testing/jest/JestObjectMetadataItemSetter';
Expand Down Expand Up @@ -128,10 +127,8 @@ describe('useActivityTargetObjectRecords', () => {
objectMetadataItemsState,
);

const { activityTargetObjectRecords } = useActivityTargetObjectRecords(
task,
CoreObjectNameSingular.Task,
);
const { activityTargetObjectRecords } =
useActivityTargetObjectRecords(task);

return {
activityTargetObjectRecords,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { useApolloClient } from '@apollo/client';
import { useRecoilValue } from 'recoil';
import { Nullable } from 'twenty-ui';

Expand All @@ -7,46 +6,37 @@ import { Note } from '@/activities/types/Note';
import { NoteTarget } from '@/activities/types/NoteTarget';
import { Task } from '@/activities/types/Task';
import { TaskTarget } from '@/activities/types/TaskTarget';
import { getJoinObjectNameSingular } from '@/activities/utils/getJoinObjectNameSingular';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
import { isDefined } from '~/utils/isDefined';

export const useActivityTargetObjectRecords = (
activity: Task | Note,
objectNameSingular: CoreObjectNameSingular,
activity?: Task | Note,
activityTargets?: NoteTarget[] | TaskTarget[],
) => {
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);

const activityTargets =
'noteTargets' in activity && activity.noteTargets
if (!isDefined(activity) && !isDefined(activityTargets)) {
return { activityTargetObjectRecords: [] };
}

const targets = activityTargets
? activityTargets
: activity && 'noteTargets' in activity && activity.noteTargets
? activity.noteTargets
: 'taskTargets' in activity && activity.taskTargets
: activity && 'taskTargets' in activity && activity.taskTargets
? activity.taskTargets
: [];

const getRecordFromCache = useGetRecordFromCache({
objectNameSingular: getJoinObjectNameSingular(objectNameSingular),
});

const apolloClient = useApolloClient();

const activityTargetObjectRecords = activityTargets
const activityTargetObjectRecords = targets
.map<Nullable<ActivityTargetWithTargetRecord>>((activityTarget) => {
const activityTargetFromCache = getRecordFromCache<
NoteTarget | TaskTarget
>(activityTarget.id, apolloClient.cache);

if (!isDefined(activityTargetFromCache)) {
throw new Error(
`Cannot find activity target ${activityTarget.id} in cache, this shouldn't happen.`,
);
if (!isDefined(activityTarget)) {
throw new Error(`Cannot find activity target`);
}

const correspondingObjectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
isDefined(activityTargetFromCache[objectMetadataItem.nameSingular]) &&
isDefined(activityTarget[objectMetadataItem.nameSingular]) &&
![CoreObjectNameSingular.Note, CoreObjectNameSingular.Task].includes(
objectMetadataItem.nameSingular as CoreObjectNameSingular,
),
Expand All @@ -57,7 +47,7 @@ export const useActivityTargetObjectRecords = (
}

const targetObjectRecord =
activityTargetFromCache[correspondingObjectMetadataItem.nameSingular];
activityTarget[correspondingObjectMetadataItem.nameSingular];

if (!targetObjectRecord) {
throw new Error(
Expand All @@ -66,7 +56,7 @@ export const useActivityTargetObjectRecords = (
}

return {
activityTarget: activityTargetFromCache ?? activityTarget,
activityTarget,
targetObject: targetObjectRecord ?? undefined,
targetObjectMetadataItem: correspondingObjectMetadataItem,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,8 @@ export const ActivityTargetsInlineCell = ({
readonly,
activityObjectNameSingular,
}: ActivityTargetsInlineCellProps) => {
const { activityTargetObjectRecords } = useActivityTargetObjectRecords(
activity,
activityObjectNameSingular,
);
const { activityTargetObjectRecords } =
useActivityTargetObjectRecords(activity);

const { closeInlineCell } = useInlineCell();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,6 @@ export const getLabelIdentifierFieldValue = (
return `${record.name?.firstName ?? ''} ${record.name?.lastName ?? ''}`;
}

if (objectNameSingular === CoreObjectNameSingular.NoteTarget) {
return record.note?.title ?? '';
}

if (objectNameSingular === CoreObjectNameSingular.TaskTarget) {
return record.task?.title ?? '';
}

if (isDefined(labelIdentifierFieldMetadataItem?.name)) {
return String(record[labelIdentifierFieldMetadataItem.name]);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { useActivityTargetObjectRecords } from '@/activities/hooks/useActivityTargetObjectRecords';
import { NoteTarget } from '@/activities/types/NoteTarget';
import { TaskTarget } from '@/activities/types/TaskTarget';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { RecordChip } from '@/object-record/components/RecordChip';
import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus';
import { useRelationFromManyFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useRelationFromManyFieldDisplay';
Expand All @@ -7,24 +11,74 @@ export const RelationFromManyFieldDisplay = () => {
const { fieldValue, fieldDefinition } = useRelationFromManyFieldDisplay();
const { isFocused } = useFieldFocus();

const { fieldName, objectMetadataNameSingular } = fieldDefinition.metadata;

const relationObjectNameSingular =
fieldDefinition?.metadata.relationObjectMetadataNameSingular;

const { activityTargetObjectRecords } = useActivityTargetObjectRecords(
undefined,
fieldValue as NoteTarget[] | TaskTarget[],
);

if (!fieldValue || !relationObjectNameSingular) {
return null;
}

return (
<ExpandableList isChipCountDisplayed={isFocused}>
{fieldValue.map((record) => {
return (
const isRelationFromActivityTargets =
(fieldName === 'noteTargets' &&
objectMetadataNameSingular === CoreObjectNameSingular.Note) ||
(fieldName === 'taskTargets' &&
objectMetadataNameSingular === CoreObjectNameSingular.Task);

const isRelationFromManyActivities =
(fieldName === 'noteTargets' &&
objectMetadataNameSingular !== CoreObjectNameSingular.Note) ||
(fieldName === 'taskTargets' &&
objectMetadataNameSingular !== CoreObjectNameSingular.Task);

if (isRelationFromManyActivities) {
const objectNameSingular =
fieldName === 'noteTargets'
? CoreObjectNameSingular.Note
: CoreObjectNameSingular.Task;

const relationFieldName = fieldName === 'noteTargets' ? 'note' : 'task';

return (
<ExpandableList isChipCountDisplayed={isFocused}>
{fieldValue.map((record) => (
<RecordChip
key={record.id}
objectNameSingular={objectNameSingular}
record={record[relationFieldName]}
/>
))}
</ExpandableList>
);
} else if (isRelationFromActivityTargets) {
return (
<ExpandableList isChipCountDisplayed={isFocused}>
{activityTargetObjectRecords.map((record) => (
<RecordChip
key={record.targetObject.id}
objectNameSingular={record.targetObjectMetadataItem.nameSingular}
record={record.targetObject}
/>
))}
</ExpandableList>
);
} else {
return (
<ExpandableList isChipCountDisplayed={isFocused}>
{fieldValue.map((record) => (
<RecordChip
key={record.id}
objectNameSingular={relationObjectNameSingular}
record={record}
/>
);
})}
</ExpandableList>
);
))}
</ExpandableList>
);
}
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useEffect } from 'react';
import { Meta, StoryObj } from '@storybook/react';
import { useEffect } from 'react';
import { useSetRecoilState } from 'recoil';
import { ComponentDecorator } from 'twenty-ui';

Expand Down Expand Up @@ -94,9 +94,10 @@ type Story = StoryObj<typeof RelationFromManyFieldDisplay>;

export const Default: Story = {};

// TODO: optimize this component once we have morph many
export const Performance = getProfilingStory({
componentName: 'RelationFromManyFieldDisplay',
averageThresholdInMs: 0.5,
averageThresholdInMs: 1,
numberOfRuns: 20,
numberOfTestsPerRun: 100,
});
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { useRecoilValue } from 'recoil';

import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { getObjectMetadataIdentifierFields } from '@/object-metadata/utils/getObjectMetadataIdentifierFields';
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
import { isDefined } from '~/utils/isDefined';

Expand All @@ -27,25 +30,29 @@ export const useRecordTableRecordGqlFields = ({
identifierQueryFields[imageIdentifierFieldMetadataItem.name] = true;
}

const { objectMetadataItem: noteTargetObjectMetadataItem } =
useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.NoteTarget,
});

const { objectMetadataItem: taskTargetObjectMetadataItem } =
useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.TaskTarget,
});

const recordGqlFields: Record<string, any> = {
id: true,
...Object.fromEntries(
visibleTableColumns.map((column) => [column.metadata.fieldName, true]),
),
...identifierQueryFields,
position: true,
noteTargets: {
note: {
id: true,
title: true,
},
},
taskTargets: {
task: {
id: true,
title: true,
},
},
noteTargets: generateDepthOneRecordGqlFields({
objectMetadataItem: noteTargetObjectMetadataItem,
}),
taskTargets: generateDepthOneRecordGqlFields({
objectMetadataItem: taskTargetObjectMetadataItem,
}),
};

return recordGqlFields;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const notesAllView = async (
{
fieldMetadataId:
objectMetadataMap[STANDARD_OBJECT_IDS.note].fields[
NOTE_STANDARD_FIELD_IDS.body
NOTE_STANDARD_FIELD_IDS.noteTargets
],
position: 1,
isVisible: true,
Expand All @@ -39,7 +39,7 @@ export const notesAllView = async (
{
fieldMetadataId:
objectMetadataMap[STANDARD_OBJECT_IDS.note].fields[
NOTE_STANDARD_FIELD_IDS.createdBy
NOTE_STANDARD_FIELD_IDS.body
],
position: 2,
isVisible: true,
Expand All @@ -48,12 +48,21 @@ export const notesAllView = async (
{
fieldMetadataId:
objectMetadataMap[STANDARD_OBJECT_IDS.note].fields[
BASE_OBJECT_STANDARD_FIELD_IDS.createdAt
NOTE_STANDARD_FIELD_IDS.createdBy
],
position: 3,
isVisible: true,
size: 150,
},
{
fieldMetadataId:
objectMetadataMap[STANDARD_OBJECT_IDS.note].fields[
BASE_OBJECT_STANDARD_FIELD_IDS.createdAt
],
position: 4,
isVisible: true,
size: 150,
},
/*
TODO: Add later, since we don't have real-time it probably doesn't work well?
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export const tasksAllView = async (
{
fieldMetadataId:
objectMetadataMap[STANDARD_OBJECT_IDS.task].fields[
TASK_STANDARD_FIELD_IDS.createdBy
TASK_STANDARD_FIELD_IDS.taskTargets
],
position: 3,
isVisible: true,
Expand All @@ -58,7 +58,7 @@ export const tasksAllView = async (
{
fieldMetadataId:
objectMetadataMap[STANDARD_OBJECT_IDS.task].fields[
TASK_STANDARD_FIELD_IDS.dueAt
TASK_STANDARD_FIELD_IDS.createdBy
],
position: 4,
isVisible: true,
Expand All @@ -67,7 +67,7 @@ export const tasksAllView = async (
{
fieldMetadataId:
objectMetadataMap[STANDARD_OBJECT_IDS.task].fields[
TASK_STANDARD_FIELD_IDS.assignee
TASK_STANDARD_FIELD_IDS.dueAt
],
position: 5,
isVisible: true,
Expand All @@ -76,7 +76,7 @@ export const tasksAllView = async (
{
fieldMetadataId:
objectMetadataMap[STANDARD_OBJECT_IDS.task].fields[
TASK_STANDARD_FIELD_IDS.body
TASK_STANDARD_FIELD_IDS.assignee
],
position: 6,
isVisible: true,
Expand All @@ -85,12 +85,21 @@ export const tasksAllView = async (
{
fieldMetadataId:
objectMetadataMap[STANDARD_OBJECT_IDS.task].fields[
BASE_OBJECT_STANDARD_FIELD_IDS.createdAt
TASK_STANDARD_FIELD_IDS.body
],
position: 7,
isVisible: true,
size: 150,
},
{
fieldMetadataId:
objectMetadataMap[STANDARD_OBJECT_IDS.task].fields[
BASE_OBJECT_STANDARD_FIELD_IDS.createdAt
],
position: 8,
isVisible: true,
size: 150,
},
/*
TODO: Add later, since we don't have real-time it probably doesn't work well?
{
Expand Down
Loading

0 comments on commit f8e5b33

Please sign in to comment.