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