mirror of
https://github.com/twentyhq/twenty.git
synced 2024-09-11 23:37:21 +03:00
Fix opportunity relation (#3478)
* Fix opportunity relation * Fix * Fix * Fix tests * Fix * Fix
This commit is contained in:
parent
bf67f07291
commit
fb93bb69fb
@ -42,7 +42,7 @@ export const HooksCompanyBoardEffect = ({
|
||||
|
||||
const setAvailableBoardCardFields = useSetRecoilScopedStateV2(
|
||||
availableRecordBoardCardFieldsScopedState,
|
||||
'company-board-view',
|
||||
'company-board',
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -55,7 +55,7 @@ export const ObjectMetadataItemsRelationPickerEffect = () => {
|
||||
if (['opportunity'].includes(objectMetadataItemSingularName)) {
|
||||
return {
|
||||
id: record.id,
|
||||
name: record?.company?.name,
|
||||
name: record?.company?.name ?? record.name,
|
||||
avatarUrl: record.avatarUrl,
|
||||
avatarType: 'rounded',
|
||||
record: record,
|
||||
|
@ -18,7 +18,7 @@ export const getObjectRecordIdentifier = ({
|
||||
case CoreObjectNameSingular.Opportunity:
|
||||
return {
|
||||
id: record.id,
|
||||
name: record?.company?.name,
|
||||
name: record?.company?.name ?? record.name,
|
||||
avatarUrl: record.avatarUrl,
|
||||
avatarType: 'rounded',
|
||||
linkToShowPage: `/opportunities/${record.id}`,
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
@ -70,11 +71,13 @@ export const RecordShowPage = () => {
|
||||
const { record, loading } = useFindOneRecord({
|
||||
objectRecordId,
|
||||
objectNameSingular,
|
||||
onCompleted: (data) => {
|
||||
setEntityFields(data);
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!record) return;
|
||||
setEntityFields(record);
|
||||
}, [record, setEntityFields]);
|
||||
|
||||
const [uploadImage] = useUploadImageMutation();
|
||||
const { updateOneRecord } = useUpdateOneRecord({ objectNameSingular });
|
||||
|
||||
@ -285,6 +288,7 @@ export const RecordShowPage = () => {
|
||||
if (!relationObjectMetadataItem) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isObjectMetadataAvailableForRelation(
|
||||
relationObjectMetadataItem,
|
||||
);
|
||||
|
@ -27,7 +27,6 @@ import { isFieldUuid } from '../types/guards/isFieldUuid';
|
||||
|
||||
export const FieldDisplay = () => {
|
||||
const { fieldDefinition, isLabelIdentifier } = useContext(FieldContext);
|
||||
|
||||
if (
|
||||
isLabelIdentifier &&
|
||||
(isFieldText(fieldDefinition) || isFieldFullName(fieldDefinition))
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { Modifiers } from '@apollo/client/cache';
|
||||
import { Modifier, Reference } from '@apollo/client/cache';
|
||||
|
||||
import { EMPTY_MUTATION } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
@ -12,7 +12,10 @@ export const useModifyRecordFromCache = ({
|
||||
}) => {
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
return (recordId: string, fieldModifiers: Modifiers) => {
|
||||
return (
|
||||
recordId: string,
|
||||
fieldModifiers: Record<string, Modifier<Reference>>,
|
||||
) => {
|
||||
if (!objectMetadataItem) {
|
||||
return EMPTY_MUTATION;
|
||||
}
|
||||
@ -23,7 +26,7 @@ export const useModifyRecordFromCache = ({
|
||||
id: recordId,
|
||||
});
|
||||
|
||||
cache.modify({
|
||||
cache.modify<Record<string, Reference>>({
|
||||
id: cachedRecordId,
|
||||
fields: fieldModifiers,
|
||||
});
|
||||
|
@ -46,6 +46,7 @@ const mocks = [
|
||||
variables: {
|
||||
input: {
|
||||
id: mockedUuid,
|
||||
name: 'Opportunity',
|
||||
pipelineStepId: 'pipelineStepId',
|
||||
companyId: 'New Opportunity',
|
||||
},
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { RecoilRoot, useRecoilState, useRecoilValue } from 'recoil';
|
||||
|
||||
import { FieldType } from '@/object-record/field/types/FieldType';
|
||||
@ -48,10 +48,15 @@ describe('useRecordBoardCardFieldsInternal', () => {
|
||||
expect(result.current.cardFieldsList[0].isVisible).toBe(true);
|
||||
|
||||
act(() => {
|
||||
result.current.boardCardFields.handleFieldVisibilityChange(field);
|
||||
result.current.boardCardFields.handleFieldVisibilityChange({
|
||||
...field,
|
||||
isVisible: true,
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current.cardFieldsList[0].isVisible).toBe(false);
|
||||
waitFor(() => {
|
||||
expect(result.current.cardFieldsList[0].isVisible).toBe(false);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.boardCardFields.handleFieldVisibilityChange({
|
||||
@ -60,7 +65,9 @@ describe('useRecordBoardCardFieldsInternal', () => {
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current.cardFieldsList[0].isVisible).toBe(true);
|
||||
waitFor(() => {
|
||||
expect(result.current.cardFieldsList[0].isVisible).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should call the onFieldsChange callback and update board card states', async () => {
|
||||
|
@ -24,6 +24,7 @@ export const useCreateOpportunity = () => {
|
||||
|
||||
await createOneOpportunity?.({
|
||||
id: newUuid,
|
||||
name: 'Opportunity',
|
||||
pipelineStepId,
|
||||
companyId: companyId,
|
||||
});
|
||||
|
@ -30,17 +30,46 @@ export const useRecordBoardCardFieldsInternal = (
|
||||
savedRecordBoardCardFieldsScopedState({ scopeId }),
|
||||
);
|
||||
|
||||
const handleFieldVisibilityChange = (
|
||||
field: Omit<ColumnDefinition<FieldMetadata>, 'size' | 'position'>,
|
||||
) => {
|
||||
setBoardCardFields((previousFields) =>
|
||||
previousFields.map((previousField) =>
|
||||
previousField.fieldMetadataId === field.fieldMetadataId
|
||||
? { ...previousField, isVisible: !field.isVisible }
|
||||
: previousField,
|
||||
),
|
||||
);
|
||||
};
|
||||
const handleFieldVisibilityChange = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
async (
|
||||
field: Omit<ColumnDefinition<FieldMetadata>, 'size' | 'position'>,
|
||||
) => {
|
||||
const existingFields = await snapshot
|
||||
.getLoadable(recordBoardCardFieldsScopedState({ scopeId }))
|
||||
.getValue();
|
||||
|
||||
const existingFieldsUpdated = existingFields.map((previousField) =>
|
||||
previousField.fieldMetadataId === field.fieldMetadataId
|
||||
? { ...previousField, isVisible: !field.isVisible }
|
||||
: previousField,
|
||||
);
|
||||
|
||||
const isNewField = !existingFields.find(
|
||||
({ fieldMetadataId }) => field.fieldMetadataId === fieldMetadataId,
|
||||
);
|
||||
|
||||
const fields = isNewField
|
||||
? [
|
||||
...existingFieldsUpdated,
|
||||
{
|
||||
...field,
|
||||
position: existingFieldsUpdated.length,
|
||||
},
|
||||
]
|
||||
: existingFieldsUpdated;
|
||||
|
||||
setSavedBoardCardFields(fields);
|
||||
setBoardCardFields(fields);
|
||||
|
||||
const onFieldsChange = snapshot
|
||||
.getLoadable(onFieldsChangeScopedState({ scopeId }))
|
||||
.getValue();
|
||||
|
||||
onFieldsChange?.(fields);
|
||||
},
|
||||
[scopeId, setBoardCardFields, setSavedBoardCardFields],
|
||||
);
|
||||
|
||||
const handleFieldsChange = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
|
@ -11,6 +11,7 @@ export const hiddenRecordBoardCardFieldsScopedSelector = createSelectorScopeMap(
|
||||
({ get }) => {
|
||||
const fields = get(recordBoardCardFieldsScopedState({ scopeId }));
|
||||
const fieldKeys = fields.map(({ fieldMetadataId }) => fieldMetadataId);
|
||||
|
||||
const otherAvailableKeys = get(
|
||||
availableRecordBoardCardFieldsScopedState({ scopeId }),
|
||||
).filter(({ fieldMetadataId }) => !fieldKeys.includes(fieldMetadataId));
|
||||
|
@ -1,14 +1,19 @@
|
||||
import { useContext } from 'react';
|
||||
import { useContext, useEffect } from 'react';
|
||||
import { Reference } from '@apollo/client';
|
||||
import { css } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { LightIconButton, MenuItem } from 'tsup.ui.index';
|
||||
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { FieldDisplay } from '@/object-record/field/components/FieldDisplay';
|
||||
import { FieldContext } from '@/object-record/field/contexts/FieldContext';
|
||||
import { usePersistField } from '@/object-record/field/hooks/usePersistField';
|
||||
import { entityFieldsFamilyState } from '@/object-record/field/states/entityFieldsFamilyState';
|
||||
import { FieldRelationMetadata } from '@/object-record/field/types/FieldMetadata';
|
||||
import { useFieldContext } from '@/object-record/hooks/useFieldContext';
|
||||
import { useModifyRecordFromCache } from '@/object-record/hooks/useModifyRecordFromCache';
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { IconDotsVertical, IconUnlink } from '@/ui/display/icon';
|
||||
@ -56,12 +61,24 @@ export const RecordRelationFieldCardContent = ({
|
||||
divider,
|
||||
relationRecord,
|
||||
}: RecordRelationFieldCardContentProps) => {
|
||||
const { fieldDefinition } = useContext(FieldContext);
|
||||
const { fieldDefinition, entityId } = useContext(FieldContext);
|
||||
|
||||
const {
|
||||
relationFieldMetadataId,
|
||||
relationObjectMetadataNameSingular,
|
||||
relationType,
|
||||
fieldName,
|
||||
objectMetadataNameSingular,
|
||||
} = fieldDefinition.metadata as FieldRelationMetadata;
|
||||
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular: objectMetadataNameSingular ?? '',
|
||||
});
|
||||
|
||||
const modifyObjectMetadataInCache = useModifyRecordFromCache({
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const isToOneObject = relationType === 'TO_ONE_OBJECT';
|
||||
const {
|
||||
labelIdentifierFieldMetadata: relationLabelIdentifierFieldMetadata,
|
||||
@ -86,6 +103,15 @@ export const RecordRelationFieldCardContent = ({
|
||||
|
||||
const { closeDropdown, isDropdownOpen } = useDropdown(dropdownScopeId);
|
||||
|
||||
// TODO: temporary as ChipDisplay expect to find the entity in the entityFieldsFamilyState
|
||||
const setEntityFields = useSetRecoilState(
|
||||
entityFieldsFamilyState(relationRecord.id),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setEntityFields(relationRecord);
|
||||
}, [relationRecord, setEntityFields]);
|
||||
|
||||
if (!FieldContextProvider) return null;
|
||||
|
||||
const handleDetach = () => {
|
||||
@ -109,38 +135,66 @@ export const RecordRelationFieldCardContent = ({
|
||||
[relationFieldMetadataItem.name]: null,
|
||||
},
|
||||
});
|
||||
|
||||
modifyObjectMetadataInCache(entityId, {
|
||||
[fieldName]: (relationRef, { readField }) => {
|
||||
const edges = readField<{ node: Reference }[]>('edges', relationRef);
|
||||
|
||||
if (!edges) {
|
||||
return relationRef;
|
||||
}
|
||||
|
||||
return {
|
||||
...relationRef,
|
||||
edges: edges.filter(({ node }) => {
|
||||
const id = readField('id', node);
|
||||
return id !== relationRecord.id;
|
||||
}),
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const isOpportunityCompanyRelation =
|
||||
(objectMetadataNameSingular === CoreObjectNameSingular.Opportunity &&
|
||||
relationObjectMetadataNameSingular === CoreObjectNameSingular.Company) ||
|
||||
(objectMetadataNameSingular === CoreObjectNameSingular.Company &&
|
||||
relationObjectMetadataNameSingular ===
|
||||
CoreObjectNameSingular.Opportunity);
|
||||
|
||||
return (
|
||||
<StyledCardContent isDropdownOpen={isDropdownOpen} divider={divider}>
|
||||
<FieldContextProvider>
|
||||
<FieldDisplay />
|
||||
</FieldContextProvider>
|
||||
<DropdownScope dropdownScopeId={dropdownScopeId}>
|
||||
<Dropdown
|
||||
dropdownId={dropdownScopeId}
|
||||
dropdownPlacement="right-start"
|
||||
clickableComponent={
|
||||
<LightIconButton
|
||||
className="displayOnHover"
|
||||
Icon={IconDotsVertical}
|
||||
accent="tertiary"
|
||||
/>
|
||||
}
|
||||
dropdownComponents={
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItem
|
||||
LeftIcon={IconUnlink}
|
||||
text="Detach"
|
||||
onClick={handleDetach}
|
||||
{/* TODO: temporary to prevent removing a company from an opportunity */}
|
||||
{isOpportunityCompanyRelation && (
|
||||
<DropdownScope dropdownScopeId={dropdownScopeId}>
|
||||
<Dropdown
|
||||
dropdownId={dropdownScopeId}
|
||||
dropdownPlacement="right-start"
|
||||
clickableComponent={
|
||||
<LightIconButton
|
||||
className="displayOnHover"
|
||||
Icon={IconDotsVertical}
|
||||
accent="tertiary"
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
}
|
||||
dropdownHotkeyScope={{
|
||||
scope: dropdownScopeId,
|
||||
}}
|
||||
/>
|
||||
</DropdownScope>
|
||||
}
|
||||
dropdownComponents={
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItem
|
||||
LeftIcon={IconUnlink}
|
||||
text="Detach"
|
||||
onClick={handleDetach}
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
}
|
||||
dropdownHotkeyScope={{
|
||||
scope: dropdownScopeId,
|
||||
}}
|
||||
/>
|
||||
</DropdownScope>
|
||||
)}
|
||||
</StyledCardContent>
|
||||
);
|
||||
};
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { useCallback, useContext, useEffect, useMemo } from 'react';
|
||||
import { useCallback, useContext } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Reference } from '@apollo/client';
|
||||
import { css } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import qs from 'qs';
|
||||
@ -12,10 +13,8 @@ import { usePersistField } from '@/object-record/field/hooks/usePersistField';
|
||||
import { entityFieldsFamilyState } from '@/object-record/field/states/entityFieldsFamilyState';
|
||||
import { entityFieldsFamilySelector } from '@/object-record/field/states/selectors/entityFieldsFamilySelector';
|
||||
import { FieldRelationMetadata } from '@/object-record/field/types/FieldMetadata';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||
import { useModifyRecordFromCache } from '@/object-record/hooks/useModifyRecordFromCache';
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import { useUpsertRecordFromState } from '@/object-record/hooks/useUpsertRecordFromState';
|
||||
import { RecordRelationFieldCardContent } from '@/object-record/record-relation-card/components/RecordRelationFieldCardContent';
|
||||
import { SingleEntitySelectMenuItemsWithSearch } from '@/object-record/relation-picker/components/SingleEntitySelectMenuItemsWithSearch';
|
||||
import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker';
|
||||
@ -87,6 +86,7 @@ export const RecordRelationFieldCardSection = () => {
|
||||
relationFieldMetadataId,
|
||||
relationObjectMetadataNameSingular,
|
||||
relationType,
|
||||
objectMetadataNameSingular,
|
||||
} = fieldDefinition.metadata as FieldRelationMetadata;
|
||||
const record = useRecoilValue(entityFieldsFamilyState(entityId));
|
||||
|
||||
@ -97,6 +97,10 @@ export const RecordRelationFieldCardSection = () => {
|
||||
objectNameSingular: relationObjectMetadataNameSingular,
|
||||
});
|
||||
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular: objectMetadataNameSingular ?? '',
|
||||
});
|
||||
|
||||
const relationFieldMetadataItem = relationObjectMetadataItem.fields.find(
|
||||
({ id }) => id === relationFieldMetadataId,
|
||||
);
|
||||
@ -107,48 +111,12 @@ export const RecordRelationFieldCardSection = () => {
|
||||
|
||||
const isToOneObject = relationType === 'TO_ONE_OBJECT';
|
||||
|
||||
const { record: relationRecordFromFieldValue } = useFindOneRecord({
|
||||
objectNameSingular: relationObjectMetadataNameSingular,
|
||||
objectRecordId: fieldValue?.id,
|
||||
skip: !relationLabelIdentifierFieldMetadata || !isToOneObject,
|
||||
});
|
||||
|
||||
// ONE_TO_MANY records cannot be retrieved from the field value,
|
||||
// as the record's field is an empty "Connection" object.
|
||||
// TODO: maybe the backend could return an array of related records instead?
|
||||
const { records: relationRecordsFromQuery } = useFindManyRecords({
|
||||
objectNameSingular: relationObjectMetadataNameSingular,
|
||||
filter: {
|
||||
// TODO: this won't work for MANY_TO_MANY relations.
|
||||
[`${relationFieldMetadataItem?.name}Id`]: {
|
||||
eq: entityId,
|
||||
},
|
||||
},
|
||||
skip:
|
||||
!relationLabelIdentifierFieldMetadata ||
|
||||
!relationFieldMetadataItem?.name ||
|
||||
isToOneObject,
|
||||
});
|
||||
|
||||
const relationRecords = useMemo(
|
||||
() =>
|
||||
relationRecordFromFieldValue
|
||||
? [relationRecordFromFieldValue]
|
||||
: relationRecordsFromQuery,
|
||||
[relationRecordFromFieldValue, relationRecordsFromQuery],
|
||||
);
|
||||
const relationRecordIds = useMemo(
|
||||
() => relationRecords.map(({ id }) => id),
|
||||
[relationRecords],
|
||||
);
|
||||
|
||||
const upsertRecordFromState = useUpsertRecordFromState();
|
||||
|
||||
useEffect(() => {
|
||||
relationRecords.forEach((relationRecord) =>
|
||||
upsertRecordFromState(relationRecord),
|
||||
);
|
||||
}, [relationRecords, upsertRecordFromState]);
|
||||
const relationRecords = !isToOneObject
|
||||
? fieldValue?.edges.map(({ node }: { node: any }) => node) ?? []
|
||||
: fieldValue
|
||||
? [fieldValue]
|
||||
: [];
|
||||
const relationRecordIds = relationRecords.map(({ id }: { id: string }) => id);
|
||||
|
||||
const dropdownId = `record-field-card-relation-picker-${fieldDefinition.label}`;
|
||||
|
||||
@ -186,6 +154,10 @@ export const RecordRelationFieldCardSection = () => {
|
||||
objectNameSingular: relationObjectMetadataNameSingular,
|
||||
});
|
||||
|
||||
const modifyObjectMetadataInCache = useModifyRecordFromCache({
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const handleRelationPickerEntitySelected = (
|
||||
selectedRelationEntity?: EntityForSelect,
|
||||
) => {
|
||||
@ -207,9 +179,22 @@ export const RecordRelationFieldCardSection = () => {
|
||||
[relationFieldMetadataItem.name]: record,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
if (!relationLabelIdentifierFieldMetadata) return null;
|
||||
modifyObjectMetadataInCache(entityId, {
|
||||
[fieldName]: (relationRef, { readField }) => {
|
||||
const edges = readField<{ node: Reference }[]>('edges', relationRef);
|
||||
|
||||
if (!edges) {
|
||||
return relationRef;
|
||||
}
|
||||
|
||||
return {
|
||||
...relationRef,
|
||||
edges: [...edges, { node: record }],
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const filterQueryParams: FilterQueryParams = {
|
||||
filter: {
|
||||
@ -263,13 +248,15 @@ export const RecordRelationFieldCardSection = () => {
|
||||
</StyledHeader>
|
||||
{!!relationRecords.length && (
|
||||
<Card>
|
||||
{relationRecords.slice(0, 5).map((relationRecord, index) => (
|
||||
<RecordRelationFieldCardContent
|
||||
key={`${relationRecord.id}${relationLabelIdentifierFieldMetadata?.id}`}
|
||||
divider={index < relationRecords.length - 1}
|
||||
relationRecord={relationRecord}
|
||||
/>
|
||||
))}
|
||||
{relationRecords
|
||||
.slice(0, 5)
|
||||
.map((relationRecord: any, index: number) => (
|
||||
<RecordRelationFieldCardContent
|
||||
key={`${relationRecord.id}${relationLabelIdentifierFieldMetadata?.id}`}
|
||||
divider={index < relationRecords.length - 1}
|
||||
relationRecord={relationRecord}
|
||||
/>
|
||||
))}
|
||||
</Card>
|
||||
)}
|
||||
</RelationPickerScope>
|
||||
|
@ -1,20 +1,13 @@
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { AddPersonToCompany } from '@/companies/components/AddPersonToCompany';
|
||||
import { companyProgressesFamilyState } from '@/companies/states/companyProgressesFamilyState';
|
||||
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
|
||||
import { FieldDefinition } from '@/object-record/field/types/FieldDefinition';
|
||||
import { FieldRelationMetadata } from '@/object-record/field/types/FieldMetadata';
|
||||
import { BoardCardIdContext } from '@/object-record/record-board/contexts/BoardCardIdContext';
|
||||
import { SingleEntitySelect } from '@/object-record/relation-picker/components/SingleEntitySelect';
|
||||
import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker';
|
||||
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
|
||||
import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
|
||||
import { useFilteredSearchEntityQuery } from '@/search/hooks/useFilteredSearchEntityQuery';
|
||||
import { IconForbid } from '@/ui/display/icon';
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export type RelationPickerProps = {
|
||||
recordId?: string;
|
||||
@ -40,26 +33,12 @@ export const RelationPicker = ({
|
||||
setRelationPickerSearchFilter,
|
||||
identifiersMapper,
|
||||
searchQuery,
|
||||
} = useRelationPicker();
|
||||
|
||||
const [showAddNewDropdown, setShowAddNewDropdown] = useState(false);
|
||||
|
||||
const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope();
|
||||
} = useRelationPicker({ relationPickerScopeId: 'relation-picker' });
|
||||
|
||||
useEffect(() => {
|
||||
setRelationPickerSearchFilter(initialSearchFilter ?? '');
|
||||
}, [initialSearchFilter, setRelationPickerSearchFilter]);
|
||||
|
||||
const boardCardId = useContext(BoardCardIdContext);
|
||||
const weAreInOpportunitiesPageCard = isDefined(boardCardId);
|
||||
|
||||
const [companyProgress] = useRecoilState(
|
||||
companyProgressesFamilyState(boardCardId ?? ''),
|
||||
);
|
||||
|
||||
const { company } = companyProgress ?? {};
|
||||
const companyId = company?.id;
|
||||
|
||||
const { objectNameSingular: relationObjectNameSingular } =
|
||||
useObjectNameSingularFromPlural({
|
||||
objectNamePlural:
|
||||
@ -90,41 +69,18 @@ export const RelationPicker = ({
|
||||
const handleEntitySelected = (selectedEntity: any | null | undefined) =>
|
||||
onSubmit(selectedEntity ?? null);
|
||||
|
||||
const entitiesToSelect = entities.entitiesToSelect.filter((entity) =>
|
||||
weAreInOpportunitiesPageCard ? entity.record.companyId === companyId : true,
|
||||
);
|
||||
|
||||
const weAreAddingNewPerson =
|
||||
weAreInOpportunitiesPageCard && showAddNewDropdown && companyId;
|
||||
|
||||
return (
|
||||
<>
|
||||
{!weAreAddingNewPerson ? (
|
||||
<SingleEntitySelect
|
||||
EmptyIcon={IconForbid}
|
||||
emptyLabel={'No ' + fieldDefinition.label}
|
||||
entitiesToSelect={entitiesToSelect}
|
||||
loading={entities.loading}
|
||||
onCancel={onCancel}
|
||||
onEntitySelected={handleEntitySelected}
|
||||
selectedEntity={entities.selectedEntities[0]}
|
||||
width={width}
|
||||
onCreate={() => {
|
||||
if (weAreInOpportunitiesPageCard) {
|
||||
setShowAddNewDropdown(true);
|
||||
setHotkeyScopeAndMemorizePreviousScope(
|
||||
RelationPickerHotkeyScope.AddNew,
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<AddPersonToCompany
|
||||
companyId={companyId}
|
||||
onEntitySelected={handleEntitySelected}
|
||||
closeDropdown={() => setShowAddNewDropdown(false)}
|
||||
/>
|
||||
)}
|
||||
<SingleEntitySelect
|
||||
EmptyIcon={IconForbid}
|
||||
emptyLabel={'No ' + fieldDefinition.label}
|
||||
entitiesToSelect={entities.entitiesToSelect}
|
||||
loading={entities.loading}
|
||||
onCancel={onCancel}
|
||||
onEntitySelected={handleEntitySelected}
|
||||
selectedEntity={entities.selectedEntities[0]}
|
||||
width={width}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,8 +1,7 @@
|
||||
import { useContext, useRef } from 'react';
|
||||
import { useRef } from 'react';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { Key } from 'ts-key-enum';
|
||||
|
||||
import { BoardCardIdContext } from '@/object-record/record-board/contexts/BoardCardIdContext';
|
||||
import { SelectableMenuItemSelect } from '@/object-record/relation-picker/components/SelectableMenuItemSelect';
|
||||
import { IconPlus } from '@/ui/display/icon';
|
||||
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
|
||||
@ -15,7 +14,6 @@ import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||
import { MenuItemSelect } from '@/ui/navigation/menu-item/components/MenuItemSelect';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { assertNotNull } from '~/utils/assert';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
import { EntityForSelect } from '../types/EntityForSelect';
|
||||
import { RelationPickerHotkeyScope } from '../types/RelationPickerHotkeyScope';
|
||||
@ -71,12 +69,6 @@ export const SingleEntitySelectMenuItems = ({
|
||||
|
||||
const selectableItemIds = entitiesInDropdown.map((entity) => entity.id);
|
||||
|
||||
const boardCardId = useContext(BoardCardIdContext);
|
||||
const weAreInOpportunitiesPageCard = isDefined(boardCardId);
|
||||
|
||||
const hideSearchResults =
|
||||
weAreInOpportunitiesPageCard && !entitiesInDropdown.length;
|
||||
|
||||
return (
|
||||
<div ref={containerRef}>
|
||||
<SelectableList
|
||||
@ -94,51 +86,49 @@ export const SingleEntitySelectMenuItems = ({
|
||||
}
|
||||
}}
|
||||
>
|
||||
{!hideSearchResults && (
|
||||
<>
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
{loading ? (
|
||||
<DropdownMenuSkeletonItem />
|
||||
) : entitiesInDropdown.length === 0 && !isAllEntitySelectShown ? (
|
||||
<MenuItem text="No result" />
|
||||
) : (
|
||||
<>
|
||||
{isAllEntitySelectShown &&
|
||||
selectAllLabel &&
|
||||
onAllEntitySelected && (
|
||||
<MenuItemSelect
|
||||
key="select-all"
|
||||
onClick={() => onAllEntitySelected()}
|
||||
LeftIcon={SelectAllIcon}
|
||||
text={selectAllLabel}
|
||||
selected={!!isAllEntitySelected}
|
||||
/>
|
||||
)}
|
||||
{emptyLabel && (
|
||||
<>
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
{loading ? (
|
||||
<DropdownMenuSkeletonItem />
|
||||
) : entitiesInDropdown.length === 0 && !isAllEntitySelectShown ? (
|
||||
<MenuItem text="No result" />
|
||||
) : (
|
||||
<>
|
||||
{isAllEntitySelectShown &&
|
||||
selectAllLabel &&
|
||||
onAllEntitySelected && (
|
||||
<MenuItemSelect
|
||||
key="select-none"
|
||||
onClick={() => onEntitySelected()}
|
||||
LeftIcon={EmptyIcon}
|
||||
text={emptyLabel}
|
||||
selected={!selectedEntity}
|
||||
key="select-all"
|
||||
onClick={() => onAllEntitySelected()}
|
||||
LeftIcon={SelectAllIcon}
|
||||
text={selectAllLabel}
|
||||
selected={!!isAllEntitySelected}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuItemsContainer>
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
{entitiesInDropdown?.map((entity) => (
|
||||
<SelectableMenuItemSelect
|
||||
key={entity.id}
|
||||
entity={entity}
|
||||
onEntitySelected={onEntitySelected}
|
||||
selectedEntity={selectedEntity}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
)}
|
||||
{(hideSearchResults || showCreateButton) && !loading && (
|
||||
{emptyLabel && (
|
||||
<MenuItemSelect
|
||||
key="select-none"
|
||||
onClick={() => onEntitySelected()}
|
||||
LeftIcon={EmptyIcon}
|
||||
text={emptyLabel}
|
||||
selected={!selectedEntity}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuItemsContainer>
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
{entitiesInDropdown?.map((entity) => (
|
||||
<SelectableMenuItemSelect
|
||||
key={entity.id}
|
||||
entity={entity}
|
||||
onEntitySelected={onEntitySelected}
|
||||
selectedEntity={selectedEntity}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
{showCreateButton && !loading && (
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
{entitiesToSelect.length > 0 && <DropdownMenuSeparator />}
|
||||
<CreateNewButton
|
||||
|
@ -1,6 +1,3 @@
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { BoardCardIdContext } from '@/object-record/record-board/contexts/BoardCardIdContext';
|
||||
import {
|
||||
SingleEntitySelectMenuItems,
|
||||
SingleEntitySelectMenuItemsProps,
|
||||
@ -38,20 +35,13 @@ export const SingleEntitySelectMenuItemsWithSearch = ({
|
||||
|
||||
const showCreateButton = isDefined(onCreate) && searchFilter !== '';
|
||||
|
||||
const boardCardId = useContext(BoardCardIdContext);
|
||||
const weAreInOpportunitiesPageCard = isDefined(boardCardId);
|
||||
const hideSearchInput =
|
||||
weAreInOpportunitiesPageCard && !entitiesToSelect.length && !selectedEntity;
|
||||
|
||||
return (
|
||||
<>
|
||||
{!hideSearchInput && (
|
||||
<DropdownMenuSearchInput
|
||||
value={searchFilter}
|
||||
onChange={handleSearchFilterChange}
|
||||
autoFocus
|
||||
/>
|
||||
)}
|
||||
<DropdownMenuSearchInput
|
||||
value={searchFilter}
|
||||
onChange={handleSearchFilterChange}
|
||||
autoFocus
|
||||
/>
|
||||
<DropdownMenuSeparator />
|
||||
<SingleEntitySelectMenuItems
|
||||
{...{
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { Reference, useApolloClient } from '@apollo/client';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
@ -127,18 +127,22 @@ export const useViewFields = (viewScopeId: string) => {
|
||||
}
|
||||
|
||||
modifyRecordFromCache(viewIdToPersist ?? '', {
|
||||
viewFields: () => ({
|
||||
edges: viewFieldsToPersist.map((viewField) => ({
|
||||
node: viewField,
|
||||
cursor: '',
|
||||
})),
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor: '',
|
||||
endCursor: '',
|
||||
},
|
||||
}),
|
||||
viewFields: (viewFieldsRef, { readField }) => {
|
||||
const edges = readField<{ node: Reference }[]>(
|
||||
'edges',
|
||||
viewFieldsRef,
|
||||
);
|
||||
|
||||
if (!edges) return viewFieldsRef;
|
||||
|
||||
return {
|
||||
...viewFieldsRef,
|
||||
edges: viewFieldsToPersist.map((viewField) => ({
|
||||
node: viewField,
|
||||
cursor: '',
|
||||
})),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
onViewFieldsChange?.(viewFieldsToPersist);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { Reference, useApolloClient } from '@apollo/client';
|
||||
import { produce } from 'immer';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
@ -145,18 +145,22 @@ export const useViewFilters = (viewScopeId: string) => {
|
||||
}
|
||||
|
||||
modifyRecordFromCache(existingViewId, {
|
||||
viewFilters: () => ({
|
||||
edges: currentViewFilters.map((viewFilter) => ({
|
||||
node: viewFilter,
|
||||
cursor: '',
|
||||
})),
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor: '',
|
||||
endCursor: '',
|
||||
},
|
||||
}),
|
||||
viewFilters: (viewFiltersRef, { readField }) => {
|
||||
const edges = readField<{ node: Reference }[]>(
|
||||
'edges',
|
||||
viewFiltersRef,
|
||||
);
|
||||
|
||||
if (!edges) return viewFiltersRef;
|
||||
|
||||
return {
|
||||
...viewFiltersRef,
|
||||
edges: currentViewFilters.map((viewFilter) => ({
|
||||
node: viewFilter,
|
||||
cursor: '',
|
||||
})),
|
||||
};
|
||||
},
|
||||
});
|
||||
},
|
||||
[
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { Reference, useApolloClient } from '@apollo/client';
|
||||
import { produce } from 'immer';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
@ -138,18 +138,22 @@ export const useViewSorts = (viewScopeId: string) => {
|
||||
}
|
||||
|
||||
modifyRecordFromCache(existingViewId, {
|
||||
viewSorts: () => ({
|
||||
edges: currentViewSorts.map((viewSort) => ({
|
||||
node: viewSort,
|
||||
cursor: '',
|
||||
})),
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor: '',
|
||||
endCursor: '',
|
||||
},
|
||||
}),
|
||||
viewSorts: (viewSortsRef, { readField }) => {
|
||||
const edges = readField<{ node: Reference }[]>(
|
||||
'edges',
|
||||
viewSortsRef,
|
||||
);
|
||||
|
||||
if (!edges) return viewSortsRef;
|
||||
|
||||
return {
|
||||
...viewSortsRef,
|
||||
edges: currentViewSorts.map((viewSort) => ({
|
||||
node: viewSort,
|
||||
cursor: '',
|
||||
})),
|
||||
};
|
||||
},
|
||||
});
|
||||
},
|
||||
[
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { Reference } from '@apollo/client';
|
||||
|
||||
import { useCreateOneRelationMetadataItem } from '@/object-metadata/hooks/useCreateOneRelationMetadataItem';
|
||||
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
|
||||
@ -163,15 +164,17 @@ export const SettingsObjectNewFieldStep2 = () => {
|
||||
|
||||
modifyViewFromCache(view.id, {
|
||||
// Todo fix typing
|
||||
viewFields: (viewFields: any) => {
|
||||
viewFields: (viewFieldsRef, { readField }) => {
|
||||
const edges = readField<{ node: Reference }[]>(
|
||||
'edges',
|
||||
viewFieldsRef,
|
||||
);
|
||||
|
||||
if (!edges) return viewFieldsRef;
|
||||
|
||||
return {
|
||||
edges: viewFields.edges.concat({ node: viewFieldToCreate }),
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor: '',
|
||||
endCursor: '',
|
||||
},
|
||||
...viewFieldsRef,
|
||||
edges: [...edges, { node: viewFieldToCreate }],
|
||||
};
|
||||
},
|
||||
});
|
||||
@ -188,16 +191,17 @@ export const SettingsObjectNewFieldStep2 = () => {
|
||||
size: 100,
|
||||
};
|
||||
modifyViewFromCache(view.id, {
|
||||
// Todo fix typing
|
||||
viewFields: (viewFields: any) => {
|
||||
viewFields: (viewFieldsRef, { readField }) => {
|
||||
const edges = readField<{ node: Reference }[]>(
|
||||
'edges',
|
||||
viewFieldsRef,
|
||||
);
|
||||
|
||||
if (!edges) return viewFieldsRef;
|
||||
|
||||
return {
|
||||
edges: viewFields.edges.concat({ node: viewFieldToCreate }),
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor: '',
|
||||
endCursor: '',
|
||||
},
|
||||
...viewFieldsRef,
|
||||
edges: [...edges, { node: viewFieldToCreate }],
|
||||
};
|
||||
},
|
||||
});
|
||||
@ -232,16 +236,17 @@ export const SettingsObjectNewFieldStep2 = () => {
|
||||
};
|
||||
|
||||
modifyViewFromCache(view.id, {
|
||||
// Todo fix typing
|
||||
viewFields: (viewFields: any) => {
|
||||
viewFields: (viewFieldsRef, { readField }) => {
|
||||
const edges = readField<{ node: Reference }[]>(
|
||||
'edges',
|
||||
viewFieldsRef,
|
||||
);
|
||||
|
||||
if (!edges) return viewFieldsRef;
|
||||
|
||||
return {
|
||||
edges: viewFields.edges.concat({ node: viewFieldToCreate }),
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor: '',
|
||||
endCursor: '',
|
||||
},
|
||||
...viewFieldsRef,
|
||||
edges: [...edges, { node: viewFieldToCreate }],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
@ -11,59 +11,59 @@ export const seedOpportunity = async (
|
||||
.insert()
|
||||
.into(`${schemaName}.${tableName}`, [
|
||||
'id',
|
||||
'name',
|
||||
'amountAmountMicros',
|
||||
'amountCurrencyCode',
|
||||
'closeDate',
|
||||
'probability',
|
||||
'pipelineStepId',
|
||||
'pointOfContactId',
|
||||
'personId',
|
||||
'companyId',
|
||||
])
|
||||
.orIgnore()
|
||||
.values([
|
||||
{
|
||||
id: '7c887ee3-be10-412b-a663-16bd3c2228e1',
|
||||
name: 'Opportunity 1',
|
||||
amountAmountMicros: 100000,
|
||||
amountCurrencyCode: 'USD',
|
||||
closeDate: new Date(),
|
||||
probability: 0.5,
|
||||
pipelineStepId: '6edf4ead-006a-46e1-9c6d-228f1d0143c9',
|
||||
pointOfContactId: '86083141-1c0e-494c-a1b6-85b1c6fefaa5',
|
||||
personId: '86083141-1c0e-494c-a1b6-85b1c6fefaa5',
|
||||
companyId: 'fe256b39-3ec3-4fe3-8997-b76aa0bfa408',
|
||||
},
|
||||
{
|
||||
id: '53f66647-0543-4cc2-9f96-95cc699960f2',
|
||||
name: 'Opportunity 2',
|
||||
amountAmountMicros: 2000000,
|
||||
amountCurrencyCode: 'USD',
|
||||
closeDate: new Date(),
|
||||
probability: 0.5,
|
||||
pipelineStepId: 'd8361722-03fb-4e65-bd4f-ec9e52e5ec0a',
|
||||
pointOfContactId: '93c72d2e-f517-42fd-80ae-14173b3b70ae',
|
||||
personId: '93c72d2e-f517-42fd-80ae-14173b3b70ae',
|
||||
companyId: '118995f3-5d81-46d6-bf83-f7fd33ea6102',
|
||||
},
|
||||
{
|
||||
id: '81ab695d-2f89-406f-90ea-180f433b2445',
|
||||
name: 'Opportunity 3',
|
||||
amountAmountMicros: 300000,
|
||||
amountCurrencyCode: 'USD',
|
||||
closeDate: new Date(),
|
||||
probability: 0.5,
|
||||
pipelineStepId: '30b14887-d592-427d-bd97-6e670158db02',
|
||||
pointOfContactId: '9b324a88-6784-4449-afdf-dc62cb8702f2',
|
||||
personId: '9b324a88-6784-4449-afdf-dc62cb8702f2',
|
||||
companyId: '460b6fb1-ed89-413a-b31a-962986e67bb4',
|
||||
},
|
||||
{
|
||||
id: '9b059852-35b1-4045-9cde-42f715148954',
|
||||
name: 'Opportunity 4',
|
||||
amountAmountMicros: 4000000,
|
||||
amountCurrencyCode: 'USD',
|
||||
closeDate: new Date(),
|
||||
probability: 0.5,
|
||||
pipelineStepId: '30b14887-d592-427d-bd97-6e670158db02',
|
||||
pointOfContactId: '98406e26-80f1-4dff-b570-a74942528de3',
|
||||
personId: '98406e26-80f1-4dff-b570-a74942528de3',
|
||||
companyId: '460b6fb1-ed89-413a-b31a-962986e67bb4',
|
||||
},
|
||||
])
|
||||
|
@ -6,6 +6,7 @@ import { ObjectMetadata } from 'src/workspace/workspace-sync-metadata/decorators
|
||||
import { ActivityObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/activity.object-metadata';
|
||||
import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata';
|
||||
import { CompanyObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/company.object-metadata';
|
||||
import { OpportunityObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/opportunity.object-metadata';
|
||||
import { PersonObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/person.object-metadata';
|
||||
|
||||
@ObjectMetadata({
|
||||
@ -46,4 +47,14 @@ export class ActivityTargetObjectMetadata extends BaseObjectMetadata {
|
||||
})
|
||||
@IsNullable()
|
||||
company: CompanyObjectMetadata;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Opportunity',
|
||||
description: 'ActivityTarget opportunity',
|
||||
icon: 'IconTargetArrow',
|
||||
joinColumn: 'opportunityId',
|
||||
})
|
||||
@IsNullable()
|
||||
opportunity: OpportunityObjectMetadata;
|
||||
}
|
||||
|
@ -1,8 +1,11 @@
|
||||
import { CurrencyMetadata } from 'src/metadata/field-metadata/composite-types/currency.composite-type';
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity';
|
||||
import { FieldMetadata } from 'src/workspace/workspace-sync-metadata/decorators/field-metadata.decorator';
|
||||
import { IsNullable } from 'src/workspace/workspace-sync-metadata/decorators/is-nullable.decorator';
|
||||
import { ObjectMetadata } from 'src/workspace/workspace-sync-metadata/decorators/object-metadata.decorator';
|
||||
import { RelationMetadata } from 'src/workspace/workspace-sync-metadata/decorators/relation-metadata.decorator';
|
||||
import { ActivityTargetObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/activity-target.object-metadata';
|
||||
import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata';
|
||||
import { CompanyObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/company.object-metadata';
|
||||
import { PersonObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/person.object-metadata';
|
||||
@ -16,6 +19,14 @@ import { PipelineStepObjectMetadata } from 'src/workspace/workspace-sync-metadat
|
||||
icon: 'IconTargetArrow',
|
||||
})
|
||||
export class OpportunityObjectMetadata extends BaseObjectMetadata {
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Name',
|
||||
description: 'The opportunity name',
|
||||
icon: 'IconTargetArrow',
|
||||
})
|
||||
name: string;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.CURRENCY,
|
||||
label: 'Amount',
|
||||
@ -65,16 +76,6 @@ export class OpportunityObjectMetadata extends BaseObjectMetadata {
|
||||
@IsNullable()
|
||||
pointOfContact: PersonObjectMetadata;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Person',
|
||||
description: 'Opportunity person',
|
||||
icon: 'IconUser',
|
||||
joinColumn: 'personId',
|
||||
})
|
||||
@IsNullable()
|
||||
person: PersonObjectMetadata;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Company',
|
||||
@ -84,4 +85,17 @@ export class OpportunityObjectMetadata extends BaseObjectMetadata {
|
||||
})
|
||||
@IsNullable()
|
||||
company: CompanyObjectMetadata;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Activities',
|
||||
description: 'Activities tied to the opportunity',
|
||||
icon: 'IconCheckbox',
|
||||
})
|
||||
@RelationMetadata({
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
objectName: 'activityTarget',
|
||||
})
|
||||
@IsNullable()
|
||||
activityTargets: ActivityTargetObjectMetadata[];
|
||||
}
|
||||
|
@ -135,19 +135,6 @@ export class PersonObjectMetadata extends BaseObjectMetadata {
|
||||
@IsNullable()
|
||||
activityTargets: ActivityTargetObjectMetadata[];
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Opportunities',
|
||||
description: 'Opportunities linked to the contact.',
|
||||
icon: 'IconTargetArrow',
|
||||
})
|
||||
@RelationMetadata({
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
objectName: 'opportunity',
|
||||
})
|
||||
@IsNullable()
|
||||
opportunities: OpportunityObjectMetadata[];
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Favorites',
|
||||
|
Loading…
Reference in New Issue
Block a user