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 { CompanyChip } from '@/companies/components/CompanyChip';
|
||||
import { PersonChip } from '@/people/components/PersonChip';
|
||||
import { getLogoUrlFromDomainName } from '~/utils';
|
||||
import { ActivityTargetObjectRecord } from '@/activities/types/ActivityTargetObject';
|
||||
import { RecordChip } from '@/object-record/components/RecordChip';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
@ -10,37 +9,21 @@ const StyledContainer = styled.div`
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
// TODO: fix edges pagination formatting on n+N
|
||||
export const ActivityTargetChips = ({ targets }: { targets?: any }) => {
|
||||
if (!targets) {
|
||||
return null;
|
||||
}
|
||||
|
||||
export const ActivityTargetChips = ({
|
||||
activityTargetObjectRecords,
|
||||
}: {
|
||||
activityTargetObjectRecords: ActivityTargetObjectRecord[];
|
||||
}) => {
|
||||
return (
|
||||
<StyledContainer>
|
||||
{targets?.map(({ company, person }: any) => {
|
||||
if (company) {
|
||||
return (
|
||||
<CompanyChip
|
||||
key={company.id}
|
||||
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 <></>;
|
||||
})}
|
||||
{activityTargetObjectRecords?.map((activityTargetObjectRecord) => (
|
||||
<RecordChip
|
||||
record={activityTargetObjectRecord.targetObjectRecord}
|
||||
objectNameSingular={
|
||||
activityTargetObjectRecord.targetObjectMetadataItem.nameSingular
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@ -4,7 +4,7 @@ import styled from '@emotion/styled';
|
||||
import { ThreadPreview } from '@/activities/emails/components/ThreadPreview';
|
||||
import { getTimelineThreadsFromCompanyId } from '@/activities/emails/queries/getTimelineThreadsFromCompanyId';
|
||||
import { getTimelineThreadsFromPersonId } from '@/activities/emails/queries/getTimelineThreadsFromPersonId';
|
||||
import { ActivityTargetableEntity } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import {
|
||||
H1Title,
|
||||
H1TitleFontColor,
|
||||
@ -29,14 +29,14 @@ const StyledEmailCount = styled.span`
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
`;
|
||||
|
||||
export const Threads = ({ entity }: { entity: ActivityTargetableEntity }) => {
|
||||
export const Threads = ({ entity }: { entity: ActivityTargetableObject }) => {
|
||||
const threadQuery =
|
||||
entity.type === 'Person'
|
||||
entity.targetObjectNameSingular === 'person'
|
||||
? getTimelineThreadsFromPersonId
|
||||
: getTimelineThreadsFromCompanyId;
|
||||
|
||||
const threadQueryVariables =
|
||||
entity.type === 'Person'
|
||||
entity.targetObjectNameSingular === 'person'
|
||||
? { personId: entity.id }
|
||||
: { companyId: entity.id };
|
||||
|
||||
@ -50,7 +50,7 @@ export const Threads = ({ entity }: { entity: ActivityTargetableEntity }) => {
|
||||
|
||||
const timelineThreads: TimelineThread[] =
|
||||
threads.data[
|
||||
entity.type === 'Person'
|
||||
entity.targetObjectNameSingular === 'Person'
|
||||
? 'getTimelineThreadsFromPersonId'
|
||||
: 'getTimelineThreadsFromCompanyId'
|
||||
];
|
||||
|
||||
@ -7,7 +7,7 @@ const meta: Meta<typeof Threads> = {
|
||||
component: Threads,
|
||||
args: {
|
||||
entity: {
|
||||
type: 'Person',
|
||||
targetObjectNameSingular: 'person',
|
||||
id: '52ba3fd0-c723-4482-8b11-5fc24a587c71',
|
||||
},
|
||||
},
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
import { ChangeEvent, useRef } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { isNonEmptyArray } from '@sniptt/guards';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { AttachmentList } from '@/activities/files/components/AttachmentList';
|
||||
import { useAttachments } from '@/activities/files/hooks/useAttachments';
|
||||
import { Attachment } from '@/activities/files/types/Attachment';
|
||||
import { getFileType } from '@/activities/files/utils/getFileType';
|
||||
import { ActivityTargetableEntity } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||
@ -56,13 +58,13 @@ const StyledFileInput = styled.input`
|
||||
`;
|
||||
|
||||
export const Attachments = ({
|
||||
targetableEntity,
|
||||
targetableObject,
|
||||
}: {
|
||||
targetableEntity: ActivityTargetableEntity;
|
||||
targetableObject: ActivityTargetableObject;
|
||||
}) => {
|
||||
const inputFileRef = useRef<HTMLInputElement>(null);
|
||||
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
||||
const { attachments } = useAttachments(targetableEntity);
|
||||
const { attachments } = useAttachments(targetableObject);
|
||||
|
||||
const [uploadFile] = useUploadFileMutation();
|
||||
|
||||
@ -92,22 +94,23 @@ export const Attachments = ({
|
||||
if (!attachmentUrl) {
|
||||
return;
|
||||
}
|
||||
if (!createOneAttachment) {
|
||||
return;
|
||||
}
|
||||
|
||||
await createOneAttachment({
|
||||
const targetableObjectFieldIdName = getActivityTargetObjectFieldIdName({
|
||||
nameSingular: targetableObject.targetObjectNameSingular,
|
||||
});
|
||||
|
||||
const attachmentToCreate = {
|
||||
authorId: currentWorkspaceMember?.id,
|
||||
name: file.name,
|
||||
fullPath: attachmentUrl,
|
||||
type: getFileType(file.name),
|
||||
companyId:
|
||||
targetableEntity.type === 'Company' ? targetableEntity.id : null,
|
||||
personId: targetableEntity.type === 'Person' ? targetableEntity.id : null,
|
||||
});
|
||||
[targetableObjectFieldIdName]: targetableObject.id,
|
||||
};
|
||||
|
||||
await createOneAttachment(attachmentToCreate);
|
||||
};
|
||||
|
||||
if (attachments?.length === 0 && targetableEntity.type !== 'Custom') {
|
||||
if (!isNonEmptyArray(attachments)) {
|
||||
return (
|
||||
<StyledTaskGroupEmptyContainer>
|
||||
<StyledFileInput
|
||||
|
||||
@ -1,15 +1,21 @@
|
||||
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 { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
|
||||
import { ActivityTargetableEntity } from '../../types/ActivityTargetableEntity';
|
||||
|
||||
// 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({
|
||||
objectNameSingular: CoreObjectNameSingular.Attachment,
|
||||
filter: {
|
||||
[entity.type === 'Company' ? 'companyId' : 'personId']: { eq: entity.id },
|
||||
[targetableObjectFieldIdName]: {
|
||||
eq: targetableObject.id,
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
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 { ActivityTarget } from '@/activities/types/ActivityTarget';
|
||||
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useCreateManyRecords } from '@/object-record/hooks/useCreateManyRecords';
|
||||
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
|
||||
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
|
||||
@ -14,13 +16,13 @@ import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope
|
||||
|
||||
import { activityTargetableEntityArrayState } from '../states/activityTargetableEntityArrayState';
|
||||
import { viewableActivityIdState } from '../states/viewableActivityIdState';
|
||||
import { ActivityTargetableEntity } from '../types/ActivityTargetableEntity';
|
||||
import { getTargetableEntitiesWithParents } from '../utils/getTargetableEntitiesWithParents';
|
||||
import { ActivityTargetableObject } from '../types/ActivityTargetableEntity';
|
||||
import { flattenTargetableObjectsAndTheirRelatedTargetableObjects } from '../utils/flattenTargetableObjectsAndTheirRelatedTargetableObjects';
|
||||
|
||||
export const useOpenCreateActivityDrawer = () => {
|
||||
const { openRightDrawer } = useRightDrawer();
|
||||
const { createOneRecord: createOneActivityTarget } =
|
||||
useCreateOneRecord<ActivityTarget>({
|
||||
const { createManyRecords: createManyActivityTargets } =
|
||||
useCreateManyRecords<ActivityTarget>({
|
||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
||||
});
|
||||
const { createOneRecord: createOneActivity } = useCreateOneRecord<Activity>({
|
||||
@ -37,15 +39,17 @@ export const useOpenCreateActivityDrawer = () => {
|
||||
return useCallback(
|
||||
async ({
|
||||
type,
|
||||
targetableEntities,
|
||||
targetableObjects,
|
||||
assigneeId,
|
||||
}: {
|
||||
type: ActivityType;
|
||||
targetableEntities?: ActivityTargetableEntity[];
|
||||
targetableObjects?: ActivityTargetableObject[];
|
||||
assigneeId?: string;
|
||||
}) => {
|
||||
const targetableEntitiesWithRelations = targetableEntities
|
||||
? getTargetableEntitiesWithParents(targetableEntities)
|
||||
const flattenedTargetableObjects = targetableObjects
|
||||
? flattenTargetableObjectsAndTheirRelatedTargetableObjects(
|
||||
targetableObjects,
|
||||
)
|
||||
: [];
|
||||
|
||||
const createdActivity = await createOneActivity?.({
|
||||
@ -61,21 +65,25 @@ export const useOpenCreateActivityDrawer = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
targetableEntitiesWithRelations.map(async (targetableEntity) => {
|
||||
await createOneActivityTarget?.({
|
||||
companyId:
|
||||
targetableEntity.type === 'Company' ? targetableEntity.id : null,
|
||||
personId:
|
||||
targetableEntity.type === 'Person' ? targetableEntity.id : null,
|
||||
const activityTargetsToCreate = flattenedTargetableObjects.map(
|
||||
(targetableObject) => {
|
||||
const targetableObjectFieldIdName =
|
||||
getActivityTargetObjectFieldIdName({
|
||||
nameSingular: targetableObject.targetObjectNameSingular,
|
||||
});
|
||||
|
||||
return {
|
||||
[targetableObjectFieldIdName]: targetableObject.id,
|
||||
activityId: createdActivity.id,
|
||||
});
|
||||
}),
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
await createManyActivityTargets(activityTargetsToCreate);
|
||||
|
||||
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
|
||||
setViewableActivityId(createdActivity.id);
|
||||
setActivityTargetableEntityArray(targetableEntities ?? []);
|
||||
setActivityTargetableEntityArray(targetableObjects ?? []);
|
||||
openRightDrawer(RightDrawerPages.CreateActivity);
|
||||
},
|
||||
[
|
||||
@ -84,7 +92,7 @@ export const useOpenCreateActivityDrawer = () => {
|
||||
setHotkeyScope,
|
||||
setViewableActivityId,
|
||||
createOneActivity,
|
||||
createOneActivityTarget,
|
||||
createManyActivityTargets,
|
||||
currentWorkspaceMember,
|
||||
],
|
||||
);
|
||||
|
||||
@ -4,10 +4,7 @@ import { ActivityType } from '@/activities/types/Activity';
|
||||
import { useRecordTableScopedStates } from '@/object-record/record-table/hooks/internal/useRecordTableScopedStates';
|
||||
import { getRecordTableScopeInjector } from '@/object-record/record-table/utils/getRecordTableScopeInjector';
|
||||
|
||||
import {
|
||||
ActivityTargetableEntity,
|
||||
ActivityTargetableEntityType,
|
||||
} from '../types/ActivityTargetableEntity';
|
||||
import { ActivityTargetableObject } from '../types/ActivityTargetableEntity';
|
||||
|
||||
import { useOpenCreateActivityDrawer } from './useOpenCreateActivityDrawer';
|
||||
|
||||
@ -25,8 +22,8 @@ export const useOpenCreateActivityDrawerForSelectedRowIds = (
|
||||
({ snapshot }) =>
|
||||
(
|
||||
type: ActivityType,
|
||||
entityType: ActivityTargetableEntityType,
|
||||
relatedEntities?: ActivityTargetableEntity[],
|
||||
objectNameSingular: string,
|
||||
relatedEntities?: ActivityTargetableObject[],
|
||||
) => {
|
||||
const selectedRowIds =
|
||||
injectSelectorSnapshotValueWithRecordTableScopeId(
|
||||
@ -34,18 +31,21 @@ export const useOpenCreateActivityDrawerForSelectedRowIds = (
|
||||
selectedRowIdsScopeInjector,
|
||||
);
|
||||
|
||||
let activityTargetableEntityArray: ActivityTargetableEntity[] =
|
||||
let activityTargetableEntityArray: ActivityTargetableObject[] =
|
||||
selectedRowIds.map((id: string) => ({
|
||||
type: entityType,
|
||||
type: 'Custom',
|
||||
targetObjectNameSingular: objectNameSingular,
|
||||
id,
|
||||
}));
|
||||
|
||||
if (relatedEntities) {
|
||||
activityTargetableEntityArray =
|
||||
activityTargetableEntityArray.concat(relatedEntities);
|
||||
}
|
||||
|
||||
openCreateActivityDrawer({
|
||||
type,
|
||||
targetableEntities: activityTargetableEntityArray,
|
||||
targetableObjects: activityTargetableEntityArray,
|
||||
});
|
||||
},
|
||||
[
|
||||
|
||||
@ -1,22 +1,15 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useQuery } from '@apollo/client';
|
||||
import styled from '@emotion/styled';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { useHandleCheckableActivityTargetChange } from '@/activities/hooks/useHandleCheckableActivityTargetChange';
|
||||
import { ActivityTarget } from '@/activities/types/ActivityTarget';
|
||||
import { flatMapAndSortEntityForSelectArrayOfArrayByName } from '@/activities/utils/flatMapAndSortEntityForSelectArrayByName';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { ActivityTargetObjectRecord } from '@/activities/types/ActivityTargetObject';
|
||||
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useCreateManyRecords } from '@/object-record/hooks/useCreateManyRecords';
|
||||
import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords';
|
||||
import { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlineCell';
|
||||
import { MultipleEntitySelect } from '@/object-record/relation-picker/components/MultipleEntitySelect';
|
||||
import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker';
|
||||
import { useFilteredSearchEntityQuery } from '@/search/hooks/useFilteredSearchEntityQuery';
|
||||
import { assertNotNull } from '~/utils/assert';
|
||||
|
||||
type ActivityTargetInlineCellEditModeProps = {
|
||||
activityId: string;
|
||||
activityTargets: Array<Pick<ActivityTarget, 'id' | 'personId' | 'companyId'>>;
|
||||
};
|
||||
import { MultipleObjectRecordSelect } from '@/object-record/relation-picker/components/MultipleObjectRecordSelect';
|
||||
import { ObjectRecordForSelect } from '@/object-record/relation-picker/hooks/useMultiObjectSearch';
|
||||
|
||||
const StyledSelectContainer = styled.div`
|
||||
left: 0px;
|
||||
@ -24,125 +17,77 @@ const StyledSelectContainer = styled.div`
|
||||
top: -8px;
|
||||
`;
|
||||
|
||||
type ActivityTargetInlineCellEditModeProps = {
|
||||
activityId: string;
|
||||
activityTargetObjectRecords: ActivityTargetObjectRecord[];
|
||||
};
|
||||
|
||||
export const ActivityTargetInlineCellEditMode = ({
|
||||
activityId,
|
||||
activityTargets,
|
||||
activityTargetObjectRecords,
|
||||
}: ActivityTargetInlineCellEditModeProps) => {
|
||||
const [searchFilter, setSearchFilter] = useState('');
|
||||
|
||||
const initialPeopleIds = useMemo(
|
||||
() =>
|
||||
activityTargets
|
||||
?.filter(({ personId }) => personId !== null)
|
||||
.map(({ personId }) => personId)
|
||||
.filter(assertNotNull) ?? [],
|
||||
[activityTargets],
|
||||
const selectedObjectRecordIds = activityTargetObjectRecords.map(
|
||||
(activityTarget) => ({
|
||||
objectNameSingular: activityTarget.targetObjectNameSingular,
|
||||
id: activityTarget.targetObjectRecord.id,
|
||||
}),
|
||||
);
|
||||
|
||||
const initialCompanyIds = useMemo(
|
||||
() =>
|
||||
activityTargets
|
||||
?.filter(({ companyId }) => companyId !== null)
|
||||
.map(({ companyId }) => companyId)
|
||||
.filter(assertNotNull) ?? [],
|
||||
[activityTargets],
|
||||
);
|
||||
|
||||
const initialSelectedEntityIds = useMemo(
|
||||
() =>
|
||||
[...initialPeopleIds, ...initialCompanyIds].reduce<
|
||||
Record<string, boolean>
|
||||
>((result, entityId) => ({ ...result, [entityId]: true }), {}),
|
||||
[initialPeopleIds, initialCompanyIds],
|
||||
);
|
||||
|
||||
const { findManyRecordsQuery: findManyPeopleQuery } = useObjectMetadataItem({
|
||||
objectNameSingular: CoreObjectNameSingular.Person,
|
||||
});
|
||||
|
||||
const { findManyRecordsQuery: findManyCompaniesQuery } =
|
||||
useObjectMetadataItem({
|
||||
objectNameSingular: CoreObjectNameSingular.Company,
|
||||
const { createManyRecords: createManyActivityTargets } =
|
||||
useCreateManyRecords<ActivityTarget>({
|
||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
||||
});
|
||||
|
||||
const useFindManyPeopleQuery = (options: any) =>
|
||||
useQuery(findManyPeopleQuery, options);
|
||||
const { deleteManyRecords: deleteManyActivityTargets } = useDeleteManyRecords(
|
||||
{
|
||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
||||
},
|
||||
);
|
||||
|
||||
const useFindManyCompaniesQuery = (options: any) =>
|
||||
useQuery(findManyCompaniesQuery, options);
|
||||
|
||||
const [selectedEntityIds, setSelectedEntityIds] = useState<
|
||||
Record<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 handleSubmit = useCallback(() => {
|
||||
handleCheckItemsChange(
|
||||
selectedEntityIds,
|
||||
entitiesToSelect,
|
||||
selectedEntities,
|
||||
);
|
||||
const handleSubmit = async (selectedRecords: ObjectRecordForSelect[]) => {
|
||||
closeEditableField();
|
||||
}, [
|
||||
closeEditableField,
|
||||
entitiesToSelect,
|
||||
handleCheckItemsChange,
|
||||
selectedEntities,
|
||||
selectedEntityIds,
|
||||
]);
|
||||
|
||||
const activityTargetRecordsToDelete = activityTargetObjectRecords.filter(
|
||||
(activityTargetObjectRecord) =>
|
||||
!selectedRecords.some(
|
||||
(selectedRecord) =>
|
||||
selectedRecord.recordIdentifier.id ===
|
||||
activityTargetObjectRecord.targetObjectRecord.id,
|
||||
),
|
||||
);
|
||||
|
||||
const activityTargetRecordsToCreate = selectedRecords.filter(
|
||||
(selectedRecord) =>
|
||||
!activityTargetObjectRecords.some(
|
||||
(activityTargetObjectRecord) =>
|
||||
activityTargetObjectRecord.targetObjectRecord.id ===
|
||||
selectedRecord.recordIdentifier.id,
|
||||
),
|
||||
);
|
||||
|
||||
if (activityTargetRecordsToCreate.length > 0) {
|
||||
await createManyActivityTargets(
|
||||
activityTargetRecordsToCreate.map((selectedRecord) => ({
|
||||
id: v4(),
|
||||
activityId,
|
||||
[getActivityTargetObjectFieldIdName({
|
||||
nameSingular: selectedRecord.objectMetadataItem.nameSingular,
|
||||
})]: selectedRecord.recordIdentifier.id,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
if (activityTargetRecordsToDelete.length > 0) {
|
||||
await deleteManyActivityTargets(
|
||||
activityTargetRecordsToDelete.map(
|
||||
(activityTargetObjectRecord) =>
|
||||
activityTargetObjectRecord.activityTargetRecord.id,
|
||||
),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
closeEditableField();
|
||||
@ -150,17 +95,8 @@ export const ActivityTargetInlineCellEditMode = ({
|
||||
|
||||
return (
|
||||
<StyledSelectContainer>
|
||||
<MultipleEntitySelect
|
||||
entities={{
|
||||
entitiesToSelect,
|
||||
filteredSelectedEntities,
|
||||
selectedEntities,
|
||||
loading: false,
|
||||
}}
|
||||
onChange={setSelectedEntityIds}
|
||||
onSearchFilterChange={setSearchFilter}
|
||||
searchFilter={searchFilter}
|
||||
value={selectedEntityIds}
|
||||
<MultipleObjectRecordSelect
|
||||
selectedObjectRecordIds={selectedObjectRecordIds}
|
||||
onCancel={handleCancel}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
import { ActivityTargetChips } from '@/activities/components/ActivityTargetChips';
|
||||
import { useActivityTargetObjectRecords } from '@/activities/hooks/useActivityTargetObjectRecords';
|
||||
import { ActivityTargetInlineCellEditMode } from '@/activities/inline-cell/components/ActivityTargetInlineCellEditMode';
|
||||
import { ActivityTarget } from '@/activities/types/ActivityTarget';
|
||||
import { GraphQLActivity } from '@/activities/types/GraphQLActivity';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { RecordInlineCellContainer } from '@/object-record/record-inline-cell/components/RecordInlineCellContainer';
|
||||
import { FieldRecoilScopeContext } from '@/object-record/record-inline-cell/states/recoil-scope-contexts/FieldRecoilScopeContext';
|
||||
import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
|
||||
@ -23,14 +22,8 @@ type ActivityTargetsInlineCellProps = {
|
||||
export const ActivityTargetsInlineCell = ({
|
||||
activity,
|
||||
}: ActivityTargetsInlineCellProps) => {
|
||||
const activityTargetIds =
|
||||
activity?.activityTargets?.edges?.map(
|
||||
(activityTarget) => activityTarget.node.id,
|
||||
) ?? [];
|
||||
|
||||
const { records: activityTargets } = useFindManyRecords<ActivityTarget>({
|
||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
||||
filter: { id: { in: activityTargetIds } },
|
||||
const { activityTargetObjectRecords } = useActivityTargetObjectRecords({
|
||||
activityId: activity?.id ?? '',
|
||||
});
|
||||
|
||||
return (
|
||||
@ -44,11 +37,15 @@ export const ActivityTargetsInlineCell = ({
|
||||
editModeContent={
|
||||
<ActivityTargetInlineCellEditMode
|
||||
activityId={activity?.id ?? ''}
|
||||
activityTargets={activityTargets as any}
|
||||
activityTargetObjectRecords={activityTargetObjectRecords as any}
|
||||
/>
|
||||
}
|
||||
label="Relations"
|
||||
displayModeContent={<ActivityTargetChips targets={activityTargets} />}
|
||||
displayModeContent={
|
||||
<ActivityTargetChips
|
||||
activityTargetObjectRecords={activityTargetObjectRecords}
|
||||
/>
|
||||
}
|
||||
isDisplayModeContentEmpty={
|
||||
activity?.activityTargets?.edges?.length === 0
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ import styled from '@emotion/styled';
|
||||
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
|
||||
import { NoteList } from '@/activities/notes/components/NoteList';
|
||||
import { useNotes } from '@/activities/notes/hooks/useNotes';
|
||||
import { ActivityTargetableEntity } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { IconPlus } from '@/ui/display/icon';
|
||||
import { Button } from '@/ui/input/button/components/Button';
|
||||
|
||||
@ -44,12 +44,16 @@ const StyledNotesContainer = styled.div`
|
||||
overflow: auto;
|
||||
`;
|
||||
|
||||
export const Notes = ({ entity }: { entity: ActivityTargetableEntity }) => {
|
||||
const { notes } = useNotes(entity);
|
||||
export const Notes = ({
|
||||
targetableObject,
|
||||
}: {
|
||||
targetableObject: ActivityTargetableObject;
|
||||
}) => {
|
||||
const { notes } = useNotes(targetableObject);
|
||||
|
||||
const openCreateActivity = useOpenCreateActivityDrawer();
|
||||
|
||||
if (notes?.length === 0 && entity.type !== 'Custom') {
|
||||
if (notes?.length === 0) {
|
||||
return (
|
||||
<StyledTaskGroupEmptyContainer>
|
||||
<StyledEmptyTaskGroupTitle>No note yet</StyledEmptyTaskGroupTitle>
|
||||
@ -61,7 +65,7 @@ export const Notes = ({ entity }: { entity: ActivityTargetableEntity }) => {
|
||||
onClick={() =>
|
||||
openCreateActivity({
|
||||
type: 'Note',
|
||||
targetableEntities: [entity],
|
||||
targetableObjects: [targetableObject],
|
||||
})
|
||||
}
|
||||
/>
|
||||
@ -83,7 +87,7 @@ export const Notes = ({ entity }: { entity: ActivityTargetableEntity }) => {
|
||||
onClick={() =>
|
||||
openCreateActivity({
|
||||
type: 'Note',
|
||||
targetableEntities: [entity],
|
||||
targetableObjects: [targetableObject],
|
||||
})
|
||||
}
|
||||
></Button>
|
||||
|
||||
@ -1,17 +1,13 @@
|
||||
import { useActivityTargets } from '@/activities/hooks/useActivityTargets';
|
||||
import { Note } from '@/activities/types/Note';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { OrderByField } from '@/object-metadata/types/OrderByField';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
|
||||
import { ActivityTargetableEntity } from '../../types/ActivityTargetableEntity';
|
||||
import { ActivityTargetableObject } from '../../types/ActivityTargetableEntity';
|
||||
|
||||
export const useNotes = (entity: ActivityTargetableEntity) => {
|
||||
const { records: activityTargets } = useFindManyRecords({
|
||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
||||
filter: {
|
||||
[entity.type === 'Company' ? 'companyId' : 'personId']: { eq: entity.id },
|
||||
},
|
||||
});
|
||||
export const useNotes = (targetableObject: ActivityTargetableObject) => {
|
||||
const { activityTargets } = useActivityTargets({ targetableObject });
|
||||
|
||||
const filter = {
|
||||
id: {
|
||||
@ -19,6 +15,7 @@ export const useNotes = (entity: ActivityTargetableEntity) => {
|
||||
},
|
||||
type: { eq: 'Note' },
|
||||
};
|
||||
|
||||
const orderBy = {
|
||||
createdAt: 'AscNullsFirst',
|
||||
} as OrderByField;
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
import { ActivityTargetableEntity } from '../types/ActivityTargetableEntity';
|
||||
import { ActivityTargetableObject } from '../types/ActivityTargetableEntity';
|
||||
|
||||
export const activityTargetableEntityArrayState = atom<
|
||||
ActivityTargetableEntity[]
|
||||
ActivityTargetableObject[]
|
||||
>({
|
||||
key: 'activities/targetable-entity-array',
|
||||
default: [],
|
||||
|
||||
@ -2,6 +2,7 @@ import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { TasksRecoilScopeContext } from '@/activities/states/recoil-scope-contexts/TasksRecoilScopeContext';
|
||||
import { TaskGroups } from '@/activities/tasks/components/TaskGroups';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { ObjectFilterDropdownScope } from '@/object-record/object-filter-dropdown/scopes/ObjectFilterDropdownScope';
|
||||
import { ComponentWithRecoilScopeDecorator } from '~/testing/decorators/ComponentWithRecoilScopeDecorator';
|
||||
import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator';
|
||||
@ -35,9 +36,11 @@ export const Empty: Story = {};
|
||||
|
||||
export const WithTasks: Story = {
|
||||
args: {
|
||||
entity: {
|
||||
id: mockedTasks[0].authorId,
|
||||
type: 'Person',
|
||||
},
|
||||
targetableObjects: [
|
||||
{
|
||||
id: mockedTasks[0].authorId,
|
||||
targetObjectNameSingular: 'person',
|
||||
},
|
||||
] as ActivityTargetableObject[],
|
||||
},
|
||||
};
|
||||
|
||||
@ -1,16 +1,18 @@
|
||||
import { isNonEmptyArray } from '@sniptt/guards';
|
||||
|
||||
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
|
||||
import { ActivityTargetableEntity } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { IconPlus } from '@/ui/display/icon';
|
||||
import { Button } from '@/ui/input/button/components/Button';
|
||||
|
||||
export const AddTaskButton = ({
|
||||
activityTargetEntity,
|
||||
activityTargetableObjects,
|
||||
}: {
|
||||
activityTargetEntity?: ActivityTargetableEntity;
|
||||
activityTargetableObjects?: ActivityTargetableObject[];
|
||||
}) => {
|
||||
const openCreateActivity = useOpenCreateActivityDrawer();
|
||||
|
||||
if (!activityTargetEntity) {
|
||||
if (!isNonEmptyArray(activityTargetableObjects)) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
@ -23,7 +25,7 @@ export const AddTaskButton = ({
|
||||
onClick={() =>
|
||||
openCreateActivity({
|
||||
type: 'Task',
|
||||
targetableEntities: [activityTargetEntity],
|
||||
targetableObjects: activityTargetableObjects,
|
||||
})
|
||||
}
|
||||
></Button>
|
||||
|
||||
@ -2,7 +2,7 @@ import styled from '@emotion/styled';
|
||||
|
||||
import { TasksRecoilScopeContext } from '@/activities/states/recoil-scope-contexts/TasksRecoilScopeContext';
|
||||
import { TaskGroups } from '@/activities/tasks/components/TaskGroups';
|
||||
import { ActivityTargetableEntity } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { ObjectFilterDropdownScope } from '@/object-record/object-filter-dropdown/scopes/ObjectFilterDropdownScope';
|
||||
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
|
||||
|
||||
@ -14,16 +14,16 @@ const StyledContainer = styled.div`
|
||||
overflow: auto;
|
||||
`;
|
||||
|
||||
export const EntityTasks = ({
|
||||
entity,
|
||||
export const ObjectTasks = ({
|
||||
targetableObject,
|
||||
}: {
|
||||
entity: ActivityTargetableEntity;
|
||||
targetableObject: ActivityTargetableObject;
|
||||
}) => {
|
||||
return (
|
||||
<StyledContainer>
|
||||
<RecoilScope CustomRecoilScopeContext={TasksRecoilScopeContext}>
|
||||
<ObjectFilterDropdownScope filterScopeId="entity-tasks-filter-scope">
|
||||
<TaskGroups entity={entity} showAddButton />
|
||||
<TaskGroups targetableObjects={[targetableObject]} showAddButton />
|
||||
</ObjectFilterDropdownScope>
|
||||
</RecoilScope>
|
||||
</StyledContainer>
|
||||
@ -3,7 +3,7 @@ import styled from '@emotion/styled';
|
||||
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
|
||||
import { TasksRecoilScopeContext } from '@/activities/states/recoil-scope-contexts/TasksRecoilScopeContext';
|
||||
import { useTasks } from '@/activities/tasks/hooks/useTasks';
|
||||
import { ActivityTargetableEntity } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { IconPlus } from '@/ui/display/icon';
|
||||
import { Button } from '@/ui/input/button/components/Button';
|
||||
import { activeTabIdScopedState } from '@/ui/layout/tab/states/activeTabIdScopedState';
|
||||
@ -12,12 +12,6 @@ import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoi
|
||||
import { AddTaskButton } from './AddTaskButton';
|
||||
import { TaskList } from './TaskList';
|
||||
|
||||
type TaskGroupsProps = {
|
||||
filterDropdownId?: string;
|
||||
entity?: ActivityTargetableEntity;
|
||||
showAddButton?: boolean;
|
||||
};
|
||||
|
||||
const StyledTaskGroupEmptyContainer = styled.div`
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
@ -52,9 +46,15 @@ const StyledContainer = styled.div`
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
type TaskGroupsProps = {
|
||||
filterDropdownId?: string;
|
||||
targetableObjects?: ActivityTargetableObject[];
|
||||
showAddButton?: boolean;
|
||||
};
|
||||
|
||||
export const TaskGroups = ({
|
||||
filterDropdownId,
|
||||
entity,
|
||||
targetableObjects,
|
||||
showAddButton,
|
||||
}: TaskGroupsProps) => {
|
||||
const {
|
||||
@ -62,7 +62,10 @@ export const TaskGroups = ({
|
||||
upcomingTasks,
|
||||
unscheduledTasks,
|
||||
completedTasks,
|
||||
} = useTasks({ filterDropdownId: filterDropdownId, entity });
|
||||
} = useTasks({
|
||||
filterDropdownId: filterDropdownId,
|
||||
targetableObjects: targetableObjects ?? [],
|
||||
});
|
||||
|
||||
const openCreateActivity = useOpenCreateActivityDrawer();
|
||||
|
||||
@ -71,10 +74,6 @@ export const TaskGroups = ({
|
||||
TasksRecoilScopeContext,
|
||||
);
|
||||
|
||||
if (entity?.type === 'Custom') {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
if (
|
||||
(activeTabId !== 'done' &&
|
||||
todayOrPreviousTasks?.length === 0 &&
|
||||
@ -93,7 +92,7 @@ export const TaskGroups = ({
|
||||
onClick={() =>
|
||||
openCreateActivity({
|
||||
type: 'Task',
|
||||
targetableEntities: entity ? [entity] : undefined,
|
||||
targetableObjects,
|
||||
})
|
||||
}
|
||||
/>
|
||||
@ -107,7 +106,9 @@ export const TaskGroups = ({
|
||||
<TaskList
|
||||
tasks={completedTasks ?? []}
|
||||
button={
|
||||
showAddButton && <AddTaskButton activityTargetEntity={entity} />
|
||||
showAddButton && (
|
||||
<AddTaskButton activityTargetableObjects={targetableObjects} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
@ -116,7 +117,9 @@ export const TaskGroups = ({
|
||||
title="Today"
|
||||
tasks={todayOrPreviousTasks ?? []}
|
||||
button={
|
||||
showAddButton && <AddTaskButton activityTargetEntity={entity} />
|
||||
showAddButton && (
|
||||
<AddTaskButton activityTargetableObjects={targetableObjects} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
<TaskList
|
||||
@ -125,7 +128,7 @@ export const TaskGroups = ({
|
||||
button={
|
||||
showAddButton &&
|
||||
!todayOrPreviousTasks?.length && (
|
||||
<AddTaskButton activityTargetEntity={entity} />
|
||||
<AddTaskButton activityTargetableObjects={targetableObjects} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
@ -136,7 +139,7 @@ export const TaskGroups = ({
|
||||
showAddButton &&
|
||||
!todayOrPreviousTasks?.length &&
|
||||
!upcomingTasks?.length && (
|
||||
<AddTaskButton activityTargetEntity={entity} />
|
||||
<AddTaskButton activityTargetableObjects={targetableObjects} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
@ -2,12 +2,10 @@ import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { ActivityTargetChips } from '@/activities/components/ActivityTargetChips';
|
||||
import { useActivityTargetObjectRecords } from '@/activities/hooks/useActivityTargetObjectRecords';
|
||||
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
|
||||
import { ActivityTarget } from '@/activities/types/ActivityTarget';
|
||||
import { GraphQLActivity } from '@/activities/types/GraphQLActivity';
|
||||
import { getActivitySummary } from '@/activities/utils/getActivitySummary';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { IconCalendar, IconComment } from '@/ui/display/icon';
|
||||
import { OverflowingTextWithTooltip } from '@/ui/display/tooltip/OverflowingTextWithTooltip';
|
||||
import { Checkbox, CheckboxShape } from '@/ui/input/components/Checkbox';
|
||||
@ -76,14 +74,8 @@ export const TaskRow = ({
|
||||
const body = getActivitySummary(task.body);
|
||||
const { completeTask } = useCompleteTask(task);
|
||||
|
||||
const activityTargetIds =
|
||||
task?.activityTargets?.edges?.map(
|
||||
(activityTarget) => activityTarget.node.id,
|
||||
) ?? [];
|
||||
|
||||
const { records: activityTargets } = useFindManyRecords<ActivityTarget>({
|
||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
||||
filter: { id: { in: activityTargetIds } },
|
||||
const { activityTargetObjectRecords } = useActivityTargetObjectRecords({
|
||||
activityId: task.id,
|
||||
});
|
||||
|
||||
return (
|
||||
@ -115,7 +107,9 @@ export const TaskRow = ({
|
||||
)}
|
||||
</StyledTaskBody>
|
||||
<StyledFieldsContainer>
|
||||
<ActivityTargetChips targets={activityTargets} />
|
||||
<ActivityTargetChips
|
||||
activityTargetObjectRecords={activityTargetObjectRecords}
|
||||
/>
|
||||
<StyledDueDate
|
||||
isPast={
|
||||
!!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 { 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 { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
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 { isDefined } from '~/utils/isDefined';
|
||||
|
||||
type UseTasksProps = {
|
||||
filterDropdownId?: string;
|
||||
entity?: ActivityTargetableEntity;
|
||||
targetableObjects: ActivityTargetableObject[];
|
||||
};
|
||||
|
||||
export const useTasks = (props?: UseTasksProps) => {
|
||||
const { filterDropdownId, entity } = props ?? {};
|
||||
|
||||
export const useTasks = ({
|
||||
targetableObjects,
|
||||
filterDropdownId,
|
||||
}: UseTasksProps) => {
|
||||
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({
|
||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
||||
filter: isDefined(entity)
|
||||
? {
|
||||
[entity?.type === 'Company' ? 'companyId' : 'personId']: {
|
||||
eq: entity?.id,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
filter: targetableObjectsFilter,
|
||||
});
|
||||
|
||||
const skipRequest = !isNonEmptyArray(activityTargets) && !selectedFilter;
|
||||
|
||||
const idFilter = {
|
||||
id: {
|
||||
in: activityTargets.map((activityTarget) => activityTarget.activityId),
|
||||
},
|
||||
};
|
||||
|
||||
const assigneeIdFilter = selectedFilter
|
||||
? {
|
||||
assigneeId: {
|
||||
in: JSON.parse(selectedFilter.value),
|
||||
},
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const { records: completeTasksData } = useFindManyRecords({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
skip: !entity && !selectedFilter,
|
||||
skip: skipRequest,
|
||||
filter: {
|
||||
completedAt: { is: 'NOT_NULL' },
|
||||
...(isDefined(entity) && {
|
||||
id: {
|
||||
in: activityTargets?.map(
|
||||
(activityTarget) => activityTarget.activityId,
|
||||
),
|
||||
},
|
||||
}),
|
||||
...idFilter,
|
||||
type: { eq: 'Task' },
|
||||
...(isNonEmptyString(selectedFilter?.value) && {
|
||||
assigneeId: {
|
||||
in: JSON.parse(selectedFilter?.value),
|
||||
},
|
||||
}),
|
||||
...assigneeIdFilter,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'DescNullsFirst',
|
||||
@ -58,22 +78,12 @@ export const useTasks = (props?: UseTasksProps) => {
|
||||
|
||||
const { records: incompleteTaskData } = useFindManyRecords({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
skip: !entity && !selectedFilter,
|
||||
skip: skipRequest,
|
||||
filter: {
|
||||
completedAt: { is: 'NULL' },
|
||||
...(isDefined(entity) && {
|
||||
id: {
|
||||
in: activityTargets?.map(
|
||||
(activityTarget) => activityTarget.activityId,
|
||||
),
|
||||
},
|
||||
}),
|
||||
...idFilter,
|
||||
type: { eq: 'Task' },
|
||||
...(isNonEmptyString(selectedFilter?.value) && {
|
||||
assigneeId: {
|
||||
in: JSON.parse(selectedFilter?.value),
|
||||
},
|
||||
}),
|
||||
...assigneeIdFilter,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'DescNullsFirst',
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
import React from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
import { ActivityCreateButton } from '@/activities/components/ActivityCreateButton';
|
||||
import { useActivityTargets } from '@/activities/hooks/useActivityTargets';
|
||||
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { ActivityTargetableEntity } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
@ -48,20 +50,21 @@ const StyledEmptyTimelineSubTitle = styled.div`
|
||||
margin-bottom: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
export const Timeline = ({ entity }: { entity: ActivityTargetableEntity }) => {
|
||||
const { records: activityTargets, loading } = useFindManyRecords({
|
||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
||||
filter: {
|
||||
[entity.type === 'Company' ? 'companyId' : 'personId']: { eq: entity.id },
|
||||
},
|
||||
});
|
||||
export const Timeline = ({
|
||||
targetableObject,
|
||||
}: {
|
||||
targetableObject: ActivityTargetableObject;
|
||||
}) => {
|
||||
const { activityTargets } = useActivityTargets({ targetableObject });
|
||||
|
||||
const { records: activities } = useFindManyRecords({
|
||||
skip: !activityTargets?.length,
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
filter: {
|
||||
id: {
|
||||
in: activityTargets?.map((activityTarget) => activityTarget.activityId),
|
||||
in: activityTargets
|
||||
?.map((activityTarget) => activityTarget.activityId)
|
||||
.filter(isNonEmptyString),
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
@ -71,10 +74,6 @@ export const Timeline = ({ entity }: { entity: ActivityTargetableEntity }) => {
|
||||
|
||||
const openCreateActivity = useOpenCreateActivityDrawer();
|
||||
|
||||
if (loading || entity.type === 'Custom') {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
if (!activities.length) {
|
||||
return (
|
||||
<StyledTimelineEmptyContainer>
|
||||
@ -84,13 +83,13 @@ export const Timeline = ({ entity }: { entity: ActivityTargetableEntity }) => {
|
||||
onNoteClick={() =>
|
||||
openCreateActivity({
|
||||
type: 'Note',
|
||||
targetableEntities: [entity],
|
||||
targetableObjects: [targetableObject],
|
||||
})
|
||||
}
|
||||
onTaskClick={() =>
|
||||
openCreateActivity({
|
||||
type: 'Task',
|
||||
targetableEntities: [entity],
|
||||
targetableObjects: [targetableObject],
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
||||
@ -145,7 +145,7 @@ type TimelineActivityProps = {
|
||||
| 'type'
|
||||
| 'comments'
|
||||
| 'dueAt'
|
||||
> & { author: Pick<WorkspaceMember, 'name' | 'avatarUrl'> } & {
|
||||
> & { author?: Pick<WorkspaceMember, 'name' | 'avatarUrl'> } & {
|
||||
assignee?: Pick<WorkspaceMember, 'id' | 'name' | 'avatarUrl'> | null;
|
||||
};
|
||||
isLastActivity?: boolean;
|
||||
@ -165,8 +165,8 @@ export const TimelineActivity = ({
|
||||
<StyledTimelineItemContainer>
|
||||
<StyledAvatarContainer>
|
||||
<Avatar
|
||||
avatarUrl={activity.author.avatarUrl}
|
||||
placeholder={activity.author.name.firstName ?? ''}
|
||||
avatarUrl={activity.author?.avatarUrl}
|
||||
placeholder={activity.author?.name.firstName ?? ''}
|
||||
size="sm"
|
||||
type="rounded"
|
||||
/>
|
||||
@ -175,7 +175,8 @@ export const TimelineActivity = ({
|
||||
<StyledItemTitleContainer>
|
||||
<StyledItemAuthorText>
|
||||
<span>
|
||||
{activity.author.name.firstName} {activity.author.name.lastName}
|
||||
{activity.author?.name.firstName}{' '}
|
||||
{activity.author?.name.lastName}
|
||||
</span>
|
||||
created a {activity.type.toLowerCase()}
|
||||
</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 ActivityTargetableEntity = {
|
||||
export type ActivityTargetableObject = {
|
||||
id: string;
|
||||
type: ActivityTargetableEntityType;
|
||||
relatedEntities?: ActivityTargetableEntity[];
|
||||
targetObjectNameSingular: string;
|
||||
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 { getTargetableEntitiesWithParents } from '@/activities/utils/getTargetableEntitiesWithParents';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { flattenTargetableObjectsAndTheirRelatedTargetableObjects } from '@/activities/utils/flattenTargetableObjectsAndTheirRelatedTargetableObjects';
|
||||
|
||||
describe('getTargetableEntitiesWithParents', () => {
|
||||
it('should return the correct value', () => {
|
||||
const entities: ActivityTargetableEntity[] = [
|
||||
const entities: ActivityTargetableObject[] = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'Person',
|
||||
relatedEntities: [
|
||||
targetObjectNameSingular: 'person',
|
||||
relatedTargetableObjects: [
|
||||
{
|
||||
id: '2',
|
||||
type: 'Company',
|
||||
targetObjectNameSingular: 'company',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
type: 'Company',
|
||||
targetObjectNameSingular: 'person',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: 'Custom',
|
||||
relatedEntities: [
|
||||
targetObjectNameSingular: 'car',
|
||||
relatedTargetableObjects: [
|
||||
{
|
||||
id: '6',
|
||||
type: 'Person',
|
||||
targetObjectNameSingular: 'person',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
type: 'Company',
|
||||
targetObjectNameSingular: 'company',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const res = getTargetableEntitiesWithParents(entities);
|
||||
const res =
|
||||
flattenTargetableObjectsAndTheirRelatedTargetableObjects(entities);
|
||||
|
||||
expect(res).toHaveLength(6);
|
||||
expect(res[0].id).toBe('1');
|
||||
|
||||
@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user