Rework relations (#3431)

* Rework relations

* Fix tests
This commit is contained in:
Charles Bochet
2024-01-15 12:07:23 +01:00
committed by GitHub
parent 8c96acc2a3
commit 16a24c5f0c
60 changed files with 392 additions and 463 deletions

View File

@ -46,6 +46,7 @@ export const FIND_MANY_OBJECT_METADATA_ITEMS = gql`
dataSourceId
nameSingular
namePlural
isSystem
}
toFieldMetadataId
}
@ -57,6 +58,7 @@ export const FIND_MANY_OBJECT_METADATA_ITEMS = gql`
dataSourceId
nameSingular
namePlural
isSystem
}
fromFieldMetadataId
}

View File

@ -54,6 +54,26 @@ export const useMapFieldMetadataToGraphQLQuery = () => {
)
.join('\n')}
}`;
} else if (
fieldType === 'RELATION' &&
field.toRelationMetadata?.relationType === 'ONE_TO_ONE'
) {
const relationMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.id ===
(field.toRelationMetadata as any)?.fromObjectMetadata?.id,
);
return `${field.name}
{
id
${(relationMetadataItem?.fields ?? [])
.filter((field) => field.type !== 'RELATION')
.map((field) =>
mapFieldMetadataToGraphQLQuery(field, maxDepthForRelations - 1),
)
.join('\n')}
}`;
} else if (
fieldType === 'RELATION' &&
field.fromRelationMetadata?.relationType === 'ONE_TO_MANY'

View File

@ -9,7 +9,7 @@ export type FieldMetadataItem = Omit<
| (Pick<Relation, 'id' | 'toFieldMetadataId' | 'relationType'> & {
toObjectMetadata: Pick<
Relation['toObjectMetadata'],
'id' | 'nameSingular' | 'namePlural'
'id' | 'nameSingular' | 'namePlural' | 'isSystem'
>;
})
| null;
@ -17,7 +17,7 @@ export type FieldMetadataItem = Omit<
| (Pick<Relation, 'id' | 'fromFieldMetadataId' | 'relationType'> & {
fromObjectMetadata: Pick<
Relation['fromObjectMetadata'],
'id' | 'nameSingular' | 'namePlural'
'id' | 'nameSingular' | 'namePlural' | 'isSystem'
>;
})
| null;

View File

@ -1,5 +0,0 @@
export enum StandardObjectNameSingular {
Company = 'company',
Person = 'person',
Opportunity = 'opportunity',
}

View File

@ -0,0 +1,11 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
export const isObjectMetadataAvailableForRelation = (
objectMetadataItem: Pick<ObjectMetadataItem, 'isSystem' | 'nameSingular'>,
) => {
return (
!objectMetadataItem.isSystem ||
objectMetadataItem.nameSingular === CoreObjectNameSingular.WorkspaceMember
);
};

View File

@ -1,11 +0,0 @@
import { StandardObjectNameSingular } from '@/object-metadata/types/StandardObjectNameSingular';
export const isStandardObject = (objectNameSingular: string) => {
const standardObjectNames = [
StandardObjectNameSingular.Company,
StandardObjectNameSingular.Person,
StandardObjectNameSingular.Opportunity,
] as string[];
return standardObjectNames.includes(objectNameSingular);
};

View File

@ -1,9 +1,10 @@
import { useParams } from 'react-router-dom';
import { useRecoilState } from 'recoil';
import { useSetRecoilState } from 'recoil';
import { useFavorites } from '@/favorites/hooks/useFavorites';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
import { isObjectMetadataAvailableForRelation } from '@/object-metadata/utils/isObjectMetadataAvailableForRelation';
import { parseFieldRelationType } from '@/object-metadata/utils/parseFieldRelationType';
import { parseFieldType } from '@/object-metadata/utils/parseFieldType';
import {
@ -62,7 +63,7 @@ export const RecordShowPage = () => {
const { favorites, createFavorite, deleteFavorite } = useFavorites();
const [, setEntityFields] = useRecoilState(
const setEntityFields = useSetRecoilState(
entityFieldsFamilyState(objectRecordId ?? ''),
);
@ -274,8 +275,21 @@ export const RecordShowPage = () => {
)}
</PropertyBox>
{isRelationFieldCardEnabled &&
relationFieldMetadataItems.map(
(fieldMetadataItem, index) => (
relationFieldMetadataItems
.filter((item) => {
const relationObjectMetadataItem =
item.toRelationMetadata
? item.toRelationMetadata.fromObjectMetadata
: item.fromRelationMetadata?.toObjectMetadata;
if (!relationObjectMetadataItem) {
return false;
}
return isObjectMetadataAvailableForRelation(
relationObjectMetadataItem,
);
})
.map((fieldMetadataItem, index) => (
<FieldContext.Provider
key={record.id + fieldMetadataItem.id}
value={{
@ -294,8 +308,7 @@ export const RecordShowPage = () => {
>
<RecordRelationFieldCardSection />
</FieldContext.Provider>
),
)}
))}
</>
)}
</ShowPageLeftContainer>

View File

@ -6,7 +6,9 @@ import { useRelationField } from '../../hooks/useRelationField';
export const RelationFieldDisplay = () => {
const { fieldValue, fieldDefinition } = useRelationField();
const { identifiersMapper } = useRelationPicker();
const { identifiersMapper } = useRelationPicker({
relationPickerScopeId: 'relation-picker',
});
if (!fieldValue || !fieldDefinition || !identifiersMapper) {
return <></>;

View File

@ -19,7 +19,9 @@ export const useChipField = () => {
const record = useRecoilValue<any | null>(entityFieldsFamilyState(entityId));
const { identifiersMapper } = useRelationPicker();
const { identifiersMapper } = useRelationPicker({
relationPickerScopeId: 'relation-picker',
});
return {
basePathToShowPage,

View File

@ -19,6 +19,7 @@ import { useUpsertRecordFromState } from '@/object-record/hooks/useUpsertRecordF
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';
import { RelationPickerScope } from '@/object-record/relation-picker/scopes/RelationPickerScope';
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
import { useFilteredSearchEntityQuery } from '@/search/hooks/useFilteredSearchEntityQuery';
import { IconForbid, IconPlus } from '@/ui/display/icon';
@ -153,12 +154,10 @@ export const RecordRelationFieldCardSection = () => {
const { closeDropdown, isDropdownOpen } = useDropdown(dropdownId);
const {
identifiersMapper,
relationPickerSearchFilter,
searchQuery,
setRelationPickerSearchFilter,
} = useRelationPicker();
const { relationPickerSearchFilter, setRelationPickerSearchFilter } =
useRelationPicker({ relationPickerScopeId: dropdownId });
const { identifiersMapper, searchQuery } = useRelationPicker();
const entities = useFilteredSearchEntityQuery({
filters: [
@ -225,53 +224,55 @@ export const RecordRelationFieldCardSection = () => {
return (
<Section>
<StyledHeader isDropdownOpen={isDropdownOpen}>
<StyledTitle>
<StyledTitleLabel>{fieldDefinition.label}</StyledTitleLabel>
{parseFieldRelationType(relationFieldMetadataItem) ===
'TO_ONE_OBJECT' && (
<StyledLink to={filterLinkHref}>
All ({relationRecords.length})
</StyledLink>
)}
</StyledTitle>
<DropdownScope dropdownScopeId={dropdownId}>
<StyledAddDropdown
dropdownId={dropdownId}
dropdownPlacement="right-start"
onClose={handleCloseRelationPickerDropdown}
clickableComponent={
<LightIconButton
className="displayOnHover"
Icon={IconPlus}
accent="tertiary"
/>
}
dropdownComponents={
<SingleEntitySelectMenuItemsWithSearch
EmptyIcon={IconForbid}
entitiesToSelect={entities.entitiesToSelect}
loading={entities.loading}
onEntitySelected={handleRelationPickerEntitySelected}
/>
}
dropdownHotkeyScope={{
scope: dropdownId,
}}
/>
</DropdownScope>
</StyledHeader>
{!!relationRecords.length && (
<Card>
{relationRecords.slice(0, 5).map((relationRecord, index) => (
<RecordRelationFieldCardContent
key={`${relationRecord.id}${relationLabelIdentifierFieldMetadata?.id}`}
divider={index < relationRecords.length - 1}
relationRecord={relationRecord}
<RelationPickerScope relationPickerScopeId={dropdownId}>
<StyledHeader isDropdownOpen={isDropdownOpen}>
<StyledTitle>
<StyledTitleLabel>{fieldDefinition.label}</StyledTitleLabel>
{parseFieldRelationType(relationFieldMetadataItem) ===
'TO_ONE_OBJECT' && (
<StyledLink to={filterLinkHref}>
All ({relationRecords.length})
</StyledLink>
)}
</StyledTitle>
<DropdownScope dropdownScopeId={dropdownId}>
<StyledAddDropdown
dropdownId={dropdownId}
dropdownPlacement="right-start"
onClose={handleCloseRelationPickerDropdown}
clickableComponent={
<LightIconButton
className="displayOnHover"
Icon={IconPlus}
accent="tertiary"
/>
}
dropdownComponents={
<SingleEntitySelectMenuItemsWithSearch
EmptyIcon={IconForbid}
entitiesToSelect={entities.entitiesToSelect}
loading={entities.loading}
onEntitySelected={handleRelationPickerEntitySelected}
/>
}
dropdownHotkeyScope={{
scope: dropdownId,
}}
/>
))}
</Card>
)}
</DropdownScope>
</StyledHeader>
{!!relationRecords.length && (
<Card>
{relationRecords.slice(0, 5).map((relationRecord, index) => (
<RecordRelationFieldCardContent
key={`${relationRecord.id}${relationLabelIdentifierFieldMetadata?.id}`}
divider={index < relationRecords.length - 1}
relationRecord={relationRecord}
/>
))}
</Card>
)}
</RelationPickerScope>
</Section>
);
};

View File

@ -4,6 +4,7 @@ import { expect, userEvent, within } from '@storybook/test';
import { IconUserCircle } from '@/ui/display/icon';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { ComponentWithRecoilScopeDecorator } from '~/testing/decorators/ComponentWithRecoilScopeDecorator';
import { RelationPickerDecorator } from '~/testing/decorators/RelationPickerDecorator';
import { mockedPeopleData } from '~/testing/mock-data/people';
import { sleep } from '~/testing/sleep';
@ -19,7 +20,11 @@ const entities = mockedPeopleData.map<EntityForSelect>((person) => ({
const meta: Meta<typeof SingleEntitySelect> = {
title: 'UI/Input/RelationPicker/SingleEntitySelect',
component: SingleEntitySelect,
decorators: [ComponentDecorator, ComponentWithRecoilScopeDecorator],
decorators: [
ComponentDecorator,
ComponentWithRecoilScopeDecorator,
RelationPickerDecorator,
],
argTypes: {
selectedEntity: {
options: entities.map(({ name }) => name),

View File

@ -7,9 +7,7 @@ export const useEntitySelectSearch = () => {
setRelationPickerPreselectedId,
relationPickerSearchFilter,
setRelationPickerSearchFilter,
} = useRelationPicker({
relationPickerScopeId: 'relation-picker',
});
} = useRelationPicker();
const debouncedSetSearchFilter = debounce(
setRelationPickerSearchFilter,

View File

@ -1,6 +1,7 @@
import styled from '@emotion/styled';
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
import { isObjectMetadataAvailableForRelation } from '@/object-metadata/utils/isObjectMetadataAvailableForRelation';
import { validateMetadataLabel } from '@/object-metadata/utils/validateMetadataLabel';
import { useIcons } from '@/ui/display/icon/hooks/useIcons';
import { IconPicker } from '@/ui/input/components/IconPicker';
@ -74,13 +75,13 @@ export const SettingsObjectFieldRelationForm = ({
fullWidth
disabled={disableRelationEdition}
value={values.type}
options={Object.entries(relationTypes).map(
([value, { label, Icon }]) => ({
options={Object.entries(relationTypes)
.filter(([value]) => 'ONE_TO_ONE' !== value)
.map(([value, { label, Icon }]) => ({
label,
value: value as RelationType,
Icon,
}),
)}
}))}
onChange={(value) => onChange({ type: value })}
/>
<Select
@ -90,7 +91,9 @@ export const SettingsObjectFieldRelationForm = ({
disabled={disableRelationEdition}
value={values.objectMetadataId}
options={objectMetadataItems
.filter((objectMetadataItem) => !objectMetadataItem.isSystem)
.filter((objectMetadataItem) =>
isObjectMetadataAvailableForRelation(objectMetadataItem),
)
.map((objectMetadataItem) => ({
label: objectMetadataItem.labelPlural,
value: objectMetadataItem.id,

View File

@ -7,7 +7,7 @@ import { Notes } from '@/activities/notes/components/Notes';
import { ObjectTasks } from '@/activities/tasks/components/ObjectTasks';
import { Timeline } from '@/activities/timeline/components/Timeline';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { isStandardObject } from '@/object-metadata/utils/isStandardObject';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import {
IconCheckbox,
IconMail,
@ -41,7 +41,7 @@ const StyledTabListContainer = styled.div`
const TAB_LIST_COMPONENT_ID = 'show-page-right-tab-list';
type ShowPageRightContainerProps = {
targetableObject?: ActivityTargetableObject;
targetableObject: ActivityTargetableObject;
timeline?: boolean;
tasks?: boolean;
notes?: boolean;
@ -60,11 +60,10 @@ export const ShowPageRightContainer = ({
const { activeTabIdState } = useTabList(TAB_LIST_COMPONENT_ID);
const activeTabId = useRecoilValue(activeTabIdState());
if (!targetableObject) return <></>;
const targetableObjectIsStandardObject = isStandardObject(
targetableObject.targetObjectNameSingular,
);
const { objectMetadataItem: targetableObjectMetadataItem } =
useObjectMetadataItem({
objectNameSingular: targetableObject.targetObjectNameSingular,
});
const TASK_TABS = [
{
@ -90,14 +89,14 @@ export const ShowPageRightContainer = ({
title: 'Files',
Icon: IconPaperclip,
hide: !notes,
disabled: !targetableObjectIsStandardObject,
disabled: targetableObjectMetadataItem.isCustom,
},
{
id: 'emails',
title: 'Emails',
Icon: IconMail,
hide: !emails,
disabled: !isMessagingEnabled || !targetableObjectIsStandardObject,
disabled: !isMessagingEnabled || targetableObjectMetadataItem.isCustom,
},
];