Add ability to copy all data from cards/docs tables (#10505)

Signed-off-by: Artem Savchenko <armisav@gmail.com>
This commit is contained in:
Artyom Savchenko
2026-02-13 15:30:43 +05:00
committed by GitHub
parent 3ec280cb6e
commit 315f390920
49 changed files with 491 additions and 146 deletions
+31 -1
View File
@@ -19017,6 +19017,12 @@ importers:
'@hcengineering/platform':
specifier: workspace:^0.7.19
version: link:../../foundations/core/packages/platform
'@hcengineering/presentation':
specifier: workspace:^0.7.0
version: link:../../packages/presentation
'@hcengineering/ui':
specifier: workspace:^0.7.0
version: link:../../packages/ui
'@hcengineering/view':
specifier: workspace:^0.7.0
version: link:../view
@@ -19090,6 +19096,9 @@ importers:
'@hcengineering/view-resources':
specifier: workspace:^0.7.0
version: link:../view-resources
svelte:
specifier: ^4.2.20
version: 4.2.20
devDependencies:
'@hcengineering/platform-rig':
specifier: workspace:^0.7.19
@@ -19118,15 +19127,36 @@ importers:
eslint-plugin-promise:
specifier: ^6.1.1
version: 6.6.0(eslint@8.57.1)
eslint-plugin-svelte:
specifier: ^2.35.1
version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@swc/core@1.15.1)(@types/node@22.19.0)(typescript@5.9.3))
jest:
specifier: ^29.7.0
version: 29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@swc/core@1.15.1)(@types/node@22.19.0)(typescript@5.9.3))
prettier:
specifier: ^3.6.2
version: 3.6.2
prettier-plugin-svelte:
specifier: ^3.4.0
version: 3.4.0(prettier@3.6.2)(svelte@4.2.20)
sass:
specifier: ^1.80.0
version: 1.93.3
svelte-check:
specifier: ^3.6.9
version: 3.8.6(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.1)(@types/node@22.19.0)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.93.3)(svelte@4.2.20)
svelte-eslint-parser:
specifier: ^0.33.1
version: 0.33.1(svelte@4.2.20)
svelte-loader:
specifier: ^3.2.0
version: 3.2.4(svelte@4.2.20)
svelte-preprocess:
specifier: ^5.1.4
version: 5.1.4(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.1)(@types/node@22.19.0)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.93.3)(svelte@4.2.20)(typescript@5.9.3)
ts-jest:
specifier: ^29.1.1
version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3)
version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@swc/core@1.15.1)(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3)
typescript:
specifier: ^5.9.3
version: 5.9.3
+54
View File
@@ -728,6 +728,60 @@ export function createModel (builder: Builder): void {
card.viewlet.CardRelationshipTable
)
builder.createDoc(
presentation.class.ComponentPointExtension,
core.space.Model,
{
extension: converter.extensions.CopyAsMarkdownAction,
component: converter.component.CopyAsMarkdownButton
},
converter.extensions.CopyAsMarkdownButton
)
builder.createDoc(
view.class.ViewletViewAction,
core.space.Model,
{
descriptor: view.viewlet.RelationshipTable,
extension: converter.extensions.CopyAsMarkdownAction,
applicableToClass: card.class.Card
},
card.specialViewAction.CardRelationshipTable
)
builder.createDoc(
view.class.ViewletViewAction,
core.space.Model,
{
descriptor: view.viewlet.Table,
extension: converter.extensions.CopyAsMarkdownAction,
applicableToClass: card.class.Card
},
card.specialViewAction.CardTable
)
builder.createDoc(
view.class.ViewletViewAction,
core.space.Model,
{
descriptor: view.viewlet.Table,
extension: converter.extensions.CopyAsMarkdownAction,
applicableToClass: card.class.Card
},
card.specialViewAction.CopyAsMarkdownTable
)
builder.createDoc(
view.class.ViewletViewAction,
core.space.Model,
{
descriptor: view.viewlet.RelationshipTable,
extension: converter.extensions.CopyAsMarkdownAction,
applicableToClass: card.class.Card
},
card.specialViewAction.CopyAsMarkdownRelationshipTable
)
builder.mixin(card.class.Card, core.class.Class, view.mixin.ObjectPresenter, {
presenter: card.component.CardPresenter
})
+7 -1
View File
@@ -13,6 +13,7 @@
// limitations under the License.
//
import type { ViewletViewAction, Action, ActionCategory, ViewAction } from '@hcengineering/view'
import { type Card, cardId } from '@hcengineering/card'
import card from '@hcengineering/card-resources/src/plugin'
import type { Client, Doc, Ref } from '@hcengineering/core'
@@ -20,7 +21,6 @@ import {} from '@hcengineering/core'
import { mergeIds, type Resource } from '@hcengineering/platform'
import { type TagCategory } from '@hcengineering/tags'
import { type Location, type ResolvedLocation } from '@hcengineering/ui/src/types'
import { type Action, type ActionCategory, type ViewAction } from '@hcengineering/view'
import { type LocationData } from '@hcengineering/workbench'
import { type NotificationGroup, type NotificationType } from '@hcengineering/notification'
@@ -44,6 +44,12 @@ export default mergeIds(cardId, card, {
Card: '' as Ref<ActionCategory>,
Labels: '' as Ref<TagCategory>
},
specialViewAction: {
CardRelationshipTable: '' as Ref<ViewletViewAction>,
CardTable: '' as Ref<ViewletViewAction>,
CopyAsMarkdownTable: '' as Ref<ViewletViewAction>,
CopyAsMarkdownRelationshipTable: '' as Ref<ViewletViewAction>
},
ids: {
MasterTags: '' as Ref<Doc>,
ManageMasterTags: '' as Ref<Doc>,
+24 -10
View File
@@ -21,6 +21,7 @@ import documentsPlugin, {
DocumentState,
type Document,
type DocumentSpace,
type DocumentTemplate,
type ProjectDocument,
type ChangeControl,
type DocumentRequest
@@ -43,7 +44,7 @@ import setting from '@hcengineering/setting'
import tags from '@hcengineering/tags'
import textEditor from '@hcengineering/text-editor'
import { AccountRole, type ClassCollaborators, type Class, type Doc, type Ref } from '@hcengineering/core'
import { AccountRole, type ClassCollaborators, type Class, type Doc, type Lookup, type Ref } from '@hcengineering/core'
import { type Action } from '@hcengineering/view'
import { definePermissions } from './permissions'
import documents from './plugin'
@@ -255,6 +256,11 @@ export function createModel (builder: Builder): void {
)
// Workflow
const documentTableLookup: Lookup<Document> = {
owner: contact.mixin.Employee,
category: documents.class.DocumentCategory,
template: documents.mixin.DocumentTemplate
}
builder.createDoc(
view.class.Viewlet,
core.space.Model,
@@ -308,16 +314,27 @@ export function createModel (builder: Builder): void {
'modifiedOn'
],
options: {
lookup: {
owner: contact.mixin.Employee,
category: documents.class.DocumentCategory,
template: documents.mixin.DocumentTemplate
}
lookup: documentTableLookup
}
},
documents.viewlet.TableDocument
)
builder.createDoc(
view.class.ViewletViewAction,
core.space.Model,
{
descriptor: view.viewlet.Table,
extension: converter.extensions.CopyAsMarkdownAction,
applicableToClass: documents.class.Document
},
documents.specialViewAction.TableDocument
)
const documentTemplateTableLookup: Lookup<DocumentTemplate> = {
owner: contact.mixin.Employee,
category: documents.class.DocumentCategory
}
builder.createDoc(
view.class.Viewlet,
core.space.Model,
@@ -367,10 +384,7 @@ export function createModel (builder: Builder): void {
hiddenKeys: ['attachedTo']
},
options: {
lookup: {
owner: contact.mixin.Employee,
category: documents.class.DocumentCategory
}
lookup: documentTemplateTableLookup
}
},
documents.viewlet.TableDocumentTemplate
+8 -5
View File
@@ -20,7 +20,7 @@ import { type ObjectSearchCategory, type ObjectSearchFactory } from '@hcengineer
import { mergeIds, type Resource } from '@hcengineering/platform'
import { type TagCategory } from '@hcengineering/tags'
import { type AnyComponent } from '@hcengineering/ui/src/types'
import { type ActionCategory, type ViewAction } from '@hcengineering/view'
import type { ActionCategory, ViewAction, Viewlet, ViewletViewAction } from '@hcengineering/view'
import { type NotificationType, type NotificationGroup } from '@hcengineering/notification'
import { type TextActionVisibleFunction, type TextActionFunction } from '@hcengineering/text-editor'
@@ -78,11 +78,14 @@ export default mergeIds(documentsId, documents, {
TransferDocument: '' as ViewAction,
EditDocSpace: '' as ViewAction
},
specialViewAction: {
TableDocument: '' as Ref<ViewletViewAction>
},
viewlet: {
TableDocument: '' as Ref<Doc>,
ListDocument: '' as Ref<Doc>,
TableDocumentTemplate: '' as Ref<Doc>,
TableDocumentDomain: '' as Ref<Doc>
TableDocument: '' as Ref<Viewlet>,
ListDocument: '' as Ref<Viewlet>,
TableDocumentTemplate: '' as Ref<Viewlet>,
TableDocumentDomain: '' as Ref<Viewlet>
},
notification: {
DocumentsNotificationGroup: '' as Ref<NotificationGroup>,
+4 -5
View File
@@ -323,14 +323,13 @@ export function createModel (builder: Builder): void {
}
}
builder.createDoc(
workbench.class.ViewletSpecialViewAction,
view.class.ViewletViewAction,
core.space.Model,
{
attachedTo: recruit.viewlet.TableCandidate,
attachedToClass: view.class.Viewlet,
collection: 'specialViewActions',
descriptor: view.viewlet.Table,
extension: workbench.extensions.SpecialViewAction,
config: tableCandidateExportConfig
config: tableCandidateExportConfig,
applicableToClass: recruit.mixin.Candidate
},
recruit.specialViewAction.TableCandidate
)
+3 -3
View File
@@ -23,14 +23,14 @@ import { recruitId } from '@hcengineering/recruit'
import recruit from '@hcengineering/recruit-resources/src/plugin'
import { type ProjectType, type TaskTypeDescriptor } from '@hcengineering/task'
import type { AnyComponent, Location } from '@hcengineering/ui/src/types'
import type { ViewletSpecialViewAction } from '@hcengineering/workbench'
import type {
Action,
ActionCategory,
ViewAction,
ViewActionAvailabilityFunction,
ViewQueryAction,
Viewlet
Viewlet,
ViewletViewAction
} from '@hcengineering/view'
export default mergeIds(recruitId, recruit, {
@@ -134,7 +134,7 @@ export default mergeIds(recruitId, recruit, {
DefaultVacancy: '' as Ref<ProjectType>
},
specialViewAction: {
TableCandidate: '' as Ref<ViewletSpecialViewAction>
TableCandidate: '' as Ref<ViewletViewAction>
},
viewlet: {
TableCandidate: '' as Ref<Viewlet>,
+11
View File
@@ -487,6 +487,17 @@ export function createModel (builder: Builder): void {
defineViewlets(builder)
builder.createDoc(
view.class.ViewletViewAction,
core.space.Model,
{
descriptor: view.viewlet.List,
extension: converter.extensions.CopyAsMarkdownAction,
applicableToClass: tracker.class.Issue
},
tracker.specialViewAction.IssueList
)
const issuesId = 'issues'
const componentsId = 'components'
const milestonesId = 'milestones'
+4 -1
View File
@@ -22,7 +22,7 @@ import { type ProjectType } from '@hcengineering/task'
import { trackerId } from '@hcengineering/tracker'
import tracker from '@hcengineering/tracker-resources/src/plugin'
import type { AnyComponent } from '@hcengineering/ui/src/types'
import { type Action, type ViewAction, type Viewlet } from '@hcengineering/view'
import { type Action, type ViewAction, type Viewlet, type ViewletViewAction } from '@hcengineering/view'
import { type Application } from '@hcengineering/workbench'
export default mergeIds(trackerId, tracker, {
@@ -63,6 +63,9 @@ export default mergeIds(trackerId, tracker, {
app: {
Tracker: '' as Ref<Application>
},
specialViewAction: {
IssueList: '' as Ref<ViewletViewAction>
},
viewlet: {
IssueList: '' as Ref<Viewlet>,
IssueTemplateList: '' as Ref<Viewlet>,
+33 -4
View File
@@ -29,12 +29,17 @@ import {
type Ref,
type Space
} from '@hcengineering/core'
import { type Builder, Mixin, Model, UX } from '@hcengineering/model'
import { type Builder, Mixin, Model, Prop, TypeRecord, TypeRef, TypeString, UX } from '@hcengineering/model'
import core, { TClass, TDoc } from '@hcengineering/model-core'
import preference, { TPreference } from '@hcengineering/model-preference'
import presentation from '@hcengineering/model-presentation'
import { type Asset, type IntlString, type Resource, type Status } from '@hcengineering/platform'
import { type AnyComponent, type LabelAndProps, type Location } from '@hcengineering/ui/src/types'
import { type Asset, getEmbeddedLabel, type IntlString, type Resource, type Status } from '@hcengineering/platform'
import {
type AnyComponent,
type ComponentExtensionId,
type LabelAndProps,
type Location
} from '@hcengineering/ui/src/types'
import {
type TypeEditor,
type Action,
@@ -99,7 +104,8 @@ import {
type ViewOptionsModel,
type Viewlet,
type ViewletDescriptor,
type ViewletPreference
type ViewletPreference,
type ViewletViewAction
} from '@hcengineering/view'
import view from './plugin'
@@ -318,6 +324,28 @@ export class TViewletDescriptor extends TDoc implements ViewletDescriptor {
label!: IntlString
}
@Model(view.class.ViewletViewAction, core.class.Doc, DOMAIN_MODEL)
@UX(view.string.ViewletViewAction)
export class TViewletViewAction extends TDoc implements ViewletViewAction {
@Prop(TypeRef(view.class.Viewlet), getEmbeddedLabel('Viewlet'))
declare viewlet?: Ref<Viewlet>
@Prop(TypeRef(view.class.ViewletDescriptor), getEmbeddedLabel('Descriptor'))
declare descriptor?: Ref<ViewletDescriptor>
@Prop(TypeString(), getEmbeddedLabel('Extension'))
declare extension: ComponentExtensionId
@Prop(TypeRecord(), getEmbeddedLabel('Config'))
declare config?: Record<string, any>
@Prop(TypeRef(core.class.Class), getEmbeddedLabel('ApplicableToClass'))
declare applicableToClass?: Ref<Class<Doc>>
@Prop(TypeRef(core.class.Class), getEmbeddedLabel('DisabledForClass'))
declare disabledForClass?: Ref<Class<Doc>>
}
@Model(view.class.Viewlet, core.class.Doc, DOMAIN_MODEL)
export class TViewlet extends TDoc implements Viewlet {
attachTo!: Ref<Class<Doc>>
@@ -472,6 +500,7 @@ export function createModel (builder: Builder): void {
TViewletPreference,
TViewletDescriptor,
TViewlet,
TViewletViewAction,
TAction,
TActionCategory,
TObjectValidator,
+4 -24
View File
@@ -14,26 +14,25 @@
//
import { AccountRole, type AccountUuid, type Class, DOMAIN_MODEL, type Ref, type Space } from '@hcengineering/core'
import { type Builder, Mixin, Model, Prop, TypeRecord, TypeRef, TypeString, UX } from '@hcengineering/model'
import { type Builder, Mixin, Model, Prop, TypeRef, UX } from '@hcengineering/model'
import preference, { TPreference } from '@hcengineering/model-preference'
import { createAction } from '@hcengineering/model-view'
import core, { TAttachedDoc, TClass, TDoc } from '@hcengineering/model-core'
import core, { TClass, TDoc } from '@hcengineering/model-core'
import { type Asset, getEmbeddedLabel, type IntlString, type Resource } from '@hcengineering/platform'
import view, { type KeyBinding, type Viewlet } from '@hcengineering/view'
import view, { type KeyBinding } from '@hcengineering/view'
import type {
Application,
ApplicationNavModel,
HiddenApplication,
SpaceView,
ViewConfiguration,
ViewletSpecialViewAction,
Widget,
WidgetPreference,
WidgetTab,
WidgetType,
WorkbenchTab
} from '@hcengineering/workbench'
import { type AnyComponent, type ComponentExtensionId } from '@hcengineering/ui/src/types'
import { type AnyComponent } from '@hcengineering/ui/src/types'
import presentation from '@hcengineering/model-presentation'
import workbench from './plugin'
@@ -106,29 +105,10 @@ export class TWorkbenchTab extends TPreference implements WorkbenchTab {
isPinned!: boolean
}
@Model(workbench.class.ViewletSpecialViewAction, core.class.AttachedDoc, DOMAIN_MODEL)
@UX(workbench.string.ViewletSpecialViewAction)
export class TViewletSpecialViewAction extends TAttachedDoc implements ViewletSpecialViewAction {
@Prop(TypeRef(view.class.Viewlet), core.string.AttachedTo)
declare attachedTo: Ref<Viewlet>
@Prop(TypeRef(view.class.Viewlet), core.string.AttachedToClass)
declare attachedToClass: Ref<Class<Viewlet>>
override collection: 'specialViewActions' = 'specialViewActions'
@Prop(TypeString(), getEmbeddedLabel('Extension'))
declare extension: ComponentExtensionId
@Prop(TypeRecord(), getEmbeddedLabel('Config'))
declare config?: Record<string, any>
}
export function createModel (builder: Builder): void {
builder.createModel(
TApplication,
TSpaceView,
TViewletSpecialViewAction,
THiddenApplication,
TApplicationNavModel,
TWidget,
+2 -5
View File
@@ -1,7 +1,4 @@
module.exports = {
extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'],
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json'
}
extends: ['./node_modules/@hcengineering/platform-rig/profiles/ui/eslint.config.json'],
parserOptions: { tsconfigRootDir: __dirname }
}
+22
View File
@@ -0,0 +1,22 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"trailingComma": "none",
"tabWidth": 2,
"semi": false,
"singleQuote": true,
"printWidth": 120,
"useTabs": false,
"bracketSpacing": true,
"proseWrap": "preserve",
"plugins": [
"prettier-plugin-svelte"
],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
]
}
+16 -11
View File
@@ -1,21 +1,18 @@
{
"name": "@hcengineering/converter-resources",
"version": "0.7.0",
"main": "lib/index.js",
"main": "src/index.ts",
"svelte": "src/index.ts",
"types": "types/index.d.ts",
"files": [
"lib/**/*",
"types/**/*",
"tsconfig.json"
],
"author": "Copyright © Hardcore Engineering Inc.",
"license": "EPL-2.0",
"scripts": {
"build": "compile",
"build": "compile ui",
"format": "format src",
"build:watch": "compile",
"_phase:build": "compile transpile src",
"svelte-check": "do-svelte-check",
"_phase:svelte-check": "do-svelte-check",
"build:watch": "compile ui",
"_phase:build": "compile ui",
"_phase:format": "format src",
"_phase:validate": "compile validate",
"_phase:test": "jest --passWithNoTests --silent",
@@ -33,8 +30,15 @@
"typescript": "^5.9.3",
"eslint": "^8.54.0",
"jest": "^29.7.0",
"svelte-check": "^3.6.9",
"svelte-eslint-parser": "^0.33.1",
"svelte-loader": "^3.2.0",
"svelte-preprocess": "^5.1.4",
"ts-jest": "^29.1.1",
"@types/jest": "^29.5.5"
"@types/jest": "^29.5.5",
"sass": "^1.80.0",
"eslint-plugin-svelte": "^2.35.1",
"prettier-plugin-svelte": "^3.4.0"
},
"dependencies": {
"@hcengineering/platform": "workspace:^0.7.19",
@@ -45,6 +49,7 @@
"@hcengineering/ui": "workspace:^0.7.0",
"@hcengineering/theme": "workspace:^0.7.0",
"@hcengineering/contact": "workspace:^0.7.0",
"@hcengineering/view-resources": "workspace:^0.7.0"
"@hcengineering/view-resources": "workspace:^0.7.0",
"svelte": "^4.2.20"
}
}
@@ -0,0 +1,5 @@
module.exports = {
plugins: [
require('autoprefixer')
]
}
@@ -13,7 +13,8 @@
// limitations under the License.
//
import { buildMarkdownTableFromDocs, copyAsMarkdownTable, isIntlString } from '../index'
import { buildMarkdownTableFromDocs, copyAsMarkdownTable } from '../markdown'
import { isIntlString } from '../formatter'
import type { Doc, Class, Ref } from '@hcengineering/core'
import type { CopyAsMarkdownTableProps } from '../types'
import { getClient } from '@hcengineering/presentation'
@@ -0,0 +1,46 @@
<!--
// Copyright © 2026 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use it except in compliance with the License. You may obtain
// a copy of the License at https://www.eclipse.org/legal/epl-2.0
-->
<script lang="ts">
import type { Class, Doc, DocumentQuery, Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { Button, IconCopy } from '@hcengineering/ui'
import view from '@hcengineering/view'
import { viewletContextStore } from '@hcengineering/view-resources'
import { copyAsMarkdownTableFromResource, copyRelationshipTableAsMarkdown } from '../markdown/copyActions'
export let _class: Ref<Class<Doc>>
export let query: DocumentQuery<Doc> = {}
export let config: Array<string | import('@hcengineering/view').BuildModelKey> = []
$: ctx = $viewletContextStore?.getLastContext()
$: relationshipTableData = ctx?.relationshipTableData
$: viewlet = ctx?.viewlet
$: hasData = relationshipTableData !== undefined || (_class !== undefined && query !== undefined)
async function handleClick (e: MouseEvent): Promise<void> {
if (relationshipTableData !== undefined) {
await copyRelationshipTableAsMarkdown(e, relationshipTableData)
} else if (_class !== undefined && query !== undefined) {
const client = getClient()
const docs = await client.findAll(_class, query, { limit: 1000 })
await copyAsMarkdownTableFromResource(e, {
cardClass: _class,
viewlet,
config,
query,
docs
})
}
}
</script>
{#if hasData}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<Button icon={IconCopy} label={view.string.CopyAll} on:click={handleClick} />
{/if}
@@ -47,10 +47,10 @@ function expandRowsForRoot (
root: Doc,
associationAttrs: AttributeModel[],
pathPerAttr: string[][]
): Array<{ docsByLevel: (Doc | undefined)[] }> {
const rows: Array<{ docsByLevel: (Doc | undefined)[] }> = []
): Array<{ docsByLevel: Array<Doc | undefined> }> {
const rows: Array<{ docsByLevel: Array<Doc | undefined> }> = []
function expand (parent: Doc | undefined, level: number, docsSoFar: (Doc | undefined)[]): void {
function expand (parent: Doc | undefined, level: number, docsSoFar: Array<Doc | undefined>): void {
if (level >= associationAttrs.length) {
rows.push({ docsByLevel: docsSoFar })
return
@@ -78,7 +78,7 @@ function expandRowsForRoot (
/**
* Compute rowSpan for a given level at each row index: how many consecutive rows share the same doc at this level.
*/
function computeRowSpans (rows: Array<{ docsByLevel: (Doc | undefined)[] }>, level: number): number[] {
function computeRowSpans (rows: Array<{ docsByLevel: Array<Doc | undefined> }>, level: number): number[] {
const spans: number[] = []
for (let i = 0; i < rows.length; i++) {
const doc = rows[i].docsByLevel[level]
+4
View File
@@ -21,6 +21,7 @@ import {
buildMarkdownTableFromDocs as buildMarkdownTableFromDocsImpl
} from './markdown'
import { copyAsMarkdownTableAction } from './actionImpl'
import CopyAsMarkdownButton from './components/CopyAsMarkdownButton.svelte'
export * from './formatter'
export * from './model'
@@ -50,5 +51,8 @@ export default async (): Promise<Resources> => ({
},
actionImpl: {
CopyAsMarkdownTable: copyAsMarkdownTableAction
},
component: {
CopyAsMarkdownButton
}
})
@@ -15,7 +15,13 @@
import type { Class, Client, Doc, Hierarchy, Ref, PersonId } from '@hcengineering/core'
import { getCurrentLanguage } from '@hcengineering/theme'
import type { AttributeModel, BuildMarkdownTableMetadata, TableMetadata, Viewlet } from '@hcengineering/view'
import type {
AttributeModel,
BuildMarkdownTableMetadata,
TableMetadata,
Viewlet,
BuildModelKey
} from '@hcengineering/view'
import viewPlugin from '@hcengineering/view'
import { buildConfigLookup, buildModel, getAttributeValue } from '@hcengineering/view-resources'
import type { CopyAsMarkdownTableProps, CopyRelationshipTableAsMarkdownProps } from '../types'
@@ -133,7 +139,7 @@ export async function buildMarkdownTableFromDocs (
const model = await buildModel({
client,
_class: props.cardClass,
keys: actualConfig.filter((key: string | import('@hcengineering/view').BuildModelKey) => {
keys: actualConfig.filter((key: string | BuildModelKey) => {
if (typeof key === 'string') {
return !hiddenKeys.includes(key)
}
@@ -0,0 +1,10 @@
const sveltePreprocess = require('svelte-preprocess')
module.exports = {
preprocess: sveltePreprocess({
scss: {
// This is expected as svelte-preprocess hasn't fully migrated to the modern API yet
silenceDeprecations: ['legacy-js-api']
}
})
};
+2 -3
View File
@@ -1,11 +1,10 @@
{
"extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json",
"extends": "./node_modules/@hcengineering/platform-rig/profiles/ui/tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib",
"declarationDir": "./types",
"tsBuildInfoFile": ".build/build.tsbuildinfo"
"declarationDir": "./types"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "lib", "dist", "types", "bundle"]
+3 -1
View File
@@ -39,6 +39,8 @@
"dependencies": {
"@hcengineering/platform": "workspace:^0.7.19",
"@hcengineering/core": "workspace:^0.7.24",
"@hcengineering/view": "workspace:^0.7.0"
"@hcengineering/view": "workspace:^0.7.0",
"@hcengineering/presentation": "workspace:^0.7.0",
"@hcengineering/ui": "workspace:^0.7.0"
}
}
+9
View File
@@ -15,6 +15,8 @@
import type { Client, Doc, Mixin, Ref } from '@hcengineering/core'
import { type Plugin, type Resource, plugin } from '@hcengineering/platform'
import type { AnyComponent, ComponentExtensionId } from '@hcengineering/ui'
import type { ComponentPointExtension } from '@hcengineering/presentation'
import type { Action, BuildMarkdownTableMetadata, ViewAction } from '@hcengineering/view'
import type {
CopyAsMarkdownTableFunction,
@@ -39,6 +41,13 @@ export const converterPlugin = plugin(converterId, {
(docs: Doc[], props: CopyAsMarkdownTableProps, client: Client) => Promise<string>
>
},
extensions: {
CopyAsMarkdownAction: '' as ComponentExtensionId,
CopyAsMarkdownButton: '' as Ref<ComponentPointExtension>
},
component: {
CopyAsMarkdownButton: '' as AnyComponent
},
action: {
CopyAsMarkdownTable: '' as Ref<Action<Doc, Record<string, any>>>
},
@@ -45,6 +45,7 @@
{viewlets}
{label}
{space}
{resultQuery}
{modeSelectorProps}
>
<svelte:fragment slot="header-tools">
+1
View File
@@ -113,6 +113,7 @@
"Or": "Nebo",
"HyperlinkPlaceholder": "https://jappleseed.com",
"CopyToClipboard": "Kopírovat do schránky",
"CopyAll": "Kopírovat vše",
"CopyDocumentMarkdown": "Kopírovat jako Markdown",
"CopyAsMarkdownTable": "Kopírovat jako Markdown tabulka",
"TableCopiedToClipboard": "Tabulka zkopírována do schránky",
+1
View File
@@ -113,6 +113,7 @@
"Or": "Oder",
"HyperlinkPlaceholder": "https://jappleseed.com",
"CopyToClipboard": "In die Zwischenablage kopieren",
"CopyAll": "Alle kopieren",
"CopyDocumentMarkdown": "Als Markdown kopieren",
"CopyAsMarkdownTable": "Als Markdown-Tabelle kopieren",
"TableCopiedToClipboard": "Tabelle in die Zwischenablage kopiert",
+1
View File
@@ -113,6 +113,7 @@
"Or": "Or",
"HyperlinkPlaceholder": "https://jappleseed.com",
"CopyToClipboard": "Copy to clipboard",
"CopyAll": "Copy All",
"CopyDocumentMarkdown": "Copy as Markdown",
"CopyAsMarkdownTable": "Copy as Markdown Table",
"TableCopiedToClipboard": "Table copied to clipboard",
+1
View File
@@ -109,6 +109,7 @@
"Or": "O",
"HyperlinkPlaceholder": "https://jappleseed.com",
"CopyToClipboard": "Copiar al portapapeles",
"CopyAll": "Copiar todo",
"CopyDocumentMarkdown": "Copiar como Markdown",
"CopyAsMarkdownTable": "Copiar como tabla Markdown",
"TableCopiedToClipboard": "Tabla copiada al portapapeles",
+1
View File
@@ -109,6 +109,7 @@
"Or": "Ou",
"HyperlinkPlaceholder": "https://jappleseed.com",
"CopyToClipboard": "Copier dans le presse-papiers",
"CopyAll": "Tout copier",
"CopyDocumentMarkdown": "Copier en Markdown",
"CopyAsMarkdownTable": "Copier en tableau Markdown",
"TableCopiedToClipboard": "Tableau copié dans le presse-papiers",
+1
View File
@@ -109,6 +109,7 @@
"Or": "O",
"HyperlinkPlaceholder": "https://jappleseed.com",
"CopyToClipboard": "Copia negli appunti",
"CopyAll": "Copia tutto",
"CopyDocumentMarkdown": "Copia come Markdown",
"CopyAsMarkdownTable": "Copia come tabella Markdown",
"TableCopiedToClipboard": "Tabella copiata negli appunti",
+1
View File
@@ -109,6 +109,7 @@
"Or": "または",
"HyperlinkPlaceholder": "https://jappleseed.com",
"CopyToClipboard": "クリップボードにコピー",
"CopyAll": "すべてコピー",
"CopyDocumentMarkdown": "Markdownとしてコピー",
"CopyAsMarkdownTable": "Markdownテーブルとしてコピー",
"TableCopiedToClipboard": "テーブルをクリップボードにコピーしました",
+1
View File
@@ -109,6 +109,7 @@
"Or": "Ou",
"HyperlinkPlaceholder": "https://jappleseed.com",
"CopyToClipboard": "Copiar para área de transferência",
"CopyAll": "Copiar tudo",
"CopyDocumentMarkdown": "Copiar como Markdown",
"CopyAsMarkdownTable": "Copiar como tabela Markdown",
"TableCopiedToClipboard": "Tabela copiada para área de transferência",
+1
View File
@@ -109,6 +109,7 @@
"Or": "Ou",
"HyperlinkPlaceholder": "https://jappleseed.com",
"CopyToClipboard": "Copiar para área de transferência",
"CopyAll": "Copiar tudo",
"CopyDocumentMarkdown": "Copiar como Markdown",
"CopyAsMarkdownTable": "Copiar como tabela Markdown",
"TableCopiedToClipboard": "Tabela copiada para área de transferência",
+1
View File
@@ -109,6 +109,7 @@
"Or": "Или",
"HyperlinkPlaceholder": "https://jappleseed.com",
"CopyToClipboard": "Скопировать в буфер обмена",
"CopyAll": "Копировать всё",
"CopyDocumentMarkdown": "Скопировать как Markdown",
"CopyAsMarkdownTable": "Скопировать как таблицу Markdown",
"TableCopiedToClipboard": "Таблица скопирована в буфер обмена",
+1
View File
@@ -109,6 +109,7 @@
"Or": "Veya",
"HyperlinkPlaceholder": "https://ornek.com",
"CopyToClipboard": "Panoya kopyala",
"CopyAll": "Tümünü kopyala",
"CopyDocumentMarkdown": "Markdown olarak kopyala",
"CopyAsMarkdownTable": "Markdown tablosu olarak kopyala",
"TableCopiedToClipboard": "Tablo panoya kopyalandı",
+1
View File
@@ -113,6 +113,7 @@
"Or": "或",
"HyperlinkPlaceholder": "https://jappleseed.com",
"CopyToClipboard": "复制到剪贴板",
"CopyAll": "复制全部",
"CopyDocumentMarkdown": "复制为 Markdown",
"CopyAsMarkdownTable": "复制为 Markdown 表格",
"TableCopiedToClipboard": "表格已复制到剪贴板",
@@ -34,7 +34,6 @@
import { createQuery, getClient, reduceCalls, updateAttribute } from '@hcengineering/presentation'
import ui, {
Button,
IconCopy,
Label,
Loading,
eventToHTMLElement,
@@ -44,14 +43,14 @@
} from '@hcengineering/ui'
import { AttributeModel, BuildModelKey, BuildModelOptions, ViewOptionModel, ViewOptions } from '@hcengineering/view'
import { deepEqual } from 'fast-equals'
import { createEventDispatcher, onMount } from 'svelte'
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
import { Readable } from 'svelte/store'
import { showMenu } from '../actions'
import { canChangeAttribute } from '../permissions'
import view from '../plugin'
import { ViewletContextStore, viewletContextStore } from '../viewletContextStore'
import { buildConfigAssociation, buildConfigLookup, buildModel, getAttributeValue, restrictionStore } from '../utils'
import { getResultOptions, getResultQuery } from '../viewOptions'
import converter from '@hcengineering/converter'
import IconUpDown from './icons/UpDown.svelte'
import RelationsSelectorPopup from './RelationsSelectorPopup.svelte'
@@ -539,16 +538,38 @@
)
}
async function handleCopyAsMarkdown (e: MouseEvent): Promise<void> {
if (model === undefined || viewModel.length === 0) return
const copyFn = await getResource(converter.function.CopyRelationshipAsMarkdown)
await copyFn(e, {
viewModel,
model,
objects,
cardClass: _class
$: relationshipTableData =
model !== undefined && viewModel.length > 0 ? { viewModel, model, objects, cardClass: _class } : undefined
$: {
viewletContextStore.update((cur) => {
const contexts = cur.contexts
const last = contexts[contexts.length - 1]
if (last === undefined) return cur
const updated =
relationshipTableData !== undefined
? { ...last, relationshipTableData }
: (() => {
const rest = { ...last }
delete rest.relationshipTableData
return rest
})()
return new ViewletContextStore([...contexts.slice(0, -1), updated])
})
}
onDestroy(() => {
viewletContextStore.update((cur) => {
const contexts = cur.contexts
const last = contexts[contexts.length - 1]
if (last?.relationshipTableData !== undefined) {
const rest = { ...last }
delete rest.relationshipTableData
return new ViewletContextStore([...contexts.slice(0, -1), rest])
}
return cur
})
})
</script>
{#if !model || isBuildingModel}
@@ -677,19 +698,6 @@
}}
/>
{/if}
{#if objects.length > 0 && viewModel.length > 0 && model !== undefined}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="px-1">
<Button
icon={IconCopy}
label={view.string.CopyToClipboard}
kind={'ghost'}
size={'small'}
on:click={handleCopyAsMarkdown}
/>
</div>
{/if}
</div>
</div>
{/if}
@@ -2,7 +2,9 @@
import { Class, Doc, DocumentQuery, Ref, Space, WithLookup } from '@hcengineering/core'
import { IModeSelector, ModeSelector, SearchInput, Header, Breadcrumb, HeaderAdaptive } from '@hcengineering/ui'
import type { Asset } from '@hcengineering/platform'
import { Viewlet } from '@hcengineering/view'
import { ComponentExtensions, getClient } from '@hcengineering/presentation'
import view, { Viewlet, type ViewletViewAction } from '@hcengineering/view'
import { getViewletSpecialActions } from '../viewletUtils'
import ViewletSelector from './ViewletSelector.svelte'
import FilterButton from './filter/FilterButton.svelte'
@@ -17,8 +19,11 @@
export let showLabelSelector = false
export let modeSelectorProps: IModeSelector | undefined = undefined
export let adaptive: HeaderAdaptive = 'doubleRow'
export let resultQuery: DocumentQuery<Doc> = {}
let scroller: HTMLElement
$: viewletActions = viewlet != null ? getViewletSpecialActions(getClient(), viewlet) : []
</script>
<Header
@@ -48,6 +53,16 @@
<FilterButton {_class} {space} />
</svelte:fragment>
<svelte:fragment slot="actions">
{#each viewletActions as action (action._id)}
<ComponentExtensions
extension={action.extension}
props={{
_class,
query: resultQuery,
config: action.config ?? {}
}}
/>
{/each}
<slot name="actions" />
</svelte:fragment>
<svelte:fragment slot="extra">
@@ -16,7 +16,9 @@
SearchInput,
themeStore
} from '@hcengineering/ui'
import { ViewOptions, Viewlet, ViewletPreference } from '@hcengineering/view'
import view, { ViewOptions, Viewlet, ViewletPreference, type ViewletViewAction } from '@hcengineering/view'
import { ComponentExtensions, getClient } from '@hcengineering/presentation'
import { getViewletSpecialActions } from '../viewletUtils'
import FilterBar from './filter/FilterBar.svelte'
import FilterButton from './filter/FilterButton.svelte'
@@ -51,6 +53,8 @@
$: searchQuery = search === '' ? { ...query } : { ...query, $search: search }
$: hideExtra = modeSelectorProps === undefined && $$slots.extra === undefined
$: viewletActions = viewlet != null ? getViewletSpecialActions(getClient(), viewlet) : []
</script>
<Header {adaptive} {hideActions} {hideExtra} overflowExtra>
@@ -67,6 +71,16 @@
<FilterButton {_class} />
</svelte:fragment>
<svelte:fragment slot="actions">
{#each viewletActions as action (action._id)}
<ComponentExtensions
extension={action.extension}
props={{
_class,
query: resultQuery,
config: action.config ?? {}
}}
/>
{/each}
<slot name="actions" />
</svelte:fragment>
<svelte:fragment slot="extra">
+1
View File
@@ -217,6 +217,7 @@ export {
} from './utils'
export * from './viewOptions'
export * from './viewletContextStore'
export { getViewletSpecialActions } from './viewletUtils'
export { copyMarkdown } from './actionImpl'
export { default as SimpleNotification } from './components/SimpleNotification.svelte'
export type { BuildMarkdownTableMetadata } from '@hcengineering/view'
@@ -1,6 +1,7 @@
import { type Class, type Doc, type DocumentQuery, type Ref } from '@hcengineering/core'
import { type BuildModelKey, type Viewlet, type ViewOptions } from '@hcengineering/view'
import { writable } from 'svelte/store'
import type { CopyRelationshipTableAsMarkdownProps } from '@hcengineering/converter'
/**
* Context for the current viewlet table being displayed
@@ -14,6 +15,7 @@ export interface ViewletContext {
query?: DocumentQuery<Doc>
viewOptions?: ViewOptions
_class?: Ref<Class<Doc>>
relationshipTableData?: CopyRelationshipTableAsMarkdownProps
}
/**
@@ -0,0 +1,63 @@
//
// Copyright © 2026 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.
//
import type { Client, Ref } from '@hcengineering/core'
import view, { type Viewlet, type ViewletViewAction } from '@hcengineering/view'
/** Shape used by the header UI; both viewlet- and descriptor-scoped actions provide this. */
export interface ResolvedViewletSpecialAction {
_id: Ref<ViewletViewAction>
extension: ViewletViewAction['extension']
config?: Record<string, any>
}
/**
* Resolve special view actions for a viewlet by:
* 1. One query: ViewletViewAction where viewlet is in (current + template viewlets) OR descriptor matches.
* 2. For rows with descriptor set, filter by applicableToClass and disabledForClass.
* Actions are deduplicated by extension (viewlet-scoped take precedence).
*/
export function getViewletSpecialActions (client: Client, viewlet: Viewlet): ResolvedViewletSpecialAction[] {
const model = client.getModel()
const hierarchy = client.getHierarchy()
let actions = model.findAllSync(view.class.ViewletViewAction, {
viewlet: viewlet._id
}) as ViewletViewAction[]
if (actions == null || actions.length === 0) {
actions = model.findAllSync(view.class.ViewletViewAction, {
descriptor: viewlet.descriptor
}) as ViewletViewAction[]
}
if (actions == null || actions.length === 0) {
return []
}
const filtered = actions.filter((a) => {
if (a.descriptor != null) {
if (a.applicableToClass != null && !hierarchy.isDerived(viewlet.attachTo, a.applicableToClass)) {
return false
}
if (a.disabledForClass != null && hierarchy.isDerived(viewlet.attachTo, a.disabledForClass)) {
return false
}
}
return true
})
const seen = new Set<string>()
const result: ResolvedViewletSpecialAction[] = []
const byViewlet = filtered.filter((a) => a.viewlet != null)
const byDescriptor = filtered.filter((a) => a.descriptor != null)
for (const a of [...byViewlet, ...byDescriptor]) {
if (seen.has(a.extension)) continue
seen.add(a.extension)
result.push({ _id: a._id, extension: a.extension, config: a.config })
}
return result
}
+5 -1
View File
@@ -66,7 +66,8 @@ import {
ViewAction,
Viewlet,
ViewletDescriptor,
ViewletPreference
ViewletPreference,
ViewletViewAction
} from './types'
export * from './types'
@@ -126,6 +127,7 @@ const view = plugin(viewId, {
ViewletPreference: '' as Ref<Class<ViewletPreference>>,
ViewletDescriptor: '' as Ref<Class<ViewletDescriptor>>,
Viewlet: '' as Ref<Class<Viewlet>>,
ViewletViewAction: '' as Ref<Class<ViewletViewAction>>,
Action: '' as Ref<Class<Action>>,
ActionCategory: '' as Ref<Class<ActionCategory>>,
LinkPresenter: '' as Ref<Class<LinkPresenter>>,
@@ -224,8 +226,10 @@ const view = plugin(viewId, {
Subscribed: '' as IntlString,
HyperlinkPlaceholder: '' as IntlString,
CopyToClipboard: '' as IntlString,
CopyAll: '' as IntlString,
NoGrouping: '' as IntlString,
Type: '' as IntlString,
ViewletViewAction: '' as IntlString,
UnArchive: '' as IntlString,
Archive: '' as IntlString,
Save: '' as IntlString,
+19
View File
@@ -46,6 +46,7 @@ import { Preference } from '@hcengineering/preference'
import {
AnyComponent,
AnySvelteComponent,
type ComponentExtensionId,
Location,
Location as PlatformLocation,
type LabelAndProps
@@ -458,6 +459,24 @@ export interface ViewletConfigOptions {
sortable?: boolean
}
/**
* Special view action shown in the viewlet header. Exactly one of viewlet or descriptor must be set.
* When descriptor is set, scope is filtered by applicableToClass / disabledForClass.
* @public
*/
export interface ViewletViewAction extends Doc {
/** When set, action applies to this viewlet (and template viewlets). */
viewlet?: Ref<Viewlet>
/** When set, action applies to viewlets with this descriptor; use applicableToClass / disabledForClass to scope. */
descriptor?: Ref<ViewletDescriptor>
extension: ComponentExtensionId
config?: Record<string, any>
/** When descriptor is set: show only when viewlet.attachTo is this class or a subclass. */
applicableToClass?: Ref<Class<Doc>>
/** When descriptor is set: hide when viewlet.attachTo is this class or a subclass. */
disabledForClass?: Ref<Class<Doc>>
}
/**
* @public
*/
@@ -36,10 +36,11 @@
FilterButton,
getResultOptions,
getResultQuery,
getViewletSpecialActions,
ViewletSelector,
ViewletSettingButton
} from '@hcengineering/view-resources'
import workbench, { type ParentsNavigationModel, type ViewletSpecialViewAction } from '@hcengineering/workbench'
import workbench, { type ParentsNavigationModel } from '@hcengineering/workbench'
import ComponentNavigator from './ComponentNavigator.svelte'
import { deepEqual } from 'fast-equals'
import { ComponentType } from 'svelte'
@@ -94,12 +95,7 @@
$: void updateQuery(_baseQuery, viewOptions, viewlet, queryBuilder)
$: void updateOptions(viewlet?.options, viewOptions, viewlet)
$: viewletActions =
viewlet != null
? (client.getModel().findAllSync(workbench.class.ViewletSpecialViewAction, {
attachedTo: viewlet._id
}) as ViewletSpecialViewAction[])
: []
$: viewletActions = viewlet != null ? getViewletSpecialActions(client, viewlet) : []
async function updateOptions (
_options: FindOptions<Doc> | undefined,
@@ -141,7 +137,9 @@
<Header
adaptive={modes !== undefined ? 'doubleRow' : filterVisible ? 'freezeActions' : 'disabled'}
hideActions={!(createLabel && createComponent) && createButton === undefined}
hideActions={!(createLabel && createComponent) &&
createButton === undefined &&
(viewletActions == null || viewletActions.length === 0)}
hideExtra={modes === undefined}
freezeBefore
>
-1
View File
@@ -17,7 +17,6 @@ import { workbenchId, workbenchPlugin } from './plugin'
export * from './analytics'
export * from './types'
export type { ViewletSpecialViewAction } from './types'
export * from './utils'
export { workbenchId }
+1 -4
View File
@@ -25,7 +25,6 @@ import type {
ApplicationNavModel,
HiddenApplication,
SpaceView,
ViewletSpecialViewAction,
Widget,
WidgetPreference,
WidgetTab,
@@ -43,8 +42,7 @@ export const workbenchPlugin = plugin(workbenchId, {
HiddenApplication: '' as Ref<Class<HiddenApplication>>,
Widget: '' as Ref<Class<Widget>>,
WidgetPreference: '' as Ref<Class<WidgetPreference>>,
WorkbenchTab: '' as Ref<Class<WorkbenchTab>>,
ViewletSpecialViewAction: '' as Ref<Class<ViewletSpecialViewAction>>
WorkbenchTab: '' as Ref<Class<WorkbenchTab>>
},
mixin: {
SpaceView: '' as Ref<Mixin<SpaceView>>
@@ -61,7 +59,6 @@ export const workbenchPlugin = plugin(workbenchId, {
string: {
Archive: '' as IntlString,
View: '' as IntlString,
ViewletSpecialViewAction: '' as IntlString,
ServerUnderMaintenance: '' as IntlString,
UpgradeDownloadProgress: '' as IntlString,
OpenInSidebar: '' as IntlString,
+2 -25
View File
@@ -14,28 +14,11 @@
// limitations under the License.
//
import type {
AccountRole,
AccountUuid,
AttachedDoc,
Class,
Doc,
DocumentQuery,
Obj,
Ref,
Space
} from '@hcengineering/core'
import type { Viewlet } from '@hcengineering/view'
import type { AccountRole, AccountUuid, Class, Doc, DocumentQuery, Obj, Ref, Space } from '@hcengineering/core'
import { DocNotifyContext, InboxNotification } from '@hcengineering/notification'
import type { Asset, IntlString, Resource } from '@hcengineering/platform'
import type { Preference } from '@hcengineering/preference'
import {
AnyComponent,
type AnySvelteComponent,
type ComponentExtensionId,
Location,
ResolvedLocation
} from '@hcengineering/ui'
import { AnyComponent, type AnySvelteComponent, Location, ResolvedLocation } from '@hcengineering/ui'
/** @public */
export interface LocationData {
@@ -229,9 +212,3 @@ export interface ViewConfiguration {
export interface SpaceView extends Class<Obj> {
view: ViewConfiguration
}
/** @public */
export interface ViewletSpecialViewAction extends AttachedDoc<Viewlet, 'specialViewActions'> {
extension: ComponentExtensionId
config?: Record<string, any>
}