mirror of
https://github.com/hcengineering/platform.git
synced 2026-08-21 03:42:24 +02:00
* feat: implement reference versioning functionality across various components and plugins Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com> * fix: simplify provider function calls and label retrieval in MentionPopup component Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com> * refactor: improve readability of version selection and reference handling in MentionVersionPopup and reference.ts Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com> * feat: add CardReferenceObjectProvider and integrate into card model and resources Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com> --------- Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
874 lines
26 KiB
TypeScript
874 lines
26 KiB
TypeScript
// Copyright © 2025 Hardcore Engineering Inc.
|
|
//
|
|
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License. You may
|
|
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
//
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
import { type AccountClient, getClient as getAccountClientRaw } from '@hcengineering/account-client'
|
|
import { Analytics } from '@hcengineering/analytics'
|
|
import { type Card, CardEvents, cardId, type CardSpace, type MasterTag, type Tag } from '@hcengineering/card'
|
|
import { chatId } from '@hcengineering/chat'
|
|
import communication from '@hcengineering/communication'
|
|
import { type PermissionsStore } from '@hcengineering/contact'
|
|
import core, {
|
|
AccountRole,
|
|
type Class,
|
|
type ClassPermission,
|
|
type Client,
|
|
type Data,
|
|
type Doc,
|
|
type DocumentQuery,
|
|
fillDefaults,
|
|
generateId,
|
|
getCurrentAccount,
|
|
hasAccountRole,
|
|
type Hierarchy,
|
|
makeCollabId,
|
|
makeDocCollabId,
|
|
type Markup,
|
|
type MarkupBlobRef,
|
|
type Mixin,
|
|
type Ref,
|
|
type RelatedDocument,
|
|
type Space,
|
|
toRank,
|
|
type TxOperations,
|
|
type WithLookup
|
|
} from '@hcengineering/core'
|
|
import login from '@hcengineering/login'
|
|
import { getMetadata, translate } from '@hcengineering/platform'
|
|
import presentation, {
|
|
createMarkup,
|
|
getClient,
|
|
getMarkup,
|
|
IconWithEmoji,
|
|
MessageBox,
|
|
type ObjectSearchResult
|
|
} from '@hcengineering/presentation'
|
|
import { EmptyMarkup, isEmptyMarkup } from '@hcengineering/text'
|
|
import {
|
|
getCurrentLocation,
|
|
getCurrentResolvedLocation,
|
|
getPanelURI,
|
|
type IconComponent,
|
|
type IconProps,
|
|
type Location,
|
|
navigate,
|
|
type ResolvedLocation,
|
|
showPopup
|
|
} from '@hcengineering/ui'
|
|
import view, { canCopyLink, encodeObjectURI } from '@hcengineering/view'
|
|
import { accessDeniedStore } from '@hcengineering/view-resources'
|
|
import workbench, { type LocationData, type Widget, type WidgetTab } from '@hcengineering/workbench'
|
|
import { createWidgetTab } from '@hcengineering/workbench-resources'
|
|
|
|
import attachment from '@hcengineering/attachment'
|
|
import { makeRank } from '@hcengineering/rank'
|
|
import { writable } from 'svelte/store'
|
|
import CardSearchItem from './components/CardSearchItem.svelte'
|
|
import CreateSpace from './components/navigator/CreateSpace.svelte'
|
|
import card from './plugin'
|
|
import { type NavigatorConfig } from './types'
|
|
|
|
export async function deleteMasterTag (tag: MasterTag | undefined, onDelete?: () => void): Promise<void> {
|
|
if (tag !== undefined) {
|
|
const client = getClient()
|
|
if (tag._class === card.class.MasterTag) {
|
|
showPopup(MessageBox, {
|
|
label: card.string.DeleteMasterTag,
|
|
message: card.string.DeleteMasterTagConfirm,
|
|
action: async () => {
|
|
onDelete?.()
|
|
await client.update(tag, { removed: true })
|
|
}
|
|
})
|
|
} else {
|
|
showPopup(MessageBox, {
|
|
label: card.string.DeleteTag,
|
|
message: card.string.DeleteTagConfirm,
|
|
action: async () => {
|
|
onDelete?.()
|
|
await client.remove(tag)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function createTypePermissions (masterTag: MasterTag | Tag): Promise<void> {
|
|
const client = getClient()
|
|
const hierarchy = client.getHierarchy()
|
|
const isMixin = hierarchy.isMixin(masterTag._id)
|
|
const objectClass = hierarchy.getBaseClass(masterTag._id)
|
|
const txClass = isMixin ? core.class.TxMixin : core.class.TxUpdateDoc
|
|
|
|
await client.createDoc(
|
|
core.class.ClassPermission,
|
|
core.space.Model,
|
|
{
|
|
objectClass,
|
|
txClass,
|
|
txMatch: {
|
|
[isMixin ? 'mixin' : 'objectClass']: masterTag._id
|
|
},
|
|
scope: 'space',
|
|
forbid: false,
|
|
label: view.string.AllowAttributeChanges,
|
|
description: masterTag.label,
|
|
targetClass: masterTag._id
|
|
},
|
|
`${masterTag._id}_allowed` as Ref<ClassPermission>
|
|
)
|
|
await client.createDoc(
|
|
core.class.ClassPermission,
|
|
core.space.Model,
|
|
{
|
|
objectClass,
|
|
txClass,
|
|
txMatch: {
|
|
[isMixin ? 'mixin' : 'objectClass']: masterTag._id
|
|
},
|
|
scope: 'space',
|
|
forbid: true,
|
|
label: view.string.ForbidAttributeChanges,
|
|
description: masterTag.label,
|
|
targetClass: masterTag._id
|
|
},
|
|
`${masterTag._id}_forbidden` as Ref<ClassPermission>
|
|
)
|
|
|
|
if (isMixin) {
|
|
await client.createDoc(
|
|
core.class.ClassPermission,
|
|
core.space.Model,
|
|
{
|
|
objectClass,
|
|
txClass: core.class.TxMixin,
|
|
txMatch: {
|
|
mixin: masterTag._id
|
|
},
|
|
scope: 'space',
|
|
forbid: false,
|
|
label: card.string.AddTagPermission,
|
|
description: masterTag.label,
|
|
targetClass: masterTag._id
|
|
},
|
|
`${masterTag._id}_create_allowed` as Ref<ClassPermission>
|
|
)
|
|
await client.createDoc(
|
|
core.class.ClassPermission,
|
|
core.space.Model,
|
|
{
|
|
objectClass,
|
|
txClass: core.class.TxMixin,
|
|
txMatch: {
|
|
mixin: masterTag._id
|
|
},
|
|
scope: 'space',
|
|
forbid: true,
|
|
label: card.string.ForbidAddTagPermission,
|
|
description: masterTag.label,
|
|
targetClass: masterTag._id
|
|
},
|
|
`${masterTag._id}_create_forbidden` as Ref<ClassPermission>
|
|
)
|
|
const key = `operations.$unset.${masterTag._id}`
|
|
await client.createDoc(
|
|
core.class.ClassPermission,
|
|
core.space.Model,
|
|
{
|
|
objectClass,
|
|
txClass: core.class.TxUpdateDoc,
|
|
txMatch: {
|
|
[key]: {
|
|
$exists: true
|
|
}
|
|
},
|
|
scope: 'space',
|
|
forbid: false,
|
|
label: card.string.RemoveTag,
|
|
description: masterTag.label,
|
|
targetClass: masterTag._id
|
|
},
|
|
`${masterTag._id}_remove_allowed` as Ref<ClassPermission>
|
|
)
|
|
await client.createDoc(
|
|
core.class.ClassPermission,
|
|
core.space.Model,
|
|
{
|
|
objectClass,
|
|
txClass: core.class.TxUpdateDoc,
|
|
txMatch: {
|
|
[key]: { $exists: true }
|
|
},
|
|
scope: 'space',
|
|
forbid: true,
|
|
label: card.string.ForbidRemoveTag,
|
|
description: masterTag.label,
|
|
targetClass: masterTag._id
|
|
},
|
|
`${masterTag._id}_remove_forbidden` as Ref<ClassPermission>
|
|
)
|
|
} else {
|
|
await client.createDoc(
|
|
core.class.ClassPermission,
|
|
core.space.Model,
|
|
{
|
|
objectClass,
|
|
txClass: core.class.TxCreateDoc,
|
|
scope: 'space',
|
|
forbid: false,
|
|
label: card.string.CreateCardPermission,
|
|
description: masterTag.label,
|
|
targetClass: masterTag._id
|
|
},
|
|
`${masterTag._id}_create_allowed` as Ref<ClassPermission>
|
|
)
|
|
await client.createDoc(
|
|
core.class.ClassPermission,
|
|
core.space.Model,
|
|
{
|
|
objectClass,
|
|
txClass: core.class.TxCreateDoc,
|
|
scope: 'space',
|
|
forbid: true,
|
|
label: card.string.ForbidCreateCardPermission,
|
|
description: masterTag.label,
|
|
targetClass: masterTag._id
|
|
},
|
|
`${masterTag._id}_create_forbidden` as Ref<ClassPermission>
|
|
)
|
|
await client.createDoc(
|
|
core.class.ClassPermission,
|
|
core.space.Model,
|
|
{
|
|
objectClass,
|
|
txClass: core.class.TxRemoveDoc,
|
|
scope: 'space',
|
|
forbid: false,
|
|
label: card.string.RemoveCard,
|
|
description: masterTag.label,
|
|
targetClass: masterTag._id
|
|
},
|
|
`${masterTag._id}_remove_allowed` as Ref<ClassPermission>
|
|
)
|
|
await client.createDoc(
|
|
core.class.ClassPermission,
|
|
core.space.Model,
|
|
{
|
|
objectClass,
|
|
txClass: core.class.TxRemoveDoc,
|
|
txMatch: {
|
|
objectClass: masterTag._id
|
|
},
|
|
scope: 'space',
|
|
forbid: true,
|
|
label: card.string.ForbidRemoveCard,
|
|
description: masterTag.label,
|
|
targetClass: masterTag._id
|
|
},
|
|
`${masterTag._id}_remove_forbidden` as Ref<ClassPermission>
|
|
)
|
|
}
|
|
}
|
|
|
|
interface CopySettings {
|
|
excludedProperties?: string[]
|
|
excludedRelations?: string[] // ${associationId}_${a|b}
|
|
excludeMixins?: Array<Ref<Mixin<Doc>>>
|
|
}
|
|
|
|
async function cloneCard (
|
|
origin: Card,
|
|
overrideProps: Record<string, any>,
|
|
config?: CopySettings,
|
|
copyIds: boolean = false
|
|
): Promise<Ref<Card>> {
|
|
const client = getClient()
|
|
const h = client.getHierarchy()
|
|
const props: Partial<Data<Card>> = {}
|
|
const base = h.getBaseClass(origin._class)
|
|
const mixins = h.findAllMixins(origin)
|
|
const attrs = h.getAllAttributes(base, core.class.Doc)
|
|
const skipClasses = copyIds
|
|
? [core.class.TypeCollaborativeDoc]
|
|
: [core.class.TypeCollaborativeDoc, core.class.TypeIdentifier]
|
|
const systemFields = ['_class', 'id', 'createdOn', 'modifiedOn', 'modifiedBy', 'createdBy', 'createdOn', 'rank']
|
|
|
|
for (const [key, attr] of attrs) {
|
|
if (config?.excludedProperties?.includes(key) === true || systemFields.includes(key)) {
|
|
continue
|
|
}
|
|
if (attr.type._class === core.class.Collection) {
|
|
;(props as any)[key] = 0
|
|
} else if (!skipClasses.includes(attr.type._class)) {
|
|
;(props as any)[key] = (origin as any)[key]
|
|
}
|
|
}
|
|
for (const [k, v] of Object.entries(overrideProps)) {
|
|
;(props as any)[k] = v
|
|
}
|
|
props.rank = makeRank(origin.rank, undefined)
|
|
const targetId = generateId<Card>()
|
|
const relationsA = await client.findAll(core.class.Relation, { docA: origin._id })
|
|
const relationsB = await client.findAll(core.class.Relation, { docB: origin._id })
|
|
|
|
if (config?.excludedProperties?.includes('content') !== true) {
|
|
const markup = await getMarkup(makeDocCollabId(origin, 'content'), origin.content)
|
|
if (!isEmptyMarkup(markup)) {
|
|
const collabId = makeCollabId(base, targetId, 'content')
|
|
props.content = await createMarkup(collabId, markup)
|
|
}
|
|
}
|
|
|
|
const ops = client.apply(`Duplicate_card_${origin._id}`)
|
|
await ops.createDoc(base, origin.space, props, targetId)
|
|
for (const mixin of mixins) {
|
|
if (config?.excludeMixins?.includes(mixin) === true) {
|
|
continue
|
|
}
|
|
const mixinAttrs = h.getOwnAttributes(mixin)
|
|
const as = h.as(origin, mixin)
|
|
const attributes: Partial<Data<Doc>> = {}
|
|
for (const [key] of mixinAttrs) {
|
|
;(attributes as any)[key] = (as as any)[key]
|
|
}
|
|
await ops.createMixin(targetId, base, origin.space, mixin, attributes)
|
|
}
|
|
|
|
for (const rel of relationsA) {
|
|
if (config?.excludedRelations?.includes(`${rel.association}_b`) !== true) {
|
|
await ops.createDoc(core.class.Relation, core.space.Workspace, {
|
|
docA: targetId,
|
|
docB: rel.docB,
|
|
association: rel.association
|
|
})
|
|
}
|
|
}
|
|
for (const rel of relationsB) {
|
|
if (config?.excludedRelations?.includes(`${rel.association}_a`) !== true) {
|
|
await ops.createDoc(core.class.Relation, core.space.Workspace, {
|
|
docA: rel.docA,
|
|
docB: targetId,
|
|
association: rel.association
|
|
})
|
|
}
|
|
}
|
|
await ops.commit()
|
|
|
|
if (config?.excludedProperties?.includes('attachments') !== true) {
|
|
const attachments = await client.findAll(attachment.class.Attachment, { attachedTo: origin._id })
|
|
const attachmentOps = client.apply(`Duplicate_attachments_${origin._id}`)
|
|
for (const att of attachments) {
|
|
const { _id, modifiedBy, modifiedOn, attachedTo, attachedToClass, collection, space, ...props } = att
|
|
await attachmentOps.addCollection(attachment.class.Attachment, origin.space, targetId, base, 'attachments', props)
|
|
}
|
|
await attachmentOps.commit()
|
|
}
|
|
|
|
return targetId
|
|
}
|
|
|
|
export async function duplicateCard (origin: Card, config?: CopySettings): Promise<void> {
|
|
const targetId = await cloneCard(
|
|
origin,
|
|
{
|
|
title: `${origin.title} (Copy)`
|
|
},
|
|
config
|
|
)
|
|
|
|
const loc = getCurrentLocation()
|
|
loc.path[2] = cardId
|
|
loc.path[3] = targetId
|
|
loc.path.length = 4
|
|
navigate(loc)
|
|
}
|
|
|
|
export async function resolveLocation (loc: Location): Promise<ResolvedLocation | undefined> {
|
|
if (loc.path[2] !== cardId) {
|
|
return undefined
|
|
}
|
|
|
|
const id = loc.path[3]
|
|
const specialItems = ['browser', 'type', 'all']
|
|
if (loc.path[4] === undefined && id !== undefined && !specialItems.includes(id)) {
|
|
return await generateLocation(loc, id)
|
|
}
|
|
}
|
|
|
|
export async function editSpace (value: CardSpace | undefined): Promise<void> {
|
|
if (value !== undefined) {
|
|
showPopup(CreateSpace, { space: value })
|
|
}
|
|
}
|
|
|
|
async function generateLocation (loc: Location, id: string): Promise<ResolvedLocation | undefined> {
|
|
const client = getClient()
|
|
const doc = await client.findOne(card.class.Card, { _id: id as Ref<Card> })
|
|
if (doc === undefined) {
|
|
accessDeniedStore.set(true)
|
|
return undefined
|
|
}
|
|
const appComponent = loc.path[0] ?? ''
|
|
const workspace = loc.path[1] ?? ''
|
|
const special = doc._class
|
|
|
|
const objectPanel = client.getHierarchy().classHierarchyMixin(doc._class as Ref<Class<Doc>>, view.mixin.ObjectPanel)
|
|
const component = objectPanel?.component ?? view.component.EditDoc
|
|
|
|
return {
|
|
loc: {
|
|
path: [appComponent, workspace],
|
|
fragment: getPanelURI(component, doc._id, doc._class, 'content')
|
|
},
|
|
defaultLocation: {
|
|
path: [appComponent, workspace, cardId, doc.space, special],
|
|
fragment: getPanelURI(component, doc._id, doc._class, 'content')
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function resolveLocationData (loc: Location): Promise<LocationData> {
|
|
const special = loc.path[3]
|
|
const base = { nameIntl: card.string.Cards }
|
|
if (special == null) {
|
|
return base
|
|
}
|
|
|
|
if (special === 'cards') {
|
|
return base
|
|
}
|
|
|
|
const client = getClient()
|
|
const object = await client.findOne(card.class.Card, { _id: special as Ref<Card> })
|
|
|
|
if (object === undefined) {
|
|
return base
|
|
}
|
|
|
|
return { name: object.title }
|
|
}
|
|
|
|
export async function getCardTitle (client: TxOperations, ref: Ref<Card>, doc?: Card): Promise<string> {
|
|
const object = doc ?? (await client.findOne(card.class.Card, { _id: ref }))
|
|
if (object === undefined) throw new Error(`Card not found, _id: ${ref}`)
|
|
const h = client.getHierarchy()
|
|
const attrs = [...h.getAllAttributes(object._class, core.class.Doc).values()].sort((a, b) => {
|
|
const rankA = a.rank ?? toRank(a._id) ?? ''
|
|
const rankB = b.rank ?? toRank(b._id) ?? ''
|
|
return rankA.localeCompare(rankB)
|
|
})
|
|
const res: string[] = []
|
|
for (const attr of attrs) {
|
|
const val = (object as any)[attr.name]
|
|
if (attr.showInPresenter === true && val !== undefined) {
|
|
if (typeof val === 'string' || typeof val === 'number') {
|
|
res.push(val.toString())
|
|
} else if (typeof val === 'boolean') {
|
|
res.push(val ? '✅' : '❌️')
|
|
}
|
|
}
|
|
}
|
|
|
|
const ids = res.join(' ')
|
|
let version = ''
|
|
if (h.classHierarchyMixin(object._class, core.mixin.VersionableClass)?.enabled === true) {
|
|
version = `v${object.version ?? 1}`
|
|
}
|
|
return ids + ' ' + object.title + ' ' + version
|
|
}
|
|
|
|
export async function cardReferenceObjectProvider<T extends Doc> (
|
|
client: Client,
|
|
ref: Ref<T>,
|
|
doc?: T
|
|
): Promise<Doc | undefined> {
|
|
const object =
|
|
(doc as unknown as Card | undefined) ?? (await client.findOne(card.class.Card, { _id: ref as any as Ref<Card> }))
|
|
if (object === undefined) return
|
|
|
|
const versioningEnabled = client
|
|
.getHierarchy()
|
|
.classHierarchyMixin(object._class, core.mixin.VersionableClass)?.enabled
|
|
if (versioningEnabled !== true) return object
|
|
|
|
const baseId = object.baseId ?? object._id
|
|
if (object.isLatest === true) return object
|
|
|
|
return (await client.findOne(object._class, { baseId, isLatest: true } as any)) ?? object
|
|
}
|
|
|
|
export async function getCardLink (doc: Card): Promise<Location> {
|
|
const loc = getCurrentResolvedLocation()
|
|
loc.path.length = 2
|
|
loc.fragment = undefined
|
|
loc.query = undefined
|
|
loc.path[2] = cardId
|
|
loc.path[3] = doc._id
|
|
|
|
return loc
|
|
}
|
|
|
|
export async function queryCard (
|
|
client: Client,
|
|
search: string,
|
|
filter?: { in?: RelatedDocument[], nin?: RelatedDocument[] }
|
|
): Promise<ObjectSearchResult[]> {
|
|
const q: DocumentQuery<Card> = { title: { $like: `%${search}%` } }
|
|
if (filter?.in !== undefined || filter?.nin !== undefined) {
|
|
q._id = {}
|
|
if (filter.in !== undefined) {
|
|
q._id.$in = filter.in?.map((it) => it._id as Ref<Card>)
|
|
}
|
|
if (filter.nin !== undefined) {
|
|
q._id.$nin = filter.nin?.map((it) => it._id as Ref<Card>)
|
|
}
|
|
}
|
|
return (await client.findAll(card.class.Card, q, { limit: 200 })).map(toCardObjectSearchResult)
|
|
}
|
|
|
|
const toCardObjectSearchResult = (e: WithLookup<Card>): ObjectSearchResult => ({
|
|
doc: e,
|
|
title: e.title,
|
|
icon: card.icon.Card,
|
|
component: CardSearchItem
|
|
})
|
|
|
|
export async function cardFactory (props: Record<string, any> = {}): Promise<Ref<Card> | undefined> {
|
|
const _class = props._class as Ref<MasterTag> | undefined
|
|
const space = props.space as Ref<Space> | undefined
|
|
|
|
if (_class === undefined || space === undefined) {
|
|
return undefined
|
|
}
|
|
|
|
if (isBaseTypeWithSubtypes(getClient().getHierarchy(), _class)) {
|
|
return undefined
|
|
}
|
|
|
|
return await createCard(_class, space, props.data, props.content)
|
|
}
|
|
|
|
export async function createNewVersion (card: Card): Promise<Ref<Card>> {
|
|
const client = getClient()
|
|
const mixin = client.getHierarchy().classHierarchyMixin(card._class, core.mixin.VersionableClass)
|
|
|
|
return await cloneCard(
|
|
card,
|
|
{
|
|
baseId: card.baseId,
|
|
docCreatedBy: card.docCreatedBy ?? card.createdBy ?? card.modifiedBy
|
|
},
|
|
mixin,
|
|
true
|
|
)
|
|
}
|
|
|
|
export async function createCard (
|
|
type: Ref<MasterTag>,
|
|
space: Ref<Space>,
|
|
data: Partial<Data<Card>> = {},
|
|
contentMarkup: Markup = EmptyMarkup,
|
|
id?: Ref<Card>
|
|
): Promise<Ref<Card>> {
|
|
const client = getClient()
|
|
const hierarchy = client.getHierarchy()
|
|
|
|
if (isBaseTypeWithSubtypes(hierarchy, type)) {
|
|
throw new Error(`Cannot create card with base type ${type}`)
|
|
}
|
|
|
|
const title = data.title ?? (await translate(card.string.Card, {}))
|
|
|
|
const _id = id ?? generateId()
|
|
const content = isEmptyMarkup(contentMarkup)
|
|
? ('' as MarkupBlobRef)
|
|
: await createMarkup(makeCollabId(type, _id, 'content'), contentMarkup)
|
|
|
|
const _data: Data<Card> = {
|
|
parentInfo: [],
|
|
blobs: {},
|
|
...data,
|
|
title,
|
|
rank: '',
|
|
content
|
|
}
|
|
|
|
const filledData = fillDefaults(hierarchy, _data, type)
|
|
|
|
await client.createDoc(type, space, filledData, _id)
|
|
|
|
Analytics.handleEvent(CardEvents.CardCreated)
|
|
return _id
|
|
}
|
|
|
|
export function isBaseTypeWithSubtypes (hierarchy: Hierarchy, type: Ref<MasterTag>): boolean {
|
|
const clazz = hierarchy.getClass(type) as MasterTag | undefined
|
|
if (clazz?.baseType !== true) return false
|
|
|
|
return hierarchy.getDescendants(type).some((descendant) => {
|
|
if (descendant === type || hierarchy.isMixin(descendant)) return false
|
|
const descendantClass = hierarchy.getClass(descendant) as MasterTag | undefined
|
|
return descendantClass?._class === card.class.MasterTag && descendantClass.removed !== true
|
|
})
|
|
}
|
|
|
|
export function getFirstCreatableSubtype (hierarchy: Hierarchy, type: Ref<MasterTag>): Ref<MasterTag> | undefined {
|
|
return hierarchy.getDescendants(type).find((descendant) => {
|
|
if (descendant === type || hierarchy.isMixin(descendant)) return false
|
|
const descendantClass = hierarchy.getClass(descendant) as MasterTag | undefined
|
|
return (
|
|
descendantClass?._class === card.class.MasterTag &&
|
|
descendantClass.removed !== true &&
|
|
!isBaseTypeWithSubtypes(hierarchy, descendant as Ref<MasterTag>)
|
|
)
|
|
}) as Ref<MasterTag> | undefined
|
|
}
|
|
|
|
export async function createChildCard (object: Card): Promise<void> {
|
|
const client = getClient()
|
|
const hierarchy = client.getHierarchy()
|
|
const title = await translate(card.string.Card, {})
|
|
|
|
const data: Data<Card> = {
|
|
parent: object._id,
|
|
title,
|
|
rank: '',
|
|
content: '' as MarkupBlobRef,
|
|
blobs: {},
|
|
parentInfo: [
|
|
...(object.parentInfo ?? []),
|
|
{
|
|
_id: object._id,
|
|
_class: object._class,
|
|
title: object.title
|
|
}
|
|
]
|
|
}
|
|
|
|
const filledData = fillDefaults(hierarchy, data, object._class)
|
|
|
|
const _id = await client.createDoc(object._class, object.space, filledData)
|
|
|
|
Analytics.handleEvent(CardEvents.CardCreated)
|
|
|
|
const loc = getCurrentLocation()
|
|
if (loc.path[2] === chatId) {
|
|
loc.path[3] = encodeObjectURI(_id, card.class.Card)
|
|
} else {
|
|
loc.path[2] = cardId
|
|
loc.path[3] = _id
|
|
}
|
|
loc.path.length = 4
|
|
navigate(loc)
|
|
}
|
|
|
|
export async function createChildAction (doc: Card | Card[]): Promise<void> {
|
|
if (doc !== undefined && !Array.isArray(doc)) {
|
|
await createChildCard(doc)
|
|
}
|
|
}
|
|
|
|
export function getRootType (hierarchy: Hierarchy, type: Ref<MasterTag>): Ref<MasterTag> {
|
|
const ancestors = hierarchy.getAncestors(type)
|
|
const idx = ancestors.indexOf(card.class.Card)
|
|
|
|
return idx > 0 ? ancestors[idx - 1] : type
|
|
}
|
|
|
|
export function sortNavigatorTypes (types: MasterTag[], config: NavigatorConfig): MasterTag[] {
|
|
return types.sort((a, b) => {
|
|
const aOrder = config.preorder?.find((it) => it.type === a._id)?.order ?? Infinity
|
|
const bOrder = config.preorder?.find((it) => it.type === b._id)?.order ?? Infinity
|
|
|
|
if (aOrder !== bOrder) {
|
|
return aOrder - bOrder
|
|
}
|
|
|
|
return a.label.localeCompare(b.label)
|
|
})
|
|
}
|
|
|
|
export async function openCardInSidebar (cardId: Ref<Card>, doc?: Card): Promise<void> {
|
|
const client = getClient()
|
|
|
|
const widget = client.getModel().findAllSync(workbench.class.Widget, { _id: card.ids.CardWidget as Ref<Widget> })[0]
|
|
if (widget === undefined) return
|
|
|
|
const object = doc ?? (await client.findOne(card.class.Card, { _id: cardId }))
|
|
if (object === undefined) return
|
|
|
|
const tab: WidgetTab = {
|
|
id: cardId,
|
|
name: object.title
|
|
}
|
|
|
|
createWidgetTab(widget, tab, false)
|
|
}
|
|
|
|
export function cardCustomLinkMatch (doc: Card): boolean {
|
|
const loc = getCurrentResolvedLocation()
|
|
const client = getClient()
|
|
const alias = loc.path[2]
|
|
const app = client.getModel().findAllSync(workbench.class.Application, {
|
|
alias
|
|
})[0]
|
|
|
|
return app.type === 'cards'
|
|
}
|
|
|
|
export function cardCustomLinkEncode (doc: Card): Location {
|
|
const loc = getCurrentResolvedLocation()
|
|
loc.path[3] = encodeObjectURI(doc._id, card.class.Card)
|
|
return loc
|
|
}
|
|
|
|
export async function checkOldMessagesSectionVisibility (doc: Card): Promise<boolean> {
|
|
if (!hasAccountRole(getCurrentAccount(), AccountRole.User)) {
|
|
return false
|
|
}
|
|
|
|
return getMetadata(communication.metadata.Enabled) !== true
|
|
}
|
|
|
|
export async function checkCommunicationMessagesSectionVisibility (doc: Card): Promise<boolean> {
|
|
if (!hasAccountRole(getCurrentAccount(), AccountRole.User)) {
|
|
return false
|
|
}
|
|
|
|
return getMetadata(communication.metadata.Enabled) === true
|
|
}
|
|
|
|
export async function checkChildrenSectionVisibility (doc: Card): Promise<boolean> {
|
|
return (doc.children ?? 0) > 0
|
|
}
|
|
|
|
export async function checkRelationsSectionVisibility (doc: Card): Promise<boolean> {
|
|
const client = getClient()
|
|
const h = client.getHierarchy()
|
|
|
|
const parents = h.getAncestors(doc._class)
|
|
const mixins = h.findAllMixins(doc)
|
|
const associationsB = client
|
|
.getModel()
|
|
.findAllSync(core.class.Association, { classA: { $in: [...parents, ...mixins] } })
|
|
.filter((a) => a.nameB.trim().length > 0)
|
|
|
|
if (associationsB.length > 0) {
|
|
return true
|
|
}
|
|
|
|
return (
|
|
client
|
|
.getModel()
|
|
.findAllSync(core.class.Association, { classB: { $in: [...parents, ...mixins] } })
|
|
.filter((a) => a.nameA.trim().length > 0).length > 0
|
|
)
|
|
}
|
|
|
|
export function getCardIconInfo (doc?: Card): { icon: IconComponent, props: IconProps } {
|
|
if (doc === undefined) return { icon: card.icon.Card, props: {} }
|
|
if (doc.icon === view.ids.IconWithEmoji) {
|
|
return { icon: IconWithEmoji, props: { icon: doc.color } }
|
|
}
|
|
|
|
if (doc.icon !== undefined) {
|
|
return {
|
|
icon: doc.icon,
|
|
props: {}
|
|
}
|
|
}
|
|
|
|
const client = getClient()
|
|
const hierarchy = client.getHierarchy()
|
|
const clazz = hierarchy.getClass(doc._class) as MasterTag
|
|
|
|
if (clazz?.icon === view.ids.IconWithEmoji) {
|
|
return {
|
|
icon: IconWithEmoji,
|
|
props: { icon: clazz.color }
|
|
}
|
|
}
|
|
|
|
return {
|
|
icon: clazz?.icon ?? card.icon.MasterTag,
|
|
props: {}
|
|
}
|
|
}
|
|
|
|
export function getAccountClient (): AccountClient {
|
|
const accountsUrl = getMetadata(login.metadata.AccountsUrl)
|
|
const token = getMetadata(presentation.metadata.Token)
|
|
|
|
return getAccountClientRaw(accountsUrl, token)
|
|
}
|
|
|
|
export async function getSpaceAccessPublicLink (doc?: Doc | Doc[]): Promise<string> {
|
|
doc = Array.isArray(doc) ? doc[0] : doc
|
|
if (doc === undefined) {
|
|
return ''
|
|
}
|
|
|
|
const accountClient = getAccountClient()
|
|
const navigateUrl = getCurrentLocation()
|
|
navigateUrl.path[2] = cardId
|
|
navigateUrl.path.length = 3
|
|
const accessLink = await accountClient.createAccessLink(AccountRole.Guest, {
|
|
spaces: [doc._id],
|
|
navigateUrl: JSON.stringify(navigateUrl)
|
|
})
|
|
|
|
return accessLink
|
|
}
|
|
|
|
export async function canGetSpaceAccessPublicLink (doc?: Doc | Doc[]): Promise<boolean> {
|
|
if (!hasAccountRole(getCurrentAccount(), AccountRole.User)) {
|
|
return false
|
|
}
|
|
|
|
return await canCopyLink(doc)
|
|
}
|
|
|
|
export function canLockSection (space: Ref<Space>, store: PermissionsStore): boolean {
|
|
if (getMetadata(core.metadata.DisablePermissions) === true) return true
|
|
if (store.whitelist.has(space)) return true
|
|
const allowed = store.ps[space]?.has(card.permission.LockSection)
|
|
if (allowed) return true
|
|
return !store.restrictedSpaces.has(space)
|
|
}
|
|
|
|
export function canUnlockSection (space: Ref<Space>, store: PermissionsStore): boolean {
|
|
if (getMetadata(core.metadata.DisablePermissions) === true) return true
|
|
if (store.whitelist.has(space)) return true
|
|
const allowed = store.ps[space]?.has(card.permission.UnlockSection)
|
|
if (allowed) return true
|
|
return !store.restrictedSpaces.has(space)
|
|
}
|
|
|
|
export function showAllVersions (value: any, query: DocumentQuery<Doc>): DocumentQuery<Doc> {
|
|
if (value === true) {
|
|
return { ...query, isLatest: { $in: [true, false] } }
|
|
}
|
|
return query
|
|
}
|
|
|
|
export const viewStore = writable<Record<Ref<MasterTag>, string>>(
|
|
JSON.parse(localStorage.getItem('card.layout') ?? '{}')
|
|
)
|
|
|
|
export function setViewMode (type: Ref<MasterTag>, mode: string): void {
|
|
viewStore.update((views) => {
|
|
views[type] = mode
|
|
localStorage.setItem('card.layout', JSON.stringify(views))
|
|
return views
|
|
})
|
|
}
|