From 9c278d46e4eef813e13625e1e2dcd225522e98aa Mon Sep 17 00:00:00 2001 From: Andrey Sobolev Date: Tue, 8 Feb 2022 16:02:35 +0700 Subject: [PATCH] Fix #890 (#948) Signed-off-by: Andrey Sobolev --- models/attachment/src/index.ts | 6 +- models/contact/src/index.ts | 2 + models/core/src/security.ts | 7 +- models/gmail/src/index.ts | 11 +- models/inventory/src/index.ts | 8 +- models/lead/src/index.ts | 6 +- models/recruit/src/index.ts | 15 ++- models/task/src/index.ts | 8 +- models/telegram/src/index.ts | 5 +- models/templates/src/index.ts | 6 +- .../src/components/EditVacancy.svelte | 4 +- server/core/src/fulltext.ts | 115 ++++++++++++------ tests/sanity/package.json | 2 +- tests/sanity/tests/contacts.spec.ts | 23 +++- tests/sanity/tests/playwright.config.ts | 7 ++ tests/sanity/tests/recruit.spec.ts | 24 ++++ 16 files changed, 185 insertions(+), 64 deletions(-) create mode 100644 tests/sanity/tests/playwright.config.ts diff --git a/models/attachment/src/index.ts b/models/attachment/src/index.ts index 986c6bb733..6bfc153afc 100644 --- a/models/attachment/src/index.ts +++ b/models/attachment/src/index.ts @@ -14,8 +14,8 @@ // import type { IntlString } from '@anticrm/platform' -import { Builder, Model, Prop, UX, TypeString, TypeTimestamp } from '@anticrm/model' -import type { Domain } from '@anticrm/core' +import { Builder, Model, Prop, UX, TypeString, TypeTimestamp, Index } from '@anticrm/model' +import { Domain, IndexKind } from '@anticrm/core' import core, { TAttachedDoc } from '@anticrm/model-core' import type { Attachment, Photo } from '@anticrm/attachment' import activity from '@anticrm/activity' @@ -31,6 +31,7 @@ export const DOMAIN_ATTACHMENT = 'attachment' as Domain @UX('File' as IntlString) export class TAttachment extends TAttachedDoc implements Attachment { @Prop(TypeString(), 'Name' as IntlString) + @Index(IndexKind.FullText) name!: string @Prop(TypeString(), 'File' as IntlString) @@ -40,6 +41,7 @@ export class TAttachment extends TAttachedDoc implements Attachment { size!: number @Prop(TypeString(), 'Type' as IntlString) + @Index(IndexKind.FullText) type!: string @Prop(TypeTimestamp(), 'Date' as IntlString) diff --git a/models/contact/src/index.ts b/models/contact/src/index.ts index 5543611d51..064c5ebf5e 100644 --- a/models/contact/src/index.ts +++ b/models/contact/src/index.ts @@ -59,6 +59,7 @@ export class TContact extends TDoc implements Contact { comments?: number @Prop(TypeString(), 'Location' as IntlString) + @Index(IndexKind.FullText) city!: string } @@ -69,6 +70,7 @@ export class TChannel extends TAttachedDoc implements Channel { provider!: Ref @Prop(TypeString(), 'Value' as IntlString) + @Index(IndexKind.FullText) value!: string items?: number diff --git a/models/core/src/security.ts b/models/core/src/security.ts index f91c8ab5a6..733f08e8ca 100644 --- a/models/core/src/security.ts +++ b/models/core/src/security.ts @@ -13,9 +13,8 @@ // limitations under the License. // -import type { Account, Arr, Ref, Space } from '@anticrm/core' -import { DOMAIN_MODEL } from '@anticrm/core' -import { Model, Prop, TypeBoolean, TypeString } from '@anticrm/model' +import { Account, Arr, DOMAIN_MODEL, IndexKind, Ref, Space } from '@anticrm/core' +import { Index, Model, Prop, TypeBoolean, TypeString } from '@anticrm/model' import type { IntlString } from '@anticrm/platform' import core from './component' import { TDoc } from './core' @@ -25,9 +24,11 @@ import { TDoc } from './core' @Model(core.class.Space, core.class.Doc, DOMAIN_MODEL) export class TSpace extends TDoc implements Space { @Prop(TypeString(), 'Name' as IntlString) + @Index(IndexKind.FullText) name!: string @Prop(TypeString(), 'Description' as IntlString) + @Index(IndexKind.FullText) description!: string @Prop(TypeBoolean(), 'Private' as IntlString) diff --git a/models/gmail/src/index.ts b/models/gmail/src/index.ts index e5de37889d..99f36c0e79 100644 --- a/models/gmail/src/index.ts +++ b/models/gmail/src/index.ts @@ -15,12 +15,12 @@ // import type { IntlString } from '@anticrm/platform' -import { Builder, Model, TypeString, Prop, ArrOf, TypeBoolean } from '@anticrm/model' +import { Builder, Model, TypeString, Prop, ArrOf, TypeBoolean, Index } from '@anticrm/model' import core, { TAttachedDoc } from '@anticrm/model-core' import contact from '@anticrm/model-contact' import gmail from './plugin' import type { Message, SharedMessage, SharedMessages } from '@anticrm/gmail' -import type { Domain, Type } from '@anticrm/core' +import { Domain, IndexKind, Type } from '@anticrm/core' import setting from '@anticrm/setting' import activity from '@anticrm/activity' @@ -36,24 +36,31 @@ export class TMessage extends TAttachedDoc implements Message { messageId!: string @Prop(TypeString(), 'ReplyTo' as IntlString) + @Index(IndexKind.FullText) replyTo?: string @Prop(TypeString(), 'From' as IntlString) + @Index(IndexKind.FullText) from!: string @Prop(TypeString(), 'To' as IntlString) + @Index(IndexKind.FullText) to!: string @Prop(TypeString(), 'Contact' as IntlString) + @Index(IndexKind.FullText) contact!: string @Prop(TypeString(), 'Subject' as IntlString) + @Index(IndexKind.FullText) subject!: string @Prop(TypeString(), 'Message' as IntlString) + @Index(IndexKind.FullText) content!: string @Prop(TypeString(), 'Message' as IntlString) + @Index(IndexKind.FullText) textContent!: string @Prop(ArrOf(TypeString()), 'Copy' as IntlString) diff --git a/models/inventory/src/index.ts b/models/inventory/src/index.ts index edd73774ac..0e26830b74 100644 --- a/models/inventory/src/index.ts +++ b/models/inventory/src/index.ts @@ -14,9 +14,9 @@ // limitations under the License. // -import { Doc, Domain, FindOptions, Ref } from '@anticrm/core' +import { Doc, Domain, FindOptions, IndexKind, Ref } from '@anticrm/core' import type { Category, Product, Variant } from '@anticrm/inventory' -import { Builder, Collection, Model, Prop, TypeRef, TypeString, UX } from '@anticrm/model' +import { Builder, Collection, Index, Model, Prop, TypeRef, TypeString, UX } from '@anticrm/model' import attachment from '@anticrm/model-attachment' import core, { TAttachedDoc } from '@anticrm/model-core' import workbench from '@anticrm/model-workbench' @@ -30,6 +30,7 @@ export const DOMAIN_INVENTORY = 'inventory' as Domain @UX(inventory.string.Category, inventory.icon.Categories, undefined, 'name') export class TCategory extends TAttachedDoc implements Category { @Prop(TypeString(), 'Name' as IntlString) + @Index(IndexKind.FullText) name!: string } @@ -41,6 +42,7 @@ export class TProduct extends TAttachedDoc implements Product { declare attachedTo: Ref @Prop(TypeString(), 'Name' as IntlString) + @Index(IndexKind.FullText) name!: string @Prop(Collection(attachment.class.Photo), attachment.string.Photos) @@ -61,9 +63,11 @@ export class TVariant extends TAttachedDoc implements Variant { declare attachedTo: Ref @Prop(TypeString(), 'Name' as IntlString) + @Index(IndexKind.FullText) name!: string @Prop(TypeString(), inventory.string.SKU) + @Index(IndexKind.FullText) sku!: string } diff --git a/models/lead/src/index.ts b/models/lead/src/index.ts index bff1c818d6..9cf27750b3 100644 --- a/models/lead/src/index.ts +++ b/models/lead/src/index.ts @@ -16,9 +16,9 @@ // To help typescript locate view plugin properly import type { Employee } from '@anticrm/contact' -import type { Doc, FindOptions, Lookup, Ref } from '@anticrm/core' +import { Doc, FindOptions, IndexKind, Lookup, Ref } from '@anticrm/core' import type { Customer, Funnel, Lead } from '@anticrm/lead' -import { Builder, Collection, Mixin, Model, Prop, TypeRef, TypeString, UX } from '@anticrm/model' +import { Builder, Collection, Index, Mixin, Model, Prop, TypeRef, TypeString, UX } from '@anticrm/model' import attachment from '@anticrm/model-attachment' import chunter from '@anticrm/model-chunter' import contact, { TPerson } from '@anticrm/model-contact' @@ -41,6 +41,7 @@ export class TLead extends TTask implements Lead { declare attachedTo: Ref @Prop(TypeString(), 'Title' as IntlString) + @Index(IndexKind.FullText) title!: string @Prop(Collection(chunter.class.Comment), 'Comments' as IntlString) @@ -60,6 +61,7 @@ export class TCustomer extends TPerson implements Customer { leads?: number @Prop(TypeString(), 'Description' as IntlString) + @Index(IndexKind.FullText) description!: string } diff --git a/models/recruit/src/index.ts b/models/recruit/src/index.ts index 65457b4093..004dcdf2eb 100644 --- a/models/recruit/src/index.ts +++ b/models/recruit/src/index.ts @@ -14,24 +14,25 @@ // import type { Employee } from '@anticrm/contact' -import { Doc, FindOptions, Lookup, Ref, Timestamp } from '@anticrm/core' -import { Builder, Collection, Mixin, Model, Prop, TypeBoolean, TypeDate, TypeMarkup, TypeRef, TypeString, UX } from '@anticrm/model' +import { Doc, FindOptions, IndexKind, Lookup, Ref, Timestamp } from '@anticrm/core' +import { Builder, Collection, Index, Mixin, Model, Prop, TypeBoolean, TypeDate, TypeMarkup, TypeRef, TypeString, UX } from '@anticrm/model' import attachment from '@anticrm/model-attachment' import chunter from '@anticrm/model-chunter' import contact, { TPerson } from '@anticrm/model-contact' import core, { TSpace } from '@anticrm/model-core' +import presentation from '@anticrm/model-presentation' import task, { TSpaceWithStates, TTask } from '@anticrm/model-task' import view from '@anticrm/model-view' import workbench from '@anticrm/model-workbench' import type { IntlString } from '@anticrm/platform' import { Applicant, Candidate, Candidates, Vacancy } from '@anticrm/recruit' import recruit from './plugin' -import presentation from '@anticrm/model-presentation' @Model(recruit.class.Vacancy, task.class.SpaceWithStates) @UX(recruit.string.Vacancy, recruit.icon.Vacancy) export class TVacancy extends TSpaceWithStates implements Vacancy { @Prop(TypeMarkup(), 'Full description' as IntlString) + @Index(IndexKind.FullText) fullDescription?: string @Prop(Collection(attachment.class.Attachment), 'Attachments' as IntlString) @@ -41,9 +42,11 @@ export class TVacancy extends TSpaceWithStates implements Vacancy { dueTo?: Timestamp @Prop(TypeString(), 'Location' as IntlString, recruit.icon.Location) + @Index(IndexKind.FullText) location?: string @Prop(TypeString(), 'Company' as IntlString, contact.icon.Company) + @Index(IndexKind.FullText) company?: string } @@ -55,6 +58,7 @@ export class TCandidates extends TSpace implements Candidates {} @UX('Candidate' as IntlString, recruit.icon.RecruitApplication) export class TCandidate extends TPerson implements Candidate { @Prop(TypeString(), 'Title' as IntlString) + @Index(IndexKind.FullText) title?: string @Prop(Collection(recruit.class.Applicant), 'Applications' as IntlString) @@ -67,6 +71,7 @@ export class TCandidate extends TPerson implements Candidate { remote?: boolean @Prop(TypeString(), 'Source' as IntlString) + @Index(IndexKind.FullText) source?: string } @@ -313,6 +318,6 @@ export function createModel (builder: Builder): void { }) } -export { default } from './plugin' -export { recruitOperation } from './migration' export { createDeps } from './creation' +export { recruitOperation } from './migration' +export { default } from './plugin' diff --git a/models/task/src/index.ts b/models/task/src/index.ts index 391d68470c..738333b9be 100644 --- a/models/task/src/index.ts +++ b/models/task/src/index.ts @@ -15,10 +15,11 @@ import type { Employee } from '@anticrm/contact' import contact from '@anticrm/contact' -import { Arr, Class, Doc, Domain, DOMAIN_MODEL, FindOptions, Ref, Space, Timestamp } from '@anticrm/core' +import { Arr, Class, Doc, Domain, DOMAIN_MODEL, FindOptions, IndexKind, Ref, Space, Timestamp } from '@anticrm/core' import { Builder, Collection, Hidden, Implements, + Index, Mixin, Model, Prop, TypeBoolean, @@ -106,6 +107,7 @@ export class TTask extends TAttachedDoc implements Task { @UX(task.string.Todo) export class TTodoItem extends TAttachedDoc implements TodoItem { @Prop(TypeString(), task.string.TodoName, task.icon.Task) + @Index(IndexKind.FullText) name!: string @Prop(TypeBoolean(), task.string.TaskDone) @@ -130,9 +132,11 @@ export class TIssue extends TTask implements Issue { declare attachedTo: Ref @Prop(TypeString(), task.string.IssueName) + @Index(IndexKind.FullText) name!: string @Prop(TypeMarkup(), task.string.TaskDescription) + @Index(IndexKind.FullText) description!: string @Prop(Collection(chunter.class.Comment), task.string.TaskComments) @@ -142,6 +146,7 @@ export class TIssue extends TTask implements Issue { attachments!: number @Prop(TypeString(), task.string.TaskLabels) + @Index(IndexKind.FullText) labels!: string @Prop(TypeRef(contact.class.Employee), task.string.TaskAssignee) @@ -193,6 +198,7 @@ export class TLostStateTemplate extends TDoneStateTemplate implements LostStateT @Model(task.class.KanbanTemplate, core.class.Doc, DOMAIN_KANBAN) export class TKanbanTemplate extends TDoc implements KanbanTemplate { @Prop(TypeString(), task.string.KanbanTemplateTitle) + @Index(IndexKind.FullText) title!: string @Prop(Collection(task.class.StateTemplate), task.string.States) diff --git a/models/telegram/src/index.ts b/models/telegram/src/index.ts index 18681b5bc5..a154abda17 100644 --- a/models/telegram/src/index.ts +++ b/models/telegram/src/index.ts @@ -15,12 +15,12 @@ // import type { IntlString } from '@anticrm/platform' -import { Builder, Model, TypeString, TypeBoolean, Prop, ArrOf } from '@anticrm/model' +import { Builder, Model, TypeString, TypeBoolean, Prop, ArrOf, Index } from '@anticrm/model' import core, { TAttachedDoc } from '@anticrm/model-core' import contact from '@anticrm/model-contact' import telegram from './plugin' import type { TelegramMessage, SharedTelegramMessage, SharedTelegramMessages } from '@anticrm/telegram' -import type { Domain, Type } from '@anticrm/core' +import { Domain, IndexKind, Type } from '@anticrm/core' import setting from '@anticrm/setting' import activity from '@anticrm/activity' @@ -33,6 +33,7 @@ function TypeSharedMessage (): Type { @Model(telegram.class.Message, core.class.AttachedDoc, DOMAIN_TELEGRAM) export class TTelegramMessage extends TAttachedDoc implements TelegramMessage { @Prop(TypeString(), 'Content' as IntlString) + @Index(IndexKind.FullText) content!: string @Prop(TypeBoolean(), 'Incoming' as IntlString) diff --git a/models/templates/src/index.ts b/models/templates/src/index.ts index 9802102951..30a84925ba 100644 --- a/models/templates/src/index.ts +++ b/models/templates/src/index.ts @@ -14,8 +14,8 @@ // limitations under the License. // -import type { Domain } from '@anticrm/core' -import { Builder, Model, Prop, TypeString } from '@anticrm/model' +import { Domain, IndexKind } from '@anticrm/core' +import { Builder, Index, Model, Prop, TypeString } from '@anticrm/model' import core, { TDoc } from '@anticrm/model-core' import textEditor from '@anticrm/model-text-editor' import { IntlString } from '@anticrm/platform' @@ -28,9 +28,11 @@ export const DOMAIN_TEMPLATES = 'templates' as Domain @Model(templates.class.MessageTemplate, core.class.Doc, DOMAIN_TEMPLATES) export class TMessageTemplate extends TDoc implements MessageTemplate { @Prop(TypeString(), 'Title' as IntlString) + @Index(IndexKind.FullText) title!: string; @Prop(TypeString(), 'Message' as IntlString) + @Index(IndexKind.FullText) message!: string; } diff --git a/plugins/recruit-resources/src/components/EditVacancy.svelte b/plugins/recruit-resources/src/components/EditVacancy.svelte index ea7848fe1a..9f2fba2652 100644 --- a/plugins/recruit-resources/src/components/EditVacancy.svelte +++ b/plugins/recruit-resources/src/components/EditVacancy.svelte @@ -22,7 +22,7 @@ import { AttributesBar, createQuery, getClient } from '@anticrm/presentation' import { Vacancy } from '@anticrm/recruit' import { StyledTextBox } from '@anticrm/text-editor' - import { Component, EditBox, Grid, Icon, IconClose, Label, ToggleWithLabel, ActionIcon, Scroller } from '@anticrm/ui' + import { ActionIcon, Component, EditBox, Grid, Icon, IconClose, Label, Scroller, ToggleWithLabel } from '@anticrm/ui' import { createEventDispatcher } from 'svelte' import recruit from '../plugin' @@ -108,7 +108,7 @@ {:else if selected === 2} - + {/if} {/if} diff --git a/server/core/src/fulltext.ts b/server/core/src/fulltext.ts index 4c27e5a5c2..93af4e5076 100644 --- a/server/core/src/fulltext.ts +++ b/server/core/src/fulltext.ts @@ -15,34 +15,38 @@ // import core, { - AttachedDoc, Class, ClassifierKind, Doc, Obj, Ref, TxCreateDoc, TxResult, TxUpdateDoc, AnyAttribute, + AttachedDoc, + Class, + ClassifierKind, Collection, + Doc, DocumentQuery, FindOptions, FindResult, Hierarchy, + IndexKind, MeasureContext, + Obj, PropertyType, + Ref, Tx, TxBulkWrite, TxCollectionCUD, + TxCreateDoc, TxMixin, TxProcessor, TxPutBag, - TxRemoveDoc + TxRemoveDoc, + TxResult, + TxUpdateDoc } from '@anticrm/core' import type { FullTextAdapter, IndexedDoc, WithFind } from './types' -// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -const NO_INDEX = [] as AnyAttribute[] - /** * @public */ export class FullTextIndex implements WithFind { - private readonly indexes = new Map>, AnyAttribute[]>() - constructor ( private readonly hierarchy: Hierarchy, private readonly adapter: FullTextAdapter, @@ -138,7 +142,7 @@ export class FullTextIndex implements WithFind { const { _id, $search, ...mainQuery } = query if ($search === undefined) return [] const docs = await this.adapter.search(_class, query, options?.limit) - const ids: Set> = new Set>(docs.map(p => p.id)) + const ids: Set> = new Set>(docs.map((p) => p.id)) for (const doc of docs) { if (doc.attachedTo !== undefined) { ids.add(doc.attachedTo) @@ -147,25 +151,29 @@ export class FullTextIndex implements WithFind { return await this.dbStorage.findAll(ctx, _class, { _id: { $in: Array.from(ids) as any }, ...mainQuery }, options) // TODO: remove `as any` } - private getFullTextAttributes (clazz: Ref>): AnyAttribute[] | undefined { - const attributes = this.indexes.get(clazz) - if (attributes === undefined) { - const allAttributes = this.hierarchy.getAllAttributes(clazz) - const result: AnyAttribute[] = [] - for (const [, attr] of allAttributes) { - if (attr.type._class === core.class.TypeString || attr.type._class === core.class.TypeMarkup) { - result.push(attr) - } + private getFullTextAttributes (clazz: Ref>, parentDoc?: Doc): AnyAttribute[] { + const allAttributes = this.hierarchy.getAllAttributes(clazz) + const result: AnyAttribute[] = [] + for (const [, attr] of allAttributes) { + if (isFullTextAttribute(attr)) { + result.push(attr) } - if (result.length > 0) { - this.indexes.set(clazz, result) - return result - } else { - this.indexes.set(clazz, NO_INDEX) - } - } else if (attributes !== NO_INDEX) { - return attributes } + + // We also neex to add all mixin attribues if parent is specified. + if (parentDoc !== undefined) { + this.hierarchy + .getDescendants(clazz) + .filter((m) => this.hierarchy.getClass(m).kind === ClassifierKind.MIXIN) + .forEach((m) => { + for (const [, v] of this.hierarchy.getAllAttributes(m, clazz)) { + if (isFullTextAttribute(v) && this.hierarchy.hasMixin(parentDoc, m)) { + result.push(v) + } + } + }) + } + return result } protected async txCreateDoc (ctx: MeasureContext, tx: TxCreateDoc): Promise { @@ -174,15 +182,20 @@ export class FullTextIndex implements WithFind { let parentContent: Record = {} if (this.hierarchy.isDerived(doc._class, core.class.AttachedDoc)) { const attachedDoc = doc as AttachedDoc - const parentDoc = ( - await this.dbStorage.findAll(ctx, attachedDoc.attachedToClass, { _id: attachedDoc.attachedTo }, { limit: 1 }) - )[0] - if (parentDoc !== undefined) { - const parentAttributes = this.getFullTextAttributes(parentDoc._class) - parentContent = this.getContent(parentAttributes, parentDoc) + if (attachedDoc.attachedToClass !== undefined && attachedDoc.attachedTo !== undefined) { + const parentDoc = ( + await this.dbStorage.findAll(ctx, attachedDoc.attachedToClass, { _id: attachedDoc.attachedTo }, { limit: 1 }) + )[0] + if (parentDoc !== undefined) { + const parentAttributes = this.getFullTextAttributes(parentDoc._class, parentDoc) + + if (parentAttributes.length > 0) { + parentContent = this.getContent(parentAttributes, parentDoc) + } + } } } - if (attributes === undefined && Object.keys(parentContent).length === 0) return {} + if (attributes.length === 0 && Object.keys(parentContent).length === 0) return {} let content = this.getContent(attributes, doc) content = { ...parentContent, ...content } @@ -201,7 +214,7 @@ export class FullTextIndex implements WithFind { protected async txUpdateDoc (ctx: MeasureContext, tx: TxUpdateDoc): Promise { const attributes = this.getFullTextAttributes(tx.objectClass) let result = {} - if (attributes === undefined) return result + if (attributes.length === 0) return result const ops: any = tx.operations const update: any = {} let shouldUpdate = false @@ -224,22 +237,33 @@ export class FullTextIndex implements WithFind { return result } - private getContent (attributes: AnyAttribute[] | undefined, doc: Doc): Record { + private getContent (attributes: AnyAttribute[], doc: Doc): Record { const attrs: Record = {} - for (const attr of attributes ?? []) { - attrs[attr.name] = (doc as any)[attr.name]?.toString() ?? '' + for (const attr of attributes) { + const isMixinAttr = this.hierarchy.isMixin(attr.attributeOf) + if (isMixinAttr) { + attrs[(attr.attributeOf as string) + '.' + attr.name] = + ((doc as any)[attr.attributeOf] ?? {})[attr.name]?.toString() ?? '' + } else { + attrs[attr.name] = (doc as any)[attr.name]?.toString() ?? '' + } } return attrs } - private async updateAttachedDocs (ctx: MeasureContext, tx: {objectId: Ref, objectClass: Ref>}, update: any): Promise { + private async updateAttachedDocs ( + ctx: MeasureContext, + tx: { objectId: Ref, objectClass: Ref> }, + update: any + ): Promise { const doc = (await this.dbStorage.findAll(ctx, tx.objectClass, { _id: tx.objectId }, { limit: 1 }))[0] if (doc === undefined) return const attributes = this.hierarchy.getAllAttributes(doc._class) // Find all mixin atttibutes for document. - this.hierarchy.getDescendants(doc._class) + this.hierarchy + .getDescendants(doc._class) .filter((m) => this.hierarchy.getClass(m).kind === ClassifierKind.MIXIN && this.hierarchy.hasMixin(doc, m)) .forEach((m) => { for (const [k, v] of this.hierarchy.getAllAttributes(m, doc._class)) { @@ -261,7 +285,14 @@ export class FullTextIndex implements WithFind { await this.adapter.update(attached._id, docUpdate) } catch (err: any) { if (((err.message as string) ?? '').includes('document_missing_exception:')) { - console.error('missing document in elastic for', tx.objectId, 'attached', attached._id, 'collection', attached.collection) + console.error( + 'missing document in elastic for', + tx.objectId, + 'attached', + attached._id, + 'collection', + attached.collection + ) // We have no document for attached object, so ignore for now. it is probable rebuild of elastic DB. continue } @@ -272,3 +303,9 @@ export class FullTextIndex implements WithFind { } } } +function isFullTextAttribute (attr: AnyAttribute): boolean { + return ( + attr.index === IndexKind.FullText && + (attr.type._class === core.class.TypeString || attr.type._class === core.class.TypeMarkup) + ) +} diff --git a/tests/sanity/package.json b/tests/sanity/package.json index 0fedbbbe3e..4f263a7023 100644 --- a/tests/sanity/package.json +++ b/tests/sanity/package.json @@ -11,7 +11,7 @@ "format": "prettier --write src && eslint --fix src", "ci": "playwright install --with-deps chromium", "test": "", - "uitest": "playwright test --browser chromium --reporter list,html" + "uitest": "playwright test --browser chromium --reporter list,html -c ./tests/playwright.config.ts" }, "devDependencies": { "@anticrm/platform-rig": "~0.6.0", diff --git a/tests/sanity/tests/contacts.spec.ts b/tests/sanity/tests/contacts.spec.ts index 1b15dde973..18ac7f1c27 100644 --- a/tests/sanity/tests/contacts.spec.ts +++ b/tests/sanity/tests/contacts.spec.ts @@ -1,4 +1,4 @@ -import { test } from '@playwright/test' +import { test, expect } from '@playwright/test' import { openWorkbench } from './utils' test.describe('contact tests', () => { @@ -22,4 +22,25 @@ test.describe('contact tests', () => { await page.locator('.card-container').locator('button:has-text("Create")').click() }) + test('contact-search', async ({ page }) => { + // Create user and workspace + await openWorkbench(page) + + await page.locator('[id="app-contact\\:string\\:Contacts"]').click() + + await expect(page.locator('text=Elton John')).toBeVisible() + expect(await page.locator('.tr-body').count()).toBeGreaterThan(5) + + const searchBox = page.locator('[placeholder="Search"]') + await searchBox.fill('Elton') + await searchBox.press('Enter') + + await expect(page.locator('.tr-body')).toHaveCount(1) + + await searchBox.fill('') + await searchBox.press('Enter') + + await expect(page.locator('text=Rosamund Chen')).toBeVisible() + expect(await page.locator('.tr-body').count()).toBeGreaterThan(5) + }) }) diff --git a/tests/sanity/tests/playwright.config.ts b/tests/sanity/tests/playwright.config.ts new file mode 100644 index 0000000000..b91c601f32 --- /dev/null +++ b/tests/sanity/tests/playwright.config.ts @@ -0,0 +1,7 @@ +import { PlaywrightTestConfig } from '@playwright/test' +const config: PlaywrightTestConfig = { + use: { + screenshot: 'only-on-failure' + } +} +export default config diff --git a/tests/sanity/tests/recruit.spec.ts b/tests/sanity/tests/recruit.spec.ts index 4cc59c01bb..6782fad23c 100644 --- a/tests/sanity/tests/recruit.spec.ts +++ b/tests/sanity/tests/recruit.spec.ts @@ -95,4 +95,28 @@ test.describe('recruit tests', () => { await expect(page.locator('text=John Multiseed').first()).toBeVisible() await expect(page.locator('text=Alex P.').first()).toBeVisible() }) + + test('application-search', async ({ page }) => { + // Create user and workspace + await openWorkbench(page) + + await page.locator('[id="app-recruit\\:string\\:RecruitApplication"]').click() + + await page.click('text=Software Engineer') + + await expect(page.locator('text=Andrey P.')).toBeVisible() + expect(await page.locator('.tr-body').count()).toBeGreaterThan(2) + + const searchBox = page.locator('[placeholder="Search"]') + await searchBox.fill('Frontend Engineer') + await searchBox.press('Enter') + + await expect(page.locator('.tr-body')).toHaveCount(1) + + await searchBox.fill('') + await searchBox.press('Enter') + + await expect(page.locator('text=Andrey P.')).toBeVisible() + expect(await page.locator('.tr-body').count()).toBeGreaterThan(2) + }) })