platform/plugins/view-resources/src/utils.ts

700 lines
23 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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ørensenDice 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')
}