diff --git a/plugins/export-assets/lang/cs.json b/plugins/export-assets/lang/cs.json index 338a65c0caa..faddf6fd9f9 100644 --- a/plugins/export-assets/lang/cs.json +++ b/plugins/export-assets/lang/cs.json @@ -37,6 +37,7 @@ "SelectWorkspaceToExportSpace": "Vybrat pracovní prostor pro export dokumentů z prostoru", "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ů." + "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" } } diff --git a/plugins/export-assets/lang/de.json b/plugins/export-assets/lang/de.json index 670c65d2d52..b9fcd671ddd 100644 --- a/plugins/export-assets/lang/de.json +++ b/plugins/export-assets/lang/de.json @@ -37,6 +37,7 @@ "SelectWorkspaceToExportSpace": "Arbeitsbereich zum Exportieren von Dokumenten aus dem Bereich auswählen", "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." + "RequestPermissionToImport": "Bitte fordern Sie die Berechtigung vom Eigentümer des Ziel-Arbeitsbereichs an, um Dokumente zu importieren.", + "SkipDeletedObsolete": "Archivierte oder veraltete Dokumente überspringen" } } diff --git a/plugins/export-assets/lang/en.json b/plugins/export-assets/lang/en.json index 112ea10babb..af3fbe65fb1 100644 --- a/plugins/export-assets/lang/en.json +++ b/plugins/export-assets/lang/en.json @@ -37,6 +37,7 @@ "SelectWorkspaceToExportSpace": "Select workspace to export documents from space", "SelectSpace": "Select space", "NoSelectedDocuments": "No documents selected", - "RequestPermissionToImport": "Please request permission from the target workspace owner to import documents." + "RequestPermissionToImport": "Please request permission from the target workspace owner to import documents.", + "SkipDeletedObsolete": "Skip archived or obsolete documents" } } diff --git a/plugins/export-assets/lang/es.json b/plugins/export-assets/lang/es.json index f891905558b..f6ac4c270b8 100644 --- a/plugins/export-assets/lang/es.json +++ b/plugins/export-assets/lang/es.json @@ -37,6 +37,7 @@ "SelectWorkspaceToExportSpace": "Seleccionar espacio de trabajo para exportar documentos del espacio", "SelectSpace": "Seleccionar espacio", "NoSelectedDocuments": "No hay documentos seleccionados", - "RequestPermissionToImport": "Por favor, solicite permiso al propietario del espacio de trabajo de destino para importar documentos." + "RequestPermissionToImport": "Por favor, solicite permiso al propietario del espacio de trabajo de destino para importar documentos.", + "SkipDeletedObsolete": "Omitir documentos archivados u obsoletos" } } diff --git a/plugins/export-assets/lang/fr.json b/plugins/export-assets/lang/fr.json index eaaa78a9dcf..b451b4fe4c8 100644 --- a/plugins/export-assets/lang/fr.json +++ b/plugins/export-assets/lang/fr.json @@ -37,6 +37,7 @@ "SelectWorkspaceToExportSpace": "Sélectionner l'espace de travail pour exporter les documents de l'espace", "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." + "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" } } diff --git a/plugins/export-assets/lang/it.json b/plugins/export-assets/lang/it.json index 4690c0a36e5..3db1e4eaf32 100644 --- a/plugins/export-assets/lang/it.json +++ b/plugins/export-assets/lang/it.json @@ -37,6 +37,7 @@ "SelectWorkspaceToExportSpace": "Seleziona spazio di lavoro per esportare documenti dallo spazio", "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." + "RequestPermissionToImport": "Si prega di richiedere l'autorizzazione al proprietario dello spazio di lavoro di destinazione per importare documenti.", + "SkipDeletedObsolete": "Salta documenti archivati o obsoleti" } } diff --git a/plugins/export-assets/lang/ja.json b/plugins/export-assets/lang/ja.json index feaf09c4f23..39a9f11645a 100644 --- a/plugins/export-assets/lang/ja.json +++ b/plugins/export-assets/lang/ja.json @@ -37,6 +37,7 @@ "SelectWorkspaceToExportSpace": "スペースからドキュメントをエクスポートするワークスペースを選択", "SelectSpace": "スペースを選択", "NoSelectedDocuments": "ドキュメントが選択されていません", - "RequestPermissionToImport": "ドキュメントをインポートするには、対象ワークスペースの所有者に許可をリクエストしてください。" + "RequestPermissionToImport": "ドキュメントをインポートするには、対象ワークスペースの所有者に許可をリクエストしてください。", + "SkipDeletedObsolete": "アーカイブ済みまたは廃止されたドキュメントをスキップ" } } diff --git a/plugins/export-assets/lang/pt.json b/plugins/export-assets/lang/pt.json index 14343f2077f..c863d578b82 100644 --- a/plugins/export-assets/lang/pt.json +++ b/plugins/export-assets/lang/pt.json @@ -37,6 +37,7 @@ "SelectWorkspaceToExportSpace": "Selecionar espaço de trabalho para exportar documentos do espaço", "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." + "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" } } diff --git a/plugins/export-assets/lang/ru.json b/plugins/export-assets/lang/ru.json index 20634e04beb..d24a9932e59 100644 --- a/plugins/export-assets/lang/ru.json +++ b/plugins/export-assets/lang/ru.json @@ -37,6 +37,7 @@ "SelectWorkspaceToExportSpace": "Выбрать рабочее пространство для экспорта документов из пространства", "SelectSpace": "Выбрать пространство", "NoSelectedDocuments": "Документы не выбраны", - "RequestPermissionToImport": "Пожалуйста, запросите разрешение у владельца рабочего пространства, куда вы хотите экспортировать документы." + "RequestPermissionToImport": "Пожалуйста, запросите разрешение у владельца рабочего пространства, куда вы хотите экспортировать документы.", + "SkipDeletedObsolete": "Пропускать архивированные или устаревшие документы" } } diff --git a/plugins/export-assets/lang/tr.json b/plugins/export-assets/lang/tr.json index 32c2f7e7f8c..0482f4c2192 100644 --- a/plugins/export-assets/lang/tr.json +++ b/plugins/export-assets/lang/tr.json @@ -37,6 +37,7 @@ "SelectWorkspaceToExportSpace": "Alanın belgelerini dışa aktarmak için çalışma alanı seç", "SelectSpace": "Alan seç", "NoSelectedDocuments": "Seçili belge yok", - "RequestPermissionToImport": "Belgeleri içe aktarmak için lütfen hedef çalışma alanı sahibinden izin isteyin." + "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" } } diff --git a/plugins/export-assets/lang/zh.json b/plugins/export-assets/lang/zh.json index 5c097194865..b332bdda970 100644 --- a/plugins/export-assets/lang/zh.json +++ b/plugins/export-assets/lang/zh.json @@ -37,6 +37,7 @@ "SelectWorkspaceToExportSpace": "选择工作区以从空间导出文档", "SelectSpace": "选择空间", "NoSelectedDocuments": "未选择文档", - "RequestPermissionToImport": "请向目标工作区所有者请求导入文档的权限。" + "RequestPermissionToImport": "请向目标工作区所有者请求导入文档的权限。", + "SkipDeletedObsolete": "跳过已归档或已过时的文档" } } diff --git a/plugins/export-resources/src/components/ExportToWorkspaceModal.svelte b/plugins/export-resources/src/components/ExportToWorkspaceModal.svelte index 8f60045131e..8cba809214e 100644 --- a/plugins/export-resources/src/components/ExportToWorkspaceModal.svelte +++ b/plugins/export-resources/src/components/ExportToWorkspaceModal.svelte @@ -25,10 +25,10 @@ type Class } from '@hcengineering/core' import { Card, getCurrentWorkspaceUuid } from '@hcengineering/presentation' - import { DropdownLabels, Label } from '@hcengineering/ui' + import { CheckBox, DropdownLabels, Label } from '@hcengineering/ui' import { getResource } from '@hcengineering/platform' import login from '@hcengineering/login' - import { type RelationDefinition } from '@hcengineering/export' + import { type RelationDefinition, shouldSkipDocument } from '@hcengineering/export' import { createEventDispatcher } from 'svelte' @@ -41,12 +41,6 @@ export let docClass: Ref> | undefined = undefined export let spaceExport: boolean | undefined = false - $: selectedDocs = spaceExport !== true ? (Array.isArray(value) ? value : value != null ? [value] : []) : [] - $: _class = docClass ?? (selectedDocs.length > 0 ? selectedDocs[0]._class : undefined) - - // Build query with space filter when exporting from space - $: exportQuery = spaceExport === true ? { ...(query ?? {}), space: (value as Space)._id } : query - const dispatch = createEventDispatcher() let targetWorkspace: string | undefined = undefined @@ -54,6 +48,18 @@ let workspacesWithPermission = new Set() let loading = false let workspaceLoading = false + let skipDeletedObsolete = true + + $: selectedDocs = spaceExport !== true ? (Array.isArray(value) ? value : value != null ? [value] : []) : [] + $: _class = docClass ?? (selectedDocs.length > 0 ? selectedDocs[0]._class : undefined) + + $: filteredSelectedDocs = + skipDeletedObsolete && selectedDocs.length > 0 + ? selectedDocs.filter((doc) => !shouldSkipDocument(doc)) + : selectedDocs + + // Build query with space filter when exporting from space + $: exportQuery = spaceExport === true ? { ...(query ?? {}), space: (value as Space)._id } : query async function loadWorkspaces (): Promise { try { @@ -97,13 +103,13 @@ }) ) - $: canSave = targetWorkspace !== undefined && _class != null + $: canSave = targetWorkspace !== undefined && _class != null && filteredSelectedDocs.length > 0 async function handleExport (): Promise { if (!canSave || _class == null) return loading = true - void exportToWorkspace(_class, exportQuery, selectedDocs, targetWorkspace, relations) + void exportToWorkspace(_class, exportQuery, filteredSelectedDocs, targetWorkspace, relations, skipDeletedObsolete) loading = false dispatch('close', true) } @@ -116,18 +122,18 @@ okAction={handleExport} okLabel={plugin.string.Export} canSave={canSave && !loading && !workspaceLoading} - width="x-small" + width="small" on:close={() => dispatch('close')} on:changeContent >
- + {#if !workspaceLoading && workspaces.length === 0} +
+ +
+
+
diff --git a/plugins/export-resources/src/export.ts b/plugins/export-resources/src/export.ts index b47473c25ed..a60a98c37ee 100644 --- a/plugins/export-resources/src/export.ts +++ b/plugins/export-resources/src/export.ts @@ -24,7 +24,8 @@ export async function exportToWorkspace ( query: DocumentQuery | undefined, selectedDocs: Doc[], targetWorkspace: string | undefined, - relations: RelationDefinition[] | undefined + relations: RelationDefinition[] | undefined, + skipDeletedObsolete?: boolean ): Promise { const lang = getCurrentLanguage() @@ -65,7 +66,8 @@ export async function exportToWorkspace ( targetWorkspace, _class, relations, - fieldMappers + fieldMappers, + skipDeletedObsolete } body.query = diff --git a/plugins/export-resources/src/plugin.ts b/plugins/export-resources/src/plugin.ts index 457a4c243aa..13d7e892b3d 100644 --- a/plugins/export-resources/src/plugin.ts +++ b/plugins/export-resources/src/plugin.ts @@ -50,6 +50,7 @@ export default mergeIds(exportId, exportPlugin, { SelectWorkspaceToExportSpace: '' as IntlString, SelectSpace: '' as IntlString, NoSelectedDocuments: '' as IntlString, - RequestPermissionToImport: '' as IntlString + RequestPermissionToImport: '' as IntlString, + SkipDeletedObsolete: '' as IntlString } }) diff --git a/plugins/export/src/index.ts b/plugins/export/src/index.ts index 3940d360115..1746b475797 100644 --- a/plugins/export/src/index.ts +++ b/plugins/export/src/index.ts @@ -16,6 +16,7 @@ import { exportPlugin, exportId } from './plugin' export * from './types' +export * from './utils' export { exportId } diff --git a/plugins/export/src/utils.ts b/plugins/export/src/utils.ts new file mode 100644 index 00000000000..af279644360 --- /dev/null +++ b/plugins/export/src/utils.ts @@ -0,0 +1,36 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// +import type { Doc } from '@hcengineering/core' + +/** + * Checks if a document should be skipped based on its state. + * Returns true if the document should be skipped (archived, deleted, or obsolete). + * @param doc - The document to check + * @returns true if the document should be skipped, false otherwise + */ +export function shouldSkipDocument (doc: Doc): boolean { + // Check if document has a state field + if ('state' in doc) { + const docWithState = doc as Doc & { state?: string } + if (typeof docWithState.state === 'string') { + const state = docWithState.state + // Skip documents in archived, deleted, or obsolete state + if (state === 'deleted' || state === 'archived' || state === 'obsolete') { + return true + } + } + } + return false +} diff --git a/services/export/pod-export/src/server.ts b/services/export/pod-export/src/server.ts index 217ca64a548..993f56b270c 100644 --- a/services/export/pod-export/src/server.ts +++ b/services/export/pod-export/src/server.ts @@ -404,7 +404,8 @@ export function createServer ( conflictStrategy, includeAttachments, relations: rawRelations, - fieldMappers + fieldMappers, + skipDeletedObsolete }: { targetWorkspace: WorkspaceUuid _class: Ref> @@ -415,6 +416,7 @@ export function createServer ( objectId?: Ref objectSpace?: Ref fieldMappers?: Record> + skipDeletedObsolete?: boolean } = req.body // Validate required parameters @@ -528,7 +530,8 @@ export function createServer ( conflictStrategy: conflictStrategy ?? 'duplicate', includeAttachments: includeAttachments ?? true, relations, - fieldMappers + fieldMappers, + skipDeletedObsolete: skipDeletedObsolete ?? true } const exportResult: ExportResult = await exporter.export(options) diff --git a/services/export/pod-export/src/workspace/types.ts b/services/export/pod-export/src/workspace/types.ts index 7bf3b92ffbe..4c824ceee13 100644 --- a/services/export/pod-export/src/workspace/types.ts +++ b/services/export/pod-export/src/workspace/types.ts @@ -47,6 +47,8 @@ export interface ExportOptions { // Field mappers per class: { classA: { fieldA: value, fieldB: '$currentUser' }, ... } // Special value '$currentUser' will be replaced with current account's employee ID fieldMappers?: Record> + // Whether to skip documents and templates in deleted or obsolete state + skipDeletedObsolete?: boolean } export interface ExportResult { diff --git a/services/export/pod-export/src/workspace/workspace-exporter.ts b/services/export/pod-export/src/workspace/workspace-exporter.ts index de2a00196fe..f3067c4e2c1 100644 --- a/services/export/pod-export/src/workspace/workspace-exporter.ts +++ b/services/export/pod-export/src/workspace/workspace-exporter.ts @@ -24,6 +24,7 @@ import { } from '@hcengineering/core' import contact, { type Employee } from '@hcengineering/contact' import { type StorageAdapter } from '@hcengineering/server-core' +import { shouldSkipDocument } from '@hcengineering/export' import { AttachmentExporter } from './attachment-exporter' import { DataMapper } from './data-mapper' import { DocumentExporter } from './document-exporter' @@ -124,7 +125,8 @@ export class CrossWorkspaceExporter { includeAttachments = true, mapper, relations = [], - fieldMappers = {} + fieldMappers = {}, + skipDeletedObsolete = true } = options // Store field mappers @@ -202,10 +204,13 @@ export class CrossWorkspaceExporter { this.context.info(`Processing batch: ${processedCount + 1}-${processedCount + docs.length}`) + // Filter out archived/deleted/obsolete documents if skipDeletedObsolete is enabled + const docsToProcess = skipDeletedObsolete ? docs.filter((doc) => !shouldSkipDocument(doc)) : docs + // Check for existing documents in bulk if needed const existingDocsMap = new Map, Doc>() if (conflictStrategy === 'skip') { - const docIds = docs.map((d) => d._id) + const docIds = docsToProcess.map((d) => d._id) const existing = await this.targetClient.findAll(_class, { _id: { $in: docIds } }) for (const doc of existing) { existingDocsMap.set(doc._id, doc) @@ -213,7 +218,7 @@ export class CrossWorkspaceExporter { } // Export documents in this batch - for (const doc of docs) { + for (const doc of docsToProcess) { try { // Apply mapper if provided const mappedDoc = mapper !== undefined ? await mapper(doc) : doc