diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx index 07ca2a256..2ad134409 100644 --- a/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx @@ -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({ + const { loading: isNotesLoading, records: notes } = useSearchRecords({ 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 } = diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-field.factory.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-field.factory.ts index a79838dfb..c4852a584 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-field.factory.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-field.factory.ts @@ -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[]> { const workspaceMigrations: Partial[] = []; - 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, + ); + + 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, + }, + ], }); } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts index 764c48237..25949019e 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts @@ -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 = { diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util.ts index f4f197ff5..9b177555d 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util.ts @@ -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}, '')`; } }; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/is-searchable-field.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/is-searchable-field.util.ts index bb482ae4a..78b9c62e7 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/is-searchable-field.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/is-searchable-field.util.ts @@ -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]; diff --git a/packages/twenty-server/src/modules/note/standard-objects/note.workspace-entity.ts b/packages/twenty-server/src/modules/note/standard-objects/note.workspace-entity.ts index 997e8aca1..16e3fb82c 100644 --- a/packages/twenty-server/src/modules/note/standard-objects/note.workspace-entity.ts +++ b/packages/twenty-server/src/modules/note/standard-objects/note.workspace-entity.ts @@ -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; + + @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; }