// 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 Card, CardEvents, cardId, type CardSpace, type CardSection, type MasterTag } from '@hcengineering/card' import { type Class, type Client, type Data, type Doc, type DocumentQuery, fillDefaults, type Hierarchy, type MarkupBlobRef, type Ref, type RelatedDocument, SortingOrder, type Space, type TxOperations, type WithLookup } from '@hcengineering/core' import { getClient, MessageBox, type ObjectSearchResult } from '@hcengineering/presentation' import { getCurrentResolvedLocation, getPanelURI, type Location, type ResolvedLocation, showPopup } from '@hcengineering/ui' import view, { encodeObjectURI } from '@hcengineering/view' import { accessDeniedStore } from '@hcengineering/view-resources' import workbench, { type LocationData } from '@hcengineering/workbench' import { translate } from '@hcengineering/platform' import { makeRank } from '@hcengineering/rank' import { Analytics } from '@hcengineering/analytics' import { openWidget } from '@hcengineering/workbench-resources' 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 { 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 resolveLocation (loc: Location): Promise { if (loc.path[2] !== cardId) { return undefined } const id = loc.path[3] if (loc.path[4] === undefined && id !== undefined && id !== 'browser') { return await generateLocation(loc, id) } } export async function editSpace (value: CardSpace | undefined): Promise { if (value !== undefined) { showPopup(CreateSpace, { space: value }) } } async function generateLocation (loc: Location, id: string): Promise { const client = getClient() const doc = await client.findOne(card.class.Card, { _id: id as Ref }) if (doc === undefined) { accessDeniedStore.set(true) return undefined } const appComponent = loc.path[0] ?? '' const workspace = loc.path[1] ?? '' const space = doc.space const special = doc._class const objectPanel = client.getHierarchy().classHierarchyMixin(doc._class as Ref>, 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, space, special], fragment: getPanelURI(component, doc._id, doc._class, 'content') } } } export async function resolveLocationData (loc: Location): Promise { 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 }) if (object === undefined) { return base } return { name: object.title } } export async function getCardTitle (client: TxOperations, ref: Ref, doc?: Card): Promise { const object = doc ?? (await client.findOne(card.class.Card, { _id: ref })) if (object === undefined) throw new Error(`Card not found, _id: ${ref}`) return object.title } export async function getCardId (client: TxOperations, ref: Ref, doc?: Card): Promise { const object = doc ?? (await client.findOne(card.class.Card, { _id: ref })) if (object === undefined) throw new Error(`Card not found, _id: ${ref}`) return object.title } export async function getCardLink (doc: Card): Promise { 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 { const q: DocumentQuery = { 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) } if (filter.nin !== undefined) { q._id.$nin = filter.nin?.map((it) => it._id as Ref) } } return (await client.findAll(card.class.Card, q, { limit: 200 })).map(toCardObjectSearchResult) } const toCardObjectSearchResult = (e: WithLookup): ObjectSearchResult => ({ doc: e, title: e.title, icon: card.icon.Card, component: CardSearchItem }) export async function createCard (type: Ref, space: Ref): Promise> { const client = getClient() const hierarchy = client.getHierarchy() const lastOne = await client.findOne(card.class.Card, {}, { sort: { rank: SortingOrder.Descending } }) const title = await translate(card.string.Card, {}) const data: Data = { title, rank: makeRank(lastOne?.rank, undefined), content: '' as MarkupBlobRef, parentInfo: [], blobs: {} } const filledData = fillDefaults(hierarchy, data, type) const _id = await client.createDoc(type, space, filledData) Analytics.handleEvent(CardEvents.CardCreated) return _id } export function getRootType (hierarchy: Hierarchy, type: Ref): Ref { 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 interface CardWidgetData { _id: Ref name: string tab?: Ref } export async function openCardInSidebar (cardId: Ref, doc?: Card, tabId?: Ref): Promise { const client = getClient() const widget = client.getModel().findAllSync(workbench.class.Widget, { _id: card.ids.CardWidget as any })[0] if (widget === undefined) return const object = doc ?? (await client.findOne(card.class.Card, { _id: cardId })) if (object === undefined) return const data: CardWidgetData = { _id: cardId, name: object.title, tab: tabId } openWidget(widget, data) } 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 }