feat: add RecordRelationFieldCardSection (#3176)
Closes #3123 Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
@ -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;
|
||||
|
||||
Reference in New Issue
Block a user