Cards fixes (#8003)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov
2025-02-14 01:41:25 +07:00
committed by GitHub
parent 617a64713b
commit 9d87ce7ede
96 changed files with 2358 additions and 436 deletions
+91 -6
View File
@@ -475,6 +475,9 @@ importers:
'@rush-temp/model-server-calendar':
specifier: file:./projects/model-server-calendar.tgz
version: file:projects/model-server-calendar.tgz
'@rush-temp/model-server-card':
specifier: file:./projects/model-server-card.tgz
version: file:projects/model-server-card.tgz
'@rush-temp/model-server-chunter':
specifier: file:./projects/model-server-chunter.tgz
version: file:projects/model-server-chunter.tgz
@@ -820,6 +823,12 @@ importers:
'@rush-temp/server-calendar-resources':
specifier: file:./projects/server-calendar-resources.tgz
version: file:projects/server-calendar-resources.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@20.11.19)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3))
'@rush-temp/server-card':
specifier: file:./projects/server-card.tgz
version: file:projects/server-card.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3))
'@rush-temp/server-card-resources':
specifier: file:./projects/server-card-resources.tgz
version: file:projects/server-card-resources.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@20.11.19)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3))
'@rush-temp/server-chunter':
specifier: file:./projects/server-chunter.tgz
version: file:projects/server-chunter.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3))
@@ -3920,7 +3929,7 @@ packages:
version: 0.0.0
'@rush-temp/card@file:projects/card.tgz':
resolution: {integrity: sha512-rEy3T9z44ZC/94K3q01+0gbaZ0sJBlwB7KvmQuAbiZMU31t+pzS+yoIYkry7YHm/Ca7kd7H4uvcROiAr7yb91w==, tarball: file:projects/card.tgz}
resolution: {integrity: sha512-/vbzO3L239DHkxNpG5cw/ngDrvuvXrRcoq46Y7uJzNwSNAOMpiiRSMdpj4dr2bmPXEhkFGPeA0mKLJwXLseEhw==, tarball: file:projects/card.tgz}
version: 0.0.0
'@rush-temp/chunter-assets@file:projects/chunter-assets.tgz':
@@ -4208,7 +4217,7 @@ packages:
version: 0.0.0
'@rush-temp/model-all@file:projects/model-all.tgz':
resolution: {integrity: sha512-gkINR6HbfTB1ZSXTI2WHg9B+zIZtcSmb+Bb9fVxbGtq6e+RnxSlOlvjEBoru3ElfIO8x6SYd/KoQhRossQQKnQ==, tarball: file:projects/model-all.tgz}
resolution: {integrity: sha512-aDRtqYlITENsW02SJbs+KjoynFL+/QYQmYQVN6++EqsnAT+jHGPl/UpnmSr3ZMcALGcwTx61LKQ1nnxNen8tyA==, tarball: file:projects/model-all.tgz}
version: 0.0.0
'@rush-temp/model-analytics-collector@file:projects/model-analytics-collector.tgz':
@@ -4343,6 +4352,10 @@ packages:
resolution: {integrity: sha512-tZOPOELF87VVVRzz1CpJN2JWcTMcpO5l+9pbFEB0YHiGZnBd0oujKLMRiSaI4xXiGo0vhA9BjuHpMvcw7zzw0A==, tarball: file:projects/model-server-calendar.tgz}
version: 0.0.0
'@rush-temp/model-server-card@file:projects/model-server-card.tgz':
resolution: {integrity: sha512-SP381dOkAOymE4k9VaEP2o28yv7oJxJEol5LeYjcqHXkyN/al/v8UetEGmS4/gvuxk0Np0KZPpLnczV9O3ApUg==, tarball: file:projects/model-server-card.tgz}
version: 0.0.0
'@rush-temp/model-server-chunter@file:projects/model-server-chunter.tgz':
resolution: {integrity: sha512-qZkbTEVB25W2XQRrvecg/aTQ+kJhY4uBaq6AVGaEf9jS7ZI7NBzk71rXSv9800GGIe0CPWRpSP/453yZQKfNrg==, tarball: file:projects/model-server-chunter.tgz}
version: 0.0.0
@@ -4672,7 +4685,7 @@ packages:
version: 0.0.0
'@rush-temp/prod@file:projects/prod.tgz':
resolution: {integrity: sha512-m903gPliLrwApgEDgmJCrV74n9agm3V0SYbi3pYOmiVhcY6MPCRk8FprcHnYIW/VAqbzL/Bn8BYnLF8O9IQBLQ==, tarball: file:projects/prod.tgz}
resolution: {integrity: sha512-VeRmwZegE4UJxGZfbKFP55egCMYIBuW6fCawXf1NHcXc93vnE6m9sax3s/O++FU3bMIhusbA0d74ZRRCCozxTw==, tarball: file:projects/prod.tgz}
version: 0.0.0
'@rush-temp/products-assets@file:projects/products-assets.tgz':
@@ -4803,6 +4816,14 @@ packages:
resolution: {integrity: sha512-FQkSKyOpAPE2lnQ6wDRR1ar17olXsmHTWD1vW+ajxSNMuayv4mEVFftAFVfj6b1WvqyuJMiWR8TDv3CRWW5bug==, tarball: file:projects/server-calendar.tgz}
version: 0.0.0
'@rush-temp/server-card-resources@file:projects/server-card-resources.tgz':
resolution: {integrity: sha512-lp3RSXAHV78ux6p3I0iSbVkfY3aB0fXDfxMhxQZfjU9sSXLOsKolgDxMaUNXVm+/WIpS6dwLk4jWHgGopH8Nmw==, tarball: file:projects/server-card-resources.tgz}
version: 0.0.0
'@rush-temp/server-card@file:projects/server-card.tgz':
resolution: {integrity: sha512-ATIgXsB+SGQDQHyZqzQ8ooD/yAjqyimZzjMVZLMFJvRQenPLZEwdW0D+ikWila7ixB115IvKtGhrM8y7mvnMog==, tarball: file:projects/server-card.tgz}
version: 0.0.0
'@rush-temp/server-chunter-resources@file:projects/server-chunter-resources.tgz':
resolution: {integrity: sha512-anzs0C7AXicSpf9F+jjqdokc3cZunw5O8gC0095fQmEoOW94OmvTyboGrx8iK3i+iYam4Z6Bkt60rXRo3JvXZQ==, tarball: file:projects/server-chunter-resources.tgz}
version: 0.0.0
@@ -4940,7 +4961,7 @@ packages:
version: 0.0.0
'@rush-temp/server-pipeline@file:projects/server-pipeline.tgz':
resolution: {integrity: sha512-AEDx9sHjL/Dh1KCJ0DiICkS4JZ24HrqPlz4Wp09xmI9d3q35DpCS2oP8+qUjSF1qX5kZSIu0GrDshztCFYQScQ==, tarball: file:projects/server-pipeline.tgz}
resolution: {integrity: sha512-fmLSX42maXiVGBW3TM2hB1y5UG+weaJSBpInOVUMf/5Y59N3s0rACYuHR+wuzcmIdIRiXKMN7ypET7mz4sxT2g==, tarball: file:projects/server-pipeline.tgz}
version: 0.0.0
'@rush-temp/server-preference@file:projects/server-preference.tgz':
@@ -5056,7 +5077,7 @@ packages:
version: 0.0.0
'@rush-temp/setting-resources@file:projects/setting-resources.tgz':
resolution: {integrity: sha512-XudJm31jzp8dEwS9TsP8lpxB2/yYjvafLULunIlgG46LdhQBfDG711m9w9gLb41Jj9O1quj2egHI87fHfmwrrg==, tarball: file:projects/setting-resources.tgz}
resolution: {integrity: sha512-FVaGM7H5JXFKHZ7thrdx9tj94krIQWzJ61p/QXBwhW78ycK9lNliYCHP0pLMA+rsScxAdmR6wa7n7nRO5XBGbw==, tarball: file:projects/setting-resources.tgz}
version: 0.0.0
'@rush-temp/setting@file:projects/setting.tgz':
@@ -5204,7 +5225,7 @@ packages:
version: 0.0.0
'@rush-temp/tool@file:projects/tool.tgz':
resolution: {integrity: sha512-dMjmwZoox3cdyn4o7EY2NL8Kst4si0xI+HT9YSnaQd7A4cBwapn/oXi163xq2f9mOhf2oekXpt2iT1hK8JoQQQ==, tarball: file:projects/tool.tgz}
resolution: {integrity: sha512-1/Aodec9LySBThOZAeA3XJfP83lSVTVRFdlb8uNTORq1sHJxCADx9BliCh2dTzdhydW46wcAxMPeArhopkcN1Q==, tarball: file:projects/tool.tgz}
version: 0.0.0
'@rush-temp/tracker-assets@file:projects/tracker-assets.tgz':
@@ -19404,6 +19425,20 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@rush-temp/model-server-card@file:projects/model-server-card.tgz':
dependencies:
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.3.3))(eslint@8.56.0)(typescript@5.7.3)
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.7.3)
eslint: 8.56.0
eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.3.3))(eslint@8.56.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint-plugin-n@15.7.0(eslint@8.56.0))(eslint-plugin-promise@6.1.1(eslint@8.56.0))(eslint@8.56.0)(typescript@5.7.3)
eslint-plugin-import: 2.29.1(eslint@8.56.0)
eslint-plugin-n: 15.7.0(eslint@8.56.0)
eslint-plugin-promise: 6.1.1(eslint@8.56.0)
prettier: 3.2.5
typescript: 5.7.3
transitivePeerDependencies:
- supports-color
'@rush-temp/model-server-chunter@file:projects/model-server-chunter.tgz':
dependencies:
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.3.3))(eslint@8.56.0)(typescript@5.3.3)
@@ -22459,6 +22494,56 @@ snapshots:
- supports-color
- ts-node
'@rush-temp/server-card-resources@file:projects/server-card-resources.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@20.11.19)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3))':
dependencies:
'@types/jest': 29.5.12
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.3.3))(eslint@8.56.0)(typescript@5.7.3)
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.7.3)
eslint: 8.56.0
eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.3.3))(eslint@8.56.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint-plugin-n@15.7.0(eslint@8.56.0))(eslint-plugin-promise@6.1.1(eslint@8.56.0))(eslint@8.56.0)(typescript@5.7.3)
eslint-plugin-import: 2.29.1(eslint@8.56.0)
eslint-plugin-n: 15.7.0(eslint@8.56.0)
eslint-plugin-promise: 6.1.1(eslint@8.56.0)
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3))
prettier: 3.2.5
ts-jest: 29.1.2(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(jest@29.7.0(@types/node@20.11.19)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3)))(typescript@5.7.3)
typescript: 5.7.3
transitivePeerDependencies:
- '@babel/core'
- '@jest/types'
- '@types/node'
- babel-jest
- babel-plugin-macros
- esbuild
- node-notifier
- supports-color
- ts-node
'@rush-temp/server-card@file:projects/server-card.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3))':
dependencies:
'@types/jest': 29.5.12
'@types/node': 20.11.19
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.3.3))(eslint@8.56.0)(typescript@5.7.3)
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.7.3)
eslint: 8.56.0
eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.3.3))(eslint@8.56.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint-plugin-n@15.7.0(eslint@8.56.0))(eslint-plugin-promise@6.1.1(eslint@8.56.0))(eslint@8.56.0)(typescript@5.7.3)
eslint-plugin-import: 2.29.1(eslint@8.56.0)
eslint-plugin-n: 15.7.0(eslint@8.56.0)
eslint-plugin-promise: 6.1.1(eslint@8.56.0)
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3))
prettier: 3.2.5
ts-jest: 29.1.2(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(jest@29.7.0(@types/node@20.11.19)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3)))(typescript@5.7.3)
typescript: 5.7.3
transitivePeerDependencies:
- '@babel/core'
- '@jest/types'
- babel-jest
- babel-plugin-macros
- esbuild
- node-notifier
- supports-color
- ts-node
'@rush-temp/server-chunter-resources@file:projects/server-chunter-resources.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@20.11.19)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3))':
dependencies:
'@types/jest': 29.5.12
-38
View File
@@ -110,16 +110,6 @@
"@hcengineering/inventory": "^0.6.11",
"@hcengineering/inventory-assets": "^0.6.11",
"@hcengineering/inventory-resources": "^0.6.0",
"@hcengineering/server-attachment": "^0.6.1",
"@hcengineering/server-attachment-resources": "^0.6.0",
"@hcengineering/server-collaboration": "^0.6.0",
"@hcengineering/server-collaboration-resources": "^0.6.0",
"@hcengineering/server-contact": "^0.6.1",
"@hcengineering/server-contact-resources": "^0.6.0",
"@hcengineering/server-notification": "^0.6.1",
"@hcengineering/server-notification-resources": "^0.6.0",
"@hcengineering/server-setting": "^0.6.0",
"@hcengineering/server-setting-resources": "^0.6.0",
"@hcengineering/templates": "^0.6.11",
"@hcengineering/templates-assets": "^0.6.11",
"@hcengineering/templates-resources": "^0.6.0",
@@ -133,30 +123,10 @@
"@hcengineering/tags-assets": "^0.6.0",
"@hcengineering/tags": "^0.6.16",
"@hcengineering/tags-resources": "^0.6.0",
"@hcengineering/server-chunter": "^0.6.0",
"@hcengineering/server-chunter-resources": "^0.6.0",
"@hcengineering/server-inventory": "^0.6.1",
"@hcengineering/server-inventory-resources": "^0.6.0",
"@hcengineering/server-lead": "^0.6.0",
"@hcengineering/server-lead-resources": "^0.6.0",
"@hcengineering/server-recruit": "^0.6.0",
"@hcengineering/server-recruit-resources": "^0.6.0",
"@hcengineering/server-tags": "^0.6.0",
"@hcengineering/server-tags-resources": "^0.6.0",
"@hcengineering/server-task": "^0.6.0",
"@hcengineering/server-task-resources": "^0.6.0",
"@hcengineering/calendar": "^0.6.24",
"@hcengineering/calendar-assets": "^0.6.22",
"@hcengineering/calendar-resources": "^0.6.0",
"@hcengineering/server-calendar": "^0.6.0",
"@hcengineering/server-calendar-resources": "^0.6.0",
"@hcengineering/server-gmail": "^0.6.0",
"@hcengineering/server-gmail-resources": "^0.6.0",
"@hcengineering/server-telegram": "^0.6.0",
"@hcengineering/server-telegram-resources": "^0.6.0",
"@hcengineering/presentation": "^0.6.3",
"@hcengineering/server-tracker": "^0.6.0",
"@hcengineering/server-tracker-resources": "^0.6.0",
"@hcengineering/tracker": "^0.6.24",
"@hcengineering/tracker-assets": "^0.6.0",
"@hcengineering/tracker-resources": "^0.6.0",
@@ -169,20 +139,12 @@
"@hcengineering/hr": "^0.6.19",
"@hcengineering/hr-assets": "^0.6.19",
"@hcengineering/hr-resources": "^0.6.0",
"@hcengineering/server-hr": "^0.6.0",
"@hcengineering/server-hr-resources": "^0.6.0",
"@hcengineering/bitrix": "^0.6.52",
"@hcengineering/bitrix-assets": "^0.6.0",
"@hcengineering/bitrix-resources": "^0.6.0",
"@hcengineering/request": "^0.6.14",
"@hcengineering/request-assets": "^0.6.0",
"@hcengineering/request-resources": "^0.6.0",
"@hcengineering/server-request": "^0.6.0",
"@hcengineering/server-request-resources": "^0.6.0",
"@hcengineering/server-view": "^0.6.0",
"@hcengineering/server-view-resources": "^0.6.0",
"@hcengineering/server-activity": "^0.6.0",
"@hcengineering/server-activity-resources": "^0.6.0",
"@hcengineering/drive": "^0.6.0",
"@hcengineering/drive-assets": "^0.6.0",
"@hcengineering/drive-resources": "^0.6.0",
+2
View File
@@ -101,6 +101,8 @@
"@hcengineering/server-storage": "^0.6.0",
"@hcengineering/server-calendar": "^0.6.0",
"@hcengineering/server-calendar-resources": "^0.6.0",
"@hcengineering/server-card": "^0.6.0",
"@hcengineering/server-card-resources": "^0.6.0",
"@hcengineering/server-chunter": "^0.6.0",
"@hcengineering/server-chunter-resources": "^0.6.0",
"@hcengineering/server-contact": "^0.6.1",
+2
View File
@@ -23,6 +23,7 @@ import { devTool } from '.'
import { addLocation } from '@hcengineering/platform'
import { serverActivityId } from '@hcengineering/server-activity'
import { serverAttachmentId } from '@hcengineering/server-attachment'
import { serverCardId } from '@hcengineering/server-card'
import { serverCalendarId } from '@hcengineering/server-calendar'
import { serverChunterId } from '@hcengineering/server-chunter'
import { serverCollaborationId } from '@hcengineering/server-collaboration'
@@ -59,6 +60,7 @@ addLocation(serverSettingId, () => import('@hcengineering/server-setting-resourc
addLocation(serverTaskId, () => import('@hcengineering/server-task-resources'))
addLocation(serverTrackerId, () => import('@hcengineering/server-tracker-resources'))
addLocation(serverTagsId, () => import('@hcengineering/server-tags-resources'))
addLocation(serverCardId, () => import('@hcengineering/server-card-resources'))
addLocation(serverCalendarId, () => import('@hcengineering/server-calendar-resources'))
addLocation(serverGmailId, () => import('@hcengineering/server-gmail-resources'))
addLocation(serverTelegramId, () => import('@hcengineering/server-telegram-resources'))
+1
View File
@@ -87,6 +87,7 @@
"@hcengineering/model-bitrix": "^0.6.0",
"@hcengineering/model-request": "^0.6.0",
"@hcengineering/model-support": "^0.6.0",
"@hcengineering/model-server-card": "^0.6.0",
"@hcengineering/model-server-request": "^0.6.0",
"@hcengineering/model-server-view": "^0.6.0",
"@hcengineering/model-server-activity": "^0.6.0",
+5 -2
View File
@@ -37,6 +37,7 @@ import { requestId, createModel as requestModel } from '@hcengineering/model-req
import { aiBotId, createModel as aiBotModel } from '@hcengineering/model-ai-bot'
import { serverActivityId, createModel as serverActivityModel } from '@hcengineering/model-server-activity'
import { serverAttachmentId, createModel as serverAttachmentModel } from '@hcengineering/model-server-attachment'
import { serverCardId, createModel as serverCardModel } from '@hcengineering/model-server-card'
import { serverCalendarId, createModel as serverCalendarModel } from '@hcengineering/model-server-calendar'
import { serverChunterId, createModel as serverChunterModel } from '@hcengineering/model-server-chunter'
import {
@@ -146,7 +147,8 @@ export default function buildModel (enabled: string[] = ['*'], disabled: string[
view.class.Action,
contact.class.ChannelProvider,
setting.class.IntegrationType,
setting.class.WorkspaceSettingCategory
setting.class.WorkspaceSettingCategory,
setting.class.SettingsCategory
]
const builders: BuilderConfig[] = [
@@ -366,7 +368,7 @@ export default function buildModel (enabled: string[] = ['*'], disabled: string[
{
label: card.string.Cards,
description: card.string.ConfigDescription,
enabled: false,
enabled: true,
beta: true,
icon: card.icon.Card,
classFilter: defaultFilter
@@ -440,6 +442,7 @@ export default function buildModel (enabled: string[] = ['*'], disabled: string[
[serverTagsModel, serverTagsId],
[serverTaskModel, serverTaskId],
[serverTrackerModel, serverTrackerId],
[serverCardModel, serverCardId],
[serverCalendarModel, serverCalendarId],
[serverRecruitModel, serverRecruitId],
[serverGmailModel, serverGmailId],
+1
View File
@@ -29,6 +29,7 @@
},
"dependencies": {
"@hcengineering/activity": "^0.6.0",
"@hcengineering/contact": "^0.6.24",
"@hcengineering/core": "^0.6.32",
"@hcengineering/model": "^0.6.11",
"@hcengineering/chunter": "^0.6.20",
+91 -18
View File
@@ -14,8 +14,11 @@
import activity from '@hcengineering/activity'
import { CardEvents, cardId, type Card, type MasterTag, type Tag } from '@hcengineering/card'
import chunter from '@hcengineering/chunter'
import contact from '@hcengineering/contact'
import core, {
AccountRole,
DOMAIN_MODEL,
IndexKind,
type CollectionSize,
type Domain,
type MarkupBlobRef,
@@ -24,12 +27,12 @@ import core, {
} from '@hcengineering/core'
import {
Collection,
Index,
Model,
Prop,
TypeCollaborativeDoc,
TypeRef,
TypeString,
UX,
type Builder
} from '@hcengineering/model'
import attachment from '@hcengineering/model-attachment'
@@ -38,8 +41,9 @@ import presentation from '@hcengineering/model-presentation'
import setting from '@hcengineering/model-setting'
import view, { createAction } from '@hcengineering/model-view'
import workbench from '@hcengineering/model-workbench'
import { getEmbeddedLabel } from '@hcengineering/platform'
import { getEmbeddedLabel, type IntlString } from '@hcengineering/platform'
import time, { type ToDo } from '@hcengineering/time'
import { type AnyComponent } from '@hcengineering/ui'
import card from './plugin'
export { cardId } from '@hcengineering/card'
@@ -53,12 +57,12 @@ export class TMasterTag extends TClass implements MasterTag {}
export class TTag extends TMixin implements Tag {}
@Model(card.class.Card, core.class.Doc, DOMAIN_CARD)
@UX(card.string.Card)
export class TCard extends TDoc implements Card {
@Prop(TypeRef(card.class.MasterTag), card.string.MasterTag)
declare _class: Ref<MasterTag>
@Prop(TypeString(), core.string.Name)
@Index(IndexKind.FullText)
title!: string
@Prop(TypeCollaborativeDoc(), card.string.Content)
@@ -75,8 +79,15 @@ export class TCard extends TDoc implements Card {
todos?: CollectionSize<ToDo>
}
@Model(card.class.MasterTagEditorSection, core.class.Doc, DOMAIN_MODEL)
export class MasterTagEditorSection extends TDoc implements MasterTagEditorSection {
id!: string
label!: IntlString
component!: AnyComponent
}
export function createModel (builder: Builder): void {
builder.createModel(TMasterTag, TTag, TCard)
builder.createModel(TMasterTag, TTag, TCard, MasterTagEditorSection)
builder.createDoc(
workbench.class.Application,
@@ -113,20 +124,6 @@ export function createModel (builder: Builder): void {
card.viewlet.CardTable
)
builder.createDoc(
setting.class.WorkspaceSettingCategory,
core.space.Model,
{
name: 'masterTags',
label: card.string.Tags,
icon: card.icon.Tags,
role: AccountRole.User,
component: card.component.MasterTags,
order: 4400
},
card.ids.MasterTags
)
builder.mixin(card.class.Card, core.class.Class, view.mixin.ObjectPresenter, {
presenter: card.component.CardPresenter
})
@@ -268,11 +265,55 @@ export function createModel (builder: Builder): void {
}
})
builder.createDoc(
setting.class.WorkspaceSettingCategory,
core.space.Model,
{
name: 'tagrelation',
label: card.string.TagRelations,
icon: setting.icon.Relations,
props: { _classes: [card.class.Card, contact.class.Contact], exclude: [] },
component: setting.component.RelationSetting,
group: 'settings-editor',
role: AccountRole.Maintainer,
order: 4501
},
card.ids.TagRelations
)
builder.createDoc(
setting.class.SettingsCategory,
core.space.Model,
{
name: 'masterTags',
label: card.string.MasterTags,
icon: card.icon.Card,
component: card.component.ManageMasterTagsContent,
extraComponents: {
navigation: card.component.ManageMasterTags,
tools: card.component.ManageMasterTagsTools
},
group: 'settings-editor',
role: AccountRole.User,
order: 5000,
expandable: true
},
card.ids.ManageMasterTags
)
builder.mixin(card.class.Card, core.class.Class, view.mixin.ClassFilters, {
filters: [],
ignoreKeys: ['parent']
})
builder.createDoc(core.class.FullTextSearchContext, core.space.Model, {
toClass: card.class.Card,
fullTextSummary: true,
forceIndex: true,
childProcessingAllowed: true,
propagate: []
})
builder.createDoc(
presentation.class.ObjectSearchCategory,
core.space.Model,
@@ -283,10 +324,42 @@ export function createModel (builder: Builder): void {
query: card.completion.CardQuery,
context: ['search', 'mention', 'spotlight'],
classToSearch: card.class.Card,
includeChilds: true,
priority: 500
},
card.completion.CardCategory
)
builder.createDoc(card.class.MasterTagEditorSection, core.space.Model, {
id: 'general',
label: setting.string.General,
component: card.component.GeneralSection
})
builder.createDoc(card.class.MasterTagEditorSection, core.space.Model, {
id: 'properties',
label: setting.string.Properties,
component: card.component.ProperitiesSection
})
builder.createDoc(card.class.MasterTagEditorSection, core.space.Model, {
id: 'tags',
label: card.string.Tags,
component: card.component.TagsSection
})
builder.createDoc(card.class.MasterTagEditorSection, core.space.Model, {
id: 'childs',
label: card.string.MasterTags,
masterOnly: true,
component: card.component.ChildsSection
})
builder.createDoc(card.class.MasterTagEditorSection, core.space.Model, {
id: 'relations',
label: core.string.Relations,
component: card.component.RelationsSection
})
}
export default card
+3 -1
View File
@@ -34,7 +34,9 @@ export default mergeIds(cardId, card, {
DeleteMasterTag: '' as Ref<Action>
},
ids: {
MasterTags: '' as Ref<Doc>
MasterTags: '' as Ref<Doc>,
ManageMasterTags: '' as Ref<Doc>,
TagRelations: '' as Ref<Doc>
},
completion: {
CardQuery: '' as Resource<ObjectSearchFactory>,
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
extends: ['./node_modules/@hcengineering/platform-rig/profiles/model/eslint.config.json'],
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json'
}
}
+4
View File
@@ -0,0 +1,4 @@
*
!/lib/**
!CHANGELOG.md
/lib/**/__tests__/
+5
View File
@@ -0,0 +1,5 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
"rigPackageName": "@hcengineering/platform-rig",
"rigProfile": "model"
}
+38
View File
@@ -0,0 +1,38 @@
{
"name": "@hcengineering/model-server-card",
"version": "0.6.0",
"main": "lib/index.js",
"svelte": "src/index.ts",
"types": "types/index.d.ts",
"author": "Anticrm Platform Contributors",
"template": "@hcengineering/model-package",
"license": "EPL-2.0",
"scripts": {
"build": "compile",
"build:watch": "compile",
"format": "format src",
"_phase:build": "compile transpile src",
"_phase:format": "format src",
"_phase:validate": "compile validate"
},
"devDependencies": {
"@hcengineering/platform-rig": "^0.6.0",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-n": "^15.4.0",
"eslint": "^8.54.0",
"@typescript-eslint/parser": "^6.11.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"prettier": "^3.1.0",
"typescript": "^5.3.3"
},
"dependencies": {
"@hcengineering/core": "^0.6.32",
"@hcengineering/model": "^0.6.11",
"@hcengineering/platform": "^0.6.11",
"@hcengineering/card": "^0.6.0",
"@hcengineering/server-card": "^0.6.0",
"@hcengineering/server-core": "^0.6.1"
}
}
+39
View File
@@ -0,0 +1,39 @@
//
// 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 Builder } from '@hcengineering/model'
import core from '@hcengineering/core'
import serverCore from '@hcengineering/server-core'
import serverCard from '@hcengineering/server-card'
import card from '@hcengineering/card'
export { serverCardId } from '@hcengineering/server-card'
export function createModel (builder: Builder): void {
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverCard.trigger.OnAttribute,
isAsync: true,
txMatch: {
_class: core.class.TxCreateDoc,
objectClass: core.class.Attribute
}
})
builder.mixin(card.class.Card, core.class.Class, serverCore.mixin.SearchPresenter, {
searchIcon: card.icon.Card,
title: [['title']]
})
}
+10
View File
@@ -0,0 +1,10 @@
{
"extends": "./node_modules/@hcengineering/platform-rig/profiles/model/tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib",
"declarationDir": "./types",
"tsBuildInfoFile": ".build/build.tsbuildinfo"
}
}
+12 -22
View File
@@ -32,18 +32,6 @@ export interface SearchItem {
category: ObjectSearchCategory
}
function findCategoryByClass (
categories: ObjectSearchCategory[],
_class: Ref<Class<Doc>>
): ObjectSearchCategory | undefined {
for (const category of categories) {
if (category.classToSearch === _class) {
return category
}
}
return undefined
}
function packSearchResultsForListView (sections: SearchSection[]): SearchItem[] {
let results: SearchItem[] = []
for (const section of sections) {
@@ -63,35 +51,37 @@ function packSearchResultsForListView (sections: SearchSection[]): SearchItem[]
async function searchCategory (
client: TxOperations,
cl: Ref<Class<Doc>>,
category: ObjectSearchCategory,
query: string,
categories: ObjectSearchCategory[],
limit?: number
): Promise<SearchSection | undefined> {
if (category.classToSearch === undefined) return
const classes =
category.includeChilds === true
? client.getHierarchy().getDescendants(category.classToSearch)
: [category.classToSearch]
const r = await client.searchFulltext(
{
query: `${query}*`,
classes: [cl]
classes
},
{
limit: limit ?? 5
}
)
const category = findCategoryByClass(categories, cl)
return category !== undefined ? { category, items: r.docs } : undefined
return { category, items: r.docs }
}
async function doFulltextSearch (
client: TxOperations,
classes: Array<Ref<Class<Doc>>>,
query: string,
categories: ObjectSearchCategory[],
query: string,
limit?: number
): Promise<SearchSection[]> {
const sections: SearchSection[] = []
const promises: Array<Promise<SearchSection | undefined>> = []
for (const cl of classes) {
promises.push(searchCategory(client, cl, query, categories, limit))
for (const cat of categories) {
promises.push(searchCategory(client, cat, query, limit))
}
const resolvedSections = await Promise.all(promises)
@@ -140,6 +130,6 @@ export async function searchFor (
}
}
const sections = await doFulltextSearch(client, classesToSearch, query, categories, limit)
const sections = await doFulltextSearch(client, cats, query, limit)
return { items: packSearchResultsForListView(sections), query }
}
+1
View File
@@ -72,6 +72,7 @@ export interface ObjectSearchCategory extends Doc {
// Query for documents with pattern
query: Resource<ObjectSearchFactory>
classToSearch?: Ref<Class<Doc>>
includeChilds?: boolean
priority?: number
}
+92 -8
View File
@@ -15,6 +15,7 @@
import { Analytics } from '@hcengineering/analytics'
import core, {
Association,
BulkUpdateEvent,
Class,
Client,
@@ -30,6 +31,7 @@ import core, {
Mixin,
ModelDb,
Ref,
Relation,
ReverseLookups,
SearchOptions,
SearchQuery,
@@ -659,6 +661,7 @@ export class LiveQuery implements WithTx, Client {
}
}
await this.handleDocUpdateLookup(q, tx)
await this.handleDocUpdateRelation(q, tx)
}
}
return {}
@@ -724,6 +727,50 @@ export class LiveQuery implements WithTx, Client {
return
}
await this.handleDocUpdateLookup(q, tx)
await this.handleDocUpdateRelation(q, tx)
}
private isPossibleAssociationTx (tx: TxUpdateDoc<Doc> | TxMixin<Doc, Doc>, association: Association): boolean {
const h = this.getHierarchy()
const byClass =
h.isDerived(tx.objectClass, association.classA) ||
h.isDerived(tx.objectClass, association.classB) ||
h.isDerived(association.classA, tx.objectClass) ||
h.isDerived(association.classB, tx.objectClass)
if (byClass) {
return true
}
if (tx._class === core.class.TxMixin) {
const mixinTx = tx as TxMixin<Doc, Doc>
return h.isDerived(mixinTx.mixin, association.classA) || h.isDerived(mixinTx.mixin, association.classB)
}
return false
}
private async handleDocUpdateRelation (q: Query, tx: TxUpdateDoc<Doc> | TxMixin<Doc, Doc>): Promise<void> {
if (q.options?.associations === undefined) return
for (const assoc of q.options.associations) {
const association = this.getModel().findObject(assoc[0])
if (association === undefined) continue
if (this.isPossibleAssociationTx(tx, association)) {
if (q.result instanceof Promise) {
q.result = await q.result
}
const docs = q.result.getDocs()
for (const doc of docs) {
const docToUpdate = doc.$associations?.[association._id].find((it) => it._id === tx.objectId)
if (docToUpdate !== undefined) {
if (tx._class === core.class.TxMixin) {
TxProcessor.updateMixin4Doc(docToUpdate, tx as TxMixin<Doc, Doc>)
} else {
TxProcessor.updateDoc2Doc(docToUpdate, tx as TxUpdateDoc<Doc>)
}
q.result.updateDoc(doc, false)
this.queriesToUpdate.set(q.id, q)
}
}
}
}
}
private async handleDocUpdateLookup (q: Query, tx: TxUpdateDoc<Doc> | TxMixin<Doc, Doc>): Promise<void> {
@@ -971,6 +1018,7 @@ export class LiveQuery implements WithTx, Client {
}
await this.handleDocAddLookup(q, doc)
await this.handleDocAddRelation(q, doc)
}
}
return {}
@@ -1039,6 +1087,35 @@ export class LiveQuery implements WithTx, Client {
}
}
private async handleDocAddRelation (q: Query, doc: Doc): Promise<void> {
if (q.options?.associations === undefined) return
if (doc._class !== core.class.Relation) return
const relation = doc as Relation
const assoc = q.options.associations.find((p) => p[0] === relation.association)
if (assoc !== undefined) {
if (q.result instanceof Promise) {
q.result = await q.result
}
const direct = assoc[1] === 1
const res = q.result.findDoc(direct ? relation.docA : relation.docB)
if (res === undefined) return
const association = this.getModel().findObject(assoc[0])
if (association === undefined) return
const docToPush = await this.findOne(!direct ? association.classB : association.classA, {
_id: direct ? relation.docB : relation.docA
})
if (docToPush === undefined) return
const arr = res?.$associations?.[relation.association] ?? []
arr.push(docToPush)
if (res?.$associations === undefined) {
res.$associations = {}
}
res.$associations[relation.association] = arr
q.result.updateDoc(res, false)
this.queriesToUpdate.set(q.id, q)
}
}
private async handleDocAddLookup (q: Query, doc: Doc): Promise<void> {
if (q.options?.lookup === undefined) return
const lookup = q.options.lookup
@@ -1120,16 +1197,16 @@ export class LiveQuery implements WithTx, Client {
if (q.result instanceof Promise) {
q.result = await q.result
}
if (
q.options?.limit !== undefined &&
q.options.limit === q.result.length &&
h.isDerived(q._class, tx.objectClass)
) {
await this.refresh(q)
return
}
const index = q.result.getDocs().find((p) => p._id === tx.objectId && h.isDerived(p._class, tx.objectClass))
if (index !== undefined) {
if (
q.options?.limit !== undefined &&
q.options.limit === q.result.length &&
h.isDerived(q._class, tx.objectClass)
) {
await this.refresh(q)
return
}
q.result.delete(index._id)
this.refs.updateDocuments(q, [index], true)
@@ -1139,6 +1216,13 @@ export class LiveQuery implements WithTx, Client {
await this.callback(q, true)
}
await this.handleDocRemoveLookup(q, tx)
await this.handleDocRemoveRelation(q, tx)
}
private async handleDocRemoveRelation (q: Query, tx: TxRemoveDoc<Doc>): Promise<void> {
if (q.options?.associations === undefined) return
if (tx.objectClass !== core.class.Relation) return
await this.refresh(q)
}
private async handleDocRemoveLookup (q: Query, tx: TxRemoveDoc<Doc>): Promise<void> {
+8 -7
View File
@@ -1,5 +1,6 @@
import {
resultSort,
WithLookup,
type Class,
type Doc,
type Hierarchy,
@@ -9,9 +10,9 @@ import {
} from '@hcengineering/core'
export class ResultArray {
private docs: Map<Ref<Doc>, Doc>
private docs: Map<Ref<Doc>, WithLookup<Doc>>
private readonly clones = new Map<string, Map<Ref<Doc>, Doc>>()
private readonly clones = new Map<string, Map<Ref<Doc>, WithLookup<Doc>>>()
get length (): number {
return this.docs.size
@@ -28,11 +29,11 @@ export class ResultArray {
this.clones.clear()
}
getDocs (): Doc[] {
getDocs (): WithLookup<Doc>[] {
return Array.from(this.docs.values())
}
findDoc (_id: Ref<Doc>): Doc | undefined {
findDoc (_id: Ref<Doc>): WithLookup<Doc> | undefined {
return this.docs.get(_id)
}
@@ -61,14 +62,14 @@ export class ResultArray {
return doc
}
updateDoc (doc: Doc, mainClone = true): void {
updateDoc (doc: WithLookup<Doc>, mainClone = true): void {
this.docs.set(doc._id, mainClone ? this.hierarchy.clone(doc) : doc)
for (const [, v] of this.clones.entries()) {
v.set(doc._id, this.hierarchy.clone(doc))
}
}
push (doc: Doc): void {
push (doc: WithLookup<Doc>): void {
this.docs.set(doc._id, this.hierarchy.clone(doc))
for (const [, v] of this.clones.entries()) {
v.set(doc._id, this.hierarchy.clone(doc))
@@ -76,7 +77,7 @@ export class ResultArray {
// this.changes.add(doc._id)
}
pop (): Doc | undefined {
pop (): WithLookup<Doc> | undefined {
const lastElement = Array.from(this.docs)[this.docs.size - 1]
if (lastElement !== undefined) {
this.docs.delete(lastElement[0])
@@ -13,12 +13,12 @@
// limitations under the License.
-->
<script lang="ts">
import { createEventDispatcher, ComponentType } from 'svelte'
import type { Asset, IntlString } from '@hcengineering/platform'
import { AnySvelteComponent, BreadcrumbItem } from '../types'
import type { IntlString } from '@hcengineering/platform'
import { createEventDispatcher } from 'svelte'
import { BreadcrumbItem } from '../types'
import Breadcrumb from './Breadcrumb.svelte'
import ChevronRight from './icons/ChevronRight.svelte'
import Label from './Label.svelte'
import Breadcrumb from './Breadcrumb.svelte'
export let items: BreadcrumbItem[]
export let afterLabel: IntlString | undefined = undefined
@@ -0,0 +1,62 @@
<!--
// Copyright © 2022 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.
-->
<script lang="ts">
import { IntlString } from '@hcengineering/platform'
import { createEventDispatcher } from 'svelte'
import ui from '../plugin'
import { showPopup } from '../popups'
import type { DropdownIntlItem } from '../types'
import Button from './Button.svelte'
import DropdownIcon from './icons/Dropdown.svelte'
import NestedMenu from './NestedMenu.svelte'
import Label from './Label.svelte'
export let items: [DropdownIntlItem, DropdownIntlItem[]][]
export let label: IntlString = ui.string.DropdownDefaultLabel
export let disabled: boolean = false
export let selected: DropdownIntlItem | undefined = undefined
let container: HTMLElement
let opened: boolean = false
const dispatch = createEventDispatcher()
function openPopup (): void {
if (!opened) {
opened = true
showPopup(NestedMenu, { items }, container, (result) => {
if (result !== undefined) {
selected = result
dispatch('selected', result.id)
}
opened = false
})
}
}
</script>
<div bind:this={container}>
<Button width={'min-content'} {disabled} on:click={openPopup}>
<span slot="content" class="overflow-label disabled flex-grow text-left mr-2">
<Label label={selected !== undefined ? selected.label : label} />
</span>
<svelte:fragment slot="iconRight">
<DropdownIcon
size={'small'}
fill={!disabled ? 'var(--primary-button-content-color)' : 'var(--theme-dark-color)'}
/>
</svelte:fragment>
</Button>
</div>
@@ -0,0 +1,119 @@
<!--
// 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.
-->
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import { Label, Scroller, Submenu } from '..'
import { resizeObserver } from '../resize'
import { DropdownIntlItem } from '../types'
import NestedMenu from './NestedMenu.svelte'
export let items: [DropdownIntlItem, DropdownIntlItem[]][]
export let nestedFrom: DropdownIntlItem | undefined = undefined
export let onSelect: ((val: DropdownIntlItem) => void) | undefined = undefined
const elements: HTMLElement[] = []
const dispatch = createEventDispatcher()
const actionElements: HTMLButtonElement[] = []
const keyDown = (event: KeyboardEvent, index: number): void => {
if (event.key === 'ArrowDown') {
actionElements[(index + 1) % actionElements.length]?.focus()
}
if (event.key === 'ArrowUp') {
actionElements[(actionElements.length + index - 1) % actionElements.length]?.focus()
}
if (event.key === 'ArrowLeft') {
dispatch('close')
}
}
function onNestedSelect (val: any): void {
dispatch('close', val)
}
function click (val: DropdownIntlItem): void {
onSelect?.(val)
dispatch('close', val)
}
</script>
<div class="selectPopup" use:resizeObserver={() => dispatch('changeContent')}>
<div class="menu-space" />
<Scroller>
{#if nestedFrom}
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
<button
class="menu-item"
on:keydown={(event) => {
keyDown(event, -1)
}}
on:mouseover={(event) => {
event.currentTarget.focus()
}}
on:click={() => {
if (nestedFrom !== undefined) {
click(nestedFrom)
}
}}
>
<div class="overflow-label pr-1"><Label label={nestedFrom.label} /></div>
</button>
<div class="divider" />
{/if}
{#each items as item, i}
{#if item[1].length > 0 && nestedFrom === undefined}
<Submenu
bind:element={elements[i]}
on:keydown={(event) => {
keyDown(event, i)
}}
on:mouseover={() => {
elements[i]?.focus()
}}
label={item[0].label}
props={{
items: item[1].map((p) => {
return [p, []]
}),
nestedFrom: item[0],
onSelect: onNestedSelect
}}
options={{ component: NestedMenu }}
/>
{:else}
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
<button
class="menu-item"
on:keydown={(event) => {
keyDown(event, i)
}}
on:mouseover={(event) => {
event.currentTarget.focus()
}}
on:click={() => {
click(item[0])
}}
>
<div class="overflow-label pr-1"><Label label={item[0].label} /></div>
</button>
{/if}
{/each}
</Scroller>
<div class="menu-space" />
</div>
+2
View File
@@ -281,6 +281,8 @@ export { default as CodeInput } from './components/CodeInput.svelte'
export { default as TimeLeft } from './components/TimeLeft.svelte'
export { default as SectionEmpty } from './components/SectionEmpty.svelte'
export { default as EmbeddedPDF } from './components/EmbeddedPDF.svelte'
export { default as NestedMenu } from './components/NestedMenu.svelte'
export { default as NestedDropdown } from './components/NestedDropdown.svelte'
export { default as Dock } from './components/Dock.svelte'
+2 -13
View File
@@ -26,19 +26,8 @@
clip-rule="evenodd"
d="M6 4.00012C3.79086 4.00012 2 5.79098 2 8.00012V24.0001C2 26.2093 3.79086 28.0001 6 28.0001H26C28.2091 28.0001 30 26.2093 30 24.0001V8.00012C30 5.79098 28.2091 4.00012 26 4.00012H6ZM15 6.00012H6C4.89543 6.00012 4 6.89555 4 8.00012V24.0001C4 25.1047 4.89543 26.0001 6 26.0001H15V6.00012ZM17 6.00012V26.0001H26C27.1046 26.0001 28 25.1047 28 24.0001V8.00012C28 6.89555 27.1046 6.00012 26 6.00012H17Z"
/></symbol>
<symbol id="masterTag" viewBox="0 0 27 27">
<rect width="27" height="27" rx="13.5" fill="url(#paint0_linear_1280_1795)"/>
<circle cx="13.4999" cy="13.5" r="10.2892" fill="#36373D"/>
<path d="M8.55228 12.5117C8.32591 11.8852 8.21272 11.5719 8.26099 11.3712C8.31377 11.1516 8.46992 10.9805 8.66949 10.9234C8.85196 10.8712 9.13581 10.9973 9.70351 11.2496C10.2056 11.4728 10.4567 11.5844 10.6926 11.5782C10.9523 11.5714 11.2022 11.4672 11.4009 11.2829C11.5814 11.1155 11.7025 10.8488 11.9447 10.3154L12.4783 9.13972C12.9241 8.15768 13.147 7.66666 13.5 7.66666C13.853 7.66666 14.0759 8.15768 14.5216 9.13972L15.0553 10.3154C15.2975 10.8488 15.4186 11.1155 15.5991 11.2829C15.7978 11.4672 16.0476 11.5714 16.3074 11.5782C16.5433 11.5844 16.7944 11.4728 17.2965 11.2496C17.8642 10.9973 18.1481 10.8712 18.3305 10.9234C18.5301 10.9805 18.6862 11.1516 18.739 11.3712C18.7873 11.5719 18.6741 11.8852 18.4477 12.5116L17.4747 15.2046C17.0584 16.3566 16.8504 16.9326 16.4148 17.2579C15.9793 17.5833 15.4165 17.5833 14.2909 17.5833H12.7091C11.5835 17.5833 11.0207 17.5833 10.5852 17.2579C10.1496 16.9326 9.94153 16.3566 9.52529 15.2046L8.55228 12.5117Z" stroke="#FDFDFD" stroke-width="0.875"/>
<path d="M13.5 14.6667H13.5052" stroke="#FDFDFD" stroke-width="1.16667" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.5833 19.3333H16.4166" stroke="#FDFDFD" stroke-width="0.875" stroke-linecap="round"/>
<defs>
<linearGradient id="paint0_linear_1280_1795" x1="2.51228" y1="4.28851" x2="23.7537" y2="23.254" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="0.535" stop-color="#DCE9FE"/>
<stop offset="1" stop-color="#848B98"/>
</linearGradient>
</defs>
<symbol id="masterTag" viewBox="0 0 32 32">
<path d="M27.4,19.9h-6.4l1.9-7.8h4.5c0.6,0,1-0.4,1-1s-0.4-1-1-1h-4l1.7-6.9c0.1-0.5-0.2-1.1-0.7-1.2c-0.5-0.1-1.1,0.2-1.2,0.7l-1.8,7.4h-7.7l1.7-6.9c0.1-0.5-0.2-1.1-0.7-1.2c-0.5-0.1-1.1,0.2-1.2,0.7l-1.8,7.4H4.6c-0.6,0-1,0.4-1,1s0.4,1,1,1h6.4l-1.9,7.8H4.6c-0.6,0-1,0.4-1,1s0.4,1,1,1h4l-1.7,6.9c-0.1,0.5,0.2,1.1,0.7,1.2c0.1,0,0.2,0,0.2,0c0.4,0,0.9-0.3,1-0.8l1.8-7.4h7.7l-1.7,6.9c-0.1,0.5,0.2,1.1,0.7,1.2c0.1,0,0.2,0,0.2,0c0.4,0,0.9-0.3,1-0.8l1.8-7.4h6.9c0.6,0,1-0.4,1-1S27.9,19.9,27.4,19.9z M11.2,19.9l1.9-7.8h7.7l-1.9,7.8H11.2z" />
</symbol>
<symbol id="masterTags" viewBox="0 0 16 16">
<path d="M2 4.5C2.55228 4.5 3 4.05228 3 3.5C3 2.94772 2.55228 2.5 2 2.5C1.44772 2.5 1 2.94772 1 3.5C1 4.05228 1.44772 4.5 2 4.5Z" />

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

+7 -6
View File
@@ -5,19 +5,20 @@
"Cards": "Karty",
"Content": "Obsah",
"CreateCard": "Vytvořit kartu",
"CreateMasterTag": "Vytvořit hlavní štítek",
"CreateMasterTag": "Vytvořit typ",
"CreateTag": "Vytvořit štítek",
"MasterTag": "Hlavní štítek",
"MasterTags": "Hlavní štítky",
"Parent": "Rodič",
"MasterTag": "Typ",
"MasterTags": "Typy",
"Parent": "Nadřazený",
"Tags": "Štítky",
"Tag": "Štítek",
"CardLibrary": "Knihovna karet",
"MinimizeAll": "Minimalizovat vše",
"ExpandAll": "Rozbalit vše",
"DeleteTag": "Smazat štítek",
"TagRelations": "Vztahy štítků",
"DeleteTagConfirm": "Opravdu chcete smazat tento štítek? Všechny související vlastnosti budou smazány",
"DeleteMasterTag": "Smazat hlavní štítek",
"DeleteMasterTagConfirm": "Opravdu chcete smazat tento hlavní štítek? Všechny objekty související s tímto hlavním štítkem budou smazány"
"DeleteMasterTag": "Smazat typ",
"DeleteMasterTagConfirm": "Opravdu chcete smazat tento typ? Všechny objekty související s tímto typem budou smazány"
}
}
+1
View File
@@ -15,6 +15,7 @@
"CardLibrary": "Kartenbibliothek",
"MinimizeAll": "Alle minimieren",
"ExpandAll": "Alle erweitern",
"TagRelations": "Tag-Beziehungen",
"DeleteTag": "Tag löschen",
"DeleteTagConfirm": "Möchten Sie diesen Tag wirklich löschen? Alle zugehörigen Eigenschaften werden gelöscht",
"DeleteMasterTag": "Master-Tag löschen",
+6 -5
View File
@@ -5,10 +5,10 @@
"Cards": "Cards",
"Content": "Content",
"CreateCard": "Create Card",
"CreateMasterTag": "Create Master Tag",
"CreateMasterTag": "Create Type",
"CreateTag": "Create Tag",
"MasterTag": "Master Tag",
"MasterTags": "Master Tags",
"MasterTag": "Type",
"MasterTags": "Types",
"Parent": "Parent",
"Tags": "Tags",
"Tag": "Tag",
@@ -16,8 +16,9 @@
"MinimizeAll": "Minimize All",
"ExpandAll": "Expand All",
"DeleteTag": "Delete Tag",
"TagRelations": "Tag Relations",
"DeleteTagConfirm": "Are you sure you want to delete this tag? All related properties will be deleted",
"DeleteMasterTag": "Delete Master Tag",
"DeleteMasterTagConfirm": "Are you sure you want to delete this master tag? All objects related to this master tag will be deleted"
"DeleteMasterTag": "Delete Type",
"DeleteMasterTagConfirm": "Are you sure you want to delete this type? All objects related to this type will be deleted"
}
}
+7 -6
View File
@@ -5,10 +5,10 @@
"Cards": "Tarjetas",
"Content": "Contenido",
"CreateCard": "Crear Tarjeta",
"CreateMasterTag": "Crear Etiqueta Maestra",
"CreateMasterTag": "Crear Tipo",
"CreateTag": "Crear Etiqueta",
"MasterTag": "Etiqueta Maestra",
"MasterTags": "Etiquetas Maestras",
"MasterTag": "Tipo",
"MasterTags": "Tipos",
"Parent": "Padre",
"Tags": "Etiquetas",
"Tag": "Etiqueta",
@@ -16,8 +16,9 @@
"MinimizeAll": "Minimizar Todo",
"ExpandAll": "Expandir Todo",
"DeleteTag": "Eliminar Etiqueta",
"DeleteTagConfirm": "¿Está seguro de que desea eliminar esta etiqueta? Todas las propiedades relacion",
"DeleteMasterTag": "Eliminar Etiqueta Maestra",
"DeleteMasterTagConfirm": "¿Está seguro de que desea eliminar esta etiqueta maestra? Todos los objetos relacionados con esta etiqueta maestra serán eliminados"
"TagRelations": "Relaciones de Etiquetas",
"DeleteTagConfirm": "¿Estás seguro de que quieres eliminar esta etiqueta? Todas las propiedades relacionadas serán eliminadas",
"DeleteMasterTag": "Eliminar Tipo",
"DeleteMasterTagConfirm": "¿Estás seguro de que quieres eliminar este tipo? Todos los objetos relacionados con este tipo serán eliminados"
}
}
+13 -12
View File
@@ -5,19 +5,20 @@
"Cards": "Cartes",
"Content": "Contenu",
"CreateCard": "Créer une carte",
"CreateMasterTag": "Créer un tag maître",
"CreateTag": "Créer un tag",
"MasterTag": "Tag maître",
"MasterTags": "Tags maîtres",
"CreateMasterTag": "Créer un type",
"CreateTag": "Créer une étiquette",
"MasterTag": "Type",
"MasterTags": "Types",
"Parent": "Parent",
"Tags": "Tags",
"Tag": "Tag",
"Tags": "Étiquettes",
"Tag": "Étiquette",
"CardLibrary": "Bibliothèque de cartes",
"MinimizeAll": "Minimiser tout",
"ExpandAll": "Développer tout",
"DeleteTag": "Supprimer le tag",
"DeleteTagConfirm": "Voulez-vous vraiment supprimer ce tag? Toutes les propriétés associées seront supprimées",
"DeleteMasterTag": "Supprimer le tag maître",
"DeleteMasterTagConfirm": "Voulez-vous vraiment supprimer ce tag maître? Tous les objets liés à ce tag maître seront supprimés"
"MinimizeAll": "Tout réduire",
"ExpandAll": "Tout développer",
"DeleteTag": "Supprimer l'étiquette",
"TagRelations": "Relations d'étiquettes",
"DeleteTagConfirm": "Êtes-vous sûr de vouloir supprimer cette étiquette ? Toutes les propriétés associées seront supprimées",
"DeleteMasterTag": "Supprimer le type",
"DeleteMasterTagConfirm": "Êtes-vous sûr de vouloir supprimer ce type ? Tous les objets liés à ce type seront supprimés"
}
}
+6 -5
View File
@@ -5,10 +5,10 @@
"Cards": "Carte",
"Content": "Contenuto",
"CreateCard": "Crea Carta",
"CreateMasterTag": "Crea Tag Principale",
"CreateMasterTag": "Crea Tipo",
"CreateTag": "Crea Tag",
"MasterTag": "Tag Principale",
"MasterTags": "Tag Principali",
"MasterTag": "Tipo",
"MasterTags": "Tipi",
"Parent": "Genitore",
"Tags": "Tag",
"Tag": "Tag",
@@ -16,8 +16,9 @@
"MinimizeAll": "Minimizza Tutto",
"ExpandAll": "Espandi Tutto",
"DeleteTag": "Elimina Tag",
"TagRelations": "Relazioni Tag",
"DeleteTagConfirm": "Sei sicuro di voler eliminare questo tag? Tutte le proprietà correlate verranno eliminate",
"DeleteMasterTag": "Elimina Tag Principale",
"DeleteMasterTagConfirm": "Sei sicuro di voler eliminare questo tag principale? Tutti gli oggetti correlati a questo tag principale verranno eliminati"
"DeleteMasterTag": "Elimina Tipo",
"DeleteMasterTagConfirm": "Sei sicuro di voler eliminare questo tipo? Tutti gli oggetti correlati a questo tipo verranno eliminati"
}
}
+8 -7
View File
@@ -5,19 +5,20 @@
"Cards": "Cartões",
"Content": "Conteúdo",
"CreateCard": "Criar Cartão",
"CreateMasterTag": "Criar Tag Mestre",
"CreateMasterTag": "Criar Tipo",
"CreateTag": "Criar Tag",
"MasterTag": "Tag Mestre",
"MasterTags": "Tags Mestre",
"Parent": "Pai",
"MasterTag": "Tipo",
"MasterTags": "Tipos",
"Parent": "Principal",
"Tags": "Tags",
"Tag": "Tag",
"CardLibrary": "Biblioteca de Cartões",
"MinimizeAll": "Minimizar Tudo",
"ExpandAll": "Expandir Tudo",
"DeleteTag": "Excluir Tag",
"DeleteTagConfirm": "Tem certeza de que deseja excluir esta tag? Todas as propriedades relacionadas serão excluídas",
"DeleteMasterTag": "Excluir Tag Mestre",
"DeleteMasterTagConfirm": "Tem certeza de que deseja excluir esta tag mestre? Todos os objetos relacionados a esta tag mestre serão excluídos"
"TagRelations": "Relações de Tag",
"DeleteTagConfirm": "Tem certeza que deseja excluir esta tag? Todas as propriedades relacionadas serão excluídas",
"DeleteMasterTag": "Excluir Tipo",
"DeleteMasterTagConfirm": "Tem certeza que deseja excluir este tipo? Todos os objetos relacionados a este tipo serão excluídos"
}
}
+6 -5
View File
@@ -5,10 +5,10 @@
"Cards": "Карты",
"Content": "Содержание",
"CreateCard": "Создать карту",
"CreateMasterTag": "Создать мастер-тег",
"CreateMasterTag": "Создать тип",
"CreateTag": "Создать тег",
"MasterTag": "Мастер-тег",
"MasterTags": "Мастер-теги",
"MasterTag": "Тип",
"MasterTags": "Типы",
"Parent": "Родитель",
"Tags": "Теги",
"Tag": "Тег",
@@ -16,8 +16,9 @@
"MinimizeAll": "Свернуть все",
"ExpandAll": "Развернуть все",
"DeleteTag": "Удалить тег",
"TagRelations": "Связи тегов",
"DeleteTagConfirm": "Вы действительно хотите удалить этот тег? Все связанные свойства будут удалены",
"DeleteMasterTag": "Удалить мастер-тег",
"DeleteMasterTagConfirm": "Вы действительно хотите удалить этот мастер-тег? Все объекты, связанные с этим мастер-тегом, будут удалены"
"DeleteMasterTag": "Удалить тип",
"DeleteMasterTagConfirm": "Вы действительно хотите удалить этот тип? Все объекты, связанные с этим типом, будут удалены"
}
}
+8 -7
View File
@@ -1,14 +1,14 @@
{
"string": {
"Card": "卡片",
"CardApplication": "卡片应用",
"CardApplication": "卡片",
"Cards": "卡片",
"Content": "内容",
"CreateCard": "创建卡片",
"CreateMasterTag": "创建主标签",
"CreateMasterTag": "创建类型",
"CreateTag": "创建标签",
"MasterTag": "主标签",
"MasterTags": "主标签",
"MasterTag": "类型",
"MasterTags": "类型",
"Parent": "父级",
"Tags": "标签",
"Tag": "标签",
@@ -16,8 +16,9 @@
"MinimizeAll": "全部最小化",
"ExpandAll": "全部展开",
"DeleteTag": "删除标签",
"DeleteTagConfirm": "您确定要删除此标签吗?所有相关属性将被删除",
"DeleteMasterTag": "删除标签",
"DeleteMasterTagConfirm": "您确定要删除此主标签吗?所有与此主标签相关的对象将被删除"
"TagRelations": "标签关系",
"DeleteTagConfirm": "您确定要删除标签吗?所有相关属性都将被删除",
"DeleteMasterTag": "删除类型",
"DeleteMasterTagConfirm": "您确定要删除此类型吗?所有与此类型相关的对象都将被删除"
}
}
@@ -67,3 +67,24 @@
</span>
{/if}
{/if}
<style lang="scss">
.issuePresenterRoot {
display: flex;
align-items: center;
flex-shrink: 0;
&:not(.list) {
color: var(--theme-content-color);
}
&.list {
color: var(--theme-halfcontent-color);
}
.icon {
margin-right: 0.5rem;
color: var(--theme-dark-color);
}
}
</style>
@@ -17,6 +17,7 @@
<script lang="ts">
import { Analytics } from '@hcengineering/analytics'
import attachment, { Attachment } from '@hcengineering/attachment'
import { Card, CardEvents } from '@hcengineering/card'
import { Doc, Mixin, Ref, WithLookup, generateId, type Blob } from '@hcengineering/core'
import notification from '@hcengineering/notification'
import { Panel } from '@hcengineering/panel'
@@ -24,10 +25,9 @@
import { createQuery, getClient } from '@hcengineering/presentation'
import { Heading } from '@hcengineering/text-editor'
import { TableOfContents } from '@hcengineering/text-editor-resources'
import { Button, EditBox, FocusHandler, IconMixin, IconMoreH, createFocusManager, navigate } from '@hcengineering/ui'
import { Button, EditBox, FocusHandler, IconMoreH, createFocusManager, navigate } from '@hcengineering/ui'
import view from '@hcengineering/view'
import {
DocAttributeBar,
ParentsNavigator,
RelationsEditor,
getDocMixins,
@@ -35,12 +35,11 @@
showMenu
} from '@hcengineering/view-resources'
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
import { Card, CardEvents } from '@hcengineering/card'
import card from '../plugin'
import CardAttributeEditor from './CardAttributeEditor.svelte'
import CardPresenter from './CardPresenter.svelte'
import ContentEditor from './ContentEditor.svelte'
import TagsEditor from './TagsEditor.svelte'
import CardAttributeEditor from './CardAttributeEditor.svelte'
export let _id: Ref<Card>
export let readonly: boolean = false
@@ -74,10 +74,9 @@
ev.stopPropagation()
const loc = getCurrentResolvedLocation()
loc.path[2] = settingId
loc.path[3] = 'setting'
loc.path[4] = 'masterTags'
loc.path[3] = 'masterTags'
loc.path[4] = value._class
loc.path.length = 5
loc.query = { _class: value._class }
loc.fragment = undefined
navigate(loc)
}}
@@ -14,8 +14,9 @@
-->
<script lang="ts">
import { Card, MasterTag } from '@hcengineering/card'
import { fillDefaults } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import { DropdownLabelsIntl, Icon, Label } from '@hcengineering/ui'
import { DropdownLabelsIntl, Label } from '@hcengineering/ui'
import card from '../plugin'
export let value: Card
@@ -38,17 +39,18 @@
})
async function select (event: CustomEvent): Promise<void> {
const update = { _class: event.detail } as any
const _class = event.detail
const update = { _class } as any
selected = event.detail
await client.update(value, update)
const updated = fillDefaults(hierarchy, hierarchy.clone(value), _class)
await client.diffUpdate(value, updated)
}
$: label = hierarchy.getClass(selected).label
</script>
<div class="flex flex-gap-2 items-center item caption-color">
<Icon icon={card.icon.MasterTag} size="large" />
<Label label={card.string.MasterTag} />
{#if value._class === card.class.Card}
<DropdownLabelsIntl
{items}
@@ -14,38 +14,27 @@
-->
<script lang="ts">
import { Analytics } from '@hcengineering/analytics'
import { CardEvents, MasterTag } from '@hcengineering/card'
import core, { Class, Doc, MarkupBlobRef, Ref, SortingOrder } from '@hcengineering/core'
import { Card, CardEvents, cardId, MasterTag } from '@hcengineering/card'
import core, { Class, Data, Doc, fillDefaults, MarkupBlobRef, Ref, SortingOrder } from '@hcengineering/core'
import { translate } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
import { makeRank } from '@hcengineering/rank'
import {
Button,
ButtonWithDropdown,
IconAdd,
IconDropdown,
NavItem,
Scroller,
SelectPopupValueType,
deviceOptionsStore as deviceInfo,
getCurrentLocation,
IconAdd,
navigate,
showPopup
Scroller
} from '@hcengineering/ui'
import { NavLink, showMenu } from '@hcengineering/view-resources'
import { NavFooter, NavHeader } from '@hcengineering/workbench-resources'
import { createEventDispatcher } from 'svelte'
import { NavFooter, NavHeader, SavedView } from '@hcengineering/workbench-resources'
import card from '../plugin'
import CreateTag from './CreateTag.svelte'
import TagHierarchy from './TagHierarchy.svelte'
export let _class: Ref<Class<Doc>>
const client = getClient()
const clazz = client.getHierarchy().getClass(card.class.Card)
const dispatch = createEventDispatcher()
const hierarchy = client.getHierarchy()
let classes: MasterTag[] = []
let allClasses: MasterTag[] = []
@@ -64,11 +53,15 @@
const lastOne = await client.findOne(card.class.Card, {}, { sort: { rank: SortingOrder.Descending } })
const title = await translate(card.string.Card, {})
const _id = await client.createDoc(card.class.Card, core.space.Workspace, {
const data: Data<Card> = {
title,
rank: makeRank(lastOne?.rank, undefined),
content: '' as MarkupBlobRef
})
}
const filledData = fillDefaults(hierarchy, data, _class)
const _id = await client.createDoc(_class, core.space.Workspace, filledData)
Analytics.handleEvent(CardEvents.CardCreated)
@@ -78,25 +71,7 @@
navigate(loc)
}
function createMasteTag (): void {
showPopup(CreateTag, {
parent: undefined,
_class: card.class.MasterTag
})
}
async function dropdownItemSelected (res?: SelectPopupValueType['id']): Promise<void> {
if (res === card.string.CreateCard) {
await createCard()
} else if (res === card.string.CreateMasterTag) {
createMasteTag()
}
}
const dropdownItems = [
{ id: card.string.CreateCard, label: card.string.CreateCard },
{ id: card.string.CreateMasterTag, label: card.string.CreateMasterTag }
]
let menuSelection: boolean = false
</script>
<div
@@ -107,56 +82,28 @@
<NavHeader label={card.string.Cards} />
<div class="antiNav-subheader">
{#if allClasses.length > 0}
<ButtonWithDropdown
icon={IconAdd}
justify={'left'}
kind={'primary'}
label={card.string.CreateCard}
on:click={createCard}
mainButtonId={'new-card'}
dropdownIcon={IconDropdown}
{dropdownItems}
on:dropdown-selected={(ev) => {
void dropdownItemSelected(ev.detail)
}}
/>
{:else}
<Button
icon={IconAdd}
label={card.string.CreateMasterTag}
justify={'left'}
width={'100%'}
kind={'primary'}
gap={'large'}
on:click={createMasteTag}
/>
{/if}
<Button
icon={IconAdd}
label={card.string.CreateCard}
disabled={allClasses.length === 0 || _class === undefined}
justify={'left'}
width={'100%'}
kind={'primary'}
gap={'large'}
on:click={createCard}
/>
</div>
<NavLink space={card.class.Card}>
<NavItem
_id={card.class.Card}
label={card.string.CardLibrary}
icon={card.icon.Card}
isFold
empty
selected={card.class.Card === _class}
on:click={() => {
dispatch('select', card.class.Card)
}}
on:contextmenu={(evt) => {
showMenu(evt, { object: clazz })
}}
/>
</NavLink>
<SavedView alias={cardId} on:select={(res) => (menuSelection = res.detail)} />
<div class="antiNav-divider line" />
<Scroller shrink>
<TagHierarchy bind:_class {classes} {allClasses} on:select />
<TagHierarchy bind:_class deselect={menuSelection} {classes} {allClasses} />
</Scroller>
<div class="mt-2" />
<NavFooter split />
</div>
</div>
@@ -81,10 +81,9 @@
ev.stopPropagation()
const loc = getCurrentResolvedLocation()
loc.path[2] = settingId
loc.path[3] = 'setting'
loc.path[4] = 'masterTags'
loc.path[3] = 'masterTags'
loc.path[4] = tag._id
loc.path.length = 5
loc.query = { _class: tag._id }
loc.fragment = undefined
navigate(loc)
}}
@@ -24,6 +24,7 @@
export let classes: MasterTag[] = []
export let allClasses: MasterTag[] = []
export let _class: Ref<Class<Doc>> | undefined
export let deselect: boolean = false
export let level: number = 0
const client = getClient()
@@ -58,10 +59,11 @@
<NavItem
_id={clazz._id}
label={clazz.label}
icon={clazz.icon}
isFold
empty
{level}
selected={clazz._id === _class}
selected={!deselect && clazz._id === _class}
on:click={() => {
dispatch('select', clazz._id)
}}
@@ -30,6 +30,7 @@
ScrollerBar
} from '@hcengineering/ui'
import MasterTagSelector from './MasterTagSelector.svelte'
import { fillDefaults } from '@hcengineering/core'
export let doc: Card
@@ -65,6 +66,8 @@
async (res) => {
if (res !== undefined) {
await client.createMixin(doc._id, doc._class, doc.space, res, {})
const updated = fillDefaults(hierarchy, hierarchy.clone(doc), res)
await client.diffUpdate(doc, updated)
}
}
)
@@ -75,20 +78,22 @@
<div class="container py-4 gap-2">
<MasterTagSelector value={doc} />
<div class="divider" />
<ScrollerBar gap={'none'} bind:scroller={divScroll}>
<div class="tags gap-2">
{#each activeTags as mixin}
<div class="tag no-word-wrap">
<Label label={mixin.label} />
<ButtonIcon icon={IconClose} size="extra-small" kind="tertiary" on:click={() => removeTag(mixin._id)} />
</div>
{/each}
{#if dropdownItems.length > 0}
<CircleButton icon={IconAdd} size="small" ghost on:click={add} />
{/if}
</div>
</ScrollerBar>
{#if activeTags.length > 0 || dropdownItems.length > 0}
<div class="divider" />
<ScrollerBar gap={'none'} bind:scroller={divScroll}>
<div class="tags gap-2">
{#each activeTags as mixin}
<div class="tag no-word-wrap">
<Label label={mixin.label} />
<ButtonIcon icon={IconClose} size="extra-small" kind="tertiary" on:click={() => removeTag(mixin._id)} />
</div>
{/each}
{#if dropdownItems.length > 0}
<CircleButton icon={IconAdd} size="small" ghost on:click={add} />
{/if}
</div>
</ScrollerBar>
{/if}
</div>
<style lang="scss">
@@ -0,0 +1,84 @@
<!--
// 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.
-->
<script lang="ts">
import { MasterTag } from '@hcengineering/card'
import { Class, Doc, Ref } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import { ClassHierarchy } from '@hcengineering/setting-resources'
import { ButtonIcon, getCurrentLocation, Icon, IconAdd, Label, navigate, showPopup } from '@hcengineering/ui'
import card from '../../plugin'
import CreateTag from '../CreateTag.svelte'
import TagsHierarchy from './TagsHierarchy.svelte'
export let masterTag: MasterTag
const client = getClient()
const hierarchy = client.getHierarchy()
let descendants: Ref<Class<Doc>>[] = []
$: getDescendants(masterTag._id)
const query = createQuery()
query.query(card.class.MasterTag, {}, () => {
getDescendants(masterTag._id)
})
function getDescendants (id: Ref<Class<Doc>>): void {
const desc = hierarchy.getDescendants(id)
const filtered: Ref<Class<Doc>>[] = []
for (const _id of desc) {
const _class = hierarchy.getClass(_id)
if (_class.extends === id && _class._class === card.class.MasterTag) {
filtered.push(_id)
continue
}
}
descendants = filtered
}
function open (_class: Ref<Class<Doc>>): void {
const loc = getCurrentLocation()
loc.path[4] = _class
navigate(loc)
}
</script>
<div class="hulyTableAttr-header font-medium-12">
<Icon icon={card.icon.MasterTag} size="small" />
<span><Label label={card.string.MasterTags} /></span>
<ButtonIcon
kind="primary"
icon={IconAdd}
size="small"
dataId={'btnAdd'}
on:click={() => {
showPopup(CreateTag, {
parent: masterTag,
_class: card.class.MasterTag
})
}}
/>
</div>
{#if descendants.length}
<div class="hulyTableAttr-content task">
<TagsHierarchy
classes={descendants}
_class={undefined}
on:select={(e) => {
open(e.detail)
}}
/>
</div>
{/if}
@@ -0,0 +1,84 @@
<!--
// 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.
-->
<script lang="ts">
import { MasterTag } from '@hcengineering/card'
import { getEmbeddedLabel, translateCB } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import { ButtonIcon, IconDelete, ModernEditbox, showPopup, themeStore } from '@hcengineering/ui'
import { IconPicker } from '@hcengineering/view-resources'
import card from '../../plugin'
import { deleteMasterTag } from '../../utils'
export let masterTag: MasterTag
let name: string = ''
$: translateCB(masterTag.label, {}, $themeStore.language, (p) => {
name = p
})
const client = getClient()
async function attributeUpdated<T extends keyof MasterTag> (field: T, value: MasterTag[T]): Promise<void> {
if (masterTag === undefined || masterTag[field] === value) {
return
}
await client.update(masterTag, { [field]: value })
}
async function handleDelete (): Promise<void> {
await deleteMasterTag(masterTag)
}
function setIcon (): void {
showPopup(IconPicker, { icon: masterTag.icon, showEmoji: false, showColor: false }, 'top', async (res) => {
if (res !== undefined) {
await attributeUpdated('icon', res.icon)
masterTag.icon = res.icon
}
})
}
</script>
<div class="hulyComponent-content__column-group">
<div class="hulyComponent-content__header items-center">
<div class="flex items-center">
<ButtonIcon
icon={masterTag.icon ?? card.icon.MasterTag}
size={'large'}
iconSize={'large'}
kind={'tertiary'}
on:click={setIcon}
/>
<ModernEditbox
kind="ghost"
size="large"
label={masterTag.label}
value={name}
on:blur={(evt) => {
if (evt.detail === undefined || evt.detail.trim().length === 0) {
return
}
if (name !== evt.detail) {
const emb = getEmbeddedLabel(evt.detail)
void attributeUpdated('label', emb)
}
}}
/>
</div>
<ButtonIcon icon={IconDelete} size="small" kind="secondary" on:click={handleDelete} />
</div>
</div>
@@ -0,0 +1,165 @@
<!--
// 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.
-->
<script lang="ts">
import { MasterTag } from '@hcengineering/card'
import { Ref } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import { clearSettingsStore } from '@hcengineering/setting-resources'
import {
Icon,
IconOpenedArrow,
Label,
Location,
getCurrentResolvedLocation,
navigate,
resolvedLocationStore
} from '@hcengineering/ui'
import { onDestroy } from 'svelte'
import card from '../../plugin'
export let categoryName: string
let selectedTagId: Ref<MasterTag> | undefined
onDestroy(resolvedLocationStore.subscribe(handleLocationChanged))
function handleLocationChanged (loc: Location): void {
selectedTagId = loc.path[4] as Ref<MasterTag>
}
let tags: MasterTag[] = []
const tagsQuery = createQuery()
$: tagsQuery.query(
card.class.MasterTag,
{
_class: card.class.MasterTag
},
(result) => {
tags = result
}
)
function selectProjectType (id: string): void {
clearSettingsStore()
const loc = getCurrentResolvedLocation()
loc.path[3] = categoryName
loc.path[4] = id
loc.path.length = 5
navigate(loc)
}
</script>
{#each tags as tag}
<button
class="hulyTaskNavLink-container font-regular-14"
class:selected={tag._id === selectedTagId}
on:click={() => {
selectProjectType(tag._id)
}}
>
<div class="hulyTaskNavLink-avatar">
{#if tag?.icon}
<div class="hulyTaskNavLink-icon">
<Icon icon={tag?.icon} size="small" fill="currentColor" />
</div>
{/if}
</div>
<div class="hulyTaskNavLink-content">
<span class="hulyTaskNavLink-content__title"><Label label={tag.label} /></span>
</div>
{#if tag._id === selectedTagId}
<div class="hulyTaskNavLink-icon right">
<IconOpenedArrow size={'small'} />
</div>
{/if}
</button>
{/each}
<style lang="scss">
.hulyTaskNavLink-container {
display: flex;
align-items: center;
flex-shrink: 0;
gap: 0.75rem;
padding: 0 0.75rem 0 0.5rem;
height: 3.5rem;
min-width: 0;
border: none;
border-radius: 0.375rem;
outline: none;
&.selected {
cursor: auto;
}
.hulyTaskNavLink-avatar {
display: flex;
justify-content: center;
align-items: center;
flex-shrink: 0;
width: 2.5rem;
height: 2.5rem;
min-width: 0;
background-color: var(--global-ui-BackgroundColor);
border-radius: 0.375rem;
}
.hulyTaskNavLink-icon {
flex-shrink: 0;
width: 1rem;
height: 1rem;
color: var(--global-secondary-TextColor);
&.right {
visibility: hidden;
}
}
.hulyTaskNavLink-content {
display: flex;
flex-direction: column;
gap: 0.125rem;
flex-grow: 1;
min-width: 0;
min-height: 0;
&__title {
white-space: nowrap;
word-break: break-all;
text-overflow: ellipsis;
overflow: hidden;
text-align: left;
min-width: 0;
color: var(--global-primary-TextColor);
}
}
&:hover {
background-color: var(--global-ui-hover-highlight-BackgroundColor);
}
&.selected {
background-color: var(--global-ui-highlight-BackgroundColor);
.hulyTaskNavLink-icon {
color: var(--global-accent-TextColor);
&.right {
visibility: visible;
}
}
.hulyTaskNavLink-content .hulyTaskNavLink-content__title {
font-weight: 700;
color: var(--global-accent-TextColor);
}
}
}
</style>
@@ -0,0 +1,93 @@
<!--
// 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.
-->
<script lang="ts">
import { MasterTag } from '@hcengineering/card'
import { Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import {
BreadcrumbItem,
Breadcrumbs,
Header,
Location,
deviceOptionsStore as deviceInfo,
getCurrentLocation,
navigate,
resolvedLocationStore
} from '@hcengineering/ui'
import { onDestroy, onMount } from 'svelte'
import card from '../../plugin'
import MasterTagEditor from './MasterTagEditor.svelte'
let masterTag: MasterTag | undefined
let selectedTagId: Ref<MasterTag> | undefined
onDestroy(resolvedLocationStore.subscribe(handleLocationChanged))
function handleLocationChanged ({ path }: Location): void {
if (path[3] !== 'masterTags' || path[4] === undefined) {
selectedTagId = undefined
} else {
selectedTagId = path[4] as Ref<MasterTag>
}
}
const client = getClient()
const hierarchy = client.getHierarchy()
$: masterTag = getMasterTag(selectedTagId)
function getMasterTag (_id: Ref<MasterTag> | undefined): MasterTag | undefined {
if (_id === undefined) return undefined
return client.getModel().findObject(_id)
}
function getBreadcrumbs (tag: Ref<MasterTag> | undefined): BreadcrumbItem[] {
if (tag === undefined) return []
const toAncestors = hierarchy.getAncestors(card.class.Card)
const ancestors = hierarchy.getAncestors(tag)
const filtered = ancestors.filter((it) => !toAncestors.includes(it))
return filtered.reverse().map((it) => ({
id: it,
label: hierarchy.getClass(it).label
}))
}
$: items = getBreadcrumbs(masterTag?._id)
onMount(() => {
setTimeout(() => {
if (masterTag === undefined) $deviceInfo.navigator.visible = true
}, 500)
})
function handleSelect (e: CustomEvent<any>): void {
const id = items[e.detail]?.id
if (id !== undefined) {
const loc = getCurrentLocation()
loc.path[4] = id
navigate(loc)
}
}
</script>
<div class="hulyComponent">
{#if masterTag !== undefined}
<Header adaptive={'disabled'}>
<Breadcrumbs {items} selected={items.length - 1} size={'large'} on:select={handleSelect} />
</Header>
{#key masterTag._id}
<MasterTagEditor {masterTag} on:change />
{/key}
{/if}
</div>
@@ -0,0 +1,28 @@
<!--
// 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.
-->
<script lang="ts">
import { ButtonIcon, IconAdd, showPopup } from '@hcengineering/ui'
import CreateTag from '../CreateTag.svelte'
import card from '../../plugin'
function handleAdd (): void {
showPopup(CreateTag, {
parent: undefined,
_class: card.class.MasterTag
})
}
</script>
<ButtonIcon id={'new-master-tag'} icon={IconAdd} kind={'tertiary'} size={'extra-small'} on:click={handleAdd} />
@@ -0,0 +1,83 @@
<!--
// 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.
-->
<script lang="ts">
import { MasterTag } from '@hcengineering/card'
import { getClient } from '@hcengineering/presentation'
import {
ButtonIcon,
Component,
IconDescription,
NavItem,
Scroller,
Separator,
defineSeparators,
secondNavSeparators
} from '@hcengineering/ui'
import card from '../../plugin'
export let masterTag: MasterTag
export let visibleSecondNav: boolean = true
export let readonly: boolean = true
const client = getClient()
$: isMasterTag = masterTag._class === card.class.MasterTag
$: sections = client
.getModel()
.findAllSync(card.class.MasterTagEditorSection, !isMasterTag ? { masterOnly: { $ne: true } } : {})
const sectionRefs: Record<string, HTMLElement | undefined> = {}
defineSeparators('spaceTypeEditor', secondNavSeparators)
</script>
<div class="hulyComponent-content__container columns">
{#if visibleSecondNav}
<div class="hulyComponent-content__column">
<div class="hulyComponent-content__navHeader">
<div class="hulyComponent-content__navHeader-menu">
<ButtonIcon kind="tertiary" icon={IconDescription} size="small" inheritColor />
</div>
</div>
{#each sections as navItem (navItem.id)}
<NavItem
type="type-anchor-link"
label={navItem.label}
on:click={() => {
sectionRefs[navItem.id]?.scrollIntoView()
}}
/>
{/each}
</div>
<Separator name="spaceTypeEditor" index={0} color="transparent" />
{/if}
<div class="hulyComponent-content__column content">
<Scroller align="center" padding="var(--spacing-3)" bottomPadding="var(--spacing-3)">
<div class="hulyComponent-content gap">
{#each sections as section, i}
<div bind:this={sectionRefs[section.id]} class:hulyTableAttr-container={i}>
<Component
is={section.component}
disabled={readonly}
props={{
masterTag
}}
/>
</div>
{/each}
</div>
</Scroller>
</div>
</div>
@@ -0,0 +1,23 @@
<!--
// Copyright © 2025s 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.
-->
<script lang="ts">
import { MasterTag } from '@hcengineering/card'
import { ClassAttributes } from '@hcengineering/setting-resources'
import card from '../../plugin'
export let masterTag: MasterTag
</script>
<ClassAttributes ofClass={card.class.Card} _class={masterTag._id} showHierarchy showHeader={false} disabled={false} />
@@ -0,0 +1,82 @@
<!--
// 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.
-->
<script lang="ts">
import card, { MasterTag } from '@hcengineering/card'
import contact from '@hcengineering/contact'
import core, { Association, Class, Doc, Ref } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
import setting from '@hcengineering/setting'
import { clearSettingsStore, settingsStore } from '@hcengineering/setting-resources'
import { ButtonIcon, Icon, IconAdd, Label, showPopup } from '@hcengineering/ui'
import { onDestroy } from 'svelte'
export let masterTag: MasterTag
let associations: Association[] = []
const client = getClient()
const hierarchy = client.getHierarchy()
$: descendants = new Set(hierarchy.getDescendants(masterTag._id))
$: filtered = associations.filter((it) => descendants.has(it.classA) || descendants.has(it.classB))
const query = createQuery()
query.query(core.class.Association, {}, (res) => {
associations = res
})
function getClassLabel (_class: Ref<Class<Doc>>): IntlString {
const _classLabel = hierarchy.getClass(_class)
return _classLabel.label
}
function addRelation (): void {
showPopup(setting.component.CreateRelation, {
exclude: [],
_classes: [card.class.Card, contact.class.Contact]
})
}
const handleSelect = (association: Association): void => {
$settingsStore = { id: association._id, component: setting.component.EditRelation, props: { association } }
}
onDestroy(() => {
clearSettingsStore()
})
</script>
<div class="hulyTableAttr-header font-medium-12">
<Icon icon={setting.icon.Relations} size="small" />
<span><Label label={core.string.Relations} /></span>
<ButtonIcon kind="primary" icon={IconAdd} size="small" dataId={'btnAdd'} on:click={addRelation} />
</div>
{#if filtered.length}
<div class="hulyTableAttr-content task">
{#each filtered as association}
<button
class="hulyTableAttr-content__row justify-start"
on:click|stopPropagation={() => {
handleSelect(association)
}}
>
<div class="hulyTableAttr-content__row-label font-medium-14 cursor-pointer">
{association.nameA} (<Label label={getClassLabel(association.classA)} />) - {association.nameB} (<Label
label={getClassLabel(association.classB)}
/>)
</div>
</button>
{/each}
</div>
{/if}
@@ -0,0 +1,78 @@
<!--
// 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.
-->
<script lang="ts">
import cardPlugin from '@hcengineering/card'
import core, { Class, ClassifierKind, Doc, Ref } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import { Icon, Label } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
export let classes: Ref<Class<Doc>>[] = []
export let _class: Ref<Class<Doc>> | undefined
export let kind: ClassifierKind = ClassifierKind.CLASS
export let level: number = 0
const client = getClient()
const dispatch = createEventDispatcher()
let descendants = new Map<Ref<Class<Doc>>, Ref<Class<Doc>>[]>()
function getDescendants (_class: Ref<Class<Doc>>): Ref<Class<Doc>>[] {
const hierarchy = client.getHierarchy()
const result: Ref<Class<Doc>>[] = []
const desc = hierarchy.getDescendants(_class)
for (const clazz of desc) {
const cls = hierarchy.getClass(clazz)
if (cls.extends === _class && !cls.hidden && kind === cls.kind && cls.label !== undefined) {
result.push(clazz)
}
}
return result
}
function fillDescendants (classes: Ref<Class<Doc>>[]): void {
for (const cl of classes) {
descendants.set(cl, getDescendants(cl))
}
descendants = descendants
}
const query = createQuery()
query.query(core.class.Class, {}, () => {
fillDescendants(classes)
})
$: fillDescendants(classes)
</script>
{#each classes as cl}
{@const clazz = client.getHierarchy().getClass(cl)}
<button
class="hulyTableAttr-content__row justify-start cursor-pointer"
on:click={() => {
dispatch('select', cl)
}}
>
<div
class="hulyTableAttr-content__row-label font-medium-14 flex flex-gap-2"
style:margin-left={`${level * 1.25}rem`}
>
<Icon icon={clazz.icon ?? cardPlugin.icon.Tag} size="small" />
<Label label={clazz.label} />
</div>
</button>
{#if (descendants.get(cl)?.length ?? 0) > 0}
<svelte:self classes={descendants.get(cl) ?? []} {_class} {kind} level={level + 1} on:select />
{/if}
{/each}
@@ -0,0 +1,85 @@
<!--
// 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.
-->
<script lang="ts">
import { MasterTag } from '@hcengineering/card'
import { Class, ClassifierKind, Doc, Ref } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import { ClassHierarchy } from '@hcengineering/setting-resources'
import { ButtonIcon, getCurrentLocation, Icon, IconAdd, Label, navigate, showPopup } from '@hcengineering/ui'
import card from '../../plugin'
import CreateTag from '../CreateTag.svelte'
import TagsHierarchy from './TagsHierarchy.svelte'
export let masterTag: MasterTag
const client = getClient()
const hierarchy = client.getHierarchy()
let descendants: Ref<Class<Doc>>[] = []
$: getDescendants(masterTag._id)
const query = createQuery()
query.query(card.class.Tag, {}, () => {
getDescendants(masterTag._id)
})
function getDescendants (id: Ref<Class<Doc>>): void {
const desc = hierarchy.getDescendants(id)
const filtered: Ref<Class<Doc>>[] = []
for (const _id of desc) {
const _class = hierarchy.getClass(_id)
if (_class.extends === id && _class._class === card.class.Tag) {
filtered.push(_id)
continue
}
}
descendants = filtered
}
function open (_class: Ref<Class<Doc>>): void {
const loc = getCurrentLocation()
loc.path[4] = _class
navigate(loc)
}
</script>
<div class="hulyTableAttr-header font-medium-12">
<Icon icon={card.icon.Tag} size="small" />
<span><Label label={card.string.Tags} /></span>
<ButtonIcon
kind="primary"
icon={IconAdd}
size="small"
dataId={'btnAdd'}
on:click={() => {
showPopup(CreateTag, {
parent: masterTag,
_class: card.class.Tag
})
}}
/>
</div>
{#if descendants.length}
<div class="hulyTableAttr-content task">
<TagsHierarchy
kind={ClassifierKind.MIXIN}
classes={descendants}
_class={undefined}
on:select={(e) => {
open(e.detail)
}}
/>
</div>
{/if}
+17 -1
View File
@@ -26,6 +26,14 @@ import {
queryCard,
deleteMasterTag
} from './utils'
import ManageMasterTagsContent from './components/settings/ManageMasterTagsContent.svelte'
import ManageMasterTagsTools from './components/settings/ManageMasterTagsTools.svelte'
import ManageMasterTags from './components/settings/ManageMasterTags.svelte'
import GeneralSection from './components/settings/GeneralSection.svelte'
import ProperitiesSection from './components/settings/ProperitiesSection.svelte'
import TagsSection from './components/settings/TagsSection.svelte'
import RelationsSection from './components/settings/RelationsSection.svelte'
import ChildsSection from './components/settings/ChildsSection.svelte'
export default async (): Promise<Resources> => ({
component: {
@@ -33,7 +41,15 @@ export default async (): Promise<Resources> => ({
CreateTag,
CardPresenter,
EditCard,
Main
Main,
ManageMasterTags,
ManageMasterTagsContent,
ManageMasterTagsTools,
GeneralSection,
ProperitiesSection,
TagsSection,
RelationsSection,
ChildsSection
},
completion: {
CardQuery: queryCard
+11 -2
View File
@@ -21,11 +21,19 @@ import { type Viewlet } from '@hcengineering/view'
export default mergeIds(cardId, card, {
component: {
ManageMasterTags: '' as AnyComponent,
ManageMasterTagsContent: '' as AnyComponent,
ManageMasterTagsTools: '' as AnyComponent,
MasterTags: '' as AnyComponent,
CreateTag: '' as AnyComponent,
CardPresenter: '' as AnyComponent,
EditCard: '' as AnyComponent,
Main: '' as AnyComponent
Main: '' as AnyComponent,
GeneralSection: '' as AnyComponent,
ProperitiesSection: '' as AnyComponent,
TagsSection: '' as AnyComponent,
ChildsSection: '' as AnyComponent,
RelationsSection: '' as AnyComponent
},
viewlet: {
CardTable: '' as Ref<Viewlet>
@@ -44,6 +52,7 @@ export default mergeIds(cardId, card, {
DeleteTag: '' as IntlString,
DeleteTagConfirm: '' as IntlString,
DeleteMasterTag: '' as IntlString,
DeleteMasterTagConfirm: '' as IntlString
DeleteMasterTagConfirm: '' as IntlString,
TagRelations: '' as IntlString
}
})
+11 -3
View File
@@ -57,8 +57,8 @@ export async function deleteMasterTag (tag: MasterTag | undefined): Promise<void
const desc = hierarchy.getClass(obj)
await ops.remove(desc)
}
await ops.remove(tag)
await ops.commit()
await client.remove(tag)
}
})
} else {
@@ -81,12 +81,20 @@ export async function deleteMasterTag (tag: MasterTag | undefined): Promise<void
for (const obj of cards) {
await ops.update(obj, { $unset: update })
}
await ops.remove(tag)
await ops.commit()
await client.remove(tag)
}
})
}
} else {
const ops = client.apply(undefined, 'delete-tag')
const hierarchy = client.getHierarchy()
const desc = hierarchy.getDescendants(tag._id)
for (const obj of desc) {
const desc = hierarchy.getClass(obj)
await ops.remove(desc)
}
await ops.commit()
await client.remove(tag)
}
}
@@ -112,7 +120,7 @@ async function generateLocation (loc: Location, id: string): Promise<ResolvedLoc
}
const appComponent = loc.path[0] ?? ''
const workspace = loc.path[1] ?? ''
const special = card.class.Card
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
+2 -1
View File
@@ -38,7 +38,8 @@
},
"dependencies": {
"@hcengineering/platform": "^0.6.11",
"@hcengineering/core": "^0.6.32"
"@hcengineering/core": "^0.6.32",
"@hcengineering/ui": "^0.6.15"
},
"repository": "https://github.com/hcengineering/platform",
"publishConfig": {
+10 -1
View File
@@ -13,6 +13,7 @@
import { Class, Mixin, Doc, Ref, MarkupBlobRef, Rank } from '@hcengineering/core'
import { Asset, IntlString, plugin, Plugin } from '@hcengineering/platform'
import type { AnyComponent } from '@hcengineering/ui'
export * from './analytics'
@@ -29,6 +30,13 @@ export interface Card extends Doc {
rank: Rank
}
export interface MasterTagEditorSection extends Doc {
id: string
label: IntlString
component: AnyComponent
masterOnly?: boolean
}
/**
* @public
*/
@@ -41,7 +49,8 @@ const cardPlugin = plugin(cardId, {
class: {
Card: '' as Ref<Class<Card>>,
MasterTag: '' as Ref<Class<MasterTag>>,
Tag: '' as Ref<Class<Tag>>
Tag: '' as Ref<Class<Tag>>,
MasterTagEditorSection: '' as Ref<Class<MasterTagEditorSection>>
},
icon: {
MasterTags: '' as Asset,
+1
View File
@@ -50,6 +50,7 @@
"@hcengineering/panel": "^0.6.23",
"@hcengineering/view": "^0.6.13",
"@hcengineering/view-resources": "^0.6.0",
"@hcengineering/card": "^0.6.0",
"@hcengineering/task": "^0.6.20",
"@hcengineering/contact-resources": "^0.6.0",
"@hcengineering/login": "^0.6.12",
@@ -23,20 +23,24 @@
DropdownIntlItem,
DropdownLabelsIntl,
EditBox,
Label
Label,
NestedDropdown
} from '@hcengineering/ui'
import view from '@hcengineering/view'
import card from '@hcengineering/card'
import { createEventDispatcher } from 'svelte'
import setting from '../plugin'
export let association: Association | Data<Association>
export let kind: ButtonKind = 'regular'
export let size: ButtonSize = 'medium'
export let _classes: Ref<Class<Doc>>[] = [core.class.Doc]
export let exclude: Ref<Class<Doc>>[] = [card.class.Card]
const client = getClient()
const hierarchy = client.getHierarchy()
const descendants = hierarchy.getDescendants(core.class.Doc)
const descendants = new Set(_classes.map((p) => hierarchy.getDescendants(p)).reduce((a, b) => a.concat(b)))
const viewlets = new Set(
client
.getModel()
@@ -44,43 +48,69 @@
.map((p) => p.attachTo)
)
function filterClasses (descendants: Ref<Class<Doc>>[], viewlets: Set<Ref<Class<Doc>>>): DropdownIntlItem[] {
const result = []
function filterClasses (
descendants: Set<Ref<Class<Doc>>>,
viewlets: Set<Ref<Class<Doc>>>,
exclude: Ref<Class<Doc>>[]
): [DropdownIntlItem, DropdownIntlItem[]][] {
const ignore = new Set<Ref<Class<Doc>>>()
const added = new Set<Ref<Class<Doc>>>()
for (const _id of descendants) {
if (added.has(_id)) continue
const _class = hierarchy.getClass(_id)
if (_class.extends !== undefined && added.has(_class.extends)) {
added.add(_id)
result.push({ id: _id, label: _class.label })
const base = new Map<Ref<Class<Doc>>, Class<Doc>[]>()
for (const excluded of exclude) {
const desc = hierarchy.getDescendants(excluded)
for (const _id of desc) {
ignore.add(_id)
}
}
for (const _id of descendants) {
if (added.has(_id) || ignore.has(_id)) continue
const _class = hierarchy.getClass(_id)
if (_class.label === undefined) continue
if (viewlets.has(hierarchy.getBaseClass(_id))) {
added.add(_id)
result.push({ id: _id, label: _class.label })
const descendants = hierarchy.getDescendants(_id)
const toAdd: Class<Doc>[] = []
for (const desc of descendants) {
if (added.has(desc) || ignore.has(desc)) continue
const _class = hierarchy.getClass(desc)
if (_class.label === undefined) continue
added.add(desc)
toAdd.push(_class)
}
base.set(_id, toAdd)
}
}
const result: [DropdownIntlItem, DropdownIntlItem[]][] = []
for (const [key, value] of base) {
try {
const clazz = hierarchy.getClass(key)
result.push([{ id: key, label: clazz.label }, value.map((it) => ({ id: it._id, label: it.label }))])
} catch {}
}
return result
}
const classes = filterClasses(descendants, viewlets)
const classes = filterClasses(descendants, viewlets, exclude)
let classA = classes.find((p) => p.id === association.classA) ?? classes[0]
let classB = classes.find((p) => p.id === association.classB) ?? classes[0]
let classARef = classA?.id
let classBRef = classB?.id
let classARef: Ref<Class<Doc>> | undefined = undefined
let classBRef: Ref<Class<Doc>> | undefined = undefined
let nameA = association.nameA
let nameB = association.nameB
$: classA = isEmptyClass(classARef) ? undefined : hierarchy.getClass(classARef as Ref<Class<Doc>>)
$: classB = isEmptyClass(classBRef) ? undefined : hierarchy.getClass(classBRef as Ref<Class<Doc>>)
function isEmptyClass (ref: Ref<Class<Doc>> | undefined): boolean {
return ref === undefined || ref !== ''
}
$: editable = !isAssociation(association)
$: fill(association)
function fill (association: Association | Data<Association>): void {
classA = classes.find((p) => p.id === association.classA) ?? classes[0]
classB = classes.find((p) => p.id === association.classB) ?? classes[0]
classBRef = classB?.id
classARef = classA?.id
classBRef = !isEmptyClass(association.classB) ? association.classB : undefined
classARef = !isEmptyClass(association.classA) ? association.classA : undefined
nameA = association.nameA
nameB = association.nameB
}
@@ -102,8 +132,8 @@
})
} else {
await client.createDoc(core.class.Association, core.space.Model, {
classA: classARef as Ref<Class<Doc>>,
classB: classBRef as Ref<Class<Doc>>,
classA: classARef,
classB: classBRef,
type: mode,
nameA,
nameB
@@ -143,13 +173,11 @@
</div>
<div>
{#if editable}
<DropdownLabelsIntl
label={core.string.Class}
<NestedDropdown
items={classes}
width="8rem"
bind:selected={classARef}
{kind}
{size}
on:selected={(e) => {
classARef = e.detail
}}
/>
{:else if classA}
<Label label={classA.label} />
@@ -184,13 +212,11 @@
</div>
<div>
{#if editable}
<DropdownLabelsIntl
label={core.string.Class}
<NestedDropdown
items={classes}
width="8rem"
bind:selected={classBRef}
{kind}
{size}
on:selected={(e) => {
classBRef = e.detail
}}
/>
{:else if classB}
<Label label={classB.label} />
@@ -53,11 +53,6 @@
<button class="hulyTableAttr-content__row-dragMenu" on:click|stopPropagation={clickMore}>
<IconMoreV2 size={'small'} />
</button>
{#if attribute.isCustom}
<div class="hulyChip-item font-medium-12">
<Label label={settings.string.Custom} />
</div>
{/if}
{#if attribute.icon !== undefined}
<div class="hulyTableAttr-content__row-icon">
<Icon icon={attribute.icon} size={'small'} />
@@ -13,7 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import core, { AnyAttribute, Class, ClassifierKind, Doc, Ref, Space } from '@hcengineering/core'
import core, { AnyAttribute, Class, Doc, Ref, Space } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
import {
@@ -22,28 +22,29 @@
ButtonIcon,
IconAdd,
IconEdit,
Label,
getEventPositionElement,
showPopup,
IconSettings,
Label,
ModernButton,
deviceWidths,
getEventPositionElement,
resizeObserver,
deviceWidths
showPopup
} from '@hcengineering/ui'
import { ObjectPresenter } from '@hcengineering/view-resources'
import { onDestroy } from 'svelte'
import settings from '../plugin'
import CreateAttribute from './CreateAttribute.svelte'
import { clearSettingsStore, settingsStore } from '../store'
import ClassAttributesList from './ClassAttributesList.svelte'
import CreateAttribute from './CreateAttribute.svelte'
import EditAttribute from './EditAttribute.svelte'
import EditClassLabel from './EditClassLabel.svelte'
import { settingsStore, clearSettingsStore } from '../store'
import TypesPopup from './typeEditors/TypesPopup.svelte'
import { onDestroy } from 'svelte'
export let _class: Ref<Class<Doc>>
export let ofClass: Ref<Class<Doc>> | undefined = undefined
export let showHierarchy: boolean = false
export let showTitle: boolean = !showHierarchy
export let showHeader: boolean = true
export let disabled: boolean = true
export let attributeMapper:
| {
@@ -101,16 +102,18 @@
showPopup(EditClassLabel, { clazz }, getEventPositionElement(evt))
}
const classUpdated = (_clazz: Ref<Class<Doc>>): void => {
const classUpdated = (_clazz: Ref<Class<Doc>>, to: Ref<Class<Doc>>): void => {
selected = undefined
classes = client
.getHierarchy()
const h = client.getHierarchy()
const toAncestors = new Set(h.getAncestors(to))
classes = h
.getAncestors(_class)
.map((it) => client.getHierarchy().getClass(it))
.filter((it) => {
return (
!it.hidden &&
it.label !== undefined &&
!toAncestors.has(it._id) &&
it._id !== core.class.Doc &&
it._id !== core.class.AttachedDoc &&
it._id !== _class
@@ -118,7 +121,7 @@
})
clazzHierarchy = client.getHierarchy().getClass(_class)
}
$: classUpdated(_class)
$: classUpdated(_class, ofClass ?? core.class.Doc)
settingsStore.subscribe((value) => {
if ((value.id === undefined && selected !== undefined) || (value.id !== undefined && value.id !== selected?._id)) {
@@ -163,14 +166,18 @@
}}
>
<div class="hulyTableAttr-header font-medium-12" class:withButton={showHierarchy}>
{#if showHierarchy}
<ModernButton icon={IconSettings} kind={'secondary'} size={'small'} {disabled} hasMenu>
<Label label={settings.string.ClassColon} />
<ObjectPresenter _class={clazzHierarchy._class} objectId={clazzHierarchy._id} value={clazzHierarchy} />
</ModernButton>
{#if showHeader}
{#if showHierarchy}
<ModernButton icon={IconSettings} kind={'secondary'} size={'small'} {disabled} hasMenu>
<Label label={settings.string.ClassColon} />
<ObjectPresenter _class={clazzHierarchy._class} objectId={clazzHierarchy._id} value={clazzHierarchy} />
</ModernButton>
{:else}
<IconSettings size={'small'} />
<span><Label label={settings.string.ClassProperties} /></span>
{/if}
{:else}
<IconSettings size={'small'} />
<span><Label label={settings.string.ClassProperties} /></span>
<div></div>
{/if}
<ButtonIcon
kind={'primary'}
@@ -138,7 +138,6 @@
/>
<ModernEditbox bind:value={name} label={core.string.Name} size={'large'} kind={'ghost'} autoFocus />
</div>
<ModernEditbox bind:value={name} label={core.string.Name} size={'large'} kind={'ghost'} autoFocus />
</div>
<div class="hulyModal-content__settingsSet">
<div class="hulyModal-content__settingsSet-line">
@@ -0,0 +1,175 @@
<!--
// 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.
-->
<script lang="ts">
import cardPlugin from '@hcengineering/card'
import core, { Class, Doc, Ref } from '@hcengineering/core'
import { getEmbeddedLabel } from '@hcengineering/platform'
import presentation, { Card, getClient } from '@hcengineering/presentation'
import { DropdownIntlItem, DropdownLabelsIntl, EditBox, Label } from '@hcengineering/ui'
import NestedDropdown from '@hcengineering/ui/src/components/NestedDropdown.svelte'
import view from '@hcengineering/view'
import { createEventDispatcher } from 'svelte'
import setting from '../plugin'
export let _classes: Ref<Class<Doc>>[] = [core.class.Doc]
export let exclude: Ref<Class<Doc>>[] = [cardPlugin.class.Card]
const dispatch = createEventDispatcher()
const client = getClient()
const hierarchy = client.getHierarchy()
const descendants = new Set(_classes.map((p) => hierarchy.getDescendants(p)).reduce((a, b) => a.concat(b)))
const viewlets = new Set(
client
.getModel()
.findAllSync(view.class.Viewlet, { descriptor: view.viewlet.Table })
.map((p) => p.attachTo)
)
function filterClasses (
descendants: Set<Ref<Class<Doc>>>,
viewlets: Set<Ref<Class<Doc>>>,
exclude: Ref<Class<Doc>>[]
): [DropdownIntlItem, DropdownIntlItem[]][] {
const ignore = new Set<Ref<Class<Doc>>>()
const added = new Set<Ref<Class<Doc>>>()
const base = new Map<Ref<Class<Doc>>, Class<Doc>[]>()
for (const excluded of exclude) {
const desc = hierarchy.getDescendants(excluded)
for (const _id of desc) {
ignore.add(_id)
}
}
for (const _id of descendants) {
if (added.has(_id) || ignore.has(_id)) continue
const _class = hierarchy.getClass(_id)
if (_class.label === undefined) continue
if (viewlets.has(hierarchy.getBaseClass(_id))) {
added.add(_id)
const descendants = hierarchy.getDescendants(_id)
const toAdd: Class<Doc>[] = []
for (const desc of descendants) {
if (added.has(desc) || ignore.has(desc)) continue
const _class = hierarchy.getClass(desc)
if (_class.label === undefined) continue
added.add(desc)
toAdd.push(_class)
}
base.set(_id, toAdd)
}
}
const result: [DropdownIntlItem, DropdownIntlItem[]][] = []
for (const [key, value] of base) {
try {
const clazz = hierarchy.getClass(key)
result.push([{ id: key, label: clazz.label }, value.map((it) => ({ id: it._id, label: it.label }))])
} catch {}
}
return result
}
const classes = filterClasses(descendants, viewlets, exclude)
let classARef: Ref<Class<Doc>> | undefined = undefined
let classBRef: Ref<Class<Doc>> | undefined = undefined
let nameA: string = ''
let nameB: string = ''
async function save (): Promise<void> {
if (classBRef === undefined || classARef === undefined) {
return
}
await client.createDoc(core.class.Association, core.space.Model, {
classA: classARef,
classB: classBRef,
type: mode,
nameA,
nameB
})
dispatch('close')
}
const items: DropdownIntlItem[] = [
{
id: '1:1',
label: getEmbeddedLabel('1:1')
},
{
id: '1:N',
label: getEmbeddedLabel('1:N')
},
{
id: 'N:N',
label: getEmbeddedLabel('N:N')
}
]
let mode: '1:1' | '1:N' | 'N:N' = 'N:N' as '1:1' | '1:N' | 'N:N'
</script>
<Card
label={core.string.AddRelation}
okLabel={presentation.string.Create}
canSave={nameB.trim() !== '' && nameA.trim() !== '' && classARef !== undefined && classBRef !== undefined}
okAction={save}
on:close
>
<div class="flex-between flex-gap-4">
<div class="flex-col p-4 flex-gap-2">
<div class="flex-col-center">A</div>
<div>
<EditBox bind:value={nameA} placeholder={core.string.Name} kind={'default'} />
</div>
<div>
<NestedDropdown
items={classes}
on:selected={(e) => {
classARef = e.detail
}}
/>
</div>
</div>
<div class="flex-col p-4 flex-gap-2">
<span class="label">
<Label label={setting.string.Type} />
</span>
<DropdownLabelsIntl
selected={mode}
{items}
label={setting.string.Type}
on:selected={(res) => {
mode = res.detail
}}
/>
</div>
<div class="flex-col p-4 flex-gap-2">
<div class="flex-col-center">B</div>
<div>
<EditBox bind:value={nameB} placeholder={core.string.Name} kind={'default'} />
</div>
<div>
<NestedDropdown
items={classes}
on:selected={(e) => {
classBRef = e.detail
}}
/>
</div>
</div>
</div>
</Card>
@@ -29,10 +29,10 @@
showPopup,
themeStore
} from '@hcengineering/ui'
import { IconPicker } from '@hcengineering/view-resources'
import view from '@hcengineering/view-resources/src/plugin'
import setting from '../plugin'
import { clearSettingsStore } from '../store'
import { IconPicker, iconsLibrary } from '@hcengineering/view-resources'
export let attribute: AnyAttribute
export let exist: boolean
@@ -166,11 +166,6 @@
{/if}
</svelte:fragment>
<div class="hulyModal-content__titleGroup">
{#if attribute.isCustom}
<div class="hulyChip-item font-medium-12">
<Label label={setting.string.Custom} />
</div>
{/if}
<div class="flex items-center">
<ButtonIcon
icon={icon ?? setting.icon.Enums}
@@ -0,0 +1,112 @@
<!--
// 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.
-->
<script lang="ts">
import core, { Association } from '@hcengineering/core'
import { getEmbeddedLabel, IntlString } from '@hcengineering/platform'
import presentation, { getClient, MessageBox } from '@hcengineering/presentation'
import { Button, DropdownIntlItem, EditBox, IconDelete, Label, Modal, showPopup } from '@hcengineering/ui'
import view from '@hcengineering/view'
import setting from '../plugin'
import { clearSettingsStore } from '../store'
export let association: Association
const client = getClient()
const hierarchy = client.getHierarchy()
const classA = hierarchy.getClass(association.classA)
const classB = hierarchy.getClass(association.classB)
let nameA = association.nameA
let nameB = association.nameB
async function save (): Promise<void> {
await client.diffUpdate(association, {
nameA,
nameB
})
clearSettingsStore()
}
const items: DropdownIntlItem[] = [
{
id: '1:1',
label: getEmbeddedLabel('1:1')
},
{
id: '1:N',
label: getEmbeddedLabel('1:N')
},
{
id: 'N:N',
label: getEmbeddedLabel('N:N')
}
]
const label = items.find((item) => item.id === association?.type)?.label ?? ('' as IntlString)
async function remove (): Promise<void> {
showPopup(MessageBox, {
label: view.string.DeleteObject,
message: view.string.DeleteObjectConfirm,
params: { count: 1 },
dangerous: true,
action: async () => {
await client.remove(association)
clearSettingsStore()
}
})
}
</script>
<Modal
label={core.string.Relation}
type={'type-aside'}
okLabel={presentation.string.Save}
okAction={save}
canSave={true}
onCancel={clearSettingsStore}
>
<svelte:fragment slot="actions">
<Button icon={IconDelete} kind={'dangerous'} on:click={remove} />
</svelte:fragment>
<div class="hulyModal-content__settingsSet">
<div class="flex flex-gap-4">
<div class="flex-col p-4 flex-gap-2">
<div class="flex-col-center">A</div>
<div>
<EditBox bind:value={nameA} placeholder={core.string.Name} kind={'default'} />
</div>
<div>
<Label label={classA.label} />
</div>
</div>
<div class="flex-col p-4 flex-gap-2">
<span class="label">
<Label label={setting.string.Type} />
</span>
<Label {label} />
</div>
<div class="flex-col p-4 flex-gap-2">
<div class="flex-col-center">B</div>
<div>
<EditBox bind:value={nameB} placeholder={core.string.Name} kind={'default'} />
</div>
<div>
<Label label={classB.label} />
</div>
</div>
</div>
</div>
</Modal>
@@ -1,22 +1,28 @@
<script lang="ts">
import core, { Association, Class, Data, Doc, Ref } from '@hcengineering/core'
import { IntlString, translate } from '@hcengineering/platform'
import { createQuery, getClient, MessageBox } from '@hcengineering/presentation'
import {
Button,
Header,
Breadcrumb,
Separator,
Button,
defineSeparators,
twoPanelsSeparators,
Header,
IconDelete,
showPopup
Separator,
showPopup,
twoPanelsSeparators
} from '@hcengineering/ui'
import settings from '../plugin'
import view from '@hcengineering/view-resources/src/plugin'
import settings from '../plugin'
import card from '@hcengineering/card'
import AssociationEditor from './AssociationEditor.svelte'
export let _classes: Ref<Class<Doc>>[] = [core.class.Doc]
export let exclude: Ref<Class<Doc>>[] = [card.class.Card]
const query = createQuery()
const client = getClient()
const hierarchy = client.getHierarchy()
let selected: Association | Data<Association> | undefined
@@ -26,6 +32,37 @@
associations = res
})
$: filtered = filterAssociations(associations, _classes, exclude)
function filterAssociations (
associations: Association[],
_classes: Ref<Class<Doc>>[],
exclude: Ref<Class<Doc>>[]
): Association[] {
_classes = _classes ?? [core.class.Doc]
exclude = exclude ?? [card.class.Card]
const res: Association[] = []
const descendants = new Set(_classes.map((p) => hierarchy.getDescendants(p)).reduce((a, b) => a.concat(b)))
const excluded = new Set()
for (const _class of exclude) {
const desc = hierarchy.getDescendants(_class)
for (const _id of desc) {
excluded.add(_id)
}
}
for (const association of associations) {
if (
descendants.has(association.classA) &&
descendants.has(association.classB) &&
!excluded.has(association.classA) &&
!excluded.has(association.classB)
) {
res.push(association)
}
}
return res
}
function createRelation (): void {
selected = {
classA: '' as Ref<Class<Doc>>,
@@ -40,6 +77,19 @@
return (data as Association)?._id !== undefined
}
function getClassLabel (_class: Ref<Class<Doc>>): IntlString | undefined {
const _classLabel = client.getModel().findObject(_class)
return _classLabel?.label
}
async function getLabel (association: Association): Promise<string> {
const aLabel = getClassLabel(association.classA)
const bLabel = getClassLabel(association.classB)
const aClass = aLabel !== undefined ? await translate(aLabel, {}) : undefined
const bClass = bLabel !== undefined ? await translate(bLabel, {}) : undefined
return `${association.nameA} ${aClass !== undefined ? '(' + aClass + ')' : ''} - ${association.nameB} ${bClass !== undefined ? '(' + bClass + ')' : ''}`
}
defineSeparators('workspaceSettings', twoPanelsSeparators)
async function remove (val: Association | Data<Association> | undefined): Promise<void> {
@@ -82,7 +132,7 @@
</div>
<div class="flex-col overflow-y-auto">
{#each associations as association (association._id)}
{#each filtered as association (association._id)}
<button
class="list-item"
class:selected={selected === association}
@@ -90,7 +140,9 @@
selected = association
}}
>
<span class="font-regular-14 overflow-label">{association.nameA} - {association.nameB}</span>
{#await getLabel(association) then label}
<span class="font-regular-14 overflow-label">{label}</span>
{/await}
</button>
{/each}
</div>
@@ -98,6 +150,8 @@
<Separator name={'workspaceSettings'} index={0} color={'var(--theme-divider-color)'} />
{#if selected !== undefined}
<AssociationEditor
{exclude}
{_classes}
association={selected}
on:close={() => {
selected = undefined
@@ -103,5 +103,5 @@
{:else if kind === 'content' && category === undefined}
<div class="hulyComponent" />
{:else if category}
<Component is={category.component} props={{ kind: 'content' }} on:change />
<Component is={category.component} props={{ kind: 'content', ...category.props }} on:change />
{/if}
+5 -1
View File
@@ -63,6 +63,8 @@ import WorkspaceSettings from './components/WorkspaceSettings.svelte'
import SettingsWidget from './components/SettingsWidget.svelte'
import ClassHierarchy from './components/ClassHierarchy.svelte'
import CreateAttributePopup from './components/CreateAttributePopup.svelte'
import CreateRelation from './components/CreateRelation.svelte'
import EditRelation from './components/EditRelation.svelte'
import setting from './plugin'
import { filterDescendants, getOwnerFirstName, getOwnerLastName, getOwnerPosition, getValue } from './utils'
@@ -137,7 +139,9 @@ export default async (): Promise<Resources> => ({
RoleEditor,
RoleAssignmentEditor,
SettingsWidget,
CreateAttributePopup
CreateAttributePopup,
CreateRelation,
EditRelation
},
actionImpl: {
DeleteMixin
+3 -1
View File
@@ -169,7 +169,9 @@ export default plugin(settingId, {
RoleAssignmentEditor: '' as AnyComponent,
RelationSetting: '' as AnyComponent,
Backup: '' as AnyComponent,
CreateAttributePopup: '' as AnyComponent
CreateAttributePopup: '' as AnyComponent,
CreateRelation: '' as AnyComponent,
EditRelation: '' as AnyComponent
},
string: {
Settings: '' as IntlString,
@@ -35,6 +35,8 @@
export let selection: number | undefined = undefined
export let onContextMenu: ((ev: MouseEvent, object: Doc) => void) | undefined
const client = getClient()
const hierarchy = client.getHierarchy()
@@ -62,8 +64,13 @@
const showContextMenu = async (ev: MouseEvent, object: Doc, row: number): Promise<void> => {
selection = row
const items = object
showMenu(ev, { object: items, baseMenuClass })
ev.stopPropagation()
ev.preventDefault()
if (onContextMenu !== undefined) {
onContextMenu(ev, object)
} else {
showMenu(ev, { object, baseMenuClass })
}
}
function onRow (object: Doc): void {
@@ -119,33 +119,6 @@
position: relative;
min-width: 0;
min-height: 0;
&.show::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: 1px solid transparent;
border-radius: 0.25rem;
opacity: 0.95;
}
&.show::after {
content: attr(data-tooltip);
position: absolute;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
min-width: 0;
top: 50%;
left: 50%;
width: calc(100% - 0.5rem);
text-align: center;
font-size: 0.75rem;
color: var(--theme-content-color);
transform: translate(-50%, -50%);
}
}
.editor-container {
@@ -3,6 +3,7 @@
import { IntlString } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
import { Button, IconAdd, Label, Section, showPopup } from '@hcengineering/ui'
import { showMenu } from '../actions'
import view, { Viewlet, ViewletPreference } from '@hcengineering/view'
import DocTable from './DocTable.svelte'
import ObjectBoxPopup from './ObjectBoxPopup.svelte'
@@ -14,6 +15,8 @@
export let readonly: boolean = false
export let direction: 'A' | 'B'
const client = getClient()
$: _class = direction === 'B' ? association.classB : association.classA
function add (): void {
@@ -76,6 +79,17 @@
}
$: config = preference?.config ?? viewlet?.config
async function onContextMenu (ev: MouseEvent, doc: Doc): Promise<void> {
const q =
direction === 'A'
? { docA: object._id, docB: doc._id, association: association._id }
: { docA: doc._id, docB: object._id, association: association._id }
const relation = await client.findOne(core.class.Relation, q)
if (relation !== undefined) {
showMenu(ev, { object: relation, includedActions: [view.action.Delete] })
}
}
</script>
<Section {label}>
@@ -90,9 +104,7 @@
<svelte:fragment slot="content">
<div class="antiSection-empty solid flex-col mt-3">
{#if docs?.length > 0 && config != null}
<div class="self-start flex-col flex-gap-2">
<DocTable objects={docs} {_class} {config} />
</div>
<DocTable objects={docs} {_class} {config} {onContextMenu} />
{:else if !readonly}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
@@ -19,13 +19,13 @@
import { Button, IconAdd, eventToHTMLElement, getCurrentLocation, showPopup } from '@hcengineering/ui'
import { Filter, FilteredView, ViewOptions, Viewlet } from '@hcengineering/view'
import { createEventDispatcher } from 'svelte'
import { filterStore, removeFilter, updateFilter, selectedFilterStore } from '../../filter'
import { filterStore, removeFilter, selectedFilterStore, updateFilter } from '../../filter'
import view from '../../plugin'
import { activeViewlet, getActiveViewletId, makeViewletKey } from '../../utils'
import { getViewOptions, viewOptionStore } from '../../viewOptions'
import FilterSave from './FilterSave.svelte'
import FilterSection from './FilterSection.svelte'
import FilterTypePopup from './FilterTypePopup.svelte'
import { activeViewlet, getActiveViewletId, makeViewletKey } from '../../utils'
import { getViewOptions, viewOptionStore } from '../../viewOptions'
export let _class: Ref<Class<Doc>> | undefined
export let space: Ref<Space> | undefined
@@ -145,11 +145,9 @@
$: makeQuery(query, $filterStore)
let clazz: Class<Doc<Space>>
let visible: boolean = false
$: if (_class) {
clazz = hierarchy.getClass(_class)
visible = hierarchy.hasMixin(clazz, view.mixin.ClassFilters)
visible = hierarchy.classHierarchyMixin(_class, view.mixin.ClassFilters) !== undefined
}
const me = getCurrentAccount()._id
@@ -81,8 +81,7 @@
export let visible: boolean = false
$: {
if (_class) {
const clazz = hierarchy.getClass(_class)
visible = hierarchy.hasMixin(clazz, view.mixin.ClassFilters)
visible = hierarchy.classHierarchyMixin(_class, view.mixin.ClassFilters) !== undefined
} else visible = false
}
</script>
+1 -2
View File
@@ -37,8 +37,7 @@ export default mergeIds(viewId, view, {
LabelYes: '' as IntlString,
LabelNo: '' as IntlString,
ChooseAColor: '' as IntlString,
DeleteObject: '' as IntlString,
DeleteObjectConfirm: '' as IntlString,
DeletePopupNoPermissionTitle: '' as IntlString,
DeletePopupNoPermissionLabel: '' as IntlString,
DeletePopupCreatorLabel: '' as IntlString,
+3 -1
View File
@@ -214,7 +214,9 @@ const view = plugin(viewId, {
Leave: '' as IntlString,
Copied: '' as IntlString,
And: '' as IntlString,
Title: '' as IntlString
Title: '' as IntlString,
DeleteObject: '' as IntlString,
DeleteObjectConfirm: '' as IntlString
},
icon: {
Table: '' as Asset,
@@ -146,7 +146,6 @@
return special.checkIsDisabled && (await (await getResource(special.checkIsDisabled))())
}
let savedMenu: boolean = false
let menuSelection: boolean = false
</script>
@@ -173,11 +172,7 @@
{/if}
<div class="min-h-3 flex-no-shrink" />
<SavedView
{currentApplication}
on:shown={(res) => (savedMenu = res.detail)}
on:select={(res) => (menuSelection = res.detail)}
/>
<SavedView alias={currentApplication?.alias} on:select={(res) => (menuSelection = res.detail)} />
{#if starred.length}
<StarredNav
label={preference.string.Starred}
@@ -32,13 +32,12 @@
setViewOptions,
viewOptionStore
} from '@hcengineering/view-resources'
import { Application } from '@hcengineering/workbench'
import copy from 'fast-copy'
import { createEventDispatcher } from 'svelte'
import TodoCheck from './icons/TodoCheck.svelte'
import TodoUncheck from './icons/TodoUncheck.svelte'
export let currentApplication: Application | undefined
export let alias: string | undefined
const dispatch = createEventDispatcher()
const client = getClient()
@@ -47,23 +46,19 @@
const filteredViewsQuery = createQuery()
let availableFilteredViews: FilteredView[] = []
let myFilteredViews: FilteredView[] = []
$: if (currentApplication?.alias !== undefined) {
filteredViewsQuery.query<FilteredView>(
view.class.FilteredView,
{ attachedTo: currentApplication?.alias },
(result) => {
myFilteredViews = result.filter((p) => p.users.includes(me))
availableFilteredViews = result.filter((p) => p.sharable && !p.users.includes(me))
$: if (alias !== undefined) {
filteredViewsQuery.query<FilteredView>(view.class.FilteredView, { attachedTo: alias }, (result) => {
myFilteredViews = result.filter((p) => p.users.includes(me))
availableFilteredViews = result.filter((p) => p.sharable && !p.users.includes(me))
const location = getLocation()
if (location.query?.filterViewId) {
const targetView = result.find((view) => view._id === location.query?.filterViewId)
if (targetView) {
load(targetView)
}
const location = getLocation()
if (location.query?.filterViewId) {
const targetView = result.find((view) => view._id === location.query?.filterViewId)
if (targetView) {
load(targetView)
}
}
)
})
} else {
filteredViewsQuery.unsubscribe()
myFilteredViews = []
+1
View File
@@ -40,6 +40,7 @@ export { default as TreeSeparator } from './components/navigator/TreeSeparator.s
export { default as WorkbenchTabs } from './components/WorkbenchTabs.svelte'
export { default as AppItem } from './components/AppItem.svelte'
export { default as ComponentNavigator } from './components/ComponentNavigator.svelte'
export { default as SavedView } from './components/SavedView.svelte'
export { SpecialView }
+15
View File
@@ -2260,6 +2260,21 @@
"packageName": "@hcengineering/green",
"projectFolder": "pods/green",
"shouldPublish": false
},
{
"packageName": "@hcengineering/server-card",
"projectFolder": "server-plugins/card",
"shouldPublish": false
},
{
"packageName": "@hcengineering/server-card-resources",
"projectFolder": "server-plugins/card-resources",
"shouldPublish": false
},
{
"packageName": "@hcengineering/model-server-card",
"projectFolder": "models/server-card",
"shouldPublish": false
}
]
}
@@ -0,0 +1,7 @@
module.exports = {
extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'],
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json'
}
}
+4
View File
@@ -0,0 +1,4 @@
*
!/lib/**
!CHANGELOG.md
/lib/**/__tests__/
@@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
"rigPackageName": "@hcengineering/platform-rig"
}
@@ -0,0 +1,7 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
roots: ["./src"],
coverageReporters: ["text-summary", "html"]
}
@@ -0,0 +1,46 @@
{
"name": "@hcengineering/server-card-resources",
"version": "0.6.0",
"main": "lib/index.js",
"svelte": "src/index.ts",
"types": "types/index.d.ts",
"files": [
"lib/**/*",
"types/**/*",
"tsconfig.json"
],
"author": "Anticrm Platform Contributors",
"license": "EPL-2.0",
"scripts": {
"build": "compile",
"build:watch": "compile",
"format": "format src",
"test": "jest --passWithNoTests --silent",
"_phase:build": "compile transpile src",
"_phase:test": "jest --passWithNoTests --silent",
"_phase:format": "format src",
"_phase:validate": "compile validate"
},
"devDependencies": {
"@hcengineering/platform-rig": "^0.6.0",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-n": "^15.4.0",
"eslint": "^8.54.0",
"@typescript-eslint/parser": "^6.11.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"prettier": "^3.1.0",
"typescript": "^5.3.3",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"@types/jest": "^29.5.5"
},
"dependencies": {
"@hcengineering/view": "^0.6.13",
"@hcengineering/card": "^0.6.0",
"@hcengineering/core": "^0.6.32",
"@hcengineering/platform": "^0.6.11",
"@hcengineering/server-core": "^0.6.1"
}
}
@@ -0,0 +1,55 @@
//
// 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 { AnyAttribute, Tx, TxCreateDoc, TxProcessor } from '@hcengineering/core'
import card from '@hcengineering/card'
import view from '@hcengineering/view'
import { TriggerControl } from '@hcengineering/server-core'
async function OnAttribute (ctx: TxCreateDoc<AnyAttribute>[], control: TriggerControl): Promise<Tx[]> {
const attr = TxProcessor.createDoc2Doc(ctx[0])
if (control.hierarchy.isDerived(attr.attributeOf, card.class.Card)) {
const desc = control.hierarchy.getDescendants(attr.attributeOf)
const res: Tx[] = []
for (const des of desc) {
const viewlets = control.modelDb.findAllSync(view.class.Viewlet, { attachTo: des })
for (const viewlet of viewlets) {
viewlet.config.push(attr.name)
res.push(
control.txFactory.createTxUpdateDoc(viewlet._class, viewlet.space, viewlet._id, {
config: viewlet.config
})
)
const prefs = await control.findAll(control.ctx, view.class.ViewletPreference, { attachedTo: viewlet._id })
for (const pref of prefs) {
res.push(
control.txFactory.createTxUpdateDoc(pref._class, pref.space, pref._id, {
config: viewlet.config
})
)
}
}
}
return res
}
return []
}
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export default async () => ({
trigger: {
OnAttribute
}
})
@@ -0,0 +1,10 @@
{
"extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib",
"declarationDir": "./types",
"tsBuildInfoFile": ".build/build.tsbuildinfo"
}
}
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'],
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json'
}
}
+4
View File
@@ -0,0 +1,4 @@
*
!/lib/**
!CHANGELOG.md
/lib/**/__tests__/
+4
View File
@@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
"rigPackageName": "@hcengineering/platform-rig"
}
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
roots: ["./src"],
coverageReporters: ["text-summary", "html"]
}
+45
View File
@@ -0,0 +1,45 @@
{
"name": "@hcengineering/server-card",
"version": "0.6.0",
"main": "lib/index.js",
"svelte": "src/index.ts",
"types": "types/index.d.ts",
"files": [
"lib/**/*",
"types/**/*",
"tsconfig.json"
],
"author": "Anticrm Platform Contributors",
"license": "EPL-2.0",
"scripts": {
"build": "compile",
"build:watch": "compile",
"format": "format src",
"test": "jest --passWithNoTests --silent",
"_phase:build": "compile transpile src",
"_phase:test": "jest --passWithNoTests --silent",
"_phase:format": "format src",
"_phase:validate": "compile validate"
},
"devDependencies": {
"@hcengineering/platform-rig": "^0.6.0",
"@types/node": "~20.11.16",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-n": "^15.4.0",
"eslint": "^8.54.0",
"@typescript-eslint/parser": "^6.11.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"prettier": "^3.1.0",
"typescript": "^5.3.3",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"@types/jest": "^29.5.5"
},
"dependencies": {
"@hcengineering/core": "^0.6.32",
"@hcengineering/server-core": "^0.6.1",
"@hcengineering/platform": "^0.6.11"
}
}
+32
View File
@@ -0,0 +1,32 @@
//
// 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 { Plugin, Resource } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform'
import type { TriggerFunc } from '@hcengineering/server-core'
/**
* @public
*/
export const serverCardId = 'server-card' as Plugin
/**
* @public
*/
export default plugin(serverCardId, {
trigger: {
OnAttribute: '' as Resource<TriggerFunc>
}
})
+10
View File
@@ -0,0 +1,10 @@
{
"extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib",
"declarationDir": "./types",
"tsBuildInfoFile": ".build/build.tsbuildinfo"
}
}
+3 -6
View File
@@ -19,12 +19,9 @@ import plugin, {
} from '@hcengineering/server-core'
export function findSearchPresenter (hierarchy: Hierarchy, _class: Ref<Class<Doc>>): SearchPresenter | undefined {
const ancestors = hierarchy.getAncestors(_class).reverse()
for (const _class of ancestors) {
const searchMixin = hierarchy.classHierarchyMixin(_class, plugin.mixin.SearchPresenter)
if (searchMixin !== undefined) {
return searchMixin
}
const searchMixin = hierarchy.classHierarchyMixin(_class, plugin.mixin.SearchPresenter)
if (searchMixin !== undefined) {
return searchMixin
}
return undefined
}
+2
View File
@@ -65,6 +65,8 @@
"@hcengineering/server-tracker-resources": "^0.6.0",
"@hcengineering/server-tags": "^0.6.0",
"@hcengineering/server-tags-resources": "^0.6.0",
"@hcengineering/server-card": "^0.6.0",
"@hcengineering/server-card-resources": "^0.6.0",
"@hcengineering/server-calendar": "^0.6.0",
"@hcengineering/server-calendar-resources": "^0.6.0",
"@hcengineering/server-github": "^0.6.0",
@@ -1,6 +1,7 @@
import { addLocation } from '@hcengineering/platform'
import { serverActivityId } from '@hcengineering/server-activity'
import { serverAttachmentId } from '@hcengineering/server-attachment'
import { serverCardId } from '@hcengineering/server-card'
import { serverCalendarId } from '@hcengineering/server-calendar'
import { serverChunterId } from '@hcengineering/server-chunter'
import { serverCollaborationId } from '@hcengineering/server-collaboration'
@@ -43,6 +44,7 @@ export function registerServerPlugins (): void {
addLocation(serverTaskId, () => import('@hcengineering/server-task-resources'))
addLocation(serverTrackerId, () => import('@hcengineering/server-tracker-resources'))
addLocation(serverTagsId, () => import('@hcengineering/server-tags-resources'))
addLocation(serverCardId, () => import('@hcengineering/server-card-resources'))
addLocation(serverCalendarId, () => import('@hcengineering/server-calendar-resources'))
addLocation(serverGmailId, () => import('@hcengineering/server-gmail-resources'))
addLocation(serverTelegramId, () => import('@hcengineering/server-telegram-resources'))