feat: add RecordRelationFieldCardSection (#3176)

Closes #3123

Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
Thaïs
2024-01-05 07:02:02 -03:00
committed by GitHub
parent 80c1c9aacc
commit db46dd4497
17 changed files with 296 additions and 606 deletions

View File

@ -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

View File

@ -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();

View File

@ -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 = {

View File

@ -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,

View File

@ -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,
};

View File

@ -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 = () => {
},
[],
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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);

View File

@ -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;