2021-08-07 14:49:14 +00:00
|
|
|
|
//
|
|
|
|
|
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
|
|
|
|
// Copyright © 2021 Hardcore Engineering Inc.
|
2021-12-03 10:16:16 +00:00
|
|
|
|
//
|
2021-08-07 14:49:14 +00:00
|
|
|
|
// 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
|
2021-12-03 10:16:16 +00:00
|
|
|
|
//
|
2021-08-07 14:49:14 +00:00
|
|
|
|
// 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.
|
2021-12-03 10:16:16 +00:00
|
|
|
|
//
|
2021-08-07 14:49:14 +00:00
|
|
|
|
// See the License for the specific language governing permissions and
|
|
|
|
|
// limitations under the License.
|
|
|
|
|
//
|
|
|
|
|
|
2022-05-13 06:38:54 +00:00
|
|
|
|
import core, {
|
2023-04-17 06:08:41 +00:00
|
|
|
|
AccountRole,
|
2022-05-13 06:38:54 +00:00
|
|
|
|
AttachedDoc,
|
2023-04-04 06:11:49 +00:00
|
|
|
|
CategoryType,
|
2022-05-13 06:38:54 +00:00
|
|
|
|
Class,
|
|
|
|
|
Client,
|
|
|
|
|
Collection,
|
|
|
|
|
Doc,
|
2023-02-07 11:15:59 +00:00
|
|
|
|
DocumentUpdate,
|
2023-04-17 06:08:41 +00:00
|
|
|
|
getCurrentAccount,
|
2023-04-04 06:11:49 +00:00
|
|
|
|
getObjectValue,
|
2022-05-13 06:38:54 +00:00
|
|
|
|
Hierarchy,
|
|
|
|
|
Lookup,
|
|
|
|
|
Obj,
|
|
|
|
|
Ref,
|
|
|
|
|
RefTo,
|
2023-01-29 05:44:12 +00:00
|
|
|
|
ReverseLookup,
|
2023-02-07 11:15:59 +00:00
|
|
|
|
ReverseLookups,
|
|
|
|
|
Space,
|
2023-04-04 06:11:49 +00:00
|
|
|
|
Status,
|
|
|
|
|
StatusManager,
|
|
|
|
|
StatusValue,
|
2023-04-20 05:38:46 +00:00
|
|
|
|
TxOperations,
|
|
|
|
|
WithLookup
|
2022-09-21 08:08:25 +00:00
|
|
|
|
} from '@hcengineering/core'
|
|
|
|
|
import type { IntlString } from '@hcengineering/platform'
|
|
|
|
|
import { getResource } from '@hcengineering/platform'
|
2023-04-25 07:34:10 +00:00
|
|
|
|
import {
|
|
|
|
|
AttributeCategory,
|
|
|
|
|
createQuery,
|
|
|
|
|
getAttributePresenterClass,
|
|
|
|
|
hasResource,
|
|
|
|
|
KeyedAttribute
|
|
|
|
|
} from '@hcengineering/presentation'
|
2022-09-21 08:08:25 +00:00
|
|
|
|
import {
|
|
|
|
|
AnyComponent,
|
|
|
|
|
ErrorPresenter,
|
2023-04-21 16:24:45 +00:00
|
|
|
|
getCurrentResolvedLocation,
|
2023-03-15 14:06:03 +00:00
|
|
|
|
getPanelURI,
|
2022-09-21 08:08:25 +00:00
|
|
|
|
getPlatformColorForText,
|
2023-02-07 11:15:59 +00:00
|
|
|
|
Location,
|
2022-09-21 08:08:25 +00:00
|
|
|
|
locationToUrl
|
|
|
|
|
} from '@hcengineering/ui'
|
2023-03-23 05:41:27 +00:00
|
|
|
|
import type { BuildModelOptions, Viewlet, ViewletDescriptor } from '@hcengineering/view'
|
2022-09-21 08:08:25 +00:00
|
|
|
|
import view, { AttributeModel, BuildModelKey } from '@hcengineering/view'
|
2023-04-11 13:39:23 +00:00
|
|
|
|
import { get, writable } from 'svelte/store'
|
2022-02-10 09:04:18 +00:00
|
|
|
|
import plugin from './plugin'
|
2023-01-14 10:54:54 +00:00
|
|
|
|
import { noCategory } from './viewOptions'
|
2021-08-07 14:49:14 +00:00
|
|
|
|
|
2023-03-17 15:57:36 +00:00
|
|
|
|
export { getFiltredKeys, isCollectionAttr } from '@hcengineering/presentation'
|
|
|
|
|
|
2022-01-11 09:07:29 +00:00
|
|
|
|
/**
|
|
|
|
|
* Define some properties to be used to show component until data is properly loaded.
|
|
|
|
|
*/
|
|
|
|
|
export interface LoadingProps {
|
|
|
|
|
length: number
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-23 18:46:34 +00:00
|
|
|
|
/**
|
|
|
|
|
* @public
|
|
|
|
|
*/
|
2022-01-18 10:21:32 +00:00
|
|
|
|
export async function getObjectPresenter (
|
|
|
|
|
client: Client,
|
|
|
|
|
_class: Ref<Class<Obj>>,
|
2022-06-01 03:25:13 +00:00
|
|
|
|
preserveKey: BuildModelKey,
|
2023-04-25 07:34:10 +00:00
|
|
|
|
isCollectionAttr: boolean = false,
|
|
|
|
|
checkResource = false
|
2023-05-23 10:42:39 +00:00
|
|
|
|
): Promise<AttributeModel | undefined> {
|
2022-06-01 03:25:13 +00:00
|
|
|
|
const hierarchy = client.getHierarchy()
|
2023-01-14 10:54:54 +00:00
|
|
|
|
const mixin = isCollectionAttr ? view.mixin.CollectionPresenter : view.mixin.ObjectPresenter
|
2022-06-01 03:25:13 +00:00
|
|
|
|
const clazz = hierarchy.getClass(_class)
|
2023-04-25 07:34:10 +00:00
|
|
|
|
|
|
|
|
|
const presenterMixin = hierarchy.classHierarchyMixin(_class, mixin, (m) => !checkResource || hasResource(m.presenter))
|
2023-03-29 09:18:59 +00:00
|
|
|
|
if (presenterMixin?.presenter === undefined) {
|
2023-05-23 10:42:39 +00:00
|
|
|
|
console.error(
|
2022-06-02 07:59:59 +00:00
|
|
|
|
`object presenter not found for class=${_class}, mixin=${mixin}, preserve key ${JSON.stringify(preserveKey)}`
|
|
|
|
|
)
|
2023-05-23 10:42:39 +00:00
|
|
|
|
return undefined
|
2021-08-07 14:49:14 +00:00
|
|
|
|
}
|
|
|
|
|
const presenter = await getResource(presenterMixin.presenter)
|
2021-12-23 13:59:09 +00:00
|
|
|
|
const key = preserveKey.sortingKey ?? preserveKey.key
|
2022-06-29 11:46:22 +00:00
|
|
|
|
const sortingKey = Array.isArray(key)
|
|
|
|
|
? key
|
|
|
|
|
: clazz.sortingKey !== undefined
|
|
|
|
|
? key.length > 0
|
|
|
|
|
? key + '.' + clazz.sortingKey
|
|
|
|
|
: clazz.sortingKey
|
|
|
|
|
: key
|
2021-08-07 14:49:14 +00:00
|
|
|
|
return {
|
2021-12-20 10:18:29 +00:00
|
|
|
|
key: preserveKey.key,
|
2021-12-03 10:16:16 +00:00
|
|
|
|
_class,
|
2021-12-20 10:18:29 +00:00
|
|
|
|
label: preserveKey.label ?? clazz.label,
|
2021-12-20 09:06:31 +00:00
|
|
|
|
presenter,
|
2023-05-31 08:40:47 +00:00
|
|
|
|
displayProps: preserveKey.displayProps,
|
2022-04-08 18:05:49 +00:00
|
|
|
|
props: preserveKey.props,
|
2022-06-07 04:10:34 +00:00
|
|
|
|
sortingKey,
|
2023-02-07 04:52:34 +00:00
|
|
|
|
collectionAttr: isCollectionAttr,
|
|
|
|
|
isLookup: false
|
2021-12-03 10:16:16 +00:00
|
|
|
|
}
|
2021-08-07 14:49:14 +00:00
|
|
|
|
}
|
|
|
|
|
|
2022-12-20 03:26:51 +00:00
|
|
|
|
/**
|
|
|
|
|
* @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
|
|
|
|
|
}
|
|
|
|
|
|
2022-04-19 09:38:31 +00:00
|
|
|
|
/**
|
|
|
|
|
* @public
|
|
|
|
|
*/
|
2022-04-29 05:27:17 +00:00
|
|
|
|
export async function getObjectPreview (client: Client, _class: Ref<Class<Obj>>): Promise<AnyComponent | undefined> {
|
2023-04-05 07:12:06 +00:00
|
|
|
|
const hierarchy = client.getHierarchy()
|
|
|
|
|
const presenterMixin = hierarchy.classHierarchyMixin(_class, view.mixin.PreviewPresenter)
|
2022-04-19 09:38:31 +00:00
|
|
|
|
return presenterMixin?.presenter
|
|
|
|
|
}
|
|
|
|
|
|
2022-01-18 10:21:32 +00:00
|
|
|
|
async function getAttributePresenter (
|
|
|
|
|
client: Client,
|
|
|
|
|
_class: Ref<Class<Obj>>,
|
|
|
|
|
key: string,
|
|
|
|
|
preserveKey: BuildModelKey
|
|
|
|
|
): Promise<AttributeModel> {
|
2022-05-19 07:14:05 +00:00
|
|
|
|
const hierarchy = client.getHierarchy()
|
|
|
|
|
const attribute = hierarchy.getAttribute(_class, key)
|
2022-06-08 13:55:01 +00:00
|
|
|
|
const presenterClass = getAttributePresenterClass(hierarchy, attribute)
|
|
|
|
|
const isCollectionAttr = presenterClass.category === 'collection'
|
2022-06-07 04:10:34 +00:00
|
|
|
|
const mixin = isCollectionAttr ? view.mixin.CollectionPresenter : view.mixin.AttributePresenter
|
2023-03-29 09:18:59 +00:00
|
|
|
|
const presenterMixin = hierarchy.classHierarchyMixin(presenterClass.attrClass, mixin)
|
|
|
|
|
if (presenterMixin?.presenter === undefined) {
|
2021-12-08 09:09:51 +00:00
|
|
|
|
throw new Error('attribute presenter not found for ' + JSON.stringify(preserveKey))
|
2021-08-07 14:49:14 +00:00
|
|
|
|
}
|
2021-12-23 13:59:09 +00:00
|
|
|
|
const resultKey = preserveKey.sortingKey ?? preserveKey.key
|
2022-06-29 11:46:22 +00:00
|
|
|
|
const sortingKey = Array.isArray(resultKey)
|
|
|
|
|
? resultKey
|
|
|
|
|
: attribute.type._class === core.class.ArrOf
|
|
|
|
|
? resultKey + '.length'
|
|
|
|
|
: resultKey
|
2021-08-07 14:49:14 +00:00
|
|
|
|
const presenter = await getResource(presenterMixin.presenter)
|
2022-03-18 06:37:49 +00:00
|
|
|
|
|
2021-08-07 14:49:14 +00:00
|
|
|
|
return {
|
2021-12-20 10:18:29 +00:00
|
|
|
|
key: preserveKey.key,
|
2021-12-20 09:06:31 +00:00
|
|
|
|
sortingKey,
|
2022-06-08 13:55:01 +00:00
|
|
|
|
_class: presenterClass.attrClass,
|
2022-05-19 07:14:05 +00:00
|
|
|
|
label: preserveKey.label ?? attribute.shortLabel ?? attribute.label,
|
2022-01-11 09:09:52 +00:00
|
|
|
|
presenter,
|
2023-01-14 10:54:54 +00:00
|
|
|
|
props: preserveKey.props,
|
2023-05-31 08:40:47 +00:00
|
|
|
|
displayProps: preserveKey.displayProps,
|
2022-02-07 09:21:32 +00:00
|
|
|
|
icon: presenterMixin.icon,
|
2022-06-07 04:10:34 +00:00
|
|
|
|
attribute,
|
2023-02-07 04:52:34 +00:00
|
|
|
|
collectionAttr: isCollectionAttr,
|
|
|
|
|
isLookup: false
|
2021-12-03 10:16:16 +00:00
|
|
|
|
}
|
2021-08-07 14:49:14 +00:00
|
|
|
|
}
|
|
|
|
|
|
2022-05-19 06:38:12 +00:00
|
|
|
|
export async function getPresenter<T extends Doc> (
|
2022-01-18 10:21:32 +00:00
|
|
|
|
client: Client,
|
2022-01-31 09:06:30 +00:00
|
|
|
|
_class: Ref<Class<T>>,
|
2022-01-18 10:21:32 +00:00
|
|
|
|
key: BuildModelKey,
|
|
|
|
|
preserveKey: BuildModelKey,
|
2022-06-01 03:25:13 +00:00
|
|
|
|
lookup?: Lookup<T>,
|
|
|
|
|
isCollectionAttr: boolean = false
|
2022-01-18 10:21:32 +00:00
|
|
|
|
): Promise<AttributeModel> {
|
2021-12-20 10:18:29 +00:00
|
|
|
|
if (key.presenter !== undefined) {
|
2021-12-20 09:06:31 +00:00
|
|
|
|
const { presenter, label, sortingKey } = key
|
2021-09-26 11:05:17 +00:00
|
|
|
|
return {
|
2022-02-16 09:02:31 +00:00
|
|
|
|
key: key.key ?? '',
|
2021-12-20 09:06:31 +00:00
|
|
|
|
sortingKey: sortingKey ?? '',
|
2021-12-03 10:16:16 +00:00
|
|
|
|
_class,
|
2021-09-26 11:05:17 +00:00
|
|
|
|
label: label as IntlString,
|
2022-07-18 03:16:08 +00:00
|
|
|
|
presenter: typeof presenter === 'string' ? await getResource(presenter) : presenter,
|
2023-01-14 10:54:54 +00:00
|
|
|
|
props: preserveKey.props,
|
2023-05-31 08:40:47 +00:00
|
|
|
|
displayProps: preserveKey.displayProps,
|
2023-02-07 04:52:34 +00:00
|
|
|
|
collectionAttr: isCollectionAttr,
|
|
|
|
|
isLookup: false
|
2021-09-26 11:05:17 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2021-12-20 10:18:29 +00:00
|
|
|
|
if (key.key.length === 0) {
|
2023-05-23 10:42:39 +00:00
|
|
|
|
const p = await getObjectPresenter(client, _class, preserveKey, isCollectionAttr)
|
|
|
|
|
if (p === undefined) {
|
|
|
|
|
throw new Error(`object presenter not found for class=${_class}, preserve key ${JSON.stringify(preserveKey)}`)
|
|
|
|
|
}
|
|
|
|
|
return p
|
2021-08-07 14:49:14 +00:00
|
|
|
|
} else {
|
2022-01-31 09:06:30 +00:00
|
|
|
|
if (key.key.startsWith('$lookup')) {
|
|
|
|
|
if (lookup === undefined) {
|
2022-02-10 09:04:18 +00:00
|
|
|
|
throw new Error(`lookup class does not provided for ${key.key}`)
|
2021-08-07 14:49:14 +00:00
|
|
|
|
}
|
2022-01-31 09:06:30 +00:00
|
|
|
|
return await getLookupPresenter(client, _class, key, preserveKey, lookup)
|
2021-08-07 14:49:14 +00:00
|
|
|
|
}
|
2021-12-20 10:18:29 +00:00
|
|
|
|
return await getAttributePresenter(client, _class, key.key, preserveKey)
|
2021-08-07 14:49:14 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-13 06:38:54 +00:00
|
|
|
|
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) {
|
2022-06-01 12:13:50 +00:00
|
|
|
|
if ((lookup as any)[attrib] === undefined) {
|
|
|
|
|
;(lookup as any)[attrib] = lookupClass
|
|
|
|
|
}
|
2022-05-13 06:38:54 +00:00
|
|
|
|
} 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>>,
|
2023-01-29 05:44:12 +00:00
|
|
|
|
config: Array<BuildModelKey | string>,
|
|
|
|
|
existingLookup?: Lookup<T>
|
2022-05-13 06:38:54 +00:00
|
|
|
|
): 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)
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-01-29 05:44:12 +00:00
|
|
|
|
if (existingLookup !== undefined) {
|
|
|
|
|
// Let's merg
|
|
|
|
|
const _id: ReverseLookup = {
|
|
|
|
|
...((existingLookup as ReverseLookups)._id ?? {}),
|
|
|
|
|
...((res as ReverseLookups)._id ?? {})
|
|
|
|
|
}
|
|
|
|
|
res = { ...existingLookup, ...res, _id }
|
|
|
|
|
}
|
2022-05-13 06:38:54 +00:00
|
|
|
|
return res
|
|
|
|
|
}
|
|
|
|
|
|
2021-12-03 10:16:16 +00:00
|
|
|
|
export async function buildModel (options: BuildModelOptions): Promise<AttributeModel[]> {
|
|
|
|
|
// eslint-disable-next-line array-callback-return
|
2022-01-18 10:21:32 +00:00
|
|
|
|
const model = options.keys
|
2022-11-02 08:50:14 +00:00
|
|
|
|
.map((key) => (typeof key === 'string' ? { key } : key))
|
2022-01-18 10:21:32 +00:00
|
|
|
|
.map(async (key) => {
|
|
|
|
|
try {
|
2022-08-16 10:19:33 +00:00
|
|
|
|
// 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>>
|
2022-10-31 11:57:49 +00:00
|
|
|
|
if (!mixinName.includes('$lookup')) {
|
2022-08-16 10:19:33 +00:00
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-05-13 06:38:54 +00:00
|
|
|
|
return await getPresenter(options.client, options._class, key, key, options.lookup)
|
2022-01-18 10:21:32 +00:00
|
|
|
|
} 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,
|
2022-06-07 04:10:34 +00:00
|
|
|
|
props: { error: err },
|
2023-02-07 04:52:34 +00:00
|
|
|
|
collectionAttr: false,
|
|
|
|
|
isLookup: false
|
2022-01-18 10:21:32 +00:00
|
|
|
|
}
|
|
|
|
|
return errorPresenter
|
2021-11-18 12:48:05 +00:00
|
|
|
|
}
|
2022-01-18 10:21:32 +00:00
|
|
|
|
})
|
2022-06-29 11:46:22 +00:00
|
|
|
|
return (await Promise.all(model)).filter((a) => a !== undefined) as AttributeModel[]
|
2021-09-25 16:48:22 +00:00
|
|
|
|
}
|
|
|
|
|
|
2022-01-13 09:06:50 +00:00
|
|
|
|
export async function deleteObject (client: TxOperations, object: Doc): Promise<void> {
|
2023-04-17 06:08:41 +00:00
|
|
|
|
const currentAcc = getCurrentAccount()
|
|
|
|
|
if (currentAcc.role !== AccountRole.Owner && object.createdBy !== currentAcc._id) return
|
2022-03-04 09:02:13 +00:00
|
|
|
|
if (client.getHierarchy().isDerived(object._class, core.class.AttachedDoc)) {
|
|
|
|
|
const adoc = object as AttachedDoc
|
2022-04-19 09:38:31 +00:00
|
|
|
|
await client
|
|
|
|
|
.removeCollection(object._class, object.space, adoc._id, adoc.attachedTo, adoc.attachedToClass, adoc.collection)
|
|
|
|
|
.catch((err) => console.error(err))
|
2022-03-04 09:02:13 +00:00
|
|
|
|
} else {
|
2022-04-19 09:38:31 +00:00
|
|
|
|
await client.removeDoc(object._class, object.space, object._id).catch((err) => console.error(err))
|
2021-12-06 09:16:04 +00:00
|
|
|
|
}
|
2021-12-06 15:15:53 +00:00
|
|
|
|
}
|
2022-01-18 10:21:32 +00:00
|
|
|
|
|
2023-01-27 12:14:40 +00:00
|
|
|
|
export async function deleteObjects (client: TxOperations, objects: Doc[]): Promise<void> {
|
2023-04-17 06:08:41 +00:00
|
|
|
|
const currentAcc = getCurrentAccount()
|
|
|
|
|
if (currentAcc.role !== AccountRole.Owner && objects.some((p) => p.createdBy !== currentAcc._id)) return
|
2023-01-27 12:14:40 +00:00
|
|
|
|
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()
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-24 16:53:06 +00:00
|
|
|
|
export function getMixinStyle (id: Ref<Class<Doc>>, selected: boolean, black: boolean): string {
|
|
|
|
|
const color = getPlatformColorForText(id as string, black)
|
2022-01-18 10:21:32 +00:00
|
|
|
|
return `
|
2023-03-10 00:08:04 +00:00
|
|
|
|
color: ${selected ? '#fff' : 'var(--caption-color)'};
|
2022-01-18 10:21:32 +00:00
|
|
|
|
background: ${color + (selected ? 'ff' : '33')};
|
|
|
|
|
border: 1px solid ${color + (selected ? '0f' : '66')};
|
|
|
|
|
`
|
|
|
|
|
}
|
2022-01-31 09:06:30 +00:00
|
|
|
|
|
2022-04-19 09:38:31 +00:00
|
|
|
|
async function getLookupPresenter<T extends Doc> (
|
|
|
|
|
client: Client,
|
|
|
|
|
_class: Ref<Class<T>>,
|
|
|
|
|
key: BuildModelKey,
|
|
|
|
|
preserveKey: BuildModelKey,
|
|
|
|
|
lookup: Lookup<T>
|
|
|
|
|
): Promise<AttributeModel> {
|
2022-01-31 09:06:30 +00:00
|
|
|
|
const lookupClass = getLookupClass(key.key, lookup, _class)
|
|
|
|
|
const lookupProperty = getLookupProperty(key.key)
|
|
|
|
|
const lookupKey = { ...key, key: lookupProperty[0] }
|
2022-06-01 03:25:13 +00:00
|
|
|
|
const model = await getPresenter(client, lookupClass[0], lookupKey, preserveKey, undefined, lookupClass[2])
|
2022-01-31 09:06:30 +00:00
|
|
|
|
model.label = getLookupLabel(client, lookupClass[1], lookupClass[0], lookupKey, lookupProperty[1])
|
2023-02-07 04:52:34 +00:00
|
|
|
|
model.isLookup = true
|
2022-01-31 09:06:30 +00:00
|
|
|
|
return model
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-13 06:38:54 +00:00
|
|
|
|
export function getLookupLabel<T extends Doc> (
|
2022-04-19 09:38:31 +00:00
|
|
|
|
client: Client,
|
|
|
|
|
_class: Ref<Class<T>>,
|
|
|
|
|
lookupClass: Ref<Class<Doc>>,
|
|
|
|
|
key: BuildModelKey,
|
|
|
|
|
attrib: string
|
|
|
|
|
): IntlString {
|
2022-01-31 09:06:30 +00:00
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-13 06:38:54 +00:00
|
|
|
|
export function getLookupClass<T extends Doc> (
|
2022-04-19 09:38:31 +00:00
|
|
|
|
key: string,
|
|
|
|
|
lookup: Lookup<T>,
|
|
|
|
|
parent: Ref<Class<T>>
|
2022-06-01 03:25:13 +00:00
|
|
|
|
): [Ref<Class<Doc>>, Ref<Class<Doc>>, boolean] {
|
2022-01-31 09:06:30 +00:00
|
|
|
|
const _class = getLookup(key, lookup, parent)
|
|
|
|
|
if (_class === undefined) {
|
|
|
|
|
throw new Error('lookup class does not provided for ' + key)
|
|
|
|
|
}
|
|
|
|
|
return _class
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-13 06:38:54 +00:00
|
|
|
|
export function getLookupProperty (key: string): [string, string] {
|
2022-01-31 09:06:30 +00:00
|
|
|
|
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]
|
|
|
|
|
}
|
|
|
|
|
|
2022-04-19 09:38:31 +00:00
|
|
|
|
function getLookup (
|
|
|
|
|
key: string,
|
|
|
|
|
lookup: Lookup<any>,
|
|
|
|
|
parent: Ref<Class<Doc>>
|
2022-06-01 03:25:13 +00:00
|
|
|
|
): [Ref<Class<Doc>>, Ref<Class<Doc>>, boolean] | undefined {
|
2022-01-31 09:06:30 +00:00
|
|
|
|
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.')
|
2022-02-10 09:04:18 +00:00
|
|
|
|
if (nestedKey.length > 0) {
|
2022-01-31 09:06:30 +00:00
|
|
|
|
if (!Array.isArray(current)) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
return getLookup(nestedKey, current[1], current[0])
|
|
|
|
|
}
|
|
|
|
|
if (Array.isArray(current)) {
|
2022-06-01 03:25:13 +00:00
|
|
|
|
return [current[0], parent, false]
|
2022-01-31 09:06:30 +00:00
|
|
|
|
}
|
|
|
|
|
if (current === undefined && lookup._id !== undefined) {
|
|
|
|
|
const reverse = (lookup._id as any)[currentKey]
|
2022-06-01 03:25:13 +00:00
|
|
|
|
return reverse !== undefined
|
|
|
|
|
? Array.isArray(reverse)
|
|
|
|
|
? [reverse[0], parent, true]
|
|
|
|
|
: [reverse, parent, true]
|
|
|
|
|
: undefined
|
2022-01-31 09:06:30 +00:00
|
|
|
|
}
|
2022-06-01 03:25:13 +00:00
|
|
|
|
return current !== undefined ? [current, parent, false] : undefined
|
2022-01-31 09:06:30 +00:00
|
|
|
|
}
|
2022-02-10 09:04:18 +00:00
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
2022-02-16 09:02:31 +00:00
|
|
|
|
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
|
|
|
|
|
}
|
2022-05-05 14:50:28 +00:00
|
|
|
|
|
2022-09-14 04:53:48 +00:00
|
|
|
|
export interface CategoryKey {
|
|
|
|
|
key: KeyedAttribute
|
|
|
|
|
category: AttributeCategory
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function categorizeFields (
|
2022-06-20 15:59:56 +00:00
|
|
|
|
hierarchy: Hierarchy,
|
|
|
|
|
keys: KeyedAttribute[],
|
2022-09-14 04:53:48 +00:00
|
|
|
|
useAsCollection: string[],
|
|
|
|
|
useAsAttribute: string[]
|
|
|
|
|
): {
|
|
|
|
|
attributes: CategoryKey[]
|
|
|
|
|
collections: CategoryKey[]
|
|
|
|
|
} {
|
|
|
|
|
const result = {
|
|
|
|
|
attributes: [] as CategoryKey[],
|
|
|
|
|
collections: [] as CategoryKey[]
|
|
|
|
|
}
|
2022-08-30 05:54:03 +00:00
|
|
|
|
|
2022-05-05 14:50:28 +00:00
|
|
|
|
for (const key of keys) {
|
2022-08-30 05:54:03 +00:00
|
|
|
|
const cl = getAttributePresenterClass(hierarchy, key.attr)
|
2022-09-14 04:53:48 +00:00
|
|
|
|
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 })
|
2022-06-20 15:59:56 +00:00
|
|
|
|
}
|
2022-05-05 14:50:28 +00:00
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-11 13:39:23 +00:00
|
|
|
|
export function makeViewletKey (loc?: Location): string {
|
2023-04-21 16:24:45 +00:00
|
|
|
|
loc = loc != null ? { path: loc.path } : getCurrentResolvedLocation()
|
2022-06-16 14:07:06 +00:00
|
|
|
|
loc.fragment = undefined
|
|
|
|
|
loc.query = undefined
|
|
|
|
|
return 'viewlet' + locationToUrl(loc)
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-16 11:11:40 +00:00
|
|
|
|
function getSavedViewlets (): Record<string, Ref<Viewlet> | null> {
|
|
|
|
|
const res: Record<string, Ref<Viewlet> | null> = {}
|
|
|
|
|
const keys = Object.keys(localStorage)
|
|
|
|
|
for (const key of keys) {
|
|
|
|
|
if (!key.startsWith('viewlet')) continue
|
|
|
|
|
const item = localStorage.getItem(key) as Ref<Viewlet> | null
|
|
|
|
|
res[key] = item
|
|
|
|
|
}
|
|
|
|
|
return res
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const activeViewlet = writable<Record<string, Ref<Viewlet> | null>>(getSavedViewlets())
|
2023-04-11 13:39:23 +00:00
|
|
|
|
|
2023-01-21 14:16:14 +00:00
|
|
|
|
export function setActiveViewletId (viewletId: Ref<Viewlet> | null, loc?: Location): void {
|
|
|
|
|
const key = makeViewletKey(loc)
|
2023-04-11 13:39:23 +00:00
|
|
|
|
const current = get(activeViewlet) ?? {}
|
2023-01-21 14:16:14 +00:00
|
|
|
|
if (viewletId !== null && viewletId !== undefined) {
|
2022-06-16 14:07:06 +00:00
|
|
|
|
localStorage.setItem(key, viewletId)
|
2023-04-11 13:39:23 +00:00
|
|
|
|
current[key] = viewletId
|
2022-06-16 14:07:06 +00:00
|
|
|
|
} else {
|
|
|
|
|
localStorage.removeItem(key)
|
2023-04-11 13:39:23 +00:00
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
|
|
|
delete current[key]
|
2022-06-16 14:07:06 +00:00
|
|
|
|
}
|
2023-04-11 13:39:23 +00:00
|
|
|
|
activeViewlet.set(current)
|
2022-06-16 14:07:06 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getActiveViewletId (): Ref<Viewlet> | null {
|
|
|
|
|
const key = makeViewletKey()
|
|
|
|
|
return localStorage.getItem(key) as Ref<Viewlet> | null
|
|
|
|
|
}
|
2023-01-05 06:09:30 +00:00
|
|
|
|
|
2023-04-24 09:40:03 +00:00
|
|
|
|
/**
|
|
|
|
|
* Updates the active viewlet, if one was found.
|
|
|
|
|
* Otherwise sets the default viewlet.
|
|
|
|
|
*
|
|
|
|
|
* @export
|
|
|
|
|
* @param {readonly Viewlet[]} viewlets
|
|
|
|
|
* @param {(Ref<Viewlet> | null | undefined)} activeViewletId
|
|
|
|
|
* @returns {(Viewlet | undefined)}
|
|
|
|
|
*/
|
|
|
|
|
export function updateActiveViewlet (
|
|
|
|
|
viewlets: readonly Viewlet[],
|
|
|
|
|
activeViewletId: Ref<Viewlet> | null | undefined
|
|
|
|
|
): Viewlet | undefined {
|
|
|
|
|
if (viewlets.length === 0) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let viewlet: Viewlet | undefined
|
|
|
|
|
|
|
|
|
|
if (activeViewletId !== null && activeViewletId !== undefined) {
|
|
|
|
|
viewlet = viewlets.find((viewlet) => viewlet._id === activeViewletId)
|
|
|
|
|
}
|
|
|
|
|
viewlet ??= viewlets[0]
|
|
|
|
|
|
|
|
|
|
setActiveViewletId(viewlet._id)
|
|
|
|
|
|
|
|
|
|
return viewlet
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-05 06:09:30 +00:00
|
|
|
|
export type FixedWidthStore = Record<string, number>
|
|
|
|
|
|
|
|
|
|
export const fixedWidthStore = writable<FixedWidthStore>({})
|
2023-01-14 10:54:54 +00:00
|
|
|
|
|
2023-04-13 17:24:52 +00:00
|
|
|
|
export function groupBy<T extends Doc> (docs: T[], key: string, categories?: CategoryType[]): Record<any, T[]> {
|
2023-02-28 18:36:00 +00:00
|
|
|
|
return docs.reduce((storage: { [key: string]: T[] }, item: T) => {
|
2023-04-04 06:11:49 +00:00
|
|
|
|
let group = getObjectValue(key, item) ?? undefined
|
|
|
|
|
|
|
|
|
|
if (categories !== undefined) {
|
|
|
|
|
for (const c of categories) {
|
|
|
|
|
if (typeof c === 'object') {
|
|
|
|
|
const st = c.values.find((it) => it._id === group)
|
|
|
|
|
if (st !== undefined) {
|
|
|
|
|
group = st.name
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-01-14 10:54:54 +00:00
|
|
|
|
|
|
|
|
|
storage[group] = storage[group] ?? []
|
|
|
|
|
storage[group].push(item)
|
|
|
|
|
|
|
|
|
|
return storage
|
|
|
|
|
}, {})
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-04 06:11:49 +00:00
|
|
|
|
/**
|
|
|
|
|
* @public
|
|
|
|
|
*/
|
2023-04-13 17:24:52 +00:00
|
|
|
|
export function getGroupByValues<T extends Doc> (groupByDocs: Record<any, T[]>, category: CategoryType): T[] {
|
2023-04-04 06:11:49 +00:00
|
|
|
|
if (typeof category === 'object') {
|
|
|
|
|
return groupByDocs[category.name] ?? []
|
2023-04-13 17:24:52 +00:00
|
|
|
|
} else {
|
|
|
|
|
return groupByDocs[category as any] ?? []
|
2023-04-04 06:11:49 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @public
|
|
|
|
|
*/
|
|
|
|
|
export function setGroupByValues (
|
|
|
|
|
groupByDocs: Record<string | number, Doc[]>,
|
|
|
|
|
category: CategoryType,
|
|
|
|
|
docs: Doc[]
|
|
|
|
|
): void {
|
|
|
|
|
if (typeof category === 'object') {
|
|
|
|
|
groupByDocs[category.name] = docs
|
|
|
|
|
} else if (category !== undefined) {
|
|
|
|
|
groupByDocs[category] = docs
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Group category references into categories.
|
|
|
|
|
* @public
|
|
|
|
|
*/
|
|
|
|
|
export async function groupByCategory (
|
|
|
|
|
client: TxOperations,
|
|
|
|
|
_class: Ref<Class<Doc>>,
|
|
|
|
|
key: string,
|
|
|
|
|
categories: any[],
|
|
|
|
|
mgr: StatusManager,
|
|
|
|
|
viewletDescriptorId?: Ref<ViewletDescriptor>
|
|
|
|
|
): Promise<CategoryType[]> {
|
|
|
|
|
const h = client.getHierarchy()
|
|
|
|
|
const attr = h.getAttribute(_class, key)
|
|
|
|
|
if (attr === undefined) return categories
|
|
|
|
|
if (key === noCategory) return [undefined]
|
|
|
|
|
|
|
|
|
|
const attrClass = getAttributePresenterClass(h, attr).attrClass
|
|
|
|
|
|
|
|
|
|
const isStatusField = h.isDerived(attrClass, core.class.Status)
|
|
|
|
|
|
|
|
|
|
let existingCategories: any[] = []
|
|
|
|
|
|
|
|
|
|
if (isStatusField) {
|
|
|
|
|
existingCategories = await groupByStatusCategories(h, attrClass, categories, mgr, viewletDescriptorId)
|
|
|
|
|
} else {
|
|
|
|
|
const valueSet = new Set<any>()
|
|
|
|
|
for (const v of categories) {
|
|
|
|
|
if (!valueSet.has(v)) {
|
|
|
|
|
valueSet.add(v)
|
|
|
|
|
existingCategories.push(v)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return await sortCategories(h, attrClass, existingCategories, viewletDescriptorId)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @public
|
|
|
|
|
*/
|
|
|
|
|
export async function groupByStatusCategories (
|
|
|
|
|
hierarchy: Hierarchy,
|
|
|
|
|
attrClass: Ref<Class<Doc>>,
|
|
|
|
|
categories: any[],
|
|
|
|
|
mgr: StatusManager,
|
|
|
|
|
viewletDescriptorId?: Ref<ViewletDescriptor>
|
|
|
|
|
): Promise<StatusValue[]> {
|
|
|
|
|
const existingCategories: StatusValue[] = []
|
|
|
|
|
const statusMap = new Map<string, StatusValue>()
|
|
|
|
|
|
2023-04-20 05:38:46 +00:00
|
|
|
|
const usedSpaces = new Set<Ref<Space>>()
|
|
|
|
|
const statusesList: Array<WithLookup<Status>> = []
|
2023-04-04 06:11:49 +00:00
|
|
|
|
for (const v of categories) {
|
|
|
|
|
const status = mgr.byId.get(v)
|
2023-04-20 05:38:46 +00:00
|
|
|
|
if (status !== undefined) {
|
|
|
|
|
statusesList.push(status)
|
|
|
|
|
usedSpaces.add(status.space)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const status of statusesList) {
|
2023-04-04 06:11:49 +00:00
|
|
|
|
if (status !== undefined) {
|
|
|
|
|
let fst = statusMap.get(status.name.toLowerCase().trim())
|
|
|
|
|
if (fst === undefined) {
|
|
|
|
|
const statuses = mgr.statuses
|
|
|
|
|
.filter(
|
|
|
|
|
(it) =>
|
|
|
|
|
it.ofAttribute === status.ofAttribute &&
|
|
|
|
|
it.name.toLowerCase().trim() === status.name.toLowerCase().trim() &&
|
2023-04-20 05:38:46 +00:00
|
|
|
|
(categories.includes(it._id) || usedSpaces.has(it.space))
|
2023-04-04 06:11:49 +00:00
|
|
|
|
)
|
|
|
|
|
.sort((a, b) => a.rank.localeCompare(b.rank))
|
|
|
|
|
fst = new StatusValue(status.name, status.color, statuses)
|
|
|
|
|
statusMap.set(status.name.toLowerCase().trim(), fst)
|
|
|
|
|
existingCategories.push(fst)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return await sortCategories(hierarchy, attrClass, existingCategories, viewletDescriptorId)
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-14 10:54:54 +00:00
|
|
|
|
export async function getCategories (
|
|
|
|
|
client: TxOperations,
|
|
|
|
|
_class: Ref<Class<Doc>>,
|
|
|
|
|
docs: Doc[],
|
2023-03-23 05:41:27 +00:00
|
|
|
|
key: string,
|
2023-04-04 06:11:49 +00:00
|
|
|
|
mgr: StatusManager,
|
2023-03-23 05:41:27 +00:00
|
|
|
|
viewletDescriptorId?: Ref<ViewletDescriptor>
|
2023-04-04 06:11:49 +00:00
|
|
|
|
): Promise<CategoryType[]> {
|
2023-01-14 10:54:54 +00:00
|
|
|
|
if (key === noCategory) return [undefined]
|
2023-03-23 05:41:27 +00:00
|
|
|
|
|
2023-04-04 06:11:49 +00:00
|
|
|
|
return await groupByCategory(
|
|
|
|
|
client,
|
|
|
|
|
_class,
|
|
|
|
|
key,
|
|
|
|
|
docs.map((it) => getObjectValue(key, it) ?? undefined),
|
|
|
|
|
mgr,
|
|
|
|
|
viewletDescriptorId
|
|
|
|
|
)
|
2023-03-23 05:41:27 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-04-14 14:34:03 +00:00
|
|
|
|
/**
|
|
|
|
|
* @public
|
|
|
|
|
*/
|
|
|
|
|
export function getCategorySpaces (categories: CategoryType[]): Array<Ref<Space>> {
|
|
|
|
|
return Array.from(
|
|
|
|
|
(categories.filter((it) => typeof it === 'object') as StatusValue[]).reduce<Set<Ref<Space>>>((arr, val) => {
|
|
|
|
|
val.values.forEach((it) => arr.add(it.space))
|
|
|
|
|
return arr
|
|
|
|
|
}, new Set())
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-11 16:05:01 +00:00
|
|
|
|
export function concatCategories (arr1: CategoryType[], arr2: CategoryType[]): CategoryType[] {
|
|
|
|
|
const uniqueValues: Set<string | number | undefined> = new Set()
|
|
|
|
|
const uniqueObjects: Map<string | number, StatusValue> = new Map()
|
|
|
|
|
|
|
|
|
|
for (const item of arr1) {
|
|
|
|
|
if (typeof item === 'object') {
|
|
|
|
|
const id = item.name
|
|
|
|
|
uniqueObjects.set(id, item)
|
|
|
|
|
} else {
|
|
|
|
|
uniqueValues.add(item)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const item of arr2) {
|
|
|
|
|
if (typeof item === 'object') {
|
|
|
|
|
const id = item.name
|
|
|
|
|
if (!uniqueObjects.has(id)) {
|
|
|
|
|
uniqueObjects.set(id, item)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
uniqueValues.add(item)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [...uniqueValues, ...uniqueObjects.values()]
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-04 06:11:49 +00:00
|
|
|
|
/**
|
|
|
|
|
* @public
|
|
|
|
|
*/
|
2023-03-23 05:41:27 +00:00
|
|
|
|
export async function sortCategories (
|
2023-04-04 06:11:49 +00:00
|
|
|
|
hierarchy: Hierarchy,
|
|
|
|
|
attrClass: Ref<Class<Doc>>,
|
2023-03-23 05:41:27 +00:00
|
|
|
|
existingCategories: any[],
|
|
|
|
|
viewletDescriptorId?: Ref<ViewletDescriptor>
|
|
|
|
|
): Promise<any[]> {
|
2023-01-14 10:54:54 +00:00
|
|
|
|
const clazz = hierarchy.getClass(attrClass)
|
|
|
|
|
const sortFunc = hierarchy.as(clazz, view.mixin.SortFuncs)
|
2023-04-04 06:11:49 +00:00
|
|
|
|
if (sortFunc?.func === undefined) {
|
|
|
|
|
const isStatusField = hierarchy.isDerived(attrClass, core.class.Status)
|
|
|
|
|
if (isStatusField) {
|
|
|
|
|
existingCategories.sort((a, b) => {
|
|
|
|
|
return a.values[0].rank.localeCompare(b.values[0].rank)
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
existingCategories.sort((a, b) => {
|
|
|
|
|
return JSON.stringify(a).localeCompare(JSON.stringify(b))
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return existingCategories
|
|
|
|
|
}
|
2023-01-14 10:54:54 +00:00
|
|
|
|
const f = await getResource(sortFunc.func)
|
|
|
|
|
|
2023-03-23 05:41:27 +00:00
|
|
|
|
return await f(existingCategories, viewletDescriptorId)
|
2023-01-14 10:54:54 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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])
|
2023-05-31 08:40:47 +00:00
|
|
|
|
} else if (key.length === 0) {
|
|
|
|
|
const clazz = client.getHierarchy().getClass(_class)
|
|
|
|
|
return clazz.label
|
2023-01-14 10:54:54 +00:00
|
|
|
|
} else {
|
|
|
|
|
const attribute = client.getHierarchy().getAttribute(_class, key)
|
|
|
|
|
return attribute.label
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-01-26 13:53:00 +00:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @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
|
|
|
|
|
}
|
2023-02-07 11:15:59 +00:00
|
|
|
|
|
2023-02-15 03:14:20 +00:00
|
|
|
|
/**
|
|
|
|
|
* 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)
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-07 11:15:59 +00:00
|
|
|
|
/**
|
|
|
|
|
* @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
|
|
|
|
|
})
|
|
|
|
|
}
|
2023-02-08 04:34:31 +00:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @public
|
|
|
|
|
*/
|
|
|
|
|
export function getAdditionalHeader (client: TxOperations, _class: Ref<Class<Doc>>): AnyComponent[] | undefined {
|
2023-04-25 07:34:10 +00:00
|
|
|
|
try {
|
|
|
|
|
const hierarchy = client.getHierarchy()
|
|
|
|
|
const presenterMixin = hierarchy.classHierarchyMixin(_class, view.mixin.ListHeaderExtra)
|
|
|
|
|
return presenterMixin?.presenters?.filter((it) => hasResource(it))
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
if (((e?.message as string) ?? '').includes('class not found')) {
|
|
|
|
|
return undefined
|
|
|
|
|
}
|
|
|
|
|
throw e
|
|
|
|
|
}
|
2023-02-08 04:34:31 +00:00
|
|
|
|
}
|
2023-03-15 14:06:03 +00:00
|
|
|
|
|
|
|
|
|
export async function getObjectLinkFragment (
|
|
|
|
|
hierarchy: Hierarchy,
|
|
|
|
|
object: Doc,
|
|
|
|
|
props: Record<string, any> = {},
|
|
|
|
|
component: AnyComponent = view.component.EditDoc
|
2023-03-20 08:45:52 +00:00
|
|
|
|
): Promise<Location> {
|
2023-04-25 07:34:10 +00:00
|
|
|
|
const provider = hierarchy.classHierarchyMixin(Hierarchy.mixinOrClass(object), view.mixin.LinkProvider, (m) =>
|
|
|
|
|
hasResource(m.encode)
|
|
|
|
|
)
|
2023-03-15 14:06:03 +00:00
|
|
|
|
if (provider?.encode !== undefined) {
|
|
|
|
|
const f = await getResource(provider.encode)
|
|
|
|
|
const res = await f(object, props)
|
|
|
|
|
if (res !== undefined) {
|
|
|
|
|
return res
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-04-21 16:24:45 +00:00
|
|
|
|
const loc = getCurrentResolvedLocation()
|
2023-04-25 07:34:10 +00:00
|
|
|
|
if (hasResource(component)) {
|
|
|
|
|
loc.fragment = getPanelURI(component, object._id, Hierarchy.mixinOrClass(object), 'content')
|
|
|
|
|
}
|
2023-03-20 08:45:52 +00:00
|
|
|
|
return loc
|
2023-03-15 14:06:03 +00:00
|
|
|
|
}
|
2023-04-04 06:11:49 +00:00
|
|
|
|
|
|
|
|
|
export async function statusSort (
|
|
|
|
|
value: Array<Ref<Status>>,
|
|
|
|
|
viewletDescriptorId?: Ref<ViewletDescriptor>
|
|
|
|
|
): Promise<Array<Ref<Status>>> {
|
|
|
|
|
return await new Promise((resolve) => {
|
|
|
|
|
// TODO: How we track category updates.
|
|
|
|
|
const query = createQuery(true)
|
|
|
|
|
query.query(
|
|
|
|
|
core.class.Status,
|
|
|
|
|
{ _id: { $in: value } },
|
|
|
|
|
(res) => {
|
|
|
|
|
res.sort((a, b) => {
|
|
|
|
|
const res = (a.$lookup?.category?.order ?? 0) - (b.$lookup?.category?.order ?? 0)
|
|
|
|
|
if (res === 0) {
|
|
|
|
|
return a.rank.localeCompare(b.rank)
|
|
|
|
|
}
|
|
|
|
|
return res
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
resolve(res.map((p) => p._id))
|
|
|
|
|
query.unsubscribe()
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
sort: {
|
|
|
|
|
rank: 1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
}
|
2023-05-24 06:29:18 +00:00
|
|
|
|
|
|
|
|
|
export function isAttachedDoc (doc: Doc | AttachedDoc): doc is AttachedDoc {
|
|
|
|
|
return 'attachedTo' in doc
|
|
|
|
|
}
|
2023-05-31 08:40:47 +00:00
|
|
|
|
|
|
|
|
|
export function enabledConfig (config: Array<string | BuildModelKey>, key: string): boolean {
|
|
|
|
|
for (const value of config) {
|
|
|
|
|
if (typeof value === 'string') {
|
|
|
|
|
if (value === key) return true
|
|
|
|
|
} else {
|
|
|
|
|
if (value.key === key) return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|