Feat/activities custom objects (#3213)
* WIP * WIP - MultiObjectSearch * WIP * WIP * Finished working version * Fix * Fixed and cleaned * Fix * Disabled files and emails for custom objects * Cleaned console.log * Fixed attachment * Fixed * fix lint --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -1,8 +1,7 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
import { CompanyChip } from '@/companies/components/CompanyChip';
|
import { ActivityTargetObjectRecord } from '@/activities/types/ActivityTargetObject';
|
||||||
import { PersonChip } from '@/people/components/PersonChip';
|
import { RecordChip } from '@/object-record/components/RecordChip';
|
||||||
import { getLogoUrlFromDomainName } from '~/utils';
|
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -10,37 +9,21 @@ const StyledContainer = styled.div`
|
|||||||
gap: ${({ theme }) => theme.spacing(1)};
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// TODO: fix edges pagination formatting on n+N
|
export const ActivityTargetChips = ({
|
||||||
export const ActivityTargetChips = ({ targets }: { targets?: any }) => {
|
activityTargetObjectRecords,
|
||||||
if (!targets) {
|
}: {
|
||||||
return null;
|
activityTargetObjectRecords: ActivityTargetObjectRecord[];
|
||||||
}
|
}) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
{targets?.map(({ company, person }: any) => {
|
{activityTargetObjectRecords?.map((activityTargetObjectRecord) => (
|
||||||
if (company) {
|
<RecordChip
|
||||||
return (
|
record={activityTargetObjectRecord.targetObjectRecord}
|
||||||
<CompanyChip
|
objectNameSingular={
|
||||||
key={company.id}
|
activityTargetObjectRecord.targetObjectMetadataItem.nameSingular
|
||||||
id={company.id}
|
}
|
||||||
name={company.name}
|
/>
|
||||||
avatarUrl={getLogoUrlFromDomainName(company.domainName)}
|
))}
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (person) {
|
|
||||||
return (
|
|
||||||
<PersonChip
|
|
||||||
key={person.id}
|
|
||||||
id={person.id}
|
|
||||||
name={person.name.firstName + ' ' + person.name.lastName}
|
|
||||||
avatarUrl={person.avatarUrl ?? undefined}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return <></>;
|
|
||||||
})}
|
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import styled from '@emotion/styled';
|
|||||||
import { ThreadPreview } from '@/activities/emails/components/ThreadPreview';
|
import { ThreadPreview } from '@/activities/emails/components/ThreadPreview';
|
||||||
import { getTimelineThreadsFromCompanyId } from '@/activities/emails/queries/getTimelineThreadsFromCompanyId';
|
import { getTimelineThreadsFromCompanyId } from '@/activities/emails/queries/getTimelineThreadsFromCompanyId';
|
||||||
import { getTimelineThreadsFromPersonId } from '@/activities/emails/queries/getTimelineThreadsFromPersonId';
|
import { getTimelineThreadsFromPersonId } from '@/activities/emails/queries/getTimelineThreadsFromPersonId';
|
||||||
import { ActivityTargetableEntity } from '@/activities/types/ActivityTargetableEntity';
|
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||||
import {
|
import {
|
||||||
H1Title,
|
H1Title,
|
||||||
H1TitleFontColor,
|
H1TitleFontColor,
|
||||||
@ -29,14 +29,14 @@ const StyledEmailCount = styled.span`
|
|||||||
color: ${({ theme }) => theme.font.color.light};
|
color: ${({ theme }) => theme.font.color.light};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Threads = ({ entity }: { entity: ActivityTargetableEntity }) => {
|
export const Threads = ({ entity }: { entity: ActivityTargetableObject }) => {
|
||||||
const threadQuery =
|
const threadQuery =
|
||||||
entity.type === 'Person'
|
entity.targetObjectNameSingular === 'person'
|
||||||
? getTimelineThreadsFromPersonId
|
? getTimelineThreadsFromPersonId
|
||||||
: getTimelineThreadsFromCompanyId;
|
: getTimelineThreadsFromCompanyId;
|
||||||
|
|
||||||
const threadQueryVariables =
|
const threadQueryVariables =
|
||||||
entity.type === 'Person'
|
entity.targetObjectNameSingular === 'person'
|
||||||
? { personId: entity.id }
|
? { personId: entity.id }
|
||||||
: { companyId: entity.id };
|
: { companyId: entity.id };
|
||||||
|
|
||||||
@ -50,7 +50,7 @@ export const Threads = ({ entity }: { entity: ActivityTargetableEntity }) => {
|
|||||||
|
|
||||||
const timelineThreads: TimelineThread[] =
|
const timelineThreads: TimelineThread[] =
|
||||||
threads.data[
|
threads.data[
|
||||||
entity.type === 'Person'
|
entity.targetObjectNameSingular === 'Person'
|
||||||
? 'getTimelineThreadsFromPersonId'
|
? 'getTimelineThreadsFromPersonId'
|
||||||
: 'getTimelineThreadsFromCompanyId'
|
: 'getTimelineThreadsFromCompanyId'
|
||||||
];
|
];
|
||||||
|
|||||||
@ -7,7 +7,7 @@ const meta: Meta<typeof Threads> = {
|
|||||||
component: Threads,
|
component: Threads,
|
||||||
args: {
|
args: {
|
||||||
entity: {
|
entity: {
|
||||||
type: 'Person',
|
targetObjectNameSingular: 'person',
|
||||||
id: '52ba3fd0-c723-4482-8b11-5fc24a587c71',
|
id: '52ba3fd0-c723-4482-8b11-5fc24a587c71',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,12 +1,14 @@
|
|||||||
import { ChangeEvent, useRef } from 'react';
|
import { ChangeEvent, useRef } from 'react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import { isNonEmptyArray } from '@sniptt/guards';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
import { AttachmentList } from '@/activities/files/components/AttachmentList';
|
import { AttachmentList } from '@/activities/files/components/AttachmentList';
|
||||||
import { useAttachments } from '@/activities/files/hooks/useAttachments';
|
import { useAttachments } from '@/activities/files/hooks/useAttachments';
|
||||||
import { Attachment } from '@/activities/files/types/Attachment';
|
import { Attachment } from '@/activities/files/types/Attachment';
|
||||||
import { getFileType } from '@/activities/files/utils/getFileType';
|
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 { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||||
@ -56,13 +58,13 @@ const StyledFileInput = styled.input`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const Attachments = ({
|
export const Attachments = ({
|
||||||
targetableEntity,
|
targetableObject,
|
||||||
}: {
|
}: {
|
||||||
targetableEntity: ActivityTargetableEntity;
|
targetableObject: ActivityTargetableObject;
|
||||||
}) => {
|
}) => {
|
||||||
const inputFileRef = useRef<HTMLInputElement>(null);
|
const inputFileRef = useRef<HTMLInputElement>(null);
|
||||||
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
||||||
const { attachments } = useAttachments(targetableEntity);
|
const { attachments } = useAttachments(targetableObject);
|
||||||
|
|
||||||
const [uploadFile] = useUploadFileMutation();
|
const [uploadFile] = useUploadFileMutation();
|
||||||
|
|
||||||
@ -92,22 +94,23 @@ export const Attachments = ({
|
|||||||
if (!attachmentUrl) {
|
if (!attachmentUrl) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!createOneAttachment) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await createOneAttachment({
|
const targetableObjectFieldIdName = getActivityTargetObjectFieldIdName({
|
||||||
|
nameSingular: targetableObject.targetObjectNameSingular,
|
||||||
|
});
|
||||||
|
|
||||||
|
const attachmentToCreate = {
|
||||||
authorId: currentWorkspaceMember?.id,
|
authorId: currentWorkspaceMember?.id,
|
||||||
name: file.name,
|
name: file.name,
|
||||||
fullPath: attachmentUrl,
|
fullPath: attachmentUrl,
|
||||||
type: getFileType(file.name),
|
type: getFileType(file.name),
|
||||||
companyId:
|
[targetableObjectFieldIdName]: targetableObject.id,
|
||||||
targetableEntity.type === 'Company' ? targetableEntity.id : null,
|
};
|
||||||
personId: targetableEntity.type === 'Person' ? targetableEntity.id : null,
|
|
||||||
});
|
await createOneAttachment(attachmentToCreate);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (attachments?.length === 0 && targetableEntity.type !== 'Custom') {
|
if (!isNonEmptyArray(attachments)) {
|
||||||
return (
|
return (
|
||||||
<StyledTaskGroupEmptyContainer>
|
<StyledTaskGroupEmptyContainer>
|
||||||
<StyledFileInput
|
<StyledFileInput
|
||||||
|
|||||||
@ -1,15 +1,21 @@
|
|||||||
import { Attachment } from '@/activities/files/types/Attachment';
|
import { Attachment } from '@/activities/files/types/Attachment';
|
||||||
|
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||||
|
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName';
|
||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||||
|
|
||||||
import { ActivityTargetableEntity } from '../../types/ActivityTargetableEntity';
|
|
||||||
|
|
||||||
// do we need to test this?
|
// do we need to test this?
|
||||||
export const useAttachments = (entity: ActivityTargetableEntity) => {
|
export const useAttachments = (targetableObject: ActivityTargetableObject) => {
|
||||||
|
const targetableObjectFieldIdName = getActivityTargetObjectFieldIdName({
|
||||||
|
nameSingular: targetableObject.targetObjectNameSingular,
|
||||||
|
});
|
||||||
|
|
||||||
const { records: attachments } = useFindManyRecords({
|
const { records: attachments } = useFindManyRecords({
|
||||||
objectNameSingular: CoreObjectNameSingular.Attachment,
|
objectNameSingular: CoreObjectNameSingular.Attachment,
|
||||||
filter: {
|
filter: {
|
||||||
[entity.type === 'Company' ? 'companyId' : 'personId']: { eq: entity.id },
|
[targetableObjectFieldIdName]: {
|
||||||
|
eq: targetableObject.id,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
orderBy: {
|
orderBy: {
|
||||||
createdAt: 'DescNullsFirst',
|
createdAt: 'DescNullsFirst',
|
||||||
|
|||||||
@ -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<Nullable<ActivityTargetObjectRecord>>((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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -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[],
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -4,8 +4,10 @@ import { useRecoilState, useRecoilValue } from 'recoil';
|
|||||||
|
|
||||||
import { Activity, ActivityType } from '@/activities/types/Activity';
|
import { Activity, ActivityType } from '@/activities/types/Activity';
|
||||||
import { ActivityTarget } from '@/activities/types/ActivityTarget';
|
import { ActivityTarget } from '@/activities/types/ActivityTarget';
|
||||||
|
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName';
|
||||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
|
import { useCreateManyRecords } from '@/object-record/hooks/useCreateManyRecords';
|
||||||
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||||
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
|
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
|
||||||
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
|
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 { activityTargetableEntityArrayState } from '../states/activityTargetableEntityArrayState';
|
||||||
import { viewableActivityIdState } from '../states/viewableActivityIdState';
|
import { viewableActivityIdState } from '../states/viewableActivityIdState';
|
||||||
import { ActivityTargetableEntity } from '../types/ActivityTargetableEntity';
|
import { ActivityTargetableObject } from '../types/ActivityTargetableEntity';
|
||||||
import { getTargetableEntitiesWithParents } from '../utils/getTargetableEntitiesWithParents';
|
import { flattenTargetableObjectsAndTheirRelatedTargetableObjects } from '../utils/flattenTargetableObjectsAndTheirRelatedTargetableObjects';
|
||||||
|
|
||||||
export const useOpenCreateActivityDrawer = () => {
|
export const useOpenCreateActivityDrawer = () => {
|
||||||
const { openRightDrawer } = useRightDrawer();
|
const { openRightDrawer } = useRightDrawer();
|
||||||
const { createOneRecord: createOneActivityTarget } =
|
const { createManyRecords: createManyActivityTargets } =
|
||||||
useCreateOneRecord<ActivityTarget>({
|
useCreateManyRecords<ActivityTarget>({
|
||||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
||||||
});
|
});
|
||||||
const { createOneRecord: createOneActivity } = useCreateOneRecord<Activity>({
|
const { createOneRecord: createOneActivity } = useCreateOneRecord<Activity>({
|
||||||
@ -37,15 +39,17 @@ export const useOpenCreateActivityDrawer = () => {
|
|||||||
return useCallback(
|
return useCallback(
|
||||||
async ({
|
async ({
|
||||||
type,
|
type,
|
||||||
targetableEntities,
|
targetableObjects,
|
||||||
assigneeId,
|
assigneeId,
|
||||||
}: {
|
}: {
|
||||||
type: ActivityType;
|
type: ActivityType;
|
||||||
targetableEntities?: ActivityTargetableEntity[];
|
targetableObjects?: ActivityTargetableObject[];
|
||||||
assigneeId?: string;
|
assigneeId?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const targetableEntitiesWithRelations = targetableEntities
|
const flattenedTargetableObjects = targetableObjects
|
||||||
? getTargetableEntitiesWithParents(targetableEntities)
|
? flattenTargetableObjectsAndTheirRelatedTargetableObjects(
|
||||||
|
targetableObjects,
|
||||||
|
)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
const createdActivity = await createOneActivity?.({
|
const createdActivity = await createOneActivity?.({
|
||||||
@ -61,21 +65,25 @@ export const useOpenCreateActivityDrawer = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(
|
const activityTargetsToCreate = flattenedTargetableObjects.map(
|
||||||
targetableEntitiesWithRelations.map(async (targetableEntity) => {
|
(targetableObject) => {
|
||||||
await createOneActivityTarget?.({
|
const targetableObjectFieldIdName =
|
||||||
companyId:
|
getActivityTargetObjectFieldIdName({
|
||||||
targetableEntity.type === 'Company' ? targetableEntity.id : null,
|
nameSingular: targetableObject.targetObjectNameSingular,
|
||||||
personId:
|
});
|
||||||
targetableEntity.type === 'Person' ? targetableEntity.id : null,
|
|
||||||
|
return {
|
||||||
|
[targetableObjectFieldIdName]: targetableObject.id,
|
||||||
activityId: createdActivity.id,
|
activityId: createdActivity.id,
|
||||||
});
|
};
|
||||||
}),
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await createManyActivityTargets(activityTargetsToCreate);
|
||||||
|
|
||||||
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
|
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
|
||||||
setViewableActivityId(createdActivity.id);
|
setViewableActivityId(createdActivity.id);
|
||||||
setActivityTargetableEntityArray(targetableEntities ?? []);
|
setActivityTargetableEntityArray(targetableObjects ?? []);
|
||||||
openRightDrawer(RightDrawerPages.CreateActivity);
|
openRightDrawer(RightDrawerPages.CreateActivity);
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
@ -84,7 +92,7 @@ export const useOpenCreateActivityDrawer = () => {
|
|||||||
setHotkeyScope,
|
setHotkeyScope,
|
||||||
setViewableActivityId,
|
setViewableActivityId,
|
||||||
createOneActivity,
|
createOneActivity,
|
||||||
createOneActivityTarget,
|
createManyActivityTargets,
|
||||||
currentWorkspaceMember,
|
currentWorkspaceMember,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@ -4,10 +4,7 @@ import { ActivityType } from '@/activities/types/Activity';
|
|||||||
import { useRecordTableScopedStates } from '@/object-record/record-table/hooks/internal/useRecordTableScopedStates';
|
import { useRecordTableScopedStates } from '@/object-record/record-table/hooks/internal/useRecordTableScopedStates';
|
||||||
import { getRecordTableScopeInjector } from '@/object-record/record-table/utils/getRecordTableScopeInjector';
|
import { getRecordTableScopeInjector } from '@/object-record/record-table/utils/getRecordTableScopeInjector';
|
||||||
|
|
||||||
import {
|
import { ActivityTargetableObject } from '../types/ActivityTargetableEntity';
|
||||||
ActivityTargetableEntity,
|
|
||||||
ActivityTargetableEntityType,
|
|
||||||
} from '../types/ActivityTargetableEntity';
|
|
||||||
|
|
||||||
import { useOpenCreateActivityDrawer } from './useOpenCreateActivityDrawer';
|
import { useOpenCreateActivityDrawer } from './useOpenCreateActivityDrawer';
|
||||||
|
|
||||||
@ -25,8 +22,8 @@ export const useOpenCreateActivityDrawerForSelectedRowIds = (
|
|||||||
({ snapshot }) =>
|
({ snapshot }) =>
|
||||||
(
|
(
|
||||||
type: ActivityType,
|
type: ActivityType,
|
||||||
entityType: ActivityTargetableEntityType,
|
objectNameSingular: string,
|
||||||
relatedEntities?: ActivityTargetableEntity[],
|
relatedEntities?: ActivityTargetableObject[],
|
||||||
) => {
|
) => {
|
||||||
const selectedRowIds =
|
const selectedRowIds =
|
||||||
injectSelectorSnapshotValueWithRecordTableScopeId(
|
injectSelectorSnapshotValueWithRecordTableScopeId(
|
||||||
@ -34,18 +31,21 @@ export const useOpenCreateActivityDrawerForSelectedRowIds = (
|
|||||||
selectedRowIdsScopeInjector,
|
selectedRowIdsScopeInjector,
|
||||||
);
|
);
|
||||||
|
|
||||||
let activityTargetableEntityArray: ActivityTargetableEntity[] =
|
let activityTargetableEntityArray: ActivityTargetableObject[] =
|
||||||
selectedRowIds.map((id: string) => ({
|
selectedRowIds.map((id: string) => ({
|
||||||
type: entityType,
|
type: 'Custom',
|
||||||
|
targetObjectNameSingular: objectNameSingular,
|
||||||
id,
|
id,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (relatedEntities) {
|
if (relatedEntities) {
|
||||||
activityTargetableEntityArray =
|
activityTargetableEntityArray =
|
||||||
activityTargetableEntityArray.concat(relatedEntities);
|
activityTargetableEntityArray.concat(relatedEntities);
|
||||||
}
|
}
|
||||||
|
|
||||||
openCreateActivityDrawer({
|
openCreateActivityDrawer({
|
||||||
type,
|
type,
|
||||||
targetableEntities: activityTargetableEntityArray,
|
targetableObjects: activityTargetableEntityArray,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
|
|||||||
@ -1,22 +1,15 @@
|
|||||||
import { useCallback, useMemo, useState } from 'react';
|
|
||||||
import { useQuery } from '@apollo/client';
|
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
import { useHandleCheckableActivityTargetChange } from '@/activities/hooks/useHandleCheckableActivityTargetChange';
|
|
||||||
import { ActivityTarget } from '@/activities/types/ActivityTarget';
|
import { ActivityTarget } from '@/activities/types/ActivityTarget';
|
||||||
import { flatMapAndSortEntityForSelectArrayOfArrayByName } from '@/activities/utils/flatMapAndSortEntityForSelectArrayByName';
|
import { ActivityTargetObjectRecord } from '@/activities/types/ActivityTargetObject';
|
||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName';
|
||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
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 { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlineCell';
|
||||||
import { MultipleEntitySelect } from '@/object-record/relation-picker/components/MultipleEntitySelect';
|
import { MultipleObjectRecordSelect } from '@/object-record/relation-picker/components/MultipleObjectRecordSelect';
|
||||||
import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker';
|
import { ObjectRecordForSelect } from '@/object-record/relation-picker/hooks/useMultiObjectSearch';
|
||||||
import { useFilteredSearchEntityQuery } from '@/search/hooks/useFilteredSearchEntityQuery';
|
|
||||||
import { assertNotNull } from '~/utils/assert';
|
|
||||||
|
|
||||||
type ActivityTargetInlineCellEditModeProps = {
|
|
||||||
activityId: string;
|
|
||||||
activityTargets: Array<Pick<ActivityTarget, 'id' | 'personId' | 'companyId'>>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const StyledSelectContainer = styled.div`
|
const StyledSelectContainer = styled.div`
|
||||||
left: 0px;
|
left: 0px;
|
||||||
@ -24,125 +17,77 @@ const StyledSelectContainer = styled.div`
|
|||||||
top: -8px;
|
top: -8px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
type ActivityTargetInlineCellEditModeProps = {
|
||||||
|
activityId: string;
|
||||||
|
activityTargetObjectRecords: ActivityTargetObjectRecord[];
|
||||||
|
};
|
||||||
|
|
||||||
export const ActivityTargetInlineCellEditMode = ({
|
export const ActivityTargetInlineCellEditMode = ({
|
||||||
activityId,
|
activityId,
|
||||||
activityTargets,
|
activityTargetObjectRecords,
|
||||||
}: ActivityTargetInlineCellEditModeProps) => {
|
}: ActivityTargetInlineCellEditModeProps) => {
|
||||||
const [searchFilter, setSearchFilter] = useState('');
|
const selectedObjectRecordIds = activityTargetObjectRecords.map(
|
||||||
|
(activityTarget) => ({
|
||||||
const initialPeopleIds = useMemo(
|
objectNameSingular: activityTarget.targetObjectNameSingular,
|
||||||
() =>
|
id: activityTarget.targetObjectRecord.id,
|
||||||
activityTargets
|
}),
|
||||||
?.filter(({ personId }) => personId !== null)
|
|
||||||
.map(({ personId }) => personId)
|
|
||||||
.filter(assertNotNull) ?? [],
|
|
||||||
[activityTargets],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const initialCompanyIds = useMemo(
|
const { createManyRecords: createManyActivityTargets } =
|
||||||
() =>
|
useCreateManyRecords<ActivityTarget>({
|
||||||
activityTargets
|
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
||||||
?.filter(({ companyId }) => companyId !== null)
|
|
||||||
.map(({ companyId }) => companyId)
|
|
||||||
.filter(assertNotNull) ?? [],
|
|
||||||
[activityTargets],
|
|
||||||
);
|
|
||||||
|
|
||||||
const initialSelectedEntityIds = useMemo(
|
|
||||||
() =>
|
|
||||||
[...initialPeopleIds, ...initialCompanyIds].reduce<
|
|
||||||
Record<string, boolean>
|
|
||||||
>((result, entityId) => ({ ...result, [entityId]: true }), {}),
|
|
||||||
[initialPeopleIds, initialCompanyIds],
|
|
||||||
);
|
|
||||||
|
|
||||||
const { findManyRecordsQuery: findManyPeopleQuery } = useObjectMetadataItem({
|
|
||||||
objectNameSingular: CoreObjectNameSingular.Person,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { findManyRecordsQuery: findManyCompaniesQuery } =
|
|
||||||
useObjectMetadataItem({
|
|
||||||
objectNameSingular: CoreObjectNameSingular.Company,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const useFindManyPeopleQuery = (options: any) =>
|
const { deleteManyRecords: deleteManyActivityTargets } = useDeleteManyRecords(
|
||||||
useQuery(findManyPeopleQuery, options);
|
{
|
||||||
|
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const useFindManyCompaniesQuery = (options: any) =>
|
|
||||||
useQuery(findManyCompaniesQuery, options);
|
|
||||||
|
|
||||||
const [selectedEntityIds, setSelectedEntityIds] = useState<
|
|
||||||
Record<string, boolean>
|
|
||||||
>(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 { closeInlineCell: closeEditableField } = useInlineCell();
|
||||||
|
|
||||||
const handleSubmit = useCallback(() => {
|
const handleSubmit = async (selectedRecords: ObjectRecordForSelect[]) => {
|
||||||
handleCheckItemsChange(
|
|
||||||
selectedEntityIds,
|
|
||||||
entitiesToSelect,
|
|
||||||
selectedEntities,
|
|
||||||
);
|
|
||||||
closeEditableField();
|
closeEditableField();
|
||||||
}, [
|
|
||||||
closeEditableField,
|
const activityTargetRecordsToDelete = activityTargetObjectRecords.filter(
|
||||||
entitiesToSelect,
|
(activityTargetObjectRecord) =>
|
||||||
handleCheckItemsChange,
|
!selectedRecords.some(
|
||||||
selectedEntities,
|
(selectedRecord) =>
|
||||||
selectedEntityIds,
|
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 = () => {
|
const handleCancel = () => {
|
||||||
closeEditableField();
|
closeEditableField();
|
||||||
@ -150,17 +95,8 @@ export const ActivityTargetInlineCellEditMode = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledSelectContainer>
|
<StyledSelectContainer>
|
||||||
<MultipleEntitySelect
|
<MultipleObjectRecordSelect
|
||||||
entities={{
|
selectedObjectRecordIds={selectedObjectRecordIds}
|
||||||
entitiesToSelect,
|
|
||||||
filteredSelectedEntities,
|
|
||||||
selectedEntities,
|
|
||||||
loading: false,
|
|
||||||
}}
|
|
||||||
onChange={setSelectedEntityIds}
|
|
||||||
onSearchFilterChange={setSearchFilter}
|
|
||||||
searchFilter={searchFilter}
|
|
||||||
value={selectedEntityIds}
|
|
||||||
onCancel={handleCancel}
|
onCancel={handleCancel}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
import { ActivityTargetChips } from '@/activities/components/ActivityTargetChips';
|
import { ActivityTargetChips } from '@/activities/components/ActivityTargetChips';
|
||||||
|
import { useActivityTargetObjectRecords } from '@/activities/hooks/useActivityTargetObjectRecords';
|
||||||
import { ActivityTargetInlineCellEditMode } from '@/activities/inline-cell/components/ActivityTargetInlineCellEditMode';
|
import { ActivityTargetInlineCellEditMode } from '@/activities/inline-cell/components/ActivityTargetInlineCellEditMode';
|
||||||
import { ActivityTarget } from '@/activities/types/ActivityTarget';
|
import { ActivityTarget } from '@/activities/types/ActivityTarget';
|
||||||
import { GraphQLActivity } from '@/activities/types/GraphQLActivity';
|
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 { RecordInlineCellContainer } from '@/object-record/record-inline-cell/components/RecordInlineCellContainer';
|
||||||
import { FieldRecoilScopeContext } from '@/object-record/record-inline-cell/states/recoil-scope-contexts/FieldRecoilScopeContext';
|
import { FieldRecoilScopeContext } from '@/object-record/record-inline-cell/states/recoil-scope-contexts/FieldRecoilScopeContext';
|
||||||
import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
|
import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
|
||||||
@ -23,14 +22,8 @@ type ActivityTargetsInlineCellProps = {
|
|||||||
export const ActivityTargetsInlineCell = ({
|
export const ActivityTargetsInlineCell = ({
|
||||||
activity,
|
activity,
|
||||||
}: ActivityTargetsInlineCellProps) => {
|
}: ActivityTargetsInlineCellProps) => {
|
||||||
const activityTargetIds =
|
const { activityTargetObjectRecords } = useActivityTargetObjectRecords({
|
||||||
activity?.activityTargets?.edges?.map(
|
activityId: activity?.id ?? '',
|
||||||
(activityTarget) => activityTarget.node.id,
|
|
||||||
) ?? [];
|
|
||||||
|
|
||||||
const { records: activityTargets } = useFindManyRecords<ActivityTarget>({
|
|
||||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
|
||||||
filter: { id: { in: activityTargetIds } },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -44,11 +37,15 @@ export const ActivityTargetsInlineCell = ({
|
|||||||
editModeContent={
|
editModeContent={
|
||||||
<ActivityTargetInlineCellEditMode
|
<ActivityTargetInlineCellEditMode
|
||||||
activityId={activity?.id ?? ''}
|
activityId={activity?.id ?? ''}
|
||||||
activityTargets={activityTargets as any}
|
activityTargetObjectRecords={activityTargetObjectRecords as any}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label="Relations"
|
label="Relations"
|
||||||
displayModeContent={<ActivityTargetChips targets={activityTargets} />}
|
displayModeContent={
|
||||||
|
<ActivityTargetChips
|
||||||
|
activityTargetObjectRecords={activityTargetObjectRecords}
|
||||||
|
/>
|
||||||
|
}
|
||||||
isDisplayModeContentEmpty={
|
isDisplayModeContentEmpty={
|
||||||
activity?.activityTargets?.edges?.length === 0
|
activity?.activityTargets?.edges?.length === 0
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import styled from '@emotion/styled';
|
|||||||
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
|
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
|
||||||
import { NoteList } from '@/activities/notes/components/NoteList';
|
import { NoteList } from '@/activities/notes/components/NoteList';
|
||||||
import { useNotes } from '@/activities/notes/hooks/useNotes';
|
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 { IconPlus } from '@/ui/display/icon';
|
||||||
import { Button } from '@/ui/input/button/components/Button';
|
import { Button } from '@/ui/input/button/components/Button';
|
||||||
|
|
||||||
@ -44,12 +44,16 @@ const StyledNotesContainer = styled.div`
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Notes = ({ entity }: { entity: ActivityTargetableEntity }) => {
|
export const Notes = ({
|
||||||
const { notes } = useNotes(entity);
|
targetableObject,
|
||||||
|
}: {
|
||||||
|
targetableObject: ActivityTargetableObject;
|
||||||
|
}) => {
|
||||||
|
const { notes } = useNotes(targetableObject);
|
||||||
|
|
||||||
const openCreateActivity = useOpenCreateActivityDrawer();
|
const openCreateActivity = useOpenCreateActivityDrawer();
|
||||||
|
|
||||||
if (notes?.length === 0 && entity.type !== 'Custom') {
|
if (notes?.length === 0) {
|
||||||
return (
|
return (
|
||||||
<StyledTaskGroupEmptyContainer>
|
<StyledTaskGroupEmptyContainer>
|
||||||
<StyledEmptyTaskGroupTitle>No note yet</StyledEmptyTaskGroupTitle>
|
<StyledEmptyTaskGroupTitle>No note yet</StyledEmptyTaskGroupTitle>
|
||||||
@ -61,7 +65,7 @@ export const Notes = ({ entity }: { entity: ActivityTargetableEntity }) => {
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
openCreateActivity({
|
openCreateActivity({
|
||||||
type: 'Note',
|
type: 'Note',
|
||||||
targetableEntities: [entity],
|
targetableObjects: [targetableObject],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@ -83,7 +87,7 @@ export const Notes = ({ entity }: { entity: ActivityTargetableEntity }) => {
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
openCreateActivity({
|
openCreateActivity({
|
||||||
type: 'Note',
|
type: 'Note',
|
||||||
targetableEntities: [entity],
|
targetableObjects: [targetableObject],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
></Button>
|
></Button>
|
||||||
|
|||||||
@ -1,17 +1,13 @@
|
|||||||
|
import { useActivityTargets } from '@/activities/hooks/useActivityTargets';
|
||||||
import { Note } from '@/activities/types/Note';
|
import { Note } from '@/activities/types/Note';
|
||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { OrderByField } from '@/object-metadata/types/OrderByField';
|
import { OrderByField } from '@/object-metadata/types/OrderByField';
|
||||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||||
|
|
||||||
import { ActivityTargetableEntity } from '../../types/ActivityTargetableEntity';
|
import { ActivityTargetableObject } from '../../types/ActivityTargetableEntity';
|
||||||
|
|
||||||
export const useNotes = (entity: ActivityTargetableEntity) => {
|
export const useNotes = (targetableObject: ActivityTargetableObject) => {
|
||||||
const { records: activityTargets } = useFindManyRecords({
|
const { activityTargets } = useActivityTargets({ targetableObject });
|
||||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
|
||||||
filter: {
|
|
||||||
[entity.type === 'Company' ? 'companyId' : 'personId']: { eq: entity.id },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const filter = {
|
const filter = {
|
||||||
id: {
|
id: {
|
||||||
@ -19,6 +15,7 @@ export const useNotes = (entity: ActivityTargetableEntity) => {
|
|||||||
},
|
},
|
||||||
type: { eq: 'Note' },
|
type: { eq: 'Note' },
|
||||||
};
|
};
|
||||||
|
|
||||||
const orderBy = {
|
const orderBy = {
|
||||||
createdAt: 'AscNullsFirst',
|
createdAt: 'AscNullsFirst',
|
||||||
} as OrderByField;
|
} as OrderByField;
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { atom } from 'recoil';
|
import { atom } from 'recoil';
|
||||||
|
|
||||||
import { ActivityTargetableEntity } from '../types/ActivityTargetableEntity';
|
import { ActivityTargetableObject } from '../types/ActivityTargetableEntity';
|
||||||
|
|
||||||
export const activityTargetableEntityArrayState = atom<
|
export const activityTargetableEntityArrayState = atom<
|
||||||
ActivityTargetableEntity[]
|
ActivityTargetableObject[]
|
||||||
>({
|
>({
|
||||||
key: 'activities/targetable-entity-array',
|
key: 'activities/targetable-entity-array',
|
||||||
default: [],
|
default: [],
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { Meta, StoryObj } from '@storybook/react';
|
|||||||
|
|
||||||
import { TasksRecoilScopeContext } from '@/activities/states/recoil-scope-contexts/TasksRecoilScopeContext';
|
import { TasksRecoilScopeContext } from '@/activities/states/recoil-scope-contexts/TasksRecoilScopeContext';
|
||||||
import { TaskGroups } from '@/activities/tasks/components/TaskGroups';
|
import { TaskGroups } from '@/activities/tasks/components/TaskGroups';
|
||||||
|
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||||
import { ObjectFilterDropdownScope } from '@/object-record/object-filter-dropdown/scopes/ObjectFilterDropdownScope';
|
import { ObjectFilterDropdownScope } from '@/object-record/object-filter-dropdown/scopes/ObjectFilterDropdownScope';
|
||||||
import { ComponentWithRecoilScopeDecorator } from '~/testing/decorators/ComponentWithRecoilScopeDecorator';
|
import { ComponentWithRecoilScopeDecorator } from '~/testing/decorators/ComponentWithRecoilScopeDecorator';
|
||||||
import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator';
|
import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator';
|
||||||
@ -35,9 +36,11 @@ export const Empty: Story = {};
|
|||||||
|
|
||||||
export const WithTasks: Story = {
|
export const WithTasks: Story = {
|
||||||
args: {
|
args: {
|
||||||
entity: {
|
targetableObjects: [
|
||||||
id: mockedTasks[0].authorId,
|
{
|
||||||
type: 'Person',
|
id: mockedTasks[0].authorId,
|
||||||
},
|
targetObjectNameSingular: 'person',
|
||||||
|
},
|
||||||
|
] as ActivityTargetableObject[],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,16 +1,18 @@
|
|||||||
|
import { isNonEmptyArray } from '@sniptt/guards';
|
||||||
|
|
||||||
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
|
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 { IconPlus } from '@/ui/display/icon';
|
||||||
import { Button } from '@/ui/input/button/components/Button';
|
import { Button } from '@/ui/input/button/components/Button';
|
||||||
|
|
||||||
export const AddTaskButton = ({
|
export const AddTaskButton = ({
|
||||||
activityTargetEntity,
|
activityTargetableObjects,
|
||||||
}: {
|
}: {
|
||||||
activityTargetEntity?: ActivityTargetableEntity;
|
activityTargetableObjects?: ActivityTargetableObject[];
|
||||||
}) => {
|
}) => {
|
||||||
const openCreateActivity = useOpenCreateActivityDrawer();
|
const openCreateActivity = useOpenCreateActivityDrawer();
|
||||||
|
|
||||||
if (!activityTargetEntity) {
|
if (!isNonEmptyArray(activityTargetableObjects)) {
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,7 +25,7 @@ export const AddTaskButton = ({
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
openCreateActivity({
|
openCreateActivity({
|
||||||
type: 'Task',
|
type: 'Task',
|
||||||
targetableEntities: [activityTargetEntity],
|
targetableObjects: activityTargetableObjects,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
></Button>
|
></Button>
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import styled from '@emotion/styled';
|
|||||||
|
|
||||||
import { TasksRecoilScopeContext } from '@/activities/states/recoil-scope-contexts/TasksRecoilScopeContext';
|
import { TasksRecoilScopeContext } from '@/activities/states/recoil-scope-contexts/TasksRecoilScopeContext';
|
||||||
import { TaskGroups } from '@/activities/tasks/components/TaskGroups';
|
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 { ObjectFilterDropdownScope } from '@/object-record/object-filter-dropdown/scopes/ObjectFilterDropdownScope';
|
||||||
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
|
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
|
||||||
|
|
||||||
@ -14,16 +14,16 @@ const StyledContainer = styled.div`
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const EntityTasks = ({
|
export const ObjectTasks = ({
|
||||||
entity,
|
targetableObject,
|
||||||
}: {
|
}: {
|
||||||
entity: ActivityTargetableEntity;
|
targetableObject: ActivityTargetableObject;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<RecoilScope CustomRecoilScopeContext={TasksRecoilScopeContext}>
|
<RecoilScope CustomRecoilScopeContext={TasksRecoilScopeContext}>
|
||||||
<ObjectFilterDropdownScope filterScopeId="entity-tasks-filter-scope">
|
<ObjectFilterDropdownScope filterScopeId="entity-tasks-filter-scope">
|
||||||
<TaskGroups entity={entity} showAddButton />
|
<TaskGroups targetableObjects={[targetableObject]} showAddButton />
|
||||||
</ObjectFilterDropdownScope>
|
</ObjectFilterDropdownScope>
|
||||||
</RecoilScope>
|
</RecoilScope>
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
@ -3,7 +3,7 @@ import styled from '@emotion/styled';
|
|||||||
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
|
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
|
||||||
import { TasksRecoilScopeContext } from '@/activities/states/recoil-scope-contexts/TasksRecoilScopeContext';
|
import { TasksRecoilScopeContext } from '@/activities/states/recoil-scope-contexts/TasksRecoilScopeContext';
|
||||||
import { useTasks } from '@/activities/tasks/hooks/useTasks';
|
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 { IconPlus } from '@/ui/display/icon';
|
||||||
import { Button } from '@/ui/input/button/components/Button';
|
import { Button } from '@/ui/input/button/components/Button';
|
||||||
import { activeTabIdScopedState } from '@/ui/layout/tab/states/activeTabIdScopedState';
|
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 { AddTaskButton } from './AddTaskButton';
|
||||||
import { TaskList } from './TaskList';
|
import { TaskList } from './TaskList';
|
||||||
|
|
||||||
type TaskGroupsProps = {
|
|
||||||
filterDropdownId?: string;
|
|
||||||
entity?: ActivityTargetableEntity;
|
|
||||||
showAddButton?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const StyledTaskGroupEmptyContainer = styled.div`
|
const StyledTaskGroupEmptyContainer = styled.div`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
@ -52,9 +46,15 @@ const StyledContainer = styled.div`
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
type TaskGroupsProps = {
|
||||||
|
filterDropdownId?: string;
|
||||||
|
targetableObjects?: ActivityTargetableObject[];
|
||||||
|
showAddButton?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export const TaskGroups = ({
|
export const TaskGroups = ({
|
||||||
filterDropdownId,
|
filterDropdownId,
|
||||||
entity,
|
targetableObjects,
|
||||||
showAddButton,
|
showAddButton,
|
||||||
}: TaskGroupsProps) => {
|
}: TaskGroupsProps) => {
|
||||||
const {
|
const {
|
||||||
@ -62,7 +62,10 @@ export const TaskGroups = ({
|
|||||||
upcomingTasks,
|
upcomingTasks,
|
||||||
unscheduledTasks,
|
unscheduledTasks,
|
||||||
completedTasks,
|
completedTasks,
|
||||||
} = useTasks({ filterDropdownId: filterDropdownId, entity });
|
} = useTasks({
|
||||||
|
filterDropdownId: filterDropdownId,
|
||||||
|
targetableObjects: targetableObjects ?? [],
|
||||||
|
});
|
||||||
|
|
||||||
const openCreateActivity = useOpenCreateActivityDrawer();
|
const openCreateActivity = useOpenCreateActivityDrawer();
|
||||||
|
|
||||||
@ -71,10 +74,6 @@ export const TaskGroups = ({
|
|||||||
TasksRecoilScopeContext,
|
TasksRecoilScopeContext,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (entity?.type === 'Custom') {
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(activeTabId !== 'done' &&
|
(activeTabId !== 'done' &&
|
||||||
todayOrPreviousTasks?.length === 0 &&
|
todayOrPreviousTasks?.length === 0 &&
|
||||||
@ -93,7 +92,7 @@ export const TaskGroups = ({
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
openCreateActivity({
|
openCreateActivity({
|
||||||
type: 'Task',
|
type: 'Task',
|
||||||
targetableEntities: entity ? [entity] : undefined,
|
targetableObjects,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@ -107,7 +106,9 @@ export const TaskGroups = ({
|
|||||||
<TaskList
|
<TaskList
|
||||||
tasks={completedTasks ?? []}
|
tasks={completedTasks ?? []}
|
||||||
button={
|
button={
|
||||||
showAddButton && <AddTaskButton activityTargetEntity={entity} />
|
showAddButton && (
|
||||||
|
<AddTaskButton activityTargetableObjects={targetableObjects} />
|
||||||
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@ -116,7 +117,9 @@ export const TaskGroups = ({
|
|||||||
title="Today"
|
title="Today"
|
||||||
tasks={todayOrPreviousTasks ?? []}
|
tasks={todayOrPreviousTasks ?? []}
|
||||||
button={
|
button={
|
||||||
showAddButton && <AddTaskButton activityTargetEntity={entity} />
|
showAddButton && (
|
||||||
|
<AddTaskButton activityTargetableObjects={targetableObjects} />
|
||||||
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<TaskList
|
<TaskList
|
||||||
@ -125,7 +128,7 @@ export const TaskGroups = ({
|
|||||||
button={
|
button={
|
||||||
showAddButton &&
|
showAddButton &&
|
||||||
!todayOrPreviousTasks?.length && (
|
!todayOrPreviousTasks?.length && (
|
||||||
<AddTaskButton activityTargetEntity={entity} />
|
<AddTaskButton activityTargetableObjects={targetableObjects} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@ -136,7 +139,7 @@ export const TaskGroups = ({
|
|||||||
showAddButton &&
|
showAddButton &&
|
||||||
!todayOrPreviousTasks?.length &&
|
!todayOrPreviousTasks?.length &&
|
||||||
!upcomingTasks?.length && (
|
!upcomingTasks?.length && (
|
||||||
<AddTaskButton activityTargetEntity={entity} />
|
<AddTaskButton activityTargetableObjects={targetableObjects} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -2,12 +2,10 @@ import { useTheme } from '@emotion/react';
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
import { ActivityTargetChips } from '@/activities/components/ActivityTargetChips';
|
import { ActivityTargetChips } from '@/activities/components/ActivityTargetChips';
|
||||||
|
import { useActivityTargetObjectRecords } from '@/activities/hooks/useActivityTargetObjectRecords';
|
||||||
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
|
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
|
||||||
import { ActivityTarget } from '@/activities/types/ActivityTarget';
|
|
||||||
import { GraphQLActivity } from '@/activities/types/GraphQLActivity';
|
import { GraphQLActivity } from '@/activities/types/GraphQLActivity';
|
||||||
import { getActivitySummary } from '@/activities/utils/getActivitySummary';
|
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 { IconCalendar, IconComment } from '@/ui/display/icon';
|
||||||
import { OverflowingTextWithTooltip } from '@/ui/display/tooltip/OverflowingTextWithTooltip';
|
import { OverflowingTextWithTooltip } from '@/ui/display/tooltip/OverflowingTextWithTooltip';
|
||||||
import { Checkbox, CheckboxShape } from '@/ui/input/components/Checkbox';
|
import { Checkbox, CheckboxShape } from '@/ui/input/components/Checkbox';
|
||||||
@ -76,14 +74,8 @@ export const TaskRow = ({
|
|||||||
const body = getActivitySummary(task.body);
|
const body = getActivitySummary(task.body);
|
||||||
const { completeTask } = useCompleteTask(task);
|
const { completeTask } = useCompleteTask(task);
|
||||||
|
|
||||||
const activityTargetIds =
|
const { activityTargetObjectRecords } = useActivityTargetObjectRecords({
|
||||||
task?.activityTargets?.edges?.map(
|
activityId: task.id,
|
||||||
(activityTarget) => activityTarget.node.id,
|
|
||||||
) ?? [];
|
|
||||||
|
|
||||||
const { records: activityTargets } = useFindManyRecords<ActivityTarget>({
|
|
||||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
|
||||||
filter: { id: { in: activityTargetIds } },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -115,7 +107,9 @@ export const TaskRow = ({
|
|||||||
)}
|
)}
|
||||||
</StyledTaskBody>
|
</StyledTaskBody>
|
||||||
<StyledFieldsContainer>
|
<StyledFieldsContainer>
|
||||||
<ActivityTargetChips targets={activityTargets} />
|
<ActivityTargetChips
|
||||||
|
activityTargetObjectRecords={activityTargetObjectRecords}
|
||||||
|
/>
|
||||||
<StyledDueDate
|
<StyledDueDate
|
||||||
isPast={
|
isPast={
|
||||||
!!task.dueAt && hasDatePassed(task.dueAt) && !task.completedAt
|
!!task.dueAt && hasDatePassed(task.dueAt) && !task.completedAt
|
||||||
|
|||||||
@ -1,55 +1,75 @@
|
|||||||
import { isNonEmptyString } from '@sniptt/guards';
|
import { isNonEmptyArray, isNonEmptyString } from '@sniptt/guards';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
import { Activity } from '@/activities/types/Activity';
|
import { Activity } from '@/activities/types/Activity';
|
||||||
import { ActivityTargetableEntity } from '@/activities/types/ActivityTargetableEntity';
|
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||||
|
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName';
|
||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||||
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
|
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
|
||||||
|
import { LeafObjectRecordFilter } from '@/object-record/record-filter/types/ObjectRecordQueryFilter';
|
||||||
import { parseDate } from '~/utils/date-utils';
|
import { parseDate } from '~/utils/date-utils';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
|
||||||
|
|
||||||
type UseTasksProps = {
|
type UseTasksProps = {
|
||||||
filterDropdownId?: string;
|
filterDropdownId?: string;
|
||||||
entity?: ActivityTargetableEntity;
|
targetableObjects: ActivityTargetableObject[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useTasks = (props?: UseTasksProps) => {
|
export const useTasks = ({
|
||||||
const { filterDropdownId, entity } = props ?? {};
|
targetableObjects,
|
||||||
|
filterDropdownId,
|
||||||
|
}: UseTasksProps) => {
|
||||||
const { selectedFilter } = useFilterDropdown({
|
const { selectedFilter } = useFilterDropdown({
|
||||||
filterDropdownId: filterDropdownId,
|
filterDropdownId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const targetableObjectsFilter =
|
||||||
|
targetableObjects.reduce<LeafObjectRecordFilter>(
|
||||||
|
(aggregateFilter, targetableObject) => {
|
||||||
|
const targetableObjectFieldName = getActivityTargetObjectFieldIdName({
|
||||||
|
nameSingular: targetableObject.targetObjectNameSingular,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isNonEmptyString(targetableObject.id)) {
|
||||||
|
aggregateFilter[targetableObjectFieldName] = {
|
||||||
|
eq: targetableObject.id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return aggregateFilter;
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
const { records: activityTargets } = useFindManyRecords({
|
const { records: activityTargets } = useFindManyRecords({
|
||||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
||||||
filter: isDefined(entity)
|
filter: targetableObjectsFilter,
|
||||||
? {
|
|
||||||
[entity?.type === 'Company' ? 'companyId' : 'personId']: {
|
|
||||||
eq: entity?.id,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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({
|
const { records: completeTasksData } = useFindManyRecords({
|
||||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||||
skip: !entity && !selectedFilter,
|
skip: skipRequest,
|
||||||
filter: {
|
filter: {
|
||||||
completedAt: { is: 'NOT_NULL' },
|
completedAt: { is: 'NOT_NULL' },
|
||||||
...(isDefined(entity) && {
|
...idFilter,
|
||||||
id: {
|
|
||||||
in: activityTargets?.map(
|
|
||||||
(activityTarget) => activityTarget.activityId,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
type: { eq: 'Task' },
|
type: { eq: 'Task' },
|
||||||
...(isNonEmptyString(selectedFilter?.value) && {
|
...assigneeIdFilter,
|
||||||
assigneeId: {
|
|
||||||
in: JSON.parse(selectedFilter?.value),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
orderBy: {
|
orderBy: {
|
||||||
createdAt: 'DescNullsFirst',
|
createdAt: 'DescNullsFirst',
|
||||||
@ -58,22 +78,12 @@ export const useTasks = (props?: UseTasksProps) => {
|
|||||||
|
|
||||||
const { records: incompleteTaskData } = useFindManyRecords({
|
const { records: incompleteTaskData } = useFindManyRecords({
|
||||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||||
skip: !entity && !selectedFilter,
|
skip: skipRequest,
|
||||||
filter: {
|
filter: {
|
||||||
completedAt: { is: 'NULL' },
|
completedAt: { is: 'NULL' },
|
||||||
...(isDefined(entity) && {
|
...idFilter,
|
||||||
id: {
|
|
||||||
in: activityTargets?.map(
|
|
||||||
(activityTarget) => activityTarget.activityId,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
type: { eq: 'Task' },
|
type: { eq: 'Task' },
|
||||||
...(isNonEmptyString(selectedFilter?.value) && {
|
...assigneeIdFilter,
|
||||||
assigneeId: {
|
|
||||||
in: JSON.parse(selectedFilter?.value),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
orderBy: {
|
orderBy: {
|
||||||
createdAt: 'DescNullsFirst',
|
createdAt: 'DescNullsFirst',
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import { isNonEmptyString } from '@sniptt/guards';
|
||||||
|
|
||||||
import { ActivityCreateButton } from '@/activities/components/ActivityCreateButton';
|
import { ActivityCreateButton } from '@/activities/components/ActivityCreateButton';
|
||||||
|
import { useActivityTargets } from '@/activities/hooks/useActivityTargets';
|
||||||
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
|
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
|
||||||
import { Activity } from '@/activities/types/Activity';
|
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 { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||||
@ -48,20 +50,21 @@ const StyledEmptyTimelineSubTitle = styled.div`
|
|||||||
margin-bottom: ${({ theme }) => theme.spacing(2)};
|
margin-bottom: ${({ theme }) => theme.spacing(2)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Timeline = ({ entity }: { entity: ActivityTargetableEntity }) => {
|
export const Timeline = ({
|
||||||
const { records: activityTargets, loading } = useFindManyRecords({
|
targetableObject,
|
||||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
}: {
|
||||||
filter: {
|
targetableObject: ActivityTargetableObject;
|
||||||
[entity.type === 'Company' ? 'companyId' : 'personId']: { eq: entity.id },
|
}) => {
|
||||||
},
|
const { activityTargets } = useActivityTargets({ targetableObject });
|
||||||
});
|
|
||||||
|
|
||||||
const { records: activities } = useFindManyRecords({
|
const { records: activities } = useFindManyRecords({
|
||||||
skip: !activityTargets?.length,
|
skip: !activityTargets?.length,
|
||||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||||
filter: {
|
filter: {
|
||||||
id: {
|
id: {
|
||||||
in: activityTargets?.map((activityTarget) => activityTarget.activityId),
|
in: activityTargets
|
||||||
|
?.map((activityTarget) => activityTarget.activityId)
|
||||||
|
.filter(isNonEmptyString),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
orderBy: {
|
orderBy: {
|
||||||
@ -71,10 +74,6 @@ export const Timeline = ({ entity }: { entity: ActivityTargetableEntity }) => {
|
|||||||
|
|
||||||
const openCreateActivity = useOpenCreateActivityDrawer();
|
const openCreateActivity = useOpenCreateActivityDrawer();
|
||||||
|
|
||||||
if (loading || entity.type === 'Custom') {
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!activities.length) {
|
if (!activities.length) {
|
||||||
return (
|
return (
|
||||||
<StyledTimelineEmptyContainer>
|
<StyledTimelineEmptyContainer>
|
||||||
@ -84,13 +83,13 @@ export const Timeline = ({ entity }: { entity: ActivityTargetableEntity }) => {
|
|||||||
onNoteClick={() =>
|
onNoteClick={() =>
|
||||||
openCreateActivity({
|
openCreateActivity({
|
||||||
type: 'Note',
|
type: 'Note',
|
||||||
targetableEntities: [entity],
|
targetableObjects: [targetableObject],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
onTaskClick={() =>
|
onTaskClick={() =>
|
||||||
openCreateActivity({
|
openCreateActivity({
|
||||||
type: 'Task',
|
type: 'Task',
|
||||||
targetableEntities: [entity],
|
targetableObjects: [targetableObject],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -145,7 +145,7 @@ type TimelineActivityProps = {
|
|||||||
| 'type'
|
| 'type'
|
||||||
| 'comments'
|
| 'comments'
|
||||||
| 'dueAt'
|
| 'dueAt'
|
||||||
> & { author: Pick<WorkspaceMember, 'name' | 'avatarUrl'> } & {
|
> & { author?: Pick<WorkspaceMember, 'name' | 'avatarUrl'> } & {
|
||||||
assignee?: Pick<WorkspaceMember, 'id' | 'name' | 'avatarUrl'> | null;
|
assignee?: Pick<WorkspaceMember, 'id' | 'name' | 'avatarUrl'> | null;
|
||||||
};
|
};
|
||||||
isLastActivity?: boolean;
|
isLastActivity?: boolean;
|
||||||
@ -165,8 +165,8 @@ export const TimelineActivity = ({
|
|||||||
<StyledTimelineItemContainer>
|
<StyledTimelineItemContainer>
|
||||||
<StyledAvatarContainer>
|
<StyledAvatarContainer>
|
||||||
<Avatar
|
<Avatar
|
||||||
avatarUrl={activity.author.avatarUrl}
|
avatarUrl={activity.author?.avatarUrl}
|
||||||
placeholder={activity.author.name.firstName ?? ''}
|
placeholder={activity.author?.name.firstName ?? ''}
|
||||||
size="sm"
|
size="sm"
|
||||||
type="rounded"
|
type="rounded"
|
||||||
/>
|
/>
|
||||||
@ -175,7 +175,8 @@ export const TimelineActivity = ({
|
|||||||
<StyledItemTitleContainer>
|
<StyledItemTitleContainer>
|
||||||
<StyledItemAuthorText>
|
<StyledItemAuthorText>
|
||||||
<span>
|
<span>
|
||||||
{activity.author.name.firstName} {activity.author.name.lastName}
|
{activity.author?.name.firstName}{' '}
|
||||||
|
{activity.author?.name.lastName}
|
||||||
</span>
|
</span>
|
||||||
created a {activity.type.toLowerCase()}
|
created a {activity.type.toLowerCase()}
|
||||||
</StyledItemAuthorText>
|
</StyledItemAuthorText>
|
||||||
|
|||||||
@ -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;
|
||||||
|
};
|
||||||
@ -1,7 +1,5 @@
|
|||||||
export type ActivityTargetableEntityType = 'Person' | 'Company' | 'Custom';
|
export type ActivityTargetableObject = {
|
||||||
|
|
||||||
export type ActivityTargetableEntity = {
|
|
||||||
id: string;
|
id: string;
|
||||||
type: ActivityTargetableEntityType;
|
targetObjectNameSingular: string;
|
||||||
relatedEntities?: ActivityTargetableEntity[];
|
relatedTargetableObjects?: ActivityTargetableObject[];
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,7 +0,0 @@
|
|||||||
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
|
|
||||||
|
|
||||||
import { ActivityTargetableEntityType } from './ActivityTargetableEntity';
|
|
||||||
|
|
||||||
export type ActivityTargetableEntityForSelect = EntityForSelect & {
|
|
||||||
entityType: ActivityTargetableEntityType;
|
|
||||||
};
|
|
||||||
@ -1,40 +1,41 @@
|
|||||||
import { ActivityTargetableEntity } from '@/activities/types/ActivityTargetableEntity';
|
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||||
import { getTargetableEntitiesWithParents } from '@/activities/utils/getTargetableEntitiesWithParents';
|
import { flattenTargetableObjectsAndTheirRelatedTargetableObjects } from '@/activities/utils/flattenTargetableObjectsAndTheirRelatedTargetableObjects';
|
||||||
|
|
||||||
describe('getTargetableEntitiesWithParents', () => {
|
describe('getTargetableEntitiesWithParents', () => {
|
||||||
it('should return the correct value', () => {
|
it('should return the correct value', () => {
|
||||||
const entities: ActivityTargetableEntity[] = [
|
const entities: ActivityTargetableObject[] = [
|
||||||
{
|
{
|
||||||
id: '1',
|
id: '1',
|
||||||
type: 'Person',
|
targetObjectNameSingular: 'person',
|
||||||
relatedEntities: [
|
relatedTargetableObjects: [
|
||||||
{
|
{
|
||||||
id: '2',
|
id: '2',
|
||||||
type: 'Company',
|
targetObjectNameSingular: 'company',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '4',
|
id: '4',
|
||||||
type: 'Company',
|
targetObjectNameSingular: 'person',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '3',
|
id: '3',
|
||||||
type: 'Custom',
|
targetObjectNameSingular: 'car',
|
||||||
relatedEntities: [
|
relatedTargetableObjects: [
|
||||||
{
|
{
|
||||||
id: '6',
|
id: '6',
|
||||||
type: 'Person',
|
targetObjectNameSingular: 'person',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '5',
|
id: '5',
|
||||||
type: 'Company',
|
targetObjectNameSingular: 'company',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const res = getTargetableEntitiesWithParents(entities);
|
const res =
|
||||||
|
flattenTargetableObjectsAndTheirRelatedTargetableObjects(entities);
|
||||||
|
|
||||||
expect(res).toHaveLength(6);
|
expect(res).toHaveLength(6);
|
||||||
expect(res[0].id).toBe('1');
|
expect(res[0].id).toBe('1');
|
||||||
|
|||||||
@ -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;
|
||||||
|
};
|
||||||
@ -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;
|
||||||
|
};
|
||||||
@ -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;
|
|
||||||
};
|
|
||||||
@ -28,39 +28,44 @@ export const useSpreadsheetCompanyImport = () => {
|
|||||||
...options,
|
...options,
|
||||||
onSubmit: async (data) => {
|
onSubmit: async (data) => {
|
||||||
// TODO: Add better type checking in spreadsheet import later
|
// TODO: Add better type checking in spreadsheet import later
|
||||||
const createInputs = data.validData.map((company) => ({
|
const createInputs = data.validData.map(
|
||||||
name: company.name as string | undefined,
|
(company) =>
|
||||||
domainName: company.domainName as string | undefined,
|
({
|
||||||
...(company.linkedinUrl
|
name: company.name as string | undefined,
|
||||||
? {
|
domainName: company.domainName as string | undefined,
|
||||||
linkedinLink: {
|
...(company.linkedinUrl
|
||||||
label: 'linkedinUrl',
|
? {
|
||||||
url: company.linkedinUrl as string | undefined,
|
linkedinLink: {
|
||||||
},
|
label: 'linkedinUrl',
|
||||||
}
|
url: company.linkedinUrl as string | undefined,
|
||||||
: {}),
|
},
|
||||||
...(company.annualRecurringRevenue
|
}
|
||||||
? {
|
: {}),
|
||||||
annualRecurringRevenue: {
|
...(company.annualRecurringRevenue
|
||||||
amountMicros: Number(company.annualRecurringRevenue),
|
? {
|
||||||
currencyCode: 'USD',
|
annualRecurringRevenue: {
|
||||||
},
|
amountMicros: Number(company.annualRecurringRevenue),
|
||||||
}
|
currencyCode: 'USD',
|
||||||
: {}),
|
},
|
||||||
idealCustomerProfile:
|
}
|
||||||
company.idealCustomerProfile &&
|
: {}),
|
||||||
['true', true].includes(company.idealCustomerProfile),
|
idealCustomerProfile:
|
||||||
...(company.xUrl
|
company.idealCustomerProfile &&
|
||||||
? {
|
['true', true].includes(company.idealCustomerProfile),
|
||||||
xLink: {
|
...(company.xUrl
|
||||||
label: 'xUrl',
|
? {
|
||||||
url: company.xUrl as string | undefined,
|
xLink: {
|
||||||
},
|
label: 'xUrl',
|
||||||
}
|
url: company.xUrl as string | undefined,
|
||||||
: {}),
|
},
|
||||||
address: company.address as string | undefined,
|
}
|
||||||
employees: company.employees ? Number(company.employees) : undefined,
|
: {}),
|
||||||
}));
|
address: company.address as string | undefined,
|
||||||
|
employees: company.employees
|
||||||
|
? Number(company.employees)
|
||||||
|
: undefined,
|
||||||
|
}) as Company,
|
||||||
|
);
|
||||||
// TODO: abstract this part for any object
|
// TODO: abstract this part for any object
|
||||||
try {
|
try {
|
||||||
await createManyCompanies(createInputs);
|
await createManyCompanies(createInputs);
|
||||||
|
|||||||
@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -1,16 +1,16 @@
|
|||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import { getObjectRecordIdentifier } from '@/object-metadata/utils/getObjectRecordIdentifier';
|
import { getObjectRecordIdentifier } from '@/object-metadata/utils/getObjectRecordIdentifier';
|
||||||
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
import { ObjectRecordIdentifier } from '@/object-record/types/ObjectRecordIdentifier';
|
import { ObjectRecordIdentifier } from '@/object-record/types/ObjectRecordIdentifier';
|
||||||
|
|
||||||
export const useMapToObjectRecordIdentifier = ({
|
export const useMapToObjectRecordIdentifier = ({
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
}: {
|
}: {
|
||||||
objectMetadataItem: ObjectMetadataItem;
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
}) => {
|
}): ((record: ObjectRecord) => ObjectRecordIdentifier) => {
|
||||||
return (record: any): ObjectRecordIdentifier => {
|
return (record: ObjectRecord) =>
|
||||||
return getObjectRecordIdentifier({
|
getObjectRecordIdentifier({
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
record,
|
record,
|
||||||
});
|
});
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { useGetObjectOrderByField } from '@/object-metadata/hooks/useGetObjectOr
|
|||||||
import { useMapToObjectRecordIdentifier } from '@/object-metadata/hooks/useMapToObjectRecordIdentifier';
|
import { useMapToObjectRecordIdentifier } from '@/object-metadata/hooks/useMapToObjectRecordIdentifier';
|
||||||
import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector';
|
import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector';
|
||||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||||
|
import { getBasePathToShowPage } from '@/object-metadata/utils/getBasePathToShowPage';
|
||||||
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
|
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
|
||||||
import { useGenerateCreateManyRecordMutation } from '@/object-record/hooks/useGenerateCreateManyRecordMutation';
|
import { useGenerateCreateManyRecordMutation } from '@/object-record/hooks/useGenerateCreateManyRecordMutation';
|
||||||
import { useGenerateCreateOneRecordMutation } from '@/object-record/hooks/useGenerateCreateOneRecordMutation';
|
import { useGenerateCreateOneRecordMutation } from '@/object-record/hooks/useGenerateCreateOneRecordMutation';
|
||||||
@ -121,7 +122,9 @@ export const useObjectMetadataItem = (
|
|||||||
({ name }) => name === 'name',
|
({ name }) => name === 'name',
|
||||||
);
|
);
|
||||||
|
|
||||||
const basePathToShowPage = `/object/${objectMetadataItem.nameSingular}/`;
|
const basePathToShowPage = getBasePathToShowPage({
|
||||||
|
objectMetadataItem,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
labelIdentifierFieldMetadata,
|
labelIdentifierFieldMetadata,
|
||||||
|
|||||||
@ -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<string, ObjectMetadataItem>
|
||||||
|
>({
|
||||||
|
key: 'objectMetadataItemsByNamePluralMapSelector',
|
||||||
|
get: ({ get }) => {
|
||||||
|
const objectMetadataItems = get(objectMetadataItemsState);
|
||||||
|
|
||||||
|
return new Map(
|
||||||
|
objectMetadataItems.map((objectMetadataItem) => [
|
||||||
|
objectMetadataItem.namePlural,
|
||||||
|
objectMetadataItem,
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
@ -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<string, ObjectMetadataItem>
|
||||||
|
>({
|
||||||
|
key: 'objectMetadataItemsByNameSingularMapSelector',
|
||||||
|
get: ({ get }) => {
|
||||||
|
const objectMetadataItems = get(objectMetadataItemsState);
|
||||||
|
|
||||||
|
return new Map(
|
||||||
|
objectMetadataItems.map((objectMetadataItem) => [
|
||||||
|
objectMetadataItem.nameSingular,
|
||||||
|
objectMetadataItem,
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
|
||||||
|
export const getBasePathToShowPage = ({
|
||||||
|
objectMetadataItem,
|
||||||
|
}: {
|
||||||
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
|
}) => {
|
||||||
|
const basePathToShowPage = `/object/${objectMetadataItem.nameSingular}/`;
|
||||||
|
|
||||||
|
return basePathToShowPage;
|
||||||
|
};
|
||||||
@ -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',
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -5,7 +5,7 @@ import { FieldMetadataType } from '~/generated-metadata/graphql';
|
|||||||
|
|
||||||
export const getObjectOrderByField = (
|
export const getObjectOrderByField = (
|
||||||
objectMetadataItem: ObjectMetadataItem,
|
objectMetadataItem: ObjectMetadataItem,
|
||||||
orderBy: OrderBy,
|
orderBy?: OrderBy | null,
|
||||||
): OrderByField => {
|
): OrderByField => {
|
||||||
const labelIdentifierFieldMetadata = objectMetadataItem.fields.find(
|
const labelIdentifierFieldMetadata = objectMetadataItem.fields.find(
|
||||||
(field) =>
|
(field) =>
|
||||||
@ -18,18 +18,18 @@ export const getObjectOrderByField = (
|
|||||||
case FieldMetadataType.FullName:
|
case FieldMetadataType.FullName:
|
||||||
return {
|
return {
|
||||||
[labelIdentifierFieldMetadata.name]: {
|
[labelIdentifierFieldMetadata.name]: {
|
||||||
firstName: orderBy,
|
firstName: orderBy ?? 'AscNullsLast',
|
||||||
lastName: orderBy,
|
lastName: orderBy ?? 'AscNullsLast',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
return {
|
return {
|
||||||
[labelIdentifierFieldMetadata.name]: orderBy,
|
[labelIdentifierFieldMetadata.name]: orderBy ?? 'AscNullsLast',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
createdAt: orderBy,
|
createdAt: orderBy ?? 'DescNullsLast',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
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 { ObjectRecordIdentifier } from '@/object-record/types/ObjectRecordIdentifier';
|
||||||
import { FieldMetadataType } from '~/generated/graphql';
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
import { getLogoUrlFromDomainName } from '~/utils';
|
import { getLogoUrlFromDomainName } from '~/utils';
|
||||||
|
|
||||||
export const getObjectRecordIdentifier = ({
|
export const getObjectRecordIdentifier = ({
|
||||||
@ -9,30 +12,24 @@ export const getObjectRecordIdentifier = ({
|
|||||||
record,
|
record,
|
||||||
}: {
|
}: {
|
||||||
objectMetadataItem: ObjectMetadataItem;
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
record: any;
|
record: ObjectRecord;
|
||||||
}): ObjectRecordIdentifier => {
|
}): ObjectRecordIdentifier => {
|
||||||
const basePathToShowPage = `/object/${objectMetadataItem.nameSingular}/`;
|
switch (objectMetadataItem.nameSingular) {
|
||||||
const linkToShowPage = `${basePathToShowPage}${record.id}`;
|
case CoreObjectNameSingular.Opportunity:
|
||||||
|
return {
|
||||||
if (objectMetadataItem.nameSingular === CoreObjectNameSingular.Opportunity) {
|
id: record.id,
|
||||||
return {
|
name: record?.company?.name,
|
||||||
id: record.id,
|
avatarUrl: record.avatarUrl,
|
||||||
name: record?.company?.name,
|
avatarType: 'rounded',
|
||||||
avatarUrl: record.avatarUrl,
|
};
|
||||||
avatarType: 'rounded',
|
|
||||||
linkToShowPage,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const labelIdentifierFieldMetadata = objectMetadataItem.fields.find(
|
const labelIdentifierFieldMetadataItem =
|
||||||
(field) =>
|
getLabelIdentifierFieldMetadataItem(objectMetadataItem);
|
||||||
field.id === objectMetadataItem.labelIdentifierFieldMetadataId ||
|
|
||||||
field.name === 'name',
|
|
||||||
);
|
|
||||||
|
|
||||||
let labelIdentifierFieldValue = '';
|
let labelIdentifierFieldValue = '';
|
||||||
|
|
||||||
switch (labelIdentifierFieldMetadata?.type) {
|
switch (labelIdentifierFieldMetadataItem?.type) {
|
||||||
case FieldMetadataType.FullName: {
|
case FieldMetadataType.FullName: {
|
||||||
labelIdentifierFieldValue = `${record.name?.firstName ?? ''} ${
|
labelIdentifierFieldValue = `${record.name?.firstName ?? ''} ${
|
||||||
record.name?.lastName ?? ''
|
record.name?.lastName ?? ''
|
||||||
@ -40,8 +37,8 @@ export const getObjectRecordIdentifier = ({
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
labelIdentifierFieldValue = labelIdentifierFieldMetadata
|
labelIdentifierFieldValue = labelIdentifierFieldMetadataItem
|
||||||
? record[labelIdentifierFieldMetadata.name]
|
? record[labelIdentifierFieldMetadataItem.name]
|
||||||
: '';
|
: '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,11 +60,17 @@ export const getObjectRecordIdentifier = ({
|
|||||||
? getLogoUrlFromDomainName(record['domainName'] ?? '')
|
? getLogoUrlFromDomainName(record['domainName'] ?? '')
|
||||||
: imageIdentifierFieldValue ?? null;
|
: imageIdentifierFieldValue ?? null;
|
||||||
|
|
||||||
|
const basePathToShowPage = getBasePathToShowPage({
|
||||||
|
objectMetadataItem,
|
||||||
|
});
|
||||||
|
|
||||||
|
const linkToEntity = `${basePathToShowPage}${record.id}`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: record.id,
|
id: record.id,
|
||||||
name: labelIdentifierFieldValue,
|
name: labelIdentifierFieldValue,
|
||||||
avatarUrl,
|
avatarUrl,
|
||||||
avatarType,
|
avatarType,
|
||||||
linkToShowPage,
|
linkToEntity,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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 (
|
||||||
|
<EntityChip
|
||||||
|
entityId={record.id}
|
||||||
|
name={objectRecordIdentifier.name}
|
||||||
|
avatarType={objectRecordIdentifier.avatarType}
|
||||||
|
avatarUrl={objectRecordIdentifier.avatarUrl ?? undefined}
|
||||||
|
linkToEntity={objectRecordIdentifier.linkToShowPage}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -77,13 +77,6 @@ export const RecordShowPage = () => {
|
|||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
});
|
});
|
||||||
|
|
||||||
const objectMetadataType =
|
|
||||||
objectMetadataItem?.nameSingular === 'company'
|
|
||||||
? 'Company'
|
|
||||||
: objectMetadataItem?.nameSingular === 'person'
|
|
||||||
? 'Person'
|
|
||||||
: 'Custom';
|
|
||||||
|
|
||||||
const useUpdateOneObjectRecordMutation: RecordUpdateHook = () => {
|
const useUpdateOneObjectRecordMutation: RecordUpdateHook = () => {
|
||||||
const updateEntity = ({ variables }: RecordUpdateHookParams) => {
|
const updateEntity = ({ variables }: RecordUpdateHookParams) => {
|
||||||
updateOneRecord?.({
|
updateOneRecord?.({
|
||||||
@ -171,7 +164,7 @@ export const RecordShowPage = () => {
|
|||||||
hasBackButton
|
hasBackButton
|
||||||
Icon={IconBuildingSkyscraper}
|
Icon={IconBuildingSkyscraper}
|
||||||
>
|
>
|
||||||
{record && objectMetadataType !== 'Custom' && (
|
{record && (
|
||||||
<>
|
<>
|
||||||
<PageFavoriteButton
|
<PageFavoriteButton
|
||||||
isFavorite={isFavorite}
|
isFavorite={isFavorite}
|
||||||
@ -181,7 +174,7 @@ export const RecordShowPage = () => {
|
|||||||
key="add"
|
key="add"
|
||||||
entity={{
|
entity={{
|
||||||
id: record.id,
|
id: record.id,
|
||||||
type: objectMetadataType,
|
targetObjectNameSingular: objectMetadataItem?.nameSingular,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ShowPageMoreButton
|
<ShowPageMoreButton
|
||||||
@ -275,15 +268,9 @@ export const RecordShowPage = () => {
|
|||||||
)}
|
)}
|
||||||
</ShowPageLeftContainer>
|
</ShowPageLeftContainer>
|
||||||
<ShowPageRightContainer
|
<ShowPageRightContainer
|
||||||
entity={{
|
targetableObject={{
|
||||||
id: record?.id || '',
|
id: record?.id ?? '',
|
||||||
// TODO: refacto
|
targetObjectNameSingular: objectMetadataItem?.nameSingular,
|
||||||
type:
|
|
||||||
objectMetadataItem?.nameSingular === 'company'
|
|
||||||
? 'Company'
|
|
||||||
: objectMetadataItem?.nameSingular === 'person'
|
|
||||||
? 'Person'
|
|
||||||
: 'Custom',
|
|
||||||
}}
|
}}
|
||||||
timeline
|
timeline
|
||||||
tasks
|
tasks
|
||||||
|
|||||||
@ -10,6 +10,7 @@ export const ChipFieldDisplay = () => {
|
|||||||
basePathToShowPage,
|
basePathToShowPage,
|
||||||
} = useChipField();
|
} = useChipField();
|
||||||
|
|
||||||
|
// TODO: remove this and use ObjectRecordChip instead
|
||||||
const identifiers = identifiersMapper?.(record, objectNameSingular ?? '');
|
const identifiers = identifiersMapper?.(record, objectNameSingular ?? '');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -5,11 +5,10 @@ import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimis
|
|||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
|
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
|
||||||
import { useGenerateEmptyRecord } from '@/object-record/hooks/useGenerateEmptyRecord';
|
import { useGenerateEmptyRecord } from '@/object-record/hooks/useGenerateEmptyRecord';
|
||||||
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
import { capitalize } from '~/utils/string/capitalize';
|
import { capitalize } from '~/utils/string/capitalize';
|
||||||
|
|
||||||
export const useCreateManyRecords = <
|
export const useCreateManyRecords = <T extends ObjectRecord>({
|
||||||
T extends Record<string, unknown> & { id: string },
|
|
||||||
>({
|
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
}: ObjectMetadataItemIdentifier) => {
|
}: ObjectMetadataItemIdentifier) => {
|
||||||
const { triggerOptimisticEffects } = useOptimisticEffect({
|
const { triggerOptimisticEffects } = useOptimisticEffect({
|
||||||
@ -27,17 +26,16 @@ export const useCreateManyRecords = <
|
|||||||
|
|
||||||
const apolloClient = useApolloClient();
|
const apolloClient = useApolloClient();
|
||||||
|
|
||||||
const createManyRecords = async (data: Record<string, any>[]) => {
|
const createManyRecords = async (data: Partial<T>[]) => {
|
||||||
const withIds = data.map((record) => ({
|
const withIds = data.map((record) => ({
|
||||||
...record,
|
...record,
|
||||||
id: (record.id as string) ?? v4(),
|
id: (record.id as string) ?? v4(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
withIds.forEach((record) => {
|
withIds.forEach((record) => {
|
||||||
const emptyRecord: Record<string, unknown> | undefined =
|
const emptyRecord: T | undefined = generateEmptyRecord({
|
||||||
generateEmptyRecord({
|
id: record.id,
|
||||||
id: record.id,
|
} as T);
|
||||||
});
|
|
||||||
|
|
||||||
if (emptyRecord) {
|
if (emptyRecord) {
|
||||||
triggerOptimisticEffects({
|
triggerOptimisticEffects({
|
||||||
|
|||||||
@ -34,7 +34,7 @@ export const useCreateOneRecord = <T>({
|
|||||||
const createOneRecord = async (input: Record<string, any>) => {
|
const createOneRecord = async (input: Record<string, any>) => {
|
||||||
const recordId = v4();
|
const recordId = v4();
|
||||||
|
|
||||||
const generatedEmptyRecord = generateEmptyRecord<Record<string, unknown>>({
|
const generatedEmptyRecord = generateEmptyRecord({
|
||||||
id: recordId,
|
id: recordId,
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
...input,
|
...input,
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
import { generateEmptyFieldValue } from '@/object-record/utils/generateEmptyFieldValue';
|
import { generateEmptyFieldValue } from '@/object-record/utils/generateEmptyFieldValue';
|
||||||
|
|
||||||
export const useGenerateEmptyRecord = ({
|
export const useGenerateEmptyRecord = ({
|
||||||
@ -7,11 +8,11 @@ export const useGenerateEmptyRecord = ({
|
|||||||
objectMetadataItem: ObjectMetadataItem;
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
}) => {
|
}) => {
|
||||||
// Todo fix typing once we generate the return base on Metadata
|
// Todo fix typing once we generate the return base on Metadata
|
||||||
const generateEmptyRecord = <T>(input: Partial<T> & { id: string }) => {
|
const generateEmptyRecord = <T extends ObjectRecord>(input: T) => {
|
||||||
// Todo replace this by runtime typing
|
// Todo replace this by runtime typing
|
||||||
const validatedInput = input as { id: string } & { [key: string]: any };
|
const validatedInput = input as T;
|
||||||
|
|
||||||
const emptyRecord = {} as Record<string, any>;
|
const emptyRecord = {} as any;
|
||||||
|
|
||||||
for (const fieldMetadataItem of objectMetadataItem.fields) {
|
for (const fieldMetadataItem of objectMetadataItem.fields) {
|
||||||
emptyRecord[fieldMetadataItem.name] =
|
emptyRecord[fieldMetadataItem.name] =
|
||||||
@ -19,7 +20,7 @@ export const useGenerateEmptyRecord = ({
|
|||||||
generateEmptyFieldValue(fieldMetadataItem);
|
generateEmptyFieldValue(fieldMetadataItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
return emptyRecord;
|
return emptyRecord as T;
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -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')}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
};
|
||||||
@ -1,7 +1,6 @@
|
|||||||
import { gql } from '@apollo/client';
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
import { useMapFieldMetadataToGraphQLQuery } from '@/object-metadata/hooks/useMapFieldMetadataToGraphQLQuery';
|
import { useMapFieldMetadataToGraphQLQuery } from '@/object-metadata/hooks/useMapFieldMetadataToGraphQLQuery';
|
||||||
import { EMPTY_QUERY } from '@/object-metadata/hooks/useObjectMetadataItem';
|
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import { capitalize } from '~/utils/string/capitalize';
|
import { capitalize } from '~/utils/string/capitalize';
|
||||||
|
|
||||||
@ -14,10 +13,6 @@ export const useGenerateFindManyRecordsQuery = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const mapFieldMetadataToGraphQLQuery = useMapFieldMetadataToGraphQLQuery();
|
const mapFieldMetadataToGraphQLQuery = useMapFieldMetadataToGraphQLQuery();
|
||||||
|
|
||||||
if (!objectMetadataItem) {
|
|
||||||
return EMPTY_QUERY;
|
|
||||||
}
|
|
||||||
|
|
||||||
return gql`
|
return gql`
|
||||||
query FindMany${capitalize(
|
query FindMany${capitalize(
|
||||||
objectMetadataItem.namePlural,
|
objectMetadataItem.namePlural,
|
||||||
|
|||||||
@ -67,13 +67,6 @@ export const useRecordTableContextMenuEntries = (
|
|||||||
|
|
||||||
const { createFavorite, favorites, deleteFavorite } = useFavorites();
|
const { createFavorite, favorites, deleteFavorite } = useFavorites();
|
||||||
|
|
||||||
const objectMetadataType =
|
|
||||||
objectNameSingular === 'company'
|
|
||||||
? 'Company'
|
|
||||||
: objectNameSingular === 'person'
|
|
||||||
? 'Person'
|
|
||||||
: 'Custom';
|
|
||||||
|
|
||||||
const handleFavoriteButtonClick = useRecoilCallback(({ snapshot }) => () => {
|
const handleFavoriteButtonClick = useRecoilCallback(({ snapshot }) => () => {
|
||||||
const selectedRowIds = injectSelectorSnapshotValueWithRecordTableScopeId(
|
const selectedRowIds = injectSelectorSnapshotValueWithRecordTableScopeId(
|
||||||
snapshot,
|
snapshot,
|
||||||
@ -212,14 +205,14 @@ export const useRecordTableContextMenuEntries = (
|
|||||||
label: 'Task',
|
label: 'Task',
|
||||||
Icon: IconCheckbox,
|
Icon: IconCheckbox,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
openCreateActivityDrawer('Task', objectMetadataType);
|
openCreateActivityDrawer('Task', objectNameSingular);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Note',
|
label: 'Note',
|
||||||
Icon: IconNotes,
|
Icon: IconNotes,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
openCreateActivityDrawer('Note', objectMetadataType);
|
openCreateActivityDrawer('Note', objectNameSingular);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
...(dataExecuteQuickActionOnmentEnabled
|
...(dataExecuteQuickActionOnmentEnabled
|
||||||
|
|||||||
@ -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<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const [searchFilter, setSearchFilter] = useState<string>('');
|
||||||
|
|
||||||
|
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<HTMLInputElement>) => {
|
||||||
|
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 (
|
||||||
|
<DropdownMenu ref={containerRef} data-select-disable>
|
||||||
|
<DropdownMenuSearchInput
|
||||||
|
value={searchFilter}
|
||||||
|
onChange={handleFilterChange}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItemsContainer hasMaxHeight>
|
||||||
|
{loading ? (
|
||||||
|
<MenuItem text="Loading..." />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<SelectableList
|
||||||
|
selectableListId="multiple-entity-select-list"
|
||||||
|
selectableItemIdArray={selectableItemIds}
|
||||||
|
hotkeyScope={RelationPickerHotkeyScope.RelationPicker}
|
||||||
|
onEnter={(recordId) => {
|
||||||
|
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) => (
|
||||||
|
<StyledSelectableItem
|
||||||
|
itemId={objectRecordForSelect.record.id}
|
||||||
|
key={objectRecordForSelect.record.id + v4()}
|
||||||
|
>
|
||||||
|
<MenuItemMultiSelectAvatar
|
||||||
|
selected={internalSelectedRecords?.some(
|
||||||
|
(selectedRecord) => {
|
||||||
|
return (
|
||||||
|
selectedRecord.record.id ===
|
||||||
|
objectRecordForSelect.record.id
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
onSelectChange={(newCheckedValue) =>
|
||||||
|
handleSelectChange(objectRecordForSelect, newCheckedValue)
|
||||||
|
}
|
||||||
|
avatar={
|
||||||
|
<Avatar
|
||||||
|
avatarUrl={
|
||||||
|
objectRecordForSelect.recordIdentifier.avatarUrl
|
||||||
|
}
|
||||||
|
colorId={objectRecordForSelect.record.id}
|
||||||
|
placeholder={
|
||||||
|
objectRecordForSelect.recordIdentifier.name
|
||||||
|
}
|
||||||
|
size="md"
|
||||||
|
type={
|
||||||
|
objectRecordForSelect.recordIdentifier.avatarType ??
|
||||||
|
'rounded'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
text={objectRecordForSelect.recordIdentifier.name}
|
||||||
|
/>
|
||||||
|
</StyledSelectableItem>
|
||||||
|
))}
|
||||||
|
</SelectableList>
|
||||||
|
{entitiesInDropdown?.length === 0 && <MenuItem text="No result" />}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DropdownMenuItemsContainer>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -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<MultiObjectRecordQueryResult>(multiSelectQueryForSelectedIds, {
|
||||||
|
variables: {
|
||||||
|
...selectedAndMatchesSearchFilterTextFilterPerMetadataItem,
|
||||||
|
...orderByFieldPerMetadataItem,
|
||||||
|
...limitPerMetadataItem,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
objectRecordForSelectArray: selectedAndMatchesSearchFilterObjectRecords,
|
||||||
|
} = useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray({
|
||||||
|
multiObjectRecordsQueryResult:
|
||||||
|
selectedAndMatchesSearchFilterObjectRecordsQueryResult,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
selectedAndMatchesSearchFilterObjectRecordsLoading,
|
||||||
|
selectedAndMatchesSearchFilterObjectRecords,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -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<MultiObjectRecordQueryResult>(multiSelectQuery, {
|
||||||
|
variables: {
|
||||||
|
...objectRecordsToSelectAndMatchesSearchFilterTextFilterPerMetadataItem,
|
||||||
|
...orderByFieldPerMetadataItem,
|
||||||
|
...limitPerMetadataItem,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
objectRecordForSelectArray: toSelectAndMatchesSearchFilterObjectRecords,
|
||||||
|
} = useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray({
|
||||||
|
multiObjectRecordsQueryResult:
|
||||||
|
toSelectAndMatchesSearchFilterObjectRecordsQueryResult,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
toSelectAndMatchesSearchFilterObjectRecordsLoading,
|
||||||
|
toSelectAndMatchesSearchFilterObjectRecords,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -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<MultiObjectRecordQueryResult>(multiSelectQueryForSelectedIds, {
|
||||||
|
variables: {
|
||||||
|
...selectedIdFilterPerMetadataItem,
|
||||||
|
...orderByFieldPerMetadataItem,
|
||||||
|
...limitPerMetadataItem,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { objectRecordForSelectArray: selectedObjectRecords } =
|
||||||
|
useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray({
|
||||||
|
multiObjectRecordsQueryResult: selectedObjectRecordsQueryResult,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
selectedObjectRecordsLoading,
|
||||||
|
selectedObjectRecords,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -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<ObjectRecordQueryFilter>(
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export type ObjectRecord = Record<string, any> & { id: string };
|
||||||
@ -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;
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
|
|
||||||
|
export type ObjectRecordEdge = {
|
||||||
|
node: ObjectRecord;
|
||||||
|
cursor: string;
|
||||||
|
};
|
||||||
@ -5,5 +5,6 @@ export type ObjectRecordIdentifier = {
|
|||||||
name: string;
|
name: string;
|
||||||
avatarUrl?: string | null;
|
avatarUrl?: string | null;
|
||||||
avatarType?: AvatarType | null;
|
avatarType?: AvatarType | null;
|
||||||
|
linkToEntity?: string;
|
||||||
linkToShowPage?: string;
|
linkToShowPage?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -29,33 +29,37 @@ export const useSpreadsheetPersonImport = () => {
|
|||||||
...options,
|
...options,
|
||||||
onSubmit: async (data) => {
|
onSubmit: async (data) => {
|
||||||
// TODO: Add better type checking in spreadsheet import later
|
// TODO: Add better type checking in spreadsheet import later
|
||||||
const createInputs = data.validData.map((person) => ({
|
const createInputs = data.validData.map(
|
||||||
id: v4(),
|
(person) =>
|
||||||
name: {
|
({
|
||||||
firstName: person.firstName as string | undefined,
|
id: v4(),
|
||||||
lastName: person.lastName as string | undefined,
|
name: {
|
||||||
},
|
firstName: person.firstName as string | undefined,
|
||||||
email: person.email as string | undefined,
|
lastName: person.lastName as string | undefined,
|
||||||
...(person.linkedinUrl
|
},
|
||||||
? {
|
email: person.email as string | undefined,
|
||||||
linkedinLink: {
|
...(person.linkedinUrl
|
||||||
label: 'linkedinUrl',
|
? {
|
||||||
url: person.linkedinUrl as string | undefined,
|
linkedinLink: {
|
||||||
},
|
label: 'linkedinUrl',
|
||||||
}
|
url: person.linkedinUrl as string | undefined,
|
||||||
: {}),
|
},
|
||||||
...(person.xUrl
|
}
|
||||||
? {
|
: {}),
|
||||||
xLink: {
|
...(person.xUrl
|
||||||
label: 'xUrl',
|
? {
|
||||||
url: person.xUrl as string | undefined,
|
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,
|
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
|
// TODO: abstract this part for any object
|
||||||
try {
|
try {
|
||||||
await createManyPeople(createInputs);
|
await createManyPeople(createInputs);
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { isNonEmptyString } from '@sniptt/guards';
|
|||||||
|
|
||||||
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
|
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
|
||||||
import { Avatar, AvatarType } from '@/users/components/Avatar';
|
import { Avatar, AvatarType } from '@/users/components/Avatar';
|
||||||
|
import { Nullable } from '~/types/Nullable';
|
||||||
|
|
||||||
import { Chip, ChipVariant } from './Chip';
|
import { Chip, ChipVariant } from './Chip';
|
||||||
|
|
||||||
@ -13,7 +14,7 @@ export type EntityChipProps = {
|
|||||||
entityId: string;
|
entityId: string;
|
||||||
name: string;
|
name: string;
|
||||||
avatarUrl?: string;
|
avatarUrl?: string;
|
||||||
avatarType?: AvatarType;
|
avatarType?: Nullable<AvatarType>;
|
||||||
variant?: EntityChipVariant;
|
variant?: EntityChipVariant;
|
||||||
LeftIcon?: IconComponent;
|
LeftIcon?: IconComponent;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
|||||||
@ -124,13 +124,13 @@ export const Checkbox = ({
|
|||||||
React.useState<boolean>(false);
|
React.useState<boolean>(false);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
setIsInternalChecked(checked);
|
setIsInternalChecked(checked ?? false);
|
||||||
}, [checked]);
|
}, [checked]);
|
||||||
|
|
||||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
onChange?.(event);
|
onChange?.(event);
|
||||||
onCheckedChange?.(event.target.checked);
|
onCheckedChange?.(event.target.checked);
|
||||||
setIsInternalChecked(event.target.checked);
|
setIsInternalChecked(event.target.checked ?? false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkboxId = 'checkbox' + v4();
|
const checkboxId = 'checkbox' + v4();
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import styled from '@emotion/styled';
|
|||||||
|
|
||||||
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
|
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
|
||||||
import { ActivityType } from '@/activities/types/Activity';
|
import { ActivityType } from '@/activities/types/Activity';
|
||||||
import { ActivityTargetableEntity } from '@/activities/types/ActivityTargetableEntity';
|
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||||
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
|
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
|
||||||
import { IconCheckbox, IconNotes, IconPlus } from '@/ui/display/icon/index';
|
import { IconCheckbox, IconNotes, IconPlus } from '@/ui/display/icon/index';
|
||||||
import { IconButton } from '@/ui/input/button/components/IconButton';
|
import { IconButton } from '@/ui/input/button/components/IconButton';
|
||||||
@ -21,13 +21,13 @@ const StyledContainer = styled.div`
|
|||||||
export const ShowPageAddButton = ({
|
export const ShowPageAddButton = ({
|
||||||
entity,
|
entity,
|
||||||
}: {
|
}: {
|
||||||
entity: ActivityTargetableEntity;
|
entity: ActivityTargetableObject;
|
||||||
}) => {
|
}) => {
|
||||||
const { closeDropdown, toggleDropdown } = useDropdown('add-show-page');
|
const { closeDropdown, toggleDropdown } = useDropdown('add-show-page');
|
||||||
const openCreateActivity = useOpenCreateActivityDrawer();
|
const openCreateActivity = useOpenCreateActivityDrawer();
|
||||||
|
|
||||||
const handleSelect = (type: ActivityType) => {
|
const handleSelect = (type: ActivityType) => {
|
||||||
openCreateActivity({ type, targetableEntities: [entity] });
|
openCreateActivity({ type, targetableObjects: [entity] });
|
||||||
closeDropdown();
|
closeDropdown();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -3,9 +3,10 @@ import styled from '@emotion/styled';
|
|||||||
import { Threads } from '@/activities/emails/components/Threads';
|
import { Threads } from '@/activities/emails/components/Threads';
|
||||||
import { Attachments } from '@/activities/files/components/Attachments';
|
import { Attachments } from '@/activities/files/components/Attachments';
|
||||||
import { Notes } from '@/activities/notes/components/Notes';
|
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 { 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 {
|
import {
|
||||||
IconCheckbox,
|
IconCheckbox,
|
||||||
IconMail,
|
IconMail,
|
||||||
@ -40,7 +41,7 @@ const StyledTabListContainer = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
type ShowPageRightContainerProps = {
|
type ShowPageRightContainerProps = {
|
||||||
entity?: ActivityTargetableEntity;
|
targetableObject?: ActivityTargetableObject;
|
||||||
timeline?: boolean;
|
timeline?: boolean;
|
||||||
tasks?: boolean;
|
tasks?: boolean;
|
||||||
notes?: boolean;
|
notes?: boolean;
|
||||||
@ -48,7 +49,7 @@ type ShowPageRightContainerProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const ShowPageRightContainer = ({
|
export const ShowPageRightContainer = ({
|
||||||
entity,
|
targetableObject,
|
||||||
timeline,
|
timeline,
|
||||||
tasks,
|
tasks,
|
||||||
notes,
|
notes,
|
||||||
@ -60,42 +61,44 @@ export const ShowPageRightContainer = ({
|
|||||||
ShowPageRecoilScopeContext,
|
ShowPageRecoilScopeContext,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!entity) return <></>;
|
if (!targetableObject) return <></>;
|
||||||
|
|
||||||
|
const targetableObjectIsStandardObject = isStandardObject(
|
||||||
|
targetableObject.targetObjectNameSingular,
|
||||||
|
);
|
||||||
|
|
||||||
const TASK_TABS = [
|
const TASK_TABS = [
|
||||||
{
|
{
|
||||||
id: 'timeline',
|
id: 'timeline',
|
||||||
title: 'Timeline',
|
title: 'Timeline',
|
||||||
Icon: IconTimelineEvent,
|
Icon: IconTimelineEvent,
|
||||||
hide: !timeline,
|
hide: !timeline,
|
||||||
disabled: entity.type === 'Custom',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'tasks',
|
id: 'tasks',
|
||||||
title: 'Tasks',
|
title: 'Tasks',
|
||||||
Icon: IconCheckbox,
|
Icon: IconCheckbox,
|
||||||
hide: !tasks,
|
hide: !tasks,
|
||||||
disabled: entity.type === 'Custom',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'notes',
|
id: 'notes',
|
||||||
title: 'Notes',
|
title: 'Notes',
|
||||||
Icon: IconNotes,
|
Icon: IconNotes,
|
||||||
hide: !notes,
|
hide: !notes,
|
||||||
disabled: entity.type === 'Custom',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'files',
|
id: 'files',
|
||||||
title: 'Files',
|
title: 'Files',
|
||||||
Icon: IconPaperclip,
|
Icon: IconPaperclip,
|
||||||
hide: !notes,
|
hide: !notes,
|
||||||
disabled: entity.type === 'Custom',
|
disabled: !targetableObjectIsStandardObject,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'emails',
|
id: 'emails',
|
||||||
title: 'Emails',
|
title: 'Emails',
|
||||||
Icon: IconMail,
|
Icon: IconMail,
|
||||||
hide: !emails,
|
hide: !emails,
|
||||||
disabled: !isMessagingEnabled || entity.type === 'Custom',
|
disabled: !isMessagingEnabled || !targetableObjectIsStandardObject,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -104,11 +107,17 @@ export const ShowPageRightContainer = ({
|
|||||||
<StyledTabListContainer>
|
<StyledTabListContainer>
|
||||||
<TabList context={ShowPageRecoilScopeContext} tabs={TASK_TABS} />
|
<TabList context={ShowPageRecoilScopeContext} tabs={TASK_TABS} />
|
||||||
</StyledTabListContainer>
|
</StyledTabListContainer>
|
||||||
{activeTabId === 'timeline' && <Timeline entity={entity} />}
|
{activeTabId === 'timeline' && (
|
||||||
{activeTabId === 'tasks' && <EntityTasks entity={entity} />}
|
<Timeline targetableObject={targetableObject} />
|
||||||
{activeTabId === 'notes' && <Notes entity={entity} />}
|
)}
|
||||||
{activeTabId === 'files' && <Attachments targetableEntity={entity} />}
|
{activeTabId === 'tasks' && (
|
||||||
{activeTabId === 'emails' && <Threads entity={entity} />}
|
<ObjectTasks targetableObject={targetableObject} />
|
||||||
|
)}
|
||||||
|
{activeTabId === 'notes' && <Notes targetableObject={targetableObject} />}
|
||||||
|
{activeTabId === 'files' && (
|
||||||
|
<Attachments targetableObject={targetableObject} />
|
||||||
|
)}
|
||||||
|
{activeTabId === 'emails' && <Threads entity={targetableObject} />}
|
||||||
</StyledShowPageRightContainer>
|
</StyledShowPageRightContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
import { OverflowingTextWithTooltip } from '@/ui/display/tooltip/OverflowingTextWithTooltip';
|
||||||
import { Checkbox } from '@/ui/input/components/Checkbox';
|
import { Checkbox } from '@/ui/input/components/Checkbox';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -14,6 +15,7 @@ const StyledLeftContentWithCheckboxContainer = styled.div`
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
gap: ${({ theme }) => theme.spacing(2)};
|
gap: ${({ theme }) => theme.spacing(2)};
|
||||||
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type MenuItemMultiSelectAvatarProps = {
|
type MenuItemMultiSelectAvatarProps = {
|
||||||
@ -48,7 +50,7 @@ export const MenuItemMultiSelectAvatar = ({
|
|||||||
<StyledMenuItemLeftContent>
|
<StyledMenuItemLeftContent>
|
||||||
{avatar}
|
{avatar}
|
||||||
<StyledMenuItemLabel hasLeftIcon={!!avatar}>
|
<StyledMenuItemLabel hasLeftIcon={!!avatar}>
|
||||||
{text}
|
<OverflowingTextWithTooltip text={text} />
|
||||||
</StyledMenuItemLabel>
|
</StyledMenuItemLabel>
|
||||||
</StyledMenuItemLeftContent>
|
</StyledMenuItemLeftContent>
|
||||||
</StyledLeftContentWithCheckboxContainer>
|
</StyledLeftContentWithCheckboxContainer>
|
||||||
|
|||||||
@ -71,10 +71,11 @@ export const StyledMenuItemBase = styled.li<MenuItemBaseProps>`
|
|||||||
export const StyledMenuItemLabel = styled.div<{ hasLeftIcon: boolean }>`
|
export const StyledMenuItemLabel = styled.div<{ hasLeftIcon: boolean }>`
|
||||||
font-size: ${({ theme }) => theme.font.size.sm};
|
font-size: ${({ theme }) => theme.font.size.sm};
|
||||||
font-weight: ${({ theme }) => theme.font.weight.regular};
|
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||||
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding-left: ${({ theme, hasLeftIcon }) =>
|
padding-left: ${({ theme, hasLeftIcon }) =>
|
||||||
hasLeftIcon ? '' : theme.spacing(1)};
|
hasLeftIcon ? '' : theme.spacing(1)};
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { isNonEmptyString } from '@sniptt/guards';
|
import { isNonEmptyString } from '@sniptt/guards';
|
||||||
|
|
||||||
|
import { Nullable } from '~/types/Nullable';
|
||||||
import { stringToHslColor } from '~/utils/string-to-hsl';
|
import { stringToHslColor } from '~/utils/string-to-hsl';
|
||||||
|
|
||||||
import { getImageAbsoluteURIOrBase64 } from '../utils/getProfilePictureAbsoluteURI';
|
import { getImageAbsoluteURIOrBase64 } from '../utils/getProfilePictureAbsoluteURI';
|
||||||
@ -16,7 +17,7 @@ export type AvatarProps = {
|
|||||||
size?: AvatarSize;
|
size?: AvatarSize;
|
||||||
placeholder: string | undefined;
|
placeholder: string | undefined;
|
||||||
colorId?: string;
|
colorId?: string;
|
||||||
type?: AvatarType;
|
type?: Nullable<AvatarType>;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -34,6 +34,7 @@ type MockedActivity = Pick<
|
|||||||
| 'activityId'
|
| 'activityId'
|
||||||
| 'personId'
|
| 'personId'
|
||||||
| 'companyId'
|
| 'companyId'
|
||||||
|
| 'targetObjectNameSingular'
|
||||||
> & {
|
> & {
|
||||||
activity: Pick<Activity, 'id' | 'createdAt' | 'updatedAt'>;
|
activity: Pick<Activity, 'id' | 'createdAt' | 'updatedAt'>;
|
||||||
person?: Pick<Person, 'id' | 'name' | 'avatarUrl'> | null;
|
person?: Pick<Person, 'id' | 'name' | 'avatarUrl'> | null;
|
||||||
@ -98,6 +99,7 @@ export const mockedActivities: Array<MockedActivity> = [
|
|||||||
id: '89bb825c-171e-4bcc-9cf7-43448d6fb300',
|
id: '89bb825c-171e-4bcc-9cf7-43448d6fb300',
|
||||||
createdAt: '2023-04-26T10:12:42.33625+00:00',
|
createdAt: '2023-04-26T10:12:42.33625+00:00',
|
||||||
updatedAt: '2023-04-26T10:23:42.33625+00:00',
|
updatedAt: '2023-04-26T10:23:42.33625+00:00',
|
||||||
|
targetObjectNameSingular: 'company',
|
||||||
personId: null,
|
personId: null,
|
||||||
companyId: '89bb825c-171e-4bcc-9cf7-43448d6fb280',
|
companyId: '89bb825c-171e-4bcc-9cf7-43448d6fb280',
|
||||||
company: {
|
company: {
|
||||||
@ -118,6 +120,7 @@ export const mockedActivities: Array<MockedActivity> = [
|
|||||||
id: '89bb825c-171e-4bcc-9cf7-43448d6fb301',
|
id: '89bb825c-171e-4bcc-9cf7-43448d6fb301',
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
|
targetObjectNameSingular: 'company',
|
||||||
personId: null,
|
personId: null,
|
||||||
companyId: 'b396e6b9-dc5c-4643-bcff-61b6cf7523ae',
|
companyId: 'b396e6b9-dc5c-4643-bcff-61b6cf7523ae',
|
||||||
company: {
|
company: {
|
||||||
@ -160,6 +163,7 @@ export const mockedActivities: Array<MockedActivity> = [
|
|||||||
id: '89bb825c-171e-4bcc-9cf7-43448d6fb278t',
|
id: '89bb825c-171e-4bcc-9cf7-43448d6fb278t',
|
||||||
createdAt: '2023-04-26T10:12:42.33625+00:00',
|
createdAt: '2023-04-26T10:12:42.33625+00:00',
|
||||||
updatedAt: '2023-04-26T10:23:42.33625+00:00',
|
updatedAt: '2023-04-26T10:23:42.33625+00:00',
|
||||||
|
targetObjectNameSingular: 'person',
|
||||||
personId: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b', // Alexandre
|
personId: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b', // Alexandre
|
||||||
person: {
|
person: {
|
||||||
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b',
|
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b',
|
||||||
@ -185,6 +189,7 @@ export const mockedActivities: Array<MockedActivity> = [
|
|||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
personId: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6d', // Jean d'Eau
|
personId: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6d', // Jean d'Eau
|
||||||
companyId: null,
|
companyId: null,
|
||||||
|
targetObjectNameSingular: 'person',
|
||||||
company: null,
|
company: null,
|
||||||
person: {
|
person: {
|
||||||
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6d',
|
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6d',
|
||||||
|
|||||||
@ -111,8 +111,9 @@ export class RelationFieldAliasFactory {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
let relationAlias = fieldMetadata.isCustom
|
let relationAlias = fieldMetadata.isCustom
|
||||||
? `${fieldKey}: ${fieldMetadata.targetColumnMap.value}`
|
? `${fieldKey}: ${referencedObjectMetadata.targetTableName}`
|
||||||
: fieldKey;
|
: fieldKey;
|
||||||
|
|
||||||
// For one to one relations, pg_graphql use the targetTableName on the side that is not storing the foreign key
|
// For one to one relations, pg_graphql use the targetTableName on the side that is not storing the foreign key
|
||||||
|
|||||||
@ -11,7 +11,6 @@ export default async () => {
|
|||||||
|
|
||||||
await prisma.$transaction(
|
await prisma.$transaction(
|
||||||
entities.map((entity) => {
|
entities.map((entity) => {
|
||||||
console.log('entity: ', entity);
|
|
||||||
return prisma[entity].deleteMany();
|
return prisma[entity].deleteMany();
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user