Morph-picker-MultiSelect-picker (#14113)

# 🚀 Multi-Select Morph Relations / One to Many

Adds **multi-select picker support** for morph relations on the **One To
Many** side, allowing users to select multiple related records from
different object types.

### 🔑 Main Changes (TL;DR)
- **New Component**: `MorphRelationOneToManyFieldInput` for
multi-selection
- **Enhanced Hooks**: `useAttachMorphRelatedRecordFromRecord` &
`useDetachMorphRelatedRecordFromRecord`
- **Better UX**: Optimistic updates, improved search, loading states
- **24 files changed**: Mostly additions (1,277 lines added, 56 removed)


**Summary**: This adds multi-select capability to morph relations with
proper state management and optimistic updates. The changes are mostly
additive and follow existing patterns.


**TODO** :
- optimistic rendering not working currently
- single select picker (many to one)
This commit is contained in:
Guillim
2025-08-29 10:04:51 +02:00
committed by GitHub
parent ab9cabce04
commit 0e6ce123c9
20 changed files with 1127 additions and 56 deletions
@@ -0,0 +1,17 @@
import { useRecoilValue } from 'recoil';
import { objectMetadataItemsBySingularNameSelector } from '@/object-metadata/states/objectMetadataItemsBySingularNameSelector';
import { isDefined } from 'twenty-shared/utils';
export const useDoObjectMetadataItemsExist = (
objectNameSingulars: string[],
) => {
const objectMetadataItems = useRecoilValue(
objectMetadataItemsBySingularNameSelector(objectNameSingulars),
);
return objectMetadataItems.every((objectMetadataItem) =>
isDefined(objectMetadataItem),
);
};
@@ -0,0 +1,27 @@
import { selectorFamily } from 'recoil';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { isDefined } from 'twenty-shared/utils';
export const objectMetadataItemsBySingularNameSelector = selectorFamily<
ObjectMetadataItem[],
string[]
>({
key: 'objectMetadataItemsSelector',
get:
(objectNameSingulars: string[]) =>
({ get }) => {
const objectMetadataItems = get(objectMetadataItemsState);
return objectNameSingulars
.map(
(objectNameSingular) =>
objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.nameSingular === objectNameSingular,
) ?? null,
)
.filter(isDefined);
},
});
@@ -0,0 +1,193 @@
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache';
import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
import { computeDepthOneRecordGqlFieldsFromRecord } from '@/object-record/graphql/utils/computeDepthOneRecordGqlFieldsFromRecord';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useUpdateMultipleRecordsFromManyObjects } from '@/object-record/hooks/useUpdateMultipleRecordsFromManyObjects';
import { getTargetFieldMetadataName } from '@/object-record/multiple-objects/utils/getTargetFieldMetadataName';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { useContext } from 'react';
import { useRecoilCallback } from 'recoil';
import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils';
export const useAttachMorphRelatedRecordFromRecord = () => {
const apolloCoreClient = useApolloCoreClient();
const { fieldDefinition } = useContext(FieldContext);
const { objectMetadataItems } = useObjectMetadataItems();
const { updateMultipleRecordsFromManyObjects } =
useUpdateMultipleRecordsFromManyObjects();
if (!isFieldMorphRelation(fieldDefinition)) {
throw new Error('Field is not a morph relation');
}
const objectNameSingular =
fieldDefinition.metadata.morphRelations[0].sourceObjectMetadata
.nameSingular;
const objectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.nameSingular === objectNameSingular,
);
if (!isDefined(objectMetadataItem)) {
throw new Error('Could not find object metadata item');
}
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const updateOneRecordAndAttachMorphRelations = useRecoilCallback(
({ set, snapshot }) =>
async ({
recordId,
relatedRecordId,
objectNameSingulars,
}: {
recordId: string;
relatedRecordId: string;
objectNameSingulars: string[];
}) => {
const relatedObjectMetadataItems = objectMetadataItems.filter(
(objectMetadataItem) =>
objectNameSingulars.includes(objectMetadataItem.nameSingular),
);
const parentRecord = snapshot
.getLoadable(recordStoreFamilyState(recordId))
.getValue();
if (isDefined(parentRecord)) {
relatedObjectMetadataItems.forEach((relatedObjectMetadataItem) => {
const computedFieldName = computeMorphRelationFieldName({
fieldName: fieldDefinition.metadata.fieldName,
relationDirection: fieldDefinition.metadata.relationType,
nameSingular: relatedObjectMetadataItem.nameSingular,
namePlural: relatedObjectMetadataItem.namePlural,
});
const currentMorphFieldValue = parentRecord[computedFieldName];
const objectRecordFromCache = getRecordFromCache({
objectMetadataItem: relatedObjectMetadataItem,
recordId: relatedRecordId,
cache: apolloCoreClient.cache,
objectMetadataItems,
objectPermissionsByObjectMetadataId,
});
if (!isDefined(objectRecordFromCache)) {
return;
}
set(recordStoreFamilyState(recordId), {
...parentRecord,
[computedFieldName]: [
...currentMorphFieldValue,
objectRecordFromCache,
],
});
});
}
const relatedObjectMetadataItemsWithCachedRecord =
relatedObjectMetadataItems
.map((objectMetadataItem) => {
const cachedRelatedRecord = getRecordFromCache<ObjectRecord>({
objectMetadataItem,
recordId: relatedRecordId,
cache: apolloCoreClient.cache,
objectMetadataItems,
objectPermissionsByObjectMetadataId,
});
return {
cachedRelatedRecord,
objectMetadataItem,
};
})
.find((item) => isDefined(item.cachedRelatedRecord));
if (!relatedObjectMetadataItemsWithCachedRecord) {
throw new Error('Could not find cached related record');
}
const {
objectMetadataItem: relatedObjectMetadataItem,
cachedRelatedRecord,
} = relatedObjectMetadataItemsWithCachedRecord;
if (
!isDefined(cachedRelatedRecord) ||
!isDefined(cachedRelatedRecord.id)
) {
throw new Error('Could not find cached related record');
}
const fieldOnRelatedObject = getTargetFieldMetadataName({
fieldDefinition,
objectNameSingular: relatedObjectMetadataItem.nameSingular,
});
if (!isDefined(fieldOnRelatedObject)) {
throw new Error('Could not find field on related object');
}
const previousRecordId =
cachedRelatedRecord?.[`${fieldOnRelatedObject}Id`];
if (isDefined(previousRecordId)) {
const previousRecord = getRecordFromCache<ObjectRecord>({
objectMetadataItem,
recordId: previousRecordId,
cache: apolloCoreClient.cache,
objectMetadataItems,
objectPermissionsByObjectMetadataId,
});
const previousRecordWithRelation = {
...cachedRelatedRecord,
[fieldOnRelatedObject]: previousRecord,
};
const gqlFields = computeDepthOneRecordGqlFieldsFromRecord({
objectMetadataItem: relatedObjectMetadataItem,
record: previousRecordWithRelation,
});
updateRecordFromCache({
objectMetadataItems,
objectMetadataItem: relatedObjectMetadataItem,
cache: apolloCoreClient.cache,
record: {
...cachedRelatedRecord,
[fieldOnRelatedObject]: previousRecord,
},
recordGqlFields: gqlFields,
objectPermissionsByObjectMetadataId,
});
}
const updatedManyRecordsArgs = [
{
idToUpdate: relatedRecordId,
objectNameSingulars,
relatedRecordId: recordId,
// recordGqlFields: gqlFields,
},
];
await updateMultipleRecordsFromManyObjects(updatedManyRecordsArgs);
},
[
fieldDefinition,
objectMetadataItems,
objectPermissionsByObjectMetadataId,
updateMultipleRecordsFromManyObjects,
apolloCoreClient.cache,
objectMetadataItem,
],
);
return { updateOneRecordAndAttachMorphRelations };
};
@@ -0,0 +1,176 @@
import { type Reference } from '@apollo/client';
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache';
import { getRefName } from '@/object-record/cache/utils/getRefName';
import { modifyRecordFromCache } from '@/object-record/cache/utils/modifyRecordFromCache';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useUpdateMultipleRecordsFromManyObjects } from '@/object-record/hooks/useUpdateMultipleRecordsFromManyObjects';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { useContext } from 'react';
import { useRecoilCallback } from 'recoil';
import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils';
export const useDetachMorphRelatedRecordFromRecord = () => {
const apolloCoreClient = useApolloCoreClient();
const { fieldDefinition } = useContext(FieldContext);
const { objectMetadataItems } = useObjectMetadataItems();
const { updateMultipleRecordsFromManyObjects } =
useUpdateMultipleRecordsFromManyObjects();
if (!isFieldMorphRelation(fieldDefinition)) {
throw new Error('Field is not a morph relation');
}
const objectNameSingular =
fieldDefinition.metadata.morphRelations[0].sourceObjectMetadata
.nameSingular;
const objectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.nameSingular === objectNameSingular,
);
if (!isDefined(objectMetadataItem)) {
throw new Error('Could not find object metadata item');
}
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const updateOneRecordAndDetachMorphRelations = useRecoilCallback(
({ set, snapshot }) =>
async ({
recordId,
relatedRecordId,
objectNameSingulars,
}: {
recordId: string;
relatedRecordId: string;
objectNameSingulars: string[];
}) => {
const relatedObjectMetadataItems = objectMetadataItems.filter(
(objectMetadataItem) =>
objectNameSingulars.includes(objectMetadataItem.nameSingular),
);
const parentRecord = snapshot
.getLoadable(recordStoreFamilyState(recordId))
.getValue();
if (isDefined(parentRecord)) {
relatedObjectMetadataItems.forEach((relatedObjectMetadataItem) => {
const computedFieldName = computeMorphRelationFieldName({
fieldName: fieldDefinition.metadata.fieldName,
relationDirection: fieldDefinition.metadata.relationType,
nameSingular: relatedObjectMetadataItem.nameSingular,
namePlural: relatedObjectMetadataItem.namePlural,
});
const currentMorphFieldValue = parentRecord[
computedFieldName
] as ObjectRecord[];
const objectRecordFromCache = getRecordFromCache({
objectMetadataItem: relatedObjectMetadataItem,
recordId: relatedRecordId,
cache: apolloCoreClient.cache,
objectMetadataItems,
objectPermissionsByObjectMetadataId,
});
if (!isDefined(objectRecordFromCache)) {
return;
}
if (Array.isArray(currentMorphFieldValue)) {
set(recordStoreFamilyState(recordId), {
...parentRecord,
[computedFieldName]: currentMorphFieldValue.filter(
(record) => record.id !== relatedRecordId,
),
});
}
});
}
const relatedObjectMetadataItemsWithCachedRecord =
relatedObjectMetadataItems
.map((objectMetadataItem) => {
const cachedRelatedRecord = getRecordFromCache<ObjectRecord>({
objectMetadataItem,
recordId: relatedRecordId,
cache: apolloCoreClient.cache,
objectMetadataItems,
objectPermissionsByObjectMetadataId,
});
return {
cachedRelatedRecord,
objectMetadataItem,
};
})
.find((item) => isDefined(item.cachedRelatedRecord));
if (!relatedObjectMetadataItemsWithCachedRecord) {
throw new Error('Could not find cached related record');
}
const { objectMetadataItem: relatedObjectMetadataItem } =
relatedObjectMetadataItemsWithCachedRecord;
modifyRecordFromCache({
objectMetadataItem,
cache: apolloCoreClient.cache,
fieldModifiers: {
[fieldDefinition.metadata.fieldName]: (
fieldNameOnRecordObjectConnection,
{ readField },
) => {
const edges = readField<{ node: Reference }[]>(
'edges',
fieldNameOnRecordObjectConnection,
);
if (!edges) return fieldNameOnRecordObjectConnection;
return {
...fieldNameOnRecordObjectConnection,
edges: edges.filter(
(edge) =>
!(
edge.node.__ref ===
getRefName(
relatedObjectMetadataItem.nameSingular,
relatedRecordId,
)
),
),
};
},
},
recordId,
});
const updatedManyRecordsArgs = [
{
idToUpdate: relatedRecordId,
objectNameSingulars,
relatedRecordId: null,
},
];
await updateMultipleRecordsFromManyObjects(updatedManyRecordsArgs);
},
[
fieldDefinition,
objectMetadataItems,
objectPermissionsByObjectMetadataId,
updateMultipleRecordsFromManyObjects,
apolloCoreClient.cache,
objectMetadataItem,
],
);
return { updateOneRecordAndDetachMorphRelations };
};
@@ -1,8 +1,7 @@
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { MAX_SEARCH_RESULTS } from '@/command-menu/constants/MaxSearchResults';
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { type ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
import { useDoObjectMetadataItemsExist } from '@/object-metadata/hooks/useDoObjectMetadataItemsExist';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { type WatchQueryFetchPolicy } from '@apollo/client';
import { useMemo } from 'react';
@@ -14,7 +13,9 @@ import {
} from '~/generated/graphql';
import { logError } from '~/utils/logError';
export type UseSearchRecordsParams = ObjectMetadataItemIdentifier & {
// maybe we should look at ObjectMetadataItemIdentifier to update the API even though there are many location to update
export type UseSearchRecordsParams = {
objectNameSingulars: string[];
limit?: number;
onError?: (error?: Error) => void;
skip?: boolean;
@@ -24,7 +25,7 @@ export type UseSearchRecordsParams = ObjectMetadataItemIdentifier & {
};
export const useObjectRecordSearchRecords = ({
objectNameSingular,
objectNameSingulars,
searchInput,
limit,
skip,
@@ -32,30 +33,25 @@ export const useObjectRecordSearchRecords = ({
fetchPolicy,
}: UseSearchRecordsParams) => {
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
const areDefined = useDoObjectMetadataItemsExist(objectNameSingulars);
const { enqueueErrorSnackBar } = useSnackBar();
const apolloCoreClient = useApolloCoreClient();
const { data, loading, error, previousData } = useSearchQuery({
skip:
skip ||
!objectMetadataItem ||
!currentWorkspaceMember ||
!isDefined(searchInput),
skip || !areDefined || !currentWorkspaceMember || !isDefined(searchInput),
variables: {
searchInput: searchInput ?? '',
limit: limit ?? MAX_SEARCH_RESULTS,
filter: filter ?? {},
includedObjectNameSingulars: [objectNameSingular],
includedObjectNameSingulars: objectNameSingulars,
},
fetchPolicy: fetchPolicy,
client: apolloCoreClient,
onError: (error) => {
logError(
`useSearchRecords for "${objectMetadataItem.namePlural}" error : ` +
`useSearchRecords for "${objectNameSingulars.join(', ')}" error : ` +
error,
);
enqueueErrorSnackBar({
@@ -72,7 +68,6 @@ export const useObjectRecordSearchRecords = ({
);
return {
objectMetadataItem,
searchRecords,
loading,
error,
@@ -0,0 +1,285 @@
import { triggerUpdateRecordOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { CustomError } from '@/error-handler/CustomError';
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
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 { computeDepthOneRecordGqlFieldsFromRecord } from '@/object-record/graphql/utils/computeDepthOneRecordGqlFieldsFromRecord';
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { generateUpdateOneRecordMutation } from '@/object-record/multiple-objects/utils/generateUpdateOneRecordMutation';
import { getTargetFieldMetadataName } from '@/object-record/multiple-objects/utils/getTargetFieldMetadataName';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation';
import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { computeOptimisticRecordFromInput } from '@/object-record/utils/computeOptimisticRecordFromInput';
import { getAggregateQueryName } from '@/object-record/utils/getAggregateQueryName';
import { getUpdateOneRecordMutationResponseField } from '@/object-record/utils/getUpdateOneRecordMutationResponseField';
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
import { isNull } from '@sniptt/guards';
import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { buildRecordFromKeysWithSameValue } from '~/utils/array/buildRecordFromKeysWithSameValue';
type UpdateManyRecordArgs = {
idToUpdate: string;
relatedRecordId: string | null;
objectNameSingulars: string[];
recordGqlFields?: Record<string, any>;
};
export const useUpdateMultipleRecordsFromManyObjects = () => {
const { fieldDefinition } = useContext(FieldContext);
const apolloCoreClient = useApolloCoreClient();
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const { objectMetadataItems } = useObjectMetadataItems();
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const updateMultipleRecordsFromManyObjects = async (
updatedManyRecordsArgs: UpdateManyRecordArgs[],
) => {
for (const {
idToUpdate,
objectNameSingulars,
relatedRecordId,
recordGqlFields,
} of updatedManyRecordsArgs) {
const objectMetadataItemArray = objectMetadataItems.filter(
(objectMetadataItem) =>
objectNameSingulars.includes(objectMetadataItem.nameSingular),
);
if (objectMetadataItemArray.length === 0) {
throw new CustomError(
`Object metadata item not found ${objectNameSingulars.join(', ')}`,
'OBJECT_METADATA_ITEM_NOT_FOUND',
);
}
const objectMetadataItemWithCachedRecord = objectMetadataItemArray
.map((objectMetadataItem) => {
const cachedRecord = getRecordFromCache({
objectMetadataItem,
recordId: idToUpdate,
cache: apolloCoreClient.cache,
objectMetadataItems,
objectPermissionsByObjectMetadataId,
});
return {
cachedRecord,
objectMetadataItem,
};
})
.find((item) => isDefined(item.cachedRecord));
if (!isDefined(objectMetadataItemWithCachedRecord)) {
throw new CustomError(
`Record not found ${idToUpdate}`,
'RECORD_NOT_FOUND_IN_OBJECT_METADATA_ITEM',
);
}
const { objectMetadataItem, cachedRecord } =
objectMetadataItemWithCachedRecord;
if (
!isFieldRelation(fieldDefinition) &&
!isFieldMorphRelation(fieldDefinition)
) {
throw new CustomError(
`Should never happen`,
'TARGET_FIELD_NAME_NOT_FOUND',
);
}
const targetFieldName = getTargetFieldMetadataName({
fieldDefinition,
objectNameSingular: objectMetadataItem.nameSingular,
});
if (!isDefined(targetFieldName)) {
throw new CustomError(
`Cannot find Target field name for the (morph) relation field ${fieldDefinition.metadata.fieldName} on ${objectMetadataItem.nameSingular}`,
'TARGET_FIELD_NAME_NOT_FOUND',
);
}
const updateOneRecordInput = {
[`${targetFieldName}Id`]: relatedRecordId,
};
const optimisticRecordInput = computeOptimisticRecordFromInput({
objectMetadataItem,
currentWorkspaceMember: currentWorkspaceMember,
recordInput: updateOneRecordInput,
cache: apolloCoreClient.cache,
objectMetadataItems,
objectPermissionsByObjectMetadataId,
});
const computedRecordGqlFields =
recordGqlFields ??
generateDepthOneRecordGqlFields({ objectMetadataItem });
const cachedRecordWithConnection = getRecordNodeFromRecord<ObjectRecord>({
record: cachedRecord,
objectMetadataItem,
objectMetadataItems,
recordGqlFields: computedRecordGqlFields,
computeReferences: false,
});
const computedOptimisticRecord = {
...cachedRecord,
...optimisticRecordInput,
id: idToUpdate,
__typename: getObjectTypename(objectMetadataItem.nameSingular),
};
const optimisticRecordWithConnection =
getRecordNodeFromRecord<ObjectRecord>({
record: computedOptimisticRecord,
objectMetadataItem,
objectMetadataItems,
recordGqlFields: computedRecordGqlFields,
computeReferences: false,
});
const shouldHandleOptimisticCache =
!isNull(cachedRecord) &&
isDefined(optimisticRecordWithConnection) &&
isDefined(cachedRecordWithConnection);
if (shouldHandleOptimisticCache) {
const recordGqlFields = computeDepthOneRecordGqlFieldsFromRecord({
objectMetadataItem,
record: optimisticRecordInput,
});
updateRecordFromCache({
objectMetadataItems,
objectMetadataItem,
cache: apolloCoreClient.cache,
record: computedOptimisticRecord,
recordGqlFields,
objectPermissionsByObjectMetadataId,
});
triggerUpdateRecordOptimisticEffect({
cache: apolloCoreClient.cache,
objectMetadataItem,
currentRecord: cachedRecordWithConnection,
updatedRecord: optimisticRecordWithConnection,
objectMetadataItems,
});
}
const mutationResponseField = getUpdateOneRecordMutationResponseField(
objectMetadataItem.nameSingular,
);
const sanitizedInput = {
...sanitizeRecordInput({
objectMetadataItem,
recordInput: updateOneRecordInput,
}),
};
const updateOneRecordMutation = generateUpdateOneRecordMutation({
objectMetadataItem,
objectMetadataItems,
recordGqlFields: computedRecordGqlFields,
computeReferences: false,
objectPermissionsByObjectMetadataId,
});
const updatedRecord = await apolloCoreClient
.mutate({
mutation: updateOneRecordMutation,
variables: {
idToUpdate,
input: sanitizedInput,
},
update: (cache, { data }) => {
const record = data?.[mutationResponseField];
if (!isDefined(record)) return;
triggerUpdateRecordOptimisticEffect({
cache,
objectMetadataItem,
currentRecord: computedOptimisticRecord,
updatedRecord: record,
objectMetadataItems,
});
},
})
.catch((error: Error) => {
if (!shouldHandleOptimisticCache) {
throw error;
}
const cachedRecordKeys = new Set(Object.keys(cachedRecord));
const recordKeysAddedByOptimisticCache = Object.keys(
optimisticRecordInput,
).filter((diffKey) => !cachedRecordKeys.has(diffKey));
const recordGqlFields = {
...computeDepthOneRecordGqlFieldsFromRecord({
objectMetadataItem,
record: cachedRecord,
}),
...buildRecordFromKeysWithSameValue(
recordKeysAddedByOptimisticCache,
true,
),
};
updateRecordFromCache({
objectMetadataItems,
objectMetadataItem,
cache: apolloCoreClient.cache,
record: {
...cachedRecord,
...buildRecordFromKeysWithSameValue(
recordKeysAddedByOptimisticCache,
null,
),
},
recordGqlFields,
objectPermissionsByObjectMetadataId,
});
triggerUpdateRecordOptimisticEffect({
cache: apolloCoreClient.cache,
objectMetadataItem,
currentRecord: optimisticRecordWithConnection,
updatedRecord: cachedRecordWithConnection,
objectMetadataItems,
});
throw error;
});
const refetchAggregateQueries = async () => {
const queryName = getAggregateQueryName(objectMetadataItem.namePlural);
await apolloCoreClient.refetchQueries({
include: [queryName],
});
};
await refetchAggregateQueries();
return updatedRecord?.data?.[mutationResponseField] ?? null;
}
};
return {
updateMultipleRecordsFromManyObjects,
};
};
@@ -1,15 +1,11 @@
import gql from 'graphql-tag';
import { useRecoilValue } from 'recoil';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
import { EMPTY_MUTATION } from '@/object-record/constants/EmptyMutation';
import { type RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { getUpdateOneRecordMutationResponseField } from '@/object-record/utils/getUpdateOneRecordMutationResponseField';
import { capitalize } from 'twenty-shared/utils';
import { generateUpdateOneRecordMutation } from '@/object-record/multiple-objects/utils/generateUpdateOneRecordMutation';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
export const useUpdateOneRecordMutation = ({
@@ -33,31 +29,13 @@ export const useUpdateOneRecordMutation = ({
return { updateOneRecordMutation: EMPTY_MUTATION };
}
const appliedRecordGqlFields =
recordGqlFields ??
generateDepthOneRecordGqlFields({
objectMetadataItem,
});
const capitalizedObjectName = capitalize(objectMetadataItem.nameSingular);
const mutationResponseField = getUpdateOneRecordMutationResponseField(
objectMetadataItem.nameSingular,
);
const updateOneRecordMutation = gql`
mutation UpdateOne${capitalizedObjectName}($idToUpdate: UUID!, $input: ${capitalizedObjectName}UpdateInput!) {
${mutationResponseField}(id: $idToUpdate, data: $input) ${mapObjectMetadataToGraphQLQuery(
{
objectMetadataItems,
objectMetadataItem,
computeReferences,
recordGqlFields: appliedRecordGqlFields,
objectPermissionsByObjectMetadataId,
},
)}
}
`;
const updateOneRecordMutation = generateUpdateOneRecordMutation({
objectMetadataItem,
objectMetadataItems,
recordGqlFields,
computeReferences,
objectPermissionsByObjectMetadataId,
});
return {
updateOneRecordMutation,
@@ -0,0 +1,53 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
import { type RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
import { getUpdateOneRecordMutationResponseField } from '@/object-record/utils/getUpdateOneRecordMutationResponseField';
import { gql } from '@apollo/client';
import { type ObjectPermissions } from 'twenty-shared/types';
import { capitalize } from 'twenty-shared/utils';
export const generateUpdateOneRecordMutation = ({
objectMetadataItem,
objectMetadataItems,
recordGqlFields,
computeReferences,
objectPermissionsByObjectMetadataId,
}: {
objectMetadataItem: ObjectMetadataItem;
objectMetadataItems: ObjectMetadataItem[];
recordGqlFields?: RecordGqlOperationGqlRecordFields;
computeReferences: boolean;
objectPermissionsByObjectMetadataId: Record<
string,
ObjectPermissions & { objectMetadataId: string }
>;
}) => {
const appliedRecordGqlFields =
recordGqlFields ??
generateDepthOneRecordGqlFields({
objectMetadataItem,
});
const capitalizedObjectName = capitalize(objectMetadataItem.nameSingular);
const mutationResponseField = getUpdateOneRecordMutationResponseField(
objectMetadataItem.nameSingular,
);
const updateOneRecordMutation = gql`
mutation UpdateOne${capitalizedObjectName}($idToUpdate: UUID!, $input: ${capitalizedObjectName}UpdateInput!){
${mutationResponseField}(id: $idToUpdate, data: $input) ${mapObjectMetadataToGraphQLQuery(
{
objectMetadataItems,
objectMetadataItem,
computeReferences,
recordGqlFields: appliedRecordGqlFields,
objectPermissionsByObjectMetadataId,
},
)}
}
`;
return updateOneRecordMutation;
};
@@ -0,0 +1,25 @@
import { type FieldDefinition } from '@/object-record/record-field/ui/types/FieldDefinition';
import {
type FieldMorphRelationMetadata,
type FieldRelationMetadata,
} from '@/object-record/record-field/ui/types/FieldMetadata';
import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation';
export const getTargetFieldMetadataName = ({
fieldDefinition,
objectNameSingular,
}: {
fieldDefinition:
| FieldDefinition<FieldMorphRelationMetadata>
| FieldDefinition<FieldRelationMetadata>;
objectNameSingular: string;
}) => {
if (isFieldRelation(fieldDefinition)) {
return fieldDefinition.metadata.targetFieldMetadataName;
} else {
return fieldDefinition.metadata.morphRelations.find(
(morphRelation) =>
morphRelation.targetObjectMetadata.nameSingular === objectNameSingular,
)?.targetFieldMetadata.name;
}
};
@@ -14,6 +14,7 @@ import { isFieldPhones } from '@/object-record/record-field/ui/types/guards/isFi
import { isFieldRelationFromManyObjects } from '@/object-record/record-field/ui/types/guards/isFieldRelationFromManyObjects';
import { ArrayFieldInput } from '@/object-record/record-field/ui/meta-types/input/components/ArrayFieldInput';
import { MorphRelationOneToManyFieldInput } from '@/object-record/record-field/ui/meta-types/input/components/MorphRelationOneToManyFieldInput';
import { RichTextFieldInput } from '@/object-record/record-field/ui/meta-types/input/components/RichTextFieldInput';
import { isFieldAddress } from '@/object-record/record-field/ui/types/guards/isFieldAddress';
import { isFieldArray } from '@/object-record/record-field/ui/types/guards/isFieldArray';
@@ -24,6 +25,7 @@ import { isFieldDateTime } from '@/object-record/record-field/ui/types/guards/is
import { isFieldEmails } from '@/object-record/record-field/ui/types/guards/isFieldEmails';
import { isFieldFullName } from '@/object-record/record-field/ui/types/guards/isFieldFullName';
import { isFieldLinks } from '@/object-record/record-field/ui/types/guards/isFieldLinks';
import { isFieldMorphRelationOneToMany } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelationOneToMany';
import { isFieldMultiSelect } from '@/object-record/record-field/ui/types/guards/isFieldMultiSelect';
import { isFieldNumber } from '@/object-record/record-field/ui/types/guards/isFieldNumber';
import { isFieldRating } from '@/object-record/record-field/ui/types/guards/isFieldRating';
@@ -50,6 +52,8 @@ export const FieldInput = () => {
<RelationToOneFieldInput />
) : isFieldRelationFromManyObjects(fieldDefinition) ? (
<RelationFromManyFieldInput />
) : isFieldMorphRelationOneToMany(fieldDefinition) ? (
<MorphRelationOneToManyFieldInput />
) : isFieldPhones(fieldDefinition) ? (
<PhonesFieldInput />
) : isFieldText(fieldDefinition) ? (
@@ -5,6 +5,7 @@ import { type Task } from '@/activities/types/Task';
import { type TaskTarget } from '@/activities/types/TaskTarget';
import { getActivityTargetObjectRecords } from '@/activities/utils/getActivityTargetObjectRecords';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { useOpenMorphRelationFromManyFieldInput } from '@/object-record/record-field/ui/meta-types/input/hooks/useOpenMorphRelationFromManyFieldInput';
import { useOpenRelationFromManyFieldInput } from '@/object-record/record-field/ui/meta-types/input/hooks/useOpenRelationFromManyFieldInput';
import { useOpenRelationToOneFieldInput } from '@/object-record/record-field/ui/meta-types/input/hooks/useOpenRelationToOneFieldInput';
import { type FieldDefinition } from '@/object-record/record-field/ui/types/FieldDefinition';
@@ -13,6 +14,8 @@ import {
type FieldRelationFromManyValue,
type FieldRelationValue,
} from '@/object-record/record-field/ui/types/FieldMetadata';
import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation';
import { isFieldMorphRelationOneToMany } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelationOneToMany';
import { isFieldRelationFromManyObjects } from '@/object-record/record-field/ui/types/guards/isFieldRelationFromManyObjects';
import { isFieldRelationToOneObject } from '@/object-record/record-field/ui/types/guards/isFieldRelationToOneObject';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
@@ -29,6 +32,9 @@ export const useOpenFieldInputEditMode = () => {
const { openRelationFromManyFieldInput } =
useOpenRelationFromManyFieldInput();
const { openMorphRelationFromManyFieldInput } =
useOpenMorphRelationFromManyFieldInput();
const { openActivityTargetCellEditMode } =
useOpenActivityTargetCellEditMode();
@@ -95,6 +101,19 @@ export const useOpenFieldInputEditMode = () => {
return;
}
if (isFieldMorphRelationOneToMany(fieldDefinition)) {
if (!isFieldMorphRelation(fieldDefinition)) {
throw new Error('Field is not a morph relation one to many');
}
openMorphRelationFromManyFieldInput({
recordId,
prefix,
fieldDefinition,
});
return;
}
if (isFieldRelationFromManyObjects(fieldDefinition)) {
if (
isDefined(
@@ -133,6 +152,7 @@ export const useOpenFieldInputEditMode = () => {
},
[
openActivityTargetCellEditMode,
openMorphRelationFromManyFieldInput,
openRelationFromManyFieldInput,
openRelationToOneFieldInput,
pushFocusItemToFocusStack,
@@ -0,0 +1,48 @@
import { useContext } from 'react';
import { useRecoilState } from 'recoil';
import { useGetButtonIcon } from '@/object-record/record-field/ui/hooks/useGetButtonIcon';
import { type FieldRelationValue } from '@/object-record/record-field/ui/types/FieldMetadata';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import { recordFieldInputDraftValueComponentState } from '@/object-record/record-field/ui/states/recordFieldInputDraftValueComponentState';
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 { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
export const useMorphRelationField = <
T extends ObjectRecord | ObjectRecord[],
>() => {
const { recordId, fieldDefinition, maxWidth } = useContext(FieldContext);
const button = useGetButtonIcon();
assertFieldMetadata(
FieldMetadataType.MORPH_RELATION,
isFieldMorphRelation,
fieldDefinition,
);
const fieldName = fieldDefinition.metadata.fieldName;
const [fieldValue, setFieldValue] = useRecoilState<FieldRelationValue<T>>(
recordStoreFamilySelector({ recordId, fieldName }),
);
const draftValue = useRecoilComponentValue(
recordFieldInputDraftValueComponentState,
);
const initialSearchValue = draftValue;
return {
fieldDefinition,
fieldValue,
initialSearchValue,
setFieldValue,
maxWidth: button && maxWidth ? maxWidth - 28 : maxWidth,
recordId,
};
};
@@ -0,0 +1,45 @@
import { useContext } from 'react';
import { FieldInputEventContext } from '@/object-record/record-field/ui/contexts/FieldInputEventContext';
import { useUpdateMorphRelationManyToOneFieldInput } from '@/object-record/record-field/ui/meta-types/input/hooks/useUpdateMorphRelationManyToOneFieldInput';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext';
import { recordFieldInputLayoutDirectionComponentState } from '@/object-record/record-field/ui/states/recordFieldInputLayoutDirectionComponentState';
import { MultipleRecordPicker } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPicker';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
export const MorphRelationOneToManyFieldInput = () => {
const instanceId = useAvailableComponentInstanceIdOrThrow(
RecordFieldComponentInstanceContext,
);
const { onSubmit } = useContext(FieldInputEventContext);
const { updateMorphRelationManyToOne } =
useUpdateMorphRelationManyToOneFieldInput();
const handleSubmit = () => {
onSubmit?.({ skipPersist: true });
};
const layoutDirection = useRecoilComponentValue(
recordFieldInputLayoutDirectionComponentState,
);
return (
<MultipleRecordPicker
focusId={instanceId}
componentInstanceId={instanceId}
onSubmit={handleSubmit}
onChange={(morphItem) => {
updateMorphRelationManyToOne(morphItem);
}}
onClickOutside={handleSubmit}
layoutDirection={
layoutDirection === 'downward'
? 'search-bar-on-top'
: 'search-bar-on-bottom'
}
/>
);
};
@@ -0,0 +1,140 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { type FieldDefinition } from '@/object-record/record-field/ui/types/FieldDefinition';
import { type FieldMorphRelationMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
import { useMultipleRecordPickerOpen } from '@/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerOpen';
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 { multipleRecordPickerSearchableObjectMetadataItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchableObjectMetadataItemsComponentState';
import { type RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { recordStoreMorphOneToManyValueWithObjectNameFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreMorphOneToManyValueWithObjectNameFamilySelector';
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
export const useOpenMorphRelationFromManyFieldInput = () => {
const { performSearch } = useMultipleRecordPickerPerformSearch();
const { openMultipleRecordPicker } = useMultipleRecordPickerOpen();
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
const openMorphRelationFromManyFieldInput = useRecoilCallback(
({ set, snapshot }) =>
({
recordId,
prefix,
fieldDefinition,
}: {
recordId: string;
prefix?: string;
fieldDefinition: FieldDefinition<FieldMorphRelationMetadata>;
}) => {
const recordPickerInstanceId = getRecordFieldInputInstanceId({
recordId,
fieldName: fieldDefinition.metadata.fieldName,
prefix,
});
const objectNameSingulars = fieldDefinition.metadata.morphRelations.map(
({ targetObjectMetadata }) => targetObjectMetadata.nameSingular,
);
const morphValuesWithObjectNameSingularFieldValue = snapshot
.getLoadable(
recordStoreMorphOneToManyValueWithObjectNameFamilySelector({
recordId,
morphRelations: fieldDefinition.metadata.morphRelations,
fieldName: fieldDefinition.metadata.fieldName,
}),
)
.getValue();
const objectMetadataItems = snapshot
.getLoadable(objectMetadataItemsState)
.getValue();
const objectMetadataItemArray = objectMetadataItems.filter(
(objectMetadataItem) =>
objectNameSingulars.includes(objectMetadataItem.nameSingular),
);
if (
!isDefined(objectMetadataItemArray) ||
objectMetadataItemArray.length === 0
) {
return;
}
openMultipleRecordPicker(recordPickerInstanceId);
const pickableMorphItems: RecordPickerPickableMorphItem[] =
morphValuesWithObjectNameSingularFieldValue.flatMap(
(recordWithObjectNameSingular) => {
const objectMetadataItem = objectMetadataItemArray.find(
(objectMetadataItem) =>
objectMetadataItem.nameSingular ===
recordWithObjectNameSingular.objectNameSingular,
);
if (!objectMetadataItem) {
return [];
}
return recordWithObjectNameSingular.value.map((recordValue) => ({
objectMetadataId: objectMetadataItem.id,
recordId: recordValue.id,
isSelected: true,
isMatchingSearchFilter: true,
}));
},
);
const records = morphValuesWithObjectNameSingularFieldValue.flatMap(
(recordWithObjectNameSingular) => recordWithObjectNameSingular.value,
);
for (const record of records) {
set(recordStoreFamilyState(record.id), record);
}
set(
multipleRecordPickerPickableMorphItemsComponentState.atomFamily({
instanceId: recordPickerInstanceId,
}),
pickableMorphItems,
);
set(
multipleRecordPickerSearchableObjectMetadataItemsComponentState.atomFamily(
{ instanceId: recordPickerInstanceId },
),
objectMetadataItemArray,
);
performSearch({
multipleRecordPickerInstanceId: recordPickerInstanceId,
forceSearchFilter: '',
forceSearchableObjectMetadataItems: objectMetadataItemArray,
forcePickableMorphItems: pickableMorphItems,
});
pushFocusItemToFocusStack({
focusId: recordPickerInstanceId,
component: {
type: FocusComponentType.DROPDOWN,
instanceId: recordPickerInstanceId,
},
globalHotkeysConfig: {
enableGlobalHotkeysConflictingWithKeyboard: false,
},
});
},
[openMultipleRecordPicker, performSearch, pushFocusItemToFocusStack],
);
return { openMorphRelationFromManyFieldInput };
};
@@ -0,0 +1,66 @@
import { useContext } from 'react';
import { useRecoilCallback } from 'recoil';
import { useAttachMorphRelatedRecordFromRecord } from '@/object-record/hooks/useAttachMorphRelatedRecordFromRecord';
import { useDetachMorphRelatedRecordFromRecord } from '@/object-record/hooks/useDetachMorphRelatedRecordFromRecord';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import { assertFieldMetadata } from '@/object-record/record-field/ui/types/guards/assertFieldMetadata';
import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation';
import { type RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
import { FieldMetadataType } from '~/generated-metadata/graphql';
export const useUpdateMorphRelationManyToOneFieldInput = () => {
const { recordId, fieldDefinition } = useContext(FieldContext);
assertFieldMetadata(
FieldMetadataType.MORPH_RELATION,
isFieldMorphRelation,
fieldDefinition,
);
if (!fieldDefinition.metadata.objectMetadataNameSingular) {
throw new Error('ObjectMetadataNameSingular is required');
}
const { updateOneRecordAndDetachMorphRelations } =
useDetachMorphRelatedRecordFromRecord();
const { updateOneRecordAndAttachMorphRelations } =
useAttachMorphRelatedRecordFromRecord();
const updateMorphRelationManyToOne = useRecoilCallback(
() =>
async (
morphItem: Pick<
RecordPickerPickableMorphItem,
'recordId' | 'isSelected'
>,
) => {
const recordObjectNameSingulars =
fieldDefinition.metadata.morphRelations.map(
(morphRelation) => morphRelation.targetObjectMetadata.nameSingular,
);
if (morphItem.isSelected) {
await updateOneRecordAndAttachMorphRelations({
recordId,
relatedRecordId: morphItem.recordId,
objectNameSingulars: recordObjectNameSingulars,
});
} else {
await updateOneRecordAndDetachMorphRelations({
recordId,
relatedRecordId: morphItem.recordId,
objectNameSingulars: recordObjectNameSingulars,
});
}
},
[
fieldDefinition.metadata.morphRelations,
recordId,
updateOneRecordAndAttachMorphRelations,
updateOneRecordAndDetachMorphRelations,
],
);
return { updateMorphRelationManyToOne };
};
@@ -58,7 +58,7 @@ export const SingleRecordPickerMenuItemsWithSearch = ({
);
const { records } = useSingleRecordPickerRecords({
objectNameSingular,
objectNameSingulars: [objectNameSingular],
excludedRecordIds,
});
@@ -4,10 +4,10 @@ import { useFilteredSearchRecordQuery } from '@/search/hooks/useFilteredSearchRe
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
export const useSingleRecordPickerRecords = ({
objectNameSingular,
objectNameSingulars,
excludedRecordIds = [],
}: {
objectNameSingular: string;
objectNameSingulars: string[];
excludedRecordIds?: string[];
}) => {
const recordPickerSearchFilter = useRecoilComponentValue(
@@ -17,12 +17,11 @@ export const useSingleRecordPickerRecords = ({
const selectedRecordId = useRecoilComponentValue(
singleRecordPickerSelectedIdComponentState,
);
const records = useFilteredSearchRecordQuery({
searchFilter: recordPickerSearchFilter,
selectedIds: selectedRecordId ? [selectedRecordId] : [],
excludedRecordIds: excludedRecordIds,
objectNameSingular,
objectNameSingulars,
});
return { records };
@@ -87,7 +87,7 @@ describe('useFilteredSearchRecordQuery', () => {
selectedIds: ['1'],
limit: 10,
excludedRecordIds: ['2'],
objectNameSingular: 'person',
objectNameSingulars: ['person'],
searchFilter: 'Entity',
});
},
@@ -8,13 +8,13 @@ export const useFilteredSearchRecordQuery = ({
selectedIds,
limit,
excludedRecordIds = [],
objectNameSingular,
objectNameSingulars,
searchFilter,
}: {
selectedIds: string[];
limit?: number;
excludedRecordIds?: string[];
objectNameSingular: string;
objectNameSingulars: string[];
searchFilter?: string;
}): {
selectedRecords: SingleRecordPickerRecord[];
@@ -26,7 +26,7 @@ export const useFilteredSearchRecordQuery = ({
const { loading: selectedRecordsLoading, searchRecords: selectedRecords } =
useObjectRecordSearchRecords({
objectNameSingular,
objectNameSingulars,
filter: selectedIdsFilter,
skip: !selectedIds.length,
searchInput: '',
@@ -36,7 +36,7 @@ export const useFilteredSearchRecordQuery = ({
loading: filteredSelectedRecordsLoading,
searchRecords: filteredSelectedRecords,
} = useObjectRecordSearchRecords({
objectNameSingular,
objectNameSingulars,
filter: selectedIdsFilter,
skip: !selectedIds.length,
searchInput: searchFilter,
@@ -48,7 +48,7 @@ export const useFilteredSearchRecordQuery = ({
: undefined;
const { loading: recordsToSelectLoading, searchRecords: recordsToSelect } =
useObjectRecordSearchRecords({
objectNameSingular,
objectNameSingulars,
filter: notFilter,
limit: limit ?? DEFAULT_SEARCH_REQUEST_LIMIT,
searchInput: searchFilter,
@@ -23,7 +23,7 @@ export const SettingsRoleAssignmentWorkspaceMemberPickerDropdown = ({
const { loading, searchRecords: workspaceMembers } =
useObjectRecordSearchRecords({
objectNameSingular: CoreObjectNameSingular.WorkspaceMember,
objectNameSingulars: [CoreObjectNameSingular.WorkspaceMember],
searchInput: searchFilter,
});