feat: add RecordRelationFieldCardSection (#3176)
Closes #3123 Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
@ -1,188 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { useMutation } from '@apollo/client';
|
||||
import { getOperationName } from '@apollo/client/utilities';
|
||||
import styled from '@emotion/styled';
|
||||
import { flip, offset, useFloating } from '@floating-ui/react';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { FieldDoubleText } from '@/object-record/field/types/FieldDoubleText';
|
||||
import { RelationPicker } from '@/object-record/relation-picker/components/RelationPicker';
|
||||
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
|
||||
import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
|
||||
import { IconPlus } from '@/ui/display/icon';
|
||||
import { DoubleTextInput } from '@/ui/field/input/components/DoubleTextInput';
|
||||
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
position: static;
|
||||
`;
|
||||
|
||||
const StyledInputContainer = styled.div`
|
||||
background-color: transparent;
|
||||
box-shadow: ${({ theme }) => theme.boxShadow.strong};
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(0.5)};
|
||||
width: ${({ theme }) => theme.spacing(62.5)};
|
||||
& input,
|
||||
div {
|
||||
background-color: ${({ theme }) => theme.background.primary};
|
||||
width: 100%;
|
||||
}
|
||||
div {
|
||||
border-radius: ${({ theme }) => theme.spacing(1)};
|
||||
overflow: hidden;
|
||||
}
|
||||
input {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
}
|
||||
`;
|
||||
|
||||
export const AddPersonToCompany = ({
|
||||
companyId,
|
||||
peopleIds,
|
||||
}: {
|
||||
companyId: string;
|
||||
peopleIds?: string[];
|
||||
}) => {
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
const [isCreationDropdownOpen, setIsCreationDropdownOpen] = useState(false);
|
||||
const { refs, floatingStyles } = useFloating({
|
||||
open: isDropdownOpen,
|
||||
placement: 'right-start',
|
||||
middleware: [flip(), offset({ mainAxis: 30, crossAxis: 0 })],
|
||||
});
|
||||
|
||||
const handleEscape = () => {
|
||||
if (isCreationDropdownOpen) setIsCreationDropdownOpen(false);
|
||||
if (isDropdownOpen) setIsDropdownOpen(false);
|
||||
};
|
||||
|
||||
const {
|
||||
setHotkeyScopeAndMemorizePreviousScope,
|
||||
goBackToPreviousHotkeyScope,
|
||||
} = usePreviousHotkeyScope();
|
||||
|
||||
const {
|
||||
findManyRecordsQuery,
|
||||
updateOneRecordMutation,
|
||||
createOneRecordMutation,
|
||||
} = useObjectMetadataItem({
|
||||
objectNameSingular: CoreObjectNameSingular.Person,
|
||||
});
|
||||
|
||||
const [updatePerson] = useMutation(updateOneRecordMutation);
|
||||
const [createPerson] = useMutation(createOneRecordMutation);
|
||||
|
||||
const handlePersonSelected =
|
||||
(companyId: string) => async (newPerson: EntityForSelect | null) => {
|
||||
if (!newPerson) return;
|
||||
await updatePerson({
|
||||
variables: {
|
||||
idToUpdate: newPerson.id,
|
||||
input: {
|
||||
companyId: companyId,
|
||||
},
|
||||
},
|
||||
refetchQueries: [getOperationName(findManyRecordsQuery) ?? ''],
|
||||
});
|
||||
|
||||
handleClosePicker();
|
||||
};
|
||||
|
||||
const handleClosePicker = () => {
|
||||
if (isDropdownOpen) {
|
||||
setIsDropdownOpen(false);
|
||||
goBackToPreviousHotkeyScope();
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenPicker = () => {
|
||||
if (!isDropdownOpen) {
|
||||
setIsDropdownOpen(true);
|
||||
setHotkeyScopeAndMemorizePreviousScope(
|
||||
RelationPickerHotkeyScope.RelationPicker,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreatePerson = async ({
|
||||
firstValue,
|
||||
secondValue,
|
||||
}: FieldDoubleText) => {
|
||||
if (!firstValue && !secondValue) return;
|
||||
const newPersonId = v4();
|
||||
|
||||
await createPerson({
|
||||
variables: {
|
||||
input: {
|
||||
companyId: companyId,
|
||||
id: newPersonId,
|
||||
name: {
|
||||
firstName: firstValue,
|
||||
lastName: secondValue,
|
||||
},
|
||||
},
|
||||
},
|
||||
refetchQueries: [getOperationName(findManyRecordsQuery) ?? ''],
|
||||
});
|
||||
|
||||
setIsCreationDropdownOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<RecoilScope>
|
||||
<StyledContainer ref={refs.setReference}>
|
||||
<LightIconButton
|
||||
Icon={IconPlus}
|
||||
onClick={handleOpenPicker}
|
||||
size="small"
|
||||
accent="tertiary"
|
||||
/>
|
||||
|
||||
{isDropdownOpen && (
|
||||
<div ref={refs.setFloating} style={floatingStyles}>
|
||||
{isCreationDropdownOpen ? (
|
||||
<StyledInputContainer>
|
||||
<DoubleTextInput
|
||||
firstValue=""
|
||||
secondValue=""
|
||||
firstValuePlaceholder="First Name"
|
||||
secondValuePlaceholder="Last Name"
|
||||
onClickOutside={handleEscape}
|
||||
onEnter={handleCreatePerson}
|
||||
onEscape={handleEscape}
|
||||
hotkeyScope={RelationPickerHotkeyScope.RelationPicker}
|
||||
/>
|
||||
</StyledInputContainer>
|
||||
) : (
|
||||
<RelationPicker
|
||||
recordId={''}
|
||||
onSubmit={handlePersonSelected(companyId)}
|
||||
onCancel={handleClosePicker}
|
||||
excludeRecordIds={peopleIds ?? []}
|
||||
fieldDefinition={{
|
||||
label: 'Person',
|
||||
iconName: 'IconUser',
|
||||
fieldMetadataId: '',
|
||||
type: FieldMetadataType.Relation,
|
||||
metadata: {
|
||||
relationObjectMetadataNameSingular: 'person',
|
||||
relationObjectMetadataNamePlural: 'people',
|
||||
fieldName: 'person',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</StyledContainer>
|
||||
</RecoilScope>
|
||||
);
|
||||
};
|
||||
@ -1,95 +0,0 @@
|
||||
import { useQuery } from '@apollo/client';
|
||||
import styled from '@emotion/styled';
|
||||
import { isNonEmptyArray } from '@sniptt/guards';
|
||||
|
||||
import { Company } from '@/companies/types/Company';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { mapPaginatedRecordsToRecords } from '@/object-record/utils/mapPaginatedRecordsToRecords';
|
||||
import { PeopleCard } from '@/people/components/PeopleCard';
|
||||
|
||||
import { AddPersonToCompany } from './AddPersonToCompany';
|
||||
|
||||
export type CompanyTeamProps = {
|
||||
company: Pick<Company, 'id'>;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
margin-bottom: ${({ theme }) => theme.spacing(6)};
|
||||
`;
|
||||
|
||||
const StyledTitleContainer = styled.div`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding-bottom: ${({ theme }) => theme.spacing(0)};
|
||||
padding-top: ${({ theme }) => theme.spacing(3)};
|
||||
`;
|
||||
|
||||
const StyledListContainer = styled.div`
|
||||
align-items: flex-start;
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-radius: ${({ theme }) => theme.spacing(1)};
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledTitle = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
line-height: ${({ theme }) => theme.text.lineHeight.lg};
|
||||
`;
|
||||
|
||||
export const CompanyTeam = ({ company }: { company: any }) => {
|
||||
const { findManyRecordsQuery } = useObjectMetadataItem({
|
||||
objectNameSingular: CoreObjectNameSingular.Person,
|
||||
});
|
||||
|
||||
const { data } = useQuery(findManyRecordsQuery, {
|
||||
variables: {
|
||||
filter: {
|
||||
companyId: {
|
||||
eq: company.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const people = mapPaginatedRecordsToRecords({
|
||||
objectNamePlural: 'people',
|
||||
pagedRecords: data ?? [],
|
||||
});
|
||||
|
||||
const peopleIds = people.map((person) => person.id);
|
||||
|
||||
const hasPeople = isNonEmptyArray(peopleIds);
|
||||
|
||||
return (
|
||||
<>
|
||||
{hasPeople && (
|
||||
<StyledContainer>
|
||||
<StyledTitleContainer>
|
||||
<StyledTitle>Team</StyledTitle>
|
||||
<AddPersonToCompany companyId={company.id} peopleIds={peopleIds} />
|
||||
</StyledTitleContainer>
|
||||
<StyledListContainer>
|
||||
{people.map((person: any) => (
|
||||
<PeopleCard
|
||||
key={person.id}
|
||||
person={person}
|
||||
hasBottomBorder={person.id !== people.length - 1}
|
||||
/>
|
||||
))}
|
||||
</StyledListContainer>
|
||||
</StyledContainer>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -17,7 +17,12 @@ export const formatFieldMetadataItemAsColumnDefinition = ({
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
}): ColumnDefinition<FieldMetadata> => {
|
||||
const relationObjectMetadataItem =
|
||||
field.toRelationMetadata?.fromObjectMetadata;
|
||||
field.toRelationMetadata?.fromObjectMetadata ||
|
||||
field.fromRelationMetadata?.toObjectMetadata;
|
||||
|
||||
const relationFieldMetadataId =
|
||||
field.toRelationMetadata?.fromFieldMetadataId ||
|
||||
field.fromRelationMetadata?.toFieldMetadataId;
|
||||
|
||||
return {
|
||||
position,
|
||||
@ -29,6 +34,7 @@ export const formatFieldMetadataItemAsColumnDefinition = ({
|
||||
fieldName: field.name,
|
||||
placeHolder: field.label,
|
||||
relationType: parseFieldRelationType(field),
|
||||
relationFieldMetadataId,
|
||||
relationObjectMetadataNameSingular:
|
||||
relationObjectMetadataItem?.nameSingular ?? '',
|
||||
relationObjectMetadataNamePlural:
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { CompanyTeam } from '@/companies/components/CompanyTeam';
|
||||
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
|
||||
@ -15,6 +14,7 @@ import { entityFieldsFamilyState } from '@/object-record/field/states/entityFiel
|
||||
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
|
||||
import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox';
|
||||
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
|
||||
import { RecordRelationFieldCardSection } from '@/object-record/record-relation-card/components/RecordRelationFieldCardSection';
|
||||
import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker';
|
||||
import { isFieldMetadataItemAvailable } from '@/object-record/utils/isFieldMetadataItemAvailable';
|
||||
import { IconBuildingSkyscraper } from '@/ui/display/icon';
|
||||
@ -73,9 +73,7 @@ export const RecordShowPage = () => {
|
||||
});
|
||||
|
||||
const [uploadImage] = useUploadImageMutation();
|
||||
const { updateOneRecord } = useUpdateOneRecord({
|
||||
objectNameSingular,
|
||||
});
|
||||
const { updateOneRecord } = useUpdateOneRecord({ objectNameSingular });
|
||||
|
||||
const useUpdateOneObjectRecordMutation: RecordUpdateHook = () => {
|
||||
const updateEntity = ({ variables }: RecordUpdateHookParams) => {
|
||||
@ -146,16 +144,26 @@ export const RecordShowPage = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const fieldMetadataItemsToShow = [...objectMetadataItem.fields]
|
||||
.sort((fieldMetadataItemA, fieldMetadataItemB) =>
|
||||
fieldMetadataItemA.name.localeCompare(fieldMetadataItemB.name),
|
||||
)
|
||||
.filter(isFieldMetadataItemAvailable)
|
||||
const availableFieldMetadataItems = objectMetadataItem.fields
|
||||
.filter(
|
||||
(fieldMetadataItem) =>
|
||||
isFieldMetadataItemAvailable(fieldMetadataItem) &&
|
||||
fieldMetadataItem.id !== labelIdentifierFieldMetadata?.id,
|
||||
)
|
||||
.sort((fieldMetadataItemA, fieldMetadataItemB) =>
|
||||
fieldMetadataItemA.name.localeCompare(fieldMetadataItemB.name),
|
||||
);
|
||||
|
||||
const inlineFieldMetadataItems = availableFieldMetadataItems.filter(
|
||||
(fieldMetadataItem) =>
|
||||
fieldMetadataItem.type !== FieldMetadataType.Relation,
|
||||
);
|
||||
|
||||
const relationFieldMetadataItems = availableFieldMetadataItems.filter(
|
||||
(fieldMetadataItem) =>
|
||||
fieldMetadataItem.type === FieldMetadataType.Relation,
|
||||
);
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<PageTitle title={pageName} />
|
||||
@ -189,7 +197,7 @@ export const RecordShowPage = () => {
|
||||
<RecoilScope CustomRecoilScopeContext={ShowPageRecoilScopeContext}>
|
||||
<ShowPageContainer>
|
||||
<ShowPageLeftContainer>
|
||||
{!loading && record ? (
|
||||
{!loading && !!record && (
|
||||
<>
|
||||
<ShowPageSummaryCard
|
||||
id={record.id}
|
||||
@ -232,7 +240,7 @@ export const RecordShowPage = () => {
|
||||
}
|
||||
/>
|
||||
<PropertyBox extraPadding={true}>
|
||||
{fieldMetadataItemsToShow.map(
|
||||
{inlineFieldMetadataItems.map(
|
||||
(fieldMetadataItem, index) => (
|
||||
<FieldContext.Provider
|
||||
key={record.id + fieldMetadataItem.id}
|
||||
@ -255,16 +263,29 @@ export const RecordShowPage = () => {
|
||||
),
|
||||
)}
|
||||
</PropertyBox>
|
||||
{objectNameSingular === 'company' ? (
|
||||
<>
|
||||
<CompanyTeam company={record} />
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
{relationFieldMetadataItems.map(
|
||||
(fieldMetadataItem, index) => (
|
||||
<FieldContext.Provider
|
||||
key={record.id + fieldMetadataItem.id}
|
||||
value={{
|
||||
entityId: record.id,
|
||||
recoilScopeId: record.id + fieldMetadataItem.id,
|
||||
isLabelIdentifier: false,
|
||||
fieldDefinition:
|
||||
formatFieldMetadataItemAsColumnDefinition({
|
||||
field: fieldMetadataItem,
|
||||
position: index,
|
||||
objectMetadataItem,
|
||||
}),
|
||||
useUpdateRecord: useUpdateOneObjectRecordMutation,
|
||||
hotkeyScope: InlineCellHotkeyScope.InlineCell,
|
||||
}}
|
||||
>
|
||||
<RecordRelationFieldCardSection />
|
||||
</FieldContext.Provider>
|
||||
),
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</ShowPageLeftContainer>
|
||||
<ShowPageRightContainer
|
||||
|
||||
@ -16,10 +16,7 @@ export const useRelationField = () => {
|
||||
const fieldName = fieldDefinition.metadata.fieldName;
|
||||
|
||||
const [fieldValue, setFieldValue] = useRecoilState<any | null>(
|
||||
entityFieldsFamilySelector({
|
||||
entityId: entityId,
|
||||
fieldName: fieldName,
|
||||
}),
|
||||
entityFieldsFamilySelector({ entityId, fieldName }),
|
||||
);
|
||||
|
||||
const fieldInitialValue = useFieldInitialValue();
|
||||
|
||||
@ -75,12 +75,13 @@ export type FieldDefinitionRelationType =
|
||||
| 'TO_ONE_OBJECT';
|
||||
|
||||
export type FieldRelationMetadata = {
|
||||
objectMetadataNameSingular?: string;
|
||||
fieldName: string;
|
||||
useEditButton?: boolean;
|
||||
relationType?: FieldDefinitionRelationType;
|
||||
relationObjectMetadataNameSingular: string;
|
||||
objectMetadataNameSingular?: string;
|
||||
relationFieldMetadataId: string;
|
||||
relationObjectMetadataNamePlural: string;
|
||||
relationObjectMetadataNameSingular: string;
|
||||
relationType?: FieldDefinitionRelationType;
|
||||
useEditButton?: boolean;
|
||||
};
|
||||
|
||||
export type FieldSelectMetadata = {
|
||||
|
||||
@ -11,19 +11,21 @@ import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
|
||||
|
||||
export const useFieldContext = ({
|
||||
objectNameSingular,
|
||||
fieldMetadataName,
|
||||
objectRecordId,
|
||||
fieldPosition,
|
||||
clearable,
|
||||
fieldMetadataName,
|
||||
fieldPosition,
|
||||
isLabelIdentifier = false,
|
||||
objectNameSingular,
|
||||
objectRecordId,
|
||||
}: {
|
||||
objectNameSingular: string;
|
||||
objectRecordId: string;
|
||||
clearable?: boolean;
|
||||
fieldMetadataName: string;
|
||||
fieldPosition: number;
|
||||
clearable?: boolean;
|
||||
isLabelIdentifier?: boolean;
|
||||
objectNameSingular: string;
|
||||
objectRecordId: string;
|
||||
}) => {
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
const { basePathToShowPage, objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
@ -52,9 +54,12 @@ export const useFieldContext = ({
|
||||
<FieldContext.Provider
|
||||
key={objectRecordId + fieldMetadataItem.id}
|
||||
value={{
|
||||
basePathToShowPage: isLabelIdentifier
|
||||
? basePathToShowPage
|
||||
: undefined,
|
||||
entityId: objectRecordId,
|
||||
recoilScopeId: objectRecordId + fieldMetadataItem.id,
|
||||
isLabelIdentifier: false,
|
||||
isLabelIdentifier,
|
||||
fieldDefinition: formatFieldMetadataItemAsColumnDefinition({
|
||||
field: fieldMetadataItem,
|
||||
position: fieldPosition,
|
||||
|
||||
@ -7,7 +7,7 @@ export const useFindOneRecord = <
|
||||
ObjectType extends { id: string } & Record<string, any>,
|
||||
>({
|
||||
objectNameSingular,
|
||||
objectRecordId,
|
||||
objectRecordId = '',
|
||||
onCompleted,
|
||||
depth,
|
||||
skip,
|
||||
@ -18,9 +18,7 @@ export const useFindOneRecord = <
|
||||
depth?: number;
|
||||
}) => {
|
||||
const { objectMetadataItem, findOneRecordQuery } = useObjectMetadataItem(
|
||||
{
|
||||
objectNameSingular,
|
||||
},
|
||||
{ objectNameSingular },
|
||||
depth,
|
||||
);
|
||||
|
||||
@ -29,20 +27,12 @@ export const useFindOneRecord = <
|
||||
{ objectRecordId: string }
|
||||
>(findOneRecordQuery, {
|
||||
skip: !objectMetadataItem || !objectRecordId || skip,
|
||||
variables: {
|
||||
objectRecordId: objectRecordId ?? '',
|
||||
},
|
||||
onCompleted: (data) => {
|
||||
if (onCompleted) {
|
||||
onCompleted(data[objectNameSingular]);
|
||||
}
|
||||
},
|
||||
variables: { objectRecordId },
|
||||
onCompleted: (data) => onCompleted?.(data[objectNameSingular]),
|
||||
});
|
||||
|
||||
const record = data ? data[objectNameSingular] : undefined;
|
||||
|
||||
return {
|
||||
record,
|
||||
record: data?.[objectNameSingular] || undefined,
|
||||
loading,
|
||||
error,
|
||||
};
|
||||
|
||||
@ -4,8 +4,8 @@ import { entityFieldsFamilyState } from '@/object-record/field/states/entityFiel
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
// TODO: refactor with scoped state later
|
||||
export const useUpsertRecordTableItem = () => {
|
||||
return useRecoilCallback(
|
||||
export const useUpsertRecordFromState = () =>
|
||||
useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
<T extends { id: string }>(entity: T) => {
|
||||
const currentEntity = snapshot
|
||||
@ -18,4 +18,3 @@ export const useUpsertRecordTableItem = () => {
|
||||
},
|
||||
[],
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,52 @@
|
||||
import { useContext } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { FieldDisplay } from '@/object-record/field/components/FieldDisplay';
|
||||
import { FieldContext } from '@/object-record/field/contexts/FieldContext';
|
||||
import { FieldRelationMetadata } from '@/object-record/field/types/FieldMetadata';
|
||||
import { useFieldContext } from '@/object-record/hooks/useFieldContext';
|
||||
import { CardContent } from '@/ui/layout/card/components/CardContent';
|
||||
|
||||
const StyledCardContent = styled(CardContent)`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: ${({ theme }) => theme.spacing(10)};
|
||||
padding: ${({ theme }) => theme.spacing(0, 2, 0, 3)};
|
||||
`;
|
||||
|
||||
type RecordRelationFieldCardContentProps = {
|
||||
divider?: boolean;
|
||||
relationRecordId: string;
|
||||
};
|
||||
|
||||
export const RecordRelationFieldCardContent = ({
|
||||
divider,
|
||||
relationRecordId,
|
||||
}: RecordRelationFieldCardContentProps) => {
|
||||
const { fieldDefinition } = useContext(FieldContext);
|
||||
const { relationObjectMetadataNameSingular } =
|
||||
fieldDefinition.metadata as FieldRelationMetadata;
|
||||
const { labelIdentifierFieldMetadata: relationLabelIdentifierFieldMetadata } =
|
||||
useObjectMetadataItem({
|
||||
objectNameSingular: relationObjectMetadataNameSingular,
|
||||
});
|
||||
|
||||
const { FieldContextProvider } = useFieldContext({
|
||||
fieldMetadataName: relationLabelIdentifierFieldMetadata?.name || '',
|
||||
fieldPosition: 0,
|
||||
isLabelIdentifier: true,
|
||||
objectNameSingular: relationObjectMetadataNameSingular,
|
||||
objectRecordId: relationRecordId,
|
||||
});
|
||||
|
||||
if (!FieldContextProvider) return null;
|
||||
|
||||
return (
|
||||
<StyledCardContent divider={divider}>
|
||||
<FieldContextProvider>
|
||||
<FieldDisplay />
|
||||
</FieldContextProvider>
|
||||
</StyledCardContent>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,109 @@
|
||||
import { useContext, useEffect, useMemo } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { FieldContext } from '@/object-record/field/contexts/FieldContext';
|
||||
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 { useUpsertRecordFromState } from '@/object-record/hooks/useUpsertRecordFromState';
|
||||
import { RecordRelationFieldCardContent } from '@/object-record/record-relation-card/components/RecordRelationFieldCardContent';
|
||||
import { Card } from '@/ui/layout/card/components/Card';
|
||||
import { Section } from '@/ui/layout/section/components/Section';
|
||||
|
||||
const StyledTitle = styled.div`
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
margin-bottom: ${({ theme }) => theme.spacing(2)};
|
||||
padding: ${({ theme }) => theme.spacing(0, 1)};
|
||||
`;
|
||||
|
||||
export const RecordRelationFieldCardSection = () => {
|
||||
const { entityId, fieldDefinition } = useContext(FieldContext);
|
||||
const {
|
||||
relationFieldMetadataId,
|
||||
relationObjectMetadataNameSingular,
|
||||
relationType,
|
||||
} = fieldDefinition.metadata as FieldRelationMetadata;
|
||||
|
||||
const {
|
||||
labelIdentifierFieldMetadata: relationLabelIdentifierFieldMetadata,
|
||||
objectMetadataItem: relationObjectMetadataItem,
|
||||
} = useObjectMetadataItem({
|
||||
objectNameSingular: relationObjectMetadataNameSingular,
|
||||
});
|
||||
|
||||
const relationFieldMetadataItem = relationObjectMetadataItem.fields.find(
|
||||
({ id }) => id === relationFieldMetadataId,
|
||||
);
|
||||
|
||||
const fieldValue = useRecoilValue<
|
||||
({ id: string } & Record<string, any>) | null
|
||||
>(
|
||||
entityFieldsFamilySelector({
|
||||
entityId,
|
||||
fieldName: fieldDefinition.metadata.fieldName,
|
||||
}),
|
||||
);
|
||||
|
||||
const isToOneObject = relationType === 'TO_ONE_OBJECT';
|
||||
|
||||
const { record: recordFromFieldValue } = 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 } = useFindManyRecords({
|
||||
objectNameSingular: relationObjectMetadataNameSingular,
|
||||
limit: 5,
|
||||
filter: {
|
||||
// TODO: this won't work for MANY_TO_MANY relations.
|
||||
[`${relationFieldMetadataItem?.name}Id`]: {
|
||||
eq: entityId,
|
||||
},
|
||||
},
|
||||
skip:
|
||||
!relationLabelIdentifierFieldMetadata ||
|
||||
!relationFieldMetadataItem?.name ||
|
||||
isToOneObject,
|
||||
});
|
||||
|
||||
const relationRecords = useMemo(
|
||||
() => (recordFromFieldValue ? [recordFromFieldValue] : records),
|
||||
[recordFromFieldValue, records],
|
||||
);
|
||||
|
||||
const upsertRecordFromState = useUpsertRecordFromState();
|
||||
|
||||
useEffect(() => {
|
||||
if (!relationRecords.length) return;
|
||||
|
||||
relationRecords.forEach((relationRecord) =>
|
||||
upsertRecordFromState(relationRecord),
|
||||
);
|
||||
}, [relationRecords, upsertRecordFromState]);
|
||||
|
||||
if (!relationLabelIdentifierFieldMetadata) return null;
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<StyledTitle>{fieldDefinition.label}</StyledTitle>
|
||||
{!!relationRecords.length && (
|
||||
<Card>
|
||||
{relationRecords.map((relationRecord, index) => (
|
||||
<RecordRelationFieldCardContent
|
||||
key={`${relationRecord.id}${relationLabelIdentifierFieldMetadata?.id}`}
|
||||
divider={index < relationRecords.length - 1}
|
||||
relationRecordId={relationRecord.id}
|
||||
/>
|
||||
))}
|
||||
</Card>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
@ -12,6 +12,7 @@ import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotV
|
||||
|
||||
import { FieldMetadata } from '../../field/types/FieldMetadata';
|
||||
import { onEntityCountChangeScopedState } from '../states/onEntityCountChangeScopedState';
|
||||
import { useUpsertRecordFromState } from '../../hooks/useUpsertRecordFromState';
|
||||
import { ColumnDefinition } from '../types/ColumnDefinition';
|
||||
import { TableHotkeyScope } from '../types/TableHotkeyScope';
|
||||
|
||||
@ -23,7 +24,6 @@ import { useSelectAllRows } from './internal/useSelectAllRows';
|
||||
import { useSetRecordTableData } from './internal/useSetRecordTableData';
|
||||
import { useSetRowSelectedState } from './internal/useSetRowSelectedState';
|
||||
import { useSetSoftFocusPosition } from './internal/useSetSoftFocusPosition';
|
||||
import { useUpsertRecordTableItem } from './internal/useUpsertRecordTableItem';
|
||||
|
||||
type useRecordTableProps = {
|
||||
recordTableScopeId?: string;
|
||||
@ -131,7 +131,7 @@ export const useRecordTable = (props?: useRecordTableProps) => {
|
||||
|
||||
const resetTableRowSelection = useResetTableRowSelection(scopeId);
|
||||
|
||||
const upsertRecordTableItem = useUpsertRecordTableItem();
|
||||
const upsertRecordTableItem = useUpsertRecordFromState();
|
||||
|
||||
const setSoftFocusPosition = useSetSoftFocusPosition(scopeId);
|
||||
|
||||
|
||||
@ -1,13 +1,17 @@
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { parseFieldRelationType } from '@/object-metadata/utils/parseFieldRelationType';
|
||||
import { RelationMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const isFieldMetadataItemAvailable = (
|
||||
fieldMetadataItem: FieldMetadataItem,
|
||||
) =>
|
||||
fieldMetadataItem.type !== 'UUID' &&
|
||||
// TODO: Many to many relations are not supported yet.
|
||||
!(
|
||||
fieldMetadataItem.type === 'RELATION' &&
|
||||
parseFieldRelationType(fieldMetadataItem) !== 'TO_ONE_OBJECT'
|
||||
(
|
||||
fieldMetadataItem.fromRelationMetadata ??
|
||||
fieldMetadataItem.toRelationMetadata
|
||||
)?.relationType === RelationMetadataType.ManyToMany
|
||||
) &&
|
||||
!fieldMetadataItem.isSystem &&
|
||||
!!fieldMetadataItem.isActive;
|
||||
|
||||
@ -1,195 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useMutation } from '@apollo/client';
|
||||
import { getOperationName } from '@apollo/client/utilities';
|
||||
import styled from '@emotion/styled';
|
||||
import { autoUpdate, flip, offset, useFloating } from '@floating-ui/react';
|
||||
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { Person } from '@/people/types/Person';
|
||||
import { IconDotsVertical, IconLinkOff, IconTrash } from '@/ui/display/icon';
|
||||
import { FloatingIconButton } from '@/ui/input/button/components/FloatingIconButton';
|
||||
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||
import { Avatar } from '@/users/components/Avatar';
|
||||
|
||||
export type PeopleCardProps = {
|
||||
person: Pick<Person, 'id' | 'avatarUrl' | 'name' | 'jobTitle'>;
|
||||
hasBottomBorder?: boolean;
|
||||
};
|
||||
|
||||
const StyledCard = styled.div<{
|
||||
isHovered: boolean;
|
||||
hasBottomBorder?: boolean;
|
||||
}>`
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
background: ${({ theme, isHovered }) =>
|
||||
isHovered ? theme.background.tertiary : 'auto'};
|
||||
border-bottom: 1px solid
|
||||
${({ theme, hasBottomBorder }) =>
|
||||
hasBottomBorder ? theme.border.color.light : 'transparent'};
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
height: ${({ theme }) => theme.spacing(8)};
|
||||
padding: ${({ theme }) => theme.spacing(3)};
|
||||
&:hover {
|
||||
background: ${({ theme }) => theme.background.tertiary};
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledCardInfo = styled.div`
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
flex: 1 0 0;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const StyledTitle = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
line-height: ${({ theme }) => theme.text.lineHeight.lg};
|
||||
`;
|
||||
|
||||
const StyledJobTitle = styled.div`
|
||||
border-radius: ${({ theme }) => theme.spacing(1)};
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
padding-bottom: ${({ theme }) => theme.spacing(0.5)};
|
||||
padding-left: ${({ theme }) => theme.spacing(0)};
|
||||
padding-right: ${({ theme }) => theme.spacing(2)};
|
||||
padding-top: ${({ theme }) => theme.spacing(0.5)};
|
||||
|
||||
&:hover {
|
||||
background: ${({ theme }) => theme.background.tertiary};
|
||||
}
|
||||
`;
|
||||
|
||||
export const PeopleCard = ({
|
||||
person,
|
||||
hasBottomBorder = true,
|
||||
}: PeopleCardProps) => {
|
||||
const navigate = useNavigate();
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const [isOptionsOpen, setIsOptionsOpen] = useState(false);
|
||||
|
||||
const { refs, floatingStyles } = useFloating({
|
||||
strategy: 'absolute',
|
||||
middleware: [offset(10), flip()],
|
||||
whileElementsMounted: autoUpdate,
|
||||
placement: 'right-start',
|
||||
});
|
||||
|
||||
useListenClickOutside({
|
||||
refs: [refs.floating],
|
||||
callback: () => {
|
||||
setIsOptionsOpen(false);
|
||||
if (isOptionsOpen) {
|
||||
setIsHovered(false);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
setIsHovered(true);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
if (!isOptionsOpen) {
|
||||
setIsHovered(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleOptions = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation();
|
||||
setIsOptionsOpen(!isOptionsOpen);
|
||||
};
|
||||
|
||||
const {
|
||||
findManyRecordsQuery,
|
||||
updateOneRecordMutation,
|
||||
deleteOneRecordMutation,
|
||||
} = useObjectMetadataItem({
|
||||
objectNameSingular: CoreObjectNameSingular.Person,
|
||||
});
|
||||
|
||||
const [updatePerson] = useMutation(updateOneRecordMutation);
|
||||
const [deletePerson] = useMutation(deleteOneRecordMutation);
|
||||
|
||||
const handleDetachPerson = async () => {
|
||||
await updatePerson({
|
||||
variables: {
|
||||
idToUpdate: person.id,
|
||||
input: {
|
||||
companyId: null,
|
||||
},
|
||||
},
|
||||
refetchQueries: [getOperationName(findManyRecordsQuery) ?? ''],
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeletePerson = () => {
|
||||
deletePerson({
|
||||
variables: {
|
||||
idToDelete: person.id,
|
||||
},
|
||||
refetchQueries: [getOperationName(findManyRecordsQuery) ?? ''],
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledCard
|
||||
isHovered={isHovered}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onClick={() => navigate(`/object/person/${person.id}`)}
|
||||
hasBottomBorder={hasBottomBorder}
|
||||
>
|
||||
<Avatar
|
||||
size="lg"
|
||||
type="rounded"
|
||||
placeholder={person.name.firstName + ' ' + person.name.lastName}
|
||||
avatarUrl={person.avatarUrl}
|
||||
/>
|
||||
<StyledCardInfo>
|
||||
<StyledTitle>
|
||||
{person.name.firstName + ' ' + person.name.lastName}
|
||||
</StyledTitle>
|
||||
{person.jobTitle && <StyledJobTitle>{person.jobTitle}</StyledJobTitle>}
|
||||
</StyledCardInfo>
|
||||
{isHovered && (
|
||||
<div ref={refs.setReference}>
|
||||
<FloatingIconButton
|
||||
onClick={handleToggleOptions}
|
||||
size="small"
|
||||
Icon={IconDotsVertical}
|
||||
/>
|
||||
{isOptionsOpen && (
|
||||
<DropdownMenu
|
||||
data-select-disable
|
||||
ref={refs.setFloating}
|
||||
style={floatingStyles}
|
||||
>
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItem
|
||||
onClick={handleDetachPerson}
|
||||
LeftIcon={IconLinkOff}
|
||||
text="Detach relation"
|
||||
/>
|
||||
<MenuItem
|
||||
onClick={handleDeletePerson}
|
||||
LeftIcon={IconTrash}
|
||||
text="Delete person"
|
||||
accent="danger"
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</StyledCard>
|
||||
);
|
||||
};
|
||||
@ -1,4 +1,4 @@
|
||||
import { ReactElement } from 'react';
|
||||
import { ReactNode } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
@ -7,28 +7,23 @@ import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
||||
const StyledOuterContainer = styled.div`
|
||||
background: ${({ theme }) => theme.background.secondary};
|
||||
border-bottom-left-radius: 8px;
|
||||
border-right: ${({ theme }) => {
|
||||
const isMobile = useIsMobile();
|
||||
return !isMobile ? `1px solid ${theme.border.color.medium}` : 'none';
|
||||
}};
|
||||
border-right: ${({ theme }) =>
|
||||
useIsMobile() ? 'none' : `1px solid ${theme.border.color.medium}`};
|
||||
border-top-left-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(3)};
|
||||
|
||||
z-index: 10;
|
||||
`;
|
||||
|
||||
const StyledInnerContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0px ${({ theme }) => theme.spacing(2)} 0px
|
||||
${({ theme }) => theme.spacing(3)};
|
||||
width: ${({ theme }) => {
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
return isMobile ? `calc(100% - ${theme.spacing(5)})` : '320px';
|
||||
}};
|
||||
gap: ${({ theme }) => theme.spacing(6)};
|
||||
padding: ${({ theme }) => theme.spacing(3)};
|
||||
padding-right: ${({ theme }) => theme.spacing(2)};
|
||||
width: ${({ theme }) =>
|
||||
useIsMobile() ? `calc(100% - ${theme.spacing(5)})` : '320px'};
|
||||
`;
|
||||
|
||||
const StyledIntermediateContainer = styled.div`
|
||||
@ -38,7 +33,7 @@ const StyledIntermediateContainer = styled.div`
|
||||
`;
|
||||
|
||||
export type ShowPageLeftContainerProps = {
|
||||
children: ReactElement;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const ShowPageLeftContainer = ({
|
||||
|
||||
@ -25,8 +25,7 @@ const StyledShowPageSummaryCard = styled.div`
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(3)};
|
||||
justify-content: center;
|
||||
padding: ${({ theme }) => theme.spacing(6)} ${({ theme }) => theme.spacing(3)}
|
||||
${({ theme }) => theme.spacing(3)} ${({ theme }) => theme.spacing(3)};
|
||||
padding: ${({ theme }) => theme.spacing(3)};
|
||||
`;
|
||||
|
||||
const StyledInfoContainer = styled.div`
|
||||
|
||||
@ -9,7 +9,6 @@ import {
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { ObjectMetadataInterface } from 'src/metadata/field-metadata/interfaces/object-metadata.interface';
|
||||
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import { TypeDefinitionsStorage } from 'src/workspace/workspace-schema-builder/storages/type-definitions.storage';
|
||||
import { objectContainsRelationField } from 'src/workspace/workspace-schema-builder/utils/object-contains-relation-field';
|
||||
import { getResolverArgs } from 'src/workspace/workspace-schema-builder/utils/get-resolver-args.util';
|
||||
@ -114,61 +113,52 @@ export class ExtendObjectTypeDefinitionFactory {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (fieldMetadata.type) {
|
||||
case FieldMetadataType.RELATION: {
|
||||
const relationMetadata =
|
||||
fieldMetadata.fromRelationMetadata ??
|
||||
fieldMetadata.toRelationMetadata;
|
||||
const relationMetadata =
|
||||
fieldMetadata.fromRelationMetadata ?? fieldMetadata.toRelationMetadata;
|
||||
|
||||
if (!relationMetadata) {
|
||||
this.logger.error(
|
||||
`Could not find a relation metadata for ${fieldMetadata.id}`,
|
||||
{
|
||||
fieldMetadata,
|
||||
},
|
||||
);
|
||||
if (!relationMetadata) {
|
||||
this.logger.error(
|
||||
`Could not find a relation metadata for ${fieldMetadata.id}`,
|
||||
{ fieldMetadata },
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Could not find a relation metadata for ${fieldMetadata.id}`,
|
||||
);
|
||||
}
|
||||
|
||||
const relationDirection = deduceRelationDirection(
|
||||
fieldMetadata.objectMetadataId,
|
||||
relationMetadata,
|
||||
);
|
||||
const relationType = this.relationTypeFactory.create(
|
||||
fieldMetadata,
|
||||
relationMetadata,
|
||||
relationDirection,
|
||||
);
|
||||
let argsType: GraphQLFieldConfigArgumentMap | undefined = undefined;
|
||||
|
||||
// Args are only needed when relation is of kind `oneToMany` and the relation direction is `from`
|
||||
if (
|
||||
relationMetadata.relationType ===
|
||||
RelationMetadataType.ONE_TO_MANY &&
|
||||
relationDirection === RelationDirection.FROM
|
||||
) {
|
||||
const args = getResolverArgs('findMany');
|
||||
|
||||
argsType = this.argsFactory.create(
|
||||
{
|
||||
args,
|
||||
objectMetadataId: relationMetadata.toObjectMetadataId,
|
||||
},
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
fields[fieldMetadata.name] = {
|
||||
type: relationType,
|
||||
args: argsType,
|
||||
description: fieldMetadata.description,
|
||||
};
|
||||
break;
|
||||
}
|
||||
throw new Error(
|
||||
`Could not find a relation metadata for ${fieldMetadata.id}`,
|
||||
);
|
||||
}
|
||||
|
||||
const relationDirection = deduceRelationDirection(
|
||||
fieldMetadata.objectMetadataId,
|
||||
relationMetadata,
|
||||
);
|
||||
const relationType = this.relationTypeFactory.create(
|
||||
fieldMetadata,
|
||||
relationMetadata,
|
||||
relationDirection,
|
||||
);
|
||||
let argsType: GraphQLFieldConfigArgumentMap | undefined = undefined;
|
||||
|
||||
// Args are only needed when relation is of kind `oneToMany` and the relation direction is `from`
|
||||
if (
|
||||
relationMetadata.relationType === RelationMetadataType.ONE_TO_MANY &&
|
||||
relationDirection === RelationDirection.FROM
|
||||
) {
|
||||
const args = getResolverArgs('findMany');
|
||||
|
||||
argsType = this.argsFactory.create(
|
||||
{
|
||||
args,
|
||||
objectMetadataId: relationMetadata.toObjectMetadataId,
|
||||
},
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
fields[fieldMetadata.name] = {
|
||||
type: relationType,
|
||||
args: argsType,
|
||||
description: fieldMetadata.description,
|
||||
};
|
||||
}
|
||||
|
||||
return fields;
|
||||
|
||||
Reference in New Issue
Block a user