diff --git a/packages/twenty-front/src/modules/activities/components/ActivityTargetChips.tsx b/packages/twenty-front/src/modules/activities/components/ActivityTargetChips.tsx
index 6b69c22d6..b6e69cc7b 100644
--- a/packages/twenty-front/src/modules/activities/components/ActivityTargetChips.tsx
+++ b/packages/twenty-front/src/modules/activities/components/ActivityTargetChips.tsx
@@ -1,8 +1,7 @@
import styled from '@emotion/styled';
-import { CompanyChip } from '@/companies/components/CompanyChip';
-import { PersonChip } from '@/people/components/PersonChip';
-import { getLogoUrlFromDomainName } from '~/utils';
+import { ActivityTargetObjectRecord } from '@/activities/types/ActivityTargetObject';
+import { RecordChip } from '@/object-record/components/RecordChip';
const StyledContainer = styled.div`
display: flex;
@@ -10,37 +9,21 @@ const StyledContainer = styled.div`
gap: ${({ theme }) => theme.spacing(1)};
`;
-// TODO: fix edges pagination formatting on n+N
-export const ActivityTargetChips = ({ targets }: { targets?: any }) => {
- if (!targets) {
- return null;
- }
-
+export const ActivityTargetChips = ({
+ activityTargetObjectRecords,
+}: {
+ activityTargetObjectRecords: ActivityTargetObjectRecord[];
+}) => {
return (
- {targets?.map(({ company, person }: any) => {
- if (company) {
- return (
-
- );
- }
- if (person) {
- return (
-
- );
- }
- return <>>;
- })}
+ {activityTargetObjectRecords?.map((activityTargetObjectRecord) => (
+
+ ))}
);
};
diff --git a/packages/twenty-front/src/modules/activities/emails/components/Threads.tsx b/packages/twenty-front/src/modules/activities/emails/components/Threads.tsx
index 952e7958c..bb9fc06b1 100644
--- a/packages/twenty-front/src/modules/activities/emails/components/Threads.tsx
+++ b/packages/twenty-front/src/modules/activities/emails/components/Threads.tsx
@@ -4,7 +4,7 @@ import styled from '@emotion/styled';
import { ThreadPreview } from '@/activities/emails/components/ThreadPreview';
import { getTimelineThreadsFromCompanyId } from '@/activities/emails/queries/getTimelineThreadsFromCompanyId';
import { getTimelineThreadsFromPersonId } from '@/activities/emails/queries/getTimelineThreadsFromPersonId';
-import { ActivityTargetableEntity } from '@/activities/types/ActivityTargetableEntity';
+import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import {
H1Title,
H1TitleFontColor,
@@ -29,14 +29,14 @@ const StyledEmailCount = styled.span`
color: ${({ theme }) => theme.font.color.light};
`;
-export const Threads = ({ entity }: { entity: ActivityTargetableEntity }) => {
+export const Threads = ({ entity }: { entity: ActivityTargetableObject }) => {
const threadQuery =
- entity.type === 'Person'
+ entity.targetObjectNameSingular === 'person'
? getTimelineThreadsFromPersonId
: getTimelineThreadsFromCompanyId;
const threadQueryVariables =
- entity.type === 'Person'
+ entity.targetObjectNameSingular === 'person'
? { personId: entity.id }
: { companyId: entity.id };
@@ -50,7 +50,7 @@ export const Threads = ({ entity }: { entity: ActivityTargetableEntity }) => {
const timelineThreads: TimelineThread[] =
threads.data[
- entity.type === 'Person'
+ entity.targetObjectNameSingular === 'Person'
? 'getTimelineThreadsFromPersonId'
: 'getTimelineThreadsFromCompanyId'
];
diff --git a/packages/twenty-front/src/modules/activities/emails/components/__stories__/Threads.stories.tsx b/packages/twenty-front/src/modules/activities/emails/components/__stories__/Threads.stories.tsx
index a4c4d04d4..92fbf44e5 100644
--- a/packages/twenty-front/src/modules/activities/emails/components/__stories__/Threads.stories.tsx
+++ b/packages/twenty-front/src/modules/activities/emails/components/__stories__/Threads.stories.tsx
@@ -7,7 +7,7 @@ const meta: Meta = {
component: Threads,
args: {
entity: {
- type: 'Person',
+ targetObjectNameSingular: 'person',
id: '52ba3fd0-c723-4482-8b11-5fc24a587c71',
},
},
diff --git a/packages/twenty-front/src/modules/activities/files/components/Attachments.tsx b/packages/twenty-front/src/modules/activities/files/components/Attachments.tsx
index b7b8dbd77..27225559c 100644
--- a/packages/twenty-front/src/modules/activities/files/components/Attachments.tsx
+++ b/packages/twenty-front/src/modules/activities/files/components/Attachments.tsx
@@ -1,12 +1,14 @@
import { ChangeEvent, useRef } from 'react';
import styled from '@emotion/styled';
+import { isNonEmptyArray } from '@sniptt/guards';
import { useRecoilValue } from 'recoil';
import { AttachmentList } from '@/activities/files/components/AttachmentList';
import { useAttachments } from '@/activities/files/hooks/useAttachments';
import { Attachment } from '@/activities/files/types/Attachment';
import { getFileType } from '@/activities/files/utils/getFileType';
-import { ActivityTargetableEntity } from '@/activities/types/ActivityTargetableEntity';
+import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
+import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
@@ -56,13 +58,13 @@ const StyledFileInput = styled.input`
`;
export const Attachments = ({
- targetableEntity,
+ targetableObject,
}: {
- targetableEntity: ActivityTargetableEntity;
+ targetableObject: ActivityTargetableObject;
}) => {
const inputFileRef = useRef(null);
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
- const { attachments } = useAttachments(targetableEntity);
+ const { attachments } = useAttachments(targetableObject);
const [uploadFile] = useUploadFileMutation();
@@ -92,22 +94,23 @@ export const Attachments = ({
if (!attachmentUrl) {
return;
}
- if (!createOneAttachment) {
- return;
- }
- await createOneAttachment({
+ const targetableObjectFieldIdName = getActivityTargetObjectFieldIdName({
+ nameSingular: targetableObject.targetObjectNameSingular,
+ });
+
+ const attachmentToCreate = {
authorId: currentWorkspaceMember?.id,
name: file.name,
fullPath: attachmentUrl,
type: getFileType(file.name),
- companyId:
- targetableEntity.type === 'Company' ? targetableEntity.id : null,
- personId: targetableEntity.type === 'Person' ? targetableEntity.id : null,
- });
+ [targetableObjectFieldIdName]: targetableObject.id,
+ };
+
+ await createOneAttachment(attachmentToCreate);
};
- if (attachments?.length === 0 && targetableEntity.type !== 'Custom') {
+ if (!isNonEmptyArray(attachments)) {
return (
{
+export const useAttachments = (targetableObject: ActivityTargetableObject) => {
+ const targetableObjectFieldIdName = getActivityTargetObjectFieldIdName({
+ nameSingular: targetableObject.targetObjectNameSingular,
+ });
+
const { records: attachments } = useFindManyRecords({
objectNameSingular: CoreObjectNameSingular.Attachment,
filter: {
- [entity.type === 'Company' ? 'companyId' : 'personId']: { eq: entity.id },
+ [targetableObjectFieldIdName]: {
+ eq: targetableObject.id,
+ },
},
orderBy: {
createdAt: 'DescNullsFirst',
diff --git a/packages/twenty-front/src/modules/activities/hooks/useActivityTargetObjectRecords.ts b/packages/twenty-front/src/modules/activities/hooks/useActivityTargetObjectRecords.ts
new file mode 100644
index 000000000..b3ef7be3d
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/hooks/useActivityTargetObjectRecords.ts
@@ -0,0 +1,55 @@
+import { useMemo } from 'react';
+import { useRecoilValue } from 'recoil';
+
+import { ActivityTargetObjectRecord } from '@/activities/types/ActivityTargetObject';
+import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
+import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
+import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
+import { Nullable } from '~/types/Nullable';
+import { isDefined } from '~/utils/isDefined';
+
+export const useActivityTargetObjectRecords = ({
+ activityId,
+}: {
+ activityId: string;
+}) => {
+ const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
+
+ const { records: activityTargets } = useFindManyRecords({
+ objectNameSingular: CoreObjectNameSingular.ActivityTarget,
+ filter: {
+ activityId: {
+ eq: activityId,
+ },
+ },
+ });
+
+ const activityTargetObjectRecords = useMemo(() => {
+ return activityTargets
+ .map>((activityTarget) => {
+ const correspondingObjectMetadataItem = objectMetadataItems.find(
+ (objectMetadataItem) =>
+ isDefined(activityTarget[objectMetadataItem.nameSingular]) &&
+ !objectMetadataItem.isSystem,
+ );
+
+ if (!correspondingObjectMetadataItem) {
+ return null;
+ }
+
+ return {
+ activityTargetRecord: activityTarget,
+ targetObjectRecord:
+ activityTarget[correspondingObjectMetadataItem.nameSingular],
+ targetObjectMetadataItem: correspondingObjectMetadataItem,
+ targetObjectNameSingular:
+ correspondingObjectMetadataItem.nameSingular,
+ };
+ })
+ .filter(isDefined);
+ }, [activityTargets, objectMetadataItems]);
+
+ return {
+ activityTargetObjectRecords,
+ };
+};
diff --git a/packages/twenty-front/src/modules/activities/hooks/useActivityTargets.ts b/packages/twenty-front/src/modules/activities/hooks/useActivityTargets.ts
new file mode 100644
index 000000000..eb8845073
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/hooks/useActivityTargets.ts
@@ -0,0 +1,28 @@
+import { ActivityTarget } from '@/activities/types/ActivityTarget';
+import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
+import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName';
+import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
+import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
+
+export const useActivityTargets = ({
+ targetableObject,
+}: {
+ targetableObject: ActivityTargetableObject;
+}) => {
+ const targetObjectFieldName = getActivityTargetObjectFieldIdName({
+ nameSingular: targetableObject.targetObjectNameSingular,
+ });
+
+ const { records: activityTargets } = useFindManyRecords({
+ objectNameSingular: CoreObjectNameSingular.ActivityTarget,
+ filter: {
+ [targetObjectFieldName]: {
+ eq: targetableObject.id,
+ },
+ },
+ });
+
+ return {
+ activityTargets: activityTargets as ActivityTarget[],
+ };
+};
diff --git a/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts b/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts
index 1567231f7..b0581ec5c 100644
--- a/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts
+++ b/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts
@@ -4,8 +4,10 @@ import { useRecoilState, useRecoilValue } from 'recoil';
import { Activity, ActivityType } from '@/activities/types/Activity';
import { ActivityTarget } from '@/activities/types/ActivityTarget';
+import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
+import { useCreateManyRecords } from '@/object-record/hooks/useCreateManyRecords';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
@@ -14,13 +16,13 @@ import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope
import { activityTargetableEntityArrayState } from '../states/activityTargetableEntityArrayState';
import { viewableActivityIdState } from '../states/viewableActivityIdState';
-import { ActivityTargetableEntity } from '../types/ActivityTargetableEntity';
-import { getTargetableEntitiesWithParents } from '../utils/getTargetableEntitiesWithParents';
+import { ActivityTargetableObject } from '../types/ActivityTargetableEntity';
+import { flattenTargetableObjectsAndTheirRelatedTargetableObjects } from '../utils/flattenTargetableObjectsAndTheirRelatedTargetableObjects';
export const useOpenCreateActivityDrawer = () => {
const { openRightDrawer } = useRightDrawer();
- const { createOneRecord: createOneActivityTarget } =
- useCreateOneRecord({
+ const { createManyRecords: createManyActivityTargets } =
+ useCreateManyRecords({
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
});
const { createOneRecord: createOneActivity } = useCreateOneRecord({
@@ -37,15 +39,17 @@ export const useOpenCreateActivityDrawer = () => {
return useCallback(
async ({
type,
- targetableEntities,
+ targetableObjects,
assigneeId,
}: {
type: ActivityType;
- targetableEntities?: ActivityTargetableEntity[];
+ targetableObjects?: ActivityTargetableObject[];
assigneeId?: string;
}) => {
- const targetableEntitiesWithRelations = targetableEntities
- ? getTargetableEntitiesWithParents(targetableEntities)
+ const flattenedTargetableObjects = targetableObjects
+ ? flattenTargetableObjectsAndTheirRelatedTargetableObjects(
+ targetableObjects,
+ )
: [];
const createdActivity = await createOneActivity?.({
@@ -61,21 +65,25 @@ export const useOpenCreateActivityDrawer = () => {
return;
}
- await Promise.all(
- targetableEntitiesWithRelations.map(async (targetableEntity) => {
- await createOneActivityTarget?.({
- companyId:
- targetableEntity.type === 'Company' ? targetableEntity.id : null,
- personId:
- targetableEntity.type === 'Person' ? targetableEntity.id : null,
+ const activityTargetsToCreate = flattenedTargetableObjects.map(
+ (targetableObject) => {
+ const targetableObjectFieldIdName =
+ getActivityTargetObjectFieldIdName({
+ nameSingular: targetableObject.targetObjectNameSingular,
+ });
+
+ return {
+ [targetableObjectFieldIdName]: targetableObject.id,
activityId: createdActivity.id,
- });
- }),
+ };
+ },
);
+ await createManyActivityTargets(activityTargetsToCreate);
+
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
setViewableActivityId(createdActivity.id);
- setActivityTargetableEntityArray(targetableEntities ?? []);
+ setActivityTargetableEntityArray(targetableObjects ?? []);
openRightDrawer(RightDrawerPages.CreateActivity);
},
[
@@ -84,7 +92,7 @@ export const useOpenCreateActivityDrawer = () => {
setHotkeyScope,
setViewableActivityId,
createOneActivity,
- createOneActivityTarget,
+ createManyActivityTargets,
currentWorkspaceMember,
],
);
diff --git a/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawerForSelectedRowIds.ts b/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawerForSelectedRowIds.ts
index b950b7a6f..d65e78eb2 100644
--- a/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawerForSelectedRowIds.ts
+++ b/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawerForSelectedRowIds.ts
@@ -4,10 +4,7 @@ import { ActivityType } from '@/activities/types/Activity';
import { useRecordTableScopedStates } from '@/object-record/record-table/hooks/internal/useRecordTableScopedStates';
import { getRecordTableScopeInjector } from '@/object-record/record-table/utils/getRecordTableScopeInjector';
-import {
- ActivityTargetableEntity,
- ActivityTargetableEntityType,
-} from '../types/ActivityTargetableEntity';
+import { ActivityTargetableObject } from '../types/ActivityTargetableEntity';
import { useOpenCreateActivityDrawer } from './useOpenCreateActivityDrawer';
@@ -25,8 +22,8 @@ export const useOpenCreateActivityDrawerForSelectedRowIds = (
({ snapshot }) =>
(
type: ActivityType,
- entityType: ActivityTargetableEntityType,
- relatedEntities?: ActivityTargetableEntity[],
+ objectNameSingular: string,
+ relatedEntities?: ActivityTargetableObject[],
) => {
const selectedRowIds =
injectSelectorSnapshotValueWithRecordTableScopeId(
@@ -34,18 +31,21 @@ export const useOpenCreateActivityDrawerForSelectedRowIds = (
selectedRowIdsScopeInjector,
);
- let activityTargetableEntityArray: ActivityTargetableEntity[] =
+ let activityTargetableEntityArray: ActivityTargetableObject[] =
selectedRowIds.map((id: string) => ({
- type: entityType,
+ type: 'Custom',
+ targetObjectNameSingular: objectNameSingular,
id,
}));
+
if (relatedEntities) {
activityTargetableEntityArray =
activityTargetableEntityArray.concat(relatedEntities);
}
+
openCreateActivityDrawer({
type,
- targetableEntities: activityTargetableEntityArray,
+ targetableObjects: activityTargetableEntityArray,
});
},
[
diff --git a/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetInlineCellEditMode.tsx b/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetInlineCellEditMode.tsx
index 04e0c69f2..7a22cd4dd 100644
--- a/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetInlineCellEditMode.tsx
+++ b/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetInlineCellEditMode.tsx
@@ -1,22 +1,15 @@
-import { useCallback, useMemo, useState } from 'react';
-import { useQuery } from '@apollo/client';
import styled from '@emotion/styled';
+import { v4 } from 'uuid';
-import { useHandleCheckableActivityTargetChange } from '@/activities/hooks/useHandleCheckableActivityTargetChange';
import { ActivityTarget } from '@/activities/types/ActivityTarget';
-import { flatMapAndSortEntityForSelectArrayOfArrayByName } from '@/activities/utils/flatMapAndSortEntityForSelectArrayByName';
-import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
+import { ActivityTargetObjectRecord } from '@/activities/types/ActivityTargetObject';
+import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
+import { useCreateManyRecords } from '@/object-record/hooks/useCreateManyRecords';
+import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords';
import { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlineCell';
-import { MultipleEntitySelect } from '@/object-record/relation-picker/components/MultipleEntitySelect';
-import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker';
-import { useFilteredSearchEntityQuery } from '@/search/hooks/useFilteredSearchEntityQuery';
-import { assertNotNull } from '~/utils/assert';
-
-type ActivityTargetInlineCellEditModeProps = {
- activityId: string;
- activityTargets: Array>;
-};
+import { MultipleObjectRecordSelect } from '@/object-record/relation-picker/components/MultipleObjectRecordSelect';
+import { ObjectRecordForSelect } from '@/object-record/relation-picker/hooks/useMultiObjectSearch';
const StyledSelectContainer = styled.div`
left: 0px;
@@ -24,125 +17,77 @@ const StyledSelectContainer = styled.div`
top: -8px;
`;
+type ActivityTargetInlineCellEditModeProps = {
+ activityId: string;
+ activityTargetObjectRecords: ActivityTargetObjectRecord[];
+};
+
export const ActivityTargetInlineCellEditMode = ({
activityId,
- activityTargets,
+ activityTargetObjectRecords,
}: ActivityTargetInlineCellEditModeProps) => {
- const [searchFilter, setSearchFilter] = useState('');
-
- const initialPeopleIds = useMemo(
- () =>
- activityTargets
- ?.filter(({ personId }) => personId !== null)
- .map(({ personId }) => personId)
- .filter(assertNotNull) ?? [],
- [activityTargets],
+ const selectedObjectRecordIds = activityTargetObjectRecords.map(
+ (activityTarget) => ({
+ objectNameSingular: activityTarget.targetObjectNameSingular,
+ id: activityTarget.targetObjectRecord.id,
+ }),
);
- const initialCompanyIds = useMemo(
- () =>
- activityTargets
- ?.filter(({ companyId }) => companyId !== null)
- .map(({ companyId }) => companyId)
- .filter(assertNotNull) ?? [],
- [activityTargets],
- );
-
- const initialSelectedEntityIds = useMemo(
- () =>
- [...initialPeopleIds, ...initialCompanyIds].reduce<
- Record
- >((result, entityId) => ({ ...result, [entityId]: true }), {}),
- [initialPeopleIds, initialCompanyIds],
- );
-
- const { findManyRecordsQuery: findManyPeopleQuery } = useObjectMetadataItem({
- objectNameSingular: CoreObjectNameSingular.Person,
- });
-
- const { findManyRecordsQuery: findManyCompaniesQuery } =
- useObjectMetadataItem({
- objectNameSingular: CoreObjectNameSingular.Company,
+ const { createManyRecords: createManyActivityTargets } =
+ useCreateManyRecords({
+ objectNameSingular: CoreObjectNameSingular.ActivityTarget,
});
- const useFindManyPeopleQuery = (options: any) =>
- useQuery(findManyPeopleQuery, options);
+ const { deleteManyRecords: deleteManyActivityTargets } = useDeleteManyRecords(
+ {
+ objectNameSingular: CoreObjectNameSingular.ActivityTarget,
+ },
+ );
- const useFindManyCompaniesQuery = (options: any) =>
- useQuery(findManyCompaniesQuery, options);
-
- const [selectedEntityIds, setSelectedEntityIds] = useState<
- Record
- >(initialSelectedEntityIds);
-
- const { identifiersMapper, searchQuery } = useRelationPicker();
-
- const people = useFilteredSearchEntityQuery({
- queryHook: useFindManyPeopleQuery,
- filters: [
- {
- fieldNames: searchQuery?.computeFilterFields?.('person') ?? [],
- filter: searchFilter,
- },
- ],
- orderByField: 'createdAt',
- mappingFunction: (record: any) => identifiersMapper?.(record, 'person'),
- selectedIds: initialPeopleIds,
- objectNameSingular: CoreObjectNameSingular.Person,
- limit: 3,
- });
-
- const companies = useFilteredSearchEntityQuery({
- queryHook: useFindManyCompaniesQuery,
- filters: [
- {
- fieldNames: searchQuery?.computeFilterFields?.('company') ?? [],
- filter: searchFilter,
- },
- ],
- orderByField: 'createdAt',
- mappingFunction: (record: any) => identifiersMapper?.(record, 'company'),
- selectedIds: initialCompanyIds,
- objectNameSingular: CoreObjectNameSingular.Company,
- limit: 3,
- });
-
- const selectedEntities = flatMapAndSortEntityForSelectArrayOfArrayByName([
- people.selectedEntities,
- companies.selectedEntities,
- ]);
-
- const filteredSelectedEntities =
- flatMapAndSortEntityForSelectArrayOfArrayByName([
- people.filteredSelectedEntities,
- companies.filteredSelectedEntities,
- ]);
-
- const entitiesToSelect = flatMapAndSortEntityForSelectArrayOfArrayByName([
- people.entitiesToSelect,
- companies.entitiesToSelect,
- ]);
-
- const handleCheckItemsChange = useHandleCheckableActivityTargetChange({
- activityId,
- currentActivityTargets: activityTargets,
- });
const { closeInlineCell: closeEditableField } = useInlineCell();
- const handleSubmit = useCallback(() => {
- handleCheckItemsChange(
- selectedEntityIds,
- entitiesToSelect,
- selectedEntities,
- );
+ const handleSubmit = async (selectedRecords: ObjectRecordForSelect[]) => {
closeEditableField();
- }, [
- closeEditableField,
- entitiesToSelect,
- handleCheckItemsChange,
- selectedEntities,
- selectedEntityIds,
- ]);
+
+ const activityTargetRecordsToDelete = activityTargetObjectRecords.filter(
+ (activityTargetObjectRecord) =>
+ !selectedRecords.some(
+ (selectedRecord) =>
+ selectedRecord.recordIdentifier.id ===
+ activityTargetObjectRecord.targetObjectRecord.id,
+ ),
+ );
+
+ const activityTargetRecordsToCreate = selectedRecords.filter(
+ (selectedRecord) =>
+ !activityTargetObjectRecords.some(
+ (activityTargetObjectRecord) =>
+ activityTargetObjectRecord.targetObjectRecord.id ===
+ selectedRecord.recordIdentifier.id,
+ ),
+ );
+
+ if (activityTargetRecordsToCreate.length > 0) {
+ await createManyActivityTargets(
+ activityTargetRecordsToCreate.map((selectedRecord) => ({
+ id: v4(),
+ activityId,
+ [getActivityTargetObjectFieldIdName({
+ nameSingular: selectedRecord.objectMetadataItem.nameSingular,
+ })]: selectedRecord.recordIdentifier.id,
+ })),
+ );
+ }
+
+ if (activityTargetRecordsToDelete.length > 0) {
+ await deleteManyActivityTargets(
+ activityTargetRecordsToDelete.map(
+ (activityTargetObjectRecord) =>
+ activityTargetObjectRecord.activityTargetRecord.id,
+ ),
+ );
+ }
+ };
const handleCancel = () => {
closeEditableField();
@@ -150,17 +95,8 @@ export const ActivityTargetInlineCellEditMode = ({
return (
-
diff --git a/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetsInlineCell.tsx b/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetsInlineCell.tsx
index cc94e6fee..fd062ed71 100644
--- a/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetsInlineCell.tsx
+++ b/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetsInlineCell.tsx
@@ -1,9 +1,8 @@
import { ActivityTargetChips } from '@/activities/components/ActivityTargetChips';
+import { useActivityTargetObjectRecords } from '@/activities/hooks/useActivityTargetObjectRecords';
import { ActivityTargetInlineCellEditMode } from '@/activities/inline-cell/components/ActivityTargetInlineCellEditMode';
import { ActivityTarget } from '@/activities/types/ActivityTarget';
import { GraphQLActivity } from '@/activities/types/GraphQLActivity';
-import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
-import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { RecordInlineCellContainer } from '@/object-record/record-inline-cell/components/RecordInlineCellContainer';
import { FieldRecoilScopeContext } from '@/object-record/record-inline-cell/states/recoil-scope-contexts/FieldRecoilScopeContext';
import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
@@ -23,14 +22,8 @@ type ActivityTargetsInlineCellProps = {
export const ActivityTargetsInlineCell = ({
activity,
}: ActivityTargetsInlineCellProps) => {
- const activityTargetIds =
- activity?.activityTargets?.edges?.map(
- (activityTarget) => activityTarget.node.id,
- ) ?? [];
-
- const { records: activityTargets } = useFindManyRecords({
- objectNameSingular: CoreObjectNameSingular.ActivityTarget,
- filter: { id: { in: activityTargetIds } },
+ const { activityTargetObjectRecords } = useActivityTargetObjectRecords({
+ activityId: activity?.id ?? '',
});
return (
@@ -44,11 +37,15 @@ export const ActivityTargetsInlineCell = ({
editModeContent={
}
label="Relations"
- displayModeContent={}
+ displayModeContent={
+
+ }
isDisplayModeContentEmpty={
activity?.activityTargets?.edges?.length === 0
}
diff --git a/packages/twenty-front/src/modules/activities/notes/components/Notes.tsx b/packages/twenty-front/src/modules/activities/notes/components/Notes.tsx
index 800abcb88..24dbb45d4 100644
--- a/packages/twenty-front/src/modules/activities/notes/components/Notes.tsx
+++ b/packages/twenty-front/src/modules/activities/notes/components/Notes.tsx
@@ -3,7 +3,7 @@ import styled from '@emotion/styled';
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
import { NoteList } from '@/activities/notes/components/NoteList';
import { useNotes } from '@/activities/notes/hooks/useNotes';
-import { ActivityTargetableEntity } from '@/activities/types/ActivityTargetableEntity';
+import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { IconPlus } from '@/ui/display/icon';
import { Button } from '@/ui/input/button/components/Button';
@@ -44,12 +44,16 @@ const StyledNotesContainer = styled.div`
overflow: auto;
`;
-export const Notes = ({ entity }: { entity: ActivityTargetableEntity }) => {
- const { notes } = useNotes(entity);
+export const Notes = ({
+ targetableObject,
+}: {
+ targetableObject: ActivityTargetableObject;
+}) => {
+ const { notes } = useNotes(targetableObject);
const openCreateActivity = useOpenCreateActivityDrawer();
- if (notes?.length === 0 && entity.type !== 'Custom') {
+ if (notes?.length === 0) {
return (
No note yet
@@ -61,7 +65,7 @@ export const Notes = ({ entity }: { entity: ActivityTargetableEntity }) => {
onClick={() =>
openCreateActivity({
type: 'Note',
- targetableEntities: [entity],
+ targetableObjects: [targetableObject],
})
}
/>
@@ -83,7 +87,7 @@ export const Notes = ({ entity }: { entity: ActivityTargetableEntity }) => {
onClick={() =>
openCreateActivity({
type: 'Note',
- targetableEntities: [entity],
+ targetableObjects: [targetableObject],
})
}
>
diff --git a/packages/twenty-front/src/modules/activities/notes/hooks/useNotes.ts b/packages/twenty-front/src/modules/activities/notes/hooks/useNotes.ts
index 3c8e4f867..de0f8ef37 100644
--- a/packages/twenty-front/src/modules/activities/notes/hooks/useNotes.ts
+++ b/packages/twenty-front/src/modules/activities/notes/hooks/useNotes.ts
@@ -1,17 +1,13 @@
+import { useActivityTargets } from '@/activities/hooks/useActivityTargets';
import { Note } from '@/activities/types/Note';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { OrderByField } from '@/object-metadata/types/OrderByField';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
-import { ActivityTargetableEntity } from '../../types/ActivityTargetableEntity';
+import { ActivityTargetableObject } from '../../types/ActivityTargetableEntity';
-export const useNotes = (entity: ActivityTargetableEntity) => {
- const { records: activityTargets } = useFindManyRecords({
- objectNameSingular: CoreObjectNameSingular.ActivityTarget,
- filter: {
- [entity.type === 'Company' ? 'companyId' : 'personId']: { eq: entity.id },
- },
- });
+export const useNotes = (targetableObject: ActivityTargetableObject) => {
+ const { activityTargets } = useActivityTargets({ targetableObject });
const filter = {
id: {
@@ -19,6 +15,7 @@ export const useNotes = (entity: ActivityTargetableEntity) => {
},
type: { eq: 'Note' },
};
+
const orderBy = {
createdAt: 'AscNullsFirst',
} as OrderByField;
diff --git a/packages/twenty-front/src/modules/activities/states/activityTargetableEntityArrayState.ts b/packages/twenty-front/src/modules/activities/states/activityTargetableEntityArrayState.ts
index 25e2da755..c3a06d48f 100644
--- a/packages/twenty-front/src/modules/activities/states/activityTargetableEntityArrayState.ts
+++ b/packages/twenty-front/src/modules/activities/states/activityTargetableEntityArrayState.ts
@@ -1,9 +1,9 @@
import { atom } from 'recoil';
-import { ActivityTargetableEntity } from '../types/ActivityTargetableEntity';
+import { ActivityTargetableObject } from '../types/ActivityTargetableEntity';
export const activityTargetableEntityArrayState = atom<
- ActivityTargetableEntity[]
+ ActivityTargetableObject[]
>({
key: 'activities/targetable-entity-array',
default: [],
diff --git a/packages/twenty-front/src/modules/activities/tasks/__stories__/TaskGroups.stories.tsx b/packages/twenty-front/src/modules/activities/tasks/__stories__/TaskGroups.stories.tsx
index c369e267d..7404fcf19 100644
--- a/packages/twenty-front/src/modules/activities/tasks/__stories__/TaskGroups.stories.tsx
+++ b/packages/twenty-front/src/modules/activities/tasks/__stories__/TaskGroups.stories.tsx
@@ -2,6 +2,7 @@ import { Meta, StoryObj } from '@storybook/react';
import { TasksRecoilScopeContext } from '@/activities/states/recoil-scope-contexts/TasksRecoilScopeContext';
import { TaskGroups } from '@/activities/tasks/components/TaskGroups';
+import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { ObjectFilterDropdownScope } from '@/object-record/object-filter-dropdown/scopes/ObjectFilterDropdownScope';
import { ComponentWithRecoilScopeDecorator } from '~/testing/decorators/ComponentWithRecoilScopeDecorator';
import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator';
@@ -35,9 +36,11 @@ export const Empty: Story = {};
export const WithTasks: Story = {
args: {
- entity: {
- id: mockedTasks[0].authorId,
- type: 'Person',
- },
+ targetableObjects: [
+ {
+ id: mockedTasks[0].authorId,
+ targetObjectNameSingular: 'person',
+ },
+ ] as ActivityTargetableObject[],
},
};
diff --git a/packages/twenty-front/src/modules/activities/tasks/components/AddTaskButton.tsx b/packages/twenty-front/src/modules/activities/tasks/components/AddTaskButton.tsx
index a3ee09979..3c02ddb29 100644
--- a/packages/twenty-front/src/modules/activities/tasks/components/AddTaskButton.tsx
+++ b/packages/twenty-front/src/modules/activities/tasks/components/AddTaskButton.tsx
@@ -1,16 +1,18 @@
+import { isNonEmptyArray } from '@sniptt/guards';
+
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
-import { ActivityTargetableEntity } from '@/activities/types/ActivityTargetableEntity';
+import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { IconPlus } from '@/ui/display/icon';
import { Button } from '@/ui/input/button/components/Button';
export const AddTaskButton = ({
- activityTargetEntity,
+ activityTargetableObjects,
}: {
- activityTargetEntity?: ActivityTargetableEntity;
+ activityTargetableObjects?: ActivityTargetableObject[];
}) => {
const openCreateActivity = useOpenCreateActivityDrawer();
- if (!activityTargetEntity) {
+ if (!isNonEmptyArray(activityTargetableObjects)) {
return <>>;
}
@@ -23,7 +25,7 @@ export const AddTaskButton = ({
onClick={() =>
openCreateActivity({
type: 'Task',
- targetableEntities: [activityTargetEntity],
+ targetableObjects: activityTargetableObjects,
})
}
>
diff --git a/packages/twenty-front/src/modules/activities/tasks/components/EntityTasks.tsx b/packages/twenty-front/src/modules/activities/tasks/components/ObjectTasks.tsx
similarity index 78%
rename from packages/twenty-front/src/modules/activities/tasks/components/EntityTasks.tsx
rename to packages/twenty-front/src/modules/activities/tasks/components/ObjectTasks.tsx
index f7ffd434b..fd54c2169 100644
--- a/packages/twenty-front/src/modules/activities/tasks/components/EntityTasks.tsx
+++ b/packages/twenty-front/src/modules/activities/tasks/components/ObjectTasks.tsx
@@ -2,7 +2,7 @@ import styled from '@emotion/styled';
import { TasksRecoilScopeContext } from '@/activities/states/recoil-scope-contexts/TasksRecoilScopeContext';
import { TaskGroups } from '@/activities/tasks/components/TaskGroups';
-import { ActivityTargetableEntity } from '@/activities/types/ActivityTargetableEntity';
+import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { ObjectFilterDropdownScope } from '@/object-record/object-filter-dropdown/scopes/ObjectFilterDropdownScope';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
@@ -14,16 +14,16 @@ const StyledContainer = styled.div`
overflow: auto;
`;
-export const EntityTasks = ({
- entity,
+export const ObjectTasks = ({
+ targetableObject,
}: {
- entity: ActivityTargetableEntity;
+ targetableObject: ActivityTargetableObject;
}) => {
return (
-
+
diff --git a/packages/twenty-front/src/modules/activities/tasks/components/TaskGroups.tsx b/packages/twenty-front/src/modules/activities/tasks/components/TaskGroups.tsx
index 246500a97..c9c8934aa 100644
--- a/packages/twenty-front/src/modules/activities/tasks/components/TaskGroups.tsx
+++ b/packages/twenty-front/src/modules/activities/tasks/components/TaskGroups.tsx
@@ -3,7 +3,7 @@ import styled from '@emotion/styled';
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
import { TasksRecoilScopeContext } from '@/activities/states/recoil-scope-contexts/TasksRecoilScopeContext';
import { useTasks } from '@/activities/tasks/hooks/useTasks';
-import { ActivityTargetableEntity } from '@/activities/types/ActivityTargetableEntity';
+import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { IconPlus } from '@/ui/display/icon';
import { Button } from '@/ui/input/button/components/Button';
import { activeTabIdScopedState } from '@/ui/layout/tab/states/activeTabIdScopedState';
@@ -12,12 +12,6 @@ import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoi
import { AddTaskButton } from './AddTaskButton';
import { TaskList } from './TaskList';
-type TaskGroupsProps = {
- filterDropdownId?: string;
- entity?: ActivityTargetableEntity;
- showAddButton?: boolean;
-};
-
const StyledTaskGroupEmptyContainer = styled.div`
align-items: center;
align-self: stretch;
@@ -52,9 +46,15 @@ const StyledContainer = styled.div`
flex-direction: column;
`;
+type TaskGroupsProps = {
+ filterDropdownId?: string;
+ targetableObjects?: ActivityTargetableObject[];
+ showAddButton?: boolean;
+};
+
export const TaskGroups = ({
filterDropdownId,
- entity,
+ targetableObjects,
showAddButton,
}: TaskGroupsProps) => {
const {
@@ -62,7 +62,10 @@ export const TaskGroups = ({
upcomingTasks,
unscheduledTasks,
completedTasks,
- } = useTasks({ filterDropdownId: filterDropdownId, entity });
+ } = useTasks({
+ filterDropdownId: filterDropdownId,
+ targetableObjects: targetableObjects ?? [],
+ });
const openCreateActivity = useOpenCreateActivityDrawer();
@@ -71,10 +74,6 @@ export const TaskGroups = ({
TasksRecoilScopeContext,
);
- if (entity?.type === 'Custom') {
- return <>>;
- }
-
if (
(activeTabId !== 'done' &&
todayOrPreviousTasks?.length === 0 &&
@@ -93,7 +92,7 @@ export const TaskGroups = ({
onClick={() =>
openCreateActivity({
type: 'Task',
- targetableEntities: entity ? [entity] : undefined,
+ targetableObjects,
})
}
/>
@@ -107,7 +106,9 @@ export const TaskGroups = ({
+ showAddButton && (
+
+ )
}
/>
) : (
@@ -116,7 +117,9 @@ export const TaskGroups = ({
title="Today"
tasks={todayOrPreviousTasks ?? []}
button={
- showAddButton &&
+ showAddButton && (
+
+ )
}
/>
+
)
}
/>
@@ -136,7 +139,7 @@ export const TaskGroups = ({
showAddButton &&
!todayOrPreviousTasks?.length &&
!upcomingTasks?.length && (
-
+
)
}
/>
diff --git a/packages/twenty-front/src/modules/activities/tasks/components/TaskRow.tsx b/packages/twenty-front/src/modules/activities/tasks/components/TaskRow.tsx
index d5c0e1f1f..2bf25b01d 100644
--- a/packages/twenty-front/src/modules/activities/tasks/components/TaskRow.tsx
+++ b/packages/twenty-front/src/modules/activities/tasks/components/TaskRow.tsx
@@ -2,12 +2,10 @@ import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { ActivityTargetChips } from '@/activities/components/ActivityTargetChips';
+import { useActivityTargetObjectRecords } from '@/activities/hooks/useActivityTargetObjectRecords';
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
-import { ActivityTarget } from '@/activities/types/ActivityTarget';
import { GraphQLActivity } from '@/activities/types/GraphQLActivity';
import { getActivitySummary } from '@/activities/utils/getActivitySummary';
-import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
-import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { IconCalendar, IconComment } from '@/ui/display/icon';
import { OverflowingTextWithTooltip } from '@/ui/display/tooltip/OverflowingTextWithTooltip';
import { Checkbox, CheckboxShape } from '@/ui/input/components/Checkbox';
@@ -76,14 +74,8 @@ export const TaskRow = ({
const body = getActivitySummary(task.body);
const { completeTask } = useCompleteTask(task);
- const activityTargetIds =
- task?.activityTargets?.edges?.map(
- (activityTarget) => activityTarget.node.id,
- ) ?? [];
-
- const { records: activityTargets } = useFindManyRecords({
- objectNameSingular: CoreObjectNameSingular.ActivityTarget,
- filter: { id: { in: activityTargetIds } },
+ const { activityTargetObjectRecords } = useActivityTargetObjectRecords({
+ activityId: task.id,
});
return (
@@ -115,7 +107,9 @@ export const TaskRow = ({
)}
-
+
{
- const { filterDropdownId, entity } = props ?? {};
-
+export const useTasks = ({
+ targetableObjects,
+ filterDropdownId,
+}: UseTasksProps) => {
const { selectedFilter } = useFilterDropdown({
- filterDropdownId: filterDropdownId,
+ filterDropdownId,
});
+ const targetableObjectsFilter =
+ targetableObjects.reduce(
+ (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: isDefined(entity)
- ? {
- [entity?.type === 'Company' ? 'companyId' : 'personId']: {
- eq: entity?.id,
- },
- }
- : undefined,
+ filter: targetableObjectsFilter,
});
+ const skipRequest = !isNonEmptyArray(activityTargets) && !selectedFilter;
+
+ const idFilter = {
+ id: {
+ in: activityTargets.map((activityTarget) => activityTarget.activityId),
+ },
+ };
+
+ const assigneeIdFilter = selectedFilter
+ ? {
+ assigneeId: {
+ in: JSON.parse(selectedFilter.value),
+ },
+ }
+ : undefined;
+
const { records: completeTasksData } = useFindManyRecords({
objectNameSingular: CoreObjectNameSingular.Activity,
- skip: !entity && !selectedFilter,
+ skip: skipRequest,
filter: {
completedAt: { is: 'NOT_NULL' },
- ...(isDefined(entity) && {
- id: {
- in: activityTargets?.map(
- (activityTarget) => activityTarget.activityId,
- ),
- },
- }),
+ ...idFilter,
type: { eq: 'Task' },
- ...(isNonEmptyString(selectedFilter?.value) && {
- assigneeId: {
- in: JSON.parse(selectedFilter?.value),
- },
- }),
+ ...assigneeIdFilter,
},
orderBy: {
createdAt: 'DescNullsFirst',
@@ -58,22 +78,12 @@ export const useTasks = (props?: UseTasksProps) => {
const { records: incompleteTaskData } = useFindManyRecords({
objectNameSingular: CoreObjectNameSingular.Activity,
- skip: !entity && !selectedFilter,
+ skip: skipRequest,
filter: {
completedAt: { is: 'NULL' },
- ...(isDefined(entity) && {
- id: {
- in: activityTargets?.map(
- (activityTarget) => activityTarget.activityId,
- ),
- },
- }),
+ ...idFilter,
type: { eq: 'Task' },
- ...(isNonEmptyString(selectedFilter?.value) && {
- assigneeId: {
- in: JSON.parse(selectedFilter?.value),
- },
- }),
+ ...assigneeIdFilter,
},
orderBy: {
createdAt: 'DescNullsFirst',
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 2f30431a0..cd5d749d4 100644
--- a/packages/twenty-front/src/modules/activities/timeline/components/Timeline.tsx
+++ b/packages/twenty-front/src/modules/activities/timeline/components/Timeline.tsx
@@ -1,10 +1,12 @@
import React from 'react';
import styled from '@emotion/styled';
+import { isNonEmptyString } from '@sniptt/guards';
import { ActivityCreateButton } from '@/activities/components/ActivityCreateButton';
+import { useActivityTargets } from '@/activities/hooks/useActivityTargets';
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
import { Activity } from '@/activities/types/Activity';
-import { ActivityTargetableEntity } from '@/activities/types/ActivityTargetableEntity';
+import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
@@ -48,20 +50,21 @@ const StyledEmptyTimelineSubTitle = styled.div`
margin-bottom: ${({ theme }) => theme.spacing(2)};
`;
-export const Timeline = ({ entity }: { entity: ActivityTargetableEntity }) => {
- const { records: activityTargets, loading } = useFindManyRecords({
- objectNameSingular: CoreObjectNameSingular.ActivityTarget,
- filter: {
- [entity.type === 'Company' ? 'companyId' : 'personId']: { eq: entity.id },
- },
- });
+export const Timeline = ({
+ targetableObject,
+}: {
+ targetableObject: ActivityTargetableObject;
+}) => {
+ const { activityTargets } = useActivityTargets({ targetableObject });
const { records: activities } = useFindManyRecords({
skip: !activityTargets?.length,
objectNameSingular: CoreObjectNameSingular.Activity,
filter: {
id: {
- in: activityTargets?.map((activityTarget) => activityTarget.activityId),
+ in: activityTargets
+ ?.map((activityTarget) => activityTarget.activityId)
+ .filter(isNonEmptyString),
},
},
orderBy: {
@@ -71,10 +74,6 @@ export const Timeline = ({ entity }: { entity: ActivityTargetableEntity }) => {
const openCreateActivity = useOpenCreateActivityDrawer();
- if (loading || entity.type === 'Custom') {
- return <>>;
- }
-
if (!activities.length) {
return (
@@ -84,13 +83,13 @@ export const Timeline = ({ entity }: { entity: ActivityTargetableEntity }) => {
onNoteClick={() =>
openCreateActivity({
type: 'Note',
- targetableEntities: [entity],
+ targetableObjects: [targetableObject],
})
}
onTaskClick={() =>
openCreateActivity({
type: 'Task',
- targetableEntities: [entity],
+ targetableObjects: [targetableObject],
})
}
/>
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 3c2bd4ceb..435aaafd0 100644
--- a/packages/twenty-front/src/modules/activities/timeline/components/TimelineActivity.tsx
+++ b/packages/twenty-front/src/modules/activities/timeline/components/TimelineActivity.tsx
@@ -145,7 +145,7 @@ type TimelineActivityProps = {
| 'type'
| 'comments'
| 'dueAt'
- > & { author: Pick } & {
+ > & { author?: Pick } & {
assignee?: Pick | null;
};
isLastActivity?: boolean;
@@ -165,8 +165,8 @@ export const TimelineActivity = ({
@@ -175,7 +175,8 @@ export const TimelineActivity = ({
- {activity.author.name.firstName} {activity.author.name.lastName}
+ {activity.author?.name.firstName}{' '}
+ {activity.author?.name.lastName}
created a {activity.type.toLowerCase()}
diff --git a/packages/twenty-front/src/modules/activities/types/ActivityTargetObject.ts b/packages/twenty-front/src/modules/activities/types/ActivityTargetObject.ts
new file mode 100644
index 000000000..d95d4f520
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/types/ActivityTargetObject.ts
@@ -0,0 +1,9 @@
+import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
+import { ObjectRecord } from '@/object-record/types/ObjectRecord';
+
+export type ActivityTargetObjectRecord = {
+ targetObjectMetadataItem: ObjectMetadataItem;
+ activityTargetRecord: ObjectRecord;
+ targetObjectRecord: ObjectRecord;
+ targetObjectNameSingular: string;
+};
diff --git a/packages/twenty-front/src/modules/activities/types/ActivityTargetableEntity.ts b/packages/twenty-front/src/modules/activities/types/ActivityTargetableEntity.ts
index f474d9ac6..22d25858f 100644
--- a/packages/twenty-front/src/modules/activities/types/ActivityTargetableEntity.ts
+++ b/packages/twenty-front/src/modules/activities/types/ActivityTargetableEntity.ts
@@ -1,7 +1,5 @@
-export type ActivityTargetableEntityType = 'Person' | 'Company' | 'Custom';
-
-export type ActivityTargetableEntity = {
+export type ActivityTargetableObject = {
id: string;
- type: ActivityTargetableEntityType;
- relatedEntities?: ActivityTargetableEntity[];
+ targetObjectNameSingular: string;
+ relatedTargetableObjects?: ActivityTargetableObject[];
};
diff --git a/packages/twenty-front/src/modules/activities/types/ActivityTargetableEntityForSelect.ts b/packages/twenty-front/src/modules/activities/types/ActivityTargetableEntityForSelect.ts
deleted file mode 100644
index 9d1b65874..000000000
--- a/packages/twenty-front/src/modules/activities/types/ActivityTargetableEntityForSelect.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
-
-import { ActivityTargetableEntityType } from './ActivityTargetableEntity';
-
-export type ActivityTargetableEntityForSelect = EntityForSelect & {
- entityType: ActivityTargetableEntityType;
-};
diff --git a/packages/twenty-front/src/modules/activities/utils/__tests__/getTargetableEntitiesWithParents.test.ts b/packages/twenty-front/src/modules/activities/utils/__tests__/getTargetableEntitiesWithParents.test.ts
index 4fd159560..489364a1e 100644
--- a/packages/twenty-front/src/modules/activities/utils/__tests__/getTargetableEntitiesWithParents.test.ts
+++ b/packages/twenty-front/src/modules/activities/utils/__tests__/getTargetableEntitiesWithParents.test.ts
@@ -1,40 +1,41 @@
-import { ActivityTargetableEntity } from '@/activities/types/ActivityTargetableEntity';
-import { getTargetableEntitiesWithParents } from '@/activities/utils/getTargetableEntitiesWithParents';
+import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
+import { flattenTargetableObjectsAndTheirRelatedTargetableObjects } from '@/activities/utils/flattenTargetableObjectsAndTheirRelatedTargetableObjects';
describe('getTargetableEntitiesWithParents', () => {
it('should return the correct value', () => {
- const entities: ActivityTargetableEntity[] = [
+ const entities: ActivityTargetableObject[] = [
{
id: '1',
- type: 'Person',
- relatedEntities: [
+ targetObjectNameSingular: 'person',
+ relatedTargetableObjects: [
{
id: '2',
- type: 'Company',
+ targetObjectNameSingular: 'company',
},
],
},
{
id: '4',
- type: 'Company',
+ targetObjectNameSingular: 'person',
},
{
id: '3',
- type: 'Custom',
- relatedEntities: [
+ targetObjectNameSingular: 'car',
+ relatedTargetableObjects: [
{
id: '6',
- type: 'Person',
+ targetObjectNameSingular: 'person',
},
{
id: '5',
- type: 'Company',
+ targetObjectNameSingular: 'company',
},
],
},
];
- const res = getTargetableEntitiesWithParents(entities);
+ const res =
+ flattenTargetableObjectsAndTheirRelatedTargetableObjects(entities);
expect(res).toHaveLength(6);
expect(res[0].id).toBe('1');
diff --git a/packages/twenty-front/src/modules/activities/utils/flattenTargetableObjectsAndTheirRelatedTargetableObjects.ts b/packages/twenty-front/src/modules/activities/utils/flattenTargetableObjectsAndTheirRelatedTargetableObjects.ts
new file mode 100644
index 000000000..1f30e422a
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/utils/flattenTargetableObjectsAndTheirRelatedTargetableObjects.ts
@@ -0,0 +1,21 @@
+import { ActivityTargetableObject } from '../types/ActivityTargetableEntity';
+
+export const flattenTargetableObjectsAndTheirRelatedTargetableObjects = (
+ targetableObjectsWithRelatedTargetableObjects: ActivityTargetableObject[],
+): ActivityTargetableObject[] => {
+ const flattenedTargetableObjects: ActivityTargetableObject[] = [];
+
+ for (const targetableObject of targetableObjectsWithRelatedTargetableObjects ??
+ []) {
+ flattenedTargetableObjects.push(targetableObject);
+
+ if (targetableObject.relatedTargetableObjects) {
+ for (const relatedEntity of targetableObject.relatedTargetableObjects ??
+ []) {
+ flattenedTargetableObjects.push(relatedEntity);
+ }
+ }
+ }
+
+ return flattenedTargetableObjects;
+};
diff --git a/packages/twenty-front/src/modules/activities/utils/getTargetObjectFilterFieldName.ts b/packages/twenty-front/src/modules/activities/utils/getTargetObjectFilterFieldName.ts
new file mode 100644
index 000000000..be1256229
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/utils/getTargetObjectFilterFieldName.ts
@@ -0,0 +1,15 @@
+import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
+
+export const getActivityTargetObjectFieldIdName = ({
+ nameSingular,
+}: {
+ nameSingular: string;
+}) => {
+ const isCoreObject =
+ nameSingular === CoreObjectNameSingular.Company ||
+ nameSingular === CoreObjectNameSingular.Person;
+
+ const objectFieldIdName = `${!isCoreObject ? '_' : ''}${nameSingular}Id`;
+
+ return objectFieldIdName;
+};
diff --git a/packages/twenty-front/src/modules/activities/utils/getTargetableEntitiesWithParents.ts b/packages/twenty-front/src/modules/activities/utils/getTargetableEntitiesWithParents.ts
deleted file mode 100644
index f97d51d4a..000000000
--- a/packages/twenty-front/src/modules/activities/utils/getTargetableEntitiesWithParents.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { ActivityTargetableEntity } from '../types/ActivityTargetableEntity';
-
-export const getTargetableEntitiesWithParents = (
- entities: ActivityTargetableEntity[],
-): ActivityTargetableEntity[] => {
- const entitiesWithRelations: ActivityTargetableEntity[] = [];
- for (const entity of entities ?? []) {
- entitiesWithRelations.push(entity);
- if (entity.relatedEntities) {
- for (const relatedEntity of entity.relatedEntities ?? []) {
- entitiesWithRelations.push(relatedEntity);
- }
- }
- }
- return entitiesWithRelations;
-};
diff --git a/packages/twenty-front/src/modules/companies/hooks/useSpreadsheetCompanyImport.ts b/packages/twenty-front/src/modules/companies/hooks/useSpreadsheetCompanyImport.ts
index 81db1f8bc..9b26fb0b5 100644
--- a/packages/twenty-front/src/modules/companies/hooks/useSpreadsheetCompanyImport.ts
+++ b/packages/twenty-front/src/modules/companies/hooks/useSpreadsheetCompanyImport.ts
@@ -28,39 +28,44 @@ export const useSpreadsheetCompanyImport = () => {
...options,
onSubmit: async (data) => {
// TODO: Add better type checking in spreadsheet import later
- const createInputs = data.validData.map((company) => ({
- name: company.name as string | undefined,
- domainName: company.domainName as string | undefined,
- ...(company.linkedinUrl
- ? {
- linkedinLink: {
- label: 'linkedinUrl',
- url: company.linkedinUrl as string | undefined,
- },
- }
- : {}),
- ...(company.annualRecurringRevenue
- ? {
- annualRecurringRevenue: {
- amountMicros: Number(company.annualRecurringRevenue),
- currencyCode: 'USD',
- },
- }
- : {}),
- idealCustomerProfile:
- company.idealCustomerProfile &&
- ['true', true].includes(company.idealCustomerProfile),
- ...(company.xUrl
- ? {
- xLink: {
- label: 'xUrl',
- url: company.xUrl as string | undefined,
- },
- }
- : {}),
- address: company.address as string | undefined,
- employees: company.employees ? Number(company.employees) : undefined,
- }));
+ const createInputs = data.validData.map(
+ (company) =>
+ ({
+ name: company.name as string | undefined,
+ domainName: company.domainName as string | undefined,
+ ...(company.linkedinUrl
+ ? {
+ linkedinLink: {
+ label: 'linkedinUrl',
+ url: company.linkedinUrl as string | undefined,
+ },
+ }
+ : {}),
+ ...(company.annualRecurringRevenue
+ ? {
+ annualRecurringRevenue: {
+ amountMicros: Number(company.annualRecurringRevenue),
+ currencyCode: 'USD',
+ },
+ }
+ : {}),
+ idealCustomerProfile:
+ company.idealCustomerProfile &&
+ ['true', true].includes(company.idealCustomerProfile),
+ ...(company.xUrl
+ ? {
+ xLink: {
+ label: 'xUrl',
+ url: company.xUrl as string | undefined,
+ },
+ }
+ : {}),
+ address: company.address as string | undefined,
+ employees: company.employees
+ ? Number(company.employees)
+ : undefined,
+ }) as Company,
+ );
// TODO: abstract this part for any object
try {
await createManyCompanies(createInputs);
diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useFilterOutUnexistingObjectMetadataItems.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useFilterOutUnexistingObjectMetadataItems.ts
new file mode 100644
index 000000000..8241aeffb
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-metadata/hooks/useFilterOutUnexistingObjectMetadataItems.ts
@@ -0,0 +1,22 @@
+import { useRecoilValue } from 'recoil';
+
+import { objectMetadataItemsByNameSingularMapSelector } from '@/object-metadata/states/objectMetadataItemsByNameSingularMapSelector';
+import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
+import { isDefined } from '~/utils/isDefined';
+
+export const useFilterOutUnexistingObjectMetadataItems = () => {
+ const objectMetadataItemsByNameSingularMap = useRecoilValue(
+ objectMetadataItemsByNameSingularMapSelector,
+ );
+
+ const filterOutUnexistingObjectMetadataItems = (
+ objectMetadatItem: ObjectMetadataItem,
+ ) =>
+ isDefined(
+ objectMetadataItemsByNameSingularMap.get(objectMetadatItem.nameSingular),
+ );
+
+ return {
+ filterOutUnexistingObjectMetadataItems,
+ };
+};
diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useMapToObjectRecordIdentifier.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useMapToObjectRecordIdentifier.ts
index 35a1c6d4e..53fcaec74 100644
--- a/packages/twenty-front/src/modules/object-metadata/hooks/useMapToObjectRecordIdentifier.ts
+++ b/packages/twenty-front/src/modules/object-metadata/hooks/useMapToObjectRecordIdentifier.ts
@@ -1,16 +1,16 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { getObjectRecordIdentifier } from '@/object-metadata/utils/getObjectRecordIdentifier';
+import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { ObjectRecordIdentifier } from '@/object-record/types/ObjectRecordIdentifier';
export const useMapToObjectRecordIdentifier = ({
objectMetadataItem,
}: {
objectMetadataItem: ObjectMetadataItem;
-}) => {
- return (record: any): ObjectRecordIdentifier => {
- return getObjectRecordIdentifier({
+}): ((record: ObjectRecord) => ObjectRecordIdentifier) => {
+ return (record: ObjectRecord) =>
+ getObjectRecordIdentifier({
objectMetadataItem,
record,
});
- };
};
diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItem.ts
index fa35ff9d8..6baed042b 100644
--- a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItem.ts
+++ b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItem.ts
@@ -7,6 +7,7 @@ import { useGetObjectOrderByField } from '@/object-metadata/hooks/useGetObjectOr
import { useMapToObjectRecordIdentifier } from '@/object-metadata/hooks/useMapToObjectRecordIdentifier';
import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
+import { getBasePathToShowPage } from '@/object-metadata/utils/getBasePathToShowPage';
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
import { useGenerateCreateManyRecordMutation } from '@/object-record/hooks/useGenerateCreateManyRecordMutation';
import { useGenerateCreateOneRecordMutation } from '@/object-record/hooks/useGenerateCreateOneRecordMutation';
@@ -121,7 +122,9 @@ export const useObjectMetadataItem = (
({ name }) => name === 'name',
);
- const basePathToShowPage = `/object/${objectMetadataItem.nameSingular}/`;
+ const basePathToShowPage = getBasePathToShowPage({
+ objectMetadataItem,
+ });
return {
labelIdentifierFieldMetadata,
diff --git a/packages/twenty-front/src/modules/object-metadata/states/objectMetadataItemsByNamePluralMapSelector.ts b/packages/twenty-front/src/modules/object-metadata/states/objectMetadataItemsByNamePluralMapSelector.ts
new file mode 100644
index 000000000..30dfb115e
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-metadata/states/objectMetadataItemsByNamePluralMapSelector.ts
@@ -0,0 +1,20 @@
+import { selector } from 'recoil';
+
+import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
+import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
+
+export const objectMetadataItemsByNamePluralMapSelector = selector<
+ Map
+>({
+ key: 'objectMetadataItemsByNamePluralMapSelector',
+ get: ({ get }) => {
+ const objectMetadataItems = get(objectMetadataItemsState);
+
+ return new Map(
+ objectMetadataItems.map((objectMetadataItem) => [
+ objectMetadataItem.namePlural,
+ objectMetadataItem,
+ ]),
+ );
+ },
+});
diff --git a/packages/twenty-front/src/modules/object-metadata/states/objectMetadataItemsByNameSingularMapSelector.ts b/packages/twenty-front/src/modules/object-metadata/states/objectMetadataItemsByNameSingularMapSelector.ts
new file mode 100644
index 000000000..2eddad42b
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-metadata/states/objectMetadataItemsByNameSingularMapSelector.ts
@@ -0,0 +1,20 @@
+import { selector } from 'recoil';
+
+import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
+import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
+
+export const objectMetadataItemsByNameSingularMapSelector = selector<
+ Map
+>({
+ key: 'objectMetadataItemsByNameSingularMapSelector',
+ get: ({ get }) => {
+ const objectMetadataItems = get(objectMetadataItemsState);
+
+ return new Map(
+ objectMetadataItems.map((objectMetadataItem) => [
+ objectMetadataItem.nameSingular,
+ objectMetadataItem,
+ ]),
+ );
+ },
+});
diff --git a/packages/twenty-front/src/modules/object-metadata/utils/getBasePathToShowPage.ts b/packages/twenty-front/src/modules/object-metadata/utils/getBasePathToShowPage.ts
new file mode 100644
index 000000000..7a42595ac
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-metadata/utils/getBasePathToShowPage.ts
@@ -0,0 +1,11 @@
+import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
+
+export const getBasePathToShowPage = ({
+ objectMetadataItem,
+}: {
+ objectMetadataItem: ObjectMetadataItem;
+}) => {
+ const basePathToShowPage = `/object/${objectMetadataItem.nameSingular}/`;
+
+ return basePathToShowPage;
+};
diff --git a/packages/twenty-front/src/modules/object-metadata/utils/getLabelIdentifierFieldMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/utils/getLabelIdentifierFieldMetadataItem.ts
new file mode 100644
index 000000000..98fc51134
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-metadata/utils/getLabelIdentifierFieldMetadataItem.ts
@@ -0,0 +1,12 @@
+import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
+import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
+
+export const getLabelIdentifierFieldMetadataItem = (
+ objectMetadataItem: ObjectMetadataItem,
+): FieldMetadataItem | undefined => {
+ return objectMetadataItem.fields.find(
+ (field) =>
+ field.id === objectMetadataItem.labelIdentifierFieldMetadataId ||
+ field.name === 'name',
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-metadata/utils/getObjectOrderByField.ts b/packages/twenty-front/src/modules/object-metadata/utils/getObjectOrderByField.ts
index 7164656c0..8fe573bb2 100644
--- a/packages/twenty-front/src/modules/object-metadata/utils/getObjectOrderByField.ts
+++ b/packages/twenty-front/src/modules/object-metadata/utils/getObjectOrderByField.ts
@@ -5,7 +5,7 @@ import { FieldMetadataType } from '~/generated-metadata/graphql';
export const getObjectOrderByField = (
objectMetadataItem: ObjectMetadataItem,
- orderBy: OrderBy,
+ orderBy?: OrderBy | null,
): OrderByField => {
const labelIdentifierFieldMetadata = objectMetadataItem.fields.find(
(field) =>
@@ -18,18 +18,18 @@ export const getObjectOrderByField = (
case FieldMetadataType.FullName:
return {
[labelIdentifierFieldMetadata.name]: {
- firstName: orderBy,
- lastName: orderBy,
+ firstName: orderBy ?? 'AscNullsLast',
+ lastName: orderBy ?? 'AscNullsLast',
},
};
default:
return {
- [labelIdentifierFieldMetadata.name]: orderBy,
+ [labelIdentifierFieldMetadata.name]: orderBy ?? 'AscNullsLast',
};
}
} else {
return {
- createdAt: orderBy,
+ createdAt: orderBy ?? 'DescNullsLast',
};
}
};
diff --git a/packages/twenty-front/src/modules/object-metadata/utils/getObjectRecordIdentifier.ts b/packages/twenty-front/src/modules/object-metadata/utils/getObjectRecordIdentifier.ts
index e42a9de52..f706d9e63 100644
--- a/packages/twenty-front/src/modules/object-metadata/utils/getObjectRecordIdentifier.ts
+++ b/packages/twenty-front/src/modules/object-metadata/utils/getObjectRecordIdentifier.ts
@@ -1,7 +1,10 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
+import { getBasePathToShowPage } from '@/object-metadata/utils/getBasePathToShowPage';
+import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem';
+import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { ObjectRecordIdentifier } from '@/object-record/types/ObjectRecordIdentifier';
-import { FieldMetadataType } from '~/generated/graphql';
+import { FieldMetadataType } from '~/generated-metadata/graphql';
import { getLogoUrlFromDomainName } from '~/utils';
export const getObjectRecordIdentifier = ({
@@ -9,30 +12,24 @@ export const getObjectRecordIdentifier = ({
record,
}: {
objectMetadataItem: ObjectMetadataItem;
- record: any;
+ record: ObjectRecord;
}): ObjectRecordIdentifier => {
- const basePathToShowPage = `/object/${objectMetadataItem.nameSingular}/`;
- const linkToShowPage = `${basePathToShowPage}${record.id}`;
-
- if (objectMetadataItem.nameSingular === CoreObjectNameSingular.Opportunity) {
- return {
- id: record.id,
- name: record?.company?.name,
- avatarUrl: record.avatarUrl,
- avatarType: 'rounded',
- linkToShowPage,
- };
+ switch (objectMetadataItem.nameSingular) {
+ case CoreObjectNameSingular.Opportunity:
+ return {
+ id: record.id,
+ name: record?.company?.name,
+ avatarUrl: record.avatarUrl,
+ avatarType: 'rounded',
+ };
}
- const labelIdentifierFieldMetadata = objectMetadataItem.fields.find(
- (field) =>
- field.id === objectMetadataItem.labelIdentifierFieldMetadataId ||
- field.name === 'name',
- );
+ const labelIdentifierFieldMetadataItem =
+ getLabelIdentifierFieldMetadataItem(objectMetadataItem);
let labelIdentifierFieldValue = '';
- switch (labelIdentifierFieldMetadata?.type) {
+ switch (labelIdentifierFieldMetadataItem?.type) {
case FieldMetadataType.FullName: {
labelIdentifierFieldValue = `${record.name?.firstName ?? ''} ${
record.name?.lastName ?? ''
@@ -40,8 +37,8 @@ export const getObjectRecordIdentifier = ({
break;
}
default:
- labelIdentifierFieldValue = labelIdentifierFieldMetadata
- ? record[labelIdentifierFieldMetadata.name]
+ labelIdentifierFieldValue = labelIdentifierFieldMetadataItem
+ ? record[labelIdentifierFieldMetadataItem.name]
: '';
}
@@ -63,11 +60,17 @@ export const getObjectRecordIdentifier = ({
? getLogoUrlFromDomainName(record['domainName'] ?? '')
: imageIdentifierFieldValue ?? null;
+ const basePathToShowPage = getBasePathToShowPage({
+ objectMetadataItem,
+ });
+
+ const linkToEntity = `${basePathToShowPage}${record.id}`;
+
return {
id: record.id,
name: labelIdentifierFieldValue,
avatarUrl,
avatarType,
- linkToShowPage,
+ linkToEntity,
};
};
diff --git a/packages/twenty-front/src/modules/object-record/components/RecordChip.tsx b/packages/twenty-front/src/modules/object-record/components/RecordChip.tsx
new file mode 100644
index 000000000..aaad350f7
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/components/RecordChip.tsx
@@ -0,0 +1,28 @@
+import * as React from 'react';
+
+import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
+import { ObjectRecord } from '@/object-record/types/ObjectRecord';
+import { EntityChip } from '@/ui/display/chip/components/EntityChip';
+
+export type RecordChipProps = {
+ objectNameSingular: string;
+ record: ObjectRecord;
+};
+
+export const RecordChip = ({ objectNameSingular, record }: RecordChipProps) => {
+ const { mapToObjectRecordIdentifier } = useObjectMetadataItem({
+ objectNameSingular,
+ });
+
+ const objectRecordIdentifier = mapToObjectRecordIdentifier(record);
+
+ return (
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-record/components/RecordShowPage.tsx b/packages/twenty-front/src/modules/object-record/components/RecordShowPage.tsx
index 5452b4e63..2ac72896e 100644
--- a/packages/twenty-front/src/modules/object-record/components/RecordShowPage.tsx
+++ b/packages/twenty-front/src/modules/object-record/components/RecordShowPage.tsx
@@ -77,13 +77,6 @@ export const RecordShowPage = () => {
objectNameSingular,
});
- const objectMetadataType =
- objectMetadataItem?.nameSingular === 'company'
- ? 'Company'
- : objectMetadataItem?.nameSingular === 'person'
- ? 'Person'
- : 'Custom';
-
const useUpdateOneObjectRecordMutation: RecordUpdateHook = () => {
const updateEntity = ({ variables }: RecordUpdateHookParams) => {
updateOneRecord?.({
@@ -171,7 +164,7 @@ export const RecordShowPage = () => {
hasBackButton
Icon={IconBuildingSkyscraper}
>
- {record && objectMetadataType !== 'Custom' && (
+ {record && (
<>
{
key="add"
entity={{
id: record.id,
- type: objectMetadataType,
+ targetObjectNameSingular: objectMetadataItem?.nameSingular,
}}
/>
{
)}
{
basePathToShowPage,
} = useChipField();
+ // TODO: remove this and use ObjectRecordChip instead
const identifiers = identifiersMapper?.(record, objectNameSingular ?? '');
return (
diff --git a/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecords.ts
index 4bb189787..f8e7a77d5 100644
--- a/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecords.ts
+++ b/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecords.ts
@@ -5,11 +5,10 @@ import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimis
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
import { useGenerateEmptyRecord } from '@/object-record/hooks/useGenerateEmptyRecord';
+import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { capitalize } from '~/utils/string/capitalize';
-export const useCreateManyRecords = <
- T extends Record & { id: string },
->({
+export const useCreateManyRecords = ({
objectNameSingular,
}: ObjectMetadataItemIdentifier) => {
const { triggerOptimisticEffects } = useOptimisticEffect({
@@ -27,17 +26,16 @@ export const useCreateManyRecords = <
const apolloClient = useApolloClient();
- const createManyRecords = async (data: Record[]) => {
+ const createManyRecords = async (data: Partial[]) => {
const withIds = data.map((record) => ({
...record,
id: (record.id as string) ?? v4(),
}));
withIds.forEach((record) => {
- const emptyRecord: Record | undefined =
- generateEmptyRecord({
- id: record.id,
- });
+ const emptyRecord: T | undefined = generateEmptyRecord({
+ id: record.id,
+ } as T);
if (emptyRecord) {
triggerOptimisticEffects({
diff --git a/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecord.ts
index 141c1cff1..a2cc0d9d7 100644
--- a/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecord.ts
+++ b/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecord.ts
@@ -34,7 +34,7 @@ export const useCreateOneRecord = ({
const createOneRecord = async (input: Record) => {
const recordId = v4();
- const generatedEmptyRecord = generateEmptyRecord>({
+ const generatedEmptyRecord = generateEmptyRecord({
id: recordId,
createdAt: new Date().toISOString(),
...input,
diff --git a/packages/twenty-front/src/modules/object-record/hooks/useGenerateEmptyRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/useGenerateEmptyRecord.ts
index 1c04168ea..df33935b4 100644
--- a/packages/twenty-front/src/modules/object-record/hooks/useGenerateEmptyRecord.ts
+++ b/packages/twenty-front/src/modules/object-record/hooks/useGenerateEmptyRecord.ts
@@ -1,4 +1,5 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
+import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { generateEmptyFieldValue } from '@/object-record/utils/generateEmptyFieldValue';
export const useGenerateEmptyRecord = ({
@@ -7,11 +8,11 @@ export const useGenerateEmptyRecord = ({
objectMetadataItem: ObjectMetadataItem;
}) => {
// Todo fix typing once we generate the return base on Metadata
- const generateEmptyRecord = (input: Partial & { id: string }) => {
+ const generateEmptyRecord = (input: T) => {
// Todo replace this by runtime typing
- const validatedInput = input as { id: string } & { [key: string]: any };
+ const validatedInput = input as T;
- const emptyRecord = {} as Record;
+ const emptyRecord = {} as any;
for (const fieldMetadataItem of objectMetadataItem.fields) {
emptyRecord[fieldMetadataItem.name] =
@@ -19,7 +20,7 @@ export const useGenerateEmptyRecord = ({
generateEmptyFieldValue(fieldMetadataItem);
}
- return emptyRecord;
+ return emptyRecord as T;
};
return {
diff --git a/packages/twenty-front/src/modules/object-record/hooks/useGenerateFindManyRecordsForMultipleMetadataItemsQuery.ts b/packages/twenty-front/src/modules/object-record/hooks/useGenerateFindManyRecordsForMultipleMetadataItemsQuery.ts
new file mode 100644
index 000000000..2c8de3258
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/hooks/useGenerateFindManyRecordsForMultipleMetadataItemsQuery.ts
@@ -0,0 +1,84 @@
+import { gql } from '@apollo/client';
+
+import { useMapFieldMetadataToGraphQLQuery } from '@/object-metadata/hooks/useMapFieldMetadataToGraphQLQuery';
+import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
+import { capitalize } from '~/utils/string/capitalize';
+
+export const useGenerateFindManyRecordsForMultipleMetadataItemsQuery = ({
+ objectMetadataItems,
+ depth,
+}: {
+ objectMetadataItems: ObjectMetadataItem[];
+ depth?: number;
+}) => {
+ const mapFieldMetadataToGraphQLQuery = useMapFieldMetadataToGraphQLQuery();
+
+ const capitalizedObjectNameSingulars = objectMetadataItems.map(
+ ({ nameSingular }) => capitalize(nameSingular),
+ );
+
+ const filterPerMetadataItemArray = capitalizedObjectNameSingulars
+ .map(
+ (capitalizedObjectNameSingular) =>
+ `$filter${capitalizedObjectNameSingular}: ${capitalizedObjectNameSingular}FilterInput`,
+ )
+ .join(', ');
+
+ const orderByPerMetadataItemArray = capitalizedObjectNameSingulars
+ .map(
+ (capitalizedObjectNameSingular) =>
+ `$orderBy${capitalizedObjectNameSingular}: ${capitalizedObjectNameSingular}OrderByInput`,
+ )
+ .join(', ');
+
+ const lastCursorPerMetadataItemArray = capitalizedObjectNameSingulars
+ .map(
+ (capitalizedObjectNameSingular) =>
+ `$lastCursor${capitalizedObjectNameSingular}: String`,
+ )
+ .join(', ');
+
+ const limitPerMetadataItemArray = capitalizedObjectNameSingulars
+ .map(
+ (capitalizedObjectNameSingular) =>
+ `$limit${capitalizedObjectNameSingular}: Float = 5`,
+ )
+ .join(', ');
+
+ return gql`
+ query FindManyRecordsMultipleMetadataItems(
+ ${filterPerMetadataItemArray},
+ ${orderByPerMetadataItemArray},
+ ${lastCursorPerMetadataItemArray},
+ ${limitPerMetadataItemArray}
+ ) {
+ ${objectMetadataItems
+ .map(
+ ({ namePlural, nameSingular, fields }) =>
+ `${namePlural}(filter: $filter${capitalize(
+ nameSingular,
+ )}, orderBy: $orderBy${capitalize(
+ nameSingular,
+ )}, first: $limit${capitalize(
+ nameSingular,
+ )}, after: $lastCursor${capitalize(nameSingular)}){
+ edges {
+ node {
+ id
+ ${fields
+ .map((field) => mapFieldMetadataToGraphQLQuery(field, depth))
+ .join('\n')}
+ }
+ cursor
+ }
+ pageInfo {
+ hasNextPage
+ startCursor
+ endCursor
+ }
+ }`,
+ )
+ .join('\n')}
+ }
+ `;
+};
diff --git a/packages/twenty-front/src/modules/object-record/hooks/useGenerateFindManyRecordsQuery.ts b/packages/twenty-front/src/modules/object-record/hooks/useGenerateFindManyRecordsQuery.ts
index c597506ad..d726f45e2 100644
--- a/packages/twenty-front/src/modules/object-record/hooks/useGenerateFindManyRecordsQuery.ts
+++ b/packages/twenty-front/src/modules/object-record/hooks/useGenerateFindManyRecordsQuery.ts
@@ -1,7 +1,6 @@
import { gql } from '@apollo/client';
import { useMapFieldMetadataToGraphQLQuery } from '@/object-metadata/hooks/useMapFieldMetadataToGraphQLQuery';
-import { EMPTY_QUERY } from '@/object-metadata/hooks/useObjectMetadataItem';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { capitalize } from '~/utils/string/capitalize';
@@ -14,10 +13,6 @@ export const useGenerateFindManyRecordsQuery = ({
}) => {
const mapFieldMetadataToGraphQLQuery = useMapFieldMetadataToGraphQLQuery();
- if (!objectMetadataItem) {
- return EMPTY_QUERY;
- }
-
return gql`
query FindMany${capitalize(
objectMetadataItem.namePlural,
diff --git a/packages/twenty-front/src/modules/object-record/hooks/useRecordTableContextMenuEntries.tsx b/packages/twenty-front/src/modules/object-record/hooks/useRecordTableContextMenuEntries.tsx
index 70737f1f8..9fa8a8966 100644
--- a/packages/twenty-front/src/modules/object-record/hooks/useRecordTableContextMenuEntries.tsx
+++ b/packages/twenty-front/src/modules/object-record/hooks/useRecordTableContextMenuEntries.tsx
@@ -67,13 +67,6 @@ export const useRecordTableContextMenuEntries = (
const { createFavorite, favorites, deleteFavorite } = useFavorites();
- const objectMetadataType =
- objectNameSingular === 'company'
- ? 'Company'
- : objectNameSingular === 'person'
- ? 'Person'
- : 'Custom';
-
const handleFavoriteButtonClick = useRecoilCallback(({ snapshot }) => () => {
const selectedRowIds = injectSelectorSnapshotValueWithRecordTableScopeId(
snapshot,
@@ -212,14 +205,14 @@ export const useRecordTableContextMenuEntries = (
label: 'Task',
Icon: IconCheckbox,
onClick: () => {
- openCreateActivityDrawer('Task', objectMetadataType);
+ openCreateActivityDrawer('Task', objectNameSingular);
},
},
{
label: 'Note',
Icon: IconNotes,
onClick: () => {
- openCreateActivityDrawer('Note', objectMetadataType);
+ openCreateActivityDrawer('Note', objectNameSingular);
},
},
...(dataExecuteQuickActionOnmentEnabled
diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/components/MultipleObjectRecordSelect.tsx b/packages/twenty-front/src/modules/object-record/relation-picker/components/MultipleObjectRecordSelect.tsx
new file mode 100644
index 000000000..03cb9107b
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/relation-picker/components/MultipleObjectRecordSelect.tsx
@@ -0,0 +1,212 @@
+import { useEffect, useMemo, useRef, useState } from 'react';
+import styled from '@emotion/styled';
+import { isNonEmptyString } from '@sniptt/guards';
+import debounce from 'lodash.debounce';
+import { v4 } from 'uuid';
+
+import {
+ ObjectRecordForSelect,
+ SelectedObjectRecordId,
+ useMultiObjectSearch,
+} from '@/object-record/relation-picker/hooks/useMultiObjectSearch';
+import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
+import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
+import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
+import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
+import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
+import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
+import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
+import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
+import { MenuItemMultiSelectAvatar } from '@/ui/navigation/menu-item/components/MenuItemMultiSelectAvatar';
+import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
+import { Avatar } from '@/users/components/Avatar';
+
+export const StyledSelectableItem = styled(SelectableItem)`
+ height: 100%;
+ width: 100%;
+`;
+
+export type EntitiesForMultipleObjectRecordSelect = {
+ filteredSelectedObjectRecords: ObjectRecordForSelect[];
+ objectRecordsToSelect: ObjectRecordForSelect[];
+ loading: boolean;
+};
+
+export const MultipleObjectRecordSelect = ({
+ onChange,
+ onSubmit,
+ selectedObjectRecordIds,
+}: {
+ onChange?: (
+ changedRecordForSelect: ObjectRecordForSelect,
+ newSelectedValue: boolean,
+ ) => void;
+ onCancel?: (objectRecordsForSelect: ObjectRecordForSelect[]) => void;
+ onSubmit?: (objectRecordsForSelect: ObjectRecordForSelect[]) => void;
+ selectedObjectRecordIds: SelectedObjectRecordId[];
+}) => {
+ const containerRef = useRef(null);
+
+ const [searchFilter, setSearchFilter] = useState('');
+
+ const {
+ filteredSelectedObjectRecords,
+ loading,
+ objectRecordsToSelect,
+ selectedObjectRecords,
+ } = useMultiObjectSearch({
+ searchFilterValue: searchFilter,
+ selectedObjectRecordIds,
+ excludedObjectRecordIds: [],
+ limit: 10,
+ });
+
+ const selectedObjectRecordsForSelect = useMemo(
+ () =>
+ selectedObjectRecords.filter((selectedObjectRecord) =>
+ selectedObjectRecordIds.some(
+ (selectedObjectRecordId) =>
+ selectedObjectRecordId.id ===
+ selectedObjectRecord.recordIdentifier.id,
+ ),
+ ),
+ [selectedObjectRecords, selectedObjectRecordIds],
+ );
+
+ const [internalSelectedRecords, setInternalSelectedRecords] = useState<
+ ObjectRecordForSelect[]
+ >([]);
+
+ useEffect(() => {
+ if (!loading) {
+ setInternalSelectedRecords(selectedObjectRecordsForSelect);
+ }
+ }, [selectedObjectRecordsForSelect, loading]);
+
+ const debouncedSetSearchFilter = debounce(setSearchFilter, 100, {
+ leading: true,
+ });
+
+ const handleFilterChange = (event: React.ChangeEvent) => {
+ debouncedSetSearchFilter(event.currentTarget.value);
+ };
+
+ const handleSelectChange = (
+ changedRecordForSelect: ObjectRecordForSelect,
+ newSelectedValue: boolean,
+ ) => {
+ const newSelectedRecords = newSelectedValue
+ ? [...internalSelectedRecords, changedRecordForSelect]
+ : internalSelectedRecords.filter(
+ (selectedRecord) =>
+ selectedRecord.record.id !== changedRecordForSelect.record.id,
+ );
+
+ setInternalSelectedRecords(newSelectedRecords);
+
+ onChange?.(changedRecordForSelect, newSelectedValue);
+ };
+
+ const entitiesInDropdown = useMemo(
+ () =>
+ [
+ ...(filteredSelectedObjectRecords ?? []),
+ ...(objectRecordsToSelect ?? []),
+ ].filter((entity) => isNonEmptyString(entity.recordIdentifier.id)),
+ [filteredSelectedObjectRecords, objectRecordsToSelect],
+ );
+
+ useListenClickOutside({
+ refs: [containerRef],
+ callback: (event) => {
+ event.stopImmediatePropagation();
+ event.stopPropagation();
+ event.preventDefault();
+
+ onSubmit?.(internalSelectedRecords);
+ },
+ });
+
+ const selectableItemIds = entitiesInDropdown.map(
+ (entity) => entity.record.id,
+ );
+
+ return (
+
+
+
+
+ {loading ? (
+
+ ) : (
+ <>
+ {
+ const recordIsSelected = internalSelectedRecords?.some(
+ (selectedRecord) => selectedRecord.record.id === recordId,
+ );
+
+ const correspondingRecordForSelect = entitiesInDropdown?.find(
+ (entity) => entity.record.id === recordId,
+ );
+
+ if (correspondingRecordForSelect) {
+ handleSelectChange(
+ correspondingRecordForSelect,
+ !recordIsSelected,
+ );
+ }
+ }}
+ >
+ {entitiesInDropdown?.map((objectRecordForSelect) => (
+
+ {
+ return (
+ selectedRecord.record.id ===
+ objectRecordForSelect.record.id
+ );
+ },
+ )}
+ onSelectChange={(newCheckedValue) =>
+ handleSelectChange(objectRecordForSelect, newCheckedValue)
+ }
+ avatar={
+
+ }
+ text={objectRecordForSelect.recordIdentifier.name}
+ />
+
+ ))}
+
+ {entitiesInDropdown?.length === 0 && }
+ >
+ )}
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useLimitPerMetadataItem.ts b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useLimitPerMetadataItem.ts
new file mode 100644
index 000000000..1f1107ea4
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useLimitPerMetadataItem.ts
@@ -0,0 +1,24 @@
+import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
+import { DEFAULT_SEARCH_REQUEST_LIMIT } from '@/search/hooks/useFilteredSearchEntityQuery';
+import { isDefined } from '~/utils/isDefined';
+import { capitalize } from '~/utils/string/capitalize';
+
+export const useLimitPerMetadataItem = ({
+ objectMetadataItems,
+ limit = DEFAULT_SEARCH_REQUEST_LIMIT,
+}: {
+ objectMetadataItems: ObjectMetadataItem[];
+ limit?: number;
+}) => {
+ const limitPerMetadataItem = Object.fromEntries(
+ objectMetadataItems
+ .map(({ nameSingular }) => {
+ return [`limit${capitalize(nameSingular)}`, limit];
+ })
+ .filter(isDefined),
+ );
+
+ return {
+ limitPerMetadataItem,
+ };
+};
diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray.ts b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray.ts
new file mode 100644
index 000000000..eedf25327
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray.ts
@@ -0,0 +1,50 @@
+import { useMemo } from 'react';
+import { useRecoilValue } from 'recoil';
+
+import { objectMetadataItemsByNamePluralMapSelector } from '@/object-metadata/states/objectMetadataItemsByNamePluralMapSelector';
+import { getObjectRecordIdentifier } from '@/object-metadata/utils/getObjectRecordIdentifier';
+import { ObjectRecordForSelect } from '@/object-record/relation-picker/hooks/useMultiObjectSearch';
+import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
+import { isDefined } from '~/utils/isDefined';
+
+export type MultiObjectRecordQueryResult = {
+ [namePlural: string]: ObjectRecordConnection;
+};
+
+export const useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray =
+ ({
+ multiObjectRecordsQueryResult,
+ }: {
+ multiObjectRecordsQueryResult:
+ | MultiObjectRecordQueryResult
+ | null
+ | undefined;
+ }) => {
+ const objectMetadataItemsByNamePluralMap = useRecoilValue(
+ objectMetadataItemsByNamePluralMapSelector,
+ );
+
+ const objectRecordForSelectArray = useMemo(() => {
+ return Object.entries(multiObjectRecordsQueryResult ?? {}).flatMap(
+ ([namePlural, objectRecordConnection]) => {
+ const objectMetadataItem =
+ objectMetadataItemsByNamePluralMap.get(namePlural);
+
+ if (!isDefined(objectMetadataItem)) return [];
+
+ return objectRecordConnection.edges.map(({ node }) => ({
+ objectMetadataItem,
+ record: node,
+ recordIdentifier: getObjectRecordIdentifier({
+ objectMetadataItem,
+ record: node,
+ }),
+ })) as ObjectRecordForSelect[];
+ },
+ );
+ }, [multiObjectRecordsQueryResult, objectMetadataItemsByNamePluralMap]);
+
+ return {
+ objectRecordForSelectArray,
+ };
+ };
diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearch.ts b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearch.ts
new file mode 100644
index 000000000..01c740433
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearch.ts
@@ -0,0 +1,72 @@
+import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
+import { useMultiObjectSearchMatchesSearchFilterAndSelectedItemsQuery } from '@/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterAndSelectedItemsQuery';
+import { useMultiObjectSearchMatchesSearchFilterAndToSelectQuery } from '@/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterAndToSelectQuery';
+import { useMultiObjectSearchSelectedItemsQuery } from '@/object-record/relation-picker/hooks/useMultiObjectSearchSelectedItemsQuery';
+import { ObjectRecord } from '@/object-record/types/ObjectRecord';
+import { ObjectRecordIdentifier } from '@/object-record/types/ObjectRecordIdentifier';
+
+export const DEFAULT_SEARCH_REQUEST_LIMIT = 5;
+
+export type ObjectRecordForSelect = {
+ objectMetadataItem: ObjectMetadataItem;
+ record: ObjectRecord;
+ recordIdentifier: ObjectRecordIdentifier;
+};
+
+export type SelectedObjectRecordId = {
+ objectNameSingular: string;
+ id: string;
+};
+
+export type MultiObjectSearch = {
+ selectedObjectRecords: ObjectRecordForSelect[];
+ filteredSelectedObjectRecords: ObjectRecordForSelect[];
+ objectRecordsToSelect: ObjectRecordForSelect[];
+ loading: boolean;
+};
+
+export const useMultiObjectSearch = ({
+ searchFilterValue,
+ selectedObjectRecordIds,
+ limit,
+ excludedObjectRecordIds = [],
+}: {
+ searchFilterValue: string;
+ selectedObjectRecordIds: SelectedObjectRecordId[];
+ limit?: number;
+ excludedObjectRecordIds?: SelectedObjectRecordId[];
+}): MultiObjectSearch => {
+ const { selectedObjectRecords, selectedObjectRecordsLoading } =
+ useMultiObjectSearchSelectedItemsQuery({
+ selectedObjectRecordIds,
+ });
+
+ const {
+ selectedAndMatchesSearchFilterObjectRecords,
+ selectedAndMatchesSearchFilterObjectRecordsLoading,
+ } = useMultiObjectSearchMatchesSearchFilterAndSelectedItemsQuery({
+ searchFilterValue,
+ selectedObjectRecordIds,
+ limit,
+ });
+
+ const {
+ toSelectAndMatchesSearchFilterObjectRecords,
+ toSelectAndMatchesSearchFilterObjectRecordsLoading,
+ } = useMultiObjectSearchMatchesSearchFilterAndToSelectQuery({
+ excludedObjectRecordIds,
+ searchFilterValue,
+ selectedObjectRecordIds,
+ limit,
+ });
+
+ return {
+ selectedObjectRecords,
+ filteredSelectedObjectRecords: selectedAndMatchesSearchFilterObjectRecords,
+ objectRecordsToSelect: toSelectAndMatchesSearchFilterObjectRecords,
+ loading:
+ selectedAndMatchesSearchFilterObjectRecordsLoading ||
+ toSelectAndMatchesSearchFilterObjectRecordsLoading ||
+ selectedObjectRecordsLoading,
+ };
+};
diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterAndSelectedItemsQuery.ts b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterAndSelectedItemsQuery.ts
new file mode 100644
index 000000000..a3b5ac363
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterAndSelectedItemsQuery.ts
@@ -0,0 +1,113 @@
+import { useQuery } from '@apollo/client';
+import { isNonEmptyArray } from '@sniptt/guards';
+import { useRecoilValue } from 'recoil';
+
+import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
+import { useGenerateFindManyRecordsForMultipleMetadataItemsQuery } from '@/object-record/hooks/useGenerateFindManyRecordsForMultipleMetadataItemsQuery';
+import { useLimitPerMetadataItem } from '@/object-record/relation-picker/hooks/useLimitPerMetadataItem';
+import {
+ MultiObjectRecordQueryResult,
+ useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray,
+} from '@/object-record/relation-picker/hooks/useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray';
+import { SelectedObjectRecordId } from '@/object-record/relation-picker/hooks/useMultiObjectSearch';
+import { useOrderByFieldPerMetadataItem } from '@/object-record/relation-picker/hooks/useOrderByFieldPerMetadataItem';
+import { useSearchFilterPerMetadataItem } from '@/object-record/relation-picker/hooks/useSearchFilterPerMetadataItem';
+import { isDefined } from '~/utils/isDefined';
+import { capitalize } from '~/utils/string/capitalize';
+
+export const useMultiObjectSearchMatchesSearchFilterAndSelectedItemsQuery = ({
+ selectedObjectRecordIds,
+ searchFilterValue,
+ limit,
+}: {
+ selectedObjectRecordIds: SelectedObjectRecordId[];
+ searchFilterValue: string;
+ limit?: number;
+}) => {
+ const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
+
+ const { searchFilterPerMetadataItemNameSingular } =
+ useSearchFilterPerMetadataItem({
+ objectMetadataItems,
+ searchFilterValue,
+ });
+
+ const objectMetadataItemsUsedInSelectedIdsQuery = objectMetadataItems.filter(
+ ({ nameSingular }) => {
+ return selectedObjectRecordIds.some(({ objectNameSingular }) => {
+ return objectNameSingular === nameSingular;
+ });
+ },
+ );
+
+ const selectedAndMatchesSearchFilterTextFilterPerMetadataItem =
+ Object.fromEntries(
+ objectMetadataItems
+ .map(({ nameSingular }) => {
+ const selectedIds = selectedObjectRecordIds
+ .filter(
+ ({ objectNameSingular }) => objectNameSingular === nameSingular,
+ )
+ .map(({ id }) => id);
+
+ if (!isNonEmptyArray(selectedIds)) return null;
+
+ const searchFilter =
+ searchFilterPerMetadataItemNameSingular[nameSingular] ?? {};
+
+ return [
+ `filter${capitalize(nameSingular)}`,
+ {
+ and: [
+ {
+ ...searchFilter,
+ },
+ {
+ id: {
+ in: selectedIds,
+ },
+ },
+ ],
+ },
+ ];
+ })
+ .filter(isDefined),
+ );
+
+ const { orderByFieldPerMetadataItem } = useOrderByFieldPerMetadataItem({
+ objectMetadataItems: objectMetadataItemsUsedInSelectedIdsQuery,
+ });
+
+ const { limitPerMetadataItem } = useLimitPerMetadataItem({
+ objectMetadataItems: objectMetadataItemsUsedInSelectedIdsQuery,
+ limit,
+ });
+
+ const multiSelectQueryForSelectedIds =
+ useGenerateFindManyRecordsForMultipleMetadataItemsQuery({
+ objectMetadataItems: objectMetadataItemsUsedInSelectedIdsQuery,
+ });
+
+ const {
+ loading: selectedAndMatchesSearchFilterObjectRecordsLoading,
+ data: selectedAndMatchesSearchFilterObjectRecordsQueryResult,
+ } = useQuery(multiSelectQueryForSelectedIds, {
+ variables: {
+ ...selectedAndMatchesSearchFilterTextFilterPerMetadataItem,
+ ...orderByFieldPerMetadataItem,
+ ...limitPerMetadataItem,
+ },
+ });
+
+ const {
+ objectRecordForSelectArray: selectedAndMatchesSearchFilterObjectRecords,
+ } = useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray({
+ multiObjectRecordsQueryResult:
+ selectedAndMatchesSearchFilterObjectRecordsQueryResult,
+ });
+
+ return {
+ selectedAndMatchesSearchFilterObjectRecordsLoading,
+ selectedAndMatchesSearchFilterObjectRecords,
+ };
+};
diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterAndToSelectQuery.ts b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterAndToSelectQuery.ts
new file mode 100644
index 000000000..00894cd56
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterAndToSelectQuery.ts
@@ -0,0 +1,130 @@
+import { useQuery } from '@apollo/client';
+import { isNonEmptyArray } from '@sniptt/guards';
+import { useRecoilValue } from 'recoil';
+
+import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
+import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
+import { useGenerateFindManyRecordsForMultipleMetadataItemsQuery } from '@/object-record/hooks/useGenerateFindManyRecordsForMultipleMetadataItemsQuery';
+import { useLimitPerMetadataItem } from '@/object-record/relation-picker/hooks/useLimitPerMetadataItem';
+import {
+ MultiObjectRecordQueryResult,
+ useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray,
+} from '@/object-record/relation-picker/hooks/useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray';
+import { SelectedObjectRecordId } from '@/object-record/relation-picker/hooks/useMultiObjectSearch';
+import { useOrderByFieldPerMetadataItem } from '@/object-record/relation-picker/hooks/useOrderByFieldPerMetadataItem';
+import { useSearchFilterPerMetadataItem } from '@/object-record/relation-picker/hooks/useSearchFilterPerMetadataItem';
+import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
+import { isDefined } from '~/utils/isDefined';
+import { capitalize } from '~/utils/string/capitalize';
+
+export const useMultiObjectSearchMatchesSearchFilterAndToSelectQuery = ({
+ selectedObjectRecordIds,
+ excludedObjectRecordIds,
+ searchFilterValue,
+ limit,
+}: {
+ selectedObjectRecordIds: SelectedObjectRecordId[];
+ excludedObjectRecordIds: SelectedObjectRecordId[];
+ searchFilterValue: string;
+ limit?: number;
+}) => {
+ const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
+
+ const nonSystemObjectMetadataItems = objectMetadataItems.filter(
+ ({ nameSingular, isSystem }) =>
+ !isSystem && nameSingular !== CoreObjectNameSingular.Opportunity,
+ );
+
+ const { searchFilterPerMetadataItemNameSingular } =
+ useSearchFilterPerMetadataItem({
+ objectMetadataItems: nonSystemObjectMetadataItems,
+ searchFilterValue,
+ });
+
+ const objectRecordsToSelectAndMatchesSearchFilterTextFilterPerMetadataItem =
+ Object.fromEntries(
+ nonSystemObjectMetadataItems
+ .map(({ nameSingular }) => {
+ const selectedIds = selectedObjectRecordIds
+ .filter(
+ ({ objectNameSingular }) => objectNameSingular === nameSingular,
+ )
+ .map(({ id }) => id);
+
+ const excludedIds = excludedObjectRecordIds
+ .filter(
+ ({ objectNameSingular }) => objectNameSingular === nameSingular,
+ )
+ .map(({ id }) => id);
+
+ const searchFilter =
+ searchFilterPerMetadataItemNameSingular[nameSingular] ?? {};
+
+ const excludedIdsUnion = [...selectedIds, ...excludedIds];
+
+ const noFilter =
+ !isNonEmptyArray(excludedIdsUnion) &&
+ isDeeplyEqual(searchFilter, {});
+
+ return [
+ `filter${capitalize(nameSingular)}`,
+ !noFilter
+ ? {
+ and: [
+ {
+ ...searchFilter,
+ },
+ isNonEmptyArray(excludedIdsUnion)
+ ? {
+ not: {
+ id: {
+ in: [...selectedIds, ...excludedIds],
+ },
+ },
+ }
+ : {},
+ ],
+ }
+ : {},
+ ];
+ })
+ .filter(isDefined),
+ );
+
+ const { orderByFieldPerMetadataItem } = useOrderByFieldPerMetadataItem({
+ objectMetadataItems: nonSystemObjectMetadataItems,
+ });
+
+ const { limitPerMetadataItem } = useLimitPerMetadataItem({
+ objectMetadataItems: nonSystemObjectMetadataItems,
+ limit,
+ });
+
+ const multiSelectQuery =
+ useGenerateFindManyRecordsForMultipleMetadataItemsQuery({
+ objectMetadataItems: nonSystemObjectMetadataItems,
+ });
+
+ const {
+ loading: toSelectAndMatchesSearchFilterObjectRecordsLoading,
+ data: toSelectAndMatchesSearchFilterObjectRecordsQueryResult,
+ } = useQuery(multiSelectQuery, {
+ variables: {
+ ...objectRecordsToSelectAndMatchesSearchFilterTextFilterPerMetadataItem,
+ ...orderByFieldPerMetadataItem,
+ ...limitPerMetadataItem,
+ },
+ });
+
+ const {
+ objectRecordForSelectArray: toSelectAndMatchesSearchFilterObjectRecords,
+ } = useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray({
+ multiObjectRecordsQueryResult:
+ toSelectAndMatchesSearchFilterObjectRecordsQueryResult,
+ });
+
+ return {
+ toSelectAndMatchesSearchFilterObjectRecordsLoading,
+ toSelectAndMatchesSearchFilterObjectRecords,
+ };
+};
diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchSelectedItemsQuery.ts b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchSelectedItemsQuery.ts
new file mode 100644
index 000000000..8869c24b7
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchSelectedItemsQuery.ts
@@ -0,0 +1,88 @@
+import { useQuery } from '@apollo/client';
+import { isNonEmptyArray } from '@sniptt/guards';
+import { useRecoilValue } from 'recoil';
+
+import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
+import { useGenerateFindManyRecordsForMultipleMetadataItemsQuery } from '@/object-record/hooks/useGenerateFindManyRecordsForMultipleMetadataItemsQuery';
+import { useLimitPerMetadataItem } from '@/object-record/relation-picker/hooks/useLimitPerMetadataItem';
+import {
+ MultiObjectRecordQueryResult,
+ useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray,
+} from '@/object-record/relation-picker/hooks/useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray';
+import { SelectedObjectRecordId } from '@/object-record/relation-picker/hooks/useMultiObjectSearch';
+import { useOrderByFieldPerMetadataItem } from '@/object-record/relation-picker/hooks/useOrderByFieldPerMetadataItem';
+import { isDefined } from '~/utils/isDefined';
+import { capitalize } from '~/utils/string/capitalize';
+
+export const useMultiObjectSearchSelectedItemsQuery = ({
+ selectedObjectRecordIds,
+}: {
+ selectedObjectRecordIds: SelectedObjectRecordId[];
+}) => {
+ const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
+
+ const objectMetadataItemsUsedInSelectedIdsQuery = objectMetadataItems.filter(
+ ({ nameSingular }) => {
+ return selectedObjectRecordIds.some(({ objectNameSingular }) => {
+ return objectNameSingular === nameSingular;
+ });
+ },
+ );
+
+ const selectedIdFilterPerMetadataItem = Object.fromEntries(
+ objectMetadataItemsUsedInSelectedIdsQuery
+ .map(({ nameSingular }) => {
+ const selectedIds = selectedObjectRecordIds
+ .filter(
+ ({ objectNameSingular }) => objectNameSingular === nameSingular,
+ )
+ .map(({ id }) => id);
+
+ if (!isNonEmptyArray(selectedIds)) return null;
+
+ return [
+ `filter${capitalize(nameSingular)}`,
+ {
+ id: {
+ in: selectedIds,
+ },
+ },
+ ];
+ })
+ .filter(isDefined),
+ );
+
+ const { orderByFieldPerMetadataItem } = useOrderByFieldPerMetadataItem({
+ objectMetadataItems: objectMetadataItemsUsedInSelectedIdsQuery,
+ });
+
+ const { limitPerMetadataItem } = useLimitPerMetadataItem({
+ objectMetadataItems: objectMetadataItemsUsedInSelectedIdsQuery,
+ });
+
+ const multiSelectQueryForSelectedIds =
+ useGenerateFindManyRecordsForMultipleMetadataItemsQuery({
+ objectMetadataItems: objectMetadataItemsUsedInSelectedIdsQuery,
+ });
+
+ const {
+ loading: selectedObjectRecordsLoading,
+ data: selectedObjectRecordsQueryResult,
+ } = useQuery(multiSelectQueryForSelectedIds, {
+ variables: {
+ ...selectedIdFilterPerMetadataItem,
+ ...orderByFieldPerMetadataItem,
+ ...limitPerMetadataItem,
+ },
+ });
+
+ const { objectRecordForSelectArray: selectedObjectRecords } =
+ useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray({
+ multiObjectRecordsQueryResult: selectedObjectRecordsQueryResult,
+ });
+
+ return {
+ selectedObjectRecordsLoading,
+ selectedObjectRecords,
+ };
+};
diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useOrderByFieldPerMetadataItem.ts b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useOrderByFieldPerMetadataItem.ts
new file mode 100644
index 000000000..42dd28f99
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useOrderByFieldPerMetadataItem.ts
@@ -0,0 +1,29 @@
+import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
+import { getObjectOrderByField } from '@/object-metadata/utils/getObjectOrderByField';
+import { isDefined } from '~/utils/isDefined';
+import { capitalize } from '~/utils/string/capitalize';
+
+export const useOrderByFieldPerMetadataItem = ({
+ objectMetadataItems,
+}: {
+ objectMetadataItems: ObjectMetadataItem[];
+}) => {
+ const orderByFieldPerMetadataItem = Object.fromEntries(
+ objectMetadataItems
+ .map((objectMetadataItem) => {
+ const orderByField = getObjectOrderByField(objectMetadataItem);
+
+ return [
+ `orderBy${capitalize(objectMetadataItem.nameSingular)}`,
+ {
+ ...orderByField,
+ },
+ ];
+ })
+ .filter(isDefined),
+ );
+
+ return {
+ orderByFieldPerMetadataItem,
+ };
+};
diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useSearchFilterPerMetadataItem.ts b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useSearchFilterPerMetadataItem.ts
new file mode 100644
index 000000000..b2f1d8c3f
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useSearchFilterPerMetadataItem.ts
@@ -0,0 +1,67 @@
+import { isNonEmptyString } from '@sniptt/guards';
+
+import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
+import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem';
+import { ObjectRecordQueryFilter } from '@/object-record/record-filter/types/ObjectRecordQueryFilter';
+import { FieldMetadataType } from '~/generated/graphql';
+import { isDefined } from '~/utils/isDefined';
+
+export const useSearchFilterPerMetadataItem = ({
+ objectMetadataItems,
+ searchFilterValue,
+}: {
+ objectMetadataItems: ObjectMetadataItem[];
+ searchFilterValue: string;
+}) => {
+ const searchFilterPerMetadataItemNameSingular =
+ Object.fromEntries(
+ objectMetadataItems
+ .map((objectMetadataItem) => {
+ if (!isNonEmptyString(searchFilterValue)) return null;
+
+ const labelIdentifierFieldMetadataItem =
+ getLabelIdentifierFieldMetadataItem(objectMetadataItem);
+
+ let searchFilter: ObjectRecordQueryFilter = {};
+
+ if (labelIdentifierFieldMetadataItem) {
+ switch (labelIdentifierFieldMetadataItem.type) {
+ case FieldMetadataType.FullName: {
+ searchFilter = {
+ or: [
+ {
+ [labelIdentifierFieldMetadataItem.name]: {
+ firstName: {
+ ilike: `%${searchFilterValue}%`,
+ },
+ },
+ },
+ {
+ [labelIdentifierFieldMetadataItem.name]: {
+ lastName: {
+ ilike: `%${searchFilterValue}%`,
+ },
+ },
+ },
+ ],
+ };
+ break;
+ }
+ default:
+ searchFilter = {
+ [labelIdentifierFieldMetadataItem.name]: {
+ ilike: `%${searchFilterValue}%`,
+ },
+ };
+ }
+ }
+
+ return [objectMetadataItem.nameSingular, searchFilter] as const;
+ })
+ .filter(isDefined),
+ );
+
+ return {
+ searchFilterPerMetadataItemNameSingular,
+ };
+};
diff --git a/packages/twenty-front/src/modules/object-record/types/ObjectRecord.ts b/packages/twenty-front/src/modules/object-record/types/ObjectRecord.ts
new file mode 100644
index 000000000..7ffedb025
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/types/ObjectRecord.ts
@@ -0,0 +1 @@
+export type ObjectRecord = Record & { id: string };
diff --git a/packages/twenty-front/src/modules/object-record/types/ObjectRecordConnection.ts b/packages/twenty-front/src/modules/object-record/types/ObjectRecordConnection.ts
new file mode 100644
index 000000000..62bfc9164
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/types/ObjectRecordConnection.ts
@@ -0,0 +1,11 @@
+import { ObjectRecordEdge } from '@/object-record/types/ObjectRecordEdge';
+
+export type ObjectRecordConnection = {
+ edges: ObjectRecordEdge[];
+ pageInfo: {
+ hasNextPage?: boolean;
+ hasPreviousPage?: boolean;
+ startCursor?: string;
+ endCursor?: string;
+ };
+};
diff --git a/packages/twenty-front/src/modules/object-record/types/ObjectRecordEdge.ts b/packages/twenty-front/src/modules/object-record/types/ObjectRecordEdge.ts
new file mode 100644
index 000000000..76581297a
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/types/ObjectRecordEdge.ts
@@ -0,0 +1,6 @@
+import { ObjectRecord } from '@/object-record/types/ObjectRecord';
+
+export type ObjectRecordEdge = {
+ node: ObjectRecord;
+ cursor: string;
+};
diff --git a/packages/twenty-front/src/modules/object-record/types/ObjectRecordIdentifier.ts b/packages/twenty-front/src/modules/object-record/types/ObjectRecordIdentifier.ts
index 50856064c..6e860d20b 100644
--- a/packages/twenty-front/src/modules/object-record/types/ObjectRecordIdentifier.ts
+++ b/packages/twenty-front/src/modules/object-record/types/ObjectRecordIdentifier.ts
@@ -5,5 +5,6 @@ export type ObjectRecordIdentifier = {
name: string;
avatarUrl?: string | null;
avatarType?: AvatarType | null;
+ linkToEntity?: string;
linkToShowPage?: string;
};
diff --git a/packages/twenty-front/src/modules/people/hooks/useSpreadsheetPersonImport.ts b/packages/twenty-front/src/modules/people/hooks/useSpreadsheetPersonImport.ts
index c92228e23..e21ee67b0 100644
--- a/packages/twenty-front/src/modules/people/hooks/useSpreadsheetPersonImport.ts
+++ b/packages/twenty-front/src/modules/people/hooks/useSpreadsheetPersonImport.ts
@@ -29,33 +29,37 @@ export const useSpreadsheetPersonImport = () => {
...options,
onSubmit: async (data) => {
// TODO: Add better type checking in spreadsheet import later
- const createInputs = data.validData.map((person) => ({
- id: v4(),
- name: {
- firstName: person.firstName as string | undefined,
- lastName: person.lastName as string | undefined,
- },
- email: person.email as string | undefined,
- ...(person.linkedinUrl
- ? {
- linkedinLink: {
- label: 'linkedinUrl',
- url: person.linkedinUrl as string | undefined,
- },
- }
- : {}),
- ...(person.xUrl
- ? {
- xLink: {
- label: 'xUrl',
- url: person.xUrl as string | undefined,
- },
- }
- : {}),
- jobTitle: person.jobTitle as string | undefined,
- phone: person.phone as string | undefined,
- city: person.city as string | undefined,
- }));
+ const createInputs = data.validData.map(
+ (person) =>
+ ({
+ id: v4(),
+ name: {
+ firstName: person.firstName as string | undefined,
+ lastName: person.lastName as string | undefined,
+ },
+ email: person.email as string | undefined,
+ ...(person.linkedinUrl
+ ? {
+ linkedinLink: {
+ label: 'linkedinUrl',
+ url: person.linkedinUrl as string | undefined,
+ },
+ }
+ : {}),
+ ...(person.xUrl
+ ? {
+ xLink: {
+ label: 'xUrl',
+ url: person.xUrl as string | undefined,
+ },
+ }
+ : {}),
+ jobTitle: person.jobTitle as string | undefined,
+ phone: person.phone as string | undefined,
+ city: person.city as string | undefined,
+ }) as Person,
+ );
+
// TODO: abstract this part for any object
try {
await createManyPeople(createInputs);
diff --git a/packages/twenty-front/src/modules/ui/display/chip/components/EntityChip.tsx b/packages/twenty-front/src/modules/ui/display/chip/components/EntityChip.tsx
index 58f78e26e..a32bee0b3 100644
--- a/packages/twenty-front/src/modules/ui/display/chip/components/EntityChip.tsx
+++ b/packages/twenty-front/src/modules/ui/display/chip/components/EntityChip.tsx
@@ -5,6 +5,7 @@ import { isNonEmptyString } from '@sniptt/guards';
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
import { Avatar, AvatarType } from '@/users/components/Avatar';
+import { Nullable } from '~/types/Nullable';
import { Chip, ChipVariant } from './Chip';
@@ -13,7 +14,7 @@ export type EntityChipProps = {
entityId: string;
name: string;
avatarUrl?: string;
- avatarType?: AvatarType;
+ avatarType?: Nullable;
variant?: EntityChipVariant;
LeftIcon?: IconComponent;
className?: string;
diff --git a/packages/twenty-front/src/modules/ui/input/components/Checkbox.tsx b/packages/twenty-front/src/modules/ui/input/components/Checkbox.tsx
index 5fb33ddcf..1d7c49bc8 100644
--- a/packages/twenty-front/src/modules/ui/input/components/Checkbox.tsx
+++ b/packages/twenty-front/src/modules/ui/input/components/Checkbox.tsx
@@ -124,13 +124,13 @@ export const Checkbox = ({
React.useState(false);
React.useEffect(() => {
- setIsInternalChecked(checked);
+ setIsInternalChecked(checked ?? false);
}, [checked]);
const handleChange = (event: React.ChangeEvent) => {
onChange?.(event);
onCheckedChange?.(event.target.checked);
- setIsInternalChecked(event.target.checked);
+ setIsInternalChecked(event.target.checked ?? false);
};
const checkboxId = 'checkbox' + v4();
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 0bc688805..d6bb05d8b 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
@@ -2,7 +2,7 @@ import styled from '@emotion/styled';
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
import { ActivityType } from '@/activities/types/Activity';
-import { ActivityTargetableEntity } from '@/activities/types/ActivityTargetableEntity';
+import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
import { IconCheckbox, IconNotes, IconPlus } from '@/ui/display/icon/index';
import { IconButton } from '@/ui/input/button/components/IconButton';
@@ -21,13 +21,13 @@ const StyledContainer = styled.div`
export const ShowPageAddButton = ({
entity,
}: {
- entity: ActivityTargetableEntity;
+ entity: ActivityTargetableObject;
}) => {
const { closeDropdown, toggleDropdown } = useDropdown('add-show-page');
const openCreateActivity = useOpenCreateActivityDrawer();
const handleSelect = (type: ActivityType) => {
- openCreateActivity({ type, targetableEntities: [entity] });
+ openCreateActivity({ type, targetableObjects: [entity] });
closeDropdown();
};
diff --git a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageRightContainer.tsx b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageRightContainer.tsx
index 6a82de0ce..9de7a4b73 100644
--- a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageRightContainer.tsx
+++ b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageRightContainer.tsx
@@ -3,9 +3,10 @@ import styled from '@emotion/styled';
import { Threads } from '@/activities/emails/components/Threads';
import { Attachments } from '@/activities/files/components/Attachments';
import { Notes } from '@/activities/notes/components/Notes';
-import { EntityTasks } from '@/activities/tasks/components/EntityTasks';
+import { ObjectTasks } from '@/activities/tasks/components/ObjectTasks';
import { Timeline } from '@/activities/timeline/components/Timeline';
-import { ActivityTargetableEntity } from '@/activities/types/ActivityTargetableEntity';
+import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
+import { isStandardObject } from '@/object-metadata/utils/isStandardObject';
import {
IconCheckbox,
IconMail,
@@ -40,7 +41,7 @@ const StyledTabListContainer = styled.div`
`;
type ShowPageRightContainerProps = {
- entity?: ActivityTargetableEntity;
+ targetableObject?: ActivityTargetableObject;
timeline?: boolean;
tasks?: boolean;
notes?: boolean;
@@ -48,7 +49,7 @@ type ShowPageRightContainerProps = {
};
export const ShowPageRightContainer = ({
- entity,
+ targetableObject,
timeline,
tasks,
notes,
@@ -60,42 +61,44 @@ export const ShowPageRightContainer = ({
ShowPageRecoilScopeContext,
);
- if (!entity) return <>>;
+ if (!targetableObject) return <>>;
+
+ const targetableObjectIsStandardObject = isStandardObject(
+ targetableObject.targetObjectNameSingular,
+ );
+
const TASK_TABS = [
{
id: 'timeline',
title: 'Timeline',
Icon: IconTimelineEvent,
hide: !timeline,
- disabled: entity.type === 'Custom',
},
{
id: 'tasks',
title: 'Tasks',
Icon: IconCheckbox,
hide: !tasks,
- disabled: entity.type === 'Custom',
},
{
id: 'notes',
title: 'Notes',
Icon: IconNotes,
hide: !notes,
- disabled: entity.type === 'Custom',
},
{
id: 'files',
title: 'Files',
Icon: IconPaperclip,
hide: !notes,
- disabled: entity.type === 'Custom',
+ disabled: !targetableObjectIsStandardObject,
},
{
id: 'emails',
title: 'Emails',
Icon: IconMail,
hide: !emails,
- disabled: !isMessagingEnabled || entity.type === 'Custom',
+ disabled: !isMessagingEnabled || !targetableObjectIsStandardObject,
},
];
@@ -104,11 +107,17 @@ export const ShowPageRightContainer = ({
- {activeTabId === 'timeline' && }
- {activeTabId === 'tasks' && }
- {activeTabId === 'notes' && }
- {activeTabId === 'files' && }
- {activeTabId === 'emails' && }
+ {activeTabId === 'timeline' && (
+
+ )}
+ {activeTabId === 'tasks' && (
+
+ )}
+ {activeTabId === 'notes' && }
+ {activeTabId === 'files' && (
+
+ )}
+ {activeTabId === 'emails' && }
);
};
diff --git a/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemMultiSelectAvatar.tsx b/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemMultiSelectAvatar.tsx
index 369f7b205..60e32d628 100644
--- a/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemMultiSelectAvatar.tsx
+++ b/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemMultiSelectAvatar.tsx
@@ -1,6 +1,7 @@
import { ReactNode } from 'react';
import styled from '@emotion/styled';
+import { OverflowingTextWithTooltip } from '@/ui/display/tooltip/OverflowingTextWithTooltip';
import { Checkbox } from '@/ui/input/components/Checkbox';
import {
@@ -14,6 +15,7 @@ const StyledLeftContentWithCheckboxContainer = styled.div`
display: flex;
flex-direction: row;
gap: ${({ theme }) => theme.spacing(2)};
+ width: 100%;
`;
type MenuItemMultiSelectAvatarProps = {
@@ -48,7 +50,7 @@ export const MenuItemMultiSelectAvatar = ({
{avatar}
- {text}
+
diff --git a/packages/twenty-front/src/modules/ui/navigation/menu-item/internals/components/StyledMenuItemBase.tsx b/packages/twenty-front/src/modules/ui/navigation/menu-item/internals/components/StyledMenuItemBase.tsx
index 2291eec5f..7fc9e0b79 100644
--- a/packages/twenty-front/src/modules/ui/navigation/menu-item/internals/components/StyledMenuItemBase.tsx
+++ b/packages/twenty-front/src/modules/ui/navigation/menu-item/internals/components/StyledMenuItemBase.tsx
@@ -71,10 +71,11 @@ export const StyledMenuItemBase = styled.li`
export const StyledMenuItemLabel = styled.div<{ hasLeftIcon: boolean }>`
font-size: ${({ theme }) => theme.font.size.sm};
font-weight: ${({ theme }) => theme.font.weight.regular};
+
overflow: hidden;
padding-left: ${({ theme, hasLeftIcon }) =>
hasLeftIcon ? '' : theme.spacing(1)};
- text-overflow: ellipsis;
+
white-space: nowrap;
`;
diff --git a/packages/twenty-front/src/modules/users/components/Avatar.tsx b/packages/twenty-front/src/modules/users/components/Avatar.tsx
index 782aedbcd..12dc109fd 100644
--- a/packages/twenty-front/src/modules/users/components/Avatar.tsx
+++ b/packages/twenty-front/src/modules/users/components/Avatar.tsx
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
import styled from '@emotion/styled';
import { isNonEmptyString } from '@sniptt/guards';
+import { Nullable } from '~/types/Nullable';
import { stringToHslColor } from '~/utils/string-to-hsl';
import { getImageAbsoluteURIOrBase64 } from '../utils/getProfilePictureAbsoluteURI';
@@ -16,7 +17,7 @@ export type AvatarProps = {
size?: AvatarSize;
placeholder: string | undefined;
colorId?: string;
- type?: AvatarType;
+ type?: Nullable;
onClick?: () => void;
};
diff --git a/packages/twenty-front/src/testing/mock-data/activities.ts b/packages/twenty-front/src/testing/mock-data/activities.ts
index c911cf065..423580a2d 100644
--- a/packages/twenty-front/src/testing/mock-data/activities.ts
+++ b/packages/twenty-front/src/testing/mock-data/activities.ts
@@ -34,6 +34,7 @@ type MockedActivity = Pick<
| 'activityId'
| 'personId'
| 'companyId'
+ | 'targetObjectNameSingular'
> & {
activity: Pick;
person?: Pick | null;
@@ -98,6 +99,7 @@ export const mockedActivities: Array = [
id: '89bb825c-171e-4bcc-9cf7-43448d6fb300',
createdAt: '2023-04-26T10:12:42.33625+00:00',
updatedAt: '2023-04-26T10:23:42.33625+00:00',
+ targetObjectNameSingular: 'company',
personId: null,
companyId: '89bb825c-171e-4bcc-9cf7-43448d6fb280',
company: {
@@ -118,6 +120,7 @@ export const mockedActivities: Array = [
id: '89bb825c-171e-4bcc-9cf7-43448d6fb301',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
+ targetObjectNameSingular: 'company',
personId: null,
companyId: 'b396e6b9-dc5c-4643-bcff-61b6cf7523ae',
company: {
@@ -160,6 +163,7 @@ export const mockedActivities: Array = [
id: '89bb825c-171e-4bcc-9cf7-43448d6fb278t',
createdAt: '2023-04-26T10:12:42.33625+00:00',
updatedAt: '2023-04-26T10:23:42.33625+00:00',
+ targetObjectNameSingular: 'person',
personId: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b', // Alexandre
person: {
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b',
@@ -185,6 +189,7 @@ export const mockedActivities: Array = [
updatedAt: new Date().toISOString(),
personId: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6d', // Jean d'Eau
companyId: null,
+ targetObjectNameSingular: 'person',
company: null,
person: {
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6d',
diff --git a/packages/twenty-server/src/workspace/workspace-query-builder/factories/relation-field-alias.factory.ts b/packages/twenty-server/src/workspace/workspace-query-builder/factories/relation-field-alias.factory.ts
index 101629597..d18f463fa 100644
--- a/packages/twenty-server/src/workspace/workspace-query-builder/factories/relation-field-alias.factory.ts
+++ b/packages/twenty-server/src/workspace/workspace-query-builder/factories/relation-field-alias.factory.ts
@@ -111,8 +111,9 @@ export class RelationFieldAliasFactory {
}
`;
}
+
let relationAlias = fieldMetadata.isCustom
- ? `${fieldKey}: ${fieldMetadata.targetColumnMap.value}`
+ ? `${fieldKey}: ${referencedObjectMetadata.targetTableName}`
: fieldKey;
// For one to one relations, pg_graphql use the targetTableName on the side that is not storing the foreign key
diff --git a/packages/twenty-server/test/utils/reset-db.ts b/packages/twenty-server/test/utils/reset-db.ts
index 2a7863eb3..f39b118ce 100644
--- a/packages/twenty-server/test/utils/reset-db.ts
+++ b/packages/twenty-server/test/utils/reset-db.ts
@@ -11,7 +11,6 @@ export default async () => {
await prisma.$transaction(
entities.map((entity) => {
- console.log('entity: ', entity);
return prisma[entity].deleteMany();
}),
);