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;
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
}): ColumnDefinition<FieldMetadata> => {
|
}): ColumnDefinition<FieldMetadata> => {
|
||||||
const relationObjectMetadataItem =
|
const relationObjectMetadataItem =
|
||||||
field.toRelationMetadata?.fromObjectMetadata;
|
field.toRelationMetadata?.fromObjectMetadata ||
|
||||||
|
field.fromRelationMetadata?.toObjectMetadata;
|
||||||
|
|
||||||
|
const relationFieldMetadataId =
|
||||||
|
field.toRelationMetadata?.fromFieldMetadataId ||
|
||||||
|
field.fromRelationMetadata?.toFieldMetadataId;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
position,
|
position,
|
||||||
@ -29,6 +34,7 @@ export const formatFieldMetadataItemAsColumnDefinition = ({
|
|||||||
fieldName: field.name,
|
fieldName: field.name,
|
||||||
placeHolder: field.label,
|
placeHolder: field.label,
|
||||||
relationType: parseFieldRelationType(field),
|
relationType: parseFieldRelationType(field),
|
||||||
|
relationFieldMetadataId,
|
||||||
relationObjectMetadataNameSingular:
|
relationObjectMetadataNameSingular:
|
||||||
relationObjectMetadataItem?.nameSingular ?? '',
|
relationObjectMetadataItem?.nameSingular ?? '',
|
||||||
relationObjectMetadataNamePlural:
|
relationObjectMetadataNamePlural:
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
|
|
||||||
import { CompanyTeam } from '@/companies/components/CompanyTeam';
|
|
||||||
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
|
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 { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
|
||||||
import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox';
|
import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox';
|
||||||
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
|
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 { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker';
|
||||||
import { isFieldMetadataItemAvailable } from '@/object-record/utils/isFieldMetadataItemAvailable';
|
import { isFieldMetadataItemAvailable } from '@/object-record/utils/isFieldMetadataItemAvailable';
|
||||||
import { IconBuildingSkyscraper } from '@/ui/display/icon';
|
import { IconBuildingSkyscraper } from '@/ui/display/icon';
|
||||||
@ -73,9 +73,7 @@ export const RecordShowPage = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const [uploadImage] = useUploadImageMutation();
|
const [uploadImage] = useUploadImageMutation();
|
||||||
const { updateOneRecord } = useUpdateOneRecord({
|
const { updateOneRecord } = useUpdateOneRecord({ objectNameSingular });
|
||||||
objectNameSingular,
|
|
||||||
});
|
|
||||||
|
|
||||||
const useUpdateOneObjectRecordMutation: RecordUpdateHook = () => {
|
const useUpdateOneObjectRecordMutation: RecordUpdateHook = () => {
|
||||||
const updateEntity = ({ variables }: RecordUpdateHookParams) => {
|
const updateEntity = ({ variables }: RecordUpdateHookParams) => {
|
||||||
@ -146,16 +144,26 @@ export const RecordShowPage = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const fieldMetadataItemsToShow = [...objectMetadataItem.fields]
|
const availableFieldMetadataItems = objectMetadataItem.fields
|
||||||
.sort((fieldMetadataItemA, fieldMetadataItemB) =>
|
|
||||||
fieldMetadataItemA.name.localeCompare(fieldMetadataItemB.name),
|
|
||||||
)
|
|
||||||
.filter(isFieldMetadataItemAvailable)
|
|
||||||
.filter(
|
.filter(
|
||||||
(fieldMetadataItem) =>
|
(fieldMetadataItem) =>
|
||||||
|
isFieldMetadataItemAvailable(fieldMetadataItem) &&
|
||||||
fieldMetadataItem.id !== labelIdentifierFieldMetadata?.id,
|
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 (
|
return (
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
<PageTitle title={pageName} />
|
<PageTitle title={pageName} />
|
||||||
@ -189,7 +197,7 @@ export const RecordShowPage = () => {
|
|||||||
<RecoilScope CustomRecoilScopeContext={ShowPageRecoilScopeContext}>
|
<RecoilScope CustomRecoilScopeContext={ShowPageRecoilScopeContext}>
|
||||||
<ShowPageContainer>
|
<ShowPageContainer>
|
||||||
<ShowPageLeftContainer>
|
<ShowPageLeftContainer>
|
||||||
{!loading && record ? (
|
{!loading && !!record && (
|
||||||
<>
|
<>
|
||||||
<ShowPageSummaryCard
|
<ShowPageSummaryCard
|
||||||
id={record.id}
|
id={record.id}
|
||||||
@ -232,7 +240,7 @@ export const RecordShowPage = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<PropertyBox extraPadding={true}>
|
<PropertyBox extraPadding={true}>
|
||||||
{fieldMetadataItemsToShow.map(
|
{inlineFieldMetadataItems.map(
|
||||||
(fieldMetadataItem, index) => (
|
(fieldMetadataItem, index) => (
|
||||||
<FieldContext.Provider
|
<FieldContext.Provider
|
||||||
key={record.id + fieldMetadataItem.id}
|
key={record.id + fieldMetadataItem.id}
|
||||||
@ -255,16 +263,29 @@ export const RecordShowPage = () => {
|
|||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
</PropertyBox>
|
</PropertyBox>
|
||||||
{objectNameSingular === 'company' ? (
|
{relationFieldMetadataItems.map(
|
||||||
<>
|
(fieldMetadataItem, index) => (
|
||||||
<CompanyTeam company={record} />
|
<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>
|
</ShowPageLeftContainer>
|
||||||
<ShowPageRightContainer
|
<ShowPageRightContainer
|
||||||
|
|||||||
@ -16,10 +16,7 @@ export const useRelationField = () => {
|
|||||||
const fieldName = fieldDefinition.metadata.fieldName;
|
const fieldName = fieldDefinition.metadata.fieldName;
|
||||||
|
|
||||||
const [fieldValue, setFieldValue] = useRecoilState<any | null>(
|
const [fieldValue, setFieldValue] = useRecoilState<any | null>(
|
||||||
entityFieldsFamilySelector({
|
entityFieldsFamilySelector({ entityId, fieldName }),
|
||||||
entityId: entityId,
|
|
||||||
fieldName: fieldName,
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const fieldInitialValue = useFieldInitialValue();
|
const fieldInitialValue = useFieldInitialValue();
|
||||||
|
|||||||
@ -75,12 +75,13 @@ export type FieldDefinitionRelationType =
|
|||||||
| 'TO_ONE_OBJECT';
|
| 'TO_ONE_OBJECT';
|
||||||
|
|
||||||
export type FieldRelationMetadata = {
|
export type FieldRelationMetadata = {
|
||||||
objectMetadataNameSingular?: string;
|
|
||||||
fieldName: string;
|
fieldName: string;
|
||||||
useEditButton?: boolean;
|
objectMetadataNameSingular?: string;
|
||||||
relationType?: FieldDefinitionRelationType;
|
relationFieldMetadataId: string;
|
||||||
relationObjectMetadataNameSingular: string;
|
|
||||||
relationObjectMetadataNamePlural: string;
|
relationObjectMetadataNamePlural: string;
|
||||||
|
relationObjectMetadataNameSingular: string;
|
||||||
|
relationType?: FieldDefinitionRelationType;
|
||||||
|
useEditButton?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FieldSelectMetadata = {
|
export type FieldSelectMetadata = {
|
||||||
|
|||||||
@ -11,19 +11,21 @@ import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
|||||||
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
|
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
|
||||||
|
|
||||||
export const useFieldContext = ({
|
export const useFieldContext = ({
|
||||||
objectNameSingular,
|
|
||||||
fieldMetadataName,
|
|
||||||
objectRecordId,
|
|
||||||
fieldPosition,
|
|
||||||
clearable,
|
clearable,
|
||||||
|
fieldMetadataName,
|
||||||
|
fieldPosition,
|
||||||
|
isLabelIdentifier = false,
|
||||||
|
objectNameSingular,
|
||||||
|
objectRecordId,
|
||||||
}: {
|
}: {
|
||||||
objectNameSingular: string;
|
clearable?: boolean;
|
||||||
objectRecordId: string;
|
|
||||||
fieldMetadataName: string;
|
fieldMetadataName: string;
|
||||||
fieldPosition: number;
|
fieldPosition: number;
|
||||||
clearable?: boolean;
|
isLabelIdentifier?: boolean;
|
||||||
|
objectNameSingular: string;
|
||||||
|
objectRecordId: string;
|
||||||
}) => {
|
}) => {
|
||||||
const { objectMetadataItem } = useObjectMetadataItem({
|
const { basePathToShowPage, objectMetadataItem } = useObjectMetadataItem({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -52,9 +54,12 @@ export const useFieldContext = ({
|
|||||||
<FieldContext.Provider
|
<FieldContext.Provider
|
||||||
key={objectRecordId + fieldMetadataItem.id}
|
key={objectRecordId + fieldMetadataItem.id}
|
||||||
value={{
|
value={{
|
||||||
|
basePathToShowPage: isLabelIdentifier
|
||||||
|
? basePathToShowPage
|
||||||
|
: undefined,
|
||||||
entityId: objectRecordId,
|
entityId: objectRecordId,
|
||||||
recoilScopeId: objectRecordId + fieldMetadataItem.id,
|
recoilScopeId: objectRecordId + fieldMetadataItem.id,
|
||||||
isLabelIdentifier: false,
|
isLabelIdentifier,
|
||||||
fieldDefinition: formatFieldMetadataItemAsColumnDefinition({
|
fieldDefinition: formatFieldMetadataItemAsColumnDefinition({
|
||||||
field: fieldMetadataItem,
|
field: fieldMetadataItem,
|
||||||
position: fieldPosition,
|
position: fieldPosition,
|
||||||
|
|||||||
@ -7,7 +7,7 @@ export const useFindOneRecord = <
|
|||||||
ObjectType extends { id: string } & Record<string, any>,
|
ObjectType extends { id: string } & Record<string, any>,
|
||||||
>({
|
>({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
objectRecordId,
|
objectRecordId = '',
|
||||||
onCompleted,
|
onCompleted,
|
||||||
depth,
|
depth,
|
||||||
skip,
|
skip,
|
||||||
@ -18,9 +18,7 @@ export const useFindOneRecord = <
|
|||||||
depth?: number;
|
depth?: number;
|
||||||
}) => {
|
}) => {
|
||||||
const { objectMetadataItem, findOneRecordQuery } = useObjectMetadataItem(
|
const { objectMetadataItem, findOneRecordQuery } = useObjectMetadataItem(
|
||||||
{
|
{ objectNameSingular },
|
||||||
objectNameSingular,
|
|
||||||
},
|
|
||||||
depth,
|
depth,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -29,20 +27,12 @@ export const useFindOneRecord = <
|
|||||||
{ objectRecordId: string }
|
{ objectRecordId: string }
|
||||||
>(findOneRecordQuery, {
|
>(findOneRecordQuery, {
|
||||||
skip: !objectMetadataItem || !objectRecordId || skip,
|
skip: !objectMetadataItem || !objectRecordId || skip,
|
||||||
variables: {
|
variables: { objectRecordId },
|
||||||
objectRecordId: objectRecordId ?? '',
|
onCompleted: (data) => onCompleted?.(data[objectNameSingular]),
|
||||||
},
|
|
||||||
onCompleted: (data) => {
|
|
||||||
if (onCompleted) {
|
|
||||||
onCompleted(data[objectNameSingular]);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const record = data ? data[objectNameSingular] : undefined;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
record,
|
record: data?.[objectNameSingular] || undefined,
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -4,8 +4,8 @@ import { entityFieldsFamilyState } from '@/object-record/field/states/entityFiel
|
|||||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||||
|
|
||||||
// TODO: refactor with scoped state later
|
// TODO: refactor with scoped state later
|
||||||
export const useUpsertRecordTableItem = () => {
|
export const useUpsertRecordFromState = () =>
|
||||||
return useRecoilCallback(
|
useRecoilCallback(
|
||||||
({ set, snapshot }) =>
|
({ set, snapshot }) =>
|
||||||
<T extends { id: string }>(entity: T) => {
|
<T extends { id: string }>(entity: T) => {
|
||||||
const currentEntity = snapshot
|
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 { FieldMetadata } from '../../field/types/FieldMetadata';
|
||||||
import { onEntityCountChangeScopedState } from '../states/onEntityCountChangeScopedState';
|
import { onEntityCountChangeScopedState } from '../states/onEntityCountChangeScopedState';
|
||||||
|
import { useUpsertRecordFromState } from '../../hooks/useUpsertRecordFromState';
|
||||||
import { ColumnDefinition } from '../types/ColumnDefinition';
|
import { ColumnDefinition } from '../types/ColumnDefinition';
|
||||||
import { TableHotkeyScope } from '../types/TableHotkeyScope';
|
import { TableHotkeyScope } from '../types/TableHotkeyScope';
|
||||||
|
|
||||||
@ -23,7 +24,6 @@ import { useSelectAllRows } from './internal/useSelectAllRows';
|
|||||||
import { useSetRecordTableData } from './internal/useSetRecordTableData';
|
import { useSetRecordTableData } from './internal/useSetRecordTableData';
|
||||||
import { useSetRowSelectedState } from './internal/useSetRowSelectedState';
|
import { useSetRowSelectedState } from './internal/useSetRowSelectedState';
|
||||||
import { useSetSoftFocusPosition } from './internal/useSetSoftFocusPosition';
|
import { useSetSoftFocusPosition } from './internal/useSetSoftFocusPosition';
|
||||||
import { useUpsertRecordTableItem } from './internal/useUpsertRecordTableItem';
|
|
||||||
|
|
||||||
type useRecordTableProps = {
|
type useRecordTableProps = {
|
||||||
recordTableScopeId?: string;
|
recordTableScopeId?: string;
|
||||||
@ -131,7 +131,7 @@ export const useRecordTable = (props?: useRecordTableProps) => {
|
|||||||
|
|
||||||
const resetTableRowSelection = useResetTableRowSelection(scopeId);
|
const resetTableRowSelection = useResetTableRowSelection(scopeId);
|
||||||
|
|
||||||
const upsertRecordTableItem = useUpsertRecordTableItem();
|
const upsertRecordTableItem = useUpsertRecordFromState();
|
||||||
|
|
||||||
const setSoftFocusPosition = useSetSoftFocusPosition(scopeId);
|
const setSoftFocusPosition = useSetSoftFocusPosition(scopeId);
|
||||||
|
|
||||||
|
|||||||
@ -1,13 +1,17 @@
|
|||||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
import { parseFieldRelationType } from '@/object-metadata/utils/parseFieldRelationType';
|
import { RelationMetadataType } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
export const isFieldMetadataItemAvailable = (
|
export const isFieldMetadataItemAvailable = (
|
||||||
fieldMetadataItem: FieldMetadataItem,
|
fieldMetadataItem: FieldMetadataItem,
|
||||||
) =>
|
) =>
|
||||||
fieldMetadataItem.type !== 'UUID' &&
|
fieldMetadataItem.type !== 'UUID' &&
|
||||||
|
// TODO: Many to many relations are not supported yet.
|
||||||
!(
|
!(
|
||||||
fieldMetadataItem.type === 'RELATION' &&
|
fieldMetadataItem.type === 'RELATION' &&
|
||||||
parseFieldRelationType(fieldMetadataItem) !== 'TO_ONE_OBJECT'
|
(
|
||||||
|
fieldMetadataItem.fromRelationMetadata ??
|
||||||
|
fieldMetadataItem.toRelationMetadata
|
||||||
|
)?.relationType === RelationMetadataType.ManyToMany
|
||||||
) &&
|
) &&
|
||||||
!fieldMetadataItem.isSystem &&
|
!fieldMetadataItem.isSystem &&
|
||||||
!!fieldMetadataItem.isActive;
|
!!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 styled from '@emotion/styled';
|
||||||
|
|
||||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||||
@ -7,28 +7,23 @@ import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
|||||||
const StyledOuterContainer = styled.div`
|
const StyledOuterContainer = styled.div`
|
||||||
background: ${({ theme }) => theme.background.secondary};
|
background: ${({ theme }) => theme.background.secondary};
|
||||||
border-bottom-left-radius: 8px;
|
border-bottom-left-radius: 8px;
|
||||||
border-right: ${({ theme }) => {
|
border-right: ${({ theme }) =>
|
||||||
const isMobile = useIsMobile();
|
useIsMobile() ? 'none' : `1px solid ${theme.border.color.medium}`};
|
||||||
return !isMobile ? `1px solid ${theme.border.color.medium}` : 'none';
|
|
||||||
}};
|
|
||||||
border-top-left-radius: 8px;
|
border-top-left-radius: 8px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: ${({ theme }) => theme.spacing(3)};
|
gap: ${({ theme }) => theme.spacing(3)};
|
||||||
|
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledInnerContainer = styled.div`
|
const StyledInnerContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 0px ${({ theme }) => theme.spacing(2)} 0px
|
gap: ${({ theme }) => theme.spacing(6)};
|
||||||
${({ theme }) => theme.spacing(3)};
|
padding: ${({ theme }) => theme.spacing(3)};
|
||||||
width: ${({ theme }) => {
|
padding-right: ${({ theme }) => theme.spacing(2)};
|
||||||
const isMobile = useIsMobile();
|
width: ${({ theme }) =>
|
||||||
|
useIsMobile() ? `calc(100% - ${theme.spacing(5)})` : '320px'};
|
||||||
return isMobile ? `calc(100% - ${theme.spacing(5)})` : '320px';
|
|
||||||
}};
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledIntermediateContainer = styled.div`
|
const StyledIntermediateContainer = styled.div`
|
||||||
@ -38,7 +33,7 @@ const StyledIntermediateContainer = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export type ShowPageLeftContainerProps = {
|
export type ShowPageLeftContainerProps = {
|
||||||
children: ReactElement;
|
children: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ShowPageLeftContainer = ({
|
export const ShowPageLeftContainer = ({
|
||||||
|
|||||||
@ -25,8 +25,7 @@ const StyledShowPageSummaryCard = styled.div`
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: ${({ theme }) => theme.spacing(3)};
|
gap: ${({ theme }) => theme.spacing(3)};
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: ${({ theme }) => theme.spacing(6)} ${({ theme }) => theme.spacing(3)}
|
padding: ${({ theme }) => theme.spacing(3)};
|
||||||
${({ theme }) => theme.spacing(3)} ${({ theme }) => theme.spacing(3)};
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledInfoContainer = styled.div`
|
const StyledInfoContainer = styled.div`
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import {
|
|||||||
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
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 { 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 { 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 { 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';
|
import { getResolverArgs } from 'src/workspace/workspace-schema-builder/utils/get-resolver-args.util';
|
||||||
@ -114,61 +113,52 @@ export class ExtendObjectTypeDefinitionFactory {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (fieldMetadata.type) {
|
const relationMetadata =
|
||||||
case FieldMetadataType.RELATION: {
|
fieldMetadata.fromRelationMetadata ?? fieldMetadata.toRelationMetadata;
|
||||||
const relationMetadata =
|
|
||||||
fieldMetadata.fromRelationMetadata ??
|
|
||||||
fieldMetadata.toRelationMetadata;
|
|
||||||
|
|
||||||
if (!relationMetadata) {
|
if (!relationMetadata) {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
`Could not find a relation metadata for ${fieldMetadata.id}`,
|
`Could not find a relation metadata for ${fieldMetadata.id}`,
|
||||||
{
|
{ fieldMetadata },
|
||||||
fieldMetadata,
|
);
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Could not find a relation metadata for ${fieldMetadata.id}`,
|
`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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
return fields;
|
||||||
|
|||||||
Reference in New Issue
Block a user