Refactor SingleRecordPicker to handle morphs (#14956)
## Scope - Refactor SingleRecordPicker to handle morphItem and match Multiple one - fix query generation to take morph into account Left: - double check SingleRecordPicker in Workflows / Form - Fix tests / stories
This commit is contained in:
+2
-8
@@ -6,7 +6,7 @@ import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadat
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
|
||||
import { searchRecordStoreComponentFamilyState } from '@/object-record/record-picker/multiple-record-picker/states/searchRecordStoreComponentFamilyState';
|
||||
import { searchRecordStoreFamilyState } from '@/object-record/record-picker/multiple-record-picker/states/searchRecordStoreComponentFamilyState';
|
||||
import { type RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { useRecoilCallback, useSetRecoilState } from 'recoil';
|
||||
@@ -58,7 +58,6 @@ export const useUpdateActivityTargetFromCell = ({
|
||||
async ({
|
||||
morphItem,
|
||||
activityTargetWithTargetRecords,
|
||||
recordPickerInstanceId,
|
||||
}: UpdateActivityTargetFromCellProps) => {
|
||||
const targetObjectName =
|
||||
activityObjectNameSingular === CoreObjectNameSingular.Task
|
||||
@@ -105,12 +104,7 @@ export const useUpdateActivityTargetFromCell = ({
|
||||
}
|
||||
} else {
|
||||
const searchRecord = snapshot
|
||||
.getLoadable(
|
||||
searchRecordStoreComponentFamilyState.atomFamily({
|
||||
instanceId: recordPickerInstanceId,
|
||||
familyKey: morphItem.recordId,
|
||||
}),
|
||||
)
|
||||
.getLoadable(searchRecordStoreFamilyState(morphItem.recordId))
|
||||
.getValue();
|
||||
|
||||
if (!isDefined(searchRecord) || !isDefined(searchRecord?.record)) {
|
||||
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
import { getAvatarType } from '@/object-metadata/utils/getAvatarType';
|
||||
import { getBasePathToShowPage } from '@/object-metadata/utils/getBasePathToShowPage';
|
||||
import { type SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord';
|
||||
import { type SearchRecord } from '~/generated/graphql';
|
||||
|
||||
export const formatSearchRecordAsSingleRecordPickerRecord = (
|
||||
searchRecord: SearchRecord,
|
||||
): SingleRecordPickerRecord => {
|
||||
return {
|
||||
id: searchRecord.recordId,
|
||||
name: searchRecord.label,
|
||||
avatarUrl: searchRecord.imageUrl ?? undefined,
|
||||
avatarType: getAvatarType(searchRecord.objectNameSingular),
|
||||
linkToShowPage:
|
||||
getBasePathToShowPage({
|
||||
objectNameSingular: searchRecord.objectNameSingular,
|
||||
}) + searchRecord.recordId,
|
||||
record: {
|
||||
id: searchRecord.recordId,
|
||||
__typename: searchRecord.objectNameSingular,
|
||||
},
|
||||
};
|
||||
};
|
||||
+2
-1
@@ -4,6 +4,7 @@ import gql from 'graphql-tag';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
|
||||
import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord';
|
||||
import { type RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields';
|
||||
import { type RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { type ObjectPermissions } from 'twenty-shared/types';
|
||||
@@ -21,7 +22,7 @@ export const updateRecordFromCache = <T extends ObjectRecord>({
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
cache: ApolloCache<object>;
|
||||
recordGqlFields: Record<string, boolean>;
|
||||
recordGqlFields: RecordGqlFields;
|
||||
record: T;
|
||||
objectPermissionsByObjectMetadataId: Record<
|
||||
string,
|
||||
|
||||
+3
-2
@@ -1,3 +1,4 @@
|
||||
import { type RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields';
|
||||
import {
|
||||
type GenerateDepthOneRecordGqlFields,
|
||||
generateDepthOneRecordGqlFields,
|
||||
@@ -11,13 +12,13 @@ type ComputeDepthOneRecordGqlFieldsFromRecordArgs =
|
||||
export const computeDepthOneRecordGqlFieldsFromRecord = ({
|
||||
objectMetadataItem,
|
||||
record,
|
||||
}: ComputeDepthOneRecordGqlFieldsFromRecordArgs) => {
|
||||
}: ComputeDepthOneRecordGqlFieldsFromRecordArgs): RecordGqlFields => {
|
||||
const depthOneRecordGqlFields = generateDepthOneRecordGqlFields({
|
||||
objectMetadataItem,
|
||||
});
|
||||
const recordKeys = Object.keys(record);
|
||||
|
||||
return Object.keys(depthOneRecordGqlFields).reduce<Record<string, boolean>>(
|
||||
return Object.keys(depthOneRecordGqlFields).reduce<RecordGqlFields>(
|
||||
(acc, key) => {
|
||||
return {
|
||||
...acc,
|
||||
|
||||
+62
-24
@@ -1,6 +1,8 @@
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields';
|
||||
import { FieldMetadataType, RelationType } from 'twenty-shared/types';
|
||||
import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export type GenerateDepthOneRecordGqlFields = {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
@@ -8,28 +10,64 @@ export type GenerateDepthOneRecordGqlFields = {
|
||||
|
||||
export const generateDepthOneRecordGqlFields = ({
|
||||
objectMetadataItem,
|
||||
}: GenerateDepthOneRecordGqlFields) =>
|
||||
objectMetadataItem.readableFields.reduce<Record<string, true>>(
|
||||
(acc, field) => {
|
||||
return {
|
||||
...acc,
|
||||
...(isDefined(field.settings?.joinColumnName)
|
||||
? {
|
||||
[field.settings.joinColumnName]: true,
|
||||
}
|
||||
: {}),
|
||||
[field.name]:
|
||||
// TODO: Remove once we have made the workflows lighter
|
||||
(objectMetadataItem.nameSingular ===
|
||||
CoreObjectNameSingular.Workflow ||
|
||||
objectMetadataItem.nameSingular ===
|
||||
CoreObjectNameSingular.WorkflowVersion ||
|
||||
objectMetadataItem.nameSingular ===
|
||||
CoreObjectNameSingular.WorkflowRun) &&
|
||||
(field.name === 'versions' || field.name === 'runs')
|
||||
? { id: true, name: true }
|
||||
: true,
|
||||
};
|
||||
}: GenerateDepthOneRecordGqlFields) => {
|
||||
const gqlFields = objectMetadataItem.readableFields.flatMap(
|
||||
(fieldMetadata) => {
|
||||
const isManyToOneMorphRelation =
|
||||
fieldMetadata.type === FieldMetadataType.MORPH_RELATION &&
|
||||
fieldMetadata.settings.relationType === RelationType.MANY_TO_ONE;
|
||||
|
||||
const isManyToOneRelation =
|
||||
fieldMetadata.type === FieldMetadataType.RELATION &&
|
||||
fieldMetadata.settings.relationType === RelationType.MANY_TO_ONE;
|
||||
|
||||
if (isManyToOneRelation) {
|
||||
return [`${fieldMetadata.name}Id`, `${fieldMetadata.name}`];
|
||||
}
|
||||
|
||||
if (isManyToOneMorphRelation) {
|
||||
if (!isDefined(fieldMetadata.morphRelations)) {
|
||||
throw new Error(
|
||||
`Field ${fieldMetadata.name} is missing, please refresh the page. If the problem persists, please contact support.`,
|
||||
);
|
||||
}
|
||||
|
||||
const morphGqlFields = fieldMetadata.morphRelations.map(
|
||||
(morphRelation) => ({
|
||||
gqlField: computeMorphRelationFieldName({
|
||||
fieldName: fieldMetadata.name,
|
||||
relationType: morphRelation.type,
|
||||
targetObjectMetadataNameSingular:
|
||||
morphRelation.targetObjectMetadata.nameSingular,
|
||||
targetObjectMetadataNamePlural:
|
||||
morphRelation.targetObjectMetadata.namePlural,
|
||||
}),
|
||||
fieldMetadata,
|
||||
}),
|
||||
);
|
||||
return morphGqlFields.flatMap((morphGqlField) => [
|
||||
morphGqlField.gqlField,
|
||||
`${morphGqlField.gqlField}Id`,
|
||||
]);
|
||||
}
|
||||
|
||||
return [fieldMetadata.name];
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
return gqlFields.reduce<RecordGqlFields>((acc, field) => {
|
||||
return {
|
||||
...acc,
|
||||
[field]:
|
||||
// TODO: Remove once we have made the workflows lighter
|
||||
(objectMetadataItem.nameSingular === CoreObjectNameSingular.Workflow ||
|
||||
objectMetadataItem.nameSingular ===
|
||||
CoreObjectNameSingular.WorkflowVersion ||
|
||||
objectMetadataItem.nameSingular ===
|
||||
CoreObjectNameSingular.WorkflowRun) &&
|
||||
(field === 'versions' || field === 'runs')
|
||||
? { id: true, name: true }
|
||||
: true,
|
||||
};
|
||||
}, {});
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
type ObjectRecordFilterInput,
|
||||
type SearchQuery,
|
||||
useSearchQuery,
|
||||
} from '~/generated/graphql';
|
||||
import { logError } from '~/utils/logError';
|
||||
@@ -18,6 +19,7 @@ export type UseSearchRecordsParams = {
|
||||
objectNameSingulars: string[];
|
||||
limit?: number;
|
||||
onError?: (error?: Error) => void;
|
||||
onCompleted?: (data: SearchQuery) => void;
|
||||
skip?: boolean;
|
||||
fetchPolicy?: WatchQueryFetchPolicy;
|
||||
searchInput?: string;
|
||||
@@ -28,6 +30,7 @@ export const useObjectRecordSearchRecords = ({
|
||||
objectNameSingulars,
|
||||
searchInput,
|
||||
limit,
|
||||
onCompleted,
|
||||
skip,
|
||||
filter,
|
||||
fetchPolicy,
|
||||
@@ -49,6 +52,7 @@ export const useObjectRecordSearchRecords = ({
|
||||
},
|
||||
fetchPolicy: fetchPolicy,
|
||||
client: apolloCoreClient,
|
||||
onCompleted: onCompleted,
|
||||
onError: (error) => {
|
||||
logError(
|
||||
`useSearchRecords for "${objectNameSingulars.join(', ')}" error : ` +
|
||||
|
||||
@@ -7,6 +7,7 @@ import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename
|
||||
import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache';
|
||||
import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord';
|
||||
import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
|
||||
import { type RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields';
|
||||
import { computeDepthOneRecordGqlFieldsFromRecord } from '@/object-record/graphql/utils/computeDepthOneRecordGqlFieldsFromRecord';
|
||||
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
|
||||
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
|
||||
@@ -24,7 +25,7 @@ type UpdateOneRecordArgs<UpdatedObjectRecord> = {
|
||||
updateOneRecordInput: Partial<Omit<UpdatedObjectRecord, 'id'>>;
|
||||
optimisticRecord?: Partial<ObjectRecord>;
|
||||
objectNameSingular: string;
|
||||
recordGqlFields?: Record<string, any>;
|
||||
recordGqlFields?: RecordGqlFields;
|
||||
};
|
||||
export const useUpdateOneRecordV2 = () => {
|
||||
const apolloCoreClient = useApolloCoreClient();
|
||||
|
||||
+6
-5
@@ -11,7 +11,7 @@ import { SingleRecordPicker } from '@/object-record/record-picker/single-record-
|
||||
import { useSingleRecordPickerOpen } from '@/object-record/record-picker/single-record-picker/hooks/useSingleRecordPickerOpen';
|
||||
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 { type SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord';
|
||||
import { type RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
|
||||
import { getRecordFieldCardRelationPickerDropdownId } from '@/object-record/record-show/utils/getRecordFieldCardRelationPickerDropdownId';
|
||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
@@ -77,13 +77,14 @@ export const RecordDetailRelationSectionDropdownToOne = () => {
|
||||
const { onSubmit } = useContext(FieldInputEventContext);
|
||||
|
||||
const handleRelationPickerEntitySelected = (
|
||||
selectedRelationEntity?: SingleRecordPickerRecord,
|
||||
selectedMorphItem?: RecordPickerPickableMorphItem,
|
||||
) => {
|
||||
closeDropdown(dropdownId);
|
||||
|
||||
if (!selectedRelationEntity?.id || !relationFieldMetadataItem?.name) return;
|
||||
if (!selectedMorphItem?.recordId || !relationFieldMetadataItem?.name)
|
||||
return;
|
||||
|
||||
onSubmit?.({ newValue: selectedRelationEntity.record });
|
||||
onSubmit?.({ newValue: selectedMorphItem.recordId });
|
||||
};
|
||||
|
||||
const { createNewRecordAndOpenRightDrawer } =
|
||||
@@ -133,7 +134,7 @@ export const RecordDetailRelationSectionDropdownToOne = () => {
|
||||
focusId={dropdownId}
|
||||
componentInstanceId={dropdownId}
|
||||
EmptyIcon={IconForbid}
|
||||
onRecordSelected={handleRelationPickerEntitySelected}
|
||||
onMorphItemSelected={handleRelationPickerEntitySelected}
|
||||
objectNameSingulars={[relationObjectMetadataNameSingular]}
|
||||
recordPickerInstanceId={dropdownId}
|
||||
onCancel={() => closeDropdown(dropdownId)}
|
||||
|
||||
+6
-9
@@ -7,7 +7,7 @@ import { type VariablePickerComponent } from '@/object-record/record-field/ui/fo
|
||||
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 { type SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord';
|
||||
import { type RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
|
||||
import { InputLabel } from '@/ui/input/components/InputLabel';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
|
||||
@@ -122,19 +122,16 @@ export const FormSingleRecordPicker = ({
|
||||
setRecordPickerSearchFilter('');
|
||||
}, [setRecordPickerSearchFilter]);
|
||||
|
||||
const handleRecordSelected = (
|
||||
selectedEntity: SingleRecordPickerRecord | null | undefined,
|
||||
const handleMorphItemSelected = (
|
||||
selectedMorphItem: RecordPickerPickableMorphItem | null | undefined,
|
||||
) => {
|
||||
if (
|
||||
!isDefined(selectedEntity?.record?.id) ||
|
||||
!isNonEmptyString(selectedEntity.record?.id)
|
||||
) {
|
||||
if (!isNonEmptyString(selectedMorphItem?.recordId)) {
|
||||
onChange(null);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
onChange(selectedEntity.record.id);
|
||||
onChange(selectedMorphItem.recordId);
|
||||
closeDropdown(dropdownId);
|
||||
};
|
||||
|
||||
@@ -216,7 +213,7 @@ export const FormSingleRecordPicker = ({
|
||||
EmptyIcon={IconForbid}
|
||||
emptyLabel={'No ' + objectNameSingulars.join(' or ')}
|
||||
onCancel={() => closeDropdown(dropdownId)}
|
||||
onRecordSelected={handleRecordSelected}
|
||||
onMorphItemSelected={handleMorphItemSelected}
|
||||
objectNameSingulars={objectNameSingulars}
|
||||
recordPickerInstanceId={dropdownId}
|
||||
dropdownWidth={GenericDropdownContentWidth.ExtraLarge}
|
||||
|
||||
+36
-10
@@ -1,33 +1,59 @@
|
||||
import { FieldInputEventContext } from '@/object-record/record-field/ui/contexts/FieldInputEventContext';
|
||||
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
|
||||
import { useMorphPersistManyToOne } from '@/object-record/record-field/ui/meta-types/input/hooks/useMorphPersistManyToOne';
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext';
|
||||
import { recordFieldInputLayoutDirectionComponentState } from '@/object-record/record-field/ui/states/recordFieldInputLayoutDirectionComponentState';
|
||||
import { recordFieldInputLayoutDirectionLoadingComponentState } from '@/object-record/record-field/ui/states/recordFieldInputLayoutDirectionLoadingComponentState';
|
||||
import { isFieldMorphRelationManyToOne } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelationManyToOne';
|
||||
import { SingleRecordPicker } from '@/object-record/record-picker/single-record-picker/components/SingleRecordPicker';
|
||||
import { type SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord';
|
||||
import { useRecordTableBodyContextOrThrow } from '@/object-record/record-table/contexts/RecordTableBodyContext';
|
||||
import { type RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useContext } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconForbid } from 'twenty-ui/display';
|
||||
|
||||
export const MorphRelationManyToOneFieldInput = () => {
|
||||
const { fieldDefinition } = useContext(FieldContext);
|
||||
const { fieldDefinition, recordId } = useContext(FieldContext);
|
||||
|
||||
const { onCancel, onSubmit } = useContext(FieldInputEventContext);
|
||||
const { onCloseTableCell } = useRecordTableBodyContextOrThrow();
|
||||
const { onCancel } = useContext(FieldInputEventContext);
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const instanceId = useAvailableComponentInstanceIdOrThrow(
|
||||
RecordFieldComponentInstanceContext,
|
||||
);
|
||||
|
||||
const handleRecordSelected = (
|
||||
selectedRecord: SingleRecordPickerRecord | null | undefined,
|
||||
const { persistMorphManyToOne } = useMorphPersistManyToOne({
|
||||
objectMetadataNameSingular:
|
||||
fieldDefinition.metadata.objectMetadataNameSingular ?? '',
|
||||
});
|
||||
|
||||
const handleMorphItemSelected = async (
|
||||
selectedMorphItem: RecordPickerPickableMorphItem | null | undefined,
|
||||
) => {
|
||||
onSubmit?.({ newValue: selectedRecord?.record ?? null });
|
||||
onCloseTableCell();
|
||||
if (!isDefined(selectedMorphItem)) {
|
||||
// Handle detach
|
||||
return;
|
||||
}
|
||||
|
||||
const targetObjectMetadataItem = objectMetadataItems.find(
|
||||
(objectMetadataItem) =>
|
||||
objectMetadataItem.id === selectedMorphItem.objectMetadataId,
|
||||
);
|
||||
|
||||
if (!isDefined(targetObjectMetadataItem)) {
|
||||
throw new Error('TargetObjectMetadataItem is required');
|
||||
}
|
||||
|
||||
await persistMorphManyToOne({
|
||||
recordId: recordId,
|
||||
fieldDefinition,
|
||||
valueToPersist: selectedMorphItem.recordId,
|
||||
targetObjectMetadataNameSingular: targetObjectMetadataItem.nameSingular,
|
||||
});
|
||||
onCancel?.();
|
||||
};
|
||||
|
||||
const layoutDirection = useRecoilComponentValue(
|
||||
@@ -56,7 +82,7 @@ export const MorphRelationManyToOneFieldInput = () => {
|
||||
EmptyIcon={IconForbid}
|
||||
emptyLabel={'No ' + fieldDefinition.label}
|
||||
onCancel={onCancel}
|
||||
onRecordSelected={handleRecordSelected}
|
||||
onMorphItemSelected={handleMorphItemSelected}
|
||||
objectNameSingulars={objectNameSingulars}
|
||||
recordPickerInstanceId={instanceId}
|
||||
layoutDirection={
|
||||
|
||||
+10
-5
@@ -8,7 +8,7 @@ import { recordFieldInputLayoutDirectionComponentState } from '@/object-record/r
|
||||
import { recordFieldInputLayoutDirectionLoadingComponentState } from '@/object-record/record-field/ui/states/recordFieldInputLayoutDirectionLoadingComponentState';
|
||||
import { SingleRecordPicker } from '@/object-record/record-picker/single-record-picker/components/SingleRecordPicker';
|
||||
import { singleRecordPickerSelectedIdComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSelectedIdComponentState';
|
||||
import { type SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord';
|
||||
import { type RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
@@ -28,9 +28,14 @@ export const RelationManyToOneFieldInput = () => {
|
||||
RecordFieldComponentInstanceContext,
|
||||
);
|
||||
|
||||
const handleRecordSelected = (
|
||||
selectedRecord: SingleRecordPickerRecord | null | undefined,
|
||||
) => onSubmit?.({ newValue: selectedRecord?.record ?? null });
|
||||
const handleMorphItemSelected = (
|
||||
selectedMorphItem: RecordPickerPickableMorphItem | null | undefined,
|
||||
) =>
|
||||
onSubmit?.({
|
||||
newValue: isDefined(selectedMorphItem)
|
||||
? { id: selectedMorphItem.recordId }
|
||||
: null,
|
||||
});
|
||||
|
||||
const { objectMetadataItem: relationObjectMetadataItem } =
|
||||
useObjectMetadataItem({
|
||||
@@ -85,7 +90,7 @@ export const RelationManyToOneFieldInput = () => {
|
||||
emptyLabel={t`No ${fieldLabel}`}
|
||||
onCancel={onCancel}
|
||||
onCreate={handleCreateNew}
|
||||
onRecordSelected={handleRecordSelected}
|
||||
onMorphItemSelected={handleMorphItemSelected}
|
||||
objectNameSingulars={[
|
||||
fieldDefinition.metadata.relationObjectMetadataNameSingular,
|
||||
]}
|
||||
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { type FieldDefinition } from '@/object-record/record-field/ui/types/FieldDefinition';
|
||||
import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import { assertFieldMetadata } from '@/object-record/record-field/ui/types/guards/assertFieldMetadata';
|
||||
import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export type MorphPersistManyToOneProps = {
|
||||
objectMetadataNameSingular: string;
|
||||
};
|
||||
|
||||
export const useMorphPersistManyToOne = ({
|
||||
objectMetadataNameSingular,
|
||||
}: MorphPersistManyToOneProps) => {
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const { updateOneRecord } = useUpdateOneRecord({
|
||||
objectNameSingular: objectMetadataNameSingular,
|
||||
});
|
||||
|
||||
const persistMorphManyToOne = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
async ({
|
||||
recordId,
|
||||
fieldDefinition,
|
||||
valueToPersist,
|
||||
targetObjectMetadataNameSingular,
|
||||
}: {
|
||||
recordId: string;
|
||||
fieldDefinition: FieldDefinition<FieldMetadata>;
|
||||
valueToPersist: string | null | undefined;
|
||||
targetObjectMetadataNameSingular: string;
|
||||
}) => {
|
||||
assertFieldMetadata(
|
||||
FieldMetadataType.MORPH_RELATION,
|
||||
isFieldMorphRelation,
|
||||
fieldDefinition,
|
||||
);
|
||||
const targetObjectMetadataItem = objectMetadataItems.find(
|
||||
(objectMetadataItem) =>
|
||||
objectMetadataItem.nameSingular ===
|
||||
targetObjectMetadataNameSingular,
|
||||
);
|
||||
|
||||
if (!isDefined(targetObjectMetadataItem)) {
|
||||
throw new Error('Object metadata item not found');
|
||||
}
|
||||
|
||||
const fieldName = fieldDefinition.metadata.fieldName;
|
||||
|
||||
if (!isDefined(valueToPersist)) {
|
||||
// Handle detach
|
||||
return;
|
||||
}
|
||||
|
||||
const computedFieldName = computeMorphRelationFieldName({
|
||||
fieldName,
|
||||
relationType: fieldDefinition.metadata.relationType,
|
||||
targetObjectMetadataNameSingular:
|
||||
targetObjectMetadataItem.nameSingular,
|
||||
targetObjectMetadataNamePlural: targetObjectMetadataItem.namePlural,
|
||||
});
|
||||
|
||||
const currentValue: unknown = snapshot
|
||||
.getLoadable(
|
||||
recordStoreFamilySelector({
|
||||
recordId,
|
||||
fieldName: computedFieldName,
|
||||
}),
|
||||
)
|
||||
.getValue();
|
||||
|
||||
if (
|
||||
isDefined(currentValue) &&
|
||||
(currentValue as ObjectRecord).id === valueToPersist
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateOneRecord?.({
|
||||
idToUpdate: recordId,
|
||||
updateOneRecordInput: {
|
||||
[`${computedFieldName}Id`]: valueToPersist,
|
||||
},
|
||||
});
|
||||
set(
|
||||
recordStoreFamilySelector({
|
||||
recordId,
|
||||
fieldName: computedFieldName,
|
||||
}),
|
||||
valueToPersist,
|
||||
);
|
||||
return;
|
||||
},
|
||||
[updateOneRecord, objectMetadataItems],
|
||||
);
|
||||
|
||||
return { persistMorphManyToOne };
|
||||
};
|
||||
+3
-5
@@ -1,7 +1,8 @@
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { searchRecordStoreComponentFamilyState } from '@/object-record/record-picker/multiple-record-picker/states/searchRecordStoreComponentFamilyState';
|
||||
import { searchRecordStoreFamilyState } from '@/object-record/record-picker/multiple-record-picker/states/searchRecordStoreComponentFamilyState';
|
||||
import { multipleRecordPickerSinglePickableMorphItemComponentFamilySelector } from '@/object-record/record-picker/multiple-record-picker/states/selectors/multipleRecordPickerSinglePickableMorphItemComponentFamilySelector';
|
||||
import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type UseRecordPickerGetRecordAndObjectMetadataItemFromRecordIdProps = {
|
||||
@@ -19,10 +20,7 @@ export const useRecordPickerGetSearchRecordAndObjectMetadataItemFromRecordId =
|
||||
recordId,
|
||||
);
|
||||
|
||||
const searchRecord = useRecoilComponentFamilyValue(
|
||||
searchRecordStoreComponentFamilyState,
|
||||
recordId,
|
||||
);
|
||||
const searchRecord = useRecoilValue(searchRecordStoreFamilyState(recordId));
|
||||
|
||||
if (!isDefined(pickableMorphItem) || !isDefined(searchRecord)) {
|
||||
return { searchRecord: null, objectMetadataItem: null };
|
||||
|
||||
+6
-15
@@ -8,7 +8,7 @@ import { multipleRecordPickerPaginationState } from '@/object-record/record-pick
|
||||
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 { searchRecordStoreComponentFamilyState } from '@/object-record/record-picker/multiple-record-picker/states/searchRecordStoreComponentFamilyState';
|
||||
import { searchRecordStoreFamilyState } from '@/object-record/record-picker/multiple-record-picker/states/searchRecordStoreComponentFamilyState';
|
||||
import { sortMorphItems } from '@/object-record/record-picker/multiple-record-picker/utils/sortMorphItems';
|
||||
import { type RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
|
||||
import { getObjectPermissionsFromMapByObjectMetadataId } from '@/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId';
|
||||
@@ -280,10 +280,7 @@ export const useMultipleRecordPickerPerformSearch = () => {
|
||||
|
||||
searchRecords.forEach((searchRecord) => {
|
||||
set(
|
||||
searchRecordStoreComponentFamilyState.atomFamily({
|
||||
instanceId: multipleRecordPickerInstanceId,
|
||||
familyKey: searchRecord.recordId,
|
||||
}),
|
||||
searchRecordStoreFamilyState(searchRecord.recordId),
|
||||
searchRecord,
|
||||
);
|
||||
});
|
||||
@@ -346,16 +343,10 @@ export const useMultipleRecordPickerPerformSearch = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
set(
|
||||
searchRecordStoreComponentFamilyState.atomFamily({
|
||||
instanceId: multipleRecordPickerInstanceId,
|
||||
familyKey: objectRecord.id,
|
||||
}),
|
||||
{
|
||||
...searchRecord,
|
||||
record: objectRecord,
|
||||
},
|
||||
);
|
||||
set(searchRecordStoreFamilyState(objectRecord.id), {
|
||||
...searchRecord,
|
||||
record: objectRecord,
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
+3
-5
@@ -1,13 +1,11 @@
|
||||
import { MultipleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/multiple-record-picker/states/contexts/MultipleRecordPickerComponentInstanceContext';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { createComponentFamilyState } from '@/ui/utilities/state/component-state/utils/createComponentFamilyState';
|
||||
import { createFamilyState } from '@/ui/utilities/state/utils/createFamilyState';
|
||||
import { type SearchRecord } from '~/generated-metadata/graphql';
|
||||
|
||||
export const searchRecordStoreComponentFamilyState = createComponentFamilyState<
|
||||
export const searchRecordStoreFamilyState = createFamilyState<
|
||||
(SearchRecord & { record?: ObjectRecord }) | undefined,
|
||||
string
|
||||
>({
|
||||
key: 'searchRecordStoreComponentFamilyState',
|
||||
key: 'searchRecordStoreFamilyState',
|
||||
defaultValue: undefined,
|
||||
componentInstanceContext: MultipleRecordPickerComponentInstanceContext,
|
||||
});
|
||||
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
import { type SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord';
|
||||
|
||||
export type MultipleRecordPickerRecords<
|
||||
CustomRecordForRecordPicker extends SingleRecordPickerRecord,
|
||||
> = {
|
||||
selectedRecords: CustomRecordForRecordPicker[];
|
||||
filteredSelectedRecords: CustomRecordForRecordPicker[];
|
||||
recordsToSelect: CustomRecordForRecordPicker[];
|
||||
loading: boolean;
|
||||
};
|
||||
+6
-6
@@ -6,7 +6,7 @@ import {
|
||||
} from '@/object-record/record-picker/single-record-picker/components/SingleRecordPickerMenuItemsWithSearch';
|
||||
import { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext';
|
||||
import { singleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSearchFilterComponentState';
|
||||
import { type SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord';
|
||||
import { type RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
|
||||
@@ -24,7 +24,7 @@ export const SingleRecordPicker = ({
|
||||
excludedRecordIds,
|
||||
onCancel,
|
||||
onCreate,
|
||||
onRecordSelected,
|
||||
onMorphItemSelected,
|
||||
objectNameSingulars,
|
||||
componentInstanceId,
|
||||
layoutDirection,
|
||||
@@ -44,12 +44,12 @@ export const SingleRecordPicker = ({
|
||||
onCancel?.();
|
||||
};
|
||||
|
||||
const handleRecordSelected = (
|
||||
selectedRecord?: SingleRecordPickerRecord | undefined,
|
||||
const handleMorphItemSelected = (
|
||||
selectedMorphItem?: RecordPickerPickableMorphItem | undefined,
|
||||
) => {
|
||||
setRecordPickerSearchFilter('');
|
||||
|
||||
onRecordSelected?.(selectedRecord);
|
||||
onMorphItemSelected?.(selectedMorphItem);
|
||||
};
|
||||
|
||||
useListenClickOutside({
|
||||
@@ -83,7 +83,7 @@ export const SingleRecordPicker = ({
|
||||
excludedRecordIds,
|
||||
onCancel: handleCancel,
|
||||
onCreate,
|
||||
onRecordSelected: handleRecordSelected,
|
||||
onMorphItemSelected: handleMorphItemSelected,
|
||||
objectNameSingulars,
|
||||
layoutDirection,
|
||||
}}
|
||||
|
||||
+32
-20
@@ -1,19 +1,23 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { getAvatarType } from '@/object-metadata/utils/getAvatarType';
|
||||
import { searchRecordStoreFamilyState } from '@/object-record/record-picker/multiple-record-picker/states/searchRecordStoreComponentFamilyState';
|
||||
import { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext';
|
||||
import { type SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord';
|
||||
import { getSingleRecordPickerSelectableListId } from '@/object-record/record-picker/single-record-picker/utils/getSingleRecordPickerSelectableListId';
|
||||
import { type RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { isSelectedItemIdComponentFamilySelector } from '@/ui/layout/selectable-list/states/selectors/isSelectedItemIdComponentFamilySelector';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Avatar } from 'twenty-ui/display';
|
||||
import { MenuItemSelectAvatar } from 'twenty-ui/navigation';
|
||||
|
||||
type SingleRecordPickerMenuItemProps = {
|
||||
record: SingleRecordPickerRecord;
|
||||
onRecordSelected: (recordSelected?: SingleRecordPickerRecord) => void;
|
||||
selectedRecord?: SingleRecordPickerRecord;
|
||||
morphItem: RecordPickerPickableMorphItem;
|
||||
onMorphItemSelected: (morphItem?: RecordPickerPickableMorphItem) => void;
|
||||
isRecordSelected: boolean;
|
||||
};
|
||||
|
||||
const StyledSelectableItem = styled(SelectableListItem)`
|
||||
@@ -21,9 +25,9 @@ const StyledSelectableItem = styled(SelectableListItem)`
|
||||
`;
|
||||
|
||||
export const SingleRecordPickerMenuItem = ({
|
||||
record,
|
||||
onRecordSelected,
|
||||
selectedRecord,
|
||||
morphItem,
|
||||
onMorphItemSelected,
|
||||
isRecordSelected,
|
||||
}: SingleRecordPickerMenuItemProps) => {
|
||||
const recordPickerComponentInstanceId =
|
||||
useAvailableComponentInstanceIdOrThrow(
|
||||
@@ -33,33 +37,41 @@ export const SingleRecordPickerMenuItem = ({
|
||||
const selectableListComponentInstanceId =
|
||||
getSingleRecordPickerSelectableListId(recordPickerComponentInstanceId);
|
||||
|
||||
const isSelectedItemId = useRecoilComponentFamilyValue(
|
||||
const isSelectedByKeyboard = useRecoilComponentFamilyValue(
|
||||
isSelectedItemIdComponentFamilySelector,
|
||||
record.id,
|
||||
morphItem.recordId,
|
||||
selectableListComponentInstanceId,
|
||||
);
|
||||
|
||||
const searchRecord = useRecoilValue(
|
||||
searchRecordStoreFamilyState(morphItem.recordId),
|
||||
);
|
||||
|
||||
if (!isDefined(searchRecord)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledSelectableItem
|
||||
itemId={record.id}
|
||||
key={record.id}
|
||||
itemId={morphItem.recordId}
|
||||
key={morphItem.recordId}
|
||||
onEnter={() => {
|
||||
onRecordSelected(record);
|
||||
onMorphItemSelected(morphItem);
|
||||
}}
|
||||
>
|
||||
<MenuItemSelectAvatar
|
||||
testId="menu-item"
|
||||
onClick={() => onRecordSelected(record)}
|
||||
text={record.name}
|
||||
selected={selectedRecord?.id === record.id}
|
||||
focused={isSelectedItemId}
|
||||
onClick={() => onMorphItemSelected(morphItem)}
|
||||
text={searchRecord.label}
|
||||
selected={isRecordSelected}
|
||||
focused={isSelectedByKeyboard}
|
||||
avatar={
|
||||
<Avatar
|
||||
avatarUrl={record.avatarUrl}
|
||||
placeholderColorSeed={record.id}
|
||||
placeholder={record.name}
|
||||
avatarUrl={searchRecord.imageUrl}
|
||||
placeholderColorSeed={morphItem.recordId}
|
||||
placeholder={searchRecord.label}
|
||||
size="md"
|
||||
type={record.avatarType ?? 'rounded'}
|
||||
type={getAvatarType(searchRecord.objectNameSingular) ?? 'rounded'}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
+21
-26
@@ -1,4 +1,4 @@
|
||||
import { isNonEmptyString, isUndefined } from '@sniptt/guards';
|
||||
import { isUndefined } from '@sniptt/guards';
|
||||
import { Key } from 'ts-key-enum';
|
||||
|
||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||
@@ -12,8 +12,8 @@ import { SingleRecordPickerComponentInstanceContext } from '@/object-record/reco
|
||||
import { singleRecordPickerSelectedIdComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSelectedIdComponentState';
|
||||
import { singleRecordPickerShouldShowInitialLoadingComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerShouldShowInitialLoadingComponentState';
|
||||
import { singleRecordPickerShouldShowSkeletonComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerShouldShowSkeletonComponentState';
|
||||
import { type SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord';
|
||||
import { getSingleRecordPickerSelectableListId } from '@/object-record/record-picker/single-record-picker/utils/getSingleRecordPickerSelectableListId';
|
||||
import { type RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { isSelectedItemIdComponentFamilySelector } from '@/ui/layout/selectable-list/states/selectors/isSelectedItemIdComponentFamilySelector';
|
||||
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
|
||||
@@ -21,36 +21,26 @@ import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/com
|
||||
import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue';
|
||||
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type IconComponent } from 'twenty-ui/display';
|
||||
import { MenuItemSelect } from 'twenty-ui/navigation';
|
||||
|
||||
export type SingleRecordPickerMenuItemsProps = {
|
||||
EmptyIcon?: IconComponent;
|
||||
emptyLabel?: string;
|
||||
recordsToSelect: SingleRecordPickerRecord[];
|
||||
pickableMorphItems: RecordPickerPickableMorphItem[];
|
||||
onCancel?: () => void;
|
||||
onRecordSelected: (entity?: SingleRecordPickerRecord) => void;
|
||||
selectedRecord?: SingleRecordPickerRecord;
|
||||
onMorphItemSelected: (morphItem?: RecordPickerPickableMorphItem) => void;
|
||||
focusId: string;
|
||||
filteredSelectedRecords: SingleRecordPickerRecord[];
|
||||
};
|
||||
|
||||
export const SingleRecordPickerMenuItems = ({
|
||||
EmptyIcon,
|
||||
emptyLabel,
|
||||
recordsToSelect,
|
||||
pickableMorphItems,
|
||||
onCancel,
|
||||
onRecordSelected,
|
||||
filteredSelectedRecords,
|
||||
selectedRecord,
|
||||
onMorphItemSelected,
|
||||
focusId,
|
||||
}: SingleRecordPickerMenuItemsProps) => {
|
||||
const recordsInDropdown = [selectedRecord, ...recordsToSelect].filter(
|
||||
(entity): entity is SingleRecordPickerRecord =>
|
||||
isDefined(entity) && isNonEmptyString(entity.name),
|
||||
);
|
||||
|
||||
const recordPickerComponentInstanceId =
|
||||
useAvailableComponentInstanceIdOrThrow(
|
||||
SingleRecordPickerComponentInstanceContext,
|
||||
@@ -79,7 +69,9 @@ export const SingleRecordPickerMenuItems = ({
|
||||
dependencies: [onCancel, resetSelectedItem],
|
||||
});
|
||||
|
||||
const selectableItemIds = recordsInDropdown.map((entity) => entity.id);
|
||||
const selectableItemIds = pickableMorphItems.map(
|
||||
(morphItem) => morphItem.recordId,
|
||||
);
|
||||
const [selectedRecordId, setSelectedRecordId] = useRecoilComponentState(
|
||||
singleRecordPickerSelectedIdComponentState,
|
||||
);
|
||||
@@ -92,8 +84,11 @@ export const SingleRecordPickerMenuItems = ({
|
||||
singleRecordPickerShouldShowInitialLoadingComponentState,
|
||||
);
|
||||
|
||||
const searchHasNoResults =
|
||||
recordsToSelect.length === 0 && filteredSelectedRecords?.length === 0;
|
||||
const itemsMatchingSearchFilter = pickableMorphItems.filter(
|
||||
(morphItem) => morphItem.isMatchingSearchFilter,
|
||||
);
|
||||
|
||||
const searchHasNoResults = itemsMatchingSearchFilter.length === 0;
|
||||
|
||||
return (
|
||||
<SelectableList
|
||||
@@ -107,13 +102,13 @@ export const SingleRecordPickerMenuItems = ({
|
||||
itemId="select-none"
|
||||
onEnter={() => {
|
||||
setSelectedRecordId(undefined);
|
||||
onRecordSelected();
|
||||
onMorphItemSelected();
|
||||
}}
|
||||
>
|
||||
<MenuItemSelect
|
||||
onClick={() => {
|
||||
setSelectedRecordId(undefined);
|
||||
onRecordSelected();
|
||||
onMorphItemSelected();
|
||||
}}
|
||||
LeftIcon={EmptyIcon}
|
||||
text={emptyLabel}
|
||||
@@ -127,12 +122,12 @@ export const SingleRecordPickerMenuItems = ({
|
||||
) : singleRecordPickerShouldShowSkeleton ? (
|
||||
<RecordPickerLoadingSkeletonList />
|
||||
) : (
|
||||
recordsInDropdown?.map((record) => (
|
||||
itemsMatchingSearchFilter.map((morphItem) => (
|
||||
<SingleRecordPickerMenuItem
|
||||
key={record.id}
|
||||
record={record}
|
||||
onRecordSelected={onRecordSelected}
|
||||
selectedRecord={selectedRecord}
|
||||
key={morphItem.recordId}
|
||||
morphItem={morphItem}
|
||||
onMorphItemSelected={onMorphItemSelected}
|
||||
isRecordSelected={selectedRecordId === morphItem.recordId}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
|
||||
+8
-16
@@ -28,11 +28,7 @@ export type SingleRecordPickerMenuItemsWithSearchProps = {
|
||||
focusId: string;
|
||||
} & Pick<
|
||||
SingleRecordPickerMenuItemsProps,
|
||||
| 'EmptyIcon'
|
||||
| 'emptyLabel'
|
||||
| 'onCancel'
|
||||
| 'onRecordSelected'
|
||||
| 'selectedRecord'
|
||||
'EmptyIcon' | 'emptyLabel' | 'onCancel' | 'onMorphItemSelected'
|
||||
>;
|
||||
|
||||
export const SingleRecordPickerMenuItemsWithSearch = ({
|
||||
@@ -41,7 +37,7 @@ export const SingleRecordPickerMenuItemsWithSearch = ({
|
||||
excludedRecordIds,
|
||||
onCancel,
|
||||
onCreate,
|
||||
onRecordSelected,
|
||||
onMorphItemSelected,
|
||||
objectNameSingulars,
|
||||
layoutDirection = 'search-bar-on-top',
|
||||
focusId,
|
||||
@@ -57,7 +53,7 @@ export const SingleRecordPickerMenuItemsWithSearch = ({
|
||||
recordPickerInstanceId,
|
||||
);
|
||||
|
||||
const { records } = useSingleRecordPickerRecords({
|
||||
const { pickableMorphItems, loading } = useSingleRecordPickerRecords({
|
||||
objectNameSingulars,
|
||||
excludedRecordIds,
|
||||
});
|
||||
@@ -83,7 +79,7 @@ export const SingleRecordPickerMenuItemsWithSearch = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<SingleRecordPickerLoadingEffect loading={records.loading} />
|
||||
<SingleRecordPickerLoadingEffect loading={loading} />
|
||||
{layoutDirection === 'search-bar-on-bottom' && (
|
||||
<>
|
||||
{isDefined(onCreate) && hasUpdatePermissions && (
|
||||
@@ -102,14 +98,12 @@ export const SingleRecordPickerMenuItemsWithSearch = ({
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
<SingleRecordPickerMenuItems
|
||||
focusId={focusId}
|
||||
recordsToSelect={records.recordsToSelect}
|
||||
selectedRecord={records.selectedRecords?.[0]}
|
||||
filteredSelectedRecords={records.filteredSelectedRecords}
|
||||
pickableMorphItems={pickableMorphItems}
|
||||
onMorphItemSelected={onMorphItemSelected}
|
||||
{...{
|
||||
EmptyIcon,
|
||||
emptyLabel,
|
||||
onCancel,
|
||||
onRecordSelected,
|
||||
}}
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
@@ -127,14 +121,12 @@ export const SingleRecordPickerMenuItemsWithSearch = ({
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
<SingleRecordPickerMenuItems
|
||||
focusId={focusId}
|
||||
recordsToSelect={records.recordsToSelect}
|
||||
selectedRecord={records.selectedRecords?.[0]}
|
||||
filteredSelectedRecords={records.filteredSelectedRecords}
|
||||
pickableMorphItems={pickableMorphItems}
|
||||
onMorphItemSelected={onMorphItemSelected}
|
||||
{...{
|
||||
EmptyIcon,
|
||||
emptyLabel,
|
||||
onCancel,
|
||||
onRecordSelected,
|
||||
}}
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
|
||||
+20
-23
@@ -5,24 +5,31 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
|
||||
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
|
||||
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
import { allMockPersonRecords } from '~/testing/mock-data/people';
|
||||
import { sleep } from '~/utils/sleep';
|
||||
|
||||
import { SingleRecordPicker } from '@/object-record/record-picker/single-record-picker/components/SingleRecordPicker';
|
||||
import { type SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord';
|
||||
import { IconUserCircle } from 'twenty-ui/display';
|
||||
import { ComponentDecorator } from 'twenty-ui/testing';
|
||||
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
||||
|
||||
const records = allMockPersonRecords.map<SingleRecordPickerRecord>(
|
||||
(person) => ({
|
||||
id: person.id,
|
||||
name: person.name.firstName + ' ' + person.name.lastName,
|
||||
avatarUrl: 'https://picsum.photos/200',
|
||||
avatarType: 'rounded',
|
||||
record: { ...person, __typename: 'Person' },
|
||||
}),
|
||||
);
|
||||
// const records = allMockPersonRecords.map<SearchRecord>((person) => ({
|
||||
// id: person.id,
|
||||
// label: person.name.firstName + ' ' + person.name.lastName,
|
||||
// imageUrl: 'https://picsum.photos/200',
|
||||
// objectNameSingular: 'Person',
|
||||
// recordId: person.id,
|
||||
// tsRank: 0,
|
||||
// tsRankCD: 0,
|
||||
// }));
|
||||
|
||||
// const pickableMorphItems = records.map<RecordPickerPickableMorphItem>(
|
||||
// (record) => ({
|
||||
// recordId: record.recordId,
|
||||
// objectMetadataId: record.objectNameSingular,
|
||||
// isSelected: false,
|
||||
// isMatchingSearchFilter: true,
|
||||
// }),
|
||||
// );
|
||||
|
||||
const meta: Meta<typeof SingleRecordPicker> = {
|
||||
title: 'UI/RecordPicker/SingleRecordPicker',
|
||||
@@ -37,15 +44,7 @@ const meta: Meta<typeof SingleRecordPicker> = {
|
||||
objectNameSingulars: [CoreObjectNameSingular.WorkspaceMember],
|
||||
componentInstanceId: 'single-record-picker',
|
||||
},
|
||||
argTypes: {
|
||||
selectedRecord: {
|
||||
options: records.map(({ name }) => name),
|
||||
mapping: records.reduce(
|
||||
(result, entity) => ({ ...result, [entity.name]: entity }),
|
||||
{},
|
||||
),
|
||||
},
|
||||
},
|
||||
argTypes: {},
|
||||
parameters: {
|
||||
msw: graphqlMocks,
|
||||
},
|
||||
@@ -56,9 +55,7 @@ type Story = StoryObj<typeof SingleRecordPicker>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const WithSelectedRecord: Story = {
|
||||
args: { selectedRecord: records[2] },
|
||||
};
|
||||
export const WithSelectedRecord: Story = {};
|
||||
|
||||
export const WithEmptyOption: Story = {
|
||||
args: {
|
||||
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { DEFAULT_SEARCH_REQUEST_LIMIT } from '@/object-record/constants/DefaultSearchRequestLimit';
|
||||
import { useObjectRecordSearchRecords } from '@/object-record/hooks/useObjectRecordSearchRecords';
|
||||
import { searchRecordStoreFamilyState } from '@/object-record/record-picker/multiple-record-picker/states/searchRecordStoreComponentFamilyState';
|
||||
import { type RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { CustomError, isDefined } from 'twenty-shared/utils';
|
||||
import { type SearchQuery } from '~/generated/graphql';
|
||||
|
||||
export const useSingleRecordPickerPerformSearch = ({
|
||||
selectedIds,
|
||||
limit,
|
||||
excludedRecordIds = [],
|
||||
objectNameSingulars,
|
||||
searchFilter,
|
||||
}: {
|
||||
selectedIds: string[];
|
||||
limit?: number;
|
||||
excludedRecordIds?: string[];
|
||||
objectNameSingulars: string[];
|
||||
searchFilter?: string;
|
||||
}): {
|
||||
pickableMorphItems: RecordPickerPickableMorphItem[];
|
||||
loading: boolean;
|
||||
} => {
|
||||
const onSearchRecordsCompleted = useRecoilCallback(
|
||||
({ set }) =>
|
||||
(data: SearchQuery) => {
|
||||
const searchRecords = data.search.edges.map((edge) => edge.node);
|
||||
|
||||
searchRecords.forEach((searchRecord) => {
|
||||
set(searchRecordStoreFamilyState(searchRecord.recordId), {
|
||||
...searchRecord,
|
||||
record: undefined,
|
||||
});
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
const selectedIdsFilter = { id: { in: selectedIds } };
|
||||
|
||||
const { loading: selectedRecordsLoading, searchRecords: selectedRecords } =
|
||||
useObjectRecordSearchRecords({
|
||||
objectNameSingulars,
|
||||
filter: selectedIdsFilter,
|
||||
skip: !selectedIds.length,
|
||||
searchInput: '',
|
||||
onCompleted: onSearchRecordsCompleted,
|
||||
});
|
||||
|
||||
const {
|
||||
loading: filteredSelectedRecordsLoading,
|
||||
searchRecords: filteredSelectedRecords,
|
||||
} = useObjectRecordSearchRecords({
|
||||
objectNameSingulars,
|
||||
filter: selectedIdsFilter,
|
||||
skip: !selectedIds.length,
|
||||
searchInput: searchFilter,
|
||||
onCompleted: onSearchRecordsCompleted,
|
||||
});
|
||||
|
||||
const notFilterIds = [...selectedIds, ...excludedRecordIds];
|
||||
const notFilter = notFilterIds.length
|
||||
? { not: { id: { in: notFilterIds } } }
|
||||
: undefined;
|
||||
const { loading: recordsToSelectLoading, searchRecords: recordsToSelect } =
|
||||
useObjectRecordSearchRecords({
|
||||
objectNameSingulars,
|
||||
filter: notFilter,
|
||||
limit: limit ?? DEFAULT_SEARCH_REQUEST_LIMIT,
|
||||
searchInput: searchFilter,
|
||||
fetchPolicy: 'cache-and-network',
|
||||
onCompleted: onSearchRecordsCompleted,
|
||||
});
|
||||
|
||||
const pickableMorphItems = [...selectedRecords, ...recordsToSelect].map(
|
||||
(record) => {
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(objectMetadataItem) =>
|
||||
objectMetadataItem.nameSingular === record.objectNameSingular,
|
||||
);
|
||||
|
||||
if (!isDefined(objectMetadataItem)) {
|
||||
throw new CustomError(
|
||||
'Object metadata item not found for object name singular: ' +
|
||||
record.objectNameSingular,
|
||||
'OBJECT_METADATA_ITEM_NOT_FOUND',
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
recordId: record.recordId,
|
||||
objectMetadataId: objectMetadataItem.id,
|
||||
isSelected: selectedRecords.some(
|
||||
(selectedRecord) => selectedRecord.recordId === record.recordId,
|
||||
),
|
||||
isMatchingSearchFilter:
|
||||
recordsToSelect.some(
|
||||
(recordsToSelectRecord) =>
|
||||
recordsToSelectRecord.recordId === record.recordId,
|
||||
) ||
|
||||
filteredSelectedRecords.some(
|
||||
(filteredSelectedRecord) =>
|
||||
filteredSelectedRecord.recordId === record.recordId,
|
||||
),
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
pickableMorphItems,
|
||||
loading:
|
||||
recordsToSelectLoading ||
|
||||
filteredSelectedRecordsLoading ||
|
||||
selectedRecordsLoading,
|
||||
};
|
||||
};
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
import { useSingleRecordPickerPerformSearch } from '@/object-record/record-picker/single-record-picker/hooks/useSingleRecordPickerPerformSearch';
|
||||
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 { useFilteredSearchRecordQuery } from '@/search/hooks/useFilteredSearchRecordQuery';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
|
||||
export const useSingleRecordPickerRecords = ({
|
||||
@@ -17,12 +17,12 @@ export const useSingleRecordPickerRecords = ({
|
||||
const selectedRecordId = useRecoilComponentValue(
|
||||
singleRecordPickerSelectedIdComponentState,
|
||||
);
|
||||
const records = useFilteredSearchRecordQuery({
|
||||
const { pickableMorphItems, loading } = useSingleRecordPickerPerformSearch({
|
||||
searchFilter: recordPickerSearchFilter,
|
||||
selectedIds: selectedRecordId ? [selectedRecordId] : [],
|
||||
excludedRecordIds: excludedRecordIds,
|
||||
objectNameSingulars,
|
||||
});
|
||||
|
||||
return { records };
|
||||
return { pickableMorphItems, loading };
|
||||
};
|
||||
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { type ObjectRecordIdentifier } from '@/object-record/types/ObjectRecordIdentifier';
|
||||
|
||||
export type SingleRecordPickerRecord = ObjectRecordIdentifier & {
|
||||
record: ObjectRecord;
|
||||
};
|
||||
-176
@@ -1,176 +0,0 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const query = gql`
|
||||
query FindManyPeople(
|
||||
$filter: PersonFilterInput
|
||||
$orderBy: [PersonOrderByInput]
|
||||
$lastCursor: String
|
||||
$limit: Int = 60
|
||||
) {
|
||||
people(
|
||||
filter: $filter
|
||||
orderBy: $orderBy
|
||||
first: $limit
|
||||
after: $lastCursor
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
opportunities {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
personId
|
||||
pointOfContactId
|
||||
updatedAt
|
||||
companyId
|
||||
stage
|
||||
closeDate
|
||||
amount {
|
||||
amountMicros
|
||||
currencyCode
|
||||
}
|
||||
id
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}
|
||||
xLink {
|
||||
primaryLinkLabel
|
||||
primaryLinkUrl
|
||||
}
|
||||
id
|
||||
pointOfContactForOpportunities {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
personId
|
||||
pointOfContactId
|
||||
updatedAt
|
||||
companyId
|
||||
stage
|
||||
closeDate
|
||||
amount {
|
||||
amountMicros
|
||||
currencyCode
|
||||
}
|
||||
id
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}
|
||||
createdAt
|
||||
company {
|
||||
id
|
||||
xLink {
|
||||
primaryLinkLabel
|
||||
primaryLinkUrl
|
||||
}
|
||||
linkedinLink {
|
||||
primaryLinkLabel
|
||||
primaryLinkUrl
|
||||
}
|
||||
domainName
|
||||
annualRecurringRevenue {
|
||||
amountMicros
|
||||
currencyCode
|
||||
}
|
||||
createdAt
|
||||
address {
|
||||
adressCity
|
||||
}
|
||||
updatedAt
|
||||
name
|
||||
accountOwnerId
|
||||
employees
|
||||
id
|
||||
idealCustomerProfile
|
||||
}
|
||||
city
|
||||
email
|
||||
jobTitle
|
||||
favorites {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
id
|
||||
companyId
|
||||
createdAt
|
||||
forWorkspaceMemberId
|
||||
personId
|
||||
position
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
attachments {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
updatedAt
|
||||
createdAt
|
||||
name
|
||||
personId
|
||||
companyId
|
||||
id
|
||||
authorId
|
||||
type
|
||||
fullPath
|
||||
}
|
||||
}
|
||||
}
|
||||
name {
|
||||
firstName
|
||||
lastName
|
||||
}
|
||||
phone
|
||||
linkedinLink {
|
||||
primaryLinkLabel
|
||||
primaryLinkUrl
|
||||
}
|
||||
updatedAt
|
||||
avatarUrl
|
||||
companyId
|
||||
}
|
||||
cursor
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
startCursor
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const variables = {
|
||||
recordsToSelect: {
|
||||
limit: 10,
|
||||
filter: {
|
||||
and: [
|
||||
{ and: [{ or: [{ name: { ilike: '%Entity%' } }] }] },
|
||||
{ not: { id: { in: ['1', '2'] } } },
|
||||
],
|
||||
},
|
||||
orderBy: [{ name: 'AscNullsLast' }],
|
||||
},
|
||||
filteredSelectedRecords: {
|
||||
limit: 60,
|
||||
filter: {
|
||||
and: [
|
||||
{ and: [{ or: [{ name: { ilike: '%Entity%' } }] }] },
|
||||
{ id: { in: ['1'] } },
|
||||
],
|
||||
},
|
||||
orderBy: [{ name: 'AscNullsLast' }],
|
||||
},
|
||||
selectedEntities: {
|
||||
limit: 60,
|
||||
filter: { id: { in: ['1'] } },
|
||||
orderBy: [{ name: 'AscNullsLast' }],
|
||||
},
|
||||
};
|
||||
|
||||
export const responseData = {
|
||||
edges: [],
|
||||
};
|
||||
-106
@@ -1,106 +0,0 @@
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { type ReactNode } from 'react';
|
||||
import { RecoilRoot, useSetRecoilState } from 'recoil';
|
||||
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
|
||||
import { type MultipleRecordPickerRecords } from '@/object-record/record-picker/multiple-record-picker/types/MultipleRecordPickerRecords';
|
||||
import { SnackBarComponentInstanceContext } from '@/ui/feedback/snack-bar-manager/contexts/SnackBarComponentInstanceContext';
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
|
||||
import {
|
||||
query,
|
||||
responseData,
|
||||
variables,
|
||||
} from '../__mocks__/useFilteredSearchRecordQuery';
|
||||
import { useFilteredSearchRecordQuery } from '../useFilteredSearchRecordQuery';
|
||||
|
||||
const mocks = [
|
||||
{
|
||||
request: {
|
||||
query,
|
||||
variables: variables.recordsToSelect,
|
||||
},
|
||||
result: jest.fn(() => ({
|
||||
data: {
|
||||
people: responseData,
|
||||
},
|
||||
})),
|
||||
},
|
||||
{
|
||||
request: {
|
||||
query,
|
||||
variables: variables.filteredSelectedRecords,
|
||||
},
|
||||
result: jest.fn(() => ({
|
||||
data: {
|
||||
people: responseData,
|
||||
},
|
||||
})),
|
||||
},
|
||||
{
|
||||
request: {
|
||||
query,
|
||||
variables: variables.selectedEntities,
|
||||
},
|
||||
result: jest.fn(() => ({
|
||||
data: {
|
||||
people: responseData,
|
||||
},
|
||||
})),
|
||||
},
|
||||
];
|
||||
|
||||
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<RecoilRoot>
|
||||
<MockedProvider mocks={mocks} addTypename={false}>
|
||||
<SnackBarComponentInstanceContext.Provider
|
||||
value={{ instanceId: 'snack-bar-manager' }}
|
||||
>
|
||||
{children}
|
||||
</SnackBarComponentInstanceContext.Provider>
|
||||
</MockedProvider>
|
||||
</RecoilRoot>
|
||||
);
|
||||
|
||||
describe('useFilteredSearchRecordQuery', () => {
|
||||
it('returns the correct result when everything is provided', async () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const setCurrentWorkspaceMember = useSetRecoilState(
|
||||
currentWorkspaceMemberState,
|
||||
);
|
||||
setCurrentWorkspaceMember({
|
||||
id: '32219445-f587-4c40-b2b1-6d3205ed96da',
|
||||
name: { firstName: 'John', lastName: 'Connor' },
|
||||
locale: 'en',
|
||||
colorScheme: 'Light',
|
||||
userEmail: 'userEmail',
|
||||
});
|
||||
|
||||
const setMetadataItems = useSetRecoilState(objectMetadataItemsState);
|
||||
|
||||
setMetadataItems(generatedMockObjectMetadataItems);
|
||||
|
||||
return useFilteredSearchRecordQuery({
|
||||
selectedIds: ['1'],
|
||||
limit: 10,
|
||||
excludedRecordIds: ['2'],
|
||||
objectNameSingulars: ['person'],
|
||||
searchFilter: 'Entity',
|
||||
});
|
||||
},
|
||||
{ wrapper: Wrapper },
|
||||
);
|
||||
|
||||
const expectedResult: MultipleRecordPickerRecords<any> = {
|
||||
selectedRecords: [],
|
||||
filteredSelectedRecords: [],
|
||||
recordsToSelect: [],
|
||||
loading: true,
|
||||
};
|
||||
|
||||
expect(result.current).toEqual(expectedResult);
|
||||
});
|
||||
});
|
||||
@@ -1,73 +0,0 @@
|
||||
import { formatSearchRecordAsSingleRecordPickerRecord } from '@/object-metadata/utils/formatSearchRecordAsSingleRecordPickerRecord';
|
||||
import { DEFAULT_SEARCH_REQUEST_LIMIT } from '@/object-record/constants/DefaultSearchRequestLimit';
|
||||
import { useObjectRecordSearchRecords } from '@/object-record/hooks/useObjectRecordSearchRecords';
|
||||
import { type SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useFilteredSearchRecordQuery = ({
|
||||
selectedIds,
|
||||
limit,
|
||||
excludedRecordIds = [],
|
||||
objectNameSingulars,
|
||||
searchFilter,
|
||||
}: {
|
||||
selectedIds: string[];
|
||||
limit?: number;
|
||||
excludedRecordIds?: string[];
|
||||
objectNameSingulars: string[];
|
||||
searchFilter?: string;
|
||||
}): {
|
||||
selectedRecords: SingleRecordPickerRecord[];
|
||||
filteredSelectedRecords: SingleRecordPickerRecord[];
|
||||
recordsToSelect: SingleRecordPickerRecord[];
|
||||
loading: boolean;
|
||||
} => {
|
||||
const selectedIdsFilter = { id: { in: selectedIds } };
|
||||
|
||||
const { loading: selectedRecordsLoading, searchRecords: selectedRecords } =
|
||||
useObjectRecordSearchRecords({
|
||||
objectNameSingulars,
|
||||
filter: selectedIdsFilter,
|
||||
skip: !selectedIds.length,
|
||||
searchInput: '',
|
||||
});
|
||||
|
||||
const {
|
||||
loading: filteredSelectedRecordsLoading,
|
||||
searchRecords: filteredSelectedRecords,
|
||||
} = useObjectRecordSearchRecords({
|
||||
objectNameSingulars,
|
||||
filter: selectedIdsFilter,
|
||||
skip: !selectedIds.length,
|
||||
searchInput: searchFilter,
|
||||
});
|
||||
|
||||
const notFilterIds = [...selectedIds, ...excludedRecordIds];
|
||||
const notFilter = notFilterIds.length
|
||||
? { not: { id: { in: notFilterIds } } }
|
||||
: undefined;
|
||||
const { loading: recordsToSelectLoading, searchRecords: recordsToSelect } =
|
||||
useObjectRecordSearchRecords({
|
||||
objectNameSingulars,
|
||||
filter: notFilter,
|
||||
limit: limit ?? DEFAULT_SEARCH_REQUEST_LIMIT,
|
||||
searchInput: searchFilter,
|
||||
fetchPolicy: 'cache-and-network',
|
||||
});
|
||||
|
||||
return {
|
||||
selectedRecords: selectedRecords
|
||||
.map(formatSearchRecordAsSingleRecordPickerRecord)
|
||||
.filter(isDefined),
|
||||
filteredSelectedRecords: filteredSelectedRecords
|
||||
.map(formatSearchRecordAsSingleRecordPickerRecord)
|
||||
.filter(isDefined),
|
||||
recordsToSelect: recordsToSelect
|
||||
.map(formatSearchRecordAsSingleRecordPickerRecord)
|
||||
.filter(isDefined),
|
||||
loading:
|
||||
recordsToSelectLoading ||
|
||||
filteredSelectedRecordsLoading ||
|
||||
selectedRecordsLoading,
|
||||
};
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
import { type DocumentNode } from 'graphql';
|
||||
|
||||
export type SearchConfigType = {
|
||||
query: DocumentNode;
|
||||
template: (searchInput: string, currentSelectedId?: string) => any;
|
||||
resultMapper: (data: any) => any;
|
||||
};
|
||||
Reference in New Issue
Block a user