mirror of
https://github.com/hcengineering/platform.git
synced 2025-02-20 19:22:10 +00:00
700 lines
23 KiB
TypeScript
700 lines
23 KiB
TypeScript
//
|
||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||
// Copyright © 2021 Hardcore Engineering Inc.
|
||
//
|
||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||
// you may not use this file except in compliance with the License. You may
|
||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||
//
|
||
// Unless required by applicable law or agreed to in writing, software
|
||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
//
|
||
// See the License for the specific language governing permissions and
|
||
// limitations under the License.
|
||
//
|
||
|
||
import core, {
|
||
AttachedDoc,
|
||
Class,
|
||
Client,
|
||
Collection,
|
||
Doc,
|
||
DocumentUpdate,
|
||
Hierarchy,
|
||
Lookup,
|
||
Obj,
|
||
Ref,
|
||
RefTo,
|
||
ReverseLookup,
|
||
ReverseLookups,
|
||
Space,
|
||
TxOperations
|
||
} from '@hcengineering/core'
|
||
import type { IntlString } from '@hcengineering/platform'
|
||
import { getResource } from '@hcengineering/platform'
|
||
import { AttributeCategory, getAttributePresenterClass, KeyedAttribute } from '@hcengineering/presentation'
|
||
import {
|
||
AnyComponent,
|
||
ErrorPresenter,
|
||
getCurrentLocation,
|
||
getPanelURI,
|
||
getPlatformColorForText,
|
||
Location,
|
||
locationToUrl
|
||
} from '@hcengineering/ui'
|
||
import type { BuildModelOptions, Viewlet } from '@hcengineering/view'
|
||
import view, { AttributeModel, BuildModelKey } from '@hcengineering/view'
|
||
import { writable } from 'svelte/store'
|
||
import plugin from './plugin'
|
||
import { noCategory } from './viewOptions'
|
||
|
||
/**
|
||
* Define some properties to be used to show component until data is properly loaded.
|
||
*/
|
||
export interface LoadingProps {
|
||
length: number
|
||
}
|
||
|
||
/**
|
||
* @public
|
||
*/
|
||
export async function getObjectPresenter (
|
||
client: Client,
|
||
_class: Ref<Class<Obj>>,
|
||
preserveKey: BuildModelKey,
|
||
isCollectionAttr: boolean = false
|
||
): Promise<AttributeModel> {
|
||
const hierarchy = client.getHierarchy()
|
||
const mixin = isCollectionAttr ? view.mixin.CollectionPresenter : view.mixin.ObjectPresenter
|
||
const clazz = hierarchy.getClass(_class)
|
||
let mixinClazz = hierarchy.getClass(_class)
|
||
let presenterMixin = hierarchy.as(clazz, mixin)
|
||
while (presenterMixin.presenter === undefined && mixinClazz.extends !== undefined) {
|
||
presenterMixin = hierarchy.as(mixinClazz, mixin)
|
||
mixinClazz = hierarchy.getClass(mixinClazz.extends)
|
||
}
|
||
if (presenterMixin.presenter === undefined) {
|
||
throw new Error(
|
||
`object presenter not found for class=${_class}, mixin=${mixin}, preserve key ${JSON.stringify(preserveKey)}`
|
||
)
|
||
}
|
||
const presenter = await getResource(presenterMixin.presenter)
|
||
const key = preserveKey.sortingKey ?? preserveKey.key
|
||
const sortingKey = Array.isArray(key)
|
||
? key
|
||
: clazz.sortingKey !== undefined
|
||
? key.length > 0
|
||
? key + '.' + clazz.sortingKey
|
||
: clazz.sortingKey
|
||
: key
|
||
return {
|
||
key: preserveKey.key,
|
||
_class,
|
||
label: preserveKey.label ?? clazz.label,
|
||
presenter,
|
||
props: preserveKey.props,
|
||
sortingKey,
|
||
collectionAttr: isCollectionAttr,
|
||
isLookup: false
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @public
|
||
*/
|
||
export async function getListItemPresenter (client: Client, _class: Ref<Class<Obj>>): Promise<AnyComponent | undefined> {
|
||
const clazz = client.getHierarchy().getClass(_class)
|
||
const presenterMixin = client.getHierarchy().as(clazz, view.mixin.ListItemPresenter)
|
||
if (presenterMixin.presenter === undefined) {
|
||
if (clazz.extends !== undefined) {
|
||
return await getListItemPresenter(client, clazz.extends)
|
||
}
|
||
}
|
||
return presenterMixin?.presenter
|
||
}
|
||
|
||
/**
|
||
* @public
|
||
*/
|
||
export async function getObjectPreview (client: Client, _class: Ref<Class<Obj>>): Promise<AnyComponent | undefined> {
|
||
const clazz = client.getHierarchy().getClass(_class)
|
||
const presenterMixin = client.getHierarchy().as(clazz, view.mixin.PreviewPresenter)
|
||
if (presenterMixin.presenter === undefined) {
|
||
if (clazz.extends !== undefined) {
|
||
return await getObjectPreview(client, clazz.extends)
|
||
}
|
||
}
|
||
return presenterMixin?.presenter
|
||
}
|
||
|
||
async function getAttributePresenter (
|
||
client: Client,
|
||
_class: Ref<Class<Obj>>,
|
||
key: string,
|
||
preserveKey: BuildModelKey
|
||
): Promise<AttributeModel> {
|
||
const hierarchy = client.getHierarchy()
|
||
const attribute = hierarchy.getAttribute(_class, key)
|
||
const presenterClass = getAttributePresenterClass(hierarchy, attribute)
|
||
const isCollectionAttr = presenterClass.category === 'collection'
|
||
const mixin = isCollectionAttr ? view.mixin.CollectionPresenter : view.mixin.AttributePresenter
|
||
const clazz = hierarchy.getClass(presenterClass.attrClass)
|
||
let presenterMixin = hierarchy.as(clazz, mixin)
|
||
let parent = clazz.extends
|
||
while (presenterMixin.presenter === undefined && parent !== undefined) {
|
||
const pclazz = hierarchy.getClass(parent)
|
||
presenterClass.attrClass = parent
|
||
presenterMixin = hierarchy.as(pclazz, mixin)
|
||
parent = pclazz.extends
|
||
}
|
||
if (presenterMixin.presenter === undefined) {
|
||
throw new Error('attribute presenter not found for ' + JSON.stringify(preserveKey))
|
||
}
|
||
const resultKey = preserveKey.sortingKey ?? preserveKey.key
|
||
const sortingKey = Array.isArray(resultKey)
|
||
? resultKey
|
||
: attribute.type._class === core.class.ArrOf
|
||
? resultKey + '.length'
|
||
: resultKey
|
||
const presenter = await getResource(presenterMixin.presenter)
|
||
|
||
return {
|
||
key: preserveKey.key,
|
||
sortingKey,
|
||
_class: presenterClass.attrClass,
|
||
label: preserveKey.label ?? attribute.shortLabel ?? attribute.label,
|
||
presenter,
|
||
props: preserveKey.props,
|
||
icon: presenterMixin.icon,
|
||
attribute,
|
||
collectionAttr: isCollectionAttr,
|
||
isLookup: false
|
||
}
|
||
}
|
||
|
||
export async function getPresenter<T extends Doc> (
|
||
client: Client,
|
||
_class: Ref<Class<T>>,
|
||
key: BuildModelKey,
|
||
preserveKey: BuildModelKey,
|
||
lookup?: Lookup<T>,
|
||
isCollectionAttr: boolean = false
|
||
): Promise<AttributeModel> {
|
||
if (key.presenter !== undefined) {
|
||
const { presenter, label, sortingKey } = key
|
||
return {
|
||
key: key.key ?? '',
|
||
sortingKey: sortingKey ?? '',
|
||
_class,
|
||
label: label as IntlString,
|
||
presenter: typeof presenter === 'string' ? await getResource(presenter) : presenter,
|
||
props: preserveKey.props,
|
||
collectionAttr: isCollectionAttr,
|
||
isLookup: false
|
||
}
|
||
}
|
||
if (key.key.length === 0) {
|
||
return await getObjectPresenter(client, _class, preserveKey, isCollectionAttr)
|
||
} else {
|
||
if (key.key.startsWith('$lookup')) {
|
||
if (lookup === undefined) {
|
||
throw new Error(`lookup class does not provided for ${key.key}`)
|
||
}
|
||
return await getLookupPresenter(client, _class, key, preserveKey, lookup)
|
||
}
|
||
return await getAttributePresenter(client, _class, key.key, preserveKey)
|
||
}
|
||
}
|
||
|
||
function getKeyLookup<T extends Doc> (
|
||
hierarchy: Hierarchy,
|
||
_class: Ref<Class<T>>,
|
||
key: string,
|
||
lookup: Lookup<T>,
|
||
lastIndex: number = 1
|
||
): Lookup<T> {
|
||
if (!key.startsWith('$lookup')) return lookup
|
||
const parts = key.split('.')
|
||
const attrib = parts[1]
|
||
const attribute = hierarchy.getAttribute(_class, attrib)
|
||
if (hierarchy.isDerived(attribute.type._class, core.class.RefTo)) {
|
||
const lookupClass = (attribute.type as RefTo<Doc>).to
|
||
const index = key.indexOf('$lookup', lastIndex)
|
||
if (index === -1) {
|
||
if ((lookup as any)[attrib] === undefined) {
|
||
;(lookup as any)[attrib] = lookupClass
|
||
}
|
||
} else {
|
||
let nested = Array.isArray((lookup as any)[attrib]) ? (lookup as any)[attrib][1] : {}
|
||
nested = getKeyLookup(hierarchy, lookupClass, key.slice(index), nested)
|
||
;(lookup as any)[attrib] = [lookupClass, nested]
|
||
}
|
||
} else if (hierarchy.isDerived(attribute.type._class, core.class.Collection)) {
|
||
if ((lookup as any)._id === undefined) {
|
||
;(lookup as any)._id = {}
|
||
}
|
||
;(lookup as any)._id[attrib] = (attribute.type as Collection<AttachedDoc>).of
|
||
}
|
||
return lookup
|
||
}
|
||
|
||
export function buildConfigLookup<T extends Doc> (
|
||
hierarchy: Hierarchy,
|
||
_class: Ref<Class<T>>,
|
||
config: Array<BuildModelKey | string>,
|
||
existingLookup?: Lookup<T>
|
||
): Lookup<T> {
|
||
let res: Lookup<T> = {}
|
||
for (const key of config) {
|
||
if (typeof key === 'string') {
|
||
res = getKeyLookup(hierarchy, _class, key, res)
|
||
} else {
|
||
res = getKeyLookup(hierarchy, _class, key.key, res)
|
||
}
|
||
}
|
||
if (existingLookup !== undefined) {
|
||
// Let's merg
|
||
const _id: ReverseLookup = {
|
||
...((existingLookup as ReverseLookups)._id ?? {}),
|
||
...((res as ReverseLookups)._id ?? {})
|
||
}
|
||
res = { ...existingLookup, ...res, _id }
|
||
}
|
||
return res
|
||
}
|
||
|
||
export async function buildModel (options: BuildModelOptions): Promise<AttributeModel[]> {
|
||
// eslint-disable-next-line array-callback-return
|
||
const model = options.keys
|
||
.map((key) => (typeof key === 'string' ? { key } : key))
|
||
.map(async (key) => {
|
||
try {
|
||
// Check if it is a mixin attribute configuration
|
||
const pos = key.key.lastIndexOf('.')
|
||
if (pos !== -1) {
|
||
const mixinName = key.key.substring(0, pos) as Ref<Class<Doc>>
|
||
if (!mixinName.includes('$lookup')) {
|
||
const realKey = key.key.substring(pos + 1)
|
||
const rkey = { ...key, key: realKey }
|
||
return {
|
||
...(await getPresenter(options.client, mixinName, rkey, rkey, options.lookup)),
|
||
castRequest: mixinName,
|
||
key: key.key,
|
||
sortingKey: key.key
|
||
}
|
||
}
|
||
}
|
||
return await getPresenter(options.client, options._class, key, key, options.lookup)
|
||
} catch (err: any) {
|
||
if (options.ignoreMissing ?? false) {
|
||
return undefined
|
||
}
|
||
const stringKey = key.label ?? key.key
|
||
console.error('Failed to find presenter for', key, err)
|
||
const errorPresenter: AttributeModel = {
|
||
key: '',
|
||
sortingKey: '',
|
||
presenter: ErrorPresenter,
|
||
label: stringKey as IntlString,
|
||
_class: core.class.TypeString,
|
||
props: { error: err },
|
||
collectionAttr: false,
|
||
isLookup: false
|
||
}
|
||
return errorPresenter
|
||
}
|
||
})
|
||
return (await Promise.all(model)).filter((a) => a !== undefined) as AttributeModel[]
|
||
}
|
||
|
||
export async function deleteObject (client: TxOperations, object: Doc): Promise<void> {
|
||
if (client.getHierarchy().isDerived(object._class, core.class.AttachedDoc)) {
|
||
const adoc = object as AttachedDoc
|
||
await client
|
||
.removeCollection(object._class, object.space, adoc._id, adoc.attachedTo, adoc.attachedToClass, adoc.collection)
|
||
.catch((err) => console.error(err))
|
||
} else {
|
||
await client.removeDoc(object._class, object.space, object._id).catch((err) => console.error(err))
|
||
}
|
||
}
|
||
|
||
export async function deleteObjects (client: TxOperations, objects: Doc[]): Promise<void> {
|
||
const ops = client.apply('delete')
|
||
for (const object of objects) {
|
||
if (client.getHierarchy().isDerived(object._class, core.class.AttachedDoc)) {
|
||
const adoc = object as AttachedDoc
|
||
await ops
|
||
.removeCollection(object._class, object.space, adoc._id, adoc.attachedTo, adoc.attachedToClass, adoc.collection)
|
||
.catch((err) => console.error(err))
|
||
} else {
|
||
await ops.removeDoc(object._class, object.space, object._id).catch((err) => console.error(err))
|
||
}
|
||
}
|
||
await ops.commit()
|
||
}
|
||
|
||
export function getMixinStyle (id: Ref<Class<Doc>>, selected: boolean): string {
|
||
const color = getPlatformColorForText(id as string)
|
||
return `
|
||
color: ${selected ? '#fff' : 'var(--caption-color)'};
|
||
background: ${color + (selected ? 'ff' : '33')};
|
||
border: 1px solid ${color + (selected ? '0f' : '66')};
|
||
`
|
||
}
|
||
|
||
async function getLookupPresenter<T extends Doc> (
|
||
client: Client,
|
||
_class: Ref<Class<T>>,
|
||
key: BuildModelKey,
|
||
preserveKey: BuildModelKey,
|
||
lookup: Lookup<T>
|
||
): Promise<AttributeModel> {
|
||
const lookupClass = getLookupClass(key.key, lookup, _class)
|
||
const lookupProperty = getLookupProperty(key.key)
|
||
const lookupKey = { ...key, key: lookupProperty[0] }
|
||
const model = await getPresenter(client, lookupClass[0], lookupKey, preserveKey, undefined, lookupClass[2])
|
||
model.label = getLookupLabel(client, lookupClass[1], lookupClass[0], lookupKey, lookupProperty[1])
|
||
model.isLookup = true
|
||
return model
|
||
}
|
||
|
||
export function getLookupLabel<T extends Doc> (
|
||
client: Client,
|
||
_class: Ref<Class<T>>,
|
||
lookupClass: Ref<Class<Doc>>,
|
||
key: BuildModelKey,
|
||
attrib: string
|
||
): IntlString {
|
||
if (key.label !== undefined) return key.label
|
||
if (key.key === '') {
|
||
try {
|
||
const attribute = client.getHierarchy().getAttribute(_class, attrib)
|
||
return attribute.label
|
||
} catch {}
|
||
const clazz = client.getHierarchy().getClass(lookupClass)
|
||
return clazz.label
|
||
} else {
|
||
const attribute = client.getHierarchy().getAttribute(lookupClass, key.key)
|
||
return attribute.label
|
||
}
|
||
}
|
||
|
||
export function getLookupClass<T extends Doc> (
|
||
key: string,
|
||
lookup: Lookup<T>,
|
||
parent: Ref<Class<T>>
|
||
): [Ref<Class<Doc>>, Ref<Class<Doc>>, boolean] {
|
||
const _class = getLookup(key, lookup, parent)
|
||
if (_class === undefined) {
|
||
throw new Error('lookup class does not provided for ' + key)
|
||
}
|
||
return _class
|
||
}
|
||
|
||
export function getLookupProperty (key: string): [string, string] {
|
||
const parts = key.split('$lookup')
|
||
const lastPart = parts[parts.length - 1]
|
||
const split = lastPart.split('.').filter((p) => p.length > 0)
|
||
const prev = split.shift() ?? ''
|
||
const result = split.join('.')
|
||
return [result, prev]
|
||
}
|
||
|
||
function getLookup (
|
||
key: string,
|
||
lookup: Lookup<any>,
|
||
parent: Ref<Class<Doc>>
|
||
): [Ref<Class<Doc>>, Ref<Class<Doc>>, boolean] | undefined {
|
||
const parts = key.split('$lookup.').filter((p) => p.length > 0)
|
||
const currentKey = parts[0].split('.').filter((p) => p.length > 0)[0]
|
||
const current = (lookup as any)[currentKey]
|
||
const nestedKey = parts.slice(1).join('$lookup.')
|
||
if (nestedKey.length > 0) {
|
||
if (!Array.isArray(current)) {
|
||
return
|
||
}
|
||
return getLookup(nestedKey, current[1], current[0])
|
||
}
|
||
if (Array.isArray(current)) {
|
||
return [current[0], parent, false]
|
||
}
|
||
if (current === undefined && lookup._id !== undefined) {
|
||
const reverse = (lookup._id as any)[currentKey]
|
||
return reverse !== undefined
|
||
? Array.isArray(reverse)
|
||
? [reverse[0], parent, true]
|
||
: [reverse, parent, true]
|
||
: undefined
|
||
}
|
||
return current !== undefined ? [current, parent, false] : undefined
|
||
}
|
||
|
||
export function getBooleanLabel (value: boolean | undefined): IntlString {
|
||
if (value === true) return plugin.string.LabelYes
|
||
if (value === false) return plugin.string.LabelNo
|
||
return plugin.string.LabelNA
|
||
}
|
||
export function getCollectionCounter (hierarchy: Hierarchy, object: Doc, key: KeyedAttribute): number {
|
||
if (hierarchy.isMixin(key.attr.attributeOf)) {
|
||
return (hierarchy.as(object, key.attr.attributeOf) as any)[key.key]
|
||
}
|
||
return (object as any)[key.key] ?? 0
|
||
}
|
||
|
||
function filterKeys (hierarchy: Hierarchy, keys: KeyedAttribute[], ignoreKeys: string[]): KeyedAttribute[] {
|
||
const docKeys: Set<string> = new Set<string>(hierarchy.getAllAttributes(core.class.AttachedDoc).keys())
|
||
keys = keys.filter((k) => !docKeys.has(k.key))
|
||
keys = keys.filter((k) => !ignoreKeys.includes(k.key))
|
||
return keys
|
||
}
|
||
|
||
export function getFiltredKeys (
|
||
hierarchy: Hierarchy,
|
||
objectClass: Ref<Class<Doc>>,
|
||
ignoreKeys: string[],
|
||
to?: Ref<Class<Doc>>
|
||
): KeyedAttribute[] {
|
||
const keys = [...hierarchy.getAllAttributes(objectClass, to).entries()]
|
||
.filter(([, value]) => value.hidden !== true)
|
||
.map(([key, attr]) => ({ key, attr }))
|
||
|
||
return filterKeys(hierarchy, keys, ignoreKeys)
|
||
}
|
||
|
||
export interface CategoryKey {
|
||
key: KeyedAttribute
|
||
category: AttributeCategory
|
||
}
|
||
|
||
export function categorizeFields (
|
||
hierarchy: Hierarchy,
|
||
keys: KeyedAttribute[],
|
||
useAsCollection: string[],
|
||
useAsAttribute: string[]
|
||
): {
|
||
attributes: CategoryKey[]
|
||
collections: CategoryKey[]
|
||
} {
|
||
const result = {
|
||
attributes: [] as CategoryKey[],
|
||
collections: [] as CategoryKey[]
|
||
}
|
||
|
||
for (const key of keys) {
|
||
const cl = getAttributePresenterClass(hierarchy, key.attr)
|
||
if (useAsCollection.includes(key.key)) {
|
||
result.collections.push({ key, category: cl.category })
|
||
} else if (useAsAttribute.includes(key.key)) {
|
||
result.attributes.push({ key, category: cl.category })
|
||
} else if (cl.category === 'collection' || cl.category === 'inplace') {
|
||
result.collections.push({ key, category: cl.category })
|
||
} else if (cl.category === 'array') {
|
||
const attrClass = getAttributePresenterClass(hierarchy, key.attr)
|
||
const clazz = hierarchy.getClass(attrClass.attrClass)
|
||
const mix = hierarchy.as(clazz, view.mixin.ArrayEditor)
|
||
if (mix.editor !== undefined && mix.inlineEditor === undefined) {
|
||
result.collections.push({ key, category: cl.category })
|
||
} else {
|
||
result.attributes.push({ key, category: cl.category })
|
||
}
|
||
} else {
|
||
result.attributes.push({ key, category: cl.category })
|
||
}
|
||
}
|
||
return result
|
||
}
|
||
|
||
export function isCollectionAttr (hierarchy: Hierarchy, key: KeyedAttribute): boolean {
|
||
return hierarchy.isDerived(key.attr.type._class, core.class.Collection)
|
||
}
|
||
|
||
function makeViewletKey (loc?: Location): string {
|
||
loc = loc ?? getCurrentLocation()
|
||
loc.fragment = undefined
|
||
loc.query = undefined
|
||
return 'viewlet' + locationToUrl(loc)
|
||
}
|
||
|
||
export function setActiveViewletId (viewletId: Ref<Viewlet> | null, loc?: Location): void {
|
||
const key = makeViewletKey(loc)
|
||
if (viewletId !== null && viewletId !== undefined) {
|
||
localStorage.setItem(key, viewletId)
|
||
} else {
|
||
localStorage.removeItem(key)
|
||
}
|
||
}
|
||
|
||
export function getActiveViewletId (): Ref<Viewlet> | null {
|
||
const key = makeViewletKey()
|
||
return localStorage.getItem(key) as Ref<Viewlet> | null
|
||
}
|
||
|
||
export type FixedWidthStore = Record<string, number>
|
||
|
||
export const fixedWidthStore = writable<FixedWidthStore>({})
|
||
|
||
export function groupBy<T extends Doc> (docs: T[], key: string): { [key: string]: T[] } {
|
||
return docs.reduce((storage: { [key: string]: T[] }, item: T) => {
|
||
const group = (item as any)[key] ?? undefined
|
||
|
||
storage[group] = storage[group] ?? []
|
||
|
||
storage[group].push(item)
|
||
|
||
return storage
|
||
}, {})
|
||
}
|
||
|
||
export async function getCategories (
|
||
client: TxOperations,
|
||
_class: Ref<Class<Doc>>,
|
||
docs: Doc[],
|
||
key: string
|
||
): Promise<any[]> {
|
||
if (key === noCategory) return [undefined]
|
||
const hierarchy = client.getHierarchy()
|
||
const existingCategories = Array.from(new Set(docs.map((x: any) => x[key] ?? undefined)))
|
||
const attr = hierarchy.getAttribute(_class, key)
|
||
if (attr === undefined) return existingCategories
|
||
const attrClass = getAttributePresenterClass(hierarchy, attr).attrClass
|
||
const clazz = hierarchy.getClass(attrClass)
|
||
const sortFunc = hierarchy.as(clazz, view.mixin.SortFuncs)
|
||
if (sortFunc?.func === undefined) return existingCategories
|
||
const f = await getResource(sortFunc.func)
|
||
|
||
return await f(existingCategories)
|
||
}
|
||
|
||
export function getKeyLabel<T extends Doc> (
|
||
client: TxOperations,
|
||
_class: Ref<Class<T>>,
|
||
key: string,
|
||
lookup: Lookup<T> | undefined
|
||
): IntlString {
|
||
if (key.startsWith('$lookup') && lookup !== undefined) {
|
||
const lookupClass = getLookupClass(key, lookup, _class)
|
||
const lookupProperty = getLookupProperty(key)
|
||
const lookupKey = { key: lookupProperty[0] }
|
||
return getLookupLabel(client, lookupClass[1], lookupClass[0], lookupKey, lookupProperty[1])
|
||
} else {
|
||
const attribute = client.getHierarchy().getAttribute(_class, key)
|
||
return attribute.label
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @public
|
||
* Implemenation of cosice similarity
|
||
*/
|
||
export function cosinesim (A: number[], B: number[]): number {
|
||
let dotproduct = 0
|
||
let mA = 0
|
||
let mB = 0
|
||
for (let i = 0; i < A.length; i++) {
|
||
dotproduct += A[i] * B[i]
|
||
mA += A[i] * A[i]
|
||
mB += B[i] * B[i]
|
||
}
|
||
mA = Math.sqrt(mA)
|
||
mB = Math.sqrt(mB)
|
||
const similarity = dotproduct / (mA * mB) // here you needed extra brackets
|
||
return similarity
|
||
}
|
||
|
||
/**
|
||
* Calculate Sørensen–Dice coefficient
|
||
*/
|
||
export function calcSørensenDiceCoefficient (a: string, b: string): number {
|
||
const first = a.replace(/\s+/g, '')
|
||
const second = b.replace(/\s+/g, '')
|
||
|
||
if (first === second) return 1 // identical or empty
|
||
if (first.length < 2 || second.length < 2) return 0 // if either is a 0-letter or 1-letter string
|
||
|
||
const firstBigrams = new Map<string, number>()
|
||
for (let i = 0; i < first.length - 1; i++) {
|
||
const bigram = first.substring(i, i + 2)
|
||
const count = (firstBigrams.get(bigram) ?? 0) + 1
|
||
|
||
firstBigrams.set(bigram, count)
|
||
}
|
||
|
||
let intersectionSize = 0
|
||
for (let i = 0; i < second.length - 1; i++) {
|
||
const bigram = second.substring(i, i + 2)
|
||
const count = firstBigrams.get(bigram) ?? 0
|
||
|
||
if (count > 0) {
|
||
firstBigrams.set(bigram, count - 1)
|
||
intersectionSize++
|
||
}
|
||
}
|
||
|
||
return (2.0 * intersectionSize) / (first.length + second.length - 2)
|
||
}
|
||
|
||
/**
|
||
* @public
|
||
*/
|
||
export async function moveToSpace (
|
||
client: TxOperations,
|
||
doc: Doc,
|
||
space: Ref<Space>,
|
||
extra?: DocumentUpdate<any>
|
||
): Promise<void> {
|
||
const hierarchy = client.getHierarchy()
|
||
const attributes = hierarchy.getAllAttributes(doc._class)
|
||
for (const [name, attribute] of attributes) {
|
||
if (hierarchy.isDerived(attribute.type._class, core.class.Collection)) {
|
||
const collection = attribute.type as Collection<AttachedDoc>
|
||
const allAttached = await client.findAll(collection.of, { attachedTo: doc._id })
|
||
for (const attached of allAttached) {
|
||
// Do not use extra for childs.
|
||
await moveToSpace(client, attached, space).catch((err) => console.log('failed to move', name, err))
|
||
}
|
||
}
|
||
}
|
||
await client.update(doc, {
|
||
space,
|
||
...extra
|
||
})
|
||
}
|
||
|
||
/**
|
||
* @public
|
||
*/
|
||
export function getAdditionalHeader (client: TxOperations, _class: Ref<Class<Doc>>): AnyComponent[] | undefined {
|
||
const hierarchy = client.getHierarchy()
|
||
const clazz = hierarchy.getClass(_class)
|
||
let mixinClazz = hierarchy.getClass(_class)
|
||
let presenterMixin = hierarchy.as(clazz, view.mixin.ListHeaderExtra)
|
||
while (presenterMixin.presenters === undefined && mixinClazz.extends !== undefined) {
|
||
presenterMixin = hierarchy.as(mixinClazz, view.mixin.ListHeaderExtra)
|
||
mixinClazz = hierarchy.getClass(mixinClazz.extends)
|
||
}
|
||
return presenterMixin.presenters
|
||
}
|
||
|
||
export async function getObjectLinkFragment (
|
||
hierarchy: Hierarchy,
|
||
object: Doc,
|
||
props: Record<string, any> = {},
|
||
component: AnyComponent = view.component.EditDoc
|
||
): Promise<string> {
|
||
let clazz = hierarchy.getClass(object._class)
|
||
let provider = hierarchy.as(clazz, view.mixin.LinkProvider)
|
||
while (provider.encode === undefined && clazz.extends !== undefined) {
|
||
clazz = hierarchy.getClass(clazz.extends)
|
||
provider = hierarchy.as(clazz, view.mixin.LinkProvider)
|
||
}
|
||
if (provider?.encode !== undefined) {
|
||
const f = await getResource(provider.encode)
|
||
const res = await f(object, props)
|
||
if (res !== undefined) {
|
||
return res
|
||
}
|
||
}
|
||
return getPanelURI(component, object._id, Hierarchy.mixinOrClass(object), 'content')
|
||
}
|