Add object type filter dropdown to side panel record searches (#18912)
## Summary - Adds an object type filter dropdown to the side panel, allowing users to scope record search results to a specific object type (e.g. People, Companies, Opportunities) - The filter appears as a funnel icon next to the search input in both the global search (SearchRecords page) and the "Pick a record" sidebar flow - Replaces the AI sparkles button on the search records page with the filter icon - Uses colored icons matching the navigation menu style (NavigationMenuItemStyleIcon + getStandardObjectIconColor) ## Test plan - [ ] Open the side panel, type something to enter SearchRecords mode, verify the filter icon appears - [ ] Click the filter icon, verify the dropdown opens with "Object" header, search input, "All Objects" and individual object types with colored icons - [ ] Select an object type, verify only records of that type appear in search results - [ ] Select "All Objects", verify all record types appear again - [ ] Verify the filter icon turns blue when a filter is active - [ ] In layout customization mode, add a new sidebar item > Record, verify the filter dropdown also works there - [ ] Close and reopen the side panel, verify the filter resets to "All Objects" - [ ] Verify the AI sparkles button no longer appears on the command menu root page - [ ] Verify the AI edit icon still appears on Ask AI pages Made with [Cursor](https://cursor.com)
This commit is contained in:
@@ -84,6 +84,24 @@ export const mockFlatObjectMetadatas: FlatObjectMetadata[] = [
|
||||
universalIdentifier: 'non-searchable-object-universal-id',
|
||||
applicationId: workspaceId,
|
||||
}),
|
||||
getFlatObjectMetadataMock({
|
||||
id: '20202020-6a7c-4e3f-9b2d-1d8f7a3e5c4b',
|
||||
nameSingular: 'message',
|
||||
namePlural: 'messages',
|
||||
labelSingular: 'Message',
|
||||
labelPlural: 'Messages',
|
||||
description: 'Message',
|
||||
icon: 'IconMessage',
|
||||
isCustom: false,
|
||||
isSystem: true,
|
||||
isSearchable: false,
|
||||
labelIdentifierFieldMetadataId: null,
|
||||
imageIdentifierFieldMetadataId: null,
|
||||
workspaceId,
|
||||
fieldIds: [],
|
||||
universalIdentifier: 'message-universal-id',
|
||||
applicationId: workspaceId,
|
||||
}),
|
||||
];
|
||||
|
||||
export const mockFlatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata> = {
|
||||
|
||||
+18
@@ -65,6 +65,24 @@ describe('SearchService', () => {
|
||||
|
||||
expect(objectMetadataItems).toEqual([mockFlatObjectMetadatas[1]]);
|
||||
});
|
||||
it('should allow non-searchable objects when explicitly included', () => {
|
||||
const objectMetadataItems = service.filterObjectMetadataItems({
|
||||
flatObjectMetadatas: mockFlatObjectMetadatas,
|
||||
includedObjectNameSingulars: ['non-searchable-object'],
|
||||
excludedObjectNameSingulars: [],
|
||||
});
|
||||
|
||||
expect(objectMetadataItems).toEqual([mockFlatObjectMetadatas[3]]);
|
||||
});
|
||||
it('should block objects with channel visibility constraints even when explicitly included', () => {
|
||||
const objectMetadataItems = service.filterObjectMetadataItems({
|
||||
flatObjectMetadatas: mockFlatObjectMetadatas,
|
||||
includedObjectNameSingulars: ['message'],
|
||||
excludedObjectNameSingulars: [],
|
||||
});
|
||||
|
||||
expect(objectMetadataItems).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLabelIdentifierColumns', () => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import chunk from 'lodash.chunk';
|
||||
import { OBJECTS_WITH_CHANNEL_VISIBILITY_CONSTRAINTS } from 'twenty-shared/constants';
|
||||
import { FieldMetadataType, ObjectRecord } from 'twenty-shared/types';
|
||||
import { getLogoUrlFromDomainName, isDefined } from 'twenty-shared/utils';
|
||||
import { Brackets, type ObjectLiteral } from 'typeorm';
|
||||
@@ -138,19 +139,35 @@ export class SearchService {
|
||||
includedObjectNameSingulars: string[];
|
||||
excludedObjectNameSingulars: string[];
|
||||
}) {
|
||||
const hasExplicitInclusion = includedObjectNameSingulars.length > 0;
|
||||
|
||||
return flatObjectMetadatas.filter(
|
||||
({ nameSingular, isSearchable, isActive }) => {
|
||||
if (!isSearchable) {
|
||||
return false;
|
||||
}
|
||||
if (!isActive) {
|
||||
return false;
|
||||
}
|
||||
if (excludedObjectNameSingulars.includes(nameSingular)) {
|
||||
|
||||
if (hasExplicitInclusion) {
|
||||
if (
|
||||
OBJECTS_WITH_CHANNEL_VISIBILITY_CONSTRAINTS.includes(
|
||||
nameSingular as (typeof OBJECTS_WITH_CHANNEL_VISIBILITY_CONSTRAINTS)[number],
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
includedObjectNameSingulars.includes(nameSingular) &&
|
||||
!excludedObjectNameSingulars.includes(nameSingular)
|
||||
);
|
||||
}
|
||||
|
||||
if (!isSearchable) {
|
||||
return false;
|
||||
}
|
||||
if (includedObjectNameSingulars.length > 0) {
|
||||
return includedObjectNameSingulars.includes(nameSingular);
|
||||
|
||||
if (excludedObjectNameSingulars.includes(nameSingular)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
Reference in New Issue
Block a user