545 replace objects icons and names with records avatars and labelidentifiers in command menu context chips (#10787)

Closes https://github.com/twentyhq/core-team-issues/issues/545

This PR:
- Introduces `commandMenuNavigationMorphItemsState` which stores the
information about the `recordId` and the `objectMetadataItemId` for each
page
- Creates `CommandMenuContextChipEffect`, which queries the records from
the previous pages in case a record has been updated during the
navigation, to keep up to date information and stores it inside
`commandMenuNavigationRecordsState`
- `useCommandMenuContextChips` returns the context chips information
- Style updates (icons background and color)
- Updates `useCommandMenu` to set and reset these new states


https://github.com/user-attachments/assets/8886848a-721d-4709-9330-8e84ebc0d51e
This commit is contained in:
Raphaël Bosi
2025-03-12 15:26:14 +01:00
committed by GitHub
parent e030fc8917
commit bfc542290b
22 changed files with 620 additions and 174 deletions

View File

@ -3,9 +3,9 @@ import { useQuery } from '@apollo/client';
import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection';
import { EMPTY_QUERY } from '@/object-record/constants/EmptyQuery';
import { RecordGqlOperationSignature } from '@/object-record/graphql/types/RecordGqlOperationSignature';
import { useCombinedFindManyRecordsQueryVariables } from '@/object-record/multiple-objects/hooks/useCombinedFindManyRecordsQueryVariables';
import { useGenerateCombinedFindManyRecordsQuery } from '@/object-record/multiple-objects/hooks/useGenerateCombinedFindManyRecordsQuery';
import { CombinedFindManyRecordsQueryResult } from '@/object-record/multiple-objects/types/CombinedFindManyRecordsQueryResult';
import { generateCombinedFindManyRecordsQueryVariables } from '@/object-record/multiple-objects/utils/generateCombinedFindManyRecordsQueryVariables';
export const useCombinedFindManyRecords = ({
operationSignatures,
@ -18,7 +18,7 @@ export const useCombinedFindManyRecords = ({
operationSignatures,
});
const queryVariables = useCombinedFindManyRecordsQueryVariables({
const queryVariables = generateCombinedFindManyRecordsQueryVariables({
operationSignatures,
});

View File

@ -0,0 +1,153 @@
import { ApolloClient, gql, useApolloClient } from '@apollo/client';
import { isUndefined } from '@sniptt/guards';
import { capitalize } from 'twenty-shared';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection';
import { EMPTY_QUERY } from '@/object-record/constants/EmptyQuery';
import { RecordGqlOperationSignature } from '@/object-record/graphql/types/RecordGqlOperationSignature';
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
import { CombinedFindManyRecordsQueryResult } from '@/object-record/multiple-objects/types/CombinedFindManyRecordsQueryResult';
import { generateCombinedFindManyRecordsQueryVariables } from '@/object-record/multiple-objects/utils/generateCombinedFindManyRecordsQueryVariables';
import { getCombinedFindManyRecordsQueryFilteringPart } from '@/object-record/multiple-objects/utils/getCombinedFindManyRecordsQueryFilteringPart';
import { useRecoilValue } from 'recoil';
export const usePerformCombinedFindManyRecords = () => {
const client = useApolloClient();
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const generateCombinedFindManyRecordsQuery = (
operationSignatures: RecordGqlOperationSignature[],
objectMetadataItemsValue: ObjectMetadataItem[],
) => {
const filterPerMetadataItemArray = operationSignatures
.map(
({ objectNameSingular }) =>
`$filter${capitalize(objectNameSingular)}: ${capitalize(
objectNameSingular,
)}FilterInput`,
)
.join(', ');
const orderByPerMetadataItemArray = operationSignatures
.map(
({ objectNameSingular }) =>
`$orderBy${capitalize(objectNameSingular)}: [${capitalize(
objectNameSingular,
)}OrderByInput]`,
)
.join(', ');
const cursorFilteringPerMetadataItemArray = operationSignatures
.map(
({ objectNameSingular }) =>
`$after${capitalize(objectNameSingular)}: String, $before${capitalize(objectNameSingular)}: String, $first${capitalize(objectNameSingular)}: Int, $last${capitalize(objectNameSingular)}: Int`,
)
.join(', ');
const limitPerMetadataItemArray = operationSignatures
.map(
({ objectNameSingular }) =>
`$limit${capitalize(objectNameSingular)}: Int`,
)
.join(', ');
const queryOperationSignatureWithObjectMetadataItemArray =
operationSignatures.map((operationSignature) => {
const objectMetadataItem = objectMetadataItemsValue.find(
(objectMetadataItem) =>
objectMetadataItem.nameSingular ===
operationSignature.objectNameSingular,
);
if (isUndefined(objectMetadataItem)) {
throw new Error(
`Object metadata item not found for object name singular: ${operationSignature.objectNameSingular}`,
);
}
return { operationSignature, objectMetadataItem };
});
return gql`
query CombinedFindManyRecords(
${filterPerMetadataItemArray},
${orderByPerMetadataItemArray},
${cursorFilteringPerMetadataItemArray},
${limitPerMetadataItemArray}
) {
${queryOperationSignatureWithObjectMetadataItemArray
.map(
({ objectMetadataItem, operationSignature }) =>
`${getCombinedFindManyRecordsQueryFilteringPart(
objectMetadataItem,
)} {
edges {
node ${mapObjectMetadataToGraphQLQuery({
objectMetadataItems: objectMetadataItemsValue,
objectMetadataItem,
recordGqlFields:
operationSignature.fields ??
generateDepthOneRecordGqlFields({
objectMetadataItem,
}),
})}
cursor
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
totalCount
}`,
)
.join('\n')}
}
`;
};
const performCombinedFindManyRecords = async ({
operationSignatures,
client: customClient,
}: {
operationSignatures: RecordGqlOperationSignature[];
client?: ApolloClient<object>;
}) => {
const apolloClient = customClient || client;
const findManyQuery = generateCombinedFindManyRecordsQuery(
operationSignatures,
objectMetadataItems,
);
const queryVariables = generateCombinedFindManyRecordsQueryVariables({
operationSignatures,
});
const { data, loading } =
await apolloClient.query<CombinedFindManyRecordsQueryResult>({
query: findManyQuery ?? EMPTY_QUERY,
variables: queryVariables,
});
const resultWithoutConnection = Object.fromEntries(
Object.entries(data ?? {}).map(([namePlural, objectRecordConnection]) => [
namePlural,
getRecordsFromRecordConnection({
recordConnection: objectRecordConnection,
}),
]),
);
return {
result: resultWithoutConnection,
loading,
};
};
return { performCombinedFindManyRecords };
};

View File

@ -0,0 +1,4 @@
export type MorphItem = {
recordId: string;
objectMetadataId: string;
};

View File

@ -1,6 +1,6 @@
import { RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields';
import { RecordGqlOperationSignature } from '@/object-record/graphql/types/RecordGqlOperationSignature';
import { useCombinedFindManyRecordsQueryVariables } from '@/object-record/multiple-objects/hooks/useCombinedFindManyRecordsQueryVariables';
import { generateCombinedFindManyRecordsQueryVariables } from '@/object-record/multiple-objects/utils/generateCombinedFindManyRecordsQueryVariables';
describe('useCombinedFindManyRecordsQueryVariables', () => {
it('should generate variables with after cursor and first limit', () => {
@ -26,7 +26,7 @@ describe('useCombinedFindManyRecordsQueryVariables', () => {
},
];
const result = useCombinedFindManyRecordsQueryVariables({
const result = generateCombinedFindManyRecordsQueryVariables({
operationSignatures,
});
@ -58,7 +58,7 @@ describe('useCombinedFindManyRecordsQueryVariables', () => {
},
];
const result = useCombinedFindManyRecordsQueryVariables({
const result = generateCombinedFindManyRecordsQueryVariables({
operationSignatures,
});
@ -86,7 +86,7 @@ describe('useCombinedFindManyRecordsQueryVariables', () => {
},
];
const result = useCombinedFindManyRecordsQueryVariables({
const result = generateCombinedFindManyRecordsQueryVariables({
operationSignatures,
});
@ -125,7 +125,7 @@ describe('useCombinedFindManyRecordsQueryVariables', () => {
},
];
const result = useCombinedFindManyRecordsQueryVariables({
const result = generateCombinedFindManyRecordsQueryVariables({
operationSignatures,
});
@ -139,7 +139,7 @@ describe('useCombinedFindManyRecordsQueryVariables', () => {
});
it('should handle empty operation signatures', () => {
const result = useCombinedFindManyRecordsQueryVariables({
const result = generateCombinedFindManyRecordsQueryVariables({
operationSignatures: [],
});
@ -157,7 +157,7 @@ describe('useCombinedFindManyRecordsQueryVariables', () => {
},
];
const result = useCombinedFindManyRecordsQueryVariables({
const result = generateCombinedFindManyRecordsQueryVariables({
operationSignatures,
});
@ -180,7 +180,7 @@ describe('useCombinedFindManyRecordsQueryVariables', () => {
},
];
const result = useCombinedFindManyRecordsQueryVariables({
const result = generateCombinedFindManyRecordsQueryVariables({
operationSignatures,
});

View File

@ -3,7 +3,7 @@ import { isNonEmptyString } from '@sniptt/guards';
import { capitalize, isDefined } from 'twenty-shared';
import { isNonEmptyArray } from '~/utils/isNonEmptyArray';
export const useCombinedFindManyRecordsQueryVariables = ({
export const generateCombinedFindManyRecordsQueryVariables = ({
operationSignatures,
}: {
operationSignatures: RecordGqlOperationSignature[];

View File

@ -0,0 +1,13 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { capitalize } from 'twenty-shared';
export const getLimitPerMetadataItem = (
objectMetadataItems: Pick<ObjectMetadataItem, 'nameSingular'>[],
limit: number,
) => {
return Object.fromEntries(
objectMetadataItems.map(({ nameSingular }) => {
return [`limit${capitalize(nameSingular)}`, limit];
}),
);
};

View File

@ -1,6 +1,7 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { CombinedFindManyRecordsQueryResult } from '@/object-record/multiple-objects/types/CombinedFindManyRecordsQueryResult';
import { generateCombinedSearchRecordsQuery } from '@/object-record/multiple-objects/utils/generateCombinedSearchRecordsQuery';
import { getLimitPerMetadataItem } from '@/object-record/multiple-objects/utils/getLimitPerMetadataItem';
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';
@ -222,12 +223,9 @@ const performSearchForPickedRecords = async ({
),
});
const limitPerMetadataItem = Object.fromEntries(
searchableObjectMetadataItems
.map(({ nameSingular }) => {
return [`limit${capitalize(nameSingular)}`, 10];
})
.filter(isDefined),
const limitPerMetadataItem = getLimitPerMetadataItem(
searchableObjectMetadataItemsFilteredOnPickedRecordId,
10,
);
const { data: combinedSearchRecordFilteredOnPickedRecordsQueryResult } =
@ -309,12 +307,9 @@ const performSearchExcludingPickedRecords = async ({
),
});
const limitPerMetadataItem = Object.fromEntries(
searchableObjectMetadataItems
.map(({ nameSingular }) => {
return [`limit${capitalize(nameSingular)}`, 10];
})
.filter(isDefined),
const limitPerMetadataItem = getLimitPerMetadataItem(
searchableObjectMetadataItems,
10,
);
const { data: combinedSearchRecordExcludingPickedRecordsQueryResult } =