mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-07 16:41:31 +00:00
211 lines
6.1 KiB
TypeScript
211 lines
6.1 KiB
TypeScript
import {
|
|
docKey,
|
|
type Branding,
|
|
type Class,
|
|
type Doc,
|
|
type DocIndexState,
|
|
type Hierarchy,
|
|
type Ref,
|
|
type RefTo,
|
|
type SearchResultDoc
|
|
} from '@hcengineering/core'
|
|
import { getResource, type Resource } from '@hcengineering/platform'
|
|
|
|
import plugin from './plugin'
|
|
import {
|
|
type ClassSearchConfigProps,
|
|
type IndexedDoc,
|
|
type SearchPresenter,
|
|
type SearchPresenterFunc,
|
|
type SearchScoring
|
|
} from './types'
|
|
|
|
interface IndexedReader {
|
|
get: (attribute: string) => any
|
|
getDoc: (attribute: string) => IndexedReader | undefined
|
|
}
|
|
|
|
// TODO: Rework to use mongo
|
|
function createIndexedReader (
|
|
_class: Ref<Class<Doc>>,
|
|
hierarchy: Hierarchy,
|
|
doc: DocIndexState,
|
|
otherDocs?: Record<string, DocIndexState | undefined>,
|
|
refAttribute?: string
|
|
): IndexedReader {
|
|
return {
|
|
get: (attr: string) => {
|
|
const realAttr = hierarchy.findAttribute(_class, attr)
|
|
if (realAttr !== undefined) {
|
|
return doc.attributes[docKey(attr, { _class: realAttr.attributeOf })] ?? (doc as any)[attr]
|
|
}
|
|
return undefined
|
|
},
|
|
getDoc: (attr: string) => {
|
|
const realAttr = hierarchy.findAttribute(_class, attr)
|
|
if (realAttr !== undefined) {
|
|
const anotherDoc = otherDocs?.[attr]
|
|
if (anotherDoc !== undefined) {
|
|
const refAtrr = realAttr.type as RefTo<Doc>
|
|
return createIndexedReader(refAtrr.to, hierarchy, anotherDoc, otherDocs, docKey(attr, { _class }))
|
|
}
|
|
}
|
|
return undefined
|
|
}
|
|
}
|
|
}
|
|
|
|
async function readAndMapProps (
|
|
reader: IndexedReader,
|
|
props: ClassSearchConfigProps[],
|
|
searchProvider?: {
|
|
hierarchy: Hierarchy
|
|
providers: SearchPresenterFunc
|
|
}
|
|
): Promise<Record<string, any>> {
|
|
const res: Record<string, any> = {}
|
|
for (const prop of props) {
|
|
if (typeof prop === 'string') {
|
|
res[prop] = reader.get(prop)
|
|
} else {
|
|
for (const [propName, rest] of Object.entries(prop)) {
|
|
if (rest.length > 1) {
|
|
const val = reader.getDoc(rest[0])?.get(rest[1])
|
|
const v = Array.isArray(val) ? val[0] : val
|
|
if (searchProvider !== undefined) {
|
|
const func =
|
|
searchProvider.providers !== undefined && Object.keys(searchProvider.providers).includes(propName)
|
|
? ((await getResource(searchProvider.providers[propName])) as any)
|
|
: undefined
|
|
if (func !== undefined) {
|
|
res[propName] = func(searchProvider.hierarchy, { _class: res?._class, [propName]: v })
|
|
continue
|
|
}
|
|
}
|
|
res[propName] = v
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return res
|
|
}
|
|
|
|
function findSearchPresenter (hierarchy: Hierarchy, _class: Ref<Class<Doc>>): SearchPresenter | undefined {
|
|
const ancestors = hierarchy.getAncestors(_class).reverse()
|
|
for (const _class of ancestors) {
|
|
const searchMixin = hierarchy.classHierarchyMixin(_class, plugin.mixin.SearchPresenter)
|
|
if (searchMixin !== undefined) {
|
|
return searchMixin
|
|
}
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
/**
|
|
* @public
|
|
*/
|
|
export async function updateDocWithPresenter (
|
|
hierarchy: Hierarchy,
|
|
doc: DocIndexState,
|
|
elasticDoc: IndexedDoc,
|
|
refDocs: {
|
|
parentDoc: DocIndexState | undefined
|
|
spaceDoc: DocIndexState | undefined
|
|
},
|
|
branding: Branding | null
|
|
): Promise<void> {
|
|
const searchPresenter = findSearchPresenter(hierarchy, doc.objectClass)
|
|
if (searchPresenter === undefined) {
|
|
return
|
|
}
|
|
|
|
const reader = createIndexedReader(doc.objectClass, hierarchy, doc, {
|
|
space: refDocs.spaceDoc,
|
|
attachedTo: refDocs.parentDoc
|
|
})
|
|
|
|
const props = [
|
|
{
|
|
name: 'searchTitle',
|
|
config: searchPresenter.searchConfig.title,
|
|
provider: searchPresenter.getSearchTitle
|
|
}
|
|
] as any[]
|
|
|
|
if (searchPresenter.searchConfig.shortTitle !== undefined) {
|
|
props.push({
|
|
name: 'searchShortTitle',
|
|
config: searchPresenter.searchConfig.shortTitle,
|
|
provider: searchPresenter.getSearchShortTitle,
|
|
lastNameFirst: branding?.lastNameFirst
|
|
})
|
|
}
|
|
|
|
if (searchPresenter.searchConfig.iconConfig !== undefined) {
|
|
props.push({
|
|
name: 'searchIcon',
|
|
config: searchPresenter.searchConfig.iconConfig
|
|
})
|
|
}
|
|
|
|
for (const prop of props) {
|
|
let value
|
|
if (prop.config.tmpl !== undefined) {
|
|
const tmpl = prop.config.tmpl
|
|
const renderProps = await readAndMapProps(reader, prop.config.props, { hierarchy, providers: prop.provider })
|
|
value = fillTemplate(tmpl, renderProps)
|
|
} else if (typeof prop.config === 'string') {
|
|
value = reader.get(prop.config)
|
|
} else if (prop.provider !== undefined) {
|
|
const func = await getResource(Object.values(prop.provider)[0] as Resource<any>)
|
|
const renderProps = await readAndMapProps(reader, prop.config.props)
|
|
value = func(hierarchy, { _class: doc.objectClass, ...renderProps })
|
|
} else if (prop.name === 'searchIcon') {
|
|
value = await readAndMapProps(reader, prop.config.props)
|
|
}
|
|
elasticDoc[prop.name] = value
|
|
}
|
|
}
|
|
|
|
export function getScoringConfig (hierarchy: Hierarchy, classes: Ref<Class<Doc>>[]): SearchScoring[] {
|
|
let results: SearchScoring[] = []
|
|
for (const _class of classes) {
|
|
const searchPresenter = findSearchPresenter(hierarchy, _class)
|
|
if (searchPresenter?.searchConfig.scoring !== undefined) {
|
|
results = results.concat(searchPresenter?.searchConfig.scoring)
|
|
}
|
|
}
|
|
return results
|
|
}
|
|
|
|
/**
|
|
* @public
|
|
*/
|
|
export function mapSearchResultDoc (hierarchy: Hierarchy, raw: IndexedDoc): SearchResultDoc {
|
|
const doc: SearchResultDoc = {
|
|
id: raw.id,
|
|
title: raw.searchTitle,
|
|
shortTitle: raw.searchShortTitle,
|
|
iconProps: raw.searchIcon,
|
|
doc: {
|
|
_id: raw.id,
|
|
_class: raw._class[0]
|
|
},
|
|
score: raw._score
|
|
}
|
|
|
|
const searchPresenter = findSearchPresenter(hierarchy, doc.doc._class)
|
|
if (searchPresenter?.searchConfig.icon !== undefined) {
|
|
doc.icon = searchPresenter.searchConfig.icon
|
|
}
|
|
if (searchPresenter?.searchConfig.iconConfig !== undefined) {
|
|
doc.iconComponent = searchPresenter.searchConfig.iconConfig.component
|
|
}
|
|
|
|
return doc
|
|
}
|
|
|
|
function fillTemplate (tmpl: string, props: Record<string, any>): string {
|
|
return tmpl.replace(/{(.*?)}/g, (_, key: string) => props[key])
|
|
}
|