feat: Add import notifications (#10464)

* feat: Add import notifications

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* Fix svelte check

Signed-off-by: Artem Savchenko <armisav@gmail.com>

---------

Signed-off-by: Artem Savchenko <armisav@gmail.com>
This commit is contained in:
Artyom Savchenko
2026-01-30 16:41:27 +07:00
committed by GitHub
parent 8974667578
commit 19ebe78218
23 changed files with 529 additions and 106 deletions
+27
View File
@@ -8067,9 +8067,18 @@ importers:
'@hcengineering/model-core':
specifier: workspace:^0.7.0
version: link:../core
'@hcengineering/model-notification':
specifier: workspace:^0.7.0
version: link:../notification
'@hcengineering/model-presentation':
specifier: workspace:^0.7.0
version: link:../presentation
'@hcengineering/model-view':
specifier: workspace:^0.7.0
version: link:../view
'@hcengineering/notification':
specifier: workspace:^0.7.0
version: link:../../plugins/notification
'@hcengineering/platform':
specifier: workspace:^0.7.19
version: link:../../foundations/core/packages/platform
@@ -8079,6 +8088,9 @@ importers:
'@hcengineering/ui':
specifier: workspace:^0.7.0
version: link:../../packages/ui
'@hcengineering/view':
specifier: workspace:^0.7.0
version: link:../../plugins/view
'@hcengineering/workbench':
specifier: workspace:^0.7.0
version: link:../../plugins/workbench
@@ -20563,6 +20575,9 @@ importers:
'@hcengineering/core':
specifier: workspace:^0.7.24
version: link:../../foundations/core/packages/core
'@hcengineering/notification':
specifier: workspace:^0.7.0
version: link:../notification
'@hcengineering/platform':
specifier: workspace:^0.7.19
version: link:../../foundations/core/packages/platform
@@ -20679,6 +20694,12 @@ importers:
'@hcengineering/login':
specifier: workspace:^0.7.0
version: link:../login
'@hcengineering/notification':
specifier: workspace:^0.7.0
version: link:../notification
'@hcengineering/panel':
specifier: workspace:^0.7.0
version: link:../../packages/panel
'@hcengineering/platform':
specifier: workspace:^0.7.19
version: link:../../foundations/core/packages/platform
@@ -20691,6 +20712,12 @@ importers:
'@hcengineering/ui':
specifier: workspace:^0.7.0
version: link:../../packages/ui
'@hcengineering/view':
specifier: workspace:^0.7.0
version: link:../view
'@hcengineering/view-resources':
specifier: workspace:^0.7.0
version: link:../view-resources
svelte:
specifier: ^4.2.20
version: 4.2.20
+5 -1
View File
@@ -42,7 +42,11 @@
"@hcengineering/workbench": "workspace:^0.7.0",
"@hcengineering/presentation": "workspace:^0.7.0",
"@hcengineering/model-presentation": "workspace:^0.7.0",
"@hcengineering/model-view": "workspace:^0.7.0",
"@hcengineering/model-notification": "workspace:^0.7.0",
"@hcengineering/export-resources": "workspace:^0.7.0",
"@hcengineering/export": "workspace:^0.7.0"
"@hcengineering/export": "workspace:^0.7.0",
"@hcengineering/view": "workspace:^0.7.0",
"@hcengineering/notification": "workspace:^0.7.0"
}
}
+76 -6
View File
@@ -13,24 +13,94 @@
// limitations under the License.
//
import { type Builder } from '@hcengineering/model'
import core from '@hcengineering/model-core'
import type { Class, Doc, Domain, Ref } from '@hcengineering/core'
import type { ExportResultRecord } from '@hcengineering/export'
import { type Builder, Model, Prop, ArrOf, TypeRef, TypeString, TypeNumber, UX } from '@hcengineering/model'
import core, { TDoc } from '@hcengineering/model-core'
import presentation from '@hcengineering/model-presentation'
import workbench from '@hcengineering/workbench'
import exportPlugin from './plugin'
import view from '@hcengineering/model-view'
import notification from '@hcengineering/notification'
import exportPlugin from '@hcengineering/export'
import exportModelPlugin from './plugin'
export { exportId } from '@hcengineering/export'
export * from './migration'
export default exportPlugin
export default exportModelPlugin
export const DOMAIN_EXPORT = 'export' as Domain
@Model(exportPlugin.class.ExportResultRecord, core.class.Doc, DOMAIN_EXPORT)
@UX(exportPlugin.string.ImportCompleted, exportPlugin.icon.Export)
export class TExportResultRecord extends TDoc implements ExportResultRecord {
@Prop(TypeString(), exportPlugin.string.SourceWorkspace)
sourceWorkspace!: string
@Prop(TypeString(), exportPlugin.string.TargetWorkspace)
targetWorkspace!: string
@Prop(TypeNumber(), exportPlugin.string.ExportedCount)
exportedCount!: number
@Prop(ArrOf(TypeRef(core.class.Doc)), exportPlugin.string.ExportedDocumentIds)
exportedDocumentIds!: Ref<Doc>[]
@Prop(TypeRef(core.class.Class), exportPlugin.string.ExportedDocumentClass)
objectClass!: Ref<Class<Doc>>
@Prop(TypeString(), view.string.Title)
title?: string
}
export function createModel (builder: Builder): void {
builder.createModel(TExportResultRecord)
builder.mixin(exportPlugin.class.ExportResultRecord, core.class.Class, view.mixin.ObjectPresenter, {
presenter: view.component.BaseDocPresenter
})
builder.mixin(exportPlugin.class.ExportResultRecord, core.class.Class, view.mixin.ObjectPanel, {
component: exportPlugin.component.ExportResultPanel
})
builder.mixin(exportPlugin.class.ExportResultRecord, core.class.Class, view.mixin.ObjectTitle, {
titleProvider: exportPlugin.function.ExportResultTitleProvider
})
builder.createDoc(
notification.class.NotificationGroup,
core.space.Model,
{
label: exportPlugin.string.Import,
icon: exportPlugin.icon.Export,
objectClass: exportPlugin.class.ExportResultRecord
},
exportPlugin.ids.ImportNotificationGroup
)
builder.createDoc(
notification.class.NotificationType,
core.space.Model,
{
hidden: false,
generated: false,
label: exportPlugin.string.ImportedDocuments,
group: exportPlugin.ids.ImportNotificationGroup,
txClasses: [],
objectClass: exportPlugin.class.ExportResultRecord,
defaultEnabled: true
},
exportPlugin.ids.ImportedDocumentsNotification
)
builder.createDoc(
presentation.class.ComponentPointExtension,
core.space.Model,
{
extension: workbench.extensions.SpecialViewAction,
component: exportPlugin.component.ExportButton
component: exportModelPlugin.component.ExportButton
},
exportPlugin.extensions.ExportButton
exportModelPlugin.extensions.ExportButton
)
}
+11 -1
View File
@@ -31,6 +31,15 @@
"ExportStartedMessage": "Export zahájen. Až bude dokončen, obdržíte oznámení.",
"ExportToWorkspaceCompleted": "Export dokončen",
"ExportToWorkspaceCompletedMessage": "Export byl úspěšně dokončen.",
"ImportToWorkspaceNotificationMessage": "{count} dokumentů importováno do vašeho pracovního prostoru",
"ImportCompleted": "Dokumenty importovány do pracovního prostoru",
"SourceWorkspace": "Zdrojový pracovní prostor",
"ExportedCount": "Počet exportovaných",
"ExportedDocumentIds": "ID exportovaných dokumentů",
"DocumentsImportedFromWorkspace": "{count, plural, one {1 dokument byl importován z pracovního prostoru {workspace}} other {# dokumentů bylo importováno z pracovního prostoru {workspace}}}:",
"ExportedDocumentClass": "Třída exportovaných dokumentů",
"Import": "Import",
"ImportedDocuments": "Nové dokumenty",
"ExportToWorkspaceFailed": "Export se nezdařil",
"SelectWorkspace": "Vybrat pracovní prostor",
"SelectWorkspaceToExport": "Vybrat pracovní prostor pro export {count, plural, =1 {1 dokument} other {# dokumentů}}",
@@ -38,6 +47,7 @@
"SelectSpace": "Vybrat prostor",
"NoSelectedDocuments": "Žádné dokumenty nevybrány",
"RequestPermissionToImport": "Prosím, požádejte vlastníka cílového pracovního prostoru o povolení k importu dokumentů.",
"SkipDeletedObsolete": "Přeskočit archivované nebo zastaralé dokumenty"
"SkipDeletedObsolete": "Přeskočit archivované nebo zastaralé dokumenty",
"ExportResultRecordTitle": "Export dokumentů z pracovního prostoru {workspace} ({date})"
}
}
+11 -1
View File
@@ -31,6 +31,15 @@
"ExportStartedMessage": "Export gestartet. Sie erhalten eine Benachrichtigung, wenn der Export abgeschlossen ist.",
"ExportToWorkspaceCompleted": "Export abgeschlossen",
"ExportToWorkspaceCompletedMessage": "Export erfolgreich abgeschlossen.",
"ImportToWorkspaceNotificationMessage": "{count} Dokumente in Ihren Arbeitsbereich importiert",
"ImportCompleted": "Dokumente in Arbeitsbereich importiert",
"SourceWorkspace": "Quell-Arbeitsbereich",
"ExportedCount": "Exportierte Anzahl",
"ExportedDocumentIds": "IDs exportierter Dokumente",
"DocumentsImportedFromWorkspace": "{count, plural, one {1 Dokument wurde aus dem Arbeitsbereich {workspace} importiert} other {# Dokumente wurden aus dem Arbeitsbereich {workspace} importiert}}:",
"ExportedDocumentClass": "Klasse exportierter Dokumente",
"Import": "Import",
"ImportedDocuments": "Neue Dokumente",
"ExportToWorkspaceFailed": "Export fehlgeschlagen",
"SelectWorkspace": "Arbeitsbereich auswählen",
"SelectWorkspaceToExport": "Arbeitsbereich zum Exportieren von {count, plural, =1 {1 Dokument} other {# Dokumenten}} auswählen",
@@ -38,6 +47,7 @@
"SelectSpace": "Bereich auswählen",
"NoSelectedDocuments": "Keine Dokumente ausgewählt",
"RequestPermissionToImport": "Bitte fordern Sie die Berechtigung vom Eigentümer des Ziel-Arbeitsbereichs an, um Dokumente zu importieren.",
"SkipDeletedObsolete": "Archivierte oder veraltete Dokumente überspringen"
"SkipDeletedObsolete": "Archivierte oder veraltete Dokumente überspringen",
"ExportResultRecordTitle": "Export von Dokumenten aus dem Arbeitsbereich {workspace} ({date})"
}
}
+11 -1
View File
@@ -31,6 +31,15 @@
"ExportStartedMessage": "Export started. You will receive a notification when it completes.",
"ExportToWorkspaceCompleted": "Export completed",
"ExportToWorkspaceCompletedMessage": "Export completed successfully.",
"ImportToWorkspaceNotificationMessage": "{count} documents imported to your workspace",
"ImportCompleted": "Documents imported to workspace",
"SourceWorkspace": "Source workspace",
"ExportedCount": "Exported count",
"ExportedDocumentIds": "Exported document IDs",
"DocumentsImportedFromWorkspace": "{count, plural, one {1 document was imported from {workspace} workspace} other {# documents were imported from {workspace} workspace}}:",
"ExportedDocumentClass": "Exported document class",
"Import": "Import",
"ImportedDocuments": "New documents",
"ExportToWorkspaceFailed": "Export failed",
"SelectWorkspace": "Select workspace",
"SelectWorkspaceToExport": "Select workspace to export {count, plural, =1 {1 document} other {# documents}}",
@@ -38,6 +47,7 @@
"SelectSpace": "Select space",
"NoSelectedDocuments": "No documents selected",
"RequestPermissionToImport": "Please request permission from the target workspace owner to import documents.",
"SkipDeletedObsolete": "Skip archived or obsolete documents"
"SkipDeletedObsolete": "Skip archived or obsolete documents",
"ExportResultRecordTitle": "Export of documents from {workspace} workspace ({date})"
}
}
+11 -1
View File
@@ -31,6 +31,15 @@
"ExportStartedMessage": "Exportación iniciada. Recibirá una notificación cuando se complete.",
"ExportToWorkspaceCompleted": "Exportación completada",
"ExportToWorkspaceCompletedMessage": "Exportación completada exitosamente.",
"ImportToWorkspaceNotificationMessage": "{count} documentos importados a su espacio de trabajo",
"ImportCompleted": "Documentos importados al espacio de trabajo",
"SourceWorkspace": "Espacio de trabajo de origen",
"ExportedCount": "Cantidad exportada",
"ExportedDocumentIds": "ID de documentos exportados",
"DocumentsImportedFromWorkspace": "{count, plural, one {1 documento fue importado desde el espacio de trabajo {workspace}} other {# documentos fueron importados desde el espacio de trabajo {workspace}}}:",
"ExportedDocumentClass": "Clase de documentos exportados",
"Import": "Importar",
"ImportedDocuments": "Documentos nuevos",
"ExportToWorkspaceFailed": "Exportación fallida",
"SelectWorkspace": "Seleccionar espacio de trabajo",
"SelectWorkspaceToExport": "Seleccionar espacio de trabajo para exportar {count, plural, =1 {1 documento} other {# documentos}}",
@@ -38,6 +47,7 @@
"SelectSpace": "Seleccionar espacio",
"NoSelectedDocuments": "No hay documentos seleccionados",
"RequestPermissionToImport": "Por favor, solicite permiso al propietario del espacio de trabajo de destino para importar documentos.",
"SkipDeletedObsolete": "Omitir documentos archivados u obsoletos"
"SkipDeletedObsolete": "Omitir documentos archivados u obsoletos",
"ExportResultRecordTitle": "Exportación de documentos del espacio de trabajo {workspace} ({date})"
}
}
+11 -1
View File
@@ -31,6 +31,15 @@
"ExportStartedMessage": "Exportation démarrée. Vous recevrez une notification une fois terminée.",
"ExportToWorkspaceCompleted": "Exportation terminée",
"ExportToWorkspaceCompletedMessage": "Exportation terminée avec succès.",
"ImportToWorkspaceNotificationMessage": "{count} documents importés dans votre espace de travail",
"ImportCompleted": "Documents importés dans l'espace de travail",
"SourceWorkspace": "Espace de travail source",
"ExportedCount": "Nombre exporté",
"ExportedDocumentIds": "ID des documents exportés",
"DocumentsImportedFromWorkspace": "{count, plural, one {1 document a été importé depuis l'espace de travail {workspace}} other {# documents ont été importés depuis l'espace de travail {workspace}}}:",
"ExportedDocumentClass": "Classe des documents exportés",
"Import": "Import",
"ImportedDocuments": "Nouveaux documents",
"ExportToWorkspaceFailed": "Exportation échouée",
"SelectWorkspace": "Sélectionner l'espace de travail",
"SelectWorkspaceToExport": "Sélectionner l'espace de travail pour exporter {count, plural, =1 {1 document} other {# documents}}",
@@ -38,6 +47,7 @@
"SelectSpace": "Sélectionner l'espace",
"NoSelectedDocuments": "Aucun document sélectionné",
"RequestPermissionToImport": "Veuillez demander l'autorisation au propriétaire de l'espace de travail cible pour importer des documents.",
"SkipDeletedObsolete": "Ignorer les documents archivés ou obsolètes"
"SkipDeletedObsolete": "Ignorer les documents archivés ou obsolètes",
"ExportResultRecordTitle": "Export de documents depuis l'espace de travail {workspace} ({date})"
}
}
+11 -1
View File
@@ -31,6 +31,15 @@
"ExportStartedMessage": "Esportazione avviata. Riceverai una notifica quando sarà completata.",
"ExportToWorkspaceCompleted": "Esportazione completata",
"ExportToWorkspaceCompletedMessage": "Esportazione completata con successo.",
"ImportToWorkspaceNotificationMessage": "{count} documenti importati nel tuo spazio di lavoro",
"ImportCompleted": "Documenti importati nell'area di lavoro",
"SourceWorkspace": "Spazio di lavoro di origine",
"ExportedCount": "Conteggio esportato",
"ExportedDocumentIds": "ID documenti esportati",
"DocumentsImportedFromWorkspace": "{count, plural, one {1 documento è stato importato dallo spazio di lavoro {workspace}} other {# documenti sono stati importati dallo spazio di lavoro {workspace}}}:",
"ExportedDocumentClass": "Classe documenti esportati",
"Import": "Importa",
"ImportedDocuments": "Nuovi documenti",
"ExportToWorkspaceFailed": "Esportazione fallita",
"SelectWorkspace": "Seleziona spazio di lavoro",
"SelectWorkspaceToExport": "Seleziona spazio di lavoro per esportare {count, plural, =1 {1 documento} other {# documenti}}",
@@ -38,6 +47,7 @@
"SelectSpace": "Seleziona spazio",
"NoSelectedDocuments": "Nessun documento selezionato",
"RequestPermissionToImport": "Si prega di richiedere l'autorizzazione al proprietario dello spazio di lavoro di destinazione per importare documenti.",
"SkipDeletedObsolete": "Salta documenti archivati o obsoleti"
"SkipDeletedObsolete": "Salta documenti archivati o obsoleti",
"ExportResultRecordTitle": "Esportazione di documenti dallo spazio di lavoro {workspace} ({date})"
}
}
+11 -1
View File
@@ -31,6 +31,15 @@
"ExportStartedMessage": "エクスポートを開始しました。完了すると通知が届きます。",
"ExportToWorkspaceCompleted": "エクスポート完了",
"ExportToWorkspaceCompletedMessage": "エクスポートが正常に完了しました。",
"ImportToWorkspaceNotificationMessage": "{count} 件のドキュメントがワークスペースにインポートされました",
"ImportCompleted": "ドキュメントがワークスペースにインポートされました",
"SourceWorkspace": "ソースワークスペース",
"ExportedCount": "エクスポート数",
"ExportedDocumentIds": "エクスポートされたドキュメントのID",
"DocumentsImportedFromWorkspace": "{count, plural, one {1 件のドキュメントがワークスペース {workspace} からインポートされました} other {# 件のドキュメントがワークスペース {workspace} からインポートされました}}:",
"ExportedDocumentClass": "エクスポートされたドキュメントのクラス",
"Import": "インポート",
"ImportedDocuments": "新しいドキュメント",
"ExportToWorkspaceFailed": "エクスポート失敗",
"SelectWorkspace": "ワークスペースを選択",
"SelectWorkspaceToExport": "{count, plural, =1 {1 件のドキュメント} other {# 件のドキュメント}}をエクスポートするワークスペースを選択",
@@ -38,6 +47,7 @@
"SelectSpace": "スペースを選択",
"NoSelectedDocuments": "ドキュメントが選択されていません",
"RequestPermissionToImport": "ドキュメントをインポートするには、対象ワークスペースの所有者に許可をリクエストしてください。",
"SkipDeletedObsolete": "アーカイブ済みまたは廃止されたドキュメントをスキップ"
"SkipDeletedObsolete": "アーカイブ済みまたは廃止されたドキュメントをスキップ",
"ExportResultRecordTitle": "ワークスペース {workspace} からのドキュメントのエクスポート ({date})"
}
}
+11 -1
View File
@@ -31,6 +31,15 @@
"ExportStartedMessage": "Exportação iniciada. Você receberá uma notificação quando for concluída.",
"ExportToWorkspaceCompleted": "Exportação concluída",
"ExportToWorkspaceCompletedMessage": "Exportação concluída com sucesso.",
"ImportToWorkspaceNotificationMessage": "{count} documentos importados para o seu espaço de trabalho",
"ImportCompleted": "Documentos importados para o espaço de trabalho",
"SourceWorkspace": "Espaço de trabalho de origem",
"ExportedCount": "Quantidade exportada",
"ExportedDocumentIds": "IDs dos documentos exportados",
"DocumentsImportedFromWorkspace": "{count, plural, one {1 documento foi importado do espaço de trabalho {workspace}} other {# documentos foram importados do espaço de trabalho {workspace}}}:",
"ExportedDocumentClass": "Classe dos documentos exportados",
"Import": "Importar",
"ImportedDocuments": "Novos documentos",
"ExportToWorkspaceFailed": "Exportação falhou",
"SelectWorkspace": "Selecionar espaço de trabalho",
"SelectWorkspaceToExport": "Selecionar espaço de trabalho para exportar {count, plural, =1 {1 documento} other {# documentos}}",
@@ -38,6 +47,7 @@
"SelectSpace": "Selecionar espaço",
"NoSelectedDocuments": "Nenhum documento selecionado",
"RequestPermissionToImport": "Por favor, solicite permissão ao proprietário do espaço de trabalho de destino para importar documentos.",
"SkipDeletedObsolete": "Pular documentos arquivados ou obsoletos"
"SkipDeletedObsolete": "Pular documentos arquivados ou obsoletos",
"ExportResultRecordTitle": "Exportação de documentos do espaço de trabalho {workspace} ({date})"
}
}
+11 -1
View File
@@ -31,6 +31,15 @@
"ExportStartedMessage": "Экспорт начат. Вы получите уведомление, когда он будет завершен.",
"ExportToWorkspaceCompleted": "Экспорт завершен",
"ExportToWorkspaceCompletedMessage": "Экспорт успешно завершен.",
"ImportToWorkspaceNotificationMessage": "{count} документов импортировано в ваше рабочее пространство",
"ImportCompleted": "Документы импортированы в рабочее пространство",
"SourceWorkspace": "Исходное рабочее пространство",
"ExportedCount": "Количество экспортированных",
"ExportedDocumentIds": "ID экспортированных документов",
"DocumentsImportedFromWorkspace": "{count, plural, one {1 документ импортирован из рабочего пространства {workspace}} few {# документа импортированы из рабочего пространства {workspace}} other {# документов импортировано из рабочего пространства {workspace}}}:",
"ExportedDocumentClass": "Класс экспортированных документов",
"Import": "Импорт",
"ImportedDocuments": "Новые документы",
"ExportToWorkspaceFailed": "Экспорт не удался",
"SelectWorkspace": "Выбрать рабочее пространство",
"SelectWorkspaceToExport": "Выбрать рабочее пространство для экспорта {count, plural, =1 {1 документ} one {# документ} few {# документа} other {# документов}}",
@@ -38,6 +47,7 @@
"SelectSpace": "Выбрать пространство",
"NoSelectedDocuments": "Документы не выбраны",
"RequestPermissionToImport": "Пожалуйста, запросите разрешение у владельца рабочего пространства, куда вы хотите экспортировать документы.",
"SkipDeletedObsolete": "Пропускать архивированные или устаревшие документы"
"SkipDeletedObsolete": "Пропускать архивированные или устаревшие документы",
"ExportResultRecordTitle": "Экспорт документов из пространства {workspace} ({date})"
}
}
+11 -1
View File
@@ -31,6 +31,15 @@
"ExportStartedMessage": "Dışa aktarma başladı. Tamamlandığında bir bildirim alacaksınız.",
"ExportToWorkspaceCompleted": "Dışa aktarma tamamlandı",
"ExportToWorkspaceCompletedMessage": "Dışa aktarma başarıyla tamamlandı.",
"ImportToWorkspaceNotificationMessage": "{count} belge çalışma alanınıza içe aktarıldı",
"ImportCompleted": "Belgeler çalışma alanına içe aktarıldı",
"SourceWorkspace": "Kaynak çalışma alanı",
"ExportedCount": "Dışa aktarılan sayı",
"ExportedDocumentIds": "Dışa aktarılan belge kimlikleri",
"DocumentsImportedFromWorkspace": "{count, plural, one {1 belge {workspace} çalışma alanından içe aktarıldı} other {# belge {workspace} çalışma alanından içe aktarıldı}}:",
"ExportedDocumentClass": "Dışa aktarılan belge sınıfı",
"Import": "İçe aktarma",
"ImportedDocuments": "Yeni belgeler",
"ExportToWorkspaceFailed": "Dışa aktarma başarısız oldu",
"SelectWorkspace": "Çalışma alanı seç",
"SelectWorkspaceToExport": "{count, plural, =1 {1 belgeyi} other {# belgeyi}} dışa aktarmak için çalışma alanı seç",
@@ -38,6 +47,7 @@
"SelectSpace": "Alan seç",
"NoSelectedDocuments": "Seçili belge yok",
"RequestPermissionToImport": "Belgeleri içe aktarmak için lütfen hedef çalışma alanı sahibinden izin isteyin.",
"SkipDeletedObsolete": "Arşivlenmiş veya kullanımdan kaldırılmış belgeleri atla"
"SkipDeletedObsolete": "Arşivlenmiş veya kullanımdan kaldırılmış belgeleri atla",
"ExportResultRecordTitle": "{workspace} çalışma alanından belge dışa aktarma ({date})"
}
}
+11 -1
View File
@@ -31,6 +31,15 @@
"ExportStartedMessage": "导出已开始。完成后您将收到通知。",
"ExportToWorkspaceCompleted": "导出完成",
"ExportToWorkspaceCompletedMessage": "导出已成功完成。",
"ImportToWorkspaceNotificationMessage": "{count} 个文档已导入到您的工作区",
"ImportCompleted": "文档已导入到工作区",
"SourceWorkspace": "源工作区",
"ExportedCount": "导出数量",
"ExportedDocumentIds": "已导出文档的 ID",
"DocumentsImportedFromWorkspace": "{count, plural, one {1 个文档已从工作区 {workspace} 导入} other {# 个文档已从工作区 {workspace} 导入}}:",
"ExportedDocumentClass": "已导出文档的类",
"Import": "导入",
"ImportedDocuments": "新文档",
"ExportToWorkspaceFailed": "导出失败",
"SelectWorkspace": "选择工作区",
"SelectWorkspaceToExport": "选择工作区以导出 {count, plural, =1 {1 文档} other {# 个文档}}",
@@ -38,6 +47,7 @@
"SelectSpace": "选择空间",
"NoSelectedDocuments": "未选择文档",
"RequestPermissionToImport": "请向目标工作区所有者请求导入文档的权限。",
"SkipDeletedObsolete": "跳过已归档或已过时的文档"
"SkipDeletedObsolete": "跳过已归档或已过时的文档",
"ExportResultRecordTitle": "从工作区 {workspace} 导出文档 ({date})"
}
}
+5 -1
View File
@@ -44,6 +44,10 @@
"@hcengineering/ui": "workspace:^0.7.0",
"@hcengineering/export": "workspace:^0.7.0",
"@hcengineering/login": "workspace:^0.7.0",
"@hcengineering/theme": "workspace:^0.7.0"
"@hcengineering/notification": "workspace:^0.7.0",
"@hcengineering/panel": "workspace:^0.7.0",
"@hcengineering/theme": "workspace:^0.7.0",
"@hcengineering/view": "workspace:^0.7.0",
"@hcengineering/view-resources": "workspace:^0.7.0"
}
}
@@ -0,0 +1,142 @@
<!--
// 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, { type Class, type Doc, type Ref } from '@hcengineering/core'
import exportPlugin, { type ExportResultRecord } from '@hcengineering/export'
import notification from '@hcengineering/notification'
import { Panel } from '@hcengineering/panel'
import { getResource } from '@hcengineering/platform'
import { createQuery } from '@hcengineering/presentation'
import { Button, IconMoreH, Label } from '@hcengineering/ui'
import { DocNavLink, ObjectPresenter, showMenu } from '@hcengineering/view-resources'
import view from '@hcengineering/view'
import { createEventDispatcher, onDestroy } from 'svelte'
export let _id: Ref<ExportResultRecord>
export let _class: Ref<Class<ExportResultRecord>>
export let embedded: boolean = false
const query = createQuery()
const docsQuery = createQuery()
const inboxClient = getResource(notification.function.GetInboxNotificationsClient).then((res) => res())
const dispatch = createEventDispatcher()
let object: ExportResultRecord | undefined = undefined
let lastId: Ref<Doc> | undefined = undefined
let exportedDocs: Doc[] = []
$: query.query(_class, { _id }, (result) => {
object = result[0]
})
$: docClass = object?.objectClass ?? core.class.Doc
$: hasExportedDocs = (object?.exportedDocumentIds?.length ?? 0) > 0
$: if (hasExportedDocs && object != null) {
docsQuery.query(docClass, { _id: { $in: object.exportedDocumentIds } }, (result) => {
exportedDocs = result
})
} else {
docsQuery.unsubscribe()
exportedDocs = []
}
$: if (object?._id !== lastId) {
const prev = lastId
lastId = object?._id
if (prev !== undefined) {
void inboxClient.then((c) => {
void c.readDoc(prev)
})
}
}
onDestroy(() => {
void inboxClient.then((c) => {
if (_id !== undefined) void c.readDoc(_id)
})
})
</script>
{#if object}
<Panel
isHeader={false}
isSub={false}
isAside={true}
{embedded}
{object}
on:open
on:close={() => dispatch('close')}
withoutInput
withoutActivity
>
<svelte:fragment slot="title">
<DocNavLink noUnderline {object}>
<div class="title">
{object.title ??
`Import completed ${object.exportedCount} document${object.exportedCount !== 1 ? 's' : ''}`}
</div>
</DocNavLink>
</svelte:fragment>
<svelte:fragment slot="utils">
<Button
icon={IconMoreH}
iconProps={{ size: 'medium' }}
kind={'icon'}
dataId="btnMoreActions"
on:click={(e) => {
showMenu(e, { object, excludedActions: [view.action.Open] })
}}
/>
</svelte:fragment>
<div class="export-result-content flex-col flex-grow flex-no-shrink step-tb-6">
<p class="export-result-summary">
<Label
label={exportPlugin.string.DocumentsImportedFromWorkspace}
params={{ count: object.exportedCount, workspace: object.sourceWorkspace }}
/>
</p>
{#if exportedDocs.length > 0}
<div class="doc-links">
{#each exportedDocs as doc (doc._id)}
<DocNavLink noUnderline object={doc}>
<ObjectPresenter
_class={docClass}
value={doc}
props={{ inline: true, size: 'small', withIcon: true, isGray: true }}
/>
</DocNavLink>
{/each}
</div>
{/if}
</div>
</Panel>
{/if}
<style lang="scss">
.export-result-content {
padding: 0 1rem;
}
.export-result-summary {
margin: 0 0 0.75rem;
}
.doc-links {
display: flex;
flex-wrap: wrap;
flex-direction: column;
gap: 0.25rem 1rem;
}
</style>
+33 -2
View File
@@ -13,19 +13,50 @@
// limitations under the License.
//
import { type Resources } from '@hcengineering/platform'
import type { Client, Doc, Ref } from '@hcengineering/core'
import exportPlugin, { type ExportResultRecord } from '@hcengineering/export'
import { type Resources, translate } from '@hcengineering/platform'
import { themeStore } from '@hcengineering/ui'
import { get } from 'svelte/store'
import ExportButton from './components/ExportButton.svelte'
import ExportSettings from './components/ExportSettings.svelte'
import ExportToWorkspaceModal from './components/ExportToWorkspaceModal.svelte'
import ExportResultPanel from './components/ExportResultPanel.svelte'
export { default as ExportButton } from './components/ExportButton.svelte'
export { default as ExportSettings } from './components/ExportSettings.svelte'
export { default as ExportToWorkspaceModal } from './components/ExportToWorkspaceModal.svelte'
export { default as ExportResultPanel } from './components/ExportResultPanel.svelte'
export async function getExportResultTitle (_client: Client, _ref: Ref<Doc>, doc?: Doc): Promise<string> {
const record = doc as ExportResultRecord | undefined
if (record === undefined) return ''
if (record.title !== undefined && record.title !== '') return record.title
const lang = get(themeStore).language
const createdOn = record.createdOn ?? Date.now()
const dateStr = new Date(createdOn).toLocaleDateString(lang, {
day: '2-digit',
month: '2-digit',
year: 'numeric'
})
return await translate(
exportPlugin.string.ExportResultRecordTitle,
{
workspace: record.sourceWorkspace,
date: dateStr
},
lang
)
}
export default async (): Promise<Resources> => ({
component: {
ExportButton,
ExportSettings,
ExportToWorkspaceModal
ExportToWorkspaceModal,
ExportResultPanel
},
function: {
ExportResultTitleProvider: getExportResultTitle
}
})
+2 -1
View File
@@ -40,6 +40,7 @@
"dependencies": {
"@hcengineering/platform": "workspace:^0.7.19",
"@hcengineering/core": "workspace:^0.7.24",
"@hcengineering/ui": "workspace:^0.7.0"
"@hcengineering/ui": "workspace:^0.7.0",
"@hcengineering/notification": "workspace:^0.7.0"
}
}
+27 -3
View File
@@ -13,29 +13,53 @@
// limitations under the License.
//
import { type IntlString, type Metadata, type Plugin, plugin, type Asset } from '@hcengineering/platform'
import type { Class, Client, Doc, Ref } from '@hcengineering/core'
import { type IntlString, type Metadata, type Plugin, plugin, type Resource, type Asset } from '@hcengineering/platform'
import { type AnyComponent } from '@hcengineering/ui/src/types'
import type { NotificationGroup, NotificationType } from '@hcengineering/notification'
import type { ExportResultRecord } from './types'
export const exportId = 'export' as Plugin
export const exportPlugin = plugin(exportId, {
ids: {
ImportNotificationGroup: '' as Ref<NotificationGroup>,
ImportedDocumentsNotification: '' as Ref<NotificationType>
},
class: {
ExportResultRecord: '' as Ref<Class<ExportResultRecord>>
},
string: {
Export: '' as IntlString,
ExportCompleted: '' as IntlString,
ExportFailed: '' as IntlString,
ExportToWorkspace: '' as IntlString,
TargetWorkspace: '' as IntlString
TargetWorkspace: '' as IntlString,
ImportCompleted: '' as IntlString,
ImportToWorkspaceNotificationMessage: '' as IntlString,
SourceWorkspace: '' as IntlString,
ExportedCount: '' as IntlString,
ExportedDocumentIds: '' as IntlString,
DocumentsImportedFromWorkspace: '' as IntlString,
ExportedDocumentClass: '' as IntlString,
Import: '' as IntlString,
ImportedDocuments: '' as IntlString,
ExportResultRecordTitle: '' as IntlString
},
component: {
ExportButton: '' as AnyComponent,
ExportSettings: '' as AnyComponent,
ExportToWorkspaceModal: '' as AnyComponent
ExportToWorkspaceModal: '' as AnyComponent,
ExportResultPanel: '' as AnyComponent
},
icon: {
Export: '' as Asset
},
metadata: {
ExportUrl: '' as Metadata<string>
},
function: {
ExportResultTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>, doc?: Doc) => Promise<string>>
}
})
+20
View File
@@ -14,6 +14,26 @@
//
import type { Class, Doc, Ref } from '@hcengineering/core'
/**
* @public
*/
export interface ExportResultDoc {
docId: Ref<Doc>
name: string
}
/**
* @public
*/
export interface ExportResultRecord extends Doc {
sourceWorkspace: string
targetWorkspace: string
exportedCount: number
exportedDocumentIds: Ref<Doc>[]
objectClass: Ref<Class<Doc>>
title?: string
}
export enum OperationType {
IDENTITY = 'identity',
GROUP_BY = 'group_by'
+58 -74
View File
@@ -13,109 +13,93 @@
//
import { getClient } from '@hcengineering/account-client'
import {
import core, {
AccountRole,
concatLink,
Doc,
type AccountUuid,
type Class,
type Doc,
generateId,
MeasureContext,
Ref,
type Ref,
systemAccountUuid,
type TxOperations,
WorkspaceIds,
WorkspaceUuid
} from '@hcengineering/core'
import exportPlugin from '@hcengineering/export'
import notification from '@hcengineering/notification'
import { generateToken } from '@hcengineering/server-token'
import envConfig from './config'
export async function sendExportCompletionEmail (
export async function sendExportCompletionNotification (
ctx: MeasureContext,
targetTxOps: TxOperations,
targetWorkspace: WorkspaceUuid,
targetWsIds: WorkspaceIds,
exportedDocuments: Array<{ docId: Ref<Doc>, name: string }>,
sourceWsIds: WorkspaceIds
sourceWsIds: WorkspaceIds,
objectClass: Ref<Class<Doc>>
): Promise<void> {
try {
const mailURL = envConfig.MailURL
if (mailURL == null || typeof mailURL !== 'string' || mailURL === '') {
ctx.warn('Mail service URL not configured, skipping email notification')
return
}
// Get workspace members to find owners
const count = exportedDocuments.length
const resultId = generateId<Doc>()
await targetTxOps.createDoc(
exportPlugin.class.ExportResultRecord,
core.space.Space,
{
sourceWorkspace: sourceWsIds.url,
targetWorkspace: targetWsIds.url,
exportedCount: count,
exportedDocumentIds: exportedDocuments.map((d) => d.docId),
objectClass
},
resultId
)
const targetWsToken = generateToken(systemAccountUuid, targetWorkspace, { service: 'export' })
const targetAccountClient = getClient(envConfig.AccountsUrl, targetWsToken)
const members = await targetAccountClient.getWorkspaceMembers()
// Find workspace owners
const owners = members.filter((m: any) => m.role === AccountRole.Owner)
const owners = members.filter((m: { role: string }) => m.role === AccountRole.Owner) as Array<{
person: AccountUuid
}>
if (owners.length === 0) {
ctx.warn('No workspace owners found for email notification', { targetWorkspace })
ctx.warn('No workspace owners found for export notification', { targetWorkspace })
return
}
// Get email addresses for owners
const ownerEmails: string[] = []
for (const owner of owners) {
try {
const personInfo = await targetAccountClient.getPersonInfo(owner.person)
const emailSocialId = personInfo.socialIds.find(
(sid: any) => (sid.type === 'EMAIL' || sid.type === 'GOOGLE') && sid.verifiedOn > 0 && sid.isDeleted !== true
)
if (emailSocialId?.value != null && emailSocialId.value !== '') {
ownerEmails.push(emailSocialId.value)
}
} catch (err) {
ctx.warn('Failed to get email for workspace owner', { owner: owner.person, error: err })
}
}
if (ownerEmails.length === 0) {
ctx.warn('No email addresses found for workspace owners', { targetWorkspace })
return
}
// Build email content
const documentList = exportedDocuments.map((doc) => ` - ${doc.name}`).join('\n')
const subject = `Export completed: ${exportedDocuments.length} document${exportedDocuments.length !== 1 ? 's' : ''} exported to your workspace`
const text = `The following ${exportedDocuments.length} document${exportedDocuments.length !== 1 ? 's have' : ' has'} been successfully exported to your workspace:\n\n${documentList}\n\nSource workspace: ${sourceWsIds.uuid}\nTarget workspace: ${targetWsIds.uuid}`
const html = `<p>The following <strong>${exportedDocuments.length}</strong> document${exportedDocuments.length !== 1 ? 's have' : ' has'} been successfully exported to your workspace:</p><ul>${exportedDocuments.map((doc) => `<li>${escapeHtml(doc.name)}</li>`).join('')}</ul><p>Source workspace: ${sourceWsIds.uuid}<br>Target workspace: ${targetWsIds.uuid}</p>`
// Send email to all owners
const mailAuth = envConfig.MailAuthToken
for (const email of ownerEmails) {
try {
const response = await fetch(concatLink(mailURL, '/send'), {
method: 'post',
keepalive: true,
headers: {
'Content-Type': 'application/json',
...(mailAuth != null ? { Authorization: `Bearer ${mailAuth}` } : {})
},
body: JSON.stringify({
text,
html,
subject,
to: [email]
})
const docNotifyContextId = await targetTxOps.createDoc(notification.class.DocNotifyContext, core.space.Space, {
objectId: resultId,
objectClass: exportPlugin.class.ExportResultRecord,
objectSpace: core.space.Space,
user: owner.person,
isPinned: false,
hidden: false
})
await targetTxOps.createDoc(notification.class.CommonInboxNotification, core.space.Space, {
user: owner.person,
objectId: resultId,
objectClass: exportPlugin.class.ExportResultRecord,
icon: exportPlugin.icon.Export,
message: exportPlugin.string.ImportToWorkspaceNotificationMessage,
props: {
count,
sourceWorkspace: sourceWsIds.uuid
},
isViewed: false,
archived: false,
docNotifyContext: docNotifyContextId,
types: [exportPlugin.ids.ImportedDocumentsNotification]
})
if (!response.ok) {
ctx.error(`Failed to send export completion email: ${response.statusText}`, { email })
} else {
ctx.info('Export completion email sent', { email, documentCount: exportedDocuments.length })
}
} catch (err) {
ctx.error('Could not send export completion email', { err, email })
ctx.error('Failed to create export notification for owner', { owner: owner.person, err })
}
}
} catch (err) {
ctx.error('Failed to send export completion email', { err })
ctx.error('Failed to send export completion notification', { err })
}
}
function escapeHtml (text: string): string {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;')
}
+5 -3
View File
@@ -64,7 +64,7 @@ import { getConfig } from '@hcengineering/server-pipeline'
import { buildStorageFromConfig } from '@hcengineering/server-storage'
import { Token, decodeToken, generateToken } from '@hcengineering/server-token'
import archiver from 'archiver'
import { sendExportCompletionEmail } from './notifications'
import { sendExportCompletionNotification } from './notifications'
import cors from 'cors'
import express, { type Express, type NextFunction, type Request, type Response } from 'express'
import { createWriteStream } from 'fs'
@@ -537,12 +537,14 @@ export function createServer (
const exportResult: ExportResult = await exporter.export(options)
if (exportResult.success && exportResult.exportedCount > 0) {
await sendExportCompletionEmail(
await sendExportCompletionNotification(
measureCtx,
targetTxOps,
targetWorkspace,
targetWsIds,
exportResult.exportedDocuments,
wsIds
wsIds,
_class
)
}
@@ -239,10 +239,14 @@ export class CrossWorkspaceExporter {
(mappedDoc as any).title ??
hierarchy.getClass(mappedDoc._class)?.label ??
mappedDoc._id
result.exportedDocuments.push({
docId: mappedDoc._id,
name: typeof docName === 'string' ? docName : String(docName)
})
// Use target workspace doc id so the notification panel can resolve docs in the target workspace
const targetDocId = this.state.idMapping.get(mappedDoc._id)
if (targetDocId !== undefined) {
result.exportedDocuments.push({
docId: targetDocId,
name: typeof docName === 'string' ? docName : String(docName)
})
}
} else {
result.skippedCount++
}