Fix opportunity relation (#3478)

* Fix opportunity relation

* Fix

* Fix

* Fix tests

* Fix

* Fix
This commit is contained in:
Charles Bochet 2024-01-16 14:39:48 +01:00 committed by GitHub
parent bf67f07291
commit fb93bb69fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 372 additions and 321 deletions

View File

@ -42,7 +42,7 @@ export const HooksCompanyBoardEffect = ({
const setAvailableBoardCardFields = useSetRecoilScopedStateV2(
availableRecordBoardCardFieldsScopedState,
'company-board-view',
'company-board',
);
useEffect(() => {

View File

@ -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,

View File

@ -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}`,

View File

@ -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,
);

View File

@ -27,7 +27,6 @@ import { isFieldUuid } from '../types/guards/isFieldUuid';
export const FieldDisplay = () => {
const { fieldDefinition, isLabelIdentifier } = useContext(FieldContext);
if (
isLabelIdentifier &&
(isFieldText(fieldDefinition) || isFieldFullName(fieldDefinition))

View File

@ -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,
});

View File

@ -46,6 +46,7 @@ const mocks = [
variables: {
input: {
id: mockedUuid,
name: 'Opportunity',
pipelineStepId: 'pipelineStepId',
companyId: 'New Opportunity',
},

View File

@ -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 () => {

View File

@ -24,6 +24,7 @@ export const useCreateOpportunity = () => {
await createOneOpportunity?.({
id: newUuid,
name: 'Opportunity',
pipelineStepId,
companyId: companyId,
});

View File

@ -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 }) =>

View File

@ -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));

View File

@ -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>
);
};

View File

@ -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>

View File

@ -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}
/>
</>
);
};

View File

@ -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

View File

@ -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
{...{

View File

@ -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);

View File

@ -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: '',
})),
};
},
});
},
[

View File

@ -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: '',
})),
};
},
});
},
[

View File

@ -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 }],
};
},
});

View File

@ -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',
},
])

View File

@ -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;
}

View File

@ -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[];
}

View File

@ -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',