Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion plugins/export-assets/lang/cs.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
3 changes: 2 additions & 1 deletion plugins/export-assets/lang/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
3 changes: 2 additions & 1 deletion plugins/export-assets/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
3 changes: 2 additions & 1 deletion plugins/export-assets/lang/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
3 changes: 2 additions & 1 deletion plugins/export-assets/lang/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
3 changes: 2 additions & 1 deletion plugins/export-assets/lang/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
3 changes: 2 additions & 1 deletion plugins/export-assets/lang/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"SelectWorkspaceToExportSpace": "スペースからドキュメントをエクスポートするワークスペースを選択",
"SelectSpace": "スペースを選択",
"NoSelectedDocuments": "ドキュメントが選択されていません",
"RequestPermissionToImport": "ドキュメントをインポートするには、対象ワークスペースの所有者に許可をリクエストしてください。"
"RequestPermissionToImport": "ドキュメントをインポートするには、対象ワークスペースの所有者に許可をリクエストしてください。",
"SkipDeletedObsolete": "アーカイブ済みまたは廃止されたドキュメントをスキップ"
}
}
3 changes: 2 additions & 1 deletion plugins/export-assets/lang/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
3 changes: 2 additions & 1 deletion plugins/export-assets/lang/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"SelectWorkspaceToExportSpace": "Выбрать рабочее пространство для экспорта документов из пространства",
"SelectSpace": "Выбрать пространство",
"NoSelectedDocuments": "Документы не выбраны",
"RequestPermissionToImport": "Пожалуйста, запросите разрешение у владельца рабочего пространства, куда вы хотите экспортировать документы."
"RequestPermissionToImport": "Пожалуйста, запросите разрешение у владельца рабочего пространства, куда вы хотите экспортировать документы.",
"SkipDeletedObsolete": "Пропускать архивированные или устаревшие документы"
}
}
3 changes: 2 additions & 1 deletion plugins/export-assets/lang/tr.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
3 changes: 2 additions & 1 deletion plugins/export-assets/lang/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"SelectWorkspaceToExportSpace": "选择工作区以从空间导出文档",
"SelectSpace": "选择空间",
"NoSelectedDocuments": "未选择文档",
"RequestPermissionToImport": "请向目标工作区所有者请求导入文档的权限。"
"RequestPermissionToImport": "请向目标工作区所有者请求导入文档的权限。",
"SkipDeletedObsolete": "跳过已归档或已过时的文档"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -41,19 +41,25 @@
export let docClass: Ref<Class<Doc>> | 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
let workspaces: WorkspaceInfoWithStatus[] = []
let workspacesWithPermission = new Set<WorkspaceUuid>()
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<void> {
try {
Expand Down Expand Up @@ -97,13 +103,13 @@
})
)

$: canSave = targetWorkspace !== undefined && _class != null
$: canSave = targetWorkspace !== undefined && _class != null && filteredSelectedDocs.length > 0

async function handleExport (): Promise<void> {
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)
}
Expand All @@ -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
>
<div class="flex-col gap-2">
<span class="pb-4 secondary-textColor">
<span class="pl-2 pb-4 secondary-textColor">
{#if !workspaceLoading && workspaces.length === 0}
<Label label={plugin.string.RequestPermissionToImport} />
{:else if spaceExport === true}
<Label label={plugin.string.SelectWorkspaceToExportSpace} />
{:else}
<Label label={plugin.string.SelectWorkspaceToExport} params={{ count: selectedDocs.length }} />
<Label label={plugin.string.SelectWorkspaceToExport} params={{ count: filteredSelectedDocs.length }} />
{/if}
</span>
<DropdownLabels
Expand All @@ -140,5 +146,11 @@
kind="regular"
size="large"
/>
<div class="flex gap-2 pt-4">
<CheckBox bind:checked={skipDeletedObsolete} />
<div class="secondary-textColor">
<Label label={plugin.string.SkipDeletedObsolete} />
</div>
</div>
</div>
</Card>
6 changes: 4 additions & 2 deletions plugins/export-resources/src/export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ export async function exportToWorkspace (
query: DocumentQuery<Doc> | undefined,
selectedDocs: Doc[],
targetWorkspace: string | undefined,
relations: RelationDefinition[] | undefined
relations: RelationDefinition[] | undefined,
skipDeletedObsolete?: boolean
): Promise<void> {
const lang = getCurrentLanguage()

Expand Down Expand Up @@ -65,7 +66,8 @@ export async function exportToWorkspace (
targetWorkspace,
_class,
relations,
fieldMappers
fieldMappers,
skipDeletedObsolete
}

body.query =
Expand Down
3 changes: 2 additions & 1 deletion plugins/export-resources/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
})
1 change: 1 addition & 0 deletions plugins/export/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import { exportPlugin, exportId } from './plugin'

export * from './types'
export * from './utils'

export { exportId }

Expand Down
36 changes: 36 additions & 0 deletions plugins/export/src/utils.ts
Original file line number Diff line number Diff line change
@@ -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
}
7 changes: 5 additions & 2 deletions services/export/pod-export/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,8 @@ export function createServer (
conflictStrategy,
includeAttachments,
relations: rawRelations,
fieldMappers
fieldMappers,
skipDeletedObsolete
}: {
targetWorkspace: WorkspaceUuid
_class: Ref<Class<Doc>>
Expand All @@ -415,6 +416,7 @@ export function createServer (
objectId?: Ref<Doc>
objectSpace?: Ref<Space>
fieldMappers?: Record<string, Record<string, any>>
skipDeletedObsolete?: boolean
} = req.body

// Validate required parameters
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions services/export/pod-export/src/workspace/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, Record<string, any>>
// Whether to skip documents and templates in deleted or obsolete state
skipDeletedObsolete?: boolean
}

export interface ExportResult {
Expand Down
11 changes: 8 additions & 3 deletions services/export/pod-export/src/workspace/workspace-exporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -124,7 +125,8 @@ export class CrossWorkspaceExporter {
includeAttachments = true,
mapper,
relations = [],
fieldMappers = {}
fieldMappers = {},
skipDeletedObsolete = true
} = options

// Store field mappers
Expand Down Expand Up @@ -202,18 +204,21 @@ 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<Ref<Doc>, 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)
}
}

// 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
Expand Down