mirror of
				https://github.com/lingble/twenty.git
				synced 2025-10-29 20:02:29 +00:00 
			
		
		
		
	Implement search for rich text fields and use it for notes (#7953)
Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
		| @@ -13,9 +13,7 @@ import { Company } from '@/companies/types/Company'; | ||||
| import { useKeyboardShortcutMenu } from '@/keyboard-shortcut-menu/hooks/useKeyboardShortcutMenu'; | ||||
| import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; | ||||
| import { getCompanyDomainName } from '@/object-metadata/utils/getCompanyDomainName'; | ||||
| import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; | ||||
| import { useSearchRecords } from '@/object-record/hooks/useSearchRecords'; | ||||
| import { makeOrFilterVariables } from '@/object-record/utils/makeOrFilterVariables'; | ||||
| import { Opportunity } from '@/opportunities/types/Opportunity'; | ||||
| import { Person } from '@/people/types/Person'; | ||||
| import { LightIconButton } from '@/ui/input/button/components/LightIconButton'; | ||||
| @@ -181,16 +179,11 @@ export const CommandMenu = () => { | ||||
|       searchInput: deferredCommandMenuSearch ?? undefined, | ||||
|     }); | ||||
|  | ||||
|   const { loading: isNotesLoading, records: notes } = useFindManyRecords<Note>({ | ||||
|   const { loading: isNotesLoading, records: notes } = useSearchRecords<Note>({ | ||||
|     skip: !isCommandMenuOpened, | ||||
|     objectNameSingular: CoreObjectNameSingular.Note, | ||||
|     filter: deferredCommandMenuSearch | ||||
|       ? makeOrFilterVariables([ | ||||
|           { title: { ilike: `%${deferredCommandMenuSearch}%` } }, | ||||
|           { body: { ilike: `%${deferredCommandMenuSearch}%` } }, | ||||
|         ]) | ||||
|       : undefined, | ||||
|     limit: 3, | ||||
|     searchInput: deferredCommandMenuSearch ?? undefined, | ||||
|   }); | ||||
|  | ||||
|   const { loading: isOpportunitiesLoading, records: opportunities } = | ||||
|   | ||||
| @@ -11,6 +11,7 @@ import { | ||||
| import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; | ||||
| import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util'; | ||||
| import { | ||||
|   WorkspaceMigrationColumnAction, | ||||
|   WorkspaceMigrationColumnActionType, | ||||
|   WorkspaceMigrationEntity, | ||||
|   WorkspaceMigrationTableAction, | ||||
| @@ -87,29 +88,57 @@ export class WorkspaceMigrationFieldFactory { | ||||
|   ): Promise<Partial<WorkspaceMigrationEntity>[]> { | ||||
|     const workspaceMigrations: Partial<WorkspaceMigrationEntity>[] = []; | ||||
|  | ||||
|     for (const fieldMetadata of fieldMetadataCollection) { | ||||
|       if (fieldMetadata.type === FieldMetadataType.RELATION) { | ||||
|         continue; | ||||
|       } | ||||
|     const fieldMetadataCollectionGroupByObjectMetadataId = | ||||
|       fieldMetadataCollection.reduce( | ||||
|         (result, currentFieldMetadata) => { | ||||
|           result[currentFieldMetadata.objectMetadataId] = [ | ||||
|             ...(result[currentFieldMetadata.objectMetadataId] || []), | ||||
|             currentFieldMetadata, | ||||
|           ]; | ||||
|  | ||||
|       const migrations: WorkspaceMigrationTableAction[] = [ | ||||
|         { | ||||
|           name: computeObjectTargetTable( | ||||
|             originalObjectMetadataMap[fieldMetadata.objectMetadataId], | ||||
|           ), | ||||
|           action: WorkspaceMigrationTableActionType.ALTER, | ||||
|           columns: this.workspaceMigrationFactory.createColumnActions( | ||||
|           return result; | ||||
|         }, | ||||
|         {} as Record<string, FieldMetadataEntity[]>, | ||||
|       ); | ||||
|  | ||||
|     for (const objectMetadataId in fieldMetadataCollectionGroupByObjectMetadataId) { | ||||
|       const fieldMetadataCollection = | ||||
|         fieldMetadataCollectionGroupByObjectMetadataId[objectMetadataId]; | ||||
|  | ||||
|       const columns: WorkspaceMigrationColumnAction[] = []; | ||||
|  | ||||
|       const objectMetadata = | ||||
|         originalObjectMetadataMap[fieldMetadataCollection[0]?.objectMetadataId]; | ||||
|  | ||||
|       for (const fieldMetadata of fieldMetadataCollection) { | ||||
|         // Relations are handled in workspace-migration-relation.factory.ts | ||||
|         if (fieldMetadata.type === FieldMetadataType.RELATION) { | ||||
|           continue; | ||||
|         } | ||||
|  | ||||
|         columns.push( | ||||
|           ...this.workspaceMigrationFactory.createColumnActions( | ||||
|             WorkspaceMigrationColumnActionType.CREATE, | ||||
|             fieldMetadata, | ||||
|           ), | ||||
|         }, | ||||
|       ]; | ||||
|         ); | ||||
|       } | ||||
|  | ||||
|       workspaceMigrations.push({ | ||||
|         workspaceId: fieldMetadata.workspaceId, | ||||
|         name: generateMigrationName(`create-${fieldMetadata.name}`), | ||||
|         workspaceId: objectMetadata.workspaceId, | ||||
|         name: generateMigrationName( | ||||
|           `create-${objectMetadata.nameSingular}-fields`, | ||||
|         ), | ||||
|         isCustom: false, | ||||
|         migrations, | ||||
|         migrations: [ | ||||
|           { | ||||
|             name: computeObjectTargetTable( | ||||
|               originalObjectMetadataMap[objectMetadataId], | ||||
|             ), | ||||
|             action: WorkspaceMigrationTableActionType.ALTER, | ||||
|             columns, | ||||
|           }, | ||||
|         ], | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -282,6 +282,7 @@ export const NOTE_STANDARD_FIELD_IDS = { | ||||
|   attachments: '20202020-4986-4c92-bf19-39934b149b16', | ||||
|   timelineActivities: '20202020-7030-42f8-929c-1a57b25d6bce', | ||||
|   favorites: '20202020-4d1d-41ac-b13b-621631298d67', | ||||
|   searchVector: '20202020-7ea8-44d4-9d4c-51dd2a757950', | ||||
| }; | ||||
|  | ||||
| export const NOTE_TARGET_STANDARD_FIELD_IDS = { | ||||
|   | ||||
| @@ -75,8 +75,9 @@ const getColumnExpression = ( | ||||
| ): string => { | ||||
|   const quotedColumnName = `"${columnName}"`; | ||||
|  | ||||
|   if (fieldType === FieldMetadataType.EMAILS) { | ||||
|     return ` | ||||
|   switch (fieldType) { | ||||
|     case FieldMetadataType.EMAILS: | ||||
|       return ` | ||||
|       COALESCE( | ||||
|         replace( | ||||
|           ${quotedColumnName}, | ||||
| @@ -86,7 +87,9 @@ const getColumnExpression = ( | ||||
|         '' | ||||
|       ) | ||||
|     `; | ||||
|   } else { | ||||
|     return `COALESCE(${quotedColumnName}, '')`; | ||||
|     case FieldMetadataType.RICH_TEXT: | ||||
|       return `COALESCE(jsonb_path_query_array(${quotedColumnName}::jsonb, '$[*].content[*]."text"'::jsonpath)::text, '')`; | ||||
|     default: | ||||
|       return `COALESCE(${quotedColumnName}, '')`; | ||||
|   } | ||||
| }; | ||||
|   | ||||
| @@ -6,6 +6,7 @@ const SEARCHABLE_FIELD_TYPES = [ | ||||
|   FieldMetadataType.EMAILS, | ||||
|   FieldMetadataType.ADDRESS, | ||||
|   FieldMetadataType.LINKS, | ||||
|   FieldMetadataType.RICH_TEXT, | ||||
| ] as const; | ||||
|  | ||||
| export type SearchableFieldType = (typeof SEARCHABLE_FIELD_TYPES)[number]; | ||||
|   | ||||
| @@ -1,27 +1,42 @@ | ||||
| import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; | ||||
|  | ||||
| import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants'; | ||||
| import { | ||||
|   ActorMetadata, | ||||
|   FieldActorSource, | ||||
| } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; | ||||
| import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; | ||||
| import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; | ||||
| import { | ||||
|   RelationMetadataType, | ||||
|   RelationOnDeleteAction, | ||||
| } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; | ||||
| import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; | ||||
| import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; | ||||
| import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-field-index.decorator'; | ||||
| import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator'; | ||||
| import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator'; | ||||
| import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator'; | ||||
| import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator'; | ||||
| import { NOTE_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; | ||||
| import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids'; | ||||
| import { | ||||
|   FieldTypeAndNameMetadata, | ||||
|   getTsVectorColumnExpressionFromFields, | ||||
| } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util'; | ||||
| import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity'; | ||||
| import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity'; | ||||
| import { NoteTargetWorkspaceEntity } from 'src/modules/note/standard-objects/note-target.workspace-entity'; | ||||
| import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity'; | ||||
|  | ||||
| const TITLE_FIELD_NAME = 'title'; | ||||
| const BODY_FIELD_NAME = 'body'; | ||||
|  | ||||
| export const SEARCH_FIELDS_FOR_NOTES: FieldTypeAndNameMetadata[] = [ | ||||
|   { name: TITLE_FIELD_NAME, type: FieldMetadataType.TEXT }, | ||||
|   { name: BODY_FIELD_NAME, type: FieldMetadataType.RICH_TEXT }, | ||||
| ]; | ||||
|  | ||||
| @WorkspaceEntity({ | ||||
|   standardId: STANDARD_OBJECT_IDS.note, | ||||
|   namePlural: 'notes', | ||||
| @@ -50,7 +65,7 @@ export class NoteWorkspaceEntity extends BaseWorkspaceEntity { | ||||
|     description: 'Note title', | ||||
|     icon: 'IconNotes', | ||||
|   }) | ||||
|   title: string; | ||||
|   [TITLE_FIELD_NAME]: string; | ||||
|  | ||||
|   @WorkspaceField({ | ||||
|     standardId: NOTE_STANDARD_FIELD_IDS.body, | ||||
| @@ -60,7 +75,7 @@ export class NoteWorkspaceEntity extends BaseWorkspaceEntity { | ||||
|     icon: 'IconFilePencil', | ||||
|   }) | ||||
|   @WorkspaceIsNullable() | ||||
|   body: string | null; | ||||
|   [BODY_FIELD_NAME]: string | null; | ||||
|  | ||||
|   @WorkspaceField({ | ||||
|     standardId: NOTE_STANDARD_FIELD_IDS.createdBy, | ||||
| @@ -122,4 +137,20 @@ export class NoteWorkspaceEntity extends BaseWorkspaceEntity { | ||||
|   }) | ||||
|   @WorkspaceIsSystem() | ||||
|   favorites: Relation<FavoriteWorkspaceEntity[]>; | ||||
|  | ||||
|   @WorkspaceField({ | ||||
|     standardId: NOTE_STANDARD_FIELD_IDS.searchVector, | ||||
|     type: FieldMetadataType.TS_VECTOR, | ||||
|     label: SEARCH_VECTOR_FIELD.label, | ||||
|     description: SEARCH_VECTOR_FIELD.description, | ||||
|     icon: 'IconUser', | ||||
|     generatedType: 'STORED', | ||||
|     asExpression: getTsVectorColumnExpressionFromFields( | ||||
|       SEARCH_FIELDS_FOR_NOTES, | ||||
|     ), | ||||
|   }) | ||||
|   @WorkspaceIsNullable() | ||||
|   @WorkspaceIsSystem() | ||||
|   @WorkspaceFieldIndex({ indexType: IndexType.GIN }) | ||||
|   [SEARCH_VECTOR_FIELD.name]: any; | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Marie
					Marie