2021-08-07 14:49:14 +00:00
|
|
|
|
//
|
|
|
|
|
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
2024-04-15 11:11:47 +00:00
|
|
|
|
// Copyright © 2021, 2024 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.
|
|
|
|
|
//
|
|
|
|
|
|
2024-02-15 15:19:33 +00:00
|
|
|
|
import { Analytics } from '@hcengineering/analytics'
|
2022-05-13 06:38:54 +00:00
|
|
|
|
import core, {
|
2023-04-17 06:08:41 +00:00
|
|
|
|
AccountRole,
|
2024-03-01 16:39:46 +00:00
|
|
|
|
ClassifierKind,
|
2024-07-03 08:16:04 +00:00
|
|
|
|
DocManager,
|
2024-03-01 16:39:46 +00:00
|
|
|
|
Hierarchy,
|
2024-07-03 08:16:04 +00:00
|
|
|
|
SortingOrder,
|
2024-03-01 16:39:46 +00:00
|
|
|
|
TxProcessor,
|
|
|
|
|
getCurrentAccount,
|
|
|
|
|
getObjectValue,
|
|
|
|
|
type Account,
|
2023-11-20 10:01:43 +00:00
|
|
|
|
type AggregateValue,
|
2024-07-03 08:16:04 +00:00
|
|
|
|
type AnyAttribute,
|
2023-11-20 10:01:43 +00:00
|
|
|
|
type AttachedDoc,
|
|
|
|
|
type CategoryType,
|
|
|
|
|
type Class,
|
|
|
|
|
type Client,
|
|
|
|
|
type Collection,
|
|
|
|
|
type Doc,
|
2023-11-27 15:49:53 +00:00
|
|
|
|
type DocumentQuery,
|
2023-11-20 10:01:43 +00:00
|
|
|
|
type DocumentUpdate,
|
2024-03-01 16:39:46 +00:00
|
|
|
|
type FindOptions,
|
2023-11-20 10:01:43 +00:00
|
|
|
|
type Lookup,
|
2024-01-11 14:46:11 +00:00
|
|
|
|
type Mixin,
|
2023-11-20 10:01:43 +00:00
|
|
|
|
type Obj,
|
2024-05-14 09:29:05 +00:00
|
|
|
|
type Permission,
|
2023-11-20 10:01:43 +00:00
|
|
|
|
type Ref,
|
|
|
|
|
type RefTo,
|
|
|
|
|
type ReverseLookup,
|
|
|
|
|
type ReverseLookups,
|
|
|
|
|
type Space,
|
2024-07-03 08:16:04 +00:00
|
|
|
|
type Tx,
|
2023-12-21 15:31:32 +00:00
|
|
|
|
type TxCUD,
|
|
|
|
|
type TxCreateDoc,
|
2024-01-11 14:46:11 +00:00
|
|
|
|
type TxMixin,
|
2024-03-01 16:39:46 +00:00
|
|
|
|
type TxOperations,
|
|
|
|
|
type TxUpdateDoc,
|
2024-03-27 08:49:36 +00:00
|
|
|
|
type TypeAny,
|
2024-06-25 05:03:01 +00:00
|
|
|
|
type TypedSpace,
|
2024-07-03 08:16:04 +00:00
|
|
|
|
type WithLookup
|
2022-09-21 08:08:25 +00:00
|
|
|
|
} from '@hcengineering/core'
|
2024-03-01 16:39:46 +00:00
|
|
|
|
import { type Restrictions } from '@hcengineering/guest'
|
2024-01-11 14:46:11 +00:00
|
|
|
|
import type { Asset, IntlString } from '@hcengineering/platform'
|
2023-12-11 11:54:36 +00:00
|
|
|
|
import { getResource, translate } from '@hcengineering/platform'
|
2022-09-21 08:08:25 +00:00
|
|
|
|
import {
|
2024-05-14 09:29:05 +00:00
|
|
|
|
createQuery,
|
2023-11-20 10:01:43 +00:00
|
|
|
|
getAttributePresenterClass,
|
2024-01-11 14:46:11 +00:00
|
|
|
|
getClient,
|
2024-03-06 08:01:05 +00:00
|
|
|
|
getFiltredKeys,
|
2024-07-03 08:16:04 +00:00
|
|
|
|
getRawLiveQuery,
|
2024-05-14 09:29:05 +00:00
|
|
|
|
hasResource,
|
2024-03-27 08:49:36 +00:00
|
|
|
|
isAdminUser,
|
2024-05-14 09:29:05 +00:00
|
|
|
|
type KeyedAttribute
|
2023-11-20 10:01:43 +00:00
|
|
|
|
} from '@hcengineering/presentation'
|
2024-04-15 11:11:47 +00:00
|
|
|
|
import { type CollaborationUser } from '@hcengineering/text-editor'
|
2023-11-20 10:01:43 +00:00
|
|
|
|
import {
|
2022-09-21 08:08:25 +00:00
|
|
|
|
ErrorPresenter,
|
2024-04-15 11:11:47 +00:00
|
|
|
|
getColorNumberByText,
|
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-09-30 16:52:27 +00:00
|
|
|
|
locationToUrl,
|
2023-10-24 08:53:33 +00:00
|
|
|
|
navigate,
|
2023-11-20 10:01:43 +00:00
|
|
|
|
resolvedLocationStore,
|
2024-03-01 16:39:46 +00:00
|
|
|
|
themeStore,
|
|
|
|
|
type AnyComponent,
|
|
|
|
|
type AnySvelteComponent,
|
|
|
|
|
type Location
|
2022-09-21 08:08:25 +00:00
|
|
|
|
} from '@hcengineering/ui'
|
2023-11-20 10:01:43 +00:00
|
|
|
|
import view, {
|
2024-05-14 09:29:05 +00:00
|
|
|
|
AttributeCategoryOrder,
|
|
|
|
|
type AttributeCategory,
|
2023-11-20 10:01:43 +00:00
|
|
|
|
type AttributeModel,
|
2024-05-14 09:29:05 +00:00
|
|
|
|
type AttributePresenter,
|
2023-11-20 10:01:43 +00:00
|
|
|
|
type BuildModelKey,
|
|
|
|
|
type BuildModelOptions,
|
2023-12-11 11:54:36 +00:00
|
|
|
|
type CollectionPresenter,
|
2024-07-03 08:16:04 +00:00
|
|
|
|
type IAggregationManager,
|
|
|
|
|
type LinkIdProvider,
|
2023-11-20 10:01:43 +00:00
|
|
|
|
type Viewlet,
|
2024-07-03 08:16:04 +00:00
|
|
|
|
type ViewletDescriptor
|
2023-11-20 10:01:43 +00:00
|
|
|
|
} from '@hcengineering/view'
|
2023-06-06 10:06:32 +00:00
|
|
|
|
|
2024-03-01 16:39:46 +00:00
|
|
|
|
import contact, { getName, type Contact, type PersonAccount } from '@hcengineering/contact'
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-25 05:03:01 +00:00
|
|
|
|
/**
|
|
|
|
|
* @public
|
|
|
|
|
*/
|
|
|
|
|
export class AggregationManager<T extends Doc> implements IAggregationManager<T> {
|
|
|
|
|
docs: T[] | undefined
|
|
|
|
|
mgr: DocManager<T> | Promise<DocManager<T>> | undefined
|
|
|
|
|
query: (() => void) | undefined
|
|
|
|
|
lqCallback: () => void
|
|
|
|
|
private readonly setStore: (manager: DocManager<T>) => void
|
|
|
|
|
private readonly filter: (doc: T, target: T) => boolean
|
|
|
|
|
private readonly _class: Ref<Class<T>>
|
|
|
|
|
|
|
|
|
|
private constructor (
|
|
|
|
|
client: Client,
|
|
|
|
|
lqCallback: () => void,
|
|
|
|
|
setStore: (manager: DocManager<T>) => void,
|
|
|
|
|
categorizingFunc: (doc: T, target: T) => boolean,
|
|
|
|
|
_class: Ref<Class<T>>
|
|
|
|
|
) {
|
|
|
|
|
this.lqCallback = lqCallback ?? (() => {})
|
|
|
|
|
this.setStore = setStore
|
|
|
|
|
this.filter = categorizingFunc
|
|
|
|
|
this._class = _class
|
|
|
|
|
void this.getManager()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static create<T extends Doc>(
|
|
|
|
|
client: Client,
|
|
|
|
|
lqCallback: () => void,
|
|
|
|
|
setStore: (manager: DocManager<T>) => void,
|
|
|
|
|
categorizingFunc: (doc: T, target: T) => boolean,
|
|
|
|
|
_class: Ref<Class<T>>
|
|
|
|
|
): AggregationManager<T> {
|
|
|
|
|
return new AggregationManager<T>(client, lqCallback, setStore, categorizingFunc, _class)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async getManager (): Promise<DocManager<T>> {
|
|
|
|
|
if (this.mgr !== undefined) {
|
|
|
|
|
if (this.mgr instanceof Promise) {
|
|
|
|
|
this.mgr = await this.mgr
|
|
|
|
|
}
|
|
|
|
|
return this.mgr
|
|
|
|
|
}
|
|
|
|
|
this.mgr = new Promise<DocManager<T>>((resolve) => {
|
2024-07-03 08:16:04 +00:00
|
|
|
|
this.query = getRawLiveQuery().query(
|
2024-06-25 05:03:01 +00:00
|
|
|
|
this._class,
|
|
|
|
|
{},
|
|
|
|
|
(res) => {
|
|
|
|
|
const first = this.docs === undefined
|
|
|
|
|
this.docs = res
|
|
|
|
|
this.mgr = new DocManager<T>(res as T[])
|
|
|
|
|
this.setStore(this.mgr)
|
|
|
|
|
if (!first) {
|
|
|
|
|
this.lqCallback()
|
|
|
|
|
}
|
|
|
|
|
resolve(this.mgr)
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
sort: {
|
|
|
|
|
label: SortingOrder.Ascending
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return await this.mgr
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
close (): void {
|
|
|
|
|
this.query?.()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async notifyTx (...tx: Tx[]): Promise<void> {
|
2024-07-03 08:16:04 +00:00
|
|
|
|
// This is intentional
|
2024-06-25 05:03:01 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getAttrClass (): Ref<Class<T>> {
|
|
|
|
|
return this._class
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async categorize (target: Array<Ref<T>>, attr: AnyAttribute): Promise<Array<Ref<T>>> {
|
|
|
|
|
const mgr = await this.getManager()
|
|
|
|
|
for (const sid of [...target]) {
|
|
|
|
|
const c = mgr.getIdMap().get(sid) as WithLookup<T>
|
|
|
|
|
if (c !== undefined) {
|
|
|
|
|
let docs = mgr.getDocs()
|
|
|
|
|
docs = docs.filter((it: T) => this.filter(it, c))
|
|
|
|
|
target.push(...docs.map((it) => it._id))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return target.filter((it, idx, arr) => arr.indexOf(it) === idx)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
|
|
2024-01-04 10:37:11 +00:00
|
|
|
|
const presenterMixin = hierarchy.classHierarchyMixin(
|
|
|
|
|
_class,
|
|
|
|
|
mixin,
|
|
|
|
|
(m) => !checkResource || hasResource(m.presenter) === true
|
|
|
|
|
)
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-11 11:54:36 +00:00
|
|
|
|
export async function getAttributePresenter (
|
2022-01-18 10:21:32 +00:00
|
|
|
|
client: Client,
|
|
|
|
|
_class: Ref<Class<Obj>>,
|
|
|
|
|
key: string,
|
2023-12-11 11:54:36 +00:00
|
|
|
|
preserveKey: BuildModelKey,
|
2024-05-14 09:29:05 +00:00
|
|
|
|
mixinClass?: Ref<Mixin<CollectionPresenter>>,
|
|
|
|
|
_category?: AttributeCategory
|
2022-01-18 10:21:32 +00:00
|
|
|
|
): Promise<AttributeModel> {
|
2023-12-11 11:54:36 +00:00
|
|
|
|
const actualMixinClass = mixinClass ?? view.mixin.AttributePresenter
|
|
|
|
|
|
2022-05-19 07:14:05 +00:00
|
|
|
|
const hierarchy = client.getHierarchy()
|
|
|
|
|
const attribute = hierarchy.getAttribute(_class, key)
|
2024-05-14 09:29:05 +00:00
|
|
|
|
let { attrClass, category } = getAttributePresenterClass(hierarchy, attribute)
|
|
|
|
|
if (_category !== undefined) {
|
|
|
|
|
category = _category
|
|
|
|
|
}
|
2024-01-25 06:00:30 +00:00
|
|
|
|
|
2024-05-14 09:29:05 +00:00
|
|
|
|
let overridedPresenter = await client
|
|
|
|
|
.getModel()
|
|
|
|
|
.findOne(view.class.AttrPresenter, { objectClass: _class, attribute: attribute._id, category })
|
|
|
|
|
if (overridedPresenter === undefined) {
|
|
|
|
|
overridedPresenter = await client
|
|
|
|
|
.getModel()
|
|
|
|
|
.findOne(view.class.AttrPresenter, { attribute: attribute._id, category })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const isCollectionAttr = category === 'collection'
|
2023-12-11 11:54:36 +00:00
|
|
|
|
const mixin = isCollectionAttr ? view.mixin.CollectionPresenter : actualMixinClass
|
|
|
|
|
|
2024-05-08 09:41:44 +00:00
|
|
|
|
let presenterMixin: AttributePresenter | CollectionPresenter | undefined = hierarchy.classHierarchyMixin(
|
2024-05-14 09:29:05 +00:00
|
|
|
|
attrClass,
|
2024-05-08 09:41:44 +00:00
|
|
|
|
mixin
|
|
|
|
|
)
|
2023-12-11 11:54:36 +00:00
|
|
|
|
|
|
|
|
|
if (presenterMixin?.presenter === undefined && mixinClass != null && mixin === mixinClass) {
|
2024-05-14 09:29:05 +00:00
|
|
|
|
presenterMixin = hierarchy.classHierarchyMixin(attrClass, view.mixin.AttributePresenter)
|
2023-12-11 11:54:36 +00:00
|
|
|
|
}
|
2024-01-25 06:00:30 +00:00
|
|
|
|
|
2024-05-14 09:29:05 +00:00
|
|
|
|
let presenter: AnySvelteComponent | undefined
|
2024-01-25 06:00:30 +00:00
|
|
|
|
|
2024-05-14 09:29:05 +00:00
|
|
|
|
if (overridedPresenter !== undefined) {
|
|
|
|
|
presenter = await getResource(overridedPresenter.component)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (presenter === undefined) {
|
|
|
|
|
const attributePresenter = presenterMixin as AttributePresenter
|
|
|
|
|
if (category === 'array' && attributePresenter.arrayPresenter !== undefined) {
|
|
|
|
|
presenter = await getResource(attributePresenter.arrayPresenter)
|
|
|
|
|
} else if (presenterMixin?.presenter !== undefined) {
|
|
|
|
|
presenter = await getResource(presenterMixin.presenter)
|
|
|
|
|
} else if (attrClass === core.class.TypeAny) {
|
|
|
|
|
const typeAny = attribute.type as TypeAny
|
|
|
|
|
presenter = await getResource(typeAny.presenter)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (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
|
|
|
|
}
|
2023-12-11 11:54:36 +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
|
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,
|
2024-05-14 09:29:05 +00:00
|
|
|
|
_class: 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,
|
2024-01-25 06:00:30 +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
|
|
|
|
}
|
|
|
|
|
|
2024-02-02 09:43:50 +00:00
|
|
|
|
export function hasAttributePresenter (
|
|
|
|
|
client: Client,
|
|
|
|
|
_class: Ref<Class<Obj>>,
|
|
|
|
|
key: string,
|
|
|
|
|
mixinClass?: Ref<Mixin<CollectionPresenter>>
|
|
|
|
|
): boolean {
|
|
|
|
|
const actualMixinClass = mixinClass ?? view.mixin.AttributePresenter
|
|
|
|
|
|
|
|
|
|
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 : actualMixinClass
|
|
|
|
|
|
|
|
|
|
let presenterMixin = hierarchy.classHierarchyMixin(presenterClass.attrClass, mixin)
|
|
|
|
|
|
|
|
|
|
if (presenterMixin?.presenter === undefined && mixinClass != null && mixin === mixinClass) {
|
|
|
|
|
presenterMixin = hierarchy.classHierarchyMixin(presenterClass.attrClass, view.mixin.AttributePresenter)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return presenterMixin?.presenter !== undefined || (attribute.type as TypeAny)?.presenter !== undefined
|
|
|
|
|
}
|
|
|
|
|
|
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>,
|
2024-05-14 09:29:05 +00:00
|
|
|
|
isCollectionAttr: boolean = false,
|
|
|
|
|
_category?: AttributeCategory
|
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
|
|
|
|
}
|
2024-05-14 09:29:05 +00:00
|
|
|
|
return await getAttributePresenter(client, _class, key.key, preserveKey, undefined, _category)
|
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 ?? {})
|
|
|
|
|
}
|
2024-08-02 14:14:49 +00:00
|
|
|
|
res = { ...existingLookup, ...res }
|
|
|
|
|
if (Object.keys(_id).length > 0) {
|
|
|
|
|
;(res as any)._id = _id
|
|
|
|
|
}
|
2023-01-29 05:44:12 +00:00
|
|
|
|
}
|
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
|
2024-02-15 15:19:33 +00:00
|
|
|
|
Analytics.handleError(err)
|
2022-01-18 10:21:32 +00:00
|
|
|
|
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()
|
2024-03-01 16:39:46 +00:00
|
|
|
|
const accounts = await getCurrentPersonAccounts()
|
|
|
|
|
if (currentAcc.role !== AccountRole.Owner && !accounts.has(object.createdBy)) 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)
|
2023-11-20 10:01:43 +00:00
|
|
|
|
.catch((err) => {
|
|
|
|
|
console.error(err)
|
|
|
|
|
})
|
2022-03-04 09:02:13 +00:00
|
|
|
|
} else {
|
2023-11-20 10:01:43 +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
|
|
|
|
|
2024-03-01 16:39:46 +00:00
|
|
|
|
export async function getCurrentPersonAccounts (): Promise<Set<Ref<Account> | undefined>> {
|
|
|
|
|
return new Set(
|
|
|
|
|
(
|
|
|
|
|
await getClient().findAll(contact.class.PersonAccount, { person: (getCurrentAccount() as PersonAccount).person })
|
|
|
|
|
).map((it) => it._id)
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-28 17:28:55 +00:00
|
|
|
|
export async function deleteObjects (client: TxOperations, objects: Doc[], skipCheck: boolean = false): Promise<void> {
|
2024-03-01 16:39:46 +00:00
|
|
|
|
let realObjects: Doc[] = []
|
2023-12-28 17:28:55 +00:00
|
|
|
|
if (!skipCheck) {
|
|
|
|
|
const currentAcc = getCurrentAccount()
|
2024-03-01 16:39:46 +00:00
|
|
|
|
|
|
|
|
|
// We need to find all person current accounts
|
|
|
|
|
const allPersonAccounts = await getCurrentPersonAccounts()
|
|
|
|
|
|
|
|
|
|
const byClass = new Map<Ref<Class<Doc>>, Doc[]>()
|
|
|
|
|
for (const d of objects) {
|
|
|
|
|
byClass.set(d._class, [...(byClass.get(d._class) ?? []), d])
|
|
|
|
|
}
|
|
|
|
|
const adminUser = isAdminUser()
|
|
|
|
|
for (const [cl, docs] of byClass.entries()) {
|
|
|
|
|
const realDocs = await client.findAll(cl, { _id: { $in: docs.map((it: Doc) => it._id) } })
|
|
|
|
|
const notAllowed = realDocs.filter((p) => !allPersonAccounts.has(p.createdBy))
|
|
|
|
|
|
|
|
|
|
if (notAllowed.length > 0) {
|
|
|
|
|
console.error('You are not allowed to delete this object', notAllowed)
|
|
|
|
|
}
|
|
|
|
|
if (currentAcc.role === AccountRole.Owner || adminUser) {
|
|
|
|
|
realObjects.push(...realDocs)
|
|
|
|
|
} else {
|
|
|
|
|
realObjects.push(...realDocs.filter((p) => allPersonAccounts.has(p.createdBy)))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
realObjects = objects
|
2023-12-28 17:28:55 +00:00
|
|
|
|
}
|
2024-09-09 08:40:49 +00:00
|
|
|
|
const ops = client.apply(undefined, 'delete-objects')
|
2024-03-01 16:39:46 +00:00
|
|
|
|
for (const object of realObjects) {
|
2023-01-27 12:14:40 +00:00
|
|
|
|
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)
|
2023-11-20 10:01:43 +00:00
|
|
|
|
.catch((err) => {
|
|
|
|
|
console.error(err)
|
|
|
|
|
})
|
2023-01-27 12:14:40 +00:00
|
|
|
|
} else {
|
2023-11-20 10:01:43 +00:00
|
|
|
|
await ops.removeDoc(object._class, object.space, object._id).catch((err) => {
|
|
|
|
|
console.error(err)
|
|
|
|
|
})
|
2023-01-27 12:14:40 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
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
|
|
|
|
|
2024-05-17 06:05:05 +00:00
|
|
|
|
export function getBooleanLabel (value: boolean | undefined | null): IntlString {
|
2022-02-10 09:04:18 +00:00
|
|
|
|
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
|
2024-02-02 09:43:50 +00:00
|
|
|
|
|
2022-06-16 14:07:06 +00:00
|
|
|
|
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-10-24 08:53:33 +00:00
|
|
|
|
resolvedLocationStore.subscribe(() => {
|
|
|
|
|
fixedWidthStore.set({})
|
|
|
|
|
})
|
|
|
|
|
|
2023-04-13 17:24:52 +00:00
|
|
|
|
export function groupBy<T extends Doc> (docs: T[], key: string, categories?: CategoryType[]): Record<any, T[]> {
|
2023-11-20 10:01:43 +00:00
|
|
|
|
return docs.reduce((storage: Record<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') {
|
2023-06-06 10:06:32 +00:00
|
|
|
|
return groupByDocs[category.name as any] ?? []
|
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') {
|
2023-06-06 10:06:32 +00:00
|
|
|
|
groupByDocs[category.name as any] = docs
|
2023-04-04 06:11:49 +00:00
|
|
|
|
} else if (category !== undefined) {
|
|
|
|
|
groupByDocs[category] = docs
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Group category references into categories.
|
|
|
|
|
* @public
|
|
|
|
|
*/
|
|
|
|
|
export async function groupByCategory (
|
|
|
|
|
client: TxOperations,
|
|
|
|
|
_class: Ref<Class<Doc>>,
|
2023-09-04 17:06:34 +00:00
|
|
|
|
space: Ref<Space> | undefined,
|
2023-04-04 06:11:49 +00:00
|
|
|
|
key: string,
|
2023-06-06 10:06:32 +00:00
|
|
|
|
categories: CategoryType[],
|
2023-04-04 06:11:49 +00:00
|
|
|
|
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
|
2023-06-06 10:06:32 +00:00
|
|
|
|
const mixin = h.classHierarchyMixin(attrClass, view.mixin.Groupping)
|
2023-04-04 06:11:49 +00:00
|
|
|
|
let existingCategories: any[] = []
|
|
|
|
|
|
2023-06-06 10:06:32 +00:00
|
|
|
|
if (mixin?.grouppingManager !== undefined) {
|
|
|
|
|
const grouppingManager = await getResource(mixin.grouppingManager)
|
|
|
|
|
existingCategories = grouppingManager.groupByCategories(categories)
|
2023-04-04 06:11:49 +00:00
|
|
|
|
} else {
|
|
|
|
|
const valueSet = new Set<any>()
|
|
|
|
|
for (const v of categories) {
|
|
|
|
|
if (!valueSet.has(v)) {
|
|
|
|
|
valueSet.add(v)
|
|
|
|
|
existingCategories.push(v)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-09-04 17:06:34 +00:00
|
|
|
|
return await sortCategories(client, attrClass, space, existingCategories, viewletDescriptorId)
|
2023-04-04 06:11:49 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-01-14 10:54:54 +00:00
|
|
|
|
export async function getCategories (
|
|
|
|
|
client: TxOperations,
|
|
|
|
|
_class: Ref<Class<Doc>>,
|
2023-09-04 17:06:34 +00:00
|
|
|
|
space: Ref<Space> | undefined,
|
2023-01-14 10:54:54 +00:00
|
|
|
|
docs: Doc[],
|
2023-03-23 05:41:27 +00:00
|
|
|
|
key: string,
|
|
|
|
|
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,
|
2023-09-04 17:06:34 +00:00
|
|
|
|
space,
|
2023-04-04 06:11:49 +00:00
|
|
|
|
key,
|
|
|
|
|
docs.map((it) => getObjectValue(key, it) ?? undefined),
|
|
|
|
|
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(
|
2023-06-06 10:06:32 +00:00
|
|
|
|
(categories.filter((it) => typeof it === 'object') as AggregateValue[]).reduce<Set<Ref<Space>>>((arr, val) => {
|
2023-04-14 14:34:03 +00:00
|
|
|
|
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[] {
|
2023-11-20 10:01:43 +00:00
|
|
|
|
const uniqueValues = new Set<string | number | undefined>()
|
|
|
|
|
const uniqueObjects = new Map<string | number, AggregateValue>()
|
2023-04-11 16:05:01 +00:00
|
|
|
|
|
|
|
|
|
for (const item of arr1) {
|
|
|
|
|
if (typeof item === 'object') {
|
|
|
|
|
const id = item.name
|
2023-06-06 10:06:32 +00:00
|
|
|
|
uniqueObjects.set(id as any, item)
|
2023-04-11 16:05:01 +00:00
|
|
|
|
} else {
|
|
|
|
|
uniqueValues.add(item)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const item of arr2) {
|
|
|
|
|
if (typeof item === 'object') {
|
|
|
|
|
const id = item.name
|
2023-06-06 10:06:32 +00:00
|
|
|
|
if (!uniqueObjects.has(id as any)) {
|
|
|
|
|
uniqueObjects.set(id as any, item)
|
2023-04-11 16:05:01 +00:00
|
|
|
|
}
|
|
|
|
|
} 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-09-04 17:06:34 +00:00
|
|
|
|
client: TxOperations,
|
2023-04-04 06:11:49 +00:00
|
|
|
|
attrClass: Ref<Class<Doc>>,
|
2023-09-04 17:06:34 +00:00
|
|
|
|
space: Ref<Space> | undefined,
|
2023-03-23 05:41:27 +00:00
|
|
|
|
existingCategories: any[],
|
|
|
|
|
viewletDescriptorId?: Ref<ViewletDescriptor>
|
|
|
|
|
): Promise<any[]> {
|
2023-09-04 17:06:34 +00:00
|
|
|
|
const hierarchy = client.getHierarchy()
|
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) {
|
|
|
|
|
return existingCategories
|
|
|
|
|
}
|
2023-01-14 10:54:54 +00:00
|
|
|
|
const f = await getResource(sortFunc.func)
|
2023-09-04 17:06:34 +00:00
|
|
|
|
return await f(client, existingCategories, space, 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.
|
2024-04-12 05:23:32 +00:00
|
|
|
|
await moveToSpace(client, attached, space).catch((err: any) => {
|
|
|
|
|
Analytics.handleError(err)
|
2023-11-20 10:01:43 +00:00
|
|
|
|
console.log('failed to move', name, err)
|
|
|
|
|
})
|
2023-02-07 11:15:59 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
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> {
|
2024-01-04 10:37:11 +00:00
|
|
|
|
const provider = hierarchy.classHierarchyMixin(
|
|
|
|
|
Hierarchy.mixinOrClass(object),
|
|
|
|
|
view.mixin.LinkProvider,
|
|
|
|
|
(m) => hasResource(m.encode) ?? false
|
2023-04-25 07:34:10 +00:00
|
|
|
|
)
|
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()
|
2024-06-18 12:21:51 +00:00
|
|
|
|
const idProvider = hierarchy.classHierarchyMixin(Hierarchy.mixinOrClass(object), view.mixin.LinkIdProvider)
|
|
|
|
|
|
|
|
|
|
let id: string = object._id
|
|
|
|
|
if (idProvider !== undefined) {
|
|
|
|
|
const encodeFn = await getResource(idProvider.encode)
|
|
|
|
|
id = await encodeFn(object)
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-04 10:37:11 +00:00
|
|
|
|
if (hasResource(component) === true) {
|
2024-06-18 12:21:51 +00:00
|
|
|
|
loc.fragment = getPanelURI(component, id, Hierarchy.mixinOrClass(object), 'content')
|
2023-04-25 07:34:10 +00:00
|
|
|
|
}
|
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
|
|
|
|
|
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
|
|
|
|
|
}
|
2023-09-30 16:52:27 +00:00
|
|
|
|
|
|
|
|
|
export async function openDoc (hierarchy: Hierarchy, object: Doc): Promise<void> {
|
|
|
|
|
const panelComponent = hierarchy.classHierarchyMixin(object._class, view.mixin.ObjectPanel)
|
|
|
|
|
const comp = panelComponent?.component ?? view.component.EditDoc
|
|
|
|
|
const loc = await getObjectLinkFragment(hierarchy, object, {}, comp)
|
|
|
|
|
navigate(loc)
|
|
|
|
|
}
|
2023-11-03 09:11:12 +00:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @public
|
|
|
|
|
*/
|
|
|
|
|
export async function getSpacePresenter (
|
|
|
|
|
client: Client,
|
|
|
|
|
_class: Ref<Class<Doc>>
|
|
|
|
|
): Promise<AnySvelteComponent | undefined> {
|
|
|
|
|
const value = client.getHierarchy().classHierarchyMixin(_class, view.mixin.SpacePresenter)
|
|
|
|
|
if (value?.presenter !== undefined) {
|
|
|
|
|
return await getResource(value.presenter)
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-11-27 15:49:53 +00:00
|
|
|
|
|
2023-12-11 11:54:36 +00:00
|
|
|
|
export async function getDocLabel (client: Client, object: Doc | undefined): Promise<string | undefined> {
|
|
|
|
|
if (object === undefined) {
|
|
|
|
|
return undefined
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const hierarchy = client.getHierarchy()
|
|
|
|
|
const name = (object as any).name
|
|
|
|
|
|
|
|
|
|
if (name !== undefined) {
|
|
|
|
|
if (hierarchy.isDerived(object._class, contact.class.Person)) {
|
|
|
|
|
return getName(hierarchy, object as Contact)
|
|
|
|
|
}
|
|
|
|
|
return name
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const label = hierarchy.getClass(object._class).label
|
|
|
|
|
|
|
|
|
|
if (label === undefined) {
|
|
|
|
|
return undefined
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return await translate(label, {}, get(themeStore).language)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function getDocTitle (
|
|
|
|
|
client: Client,
|
|
|
|
|
objectId: Ref<Doc>,
|
|
|
|
|
objectClass: Ref<Class<Doc>>,
|
|
|
|
|
object?: Doc
|
|
|
|
|
): Promise<string | undefined> {
|
|
|
|
|
const hierarchy = client.getHierarchy()
|
|
|
|
|
|
|
|
|
|
const titleProvider = hierarchy.classHierarchyMixin(objectClass, view.mixin.ObjectTitle)
|
|
|
|
|
|
|
|
|
|
if (titleProvider === undefined) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const resource = await getResource(titleProvider.titleProvider)
|
|
|
|
|
|
|
|
|
|
return await resource(client, objectId, object)
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-18 05:44:05 +00:00
|
|
|
|
export async function getDocIdentifier (
|
2023-12-11 11:54:36 +00:00
|
|
|
|
client: Client,
|
|
|
|
|
objectId: Ref<Doc>,
|
|
|
|
|
objectClass: Ref<Class<Doc>>,
|
|
|
|
|
object?: Doc
|
|
|
|
|
): Promise<string | undefined> {
|
|
|
|
|
const hierarchy = client.getHierarchy()
|
|
|
|
|
|
|
|
|
|
const identifierProvider = hierarchy.classHierarchyMixin(objectClass, view.mixin.ObjectIdentifier)
|
|
|
|
|
|
|
|
|
|
if (identifierProvider === undefined) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const resource = await getResource(identifierProvider.provider)
|
|
|
|
|
|
|
|
|
|
return await resource(client, objectId, object)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function getDocLinkTitle (
|
|
|
|
|
client: Client,
|
|
|
|
|
objectId: Ref<Doc>,
|
|
|
|
|
objectClass: Ref<Class<Doc>>,
|
|
|
|
|
object?: Doc
|
|
|
|
|
): Promise<string | undefined> {
|
|
|
|
|
const identifier = await getDocIdentifier(client, objectId, objectClass, object)
|
|
|
|
|
|
|
|
|
|
if (identifier !== undefined) {
|
|
|
|
|
return identifier
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const title = await getDocTitle(client, objectId, objectClass, object)
|
|
|
|
|
|
|
|
|
|
if (title !== undefined) {
|
|
|
|
|
return title
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return await getDocLabel(client, object)
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-27 15:49:53 +00:00
|
|
|
|
/**
|
|
|
|
|
* @public
|
|
|
|
|
*/
|
|
|
|
|
export function getCategoryQueryProjection (
|
|
|
|
|
hierarchy: Hierarchy,
|
|
|
|
|
_class: Ref<Class<Doc>>,
|
|
|
|
|
query: DocumentQuery<Doc>,
|
|
|
|
|
fields: string[]
|
|
|
|
|
): Record<string, number> {
|
|
|
|
|
const res: Record<string, number> = {}
|
|
|
|
|
for (const f of fields) {
|
|
|
|
|
/*
|
|
|
|
|
Mongo projection doesn't support properties fields which
|
|
|
|
|
start from $. Such field here is $search. The least we could do
|
|
|
|
|
is to filter all properties which start from $.
|
|
|
|
|
*/
|
|
|
|
|
if (!f.startsWith('$')) {
|
|
|
|
|
res[f] = 1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for (const f of Object.keys(query)) {
|
|
|
|
|
if (!f.startsWith('$')) {
|
|
|
|
|
res[f] = 1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (hierarchy.isDerived(_class, core.class.AttachedDoc)) {
|
|
|
|
|
res.attachedTo = 1
|
|
|
|
|
res.attachedToClass = 1
|
|
|
|
|
res.collection = 1
|
|
|
|
|
}
|
|
|
|
|
return res
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @public
|
|
|
|
|
*/
|
|
|
|
|
export function getCategoryQueryNoLookup<T extends Doc = Doc> (query: DocumentQuery<T>): DocumentQuery<T> {
|
|
|
|
|
const newQuery: DocumentQuery<T> = {}
|
|
|
|
|
for (const [k, v] of Object.entries(query)) {
|
|
|
|
|
if (!k.startsWith('$lookup.')) {
|
|
|
|
|
;(newQuery as any)[k] = v
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return newQuery
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @public
|
|
|
|
|
*/
|
|
|
|
|
export function getCategoryQueryNoLookupOptions<T extends Doc> (options: FindOptions<T>): FindOptions<T> {
|
|
|
|
|
const { lookup, ...resultOptions } = options
|
|
|
|
|
return resultOptions
|
|
|
|
|
}
|
2023-12-21 15:31:32 +00:00
|
|
|
|
|
|
|
|
|
export async function buildRemovedDoc<T extends Doc> (
|
|
|
|
|
client: Client,
|
|
|
|
|
objectId: Ref<T>,
|
|
|
|
|
_class: Ref<Class<T>>
|
|
|
|
|
): Promise<T | undefined> {
|
|
|
|
|
const txes = await client.findAll<TxCUD<Doc>>(
|
2024-11-15 04:49:35 +00:00
|
|
|
|
core.class.TxCUD,
|
|
|
|
|
{
|
|
|
|
|
objectId
|
|
|
|
|
},
|
2023-12-21 15:31:32 +00:00
|
|
|
|
{ sort: { modifiedOn: 1 } }
|
|
|
|
|
)
|
2024-11-15 04:49:35 +00:00
|
|
|
|
const createTx = txes.find((tx) => tx._class === core.class.TxCreateDoc)
|
2023-12-21 15:31:32 +00:00
|
|
|
|
|
|
|
|
|
if (createTx === undefined) return
|
|
|
|
|
let doc = TxProcessor.createDoc2Doc(createTx as TxCreateDoc<Doc>)
|
|
|
|
|
|
2024-11-15 04:49:35 +00:00
|
|
|
|
for (const tx of txes) {
|
2023-12-21 15:31:32 +00:00
|
|
|
|
if (tx._class === core.class.TxUpdateDoc) {
|
|
|
|
|
doc = TxProcessor.updateDoc2Doc(doc, tx as TxUpdateDoc<Doc>)
|
|
|
|
|
} else if (tx._class === core.class.TxMixin) {
|
|
|
|
|
const mixinTx = tx as TxMixin<Doc, Doc>
|
|
|
|
|
doc = TxProcessor.updateMixin4Doc(doc, mixinTx)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return doc as T
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function getOrBuildObject<T extends Doc> (
|
|
|
|
|
client: Client,
|
|
|
|
|
objectId: Ref<T>,
|
|
|
|
|
objectClass: Ref<Class<T>>
|
|
|
|
|
): Promise<T | undefined> {
|
|
|
|
|
const object = await client.findOne<Doc>(objectClass, { _id: objectId })
|
|
|
|
|
|
|
|
|
|
if (object !== undefined) {
|
|
|
|
|
return object as T
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return await buildRemovedDoc(client, objectId, objectClass)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function checkIsObjectRemoved (
|
|
|
|
|
client: Client,
|
|
|
|
|
objectId: Ref<Doc>,
|
|
|
|
|
objectClass: Ref<Class<Doc>>
|
|
|
|
|
): Promise<boolean> {
|
2024-03-25 03:55:14 +00:00
|
|
|
|
const object = await client.findOne(objectClass, { _id: objectId }, { projection: { _id: 1 } })
|
2023-12-21 15:31:32 +00:00
|
|
|
|
|
|
|
|
|
return object === undefined
|
|
|
|
|
}
|
2024-01-11 14:46:11 +00:00
|
|
|
|
|
|
|
|
|
export function getDocMixins (
|
|
|
|
|
object: Doc,
|
|
|
|
|
showAllMixins = false,
|
|
|
|
|
ignoreMixins = new Set<Ref<Mixin<Doc>>>(),
|
|
|
|
|
objectClass?: Ref<Class<Doc>>
|
|
|
|
|
): Array<Mixin<Doc>> {
|
|
|
|
|
if (object === undefined) {
|
|
|
|
|
return []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const client = getClient()
|
|
|
|
|
const hierarchy = client.getHierarchy()
|
|
|
|
|
|
|
|
|
|
const descendants = hierarchy.getDescendants(core.class.Doc).map((p) => hierarchy.getClass(p))
|
|
|
|
|
const _class = objectClass ?? object._class
|
|
|
|
|
|
|
|
|
|
return descendants.filter(
|
|
|
|
|
(descendant) =>
|
|
|
|
|
descendant.kind === ClassifierKind.MIXIN &&
|
|
|
|
|
!ignoreMixins.has(descendant._id) &&
|
|
|
|
|
(hierarchy.hasMixin(object, descendant._id) ||
|
|
|
|
|
(showAllMixins &&
|
|
|
|
|
hierarchy.isDerived(_class, hierarchy.getBaseClass(descendant._id)) &&
|
|
|
|
|
(descendant.extends !== undefined && hierarchy.isMixin(descendant.extends)
|
|
|
|
|
? hierarchy.hasMixin(object, descendant.extends)
|
|
|
|
|
: true)))
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function classIcon (client: Client, _class: Ref<Class<Obj>>): Asset | undefined {
|
|
|
|
|
return client.getHierarchy().getClass(_class).icon
|
|
|
|
|
}
|
2024-02-23 08:39:49 +00:00
|
|
|
|
|
|
|
|
|
export const restrictionStore = writable<Restrictions>({
|
|
|
|
|
readonly: false,
|
|
|
|
|
disableComments: false,
|
|
|
|
|
disableNavigation: false,
|
|
|
|
|
disableActions: false
|
|
|
|
|
})
|
2024-03-06 08:01:05 +00:00
|
|
|
|
|
|
|
|
|
export async function getDocAttrsInfo (
|
|
|
|
|
mixins: Array<Mixin<Doc>>,
|
|
|
|
|
ignoreKeys: string[],
|
|
|
|
|
_class: Ref<Class<Doc>>,
|
|
|
|
|
allowedCollections: string[] = [],
|
|
|
|
|
collectionArrays: string[] = []
|
|
|
|
|
): Promise<{
|
|
|
|
|
keys: KeyedAttribute[]
|
|
|
|
|
inplaceAttributes: string[]
|
|
|
|
|
editors: Array<{ key: KeyedAttribute, editor: AnyComponent, category: AttributeCategory }>
|
|
|
|
|
}> {
|
|
|
|
|
const client = getClient()
|
|
|
|
|
const hierarchy = client.getHierarchy()
|
|
|
|
|
|
|
|
|
|
const keysMap = new Map(getFiltredKeys(hierarchy, _class, ignoreKeys).map((p) => [p.attr._id, p]))
|
|
|
|
|
for (const m of mixins) {
|
|
|
|
|
const mkeys = getFiltredKeys(hierarchy, m._id, ignoreKeys)
|
|
|
|
|
for (const key of mkeys) {
|
|
|
|
|
keysMap.set(key.attr._id, key)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const filteredKeys = Array.from(keysMap.values())
|
|
|
|
|
const { attributes, collections } = categorizeFields(hierarchy, filteredKeys, collectionArrays, allowedCollections)
|
|
|
|
|
|
|
|
|
|
const keys = attributes.map((it) => it.key)
|
|
|
|
|
const editors: Array<{ key: KeyedAttribute, editor: AnyComponent, category: AttributeCategory }> = []
|
|
|
|
|
const inplaceAttributes: string[] = []
|
|
|
|
|
|
|
|
|
|
for (const k of collections) {
|
|
|
|
|
if (allowedCollections.includes(k.key.key)) continue
|
|
|
|
|
const editor = await getAttrEditor(k.key, hierarchy)
|
|
|
|
|
if (editor === undefined) continue
|
|
|
|
|
if (k.category === 'inplace') {
|
|
|
|
|
inplaceAttributes.push(k.key.key)
|
|
|
|
|
}
|
|
|
|
|
editors.push({ key: k.key, editor, category: k.category })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
keys,
|
|
|
|
|
inplaceAttributes,
|
|
|
|
|
editors: editors.sort((a, b) => AttributeCategoryOrder[a.category] - AttributeCategoryOrder[b.category])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function getAttrEditor (key: KeyedAttribute, hierarchy: Hierarchy): Promise<AnyComponent | undefined> {
|
|
|
|
|
const attrClass = getAttributePresenterClass(hierarchy, key.attr)
|
|
|
|
|
const clazz = hierarchy.getClass(attrClass.attrClass)
|
|
|
|
|
const mix = {
|
|
|
|
|
array: view.mixin.ArrayEditor,
|
|
|
|
|
collection: view.mixin.CollectionEditor,
|
|
|
|
|
inplace: view.mixin.InlineAttributEditor,
|
|
|
|
|
attribute: view.mixin.AttributeEditor,
|
|
|
|
|
object: undefined as any
|
|
|
|
|
}
|
|
|
|
|
const mixinRef = mix[attrClass.category]
|
|
|
|
|
if (mixinRef !== undefined) {
|
|
|
|
|
const editorMixin = hierarchy.as(clazz, mixinRef)
|
|
|
|
|
return (editorMixin as any).editor
|
|
|
|
|
} else {
|
|
|
|
|
return undefined
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-03-27 08:49:36 +00:00
|
|
|
|
|
|
|
|
|
type PermissionsBySpace = Record<Ref<Space>, Set<Ref<Permission>>>
|
2024-03-29 05:01:33 +00:00
|
|
|
|
type AccountsByPermission = Record<Ref<Space>, Record<Ref<Permission>, Set<Ref<Account>>>>
|
2024-04-04 15:41:28 +00:00
|
|
|
|
export interface PermissionsStore {
|
2024-03-27 08:49:36 +00:00
|
|
|
|
ps: PermissionsBySpace
|
2024-03-29 05:01:33 +00:00
|
|
|
|
ap: AccountsByPermission
|
2024-03-27 08:49:36 +00:00
|
|
|
|
whitelist: Set<Ref<Space>>
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-04 15:41:28 +00:00
|
|
|
|
export function checkMyPermission (_id: Ref<Permission>, space: Ref<TypedSpace>, store: PermissionsStore): boolean {
|
|
|
|
|
return (store.whitelist.has(space) || store.ps[space]?.has(_id)) ?? false
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-12 12:48:35 +00:00
|
|
|
|
export const accessDeniedStore = writable<boolean>(false)
|
|
|
|
|
|
2024-03-27 08:49:36 +00:00
|
|
|
|
export const permissionsStore = writable<PermissionsStore>({
|
|
|
|
|
ps: {},
|
2024-03-29 05:01:33 +00:00
|
|
|
|
ap: {},
|
2024-03-27 08:49:36 +00:00
|
|
|
|
whitelist: new Set()
|
|
|
|
|
})
|
2024-08-03 06:10:44 +00:00
|
|
|
|
|
2024-08-15 10:30:25 +00:00
|
|
|
|
const spaceSpaceQuery = createQuery(true)
|
|
|
|
|
|
|
|
|
|
export const spaceSpace = writable<TypedSpace | undefined>(undefined)
|
|
|
|
|
|
|
|
|
|
spaceSpaceQuery.query(core.class.TypedSpace, { _id: core.space.Space }, (res) => {
|
|
|
|
|
spaceSpace.set(res[0])
|
|
|
|
|
})
|
|
|
|
|
|
2024-08-03 06:10:44 +00:00
|
|
|
|
const spaceTypesQuery = createQuery(true)
|
2024-03-27 08:49:36 +00:00
|
|
|
|
const permissionsQuery = createQuery(true)
|
2024-08-03 06:10:44 +00:00
|
|
|
|
type TargetClassesProjection = Record<Ref<Class<Space>>, number>
|
2024-03-27 08:49:36 +00:00
|
|
|
|
|
2024-08-03 06:10:44 +00:00
|
|
|
|
spaceTypesQuery.query(core.class.SpaceType, {}, (types) => {
|
|
|
|
|
const targetClasses = types.reduce<TargetClassesProjection>((acc, st) => {
|
|
|
|
|
acc[st.targetClass] = 1
|
|
|
|
|
return acc
|
|
|
|
|
}, {})
|
2024-03-27 08:49:36 +00:00
|
|
|
|
|
2024-08-03 06:10:44 +00:00
|
|
|
|
permissionsQuery.query(
|
|
|
|
|
core.class.Space,
|
|
|
|
|
{},
|
|
|
|
|
(res) => {
|
|
|
|
|
const whitelistedSpaces = new Set<Ref<Space>>()
|
|
|
|
|
const permissionsBySpace: PermissionsBySpace = {}
|
|
|
|
|
const accountsByPermission: AccountsByPermission = {}
|
|
|
|
|
const client = getClient()
|
|
|
|
|
const hierarchy = client.getHierarchy()
|
|
|
|
|
const me = getCurrentAccount()
|
|
|
|
|
|
|
|
|
|
for (const s of res) {
|
|
|
|
|
if (hierarchy.isDerived(s._class, core.class.TypedSpace)) {
|
|
|
|
|
const type = client.getModel().findAllSync(core.class.SpaceType, { _id: (s as TypedSpace).type })[0]
|
|
|
|
|
const mixin = type?.targetClass
|
|
|
|
|
|
|
|
|
|
if (mixin === undefined) {
|
|
|
|
|
permissionsBySpace[s._id] = new Set()
|
|
|
|
|
accountsByPermission[s._id] = {}
|
|
|
|
|
continue
|
|
|
|
|
}
|
2024-03-27 08:49:36 +00:00
|
|
|
|
|
2024-08-03 06:10:44 +00:00
|
|
|
|
const asMixin = hierarchy.as(s, mixin)
|
|
|
|
|
const roles = client.getModel().findAllSync(core.class.Role, { attachedTo: type._id })
|
|
|
|
|
const myRoles = roles.filter((r) => ((asMixin as any)[r._id] ?? []).includes(me._id))
|
|
|
|
|
permissionsBySpace[s._id] = new Set(myRoles.flatMap((r) => r.permissions))
|
2024-03-27 08:49:36 +00:00
|
|
|
|
|
2024-08-03 06:10:44 +00:00
|
|
|
|
accountsByPermission[s._id] = {}
|
2024-03-29 05:01:33 +00:00
|
|
|
|
|
2024-08-03 06:10:44 +00:00
|
|
|
|
for (const role of roles) {
|
|
|
|
|
const assignment: Array<Ref<Account>> = (asMixin as any)[role._id] ?? []
|
2024-03-29 05:01:33 +00:00
|
|
|
|
|
2024-08-03 06:10:44 +00:00
|
|
|
|
if (assignment.length === 0) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
2024-03-29 05:01:33 +00:00
|
|
|
|
|
2024-08-03 06:10:44 +00:00
|
|
|
|
for (const permissionId of role.permissions) {
|
|
|
|
|
if (accountsByPermission[s._id][permissionId] === undefined) {
|
|
|
|
|
accountsByPermission[s._id][permissionId] = new Set()
|
|
|
|
|
}
|
2024-03-29 05:01:33 +00:00
|
|
|
|
|
2024-08-03 06:10:44 +00:00
|
|
|
|
assignment.forEach((acc) => accountsByPermission[s._id][permissionId].add(acc))
|
|
|
|
|
}
|
2024-03-29 05:01:33 +00:00
|
|
|
|
}
|
2024-08-03 06:10:44 +00:00
|
|
|
|
} else {
|
|
|
|
|
whitelistedSpaces.add(s._id)
|
2024-03-29 05:01:33 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2024-03-27 08:49:36 +00:00
|
|
|
|
|
2024-08-03 06:10:44 +00:00
|
|
|
|
permissionsStore.set({
|
|
|
|
|
ps: permissionsBySpace,
|
|
|
|
|
ap: accountsByPermission,
|
|
|
|
|
whitelist: whitelistedSpaces
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
projection: {
|
|
|
|
|
_id: 1,
|
|
|
|
|
type: 1,
|
|
|
|
|
...targetClasses
|
|
|
|
|
} as any
|
|
|
|
|
}
|
|
|
|
|
)
|
2024-03-27 08:49:36 +00:00
|
|
|
|
})
|
2024-04-15 11:11:47 +00:00
|
|
|
|
|
|
|
|
|
export function getCollaborationUser (): CollaborationUser {
|
|
|
|
|
const me = getCurrentAccount() as PersonAccount
|
|
|
|
|
const color = getColorNumberByText(me.email)
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
id: me._id,
|
|
|
|
|
name: me.email,
|
|
|
|
|
email: me.email,
|
|
|
|
|
color
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-06-18 12:21:51 +00:00
|
|
|
|
|
|
|
|
|
export async function getObjectLinkId (
|
|
|
|
|
providers: LinkIdProvider[],
|
|
|
|
|
_id: Ref<Doc>,
|
|
|
|
|
_class: Ref<Class<Doc>>,
|
|
|
|
|
doc?: Doc
|
|
|
|
|
): Promise<string> {
|
|
|
|
|
const provider = providers.find(({ _id }) => _id === _class)
|
|
|
|
|
|
|
|
|
|
if (provider === undefined) {
|
|
|
|
|
return _id
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const client = getClient()
|
|
|
|
|
const object = doc ?? (await client.findOne(_class, { _id }))
|
|
|
|
|
|
|
|
|
|
if (object === undefined) {
|
|
|
|
|
return _id
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const encodeFn = await getResource(provider.encode)
|
|
|
|
|
return await encodeFn(object)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function parseLinkId<T extends Doc> (
|
|
|
|
|
providers: LinkIdProvider[],
|
|
|
|
|
id: string,
|
|
|
|
|
_class: Ref<Class<T>>
|
|
|
|
|
): Promise<Ref<T>> {
|
2024-06-21 19:17:23 +00:00
|
|
|
|
const hierarchy = getClient().getHierarchy()
|
|
|
|
|
const provider =
|
|
|
|
|
providers.find(({ _id }) => id === _class) ?? providers.find(({ _id }) => hierarchy.isDerived(_class, _id))
|
2024-06-18 12:21:51 +00:00
|
|
|
|
|
|
|
|
|
if (provider === undefined) {
|
|
|
|
|
return id as Ref<T>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const decodeFn = await getResource(provider.decode)
|
|
|
|
|
const _id = await decodeFn(id)
|
|
|
|
|
|
|
|
|
|
return (_id ?? id) as Ref<T>
|
|
|
|
|
}
|
2024-08-14 07:37:52 +00:00
|
|
|
|
|
|
|
|
|
export async function getObjectId (object: Doc, hierarchy: Hierarchy): Promise<string> {
|
|
|
|
|
const idProvider = hierarchy.classHierarchyMixin(Hierarchy.mixinOrClass(object), view.mixin.LinkIdProvider)
|
|
|
|
|
|
|
|
|
|
if (idProvider !== undefined) {
|
|
|
|
|
const encodeFn = await getResource(idProvider.encode)
|
|
|
|
|
return await encodeFn(object)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return object._id
|
|
|
|
|
}
|