Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Display and update fields from fromManyObjects relations in Show card #5801

Merged
merged 8 commits into from
Jun 11, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -171,15 +171,10 @@ export const ActivityTargetInlineCellEditMode = ({
});
};

const handleCancel = () => {
closeEditableField();
};

return (
<StyledSelectContainer>
<MultipleObjectRecordSelect
selectedObjectRecordIds={selectedTargetObjectIds}
onCancel={handleCancel}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

was not used

onSubmit={handleSubmit}
/>
</StyledSelectContainer>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ export const formatFieldMetadataItemAsFieldDefinition = ({
relationObjectMetadataNamePlural:
relationObjectMetadataItem?.namePlural ?? '',
objectMetadataNameSingular: objectMetadataItem.nameSingular ?? '',
targetFieldMetadataName:
field.relationDefinition?.targetFieldMetadata?.name ?? '',
options: field.options,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { FullNameFieldInput } from '@/object-record/record-field/meta-types/inpu
import { LinksFieldInput } from '@/object-record/record-field/meta-types/input/components/LinksFieldInput';
import { MultiSelectFieldInput } from '@/object-record/record-field/meta-types/input/components/MultiSelectFieldInput';
import { RawJsonFieldInput } from '@/object-record/record-field/meta-types/input/components/RawJsonFieldInput';
import { RelationManyFieldInput } from '@/object-record/record-field/meta-types/input/components/RelationManyFieldInput';
import { SelectFieldInput } from '@/object-record/record-field/meta-types/input/components/SelectFieldInput';
import { RecordFieldInputScope } from '@/object-record/record-field/scopes/RecordFieldInputScope';
import { isFieldDate } from '@/object-record/record-field/types/guards/isFieldDate';
Expand All @@ -14,6 +15,7 @@ import { isFieldFullName } from '@/object-record/record-field/types/guards/isFie
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect';
import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson';
import { isFieldRelationFromManyObjects } from '@/object-record/record-field/types/guards/isFieldRelationFromManyObjects';
import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect';
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';

Expand Down Expand Up @@ -71,7 +73,13 @@ export const FieldInput = ({
recordFieldInputScopeId={getScopeIdFromComponentId(recordFieldInputdId)}
>
{isFieldRelation(fieldDefinition) ? (
<RelationFieldInput onSubmit={onSubmit} onCancel={onCancel} />
isFieldRelationFromManyObjects(fieldDefinition) ? (
<RelationManyFieldInput
relationPickerScopeId={`relation-picker-${fieldDefinition.metadata.fieldName}`}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't we have an unique id, maybe based on the field id? Also I believe you can use the function getScopeIdFromComponentId to build the right shape for a scope id

/>
) : (
<RelationFieldInput onSubmit={onSubmit} onCancel={onCancel} />
)
) : isFieldPhone(fieldDefinition) ||
isFieldDisplayedAsPhone(fieldDefinition) ? (
<PhoneFieldInput
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { useContext } from 'react';
import { useRecoilCallback } from 'recoil';

import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
import { FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { isFieldAddress } from '@/object-record/record-field/types/guards/isFieldAddress';
import { isFieldAddressValue } from '@/object-record/record-field/types/guards/isFieldAddressValue';
import { isFieldDate } from '@/object-record/record-field/types/guards/isFieldDate';
Expand All @@ -13,9 +15,11 @@ import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/is
import { isFieldMultiSelectValue } from '@/object-record/record-field/types/guards/isFieldMultiSelectValue';
import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson';
import { isFieldRawJsonValue } from '@/object-record/record-field/types/guards/isFieldRawJsonValue';
import { isFieldRelationFromManyObjects } from '@/object-record/record-field/types/guards/isFieldRelationFromManyObjects';
import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect';
import { isFieldSelectValue } from '@/object-record/record-field/types/guards/isFieldSelectValue';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';

import { FieldContext } from '../contexts/FieldContext';
import { isFieldBoolean } from '../types/guards/isFieldBoolean';
Expand Down Expand Up @@ -55,6 +59,11 @@ export const usePersistField = () => {
isFieldRelation(fieldDefinition) &&
isFieldRelationValue(valueToPersist);

const fieldIsRelationFromManyObjects =
isFieldRelationFromManyObjects(
fieldDefinition as FieldDefinition<FieldRelationMetadata>,
) && isFieldRelationValue(valueToPersist);

const fieldIsText =
isFieldText(fieldDefinition) && isFieldTextValue(valueToPersist);

Expand Down Expand Up @@ -137,15 +146,20 @@ export const usePersistField = () => {
);

if (fieldIsRelation) {
updateRecord?.({
variables: {
where: { id: entityId },
updateOneRecordInput: {
[fieldName]: valueToPersist,
[`${fieldName}Id`]: valueToPersist?.id ?? null,
if (fieldIsRelationFromManyObjects) {
throw new Error('Cannot update this relation.');
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we are not using usePersistField for FromManyObjects as it requires a specific logic that did not feel right to add here

Copy link
Contributor

@thomtrp thomtrp Jun 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels weird to throw here. You have a boolean isValuePersistable above that is true if fieldIsRelation. Can you simply change for fieldIsRelation && !fieldIsRelationFromManyObjects?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Throw an error if an update is attempted on a 'fromManyObjects' relation.

} else {
const value = valueToPersist as EntityForSelect;
updateRecord?.({
variables: {
where: { id: entityId },
updateOneRecordInput: {
[fieldName]: value,
[`${fieldName}Id`]: value?.id ?? null,
},
},
},
});
});
}
return;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,43 @@
import { isArray } from '@sniptt/guards';
import { EntityChip } from 'twenty-ui';

import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus';
import { useRelationFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useRelationFieldDisplay';
import { isFieldRelationFromManyObjects } from '@/object-record/record-field/types/guards/isFieldRelationFromManyObjects';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';

const RelationFromManyFieldDisplay = ({
fieldValue,
}: {
fieldValue: ObjectRecord[];
}) => {
const { isFocused } = useFieldFocus();
const { generateRecordChipData } = useRelationFieldDisplay();

const recordChipsData = fieldValue.map((fieldValueItem) =>
generateRecordChipData(fieldValueItem),
);

return (
<ExpandableList isChipCountDisplayed={isFocused}>
{recordChipsData.map((record) => {
return (
<EntityChip
key={record.id}
entityId={record.id}
name={record.name as any}
avatarType={record.avatarType}
avatarUrl={getImageAbsoluteURIOrBase64(record.avatarUrl) || ''}
linkToEntity={record.linkToShowPage}
/>
);
})}
</ExpandableList>
);
};

export const RelationFieldDisplay = () => {
const { fieldValue, fieldDefinition, generateRecordChipData } =
useRelationFieldDisplay();
Expand All @@ -14,6 +49,12 @@ export const RelationFieldDisplay = () => {
return null;
}

if (isArray(fieldValue) && isFieldRelationFromManyObjects(fieldDefinition)) {
return (
<RelationFromManyFieldDisplay fieldValue={fieldValue as ObjectRecord[]} />
);
}

const recordChipData = generateRecordChipData(fieldValue);

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@ import { useGetButtonIcon } from '@/object-record/record-field/hooks/useGetButto
import { useRecordFieldInput } from '@/object-record/record-field/hooks/useRecordFieldInput';
import { FieldRelationValue } from '@/object-record/record-field/types/FieldMetadata';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
import { FieldMetadataType } from '~/generated-metadata/graphql';

import { FieldContext } from '../../contexts/FieldContext';
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
import { isFieldRelation } from '../../types/guards/isFieldRelation';

// TODO: we will be able to type more precisely when we will have custom field and custom entities support
export const useRelationField = () => {
export const useRelationField = <
T extends EntityForSelect | EntityForSelect[],
>() => {
const { entityId, fieldDefinition, maxWidth } = useContext(FieldContext);
const button = useGetButtonIcon();

Expand All @@ -24,11 +26,11 @@ export const useRelationField = () => {

const fieldName = fieldDefinition.metadata.fieldName;

const [fieldValue, setFieldValue] = useRecoilState<FieldRelationValue>(
const [fieldValue, setFieldValue] = useRecoilState<FieldRelationValue<T>>(
recordStoreFamilySelector({ recordId: entityId, fieldName }),
);

const { getDraftValueSelector } = useRecordFieldInput<FieldRelationValue>(
const { getDraftValueSelector } = useRecordFieldInput<FieldRelationValue<T>>(
`${entityId}-${fieldName}`,
);
const draftValue = useRecoilValue(getDraftValueSelector());
Expand All @@ -41,5 +43,6 @@ export const useRelationField = () => {
initialSearchValue,
setFieldValue,
maxWidth: button && maxWidth ? maxWidth - 28 : maxWidth,
entityId,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const RelationFieldInput = ({
onCancel,
}: RelationFieldInputProps) => {
const { fieldDefinition, initialSearchValue, fieldValue } =
useRelationField();
useRelationField<EntityForSelect>();

const persistField = usePersistField();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { useMemo } from 'react';

import { ObjectMetadataItemsRelationPickerEffect } from '@/object-metadata/components/ObjectMetadataItemsRelationPickerEffect';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useUpdateRelationManyFieldInput } from '@/object-record/record-field/meta-types/input/hooks/useUpdateRelationManyFieldInput';
import { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlineCell';
import { MultiRecordSelect } from '@/object-record/relation-picker/components/MultiRecordSelect';
import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker';
import { useRelationPickerEntitiesOptions } from '@/object-record/relation-picker/hooks/useRelationPickerEntitiesOptions';
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';

import { useRelationField } from '../../hooks/useRelationField';

export const RelationManyFieldInput = ({
relationPickerScopeId = 'relation-picker',
}: {
relationPickerScopeId?: string;
}) => {
const { closeInlineCell: closeEditableField } = useInlineCell();

const { fieldDefinition, fieldValue } = useRelationField<EntityForSelect[]>();
const { entities, relationPickerSearchFilter } =
useRelationPickerEntitiesOptions({
relationObjectNameSingular:
fieldDefinition.metadata.relationObjectMetadataNameSingular,
relationPickerScopeId,
});

const { setRelationPickerSearchFilter } = useRelationPicker({
relationPickerScopeId,
});

const { handleChange } = useUpdateRelationManyFieldInput({ entities });

const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular:
fieldDefinition.metadata.relationObjectMetadataNameSingular,
});
const allRecords = useMemo(
() => [
...entities.entitiesToSelect.map((entity) => {
const { record, ...recordIdentifier } = entity;
return {
objectMetadataItem: objectMetadataItem,
record: record,
recordIdentifier: recordIdentifier,
};
}),
],
[entities.entitiesToSelect, objectMetadataItem],
);

const selectedRecords = useMemo(
() =>
allRecords.filter(
(entity) =>
fieldValue?.some((f) => {
return f.id === entity.recordIdentifier.id;
}),
),
[allRecords, fieldValue],
);

return (
<>
<ObjectMetadataItemsRelationPickerEffect
relationPickerScopeId={relationPickerScopeId}
/>
<MultiRecordSelect
allRecords={allRecords}
selectedObjectRecords={selectedRecords}
loading={entities.loading}
searchFilter={relationPickerSearchFilter}
setSearchFilter={setRelationPickerSearchFilter}
onSubmit={() => {
closeEditableField();
}}
onChange={handleChange}
/>
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { useRelationField } from '@/object-record/record-field/meta-types/hooks/useRelationField';
import { ObjectRecordForSelect } from '@/object-record/relation-picker/hooks/useMultiObjectSearch';
import { EntitiesForMultipleEntitySelect } from '@/object-record/relation-picker/types/EntitiesForMultipleEntitySelect';
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
import { isDefined } from '~/utils/isDefined';

export const useUpdateRelationManyFieldInput = ({
entities,
}: {
entities: EntitiesForMultipleEntitySelect<EntityForSelect>;
}) => {
const { fieldDefinition, fieldValue, setFieldValue, entityId } =
useRelationField<EntityForSelect[]>();

const { updateOneRecord } = useUpdateOneRecord({
objectNameSingular:
fieldDefinition.metadata.relationObjectMetadataNameSingular,
});

const fieldName = fieldDefinition.metadata.targetFieldMetadataName;

const handleChange = (
objectRecord: ObjectRecordForSelect | null,
isSelected: boolean,
) => {
const entityToAddOrRemove = entities.entitiesToSelect.find(
(entity) => entity.id === objectRecord?.recordIdentifier.id,
);
Comment on lines +27 to +29
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a null check for entities.entitiesToSelect to avoid potential runtime errors.


const updatedFieldValue = isSelected
? [...(fieldValue ?? []), entityToAddOrRemove]
: (fieldValue ?? []).filter(
(value) => value.id !== objectRecord?.recordIdentifier.id,
);
Comment on lines +31 to +35
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure entityToAddOrRemove is defined before adding it to updatedFieldValue to prevent adding undefined values.

setFieldValue(
updatedFieldValue.filter((value) =>
isDefined(value),
) as EntityForSelect[],
);
if (isDefined(objectRecord)) {
updateOneRecord({
idToUpdate: objectRecord.record?.id,
updateOneRecordInput: {
[`${fieldName}Id`]: isSelected ? entityId : null,
},
});
}
};

return { handleChange };
};
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
FieldTextValue,
FieldUUidValue,
} from '@/object-record/record-field/types/FieldMetadata';
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';

export type FieldTextDraftValue = string;
export type FieldNumberDraftValue = string;
Expand Down Expand Up @@ -78,7 +79,9 @@ export type FieldInputDraftValue<FieldValue> = FieldValue extends FieldTextValue
? FieldSelectDraftValue
: FieldValue extends FieldMultiSelectValue
? FieldMultiSelectDraftValue
: FieldValue extends FieldRelationValue
: FieldValue extends
| FieldRelationValue<EntityForSelect>
| FieldRelationValue<EntityForSelect[]>
? FieldRelationDraftValue
: FieldValue extends FieldAddressValue
? FieldAddressDraftValue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ export type FieldRelationMetadata = {
relationObjectMetadataNamePlural: string;
relationObjectMetadataNameSingular: string;
relationType?: FieldDefinitionRelationType;
targetFieldMetadataName?: string;
useEditButton?: boolean;
};

Expand Down Expand Up @@ -173,7 +174,8 @@ export type FieldRatingValue = (typeof RATING_VALUES)[number];
export type FieldSelectValue = string | null;
export type FieldMultiSelectValue = string[] | null;

export type FieldRelationValue = EntityForSelect | null;
export type FieldRelationValue<T extends EntityForSelect | EntityForSelect[]> =
T | null;

// See https://zod.dev/?id=json-type
type Literal = string | number | boolean | null;
Expand Down
Loading
Loading