diff --git a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx
index cf4e50c1b..28d551c15 100644
--- a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx
@@ -1,65 +1,46 @@
-import styled from '@emotion/styled';
-import { useCallback, useContext } from 'react';
+import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
-import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
+import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter';
+import { useAggregateRecords } from '@/object-record/hooks/useAggregateRecords';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/useIsRecordReadOnly';
-import { usePersistField } from '@/object-record/record-field/hooks/usePersistField';
-import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/record-field/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer';
-import { useUpdateRelationFromManyFieldInput } from '@/object-record/record-field/meta-types/input/hooks/useUpdateRelationFromManyFieldInput';
import { FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata';
-import { MultipleRecordPicker } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPicker';
-import { useMultipleRecordPickerPerformSearch } from '@/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerPerformSearch';
-import { multipleRecordPickerPickableMorphItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPickableMorphItemsComponentState';
-import { multipleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchFilterComponentState';
-import { multipleRecordPickerSearchableObjectMetadataItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchableObjectMetadataItemsComponentState';
-import { SingleRecordPicker } from '@/object-record/record-picker/single-record-picker/components/SingleRecordPicker';
-import { singleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSearchFilterComponentState';
-import { singleRecordPickerSelectedIdComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSelectedIdComponentState';
-import { SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord';
import { RecordDetailRelationRecordsList } from '@/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsList';
+import { RecordDetailRelationSectionDropdown } from '@/object-record/record-show/record-detail-section/components/RecordDetailRelationSectionDropdown';
import { RecordDetailSection } from '@/object-record/record-show/record-detail-section/components/RecordDetailSection';
import { RecordDetailSectionHeader } from '@/object-record/record-show/record-detail-section/components/RecordDetailSectionHeader';
-import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
+import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { prefetchIndexViewIdFromObjectMetadataItemFamilySelector } from '@/prefetch/states/selector/prefetchIndexViewIdFromObjectMetadataItemFamilySelector';
import { AppPath } from '@/types/AppPath';
-import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
-import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
-import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { useLingui } from '@lingui/react/macro';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
import { getAppPath } from '~/utils/navigation/getAppPath';
-import { IconForbid, IconPencil, IconPlus } from 'twenty-ui/display';
-import { LightIconButton } from 'twenty-ui/input';
type RecordDetailRelationSectionProps = {
loading: boolean;
};
-const StyledAddDropdown = styled(Dropdown)`
- margin-left: auto;
-`;
-
export const RecordDetailRelationSection = ({
loading,
}: RecordDetailRelationSectionProps) => {
const { t } = useLingui();
+
const { recordId, fieldDefinition } = useContext(FieldContext);
+
const {
fieldName,
relationFieldMetadataId,
relationObjectMetadataNameSingular,
relationType,
} = fieldDefinition.metadata as FieldRelationMetadata;
- const record = useRecoilValue(recordStoreFamilyState(recordId));
const isMobile = useIsMobile();
const { objectMetadataItem: relationObjectMetadataItem } =
@@ -86,69 +67,7 @@ export const RecordDetailRelationSection = ({
const dropdownId = `record-field-card-relation-picker-${fieldDefinition.fieldMetadataId}-${recordId}`;
- const { closeDropdown, isDropdownOpen, dropdownPlacement } =
- useDropdown(dropdownId);
-
- const setMultipleRecordPickerSearchFilter = useSetRecoilComponentStateV2(
- multipleRecordPickerSearchFilterComponentState,
- dropdownId,
- );
-
- const setMultipleRecordPickerPickableMorphItems =
- useSetRecoilComponentStateV2(
- multipleRecordPickerPickableMorphItemsComponentState,
- dropdownId,
- );
-
- const setMultipleRecordPickerSearchableObjectMetadataItems =
- useSetRecoilComponentStateV2(
- multipleRecordPickerSearchableObjectMetadataItemsComponentState,
- dropdownId,
- );
-
- const { performSearch: multipleRecordPickerPerformSearch } =
- useMultipleRecordPickerPerformSearch();
-
- const setSingleRecordPickerSearchFilter = useSetRecoilComponentStateV2(
- singleRecordPickerSearchFilterComponentState,
- dropdownId,
- );
-
- const setSingleRecordPickerSelectedId = useSetRecoilComponentStateV2(
- singleRecordPickerSelectedIdComponentState,
- dropdownId,
- );
-
- const handleCloseRelationPickerDropdown = useCallback(() => {
- setMultipleRecordPickerSearchFilter('');
- }, [setMultipleRecordPickerSearchFilter]);
-
- const persistField = usePersistField();
- const { updateOneRecord: updateOneRelationRecord } = useUpdateOneRecord({
- objectNameSingular: relationObjectMetadataNameSingular,
- });
-
- const handleRelationPickerEntitySelected = (
- selectedRelationEntity?: SingleRecordPickerRecord,
- ) => {
- closeDropdown();
-
- if (!selectedRelationEntity?.id || !relationFieldMetadataItem?.name) return;
-
- if (isToOneObject) {
- persistField(selectedRelationEntity.record);
- return;
- }
-
- updateOneRelationRecord({
- idToUpdate: selectedRelationEntity.id,
- updateOneRecordInput: {
- [relationFieldMetadataItem.name]: record,
- },
- });
- };
-
- const { updateRelation } = useUpdateRelationFromManyFieldInput();
+ const { isDropdownOpen } = useDropdown(dropdownId);
const indexViewId = useRecoilValue(
prefetchIndexViewIdFromObjectMetadataItemFamilySelector({
@@ -175,20 +94,24 @@ export const RecordDetailRelationSection = ({
filterQueryParams,
);
- const showContent = () => {
- return (
- relationRecords.length > 0 && (
-
- )
- );
- };
+ const filtersForAggregate = isToManyObjects
+ ? ({
+ [`${relationFieldMetadataItem?.name}Id`]: {
+ in: [recordId],
+ },
+ } satisfies RecordGqlOperationFilter)
+ : {};
- const { createNewRecordAndOpenRightDrawer } =
- useAddNewRecordAndOpenRightDrawer({
- relationObjectMetadataNameSingular,
- relationObjectMetadataItem,
- relationFieldMetadataItem,
- recordId,
+ const { data: relationAggregateResult, loading: aggregateLoading } =
+ useAggregateRecords<{
+ id: { COUNT: number };
+ }>({
+ objectNameSingular: relationObjectMetadataItem.nameSingular,
+ filter: filtersForAggregate,
+ skip: !isToManyObjects,
+ recordGqlFieldsAggregate: {
+ id: [AGGREGATE_OPERATIONS.count],
+ },
});
const isRecordReadOnly = useIsRecordReadOnly({
@@ -200,45 +123,9 @@ export const RecordDetailRelationSection = ({
isRecordReadOnly,
});
- if (loading) return null;
+ if (loading || aggregateLoading || isFieldReadOnly) return null;
- const relationRecordsCount = relationRecords.length;
-
- const handleOpenRelationPickerDropdown = () => {
- if (isToOneObject) {
- setSingleRecordPickerSearchFilter('');
- if (relationRecords.length > 0) {
- setSingleRecordPickerSelectedId(relationRecords[0].id);
- }
- }
-
- if (isToManyObjects) {
- setMultipleRecordPickerSearchableObjectMetadataItems([
- relationObjectMetadataItem,
- ]);
- setMultipleRecordPickerSearchFilter('');
- setMultipleRecordPickerPickableMorphItems(
- relationRecords.map((record) => ({
- recordId: record.id,
- objectMetadataId: relationObjectMetadataItem.id,
- isSelected: true,
- isMatchingSearchFilter: true,
- })),
- );
-
- multipleRecordPickerPerformSearch({
- multipleRecordPickerInstanceId: dropdownId,
- forceSearchFilter: '',
- forceSearchableObjectMetadataItems: [relationObjectMetadataItem],
- forcePickableMorphItems: relationRecords.map((record) => ({
- recordId: record.id,
- objectMetadataId: relationObjectMetadataItem.id,
- isSelected: true,
- isMatchingSearchFilter: true,
- })),
- });
- }
- };
+ const relationRecordsCount = relationAggregateResult?.id?.COUNT ?? 0;
return (
@@ -258,61 +145,12 @@ export const RecordDetailRelationSection = ({
hideRightAdornmentOnMouseLeave={!isDropdownOpen && !isMobile}
areRecordsAvailable={relationRecords.length > 0}
rightAdornment={
- !isFieldReadOnly && (
-
-
- }
- dropdownHotkeyScope={{ scope: dropdownId }}
- dropdownComponents={
- isToOneObject ? (
-
- ) : (
- {
- closeDropdown();
- createNewRecordAndOpenRightDrawer?.();
- }}
- onChange={updateRelation}
- onSubmit={closeDropdown}
- onClickOutside={closeDropdown}
- layoutDirection={
- dropdownPlacement?.includes('end')
- ? 'search-bar-on-bottom'
- : 'search-bar-on-top'
- }
- />
- )
- }
- />
-
- )
+
}
/>
- {showContent()}
+ {relationRecords.length > 0 && (
+
+ )}
);
};
diff --git a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSectionDropdown.tsx b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSectionDropdown.tsx
new file mode 100644
index 000000000..f2f6511c7
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSectionDropdown.tsx
@@ -0,0 +1,249 @@
+import styled from '@emotion/styled';
+import { useCallback, useContext } from 'react';
+import { useRecoilValue } from 'recoil';
+
+import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
+import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
+import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
+import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
+import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/useIsRecordReadOnly';
+import { usePersistField } from '@/object-record/record-field/hooks/usePersistField';
+import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/record-field/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer';
+import { useUpdateRelationFromManyFieldInput } from '@/object-record/record-field/meta-types/input/hooks/useUpdateRelationFromManyFieldInput';
+import { FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata';
+import { MultipleRecordPicker } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPicker';
+import { useMultipleRecordPickerPerformSearch } from '@/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerPerformSearch';
+import { multipleRecordPickerPickableMorphItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPickableMorphItemsComponentState';
+import { multipleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchFilterComponentState';
+import { multipleRecordPickerSearchableObjectMetadataItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchableObjectMetadataItemsComponentState';
+import { SingleRecordPicker } from '@/object-record/record-picker/single-record-picker/components/SingleRecordPicker';
+import { singleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSearchFilterComponentState';
+import { singleRecordPickerSelectedIdComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSelectedIdComponentState';
+import { SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord';
+import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
+import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
+import { ObjectRecord } from '@/object-record/types/ObjectRecord';
+import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
+import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
+import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
+import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
+import { IconForbid, IconPencil, IconPlus } from 'twenty-ui/display';
+import { LightIconButton } from 'twenty-ui/input';
+import { RelationDefinitionType } from '~/generated-metadata/graphql';
+
+type RecordDetailRelationSectionDropdownProps = {
+ loading: boolean;
+};
+
+const StyledAddDropdown = styled(Dropdown)`
+ margin-left: auto;
+`;
+
+export const RecordDetailRelationSectionDropdown = ({
+ loading,
+}: RecordDetailRelationSectionDropdownProps) => {
+ const { recordId, fieldDefinition } = useContext(FieldContext);
+ const {
+ fieldName,
+ relationFieldMetadataId,
+ relationObjectMetadataNameSingular,
+ relationType,
+ } = fieldDefinition.metadata as FieldRelationMetadata;
+
+ const record = useRecoilValue(recordStoreFamilyState(recordId));
+
+ const { objectMetadataItem: relationObjectMetadataItem } =
+ useObjectMetadataItem({
+ objectNameSingular: relationObjectMetadataNameSingular,
+ });
+
+ const relationFieldMetadataItem = relationObjectMetadataItem.fields.find(
+ ({ id }) => id === relationFieldMetadataId,
+ );
+
+ const fieldValue = useRecoilValue<
+ ({ id: string } & Record) | ObjectRecord[] | null
+ >(recordStoreFamilySelector({ recordId, fieldName }));
+
+ // TODO: use new relation type
+ const isToOneObject = relationType === RelationDefinitionType.MANY_TO_ONE;
+ const isToManyObjects = relationType === RelationDefinitionType.ONE_TO_MANY;
+
+ const relationRecords: ObjectRecord[] =
+ fieldValue && isToOneObject
+ ? [fieldValue as ObjectRecord]
+ : ((fieldValue as ObjectRecord[]) ?? []);
+
+ const dropdownId = `record-field-card-relation-picker-${fieldDefinition.fieldMetadataId}-${recordId}`;
+
+ const { closeDropdown, dropdownPlacement } = useDropdown(dropdownId);
+
+ const setMultipleRecordPickerSearchFilter = useSetRecoilComponentStateV2(
+ multipleRecordPickerSearchFilterComponentState,
+ dropdownId,
+ );
+
+ const setMultipleRecordPickerPickableMorphItems =
+ useSetRecoilComponentStateV2(
+ multipleRecordPickerPickableMorphItemsComponentState,
+ dropdownId,
+ );
+
+ const setMultipleRecordPickerSearchableObjectMetadataItems =
+ useSetRecoilComponentStateV2(
+ multipleRecordPickerSearchableObjectMetadataItemsComponentState,
+ dropdownId,
+ );
+
+ const { performSearch: multipleRecordPickerPerformSearch } =
+ useMultipleRecordPickerPerformSearch();
+
+ const setSingleRecordPickerSearchFilter = useSetRecoilComponentStateV2(
+ singleRecordPickerSearchFilterComponentState,
+ dropdownId,
+ );
+
+ const setSingleRecordPickerSelectedId = useSetRecoilComponentStateV2(
+ singleRecordPickerSelectedIdComponentState,
+ dropdownId,
+ );
+
+ const handleCloseRelationPickerDropdown = useCallback(() => {
+ setMultipleRecordPickerSearchFilter('');
+ }, [setMultipleRecordPickerSearchFilter]);
+
+ const persistField = usePersistField();
+ const { updateOneRecord: updateOneRelationRecord } = useUpdateOneRecord({
+ objectNameSingular: relationObjectMetadataNameSingular,
+ });
+
+ const handleRelationPickerEntitySelected = (
+ selectedRelationEntity?: SingleRecordPickerRecord,
+ ) => {
+ closeDropdown();
+
+ if (!selectedRelationEntity?.id || !relationFieldMetadataItem?.name) return;
+
+ if (isToOneObject) {
+ persistField(selectedRelationEntity.record);
+ return;
+ }
+
+ updateOneRelationRecord({
+ idToUpdate: selectedRelationEntity.id,
+ updateOneRecordInput: {
+ [relationFieldMetadataItem.name]: record,
+ },
+ });
+ };
+
+ const { updateRelation } = useUpdateRelationFromManyFieldInput();
+
+ const { createNewRecordAndOpenRightDrawer } =
+ useAddNewRecordAndOpenRightDrawer({
+ relationObjectMetadataNameSingular,
+ relationObjectMetadataItem,
+ relationFieldMetadataItem,
+ recordId,
+ });
+
+ const isRecordReadOnly = useIsRecordReadOnly({
+ recordId,
+ });
+
+ const isFieldReadOnly = useIsFieldValueReadOnly({
+ fieldDefinition,
+ isRecordReadOnly,
+ });
+
+ if (loading || isFieldReadOnly) return null;
+
+ const handleOpenRelationPickerDropdown = () => {
+ if (isToOneObject) {
+ setSingleRecordPickerSearchFilter('');
+ if (relationRecords.length > 0) {
+ setSingleRecordPickerSelectedId(relationRecords[0].id);
+ }
+ }
+
+ if (isToManyObjects) {
+ setMultipleRecordPickerSearchableObjectMetadataItems([
+ relationObjectMetadataItem,
+ ]);
+ setMultipleRecordPickerSearchFilter('');
+ setMultipleRecordPickerPickableMorphItems(
+ relationRecords.map((record) => ({
+ recordId: record.id,
+ objectMetadataId: relationObjectMetadataItem.id,
+ isSelected: true,
+ isMatchingSearchFilter: true,
+ })),
+ );
+
+ multipleRecordPickerPerformSearch({
+ multipleRecordPickerInstanceId: dropdownId,
+ forceSearchFilter: '',
+ forceSearchableObjectMetadataItems: [relationObjectMetadataItem],
+ forcePickableMorphItems: relationRecords.map((record) => ({
+ recordId: record.id,
+ objectMetadataId: relationObjectMetadataItem.id,
+ isSelected: true,
+ isMatchingSearchFilter: true,
+ })),
+ });
+ }
+ };
+
+ return (
+
+
+ }
+ dropdownHotkeyScope={{ scope: dropdownId }}
+ dropdownComponents={
+ isToOneObject ? (
+
+ ) : (
+ {
+ closeDropdown();
+ createNewRecordAndOpenRightDrawer?.();
+ }}
+ onChange={updateRelation}
+ onSubmit={closeDropdown}
+ onClickOutside={closeDropdown}
+ layoutDirection={
+ dropdownPlacement?.includes('end')
+ ? 'search-bar-on-bottom'
+ : 'search-bar-on-top'
+ }
+ />
+ )
+ }
+ />
+
+ );
+};