From c7dc5c48fe968fc50bdd665b369d2dca6a3d64d5 Mon Sep 17 00:00:00 2001 From: Denis Bykhov Date: Thu, 12 Feb 2026 20:46:54 +0500 Subject: [PATCH 1/4] Card notifications (#10503) Signed-off-by: Denis Bykhov --- common/config/rush/pnpm-lock.yaml | 12 +++++ models/card/package.json | 2 + models/card/src/index.ts | 45 +++++++++++++++++++ models/card/src/plugin.ts | 6 ++- models/server-card/package.json | 1 + models/server-card/src/index.ts | 5 +++ plugins/card-assets/lang/cs.json | 3 +- plugins/card-assets/lang/de.json | 3 +- plugins/card-assets/lang/en.json | 3 +- plugins/card-assets/lang/es.json | 3 +- plugins/card-assets/lang/fr.json | 3 +- plugins/card-assets/lang/it.json | 3 +- plugins/card-assets/lang/ja.json | 3 +- plugins/card-assets/lang/pt-br.json | 3 +- plugins/card-assets/lang/pt.json | 3 +- plugins/card-assets/lang/ru.json | 3 +- plugins/card-assets/lang/tr.json | 3 +- plugins/card-assets/lang/zh.json | 3 +- plugins/card-resources/src/plugin.ts | 3 +- server-plugins/card-resources/src/index.ts | 9 ++++ server-plugins/card/package.json | 1 + server-plugins/card/src/index.ts | 4 ++ .../notification-resources/src/utils.ts | 9 ++-- 23 files changed, 116 insertions(+), 17 deletions(-) diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 0c2da87ce8..7da6d5f66e 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -6857,6 +6857,9 @@ importers: '@hcengineering/model-guest': specifier: workspace:^0.7.0 version: link:../guest + '@hcengineering/model-notification': + specifier: workspace:^0.7.0 + version: link:../notification '@hcengineering/model-preference': specifier: workspace:^0.7.0 version: link:../preference @@ -6872,6 +6875,9 @@ importers: '@hcengineering/model-workbench': specifier: workspace:^0.7.0 version: link:../workbench + '@hcengineering/notification': + specifier: workspace:^0.7.0 + version: link:../../plugins/notification '@hcengineering/platform': specifier: workspace:^0.7.19 version: link:../../foundations/core/packages/platform @@ -10425,6 +10431,9 @@ importers: '@hcengineering/server-core': specifier: workspace:^0.7.18 version: link:../../foundations/server/packages/core + '@hcengineering/server-notification': + specifier: workspace:^0.7.0 + version: link:../../server-plugins/notification devDependencies: '@hcengineering/platform-rig': specifier: workspace:^0.7.19 @@ -32222,6 +32231,9 @@ importers: '@hcengineering/server-core': specifier: workspace:^0.7.18 version: link:../../foundations/server/packages/core + '@hcengineering/server-notification': + specifier: workspace:^0.7.0 + version: link:../notification devDependencies: '@hcengineering/platform-rig': specifier: workspace:^0.7.19 diff --git a/models/card/package.json b/models/card/package.json index 833ff66d01..72ce95c6fb 100644 --- a/models/card/package.json +++ b/models/card/package.json @@ -40,6 +40,8 @@ "@hcengineering/converter": "workspace:^0.7.0", "@hcengineering/core": "workspace:^0.7.24", "@hcengineering/model": "workspace:^0.7.17", + "@hcengineering/notification": "workspace:^0.7.0", + "@hcengineering/model-notification": "workspace:^0.7.0", "@hcengineering/chunter": "workspace:^0.7.0", "@hcengineering/model-setting": "workspace:^0.7.0", "@hcengineering/model-view": "workspace:^0.7.0", diff --git a/models/card/src/index.ts b/models/card/src/index.ts index e156dbeb84..b3a3a59426 100644 --- a/models/card/src/index.ts +++ b/models/card/src/index.ts @@ -84,6 +84,7 @@ import { type BuildModelKey } from '@hcengineering/view' import { createActions } from './actions' import { definePermissions } from './permissions' import card from './plugin' +import notification from '@hcengineering/notification' export { cardId } from '@hcengineering/card' @@ -149,6 +150,9 @@ export class TCard extends TDoc implements Card { @Hidden() @ReadOnly() peerId?: string + + @Prop(Collection(chunter.class.ChatMessage), chunter.string.Comments) + comments?: number } @Model(card.class.CardSpace, core.class.TypedSpace, DOMAIN_SPACE) @@ -445,6 +449,47 @@ export function createModel (builder: Builder): void { PaletteColorIndexes.Arctic ) + builder.createDoc( + notification.class.NotificationGroup, + core.space.Model, + { + label: card.string.Card, + icon: card.icon.Card + }, + card.ids.CardNotificationGroup + ) + + builder.createDoc( + notification.class.NotificationType, + core.space.Model, + { + hidden: false, + generated: false, + label: card.string.CardUpdated, + group: card.ids.CardNotificationGroup, + txClasses: [core.class.TxUpdateDoc, core.class.TxMixin], + objectClass: card.class.Card, + defaultEnabled: false + }, + card.ids.CardNotification + ) + + builder.createDoc( + notification.class.NotificationType, + core.space.Model, + { + hidden: false, + generated: false, + label: chunter.string.Comments, + group: card.ids.CardNotificationGroup, + txClasses: [core.class.TxCreateDoc], + objectClass: chunter.class.ChatMessage, + attachedToClass: card.class.Card, + defaultEnabled: true + }, + card.ids.CardMessageNotification + ) + builder.createDoc(view.class.Viewlet, core.space.Model, { attachTo: card.class.CardSpace, descriptor: view.viewlet.Table, diff --git a/models/card/src/plugin.ts b/models/card/src/plugin.ts index b17913c44c..1db60f2a06 100644 --- a/models/card/src/plugin.ts +++ b/models/card/src/plugin.ts @@ -22,6 +22,7 @@ import { type TagCategory } from '@hcengineering/tags' import { type Location, type ResolvedLocation } from '@hcengineering/ui/src/types' import { type Action, type ActionCategory, type ViewAction } from '@hcengineering/view' import { type LocationData } from '@hcengineering/workbench' +import { type NotificationGroup, type NotificationType } from '@hcengineering/notification' export default mergeIds(cardId, card, { app: { @@ -46,7 +47,10 @@ export default mergeIds(cardId, card, { ids: { MasterTags: '' as Ref, ManageMasterTags: '' as Ref, - TagRelations: '' as Ref + TagRelations: '' as Ref, + CardNotificationGroup: '' as Ref, + CardNotification: '' as Ref, + CardMessageNotification: '' as Ref }, resolver: { Location: '' as Resource<(loc: Location) => Promise>, diff --git a/models/server-card/package.json b/models/server-card/package.json index c36b9bf3cd..909cfadf07 100644 --- a/models/server-card/package.json +++ b/models/server-card/package.json @@ -39,6 +39,7 @@ "@hcengineering/platform": "workspace:^0.7.19", "@hcengineering/card": "workspace:^0.7.0", "@hcengineering/communication": "workspace:^0.7.0", + "@hcengineering/server-notification": "workspace:^0.7.0", "@hcengineering/server-card": "workspace:^0.7.0", "@hcengineering/server-core": "workspace:^0.7.18" } diff --git a/models/server-card/src/index.ts b/models/server-card/src/index.ts index 9008fbee70..e5c66f5417 100644 --- a/models/server-card/src/index.ts +++ b/models/server-card/src/index.ts @@ -20,6 +20,7 @@ import serverCore from '@hcengineering/server-core' import serverCard from '@hcengineering/server-card' import card from '@hcengineering/card' import communication from '@hcengineering/communication' +import serverNotification from '@hcengineering/server-notification' export { serverCardId } from '@hcengineering/server-card' @@ -129,4 +130,8 @@ export function createModel (builder: Builder): void { }, title: [['title']] }) + + builder.mixin(card.class.Card, core.class.Class, serverNotification.mixin.TextPresenter, { + presenter: serverCard.function.CardTextPresenter + }) } diff --git a/plugins/card-assets/lang/cs.json b/plugins/card-assets/lang/cs.json index df3b9dec1d..c66e477bd6 100644 --- a/plugins/card-assets/lang/cs.json +++ b/plugins/card-assets/lang/cs.json @@ -70,6 +70,7 @@ "NewVersionConfirmation": "Chcete vytvořit novou verzi?", "RelationCopyDescr": "Které vztahy chcete zkopírovat?", "Import": "Importovat", - "Export": "Exportovat" + "Export": "Exportovat", + "CardUpdated": "Karta aktualizována" } } diff --git a/plugins/card-assets/lang/de.json b/plugins/card-assets/lang/de.json index 374ffea2c1..1524d538ea 100644 --- a/plugins/card-assets/lang/de.json +++ b/plugins/card-assets/lang/de.json @@ -70,6 +70,7 @@ "NewVersionConfirmation": "Möchten Sie eine neue Version erstellen?", "RelationCopyDescr": "Welche Beziehungen möchten Sie kopieren?", "Import": "Importieren", - "Export": "Exportieren" + "Export": "Exportieren", + "CardUpdated": "Karte aktualisiert" } } diff --git a/plugins/card-assets/lang/en.json b/plugins/card-assets/lang/en.json index fc1defaaf4..b3eb41e468 100644 --- a/plugins/card-assets/lang/en.json +++ b/plugins/card-assets/lang/en.json @@ -70,6 +70,7 @@ "NewVersionConfirmation": "Do you want to create a new version?", "RelationCopyDescr": "Which relations do you want to copy?", "Import": "Import", - "Export": "Export" + "Export": "Export", + "CardUpdated": "Card updated" } } diff --git a/plugins/card-assets/lang/es.json b/plugins/card-assets/lang/es.json index 773fe047c1..ac1f754c3b 100644 --- a/plugins/card-assets/lang/es.json +++ b/plugins/card-assets/lang/es.json @@ -70,6 +70,7 @@ "NewVersionConfirmation": "¿Desea crear una nueva versión?", "RelationCopyDescr": "¿Qué relaciones quieres copiar?", "Import": "Importar", - "Export": "Exportar" + "Export": "Exportar", + "CardUpdated": "Tarjeta actualizada" } } diff --git a/plugins/card-assets/lang/fr.json b/plugins/card-assets/lang/fr.json index 04d85431f1..0a4867c3af 100644 --- a/plugins/card-assets/lang/fr.json +++ b/plugins/card-assets/lang/fr.json @@ -70,6 +70,7 @@ "NewVersionConfirmation": "Voulez-vous créer une nouvelle version ?", "RelationCopyDescr": "Quelles relations souhaitez-vous copier ?", "Import": "Importer", - "Export": "Exporter" + "Export": "Exporter", + "CardUpdated": "Carte mise à jour" } } diff --git a/plugins/card-assets/lang/it.json b/plugins/card-assets/lang/it.json index d06a1e04b7..98976f5e50 100644 --- a/plugins/card-assets/lang/it.json +++ b/plugins/card-assets/lang/it.json @@ -70,6 +70,7 @@ "NewVersionConfirmation": "Vuoi creare una nuova versione?", "RelationCopyDescr": "Quali relazioni vuoi copiare?", "Import": "Importa", - "Export": "Esporta" + "Export": "Esporta", + "CardUpdated": "Scheda aggiornata" } } diff --git a/plugins/card-assets/lang/ja.json b/plugins/card-assets/lang/ja.json index 2fbe52c9f7..5b18951fe0 100644 --- a/plugins/card-assets/lang/ja.json +++ b/plugins/card-assets/lang/ja.json @@ -70,6 +70,7 @@ "NewVersionConfirmation": "新しいバージョンを作成しますか?", "RelationCopyDescr": "どの関連をコピーしますか?", "Import": "インポート", - "Export": "エクスポート" + "Export": "エクスポート", + "CardUpdated": "カードが更新されました" } } diff --git a/plugins/card-assets/lang/pt-br.json b/plugins/card-assets/lang/pt-br.json index 4347f3427f..c2148a4c4b 100644 --- a/plugins/card-assets/lang/pt-br.json +++ b/plugins/card-assets/lang/pt-br.json @@ -70,6 +70,7 @@ "NewVersionConfirmation": "Deseja criar uma nova versão?", "RelationCopyDescr": "Quais relações você deseja copiar?", "Import": "Importar", - "Export": "Exportar" + "Export": "Exportar", + "CardUpdated": "Cartão atualizado" } } diff --git a/plugins/card-assets/lang/pt.json b/plugins/card-assets/lang/pt.json index 4347f3427f..c2148a4c4b 100644 --- a/plugins/card-assets/lang/pt.json +++ b/plugins/card-assets/lang/pt.json @@ -70,6 +70,7 @@ "NewVersionConfirmation": "Deseja criar uma nova versão?", "RelationCopyDescr": "Quais relações você deseja copiar?", "Import": "Importar", - "Export": "Exportar" + "Export": "Exportar", + "CardUpdated": "Cartão atualizado" } } diff --git a/plugins/card-assets/lang/ru.json b/plugins/card-assets/lang/ru.json index c5e0a3bf59..98ef2647ba 100644 --- a/plugins/card-assets/lang/ru.json +++ b/plugins/card-assets/lang/ru.json @@ -70,6 +70,7 @@ "NewVersionConfirmation": "Вы хотите создать новую версию?", "RelationCopyDescr": "Какие связи вы хотите скопировать?", "Import": "Импортировать", - "Export": "Экспортировать" + "Export": "Экспортировать", + "CardUpdated": "Карточка обновлена" } } diff --git a/plugins/card-assets/lang/tr.json b/plugins/card-assets/lang/tr.json index 89c08295b2..f0522bfaae 100644 --- a/plugins/card-assets/lang/tr.json +++ b/plugins/card-assets/lang/tr.json @@ -70,6 +70,7 @@ "NewVersionConfirmation": "Yeni bir sürüm oluşturmak istiyor musunuz?", "RelationCopyDescr": "Hangi ilişkileri kopyalamak istiyorsunuz?", "Import": "İçe aktar", - "Export": "Dışa aktar" + "Export": "Dışa aktar", + "CardUpdated": "Kart güncellendi" } } diff --git a/plugins/card-assets/lang/zh.json b/plugins/card-assets/lang/zh.json index c83eace50e..371a92de03 100644 --- a/plugins/card-assets/lang/zh.json +++ b/plugins/card-assets/lang/zh.json @@ -70,6 +70,7 @@ "NewVersionConfirmation": "您想创建一个新版本吗?", "RelationCopyDescr": "您想复制哪些关系?", "Import": "导入", - "Export": "导出" + "Export": "导出", + "CardUpdated": "卡片已更新" } } diff --git a/plugins/card-resources/src/plugin.ts b/plugins/card-resources/src/plugin.ts index bcc7d4c606..0ea733cab9 100644 --- a/plugins/card-resources/src/plugin.ts +++ b/plugins/card-resources/src/plugin.ts @@ -159,6 +159,7 @@ export default mergeIds(cardId, card, { ForbidUpdateCard: '' as IntlString, ForbidCreateCardPermission: '' as IntlString, ForbidAddTagPermission: '' as IntlString, - ForbidRemoveTag: '' as IntlString + ForbidRemoveTag: '' as IntlString, + CardUpdated: '' as IntlString } }) diff --git a/server-plugins/card-resources/src/index.ts b/server-plugins/card-resources/src/index.ts index b6c2f6ea12..98cbd5dd3f 100644 --- a/server-plugins/card-resources/src/index.ts +++ b/server-plugins/card-resources/src/index.ts @@ -876,8 +876,17 @@ export async function OnCardTag (ctx: TxMixin[], control: TriggerCon return res } +export async function CardTextPresenter (doc: Doc): Promise { + const card = doc as Card + + return card.title +} + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export default async () => ({ + function: { + CardTextPresenter + }, trigger: { OnAttribute, OnAttributeRemove, diff --git a/server-plugins/card/package.json b/server-plugins/card/package.json index 9a89ef5fa6..e36a103f00 100644 --- a/server-plugins/card/package.json +++ b/server-plugins/card/package.json @@ -39,6 +39,7 @@ }, "dependencies": { "@hcengineering/core": "workspace:^0.7.24", + "@hcengineering/server-notification": "workspace:^0.7.0", "@hcengineering/server-core": "workspace:^0.7.18", "@hcengineering/platform": "workspace:^0.7.19" } diff --git a/server-plugins/card/src/index.ts b/server-plugins/card/src/index.ts index 39e2c597a3..ad195ea658 100644 --- a/server-plugins/card/src/index.ts +++ b/server-plugins/card/src/index.ts @@ -16,6 +16,7 @@ import type { Metadata, Plugin, Resource } from '@hcengineering/platform' import { plugin } from '@hcengineering/platform' import type { TriggerFunc } from '@hcengineering/server-core' +import { type Presenter } from '@hcengineering/server-notification' /** * @public @@ -29,6 +30,9 @@ export default plugin(serverCardId, { metadata: { CommunicationEnabled: '' as Metadata }, + function: { + CardTextPresenter: '' as Resource + }, trigger: { OnAttribute: '' as Resource, OnAttributeRemove: '' as Resource, diff --git a/server-plugins/notification-resources/src/utils.ts b/server-plugins/notification-resources/src/utils.ts index 8bcd5809da..a3187ccc4c 100644 --- a/server-plugins/notification-resources/src/utils.ts +++ b/server-plugins/notification-resources/src/utils.ts @@ -266,10 +266,13 @@ function getMatchedTypes ( isSpace: boolean, field?: string ): NotificationType[] { - const allTypes = control.modelDb - .findAllSync(notification.class.NotificationType, { ...(field !== undefined ? { field } : {}) }) - .filter((p) => (isSpace ? p.spaceSubscribe === true : p.spaceSubscribe !== true)) const filtered: NotificationType[] = [] + let allTypes: NotificationType[] = control.modelDb.findAllSync(notification.class.NotificationType, {}) + allTypes = allTypes.filter( + (p) => + (isSpace ? p.spaceSubscribe === true : p.spaceSubscribe !== true) && + (field === undefined || p.field === field || p.field === undefined) + ) for (const type of allTypes) { if (isTypeMatched(control, type, tx, isOwn)) { filtered.push(type) From 3ec280cb6e4033edf170e3389edee8c6a498d16a Mon Sep 17 00:00:00 2001 From: Denis Bykhov Date: Thu, 12 Feb 2026 20:48:44 +0500 Subject: [PATCH 2/4] Change approve time format (#10504) Signed-off-by: Denis Bykhov --- .../components/ApproveRequestPresenter.svelte | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/plugins/process-resources/src/components/ApproveRequestPresenter.svelte b/plugins/process-resources/src/components/ApproveRequestPresenter.svelte index 0d28021eab..726d946190 100644 --- a/plugins/process-resources/src/components/ApproveRequestPresenter.svelte +++ b/plugins/process-resources/src/components/ApproveRequestPresenter.svelte @@ -16,15 +16,30 @@
From 315f39092032b265b9aa089a4fe67865330825f8 Mon Sep 17 00:00:00 2001 From: Artyom Savchenko Date: Fri, 13 Feb 2026 17:30:43 +0700 Subject: [PATCH 3/4] Add ability to copy all data from cards/docs tables (#10505) Signed-off-by: Artem Savchenko --- common/config/rush/pnpm-lock.yaml | 32 +++++++++- models/card/src/index.ts | 54 ++++++++++++++++ models/card/src/plugin.ts | 8 ++- models/controlled-documents/src/index.ts | 34 +++++++--- models/controlled-documents/src/plugin.ts | 13 ++-- models/recruit/src/index.ts | 9 ++- models/recruit/src/plugin.ts | 6 +- models/tracker/src/index.ts | 11 ++++ models/tracker/src/plugin.ts | 5 +- models/view/src/index.ts | 37 +++++++++-- models/workbench/src/index.ts | 28 ++------- plugins/converter-resources/.eslintrc.js | 7 +-- plugins/converter-resources/.prettierrc | 22 +++++++ plugins/converter-resources/package.json | 27 ++++---- plugins/converter-resources/postcss.config.js | 5 ++ .../src/__tests__/copyAsMarkdownTable.test.ts | 3 +- .../components/CopyAsMarkdownButton.svelte | 46 ++++++++++++++ .../src/data/relationshipBuilder.ts | 8 +-- plugins/converter-resources/src/index.ts | 4 ++ .../src/markdown/tableBuilder.ts | 10 ++- plugins/converter-resources/svelte.config.js | 10 +++ plugins/converter-resources/tsconfig.json | 5 +- plugins/converter/package.json | 4 +- plugins/converter/src/plugin.ts | 9 +++ .../src/components/issues/IssuesView.svelte | 1 + plugins/view-assets/lang/cs.json | 1 + plugins/view-assets/lang/de.json | 1 + plugins/view-assets/lang/en.json | 1 + plugins/view-assets/lang/es.json | 1 + plugins/view-assets/lang/fr.json | 1 + plugins/view-assets/lang/it.json | 1 + plugins/view-assets/lang/ja.json | 1 + plugins/view-assets/lang/pt-br.json | 1 + plugins/view-assets/lang/pt.json | 1 + plugins/view-assets/lang/ru.json | 1 + plugins/view-assets/lang/tr.json | 1 + plugins/view-assets/lang/zh.json | 1 + .../src/components/RelationshipTable.svelte | 56 ++++++++++------- .../src/components/SpaceHeader.svelte | 17 ++++- .../src/components/ViewletPanelHeader.svelte | 16 ++++- plugins/view-resources/src/index.ts | 1 + .../view-resources/src/viewletContextStore.ts | 2 + plugins/view-resources/src/viewletUtils.ts | 63 +++++++++++++++++++ plugins/view/src/index.ts | 6 +- plugins/view/src/types.ts | 19 ++++++ .../src/components/SpecialView.svelte | 14 ++--- plugins/workbench/src/index.ts | 1 - plugins/workbench/src/plugin.ts | 5 +- plugins/workbench/src/types.ts | 27 +------- 49 files changed, 491 insertions(+), 146 deletions(-) create mode 100644 plugins/converter-resources/.prettierrc create mode 100644 plugins/converter-resources/postcss.config.js create mode 100644 plugins/converter-resources/src/components/CopyAsMarkdownButton.svelte create mode 100644 plugins/converter-resources/svelte.config.js create mode 100644 plugins/view-resources/src/viewletUtils.ts diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 7da6d5f66e..92d7848f7b 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -19017,6 +19017,12 @@ importers: '@hcengineering/platform': specifier: workspace:^0.7.19 version: link:../../foundations/core/packages/platform + '@hcengineering/presentation': + specifier: workspace:^0.7.0 + version: link:../../packages/presentation + '@hcengineering/ui': + specifier: workspace:^0.7.0 + version: link:../../packages/ui '@hcengineering/view': specifier: workspace:^0.7.0 version: link:../view @@ -19090,6 +19096,9 @@ importers: '@hcengineering/view-resources': specifier: workspace:^0.7.0 version: link:../view-resources + svelte: + specifier: ^4.2.20 + version: 4.2.20 devDependencies: '@hcengineering/platform-rig': specifier: workspace:^0.7.19 @@ -19118,15 +19127,36 @@ importers: eslint-plugin-promise: specifier: ^6.1.1 version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@swc/core@1.15.1)(@types/node@22.19.0)(typescript@5.9.3)) jest: specifier: ^29.7.0 version: 29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@swc/core@1.15.1)(@types/node@22.19.0)(typescript@5.9.3)) prettier: specifier: ^3.6.2 version: 3.6.2 + prettier-plugin-svelte: + specifier: ^3.4.0 + version: 3.4.0(prettier@3.6.2)(svelte@4.2.20) + sass: + specifier: ^1.80.0 + version: 1.93.3 + svelte-check: + specifier: ^3.6.9 + version: 3.8.6(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.1)(@types/node@22.19.0)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.93.3)(svelte@4.2.20) + svelte-eslint-parser: + specifier: ^0.33.1 + version: 0.33.1(svelte@4.2.20) + svelte-loader: + specifier: ^3.2.0 + version: 3.2.4(svelte@4.2.20) + svelte-preprocess: + specifier: ^5.1.4 + version: 5.1.4(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.1)(@types/node@22.19.0)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.93.3)(svelte@4.2.20)(typescript@5.9.3) ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@swc/core@1.15.1)(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 diff --git a/models/card/src/index.ts b/models/card/src/index.ts index b3a3a59426..25f0c60e2d 100644 --- a/models/card/src/index.ts +++ b/models/card/src/index.ts @@ -728,6 +728,60 @@ export function createModel (builder: Builder): void { card.viewlet.CardRelationshipTable ) + builder.createDoc( + presentation.class.ComponentPointExtension, + core.space.Model, + { + extension: converter.extensions.CopyAsMarkdownAction, + component: converter.component.CopyAsMarkdownButton + }, + converter.extensions.CopyAsMarkdownButton + ) + + builder.createDoc( + view.class.ViewletViewAction, + core.space.Model, + { + descriptor: view.viewlet.RelationshipTable, + extension: converter.extensions.CopyAsMarkdownAction, + applicableToClass: card.class.Card + }, + card.specialViewAction.CardRelationshipTable + ) + + builder.createDoc( + view.class.ViewletViewAction, + core.space.Model, + { + descriptor: view.viewlet.Table, + extension: converter.extensions.CopyAsMarkdownAction, + applicableToClass: card.class.Card + }, + card.specialViewAction.CardTable + ) + + builder.createDoc( + view.class.ViewletViewAction, + core.space.Model, + { + descriptor: view.viewlet.Table, + extension: converter.extensions.CopyAsMarkdownAction, + applicableToClass: card.class.Card + }, + card.specialViewAction.CopyAsMarkdownTable + ) + + builder.createDoc( + view.class.ViewletViewAction, + core.space.Model, + { + descriptor: view.viewlet.RelationshipTable, + extension: converter.extensions.CopyAsMarkdownAction, + applicableToClass: card.class.Card + }, + card.specialViewAction.CopyAsMarkdownRelationshipTable + ) + builder.mixin(card.class.Card, core.class.Class, view.mixin.ObjectPresenter, { presenter: card.component.CardPresenter }) diff --git a/models/card/src/plugin.ts b/models/card/src/plugin.ts index 1db60f2a06..09c392a755 100644 --- a/models/card/src/plugin.ts +++ b/models/card/src/plugin.ts @@ -13,6 +13,7 @@ // limitations under the License. // +import type { ViewletViewAction, Action, ActionCategory, ViewAction } from '@hcengineering/view' import { type Card, cardId } from '@hcengineering/card' import card from '@hcengineering/card-resources/src/plugin' import type { Client, Doc, Ref } from '@hcengineering/core' @@ -20,7 +21,6 @@ import {} from '@hcengineering/core' import { mergeIds, type Resource } from '@hcengineering/platform' import { type TagCategory } from '@hcengineering/tags' import { type Location, type ResolvedLocation } from '@hcengineering/ui/src/types' -import { type Action, type ActionCategory, type ViewAction } from '@hcengineering/view' import { type LocationData } from '@hcengineering/workbench' import { type NotificationGroup, type NotificationType } from '@hcengineering/notification' @@ -44,6 +44,12 @@ export default mergeIds(cardId, card, { Card: '' as Ref, Labels: '' as Ref }, + specialViewAction: { + CardRelationshipTable: '' as Ref, + CardTable: '' as Ref, + CopyAsMarkdownTable: '' as Ref, + CopyAsMarkdownRelationshipTable: '' as Ref + }, ids: { MasterTags: '' as Ref, ManageMasterTags: '' as Ref, diff --git a/models/controlled-documents/src/index.ts b/models/controlled-documents/src/index.ts index d5954e059c..1268bf8c0c 100644 --- a/models/controlled-documents/src/index.ts +++ b/models/controlled-documents/src/index.ts @@ -21,6 +21,7 @@ import documentsPlugin, { DocumentState, type Document, type DocumentSpace, + type DocumentTemplate, type ProjectDocument, type ChangeControl, type DocumentRequest @@ -43,7 +44,7 @@ import setting from '@hcengineering/setting' import tags from '@hcengineering/tags' import textEditor from '@hcengineering/text-editor' -import { AccountRole, type ClassCollaborators, type Class, type Doc, type Ref } from '@hcengineering/core' +import { AccountRole, type ClassCollaborators, type Class, type Doc, type Lookup, type Ref } from '@hcengineering/core' import { type Action } from '@hcengineering/view' import { definePermissions } from './permissions' import documents from './plugin' @@ -255,6 +256,11 @@ export function createModel (builder: Builder): void { ) // Workflow + const documentTableLookup: Lookup = { + owner: contact.mixin.Employee, + category: documents.class.DocumentCategory, + template: documents.mixin.DocumentTemplate + } builder.createDoc( view.class.Viewlet, core.space.Model, @@ -308,16 +314,27 @@ export function createModel (builder: Builder): void { 'modifiedOn' ], options: { - lookup: { - owner: contact.mixin.Employee, - category: documents.class.DocumentCategory, - template: documents.mixin.DocumentTemplate - } + lookup: documentTableLookup } }, documents.viewlet.TableDocument ) + builder.createDoc( + view.class.ViewletViewAction, + core.space.Model, + { + descriptor: view.viewlet.Table, + extension: converter.extensions.CopyAsMarkdownAction, + applicableToClass: documents.class.Document + }, + documents.specialViewAction.TableDocument + ) + + const documentTemplateTableLookup: Lookup = { + owner: contact.mixin.Employee, + category: documents.class.DocumentCategory + } builder.createDoc( view.class.Viewlet, core.space.Model, @@ -367,10 +384,7 @@ export function createModel (builder: Builder): void { hiddenKeys: ['attachedTo'] }, options: { - lookup: { - owner: contact.mixin.Employee, - category: documents.class.DocumentCategory - } + lookup: documentTemplateTableLookup } }, documents.viewlet.TableDocumentTemplate diff --git a/models/controlled-documents/src/plugin.ts b/models/controlled-documents/src/plugin.ts index e0b5eca013..b82f64e56a 100644 --- a/models/controlled-documents/src/plugin.ts +++ b/models/controlled-documents/src/plugin.ts @@ -20,7 +20,7 @@ import { type ObjectSearchCategory, type ObjectSearchFactory } from '@hcengineer import { mergeIds, type Resource } from '@hcengineering/platform' import { type TagCategory } from '@hcengineering/tags' import { type AnyComponent } from '@hcengineering/ui/src/types' -import { type ActionCategory, type ViewAction } from '@hcengineering/view' +import type { ActionCategory, ViewAction, Viewlet, ViewletViewAction } from '@hcengineering/view' import { type NotificationType, type NotificationGroup } from '@hcengineering/notification' import { type TextActionVisibleFunction, type TextActionFunction } from '@hcengineering/text-editor' @@ -78,11 +78,14 @@ export default mergeIds(documentsId, documents, { TransferDocument: '' as ViewAction, EditDocSpace: '' as ViewAction }, + specialViewAction: { + TableDocument: '' as Ref + }, viewlet: { - TableDocument: '' as Ref, - ListDocument: '' as Ref, - TableDocumentTemplate: '' as Ref, - TableDocumentDomain: '' as Ref + TableDocument: '' as Ref, + ListDocument: '' as Ref, + TableDocumentTemplate: '' as Ref, + TableDocumentDomain: '' as Ref }, notification: { DocumentsNotificationGroup: '' as Ref, diff --git a/models/recruit/src/index.ts b/models/recruit/src/index.ts index 1764fd6050..c210f996b7 100644 --- a/models/recruit/src/index.ts +++ b/models/recruit/src/index.ts @@ -323,14 +323,13 @@ export function createModel (builder: Builder): void { } } builder.createDoc( - workbench.class.ViewletSpecialViewAction, + view.class.ViewletViewAction, core.space.Model, { - attachedTo: recruit.viewlet.TableCandidate, - attachedToClass: view.class.Viewlet, - collection: 'specialViewActions', + descriptor: view.viewlet.Table, extension: workbench.extensions.SpecialViewAction, - config: tableCandidateExportConfig + config: tableCandidateExportConfig, + applicableToClass: recruit.mixin.Candidate }, recruit.specialViewAction.TableCandidate ) diff --git a/models/recruit/src/plugin.ts b/models/recruit/src/plugin.ts index 729bfc5be2..1d5e2c9547 100644 --- a/models/recruit/src/plugin.ts +++ b/models/recruit/src/plugin.ts @@ -23,14 +23,14 @@ import { recruitId } from '@hcengineering/recruit' import recruit from '@hcengineering/recruit-resources/src/plugin' import { type ProjectType, type TaskTypeDescriptor } from '@hcengineering/task' import type { AnyComponent, Location } from '@hcengineering/ui/src/types' -import type { ViewletSpecialViewAction } from '@hcengineering/workbench' import type { Action, ActionCategory, ViewAction, ViewActionAvailabilityFunction, ViewQueryAction, - Viewlet + Viewlet, + ViewletViewAction } from '@hcengineering/view' export default mergeIds(recruitId, recruit, { @@ -134,7 +134,7 @@ export default mergeIds(recruitId, recruit, { DefaultVacancy: '' as Ref }, specialViewAction: { - TableCandidate: '' as Ref + TableCandidate: '' as Ref }, viewlet: { TableCandidate: '' as Ref, diff --git a/models/tracker/src/index.ts b/models/tracker/src/index.ts index 26558b5c57..1f45a0e400 100644 --- a/models/tracker/src/index.ts +++ b/models/tracker/src/index.ts @@ -487,6 +487,17 @@ export function createModel (builder: Builder): void { defineViewlets(builder) + builder.createDoc( + view.class.ViewletViewAction, + core.space.Model, + { + descriptor: view.viewlet.List, + extension: converter.extensions.CopyAsMarkdownAction, + applicableToClass: tracker.class.Issue + }, + tracker.specialViewAction.IssueList + ) + const issuesId = 'issues' const componentsId = 'components' const milestonesId = 'milestones' diff --git a/models/tracker/src/plugin.ts b/models/tracker/src/plugin.ts index 9c84de8eac..4c27d0a153 100644 --- a/models/tracker/src/plugin.ts +++ b/models/tracker/src/plugin.ts @@ -22,7 +22,7 @@ import { type ProjectType } from '@hcengineering/task' import { trackerId } from '@hcengineering/tracker' import tracker from '@hcengineering/tracker-resources/src/plugin' import type { AnyComponent } from '@hcengineering/ui/src/types' -import { type Action, type ViewAction, type Viewlet } from '@hcengineering/view' +import { type Action, type ViewAction, type Viewlet, type ViewletViewAction } from '@hcengineering/view' import { type Application } from '@hcengineering/workbench' export default mergeIds(trackerId, tracker, { @@ -63,6 +63,9 @@ export default mergeIds(trackerId, tracker, { app: { Tracker: '' as Ref }, + specialViewAction: { + IssueList: '' as Ref + }, viewlet: { IssueList: '' as Ref, IssueTemplateList: '' as Ref, diff --git a/models/view/src/index.ts b/models/view/src/index.ts index e92af38b33..d0afec43fc 100644 --- a/models/view/src/index.ts +++ b/models/view/src/index.ts @@ -29,12 +29,17 @@ import { type Ref, type Space } from '@hcengineering/core' -import { type Builder, Mixin, Model, UX } from '@hcengineering/model' +import { type Builder, Mixin, Model, Prop, TypeRecord, TypeRef, TypeString, UX } from '@hcengineering/model' import core, { TClass, TDoc } from '@hcengineering/model-core' import preference, { TPreference } from '@hcengineering/model-preference' import presentation from '@hcengineering/model-presentation' -import { type Asset, type IntlString, type Resource, type Status } from '@hcengineering/platform' -import { type AnyComponent, type LabelAndProps, type Location } from '@hcengineering/ui/src/types' +import { type Asset, getEmbeddedLabel, type IntlString, type Resource, type Status } from '@hcengineering/platform' +import { + type AnyComponent, + type ComponentExtensionId, + type LabelAndProps, + type Location +} from '@hcengineering/ui/src/types' import { type TypeEditor, type Action, @@ -99,7 +104,8 @@ import { type ViewOptionsModel, type Viewlet, type ViewletDescriptor, - type ViewletPreference + type ViewletPreference, + type ViewletViewAction } from '@hcengineering/view' import view from './plugin' @@ -318,6 +324,28 @@ export class TViewletDescriptor extends TDoc implements ViewletDescriptor { label!: IntlString } +@Model(view.class.ViewletViewAction, core.class.Doc, DOMAIN_MODEL) +@UX(view.string.ViewletViewAction) +export class TViewletViewAction extends TDoc implements ViewletViewAction { + @Prop(TypeRef(view.class.Viewlet), getEmbeddedLabel('Viewlet')) + declare viewlet?: Ref + + @Prop(TypeRef(view.class.ViewletDescriptor), getEmbeddedLabel('Descriptor')) + declare descriptor?: Ref + + @Prop(TypeString(), getEmbeddedLabel('Extension')) + declare extension: ComponentExtensionId + + @Prop(TypeRecord(), getEmbeddedLabel('Config')) + declare config?: Record + + @Prop(TypeRef(core.class.Class), getEmbeddedLabel('ApplicableToClass')) + declare applicableToClass?: Ref> + + @Prop(TypeRef(core.class.Class), getEmbeddedLabel('DisabledForClass')) + declare disabledForClass?: Ref> +} + @Model(view.class.Viewlet, core.class.Doc, DOMAIN_MODEL) export class TViewlet extends TDoc implements Viewlet { attachTo!: Ref> @@ -472,6 +500,7 @@ export function createModel (builder: Builder): void { TViewletPreference, TViewletDescriptor, TViewlet, + TViewletViewAction, TAction, TActionCategory, TObjectValidator, diff --git a/models/workbench/src/index.ts b/models/workbench/src/index.ts index b2c2fa7c1f..a23d9ba355 100644 --- a/models/workbench/src/index.ts +++ b/models/workbench/src/index.ts @@ -14,26 +14,25 @@ // import { AccountRole, type AccountUuid, type Class, DOMAIN_MODEL, type Ref, type Space } from '@hcengineering/core' -import { type Builder, Mixin, Model, Prop, TypeRecord, TypeRef, TypeString, UX } from '@hcengineering/model' +import { type Builder, Mixin, Model, Prop, TypeRef, UX } from '@hcengineering/model' import preference, { TPreference } from '@hcengineering/model-preference' import { createAction } from '@hcengineering/model-view' -import core, { TAttachedDoc, TClass, TDoc } from '@hcengineering/model-core' +import core, { TClass, TDoc } from '@hcengineering/model-core' import { type Asset, getEmbeddedLabel, type IntlString, type Resource } from '@hcengineering/platform' -import view, { type KeyBinding, type Viewlet } from '@hcengineering/view' +import view, { type KeyBinding } from '@hcengineering/view' import type { Application, ApplicationNavModel, HiddenApplication, SpaceView, ViewConfiguration, - ViewletSpecialViewAction, Widget, WidgetPreference, WidgetTab, WidgetType, WorkbenchTab } from '@hcengineering/workbench' -import { type AnyComponent, type ComponentExtensionId } from '@hcengineering/ui/src/types' +import { type AnyComponent } from '@hcengineering/ui/src/types' import presentation from '@hcengineering/model-presentation' import workbench from './plugin' @@ -106,29 +105,10 @@ export class TWorkbenchTab extends TPreference implements WorkbenchTab { isPinned!: boolean } -@Model(workbench.class.ViewletSpecialViewAction, core.class.AttachedDoc, DOMAIN_MODEL) -@UX(workbench.string.ViewletSpecialViewAction) -export class TViewletSpecialViewAction extends TAttachedDoc implements ViewletSpecialViewAction { - @Prop(TypeRef(view.class.Viewlet), core.string.AttachedTo) - declare attachedTo: Ref - - @Prop(TypeRef(view.class.Viewlet), core.string.AttachedToClass) - declare attachedToClass: Ref> - - override collection: 'specialViewActions' = 'specialViewActions' - - @Prop(TypeString(), getEmbeddedLabel('Extension')) - declare extension: ComponentExtensionId - - @Prop(TypeRecord(), getEmbeddedLabel('Config')) - declare config?: Record -} - export function createModel (builder: Builder): void { builder.createModel( TApplication, TSpaceView, - TViewletSpecialViewAction, THiddenApplication, TApplicationNavModel, TWidget, diff --git a/plugins/converter-resources/.eslintrc.js b/plugins/converter-resources/.eslintrc.js index 72235dc283..bb8fd7450d 100644 --- a/plugins/converter-resources/.eslintrc.js +++ b/plugins/converter-resources/.eslintrc.js @@ -1,7 +1,4 @@ module.exports = { - extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], - parserOptions: { - tsconfigRootDir: __dirname, - project: './tsconfig.json' - } + extends: ['./node_modules/@hcengineering/platform-rig/profiles/ui/eslint.config.json'], + parserOptions: { tsconfigRootDir: __dirname } } diff --git a/plugins/converter-resources/.prettierrc b/plugins/converter-resources/.prettierrc new file mode 100644 index 0000000000..792942803a --- /dev/null +++ b/plugins/converter-resources/.prettierrc @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "trailingComma": "none", + "tabWidth": 2, + "semi": false, + "singleQuote": true, + "printWidth": 120, + "useTabs": false, + "bracketSpacing": true, + "proseWrap": "preserve", + "plugins": [ + "prettier-plugin-svelte" + ], + "overrides": [ + { + "files": "*.svelte", + "options": { + "parser": "svelte" + } + } + ] +} \ No newline at end of file diff --git a/plugins/converter-resources/package.json b/plugins/converter-resources/package.json index 73b221692f..733d874c75 100644 --- a/plugins/converter-resources/package.json +++ b/plugins/converter-resources/package.json @@ -1,21 +1,18 @@ { "name": "@hcengineering/converter-resources", "version": "0.7.0", - "main": "lib/index.js", + "main": "src/index.ts", "svelte": "src/index.ts", "types": "types/index.d.ts", - "files": [ - "lib/**/*", - "types/**/*", - "tsconfig.json" - ], "author": "Copyright © Hardcore Engineering Inc.", "license": "EPL-2.0", "scripts": { - "build": "compile", + "build": "compile ui", "format": "format src", - "build:watch": "compile", - "_phase:build": "compile transpile src", + "svelte-check": "do-svelte-check", + "_phase:svelte-check": "do-svelte-check", + "build:watch": "compile ui", + "_phase:build": "compile ui", "_phase:format": "format src", "_phase:validate": "compile validate", "_phase:test": "jest --passWithNoTests --silent", @@ -33,8 +30,15 @@ "typescript": "^5.9.3", "eslint": "^8.54.0", "jest": "^29.7.0", + "svelte-check": "^3.6.9", + "svelte-eslint-parser": "^0.33.1", + "svelte-loader": "^3.2.0", + "svelte-preprocess": "^5.1.4", "ts-jest": "^29.1.1", - "@types/jest": "^29.5.5" + "@types/jest": "^29.5.5", + "sass": "^1.80.0", + "eslint-plugin-svelte": "^2.35.1", + "prettier-plugin-svelte": "^3.4.0" }, "dependencies": { "@hcengineering/platform": "workspace:^0.7.19", @@ -45,6 +49,7 @@ "@hcengineering/ui": "workspace:^0.7.0", "@hcengineering/theme": "workspace:^0.7.0", "@hcengineering/contact": "workspace:^0.7.0", - "@hcengineering/view-resources": "workspace:^0.7.0" + "@hcengineering/view-resources": "workspace:^0.7.0", + "svelte": "^4.2.20" } } diff --git a/plugins/converter-resources/postcss.config.js b/plugins/converter-resources/postcss.config.js new file mode 100644 index 0000000000..88752c6cb0 --- /dev/null +++ b/plugins/converter-resources/postcss.config.js @@ -0,0 +1,5 @@ +module.exports = { + plugins: [ + require('autoprefixer') + ] +} diff --git a/plugins/converter-resources/src/__tests__/copyAsMarkdownTable.test.ts b/plugins/converter-resources/src/__tests__/copyAsMarkdownTable.test.ts index d962978a5d..a54f2b415d 100644 --- a/plugins/converter-resources/src/__tests__/copyAsMarkdownTable.test.ts +++ b/plugins/converter-resources/src/__tests__/copyAsMarkdownTable.test.ts @@ -13,7 +13,8 @@ // limitations under the License. // -import { buildMarkdownTableFromDocs, copyAsMarkdownTable, isIntlString } from '../index' +import { buildMarkdownTableFromDocs, copyAsMarkdownTable } from '../markdown' +import { isIntlString } from '../formatter' import type { Doc, Class, Ref } from '@hcengineering/core' import type { CopyAsMarkdownTableProps } from '../types' import { getClient } from '@hcengineering/presentation' diff --git a/plugins/converter-resources/src/components/CopyAsMarkdownButton.svelte b/plugins/converter-resources/src/components/CopyAsMarkdownButton.svelte new file mode 100644 index 0000000000..ff75df291c --- /dev/null +++ b/plugins/converter-resources/src/components/CopyAsMarkdownButton.svelte @@ -0,0 +1,46 @@ + + + +{#if hasData} + +
- {/if} {/if} diff --git a/plugins/view-resources/src/components/SpaceHeader.svelte b/plugins/view-resources/src/components/SpaceHeader.svelte index fd6e6d655f..9926abc94c 100644 --- a/plugins/view-resources/src/components/SpaceHeader.svelte +++ b/plugins/view-resources/src/components/SpaceHeader.svelte @@ -2,7 +2,9 @@ import { Class, Doc, DocumentQuery, Ref, Space, WithLookup } from '@hcengineering/core' import { IModeSelector, ModeSelector, SearchInput, Header, Breadcrumb, HeaderAdaptive } from '@hcengineering/ui' import type { Asset } from '@hcengineering/platform' - import { Viewlet } from '@hcengineering/view' + import { ComponentExtensions, getClient } from '@hcengineering/presentation' + import view, { Viewlet, type ViewletViewAction } from '@hcengineering/view' + import { getViewletSpecialActions } from '../viewletUtils' import ViewletSelector from './ViewletSelector.svelte' import FilterButton from './filter/FilterButton.svelte' @@ -17,8 +19,11 @@ export let showLabelSelector = false export let modeSelectorProps: IModeSelector | undefined = undefined export let adaptive: HeaderAdaptive = 'doubleRow' + export let resultQuery: DocumentQuery = {} let scroller: HTMLElement + + $: viewletActions = viewlet != null ? getViewletSpecialActions(getClient(), viewlet) : []
+ {#each viewletActions as action (action._id)} + + {/each} diff --git a/plugins/view-resources/src/components/ViewletPanelHeader.svelte b/plugins/view-resources/src/components/ViewletPanelHeader.svelte index 2e44da02c6..96a528ed10 100644 --- a/plugins/view-resources/src/components/ViewletPanelHeader.svelte +++ b/plugins/view-resources/src/components/ViewletPanelHeader.svelte @@ -16,7 +16,9 @@ SearchInput, themeStore } from '@hcengineering/ui' - import { ViewOptions, Viewlet, ViewletPreference } from '@hcengineering/view' + import view, { ViewOptions, Viewlet, ViewletPreference, type ViewletViewAction } from '@hcengineering/view' + import { ComponentExtensions, getClient } from '@hcengineering/presentation' + import { getViewletSpecialActions } from '../viewletUtils' import FilterBar from './filter/FilterBar.svelte' import FilterButton from './filter/FilterButton.svelte' @@ -51,6 +53,8 @@ $: searchQuery = search === '' ? { ...query } : { ...query, $search: search } $: hideExtra = modeSelectorProps === undefined && $$slots.extra === undefined + + $: viewletActions = viewlet != null ? getViewletSpecialActions(getClient(), viewlet) : []
@@ -67,6 +71,16 @@ + {#each viewletActions as action (action._id)} + + {/each} diff --git a/plugins/view-resources/src/index.ts b/plugins/view-resources/src/index.ts index ccfadf3e02..9812d1b79b 100644 --- a/plugins/view-resources/src/index.ts +++ b/plugins/view-resources/src/index.ts @@ -217,6 +217,7 @@ export { } from './utils' export * from './viewOptions' export * from './viewletContextStore' +export { getViewletSpecialActions } from './viewletUtils' export { copyMarkdown } from './actionImpl' export { default as SimpleNotification } from './components/SimpleNotification.svelte' export type { BuildMarkdownTableMetadata } from '@hcengineering/view' diff --git a/plugins/view-resources/src/viewletContextStore.ts b/plugins/view-resources/src/viewletContextStore.ts index 1fa7e0616a..c0026986d7 100644 --- a/plugins/view-resources/src/viewletContextStore.ts +++ b/plugins/view-resources/src/viewletContextStore.ts @@ -1,6 +1,7 @@ import { type Class, type Doc, type DocumentQuery, type Ref } from '@hcengineering/core' import { type BuildModelKey, type Viewlet, type ViewOptions } from '@hcengineering/view' import { writable } from 'svelte/store' +import type { CopyRelationshipTableAsMarkdownProps } from '@hcengineering/converter' /** * Context for the current viewlet table being displayed @@ -14,6 +15,7 @@ export interface ViewletContext { query?: DocumentQuery viewOptions?: ViewOptions _class?: Ref> + relationshipTableData?: CopyRelationshipTableAsMarkdownProps } /** diff --git a/plugins/view-resources/src/viewletUtils.ts b/plugins/view-resources/src/viewletUtils.ts new file mode 100644 index 0000000000..315000e490 --- /dev/null +++ b/plugins/view-resources/src/viewletUtils.ts @@ -0,0 +1,63 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// + +import type { Client, Ref } from '@hcengineering/core' +import view, { type Viewlet, type ViewletViewAction } from '@hcengineering/view' + +/** Shape used by the header UI; both viewlet- and descriptor-scoped actions provide this. */ +export interface ResolvedViewletSpecialAction { + _id: Ref + extension: ViewletViewAction['extension'] + config?: Record +} + +/** + * Resolve special view actions for a viewlet by: + * 1. One query: ViewletViewAction where viewlet is in (current + template viewlets) OR descriptor matches. + * 2. For rows with descriptor set, filter by applicableToClass and disabledForClass. + * Actions are deduplicated by extension (viewlet-scoped take precedence). + */ +export function getViewletSpecialActions (client: Client, viewlet: Viewlet): ResolvedViewletSpecialAction[] { + const model = client.getModel() + const hierarchy = client.getHierarchy() + + let actions = model.findAllSync(view.class.ViewletViewAction, { + viewlet: viewlet._id + }) as ViewletViewAction[] + if (actions == null || actions.length === 0) { + actions = model.findAllSync(view.class.ViewletViewAction, { + descriptor: viewlet.descriptor + }) as ViewletViewAction[] + } + + if (actions == null || actions.length === 0) { + return [] + } + + const filtered = actions.filter((a) => { + if (a.descriptor != null) { + if (a.applicableToClass != null && !hierarchy.isDerived(viewlet.attachTo, a.applicableToClass)) { + return false + } + if (a.disabledForClass != null && hierarchy.isDerived(viewlet.attachTo, a.disabledForClass)) { + return false + } + } + return true + }) + + const seen = new Set() + const result: ResolvedViewletSpecialAction[] = [] + const byViewlet = filtered.filter((a) => a.viewlet != null) + const byDescriptor = filtered.filter((a) => a.descriptor != null) + for (const a of [...byViewlet, ...byDescriptor]) { + if (seen.has(a.extension)) continue + seen.add(a.extension) + result.push({ _id: a._id, extension: a.extension, config: a.config }) + } + return result +} diff --git a/plugins/view/src/index.ts b/plugins/view/src/index.ts index 7d29c0ab42..efb4a29923 100644 --- a/plugins/view/src/index.ts +++ b/plugins/view/src/index.ts @@ -66,7 +66,8 @@ import { ViewAction, Viewlet, ViewletDescriptor, - ViewletPreference + ViewletPreference, + ViewletViewAction } from './types' export * from './types' @@ -126,6 +127,7 @@ const view = plugin(viewId, { ViewletPreference: '' as Ref>, ViewletDescriptor: '' as Ref>, Viewlet: '' as Ref>, + ViewletViewAction: '' as Ref>, Action: '' as Ref>, ActionCategory: '' as Ref>, LinkPresenter: '' as Ref>, @@ -224,8 +226,10 @@ const view = plugin(viewId, { Subscribed: '' as IntlString, HyperlinkPlaceholder: '' as IntlString, CopyToClipboard: '' as IntlString, + CopyAll: '' as IntlString, NoGrouping: '' as IntlString, Type: '' as IntlString, + ViewletViewAction: '' as IntlString, UnArchive: '' as IntlString, Archive: '' as IntlString, Save: '' as IntlString, diff --git a/plugins/view/src/types.ts b/plugins/view/src/types.ts index fde684d54b..bb3b8f0e4b 100644 --- a/plugins/view/src/types.ts +++ b/plugins/view/src/types.ts @@ -46,6 +46,7 @@ import { Preference } from '@hcengineering/preference' import { AnyComponent, AnySvelteComponent, + type ComponentExtensionId, Location, Location as PlatformLocation, type LabelAndProps @@ -458,6 +459,24 @@ export interface ViewletConfigOptions { sortable?: boolean } +/** + * Special view action shown in the viewlet header. Exactly one of viewlet or descriptor must be set. + * When descriptor is set, scope is filtered by applicableToClass / disabledForClass. + * @public + */ +export interface ViewletViewAction extends Doc { + /** When set, action applies to this viewlet (and template viewlets). */ + viewlet?: Ref + /** When set, action applies to viewlets with this descriptor; use applicableToClass / disabledForClass to scope. */ + descriptor?: Ref + extension: ComponentExtensionId + config?: Record + /** When descriptor is set: show only when viewlet.attachTo is this class or a subclass. */ + applicableToClass?: Ref> + /** When descriptor is set: hide when viewlet.attachTo is this class or a subclass. */ + disabledForClass?: Ref> +} + /** * @public */ diff --git a/plugins/workbench-resources/src/components/SpecialView.svelte b/plugins/workbench-resources/src/components/SpecialView.svelte index 08b39af8e1..b3c04abe6b 100644 --- a/plugins/workbench-resources/src/components/SpecialView.svelte +++ b/plugins/workbench-resources/src/components/SpecialView.svelte @@ -36,10 +36,11 @@ FilterButton, getResultOptions, getResultQuery, + getViewletSpecialActions, ViewletSelector, ViewletSettingButton } from '@hcengineering/view-resources' - import workbench, { type ParentsNavigationModel, type ViewletSpecialViewAction } from '@hcengineering/workbench' + import workbench, { type ParentsNavigationModel } from '@hcengineering/workbench' import ComponentNavigator from './ComponentNavigator.svelte' import { deepEqual } from 'fast-equals' import { ComponentType } from 'svelte' @@ -94,12 +95,7 @@ $: void updateQuery(_baseQuery, viewOptions, viewlet, queryBuilder) $: void updateOptions(viewlet?.options, viewOptions, viewlet) - $: viewletActions = - viewlet != null - ? (client.getModel().findAllSync(workbench.class.ViewletSpecialViewAction, { - attachedTo: viewlet._id - }) as ViewletSpecialViewAction[]) - : [] + $: viewletActions = viewlet != null ? getViewletSpecialActions(client, viewlet) : [] async function updateOptions ( _options: FindOptions | undefined, @@ -141,7 +137,9 @@
diff --git a/plugins/workbench/src/index.ts b/plugins/workbench/src/index.ts index fca04faa16..735411078c 100644 --- a/plugins/workbench/src/index.ts +++ b/plugins/workbench/src/index.ts @@ -17,7 +17,6 @@ import { workbenchId, workbenchPlugin } from './plugin' export * from './analytics' export * from './types' -export type { ViewletSpecialViewAction } from './types' export * from './utils' export { workbenchId } diff --git a/plugins/workbench/src/plugin.ts b/plugins/workbench/src/plugin.ts index 7e1233090a..47313e82ea 100644 --- a/plugins/workbench/src/plugin.ts +++ b/plugins/workbench/src/plugin.ts @@ -25,7 +25,6 @@ import type { ApplicationNavModel, HiddenApplication, SpaceView, - ViewletSpecialViewAction, Widget, WidgetPreference, WidgetTab, @@ -43,8 +42,7 @@ export const workbenchPlugin = plugin(workbenchId, { HiddenApplication: '' as Ref>, Widget: '' as Ref>, WidgetPreference: '' as Ref>, - WorkbenchTab: '' as Ref>, - ViewletSpecialViewAction: '' as Ref> + WorkbenchTab: '' as Ref> }, mixin: { SpaceView: '' as Ref> @@ -61,7 +59,6 @@ export const workbenchPlugin = plugin(workbenchId, { string: { Archive: '' as IntlString, View: '' as IntlString, - ViewletSpecialViewAction: '' as IntlString, ServerUnderMaintenance: '' as IntlString, UpgradeDownloadProgress: '' as IntlString, OpenInSidebar: '' as IntlString, diff --git a/plugins/workbench/src/types.ts b/plugins/workbench/src/types.ts index 4ec59807fa..f15bf52081 100644 --- a/plugins/workbench/src/types.ts +++ b/plugins/workbench/src/types.ts @@ -14,28 +14,11 @@ // limitations under the License. // -import type { - AccountRole, - AccountUuid, - AttachedDoc, - Class, - Doc, - DocumentQuery, - Obj, - Ref, - Space -} from '@hcengineering/core' -import type { Viewlet } from '@hcengineering/view' +import type { AccountRole, AccountUuid, Class, Doc, DocumentQuery, Obj, Ref, Space } from '@hcengineering/core' import { DocNotifyContext, InboxNotification } from '@hcengineering/notification' import type { Asset, IntlString, Resource } from '@hcengineering/platform' import type { Preference } from '@hcengineering/preference' -import { - AnyComponent, - type AnySvelteComponent, - type ComponentExtensionId, - Location, - ResolvedLocation -} from '@hcengineering/ui' +import { AnyComponent, type AnySvelteComponent, Location, ResolvedLocation } from '@hcengineering/ui' /** @public */ export interface LocationData { @@ -229,9 +212,3 @@ export interface ViewConfiguration { export interface SpaceView extends Class { view: ViewConfiguration } - -/** @public */ -export interface ViewletSpecialViewAction extends AttachedDoc { - extension: ComponentExtensionId - config?: Record -} From 55eb570884935f987122a84845834b44cc65eeff Mon Sep 17 00:00:00 2001 From: Artyom Savchenko Date: Fri, 13 Feb 2026 17:32:10 +0700 Subject: [PATCH 4/4] Fix email notifications for export (#10506) Signed-off-by: Artem Savchenko --- common/config/rush/pnpm-lock.yaml | 6 + models/export/src/index.ts | 7 +- server-plugins/gmail-resources/src/index.ts | 109 +++++++++++++++--- .../notification-resources/src/index.ts | 13 ++- server/server-pipeline/package.json | 2 + .../src/internationalization.ts | 3 + .../export/pod-export/src/notifications.ts | 1 + 7 files changed, 124 insertions(+), 17 deletions(-) diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 92d7848f7b..31464f8849 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -36578,6 +36578,12 @@ importers: '@hcengineering/drive-assets': specifier: workspace:^0.7.0 version: link:../../plugins/drive-assets + '@hcengineering/export': + specifier: workspace:^0.7.0 + version: link:../../plugins/export + '@hcengineering/export-assets': + specifier: workspace:^0.7.0 + version: link:../../plugins/export-assets '@hcengineering/github': specifier: workspace:^0.7.0 version: link:../../services/github/github diff --git a/models/export/src/index.ts b/models/export/src/index.ts index 05cb3384b1..14fc130562 100644 --- a/models/export/src/index.ts +++ b/models/export/src/index.ts @@ -89,7 +89,12 @@ export function createModel (builder: Builder): void { group: exportPlugin.ids.ImportNotificationGroup, txClasses: [], objectClass: exportPlugin.class.ExportResultRecord, - defaultEnabled: true + defaultEnabled: true, + templates: { + textTemplate: '{body}', + htmlTemplate: '

{body}

{link}

', + subjectTemplate: '{title}' + } }, exportPlugin.ids.ImportedDocumentsNotification ) diff --git a/server-plugins/gmail-resources/src/index.ts b/server-plugins/gmail-resources/src/index.ts index cc62d3db4d..d8fbbf0c4c 100644 --- a/server-plugins/gmail-resources/src/index.ts +++ b/server-plugins/gmail-resources/src/index.ts @@ -44,7 +44,10 @@ import serverNotification from '@hcengineering/server-notification' import { AvailableProvidersCache, AvailableProvidersCacheKey, - getContentByTemplate + getContentByTemplate, + getNotificationProviderControl, + getReceiversInfo, + getAllowedProviders } from '@hcengineering/server-notification-resources' import { getMetadata } from '@hcengineering/platform' import activity, { ActivityMessage } from '@hcengineering/activity' @@ -156,7 +159,7 @@ async function notifyByEmail ( senderSocialId: PersonId, email: string, data: InboxNotification, - message: ActivityMessage + message?: ActivityMessage ): Promise { let senderName = sender !== undefined ? formatName(sender.name, control.branding?.lastNameFirst) : '' if (senderName === '' && senderSocialId === core.account.System) { @@ -166,6 +169,12 @@ async function notifyByEmail ( if (content !== undefined) { await sendEmailNotification(control.ctx, content.text, content.html, content.subject, email) + } else { + control.ctx.info('notifyByEmail: getContentByTemplate returned undefined, email not sent', { + notificationId: data._id, + type, + docClass: doc._class + }) } } @@ -197,7 +206,10 @@ async function processEmailNotifications (control: TriggerControl, notifications const docId = notifications[0].objectId const docClass = notifications[0].objectClass const doc = (await control.findAll(control.ctx, docClass, { _id: docId }))[0] - if (doc === undefined) return + if (doc === undefined) { + control.ctx.info('processEmailNotifications: doc not found', { docId, docClass }) + return + } const messages = await getNotificationMessages(notifications, control) const { hierarchy } = control @@ -205,7 +217,10 @@ async function processEmailNotifications (control: TriggerControl, notifications for (const n of notifications) { const type = (n.types ?? [])[0] - if (type === undefined) continue + if (type === undefined) { + control.ctx.info('processEmailNotifications: skipping notification without type', { notificationId: n._id }) + continue + } let message: ActivityMessage | undefined if (hierarchy.isDerived(n._class, notification.class.ActivityInboxNotification)) { const activityNotification = n as ActivityInboxNotification @@ -215,11 +230,22 @@ async function processEmailNotifications (control: TriggerControl, notifications if (hierarchy.isDerived(mentionNotification.mentionedInClass, activity.class.ActivityMessage)) { message = messages.find((m) => m._id === mentionNotification.mentionedIn) } + } else if (hierarchy.isDerived(n._class, notification.class.CommonInboxNotification)) { + message = undefined } - if (message === undefined) continue + if (message === undefined && !hierarchy.isDerived(n._class, notification.class.CommonInboxNotification)) { + control.ctx.info('processEmailNotifications: skipping - no ActivityMessage and not CommonInboxNotification', { + notificationId: n._id, + notificationClass: n._class + }) + continue + } const employee = await getEmployeeByAcc(control, n.user) - if (employee === undefined) continue + if (employee === undefined) { + control.ctx.info('processEmailNotifications: no employee for user', { notificationId: n._id, user: n.user }) + continue + } const emailQuery = { attachedTo: employee._id, type: { $in: [SocialIdType.EMAIL, SocialIdType.GOOGLE] }, @@ -247,30 +273,85 @@ async function processEmailNotifications (control: TriggerControl, notifications continue } - const senderSocialId = message.createdBy ?? message.modifiedBy + const senderSocialId = message !== undefined ? (message.createdBy ?? message.modifiedBy) : core.account.System const sender = senders.get(senderSocialId) ?? (await getPerson(control, senderSocialId)) if (sender != null) { senders.set(senderSocialId, sender) } + control.ctx.info('processEmailNotifications: sending email', { + notificationId: n._id, + type, + notificationClass: n._class + }) await notifyByEmail(control, type, doc, sender, senderSocialId, emails[0].value, n, message) } } +function hasEmailProvider (n: InboxNotification, availableProviders: AvailableProvidersCache): boolean { + const providers = availableProviders.get(n._id) ?? availableProviders.get(n.objectId as Ref) + return providers?.find((p) => p === gmail.providers.EmailNotificationProvider) !== undefined +} + async function NotificationsHandler (txes: TxCreateDoc[], control: TriggerControl): Promise { + control.ctx.info('NotificationsHandler: received InboxNotification txes', { + count: txes.length, + workspace: control.workspace?.url, + objectClasses: [...new Set(txes.map((tx) => tx.objectClass))] + }) + const availableProviders: AvailableProvidersCache = control.contextCache.get(AvailableProvidersCacheKey) ?? new Map() - const all: InboxNotification[] = txes - .map((tx) => TxProcessor.createDoc2Doc(tx)) - .filter( - (it) => availableProviders.get(it._id)?.find((p) => p === gmail.providers.EmailNotificationProvider) !== undefined - ) + const all: InboxNotification[] = txes.map((tx) => TxProcessor.createDoc2Doc(tx)) - if (all.length === 0) { + const notificationsWithEmail = all.filter((it) => hasEmailProvider(it, availableProviders)) + + control.ctx.info('NotificationsHandler: processing inbox notifications', { + total: all.length, + withEmailFromCache: notificationsWithEmail.length, + notificationClasses: [...new Set(all.map((n) => n._class))], + notificationTypes: [...new Set(all.flatMap((n) => n.types ?? []))] + }) + + if (notificationsWithEmail.length < all.length) { + const notificationControl = await getNotificationProviderControl(control.ctx, control) + const receivers = await getReceiversInfo(control.ctx, [...new Set(all.map((n) => n.user))], control) + const receiverByAccount = new Map(receivers.map((r) => [r.account, r])) + for (const n of all) { + if (hasEmailProvider(n, availableProviders)) continue + const type = (n.types ?? [])[0] + if (type === undefined) { + control.ctx.info('NotificationsHandler: skipping notification without type', { notificationId: n._id }) + continue + } + const notificationType = control.modelDb.getObject(type) + const receiver = receiverByAccount.get(n.user) + if (receiver === undefined) { + control.ctx.info('NotificationsHandler: no receiver info for user', { + notificationId: n._id, + type, + reason: 'user not in getReceiversInfo result' + }) + continue + } + const allowedProviders = getAllowedProviders(control, receiver.socialIds, notificationType, notificationControl) + if (allowedProviders.includes(gmail.providers.EmailNotificationProvider)) { + notificationsWithEmail.push(n) + } else { + control.ctx.info('NotificationsHandler: email provider not enabled for notification type', { + notificationId: n._id, + type + }) + } + } + } + + if (notificationsWithEmail.length === 0) { + control.ctx.info('NotificationsHandler: no notifications with email provider, skipping') return [] } - const notificationsByDocId = groupByArray(all, (n) => n.objectId) + const notificationsByDocId = groupByArray(notificationsWithEmail, (n) => n.objectId) await Promise.all( Array.from(notificationsByDocId.entries()).map(([docId, notifications]) => diff --git a/server-plugins/notification-resources/src/index.ts b/server-plugins/notification-resources/src/index.ts index f79922f42d..4a29f17781 100644 --- a/server-plugins/notification-resources/src/index.ts +++ b/server-plugins/notification-resources/src/index.ts @@ -183,13 +183,22 @@ export async function getContentByTemplate ( const notificationType = control.modelDb.getObject(type) if (notificationType.templates === undefined) return - const textPart = await getTextPart(doc, control) - if (textPart === undefined) return const params: Record = notificationData !== undefined ? await getTranslatedNotificationContent(notificationData, notificationData._class, control) : {} + let textPart = await getTextPart(doc, control) + if (textPart === undefined) { + if ( + notificationData !== undefined && + control.hierarchy.isDerived(notificationData._class, notification.class.CommonInboxNotification) + ) { + textPart = params.title ?? params.body ?? '' + } + if (textPart === undefined || textPart === '') return + } + if ( notificationData !== undefined && control.hierarchy.isDerived(notificationData._class, notification.class.MentionInboxNotification) diff --git a/server/server-pipeline/package.json b/server/server-pipeline/package.json index 879cc60a3f..574db66844 100644 --- a/server/server-pipeline/package.json +++ b/server/server-pipeline/package.json @@ -127,6 +127,8 @@ "@hcengineering/hr-assets": "workspace:^0.7.0", "@hcengineering/request-assets": "workspace:^0.7.0", "@hcengineering/document-assets": "workspace:^0.7.0", + "@hcengineering/export": "workspace:^0.7.0", + "@hcengineering/export-assets": "workspace:^0.7.0", "@hcengineering/controlled-documents-assets": "workspace:^0.7.0", "@hcengineering/products-assets": "workspace:^0.7.0", "@hcengineering/training-assets": "workspace:^0.7.0", diff --git a/server/server-pipeline/src/internationalization.ts b/server/server-pipeline/src/internationalization.ts index eb44651f5c..e24082a7c4 100644 --- a/server/server-pipeline/src/internationalization.ts +++ b/server/server-pipeline/src/internationalization.ts @@ -8,6 +8,7 @@ import { chunterId } from '@hcengineering/chunter' import { contactId } from '@hcengineering/contact' import { documentsId } from '@hcengineering/controlled-documents' import { documentId } from '@hcengineering/document' +import { exportId } from '@hcengineering/export' import { driveId } from '@hcengineering/drive' import { githubId } from '@hcengineering/github' import { gmailId } from '@hcengineering/gmail' @@ -50,6 +51,7 @@ import chunterEn from '@hcengineering/chunter-assets/lang/en.json' import contactEn from '@hcengineering/contact-assets/lang/en.json' import documentsEn from '@hcengineering/controlled-documents-assets/lang/en.json' import documentEn from '@hcengineering/document-assets/lang/en.json' +import exportEn from '@hcengineering/export-assets/lang/en.json' import driveEn from '@hcengineering/drive-assets/lang/en.json' import githubEn from '@hcengineering/github-assets/lang/en.json' import gmailEn from '@hcengineering/gmail-assets/lang/en.json' @@ -109,6 +111,7 @@ export function registerStringLoaders (): void { addStringsLoader(preferenceId, async (lang: string) => preferenceEn) addStringsLoader(hrId, async (lang: string) => hrEn) addStringsLoader(documentId, async (lang: string) => documentEn) + addStringsLoader(exportId, async (lang: string) => exportEn) addStringsLoader(requestId, async (lang: string) => requestEn) addStringsLoader(loveId, async (lang: string) => loveEn) addStringsLoader(driveId, async (lang: string) => driveEn) diff --git a/services/export/pod-export/src/notifications.ts b/services/export/pod-export/src/notifications.ts index be49c23d64..ff378253b6 100644 --- a/services/export/pod-export/src/notifications.ts +++ b/services/export/pod-export/src/notifications.ts @@ -85,6 +85,7 @@ export async function sendExportCompletionNotification ( objectId: resultId, objectClass: exportPlugin.class.ExportResultRecord, icon: exportPlugin.icon.Export, + header: exportPlugin.string.ImportCompleted, message: exportPlugin.string.ImportToWorkspaceNotificationMessage, props: { count,