Files
Denis BykhovandGitHub 65cb5987bf feat: implement reference versioning functionality across various com… (#10941)
* 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>
2026-07-01 21:40:17 +07:00

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
})
}