@ -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
|
||||
}
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
export enum StandardObjectNameSingular {
|
||||
Company = 'company',
|
||||
Person = 'person',
|
||||
Opportunity = 'opportunity',
|
||||
}
|
||||
@ -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
|
||||
);
|
||||
};
|
||||
@ -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);
|
||||
};
|
||||
@ -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>
|
||||
|
||||
@ -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 <></>;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -7,9 +7,7 @@ export const useEntitySelectSearch = () => {
|
||||
setRelationPickerPreselectedId,
|
||||
relationPickerSearchFilter,
|
||||
setRelationPickerSearchFilter,
|
||||
} = useRelationPicker({
|
||||
relationPickerScopeId: 'relation-picker',
|
||||
});
|
||||
} = useRelationPicker();
|
||||
|
||||
const debouncedSetSearchFilter = debounce(
|
||||
setRelationPickerSearchFilter,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user