diff --git a/models/controlled-documents/src/index.ts b/models/controlled-documents/src/index.ts
index 20231c6ce5..f7659ab469 100644
--- a/models/controlled-documents/src/index.ts
+++ b/models/controlled-documents/src/index.ts
@@ -676,6 +676,24 @@ export function createModel (builder: Builder): void {
provider: documents.function.DocumentIdentifierProvider
})
+ createAction(
+ builder,
+ {
+ action: documents.actionImpl.TransferDocument,
+ label: documents.string.Transfer,
+ icon: view.icon.Move,
+ input: 'any',
+ category: view.category.General,
+ target: documents.class.ProjectDocument,
+ visibilityTester: documents.function.CanTransferDocument,
+ context: {
+ mode: ['context', 'browser'],
+ group: 'copy'
+ }
+ },
+ documents.action.TransferDocument
+ )
+
createAction(
builder,
{
diff --git a/models/controlled-documents/src/plugin.ts b/models/controlled-documents/src/plugin.ts
index cf8dcbb92f..f155e919ec 100644
--- a/models/controlled-documents/src/plugin.ts
+++ b/models/controlled-documents/src/plugin.ts
@@ -61,8 +61,10 @@ export default mergeIds(documentsId, documents, {
CreateChildTemplate: '' as ViewAction,
CreateDocument: '' as ViewAction,
CreateTemplate: '' as ViewAction,
+ TransferTemplate: '' as ViewAction,
DeleteDocument: '' as ViewAction,
ArchiveDocument: '' as ViewAction,
+ TransferDocument: '' as ViewAction,
EditDocSpace: '' as ViewAction
},
viewlet: {
diff --git a/plugins/controlled-documents-assets/lang/cs.json b/plugins/controlled-documents-assets/lang/cs.json
index 242304159a..54a802f60c 100644
--- a/plugins/controlled-documents-assets/lang/cs.json
+++ b/plugins/controlled-documents-assets/lang/cs.json
@@ -129,7 +129,12 @@
"Copy": "kopírovat",
"ConfigLabel": "Řízené dokumenty",
- "ConfigDescription": "Rozšíření pro správu řízených dokumentů"
+ "ConfigDescription": "Rozšíření pro správu řízených dokumentů",
+
+ "Transfer": "Přenos",
+ "TransferWarning": "Někteří členové týmu mohou po této akci ztratit možnost prohlížet nebo upravovat tento dokument.",
+ "TransferDocuments": "Přenos řízených dokumentů",
+ "TransferDocumentsHint": "Dokumenty, které mají být přeneseny do vybraného prostoru:"
},
"controlledDocStates": {
"Empty": "",
diff --git a/plugins/controlled-documents-assets/lang/de.json b/plugins/controlled-documents-assets/lang/de.json
index a6687b5a0e..0298bad71a 100644
--- a/plugins/controlled-documents-assets/lang/de.json
+++ b/plugins/controlled-documents-assets/lang/de.json
@@ -293,7 +293,12 @@
"DeleteDocumentCategoryPermission": "Dokumentenkategorie löschen",
"DeleteDocumentCategoryDescription": "Gewährt Benutzern die Möglichkeit, eine Dokumentenkategorie zu löschen",
"ConfigLabel": "Kontrollierte Dokumente",
- "ConfigDescription": "Erweiterung zur Verwaltung kontrollierter Dokumente"
+ "ConfigDescription": "Erweiterung zur Verwaltung kontrollierter Dokumente",
+
+ "Transfer": "Übertragung",
+ "TransferWarning": "Einige Teammitglieder können dieses Dokument nach dieser Aktion möglicherweise nicht mehr anzeigen oder bearbeiten.",
+ "TransferDocuments": "Übertragung kontrollierter Dokumente",
+ "TransferDocumentsHint": "Dokumente, die in den ausgewählten Bereich übertragen werden sollen:"
},
"controlledDocStates": {
"Empty": "",
diff --git a/plugins/controlled-documents-assets/lang/en.json b/plugins/controlled-documents-assets/lang/en.json
index 6bd79621ab..9f981bf2ee 100644
--- a/plugins/controlled-documents-assets/lang/en.json
+++ b/plugins/controlled-documents-assets/lang/en.json
@@ -295,7 +295,12 @@
"DeleteDocumentCategoryPermission": "Delete document category",
"DeleteDocumentCategoryDescription": "Grants users ability to delete a document category",
"ConfigLabel": "Controlled Documents",
- "ConfigDescription": "Extension to manage controlled documents"
+ "ConfigDescription": "Extension to manage controlled documents",
+
+ "Transfer": "Transfer",
+ "TransferWarning": "Some team members may lose the ability to view or edit this document after this action.",
+ "TransferDocuments": "Transfer controlled documents",
+ "TransferDocumentsHint": "Documents to be transferred to the selected space:"
},
"controlledDocStates": {
"Empty": "",
diff --git a/plugins/controlled-documents-assets/lang/fr.json b/plugins/controlled-documents-assets/lang/fr.json
index cf9023ab24..0378c61382 100644
--- a/plugins/controlled-documents-assets/lang/fr.json
+++ b/plugins/controlled-documents-assets/lang/fr.json
@@ -253,7 +253,12 @@
"DeleteDocumentCategoryPermission": "Supprimer la catégorie de document",
"DeleteDocumentCategoryDescription": "Accorde aux utilisateurs la capacité de supprimer une catégorie de document",
"ConfigLabel": "Documents contrôlés",
- "ConfigDescription": "Extension pour gérer les documents contrôlés"
+ "ConfigDescription": "Extension pour gérer les documents contrôlés",
+
+ "Transfer": "Transfert",
+ "TransferWarning": "Certains membres de l'équipe peuvent perdre la possibilité de visualiser ou de modifier ce document après cette action.",
+ "TransferDocuments": "Transférer des documents contrôlés",
+ "TransferDocumentsHint": "Documents à transférer dans l'espace sélectionné:"
},
"controlledDocStates": {
"Empty": "",
diff --git a/plugins/controlled-documents-assets/lang/it.json b/plugins/controlled-documents-assets/lang/it.json
index 56bdc91d64..561f79624a 100644
--- a/plugins/controlled-documents-assets/lang/it.json
+++ b/plugins/controlled-documents-assets/lang/it.json
@@ -251,7 +251,12 @@
"DeleteDocumentCategoryPermission": "Elimina categoria documento",
"DeleteDocumentCategoryDescription": "Concede agli utenti la possibilità di eliminare una categoria di documento",
"ConfigLabel": "Documenti controllati",
- "ConfigDescription": "Estensione per gestire documenti controllati"
+ "ConfigDescription": "Estensione per gestire documenti controllati",
+
+ "Transfer": "Trasferimento",
+ "TransferWarning": "Alcuni membri del team potrebbero perdere la possibilità di visualizzare o modificare il documento dopo questa azione.",
+ "TransferDocuments": "Trasferimento di documenti controllati",
+ "TransferDocumentsHint": "Documenti da trasferire nello spazio selezionato:"
},
"controlledDocStates": {
"Empty": "",
diff --git a/plugins/controlled-documents-assets/lang/ru.json b/plugins/controlled-documents-assets/lang/ru.json
index cfd3485676..1b4864e0cb 100644
--- a/plugins/controlled-documents-assets/lang/ru.json
+++ b/plugins/controlled-documents-assets/lang/ru.json
@@ -295,7 +295,12 @@
"DeleteDocumentCategoryPermission": "Удалять категорию",
"DeleteDocumentCategoryDescription": "Предоставляет пользователям разрешение удалять категорию",
"ConfigLabel": "Управляемые Документы",
- "ConfigDescription": "Расширение для управления управляемыми документами"
+ "ConfigDescription": "Расширение для управления управляемыми документами",
+
+ "Transfer": "Трансфер",
+ "TransferWarning": "После этого действия некоторые члены команды могут потерять возможность просматривать или редактировать этот документ.",
+ "TransferDocuments": "Трансфер управляемых документов",
+ "TransferDocumentsHint": "Документы, которые будут перенесены в выбранное пространство:"
},
"controlledDocStates": {
"Empty": "",
diff --git a/plugins/controlled-documents-assets/lang/zh.json b/plugins/controlled-documents-assets/lang/zh.json
index 4bb386924b..930b82681f 100644
--- a/plugins/controlled-documents-assets/lang/zh.json
+++ b/plugins/controlled-documents-assets/lang/zh.json
@@ -292,7 +292,12 @@
"DeleteDocumentCategoryPermission": "删除文档类别",
"DeleteDocumentCategoryDescription": "授予用户删除文档类别的权限",
"ConfigLabel": "受控文档",
- "ConfigDescription": "用于管理受控文档的扩展"
+ "ConfigDescription": "用于管理受控文档的扩展",
+
+ "Transfer": "转让",
+ "TransferWarning": "执行此操作后,某些团队成员可能会失去查看或编辑此文档的能力",
+ "TransferDocuments": "移交受控文件",
+ "TransferDocumentsHint": "要转移到所选空间的文件:"
},
"controlledDocStates": {
"Empty": "",
diff --git a/plugins/controlled-documents-resources/src/components/document/popups/TransferDocumentPopup.svelte b/plugins/controlled-documents-resources/src/components/document/popups/TransferDocumentPopup.svelte
new file mode 100644
index 0000000000..1f77c9637a
--- /dev/null
+++ b/plugins/controlled-documents-resources/src/components/document/popups/TransferDocumentPopup.svelte
@@ -0,0 +1,284 @@
+
+
+
+
+
+
+
diff --git a/plugins/controlled-documents-resources/src/index.ts b/plugins/controlled-documents-resources/src/index.ts
index 1ee72b010b..0a7cb8032b 100644
--- a/plugins/controlled-documents-resources/src/index.ts
+++ b/plugins/controlled-documents-resources/src/index.ts
@@ -28,7 +28,9 @@ import {
type Document,
type DocumentSpace,
DocumentState,
- type DocumentMeta
+ type DocumentMeta,
+ type ProjectDocument,
+ type Project
} from '@hcengineering/controlled-documents'
import { type Resources } from '@hcengineering/platform'
import { type ObjectSearchResult, getClient, MessageBox } from '@hcengineering/presentation'
@@ -101,6 +103,7 @@ import {
createTemplate
} from './utils'
import { comment, isCommentVisible } from './text'
+import TransferDocumentPopup from './components/document/popups/TransferDocumentPopup.svelte'
export { DocumentStatusTag, DocumentTitle, DocumentVersionPresenter, StatePresenter }
@@ -207,6 +210,46 @@ async function canArchiveDocument (obj?: Doc | Doc[]): Promise {
).then((res) => res.every((r) => r))
}
+async function canTransferDocument (obj?: Doc | Doc[]): Promise {
+ if (obj == null) {
+ return false
+ }
+
+ const objs = (Array.isArray(obj) ? obj : [obj]) as Document[]
+ const spaces = new Set(objs.map((doc) => doc.space))
+
+ return await Promise.all(
+ Array.from(spaces).map(
+ async (space) => await checkPermission(getClient(), documents.permission.ArchiveDocument, space)
+ )
+ ).then((res) => res.every((r) => r))
+}
+
+async function transferDocuments (selection: Document | Document[]): Promise {
+ const objects = Array.isArray(selection) ? selection : [selection]
+
+ const client = getClient()
+ const h = client.getHierarchy()
+
+ let sourceDocumentIds: Array[> = []
+ let sourceSpaceId: Ref | undefined
+ let sourceProjectId: Ref> | undefined
+
+ if (objects.length < 1) return
+ if (h.isDerived(objects[0]._class, documents.class.ProjectDocument)) {
+ const pjDocs = objects as unknown as ProjectDocument[]
+ const pjMeta = await client.findAll(documents.class.ProjectMeta, { _id: { $in: pjDocs.map((d) => d.attachedTo) } })
+ const docMeta = await client.findAll(documents.class.DocumentMeta, { _id: { $in: pjMeta.map((d) => d.meta) } })
+ sourceDocumentIds = docMeta.map((d) => d._id)
+ sourceSpaceId = pjDocs[0].space
+ sourceProjectId = pjDocs[0].project
+ }
+
+ if (sourceDocumentIds.length < 1) return
+
+ showPopup(TransferDocumentPopup, { sourceDocumentIds, sourceSpaceId, sourceProjectId })
+}
+
async function isLatestDraftDoc (obj?: Doc | Doc[]): Promise {
if (obj == null) {
return false
@@ -322,6 +365,7 @@ export default async (): Promise => ({
GetDocumentMetaLinkFragment: getDocumentMetaLinkFragment,
CanDeleteDocument: canDeleteDocument,
CanArchiveDocument: canArchiveDocument,
+ CanTransferDocument: canTransferDocument,
DocumentIdentifierProvider: documentIdentifierProvider,
ControlledDocumentTitleProvider: getControlledDocumentTitle,
Comment: comment,
@@ -334,6 +378,7 @@ export default async (): Promise => ({
CreateTemplate: createTemplate,
DeleteDocument: deleteDocuments,
ArchiveDocument: archiveDocuments,
+ TransferDocument: transferDocuments,
EditDocSpace: editDocSpace
},
resolver: {
diff --git a/plugins/controlled-documents-resources/src/plugin.ts b/plugins/controlled-documents-resources/src/plugin.ts
index 7ee46767e1..0f63086ecf 100644
--- a/plugins/controlled-documents-resources/src/plugin.ts
+++ b/plugins/controlled-documents-resources/src/plugin.ts
@@ -238,6 +238,7 @@ export default mergeIds(documentsId, documents, {
GetDocumentMetaLinkFragment: '' as Resource<(doc: Doc, props: Record) => Promise>,
CanDeleteDocument: '' as Resource<(doc?: Doc | Doc[]) => Promise>,
CanArchiveDocument: '' as Resource<(doc?: Doc | Doc[]) => Promise>,
+ CanTransferDocument: '' as Resource<(doc?: Doc | Doc[]) => Promise>,
ControlledDocumentTitleProvider: '' as Resource<(client: Client, ref: Ref, doc?: Doc) => Promise>
}
})
diff --git a/plugins/controlled-documents/src/plugin.ts b/plugins/controlled-documents/src/plugin.ts
index e35c614ff0..49aaaef79f 100644
--- a/plugins/controlled-documents/src/plugin.ts
+++ b/plugins/controlled-documents/src/plugin.ts
@@ -118,6 +118,7 @@ export const documentsPlugin = plugin(documentsId, {
DeleteDocument: '' as Ref,
ArchiveDocument: '' as Ref,
EditDocSpace: '' as Ref,
+ TransferDocument: '' as Ref,
Print: '' as Ref>
},
function: {
@@ -259,7 +260,12 @@ export const documentsPlugin = plugin(documentsId, {
DeleteDocumentCategoryPermission: '' as IntlString,
DeleteDocumentCategoryDescription: '' as IntlString,
ConfigLabel: '' as IntlString,
- ConfigDescription: '' as IntlString
+ ConfigDescription: '' as IntlString,
+
+ Transfer: '' as IntlString,
+ TransferWarning: '' as IntlString,
+ TransferDocuments: '' as IntlString,
+ TransferDocumentsHint: '' as IntlString
},
ids: {
NoParent: '' as Ref,
diff --git a/plugins/controlled-documents/src/utils.ts b/plugins/controlled-documents/src/utils.ts
index 7ae9ad4d13..7acbb4f229 100644
--- a/plugins/controlled-documents/src/utils.ts
+++ b/plugins/controlled-documents/src/utils.ts
@@ -14,7 +14,10 @@
//
import {
ApplyOperations,
+ checkPermission,
+ Class,
Data,
+ Doc,
DocumentQuery,
DocumentUpdate,
Rank,
@@ -28,17 +31,23 @@ import LexoRankBucket from 'lexorank/lib/lexoRank/lexoRankBucket'
import documents from './plugin'
+import attachment, { Attachment } from '@hcengineering/attachment'
+import chunter, { ChatMessage } from '@hcengineering/chunter'
+import tags, { TagReference } from '@hcengineering/tags'
import {
ChangeControl,
ControlledDocument,
Document,
DocumentMeta,
+ DocumentRequest,
+ DocumentSnapshot,
DocumentSpace,
DocumentState,
Project,
ProjectDocument,
ProjectMeta
} from './types'
+import { makeRank } from '@hcengineering/rank'
/**
* @public
@@ -129,6 +138,364 @@ export async function deleteProjectDrafts (client: ApplyOperations, source: Ref<
}
}
+class ProjectDocumentTree {
+ rootDocs: ProjectMeta[]
+ childrenByParent: Map][, ProjectMeta[]>
+
+ constructor (pjMeta: ProjectMeta[]) {
+ this.rootDocs = []
+ this.childrenByParent = new Map][, Array>()
+
+ for (const meta of pjMeta) {
+ const parentId = meta.path[0] ?? documents.ids.NoParent
+
+ if (!this.childrenByParent.has(parentId)) {
+ this.childrenByParent.set(parentId, [])
+ }
+
+ this.childrenByParent.get(parentId)?.push(meta)
+
+ if (parentId === documents.ids.NoParent) {
+ this.rootDocs.push(meta)
+ }
+ }
+ }
+
+ getDescendants (parent: Ref): Ref[] {
+ const result: Ref[] = []
+ const queue: Ref[] = [parent]
+
+ while (queue.length > 0) {
+ const next = queue.pop()
+ if (next === undefined) break
+
+ const children = this.childrenByParent.get(next) ?? []
+ const childrenRefs = children.map((p) => p.meta)
+ result.push(...childrenRefs)
+ queue.push(...childrenRefs)
+ }
+
+ return result
+ }
+}
+
+export async function findProjectDocsHierarchy (
+ client: TxOperations,
+ space: Ref,
+ project?: Ref>
+): Promise {
+ const pjMeta = await client.findAll(documents.class.ProjectMeta, { space, project })
+ return new ProjectDocumentTree(pjMeta)
+}
+
+export interface DocumentBundle {
+ DocumentMeta: DocumentMeta[]
+ ProjectMeta: ProjectMeta[]
+ ProjectDocument: ProjectDocument[]
+ ControlledDocument: ControlledDocument[]
+ ChangeControl: ChangeControl[]
+ DocumentRequest: DocumentRequest[]
+ DocumentSnapshot: DocumentSnapshot[]
+ ChatMessage: ChatMessage[]
+ TagReference: TagReference[]
+ Attachment: Attachment[]
+}
+
+function emptyBundle (): DocumentBundle {
+ return {
+ DocumentMeta: [],
+ ProjectMeta: [],
+ ProjectDocument: [],
+ ControlledDocument: [],
+ ChangeControl: [],
+ DocumentRequest: [],
+ DocumentSnapshot: [],
+ ChatMessage: [],
+ TagReference: [],
+ Attachment: []
+ }
+}
+
+export async function findAllDocumentBundles (
+ client: TxOperations,
+ ids: Ref[]
+): Promise {
+ const all: DocumentBundle = { ...emptyBundle() }
+
+ async function crawl (
+ _class: Ref>,
+ bkey: keyof DocumentBundle,
+ prop: P,
+ ids: T[P][]
+ ): Promise {
+ const data = await client.findAll(_class, { [prop]: { $in: ids } } as any)
+ all[bkey].push(...(data as any))
+ return data
+ }
+
+ await crawl(documents.class.DocumentMeta, 'DocumentMeta', '_id', ids)
+ await crawl(
+ documents.class.ProjectMeta,
+ 'ProjectMeta',
+ 'meta',
+ all.DocumentMeta.map((m) => m._id)
+ )
+ await crawl(
+ documents.class.ProjectDocument,
+ 'ProjectDocument',
+ 'attachedTo',
+ all.ProjectMeta.map((m) => m._id)
+ )
+ await crawl(
+ documents.class.ControlledDocument,
+ 'ControlledDocument',
+ 'attachedTo',
+ all.DocumentMeta.map((m) => m._id)
+ )
+ await crawl(
+ documents.class.ChangeControl,
+ 'ChangeControl',
+ '_id',
+ all.ControlledDocument.map((p) => p.changeControl)
+ )
+ await crawl(
+ documents.class.DocumentRequest,
+ 'DocumentRequest',
+ 'attachedTo',
+ all.ControlledDocument.map((p) => p._id)
+ )
+ await crawl(
+ documents.class.DocumentSnapshot,
+ 'DocumentSnapshot',
+ 'attachedTo',
+ all.ControlledDocument.map((p) => p._id)
+ )
+ await crawl(
+ documents.class.DocumentComment,
+ 'ChatMessage',
+ 'attachedTo',
+ all.ControlledDocument.map((p) => p._id)
+ )
+ await crawl(
+ chunter.class.ThreadMessage,
+ 'ChatMessage',
+ 'attachedTo',
+ all.ChatMessage.map((p) => p._id)
+ )
+ await crawl(
+ tags.class.TagReference,
+ 'TagReference',
+ 'attachedTo',
+ all.ControlledDocument.map((p) => p._id)
+ )
+ await crawl(attachment.class.Attachment, 'Attachment', 'attachedTo', [
+ ...all.ChatMessage.map((p) => p._id),
+ ...all.ControlledDocument.map((p) => p._id)
+ ])
+
+ const bundles = new Map][, DocumentBundle>(all.DocumentMeta.map((m) => [m._id, { ...emptyBundle() }]))
+ const links = new Map][, Ref>()
+
+ const link = (ref: Ref, lookup: Ref): void => {
+ const meta = links.get(lookup)
+ if (meta !== undefined) links.set(ref, meta)
+ }
+
+ const relink = (ref: Ref, prop: keyof DocumentBundle, obj: DocumentBundle[typeof prop][0]): void => {
+ const meta = links.get(ref)
+ if (meta !== undefined) bundles.get(meta)?.[prop].push(obj as any)
+ }
+
+ for (const m of all.DocumentMeta) links.set(m._id, m._id) // DocumentMeta -> DocumentMeta
+ for (const m of all.ProjectMeta) links.set(m._id, m.meta) // ProjectMeta -> DocumentMeta
+ for (const m of all.ProjectDocument) {
+ link(m._id, m.attachedTo) // ProjectDocument -> ProjectMeta
+ link(m.document, m.attachedTo) // ControlledDocument -> ProjectMeta
+ }
+ for (const m of all.ControlledDocument) link(m.changeControl, m.attachedTo) // ChangeControl -> ControlledDocument
+ for (const m of all.DocumentRequest) link(m._id, m.attachedTo) // DocumentRequest -> ControlledDocument
+ for (const m of all.DocumentSnapshot) link(m._id, m.attachedTo) // DocumentSnapshot -> ControlledDocument
+ for (const m of all.ChatMessage) link(m._id, m.attachedTo) // ChatMessage -> (ControlledDocument | ChatMessage)
+ for (const m of all.TagReference) link(m._id, m.attachedTo) // TagReference -> ControlledDocument
+ for (const m of all.Attachment) link(m._id, m.attachedTo) // Attachment -> (ControlledDocument | ChatMessage)
+
+ let key: keyof DocumentBundle
+ for (key in all) {
+ all[key].forEach((value) => {
+ relink(value._id, key, value)
+ })
+ }
+
+ return Array.from(bundles.values())
+}
+
+export async function findOneDocumentBundle (
+ client: TxOperations,
+ id: Ref
+): Promise {
+ const bundles = await findAllDocumentBundles(client, [id])
+ return bundles[0]
+}
+
+export interface DocumentTransferRequest {
+ sourceDocumentIds: Ref[]
+ sourceSpaceId: Ref
+ sourceProjectId?: Ref>
+
+ targetSpaceId: Ref
+ targetParentId?: Ref
+ targetProjectId?: Ref>
+}
+
+interface DocumentTransferContext {
+ request: DocumentTransferRequest
+ bundles: DocumentBundle[]
+
+ sourceTree: ProjectDocumentTree
+ targetTree: ProjectDocumentTree
+
+ sourceSpace: DocumentSpace
+ targetSpace: DocumentSpace
+
+ targetParentBundle?: DocumentBundle
+}
+
+async function _buildDocumentTransferContext (
+ client: TxOperations,
+ request: DocumentTransferRequest
+): Promise {
+ const sourceTree = await findProjectDocsHierarchy(client, request.sourceSpaceId, request.sourceProjectId)
+ const targetTree = await findProjectDocsHierarchy(client, request.targetSpaceId, request.targetProjectId)
+
+ const docIds = new Set][>(request.sourceDocumentIds)
+ for (const id of request.sourceDocumentIds) {
+ sourceTree.getDescendants(id).forEach((d) => docIds.add(d))
+ }
+
+ const bundles = await findAllDocumentBundles(client, Array.from(docIds))
+ const targetParentBundle =
+ request.targetParentId !== undefined ? await findOneDocumentBundle(client, request.targetParentId) : undefined
+
+ const sourceSpace = await client.findOne(documents.class.DocumentSpace, { _id: request.sourceSpaceId })
+ const targetSpace = await client.findOne(documents.class.DocumentSpace, { _id: request.targetSpaceId })
+
+ if (sourceSpace === undefined || targetSpace === undefined) return
+
+ return {
+ request,
+ bundles,
+ sourceTree,
+ targetTree,
+ sourceSpace,
+ targetSpace,
+ targetParentBundle
+ }
+}
+
+export async function listDocumentsAffectedByTransfer (
+ client: TxOperations,
+ req: DocumentTransferRequest
+): Promise {
+ const cx = await _buildDocumentTransferContext(client, req)
+ return cx?.bundles.map((b) => b.DocumentMeta[0]) ?? []
+}
+
+/**
+ * @public
+ */
+export async function canTransferDocuments (client: TxOperations, req: DocumentTransferRequest): Promise {
+ const cx = await _buildDocumentTransferContext(client, req)
+ return cx !== undefined ? await _transferDocuments(client, cx, 'check') : false
+}
+
+/**
+ * @public
+ */
+export async function transferDocuments (client: TxOperations, req: DocumentTransferRequest): Promise {
+ const cx = await _buildDocumentTransferContext(client, req)
+ return cx !== undefined ? await _transferDocuments(client, cx) : false
+}
+
+async function _transferDocuments (
+ client: TxOperations,
+ cx: DocumentTransferContext,
+ mode: 'default' | 'check' = 'default'
+): Promise {
+ if (cx.bundles.length < 1) return false
+ if (cx.targetSpace._id === cx.sourceSpace._id) return false
+
+ const hierarchy = client.getHierarchy()
+
+ const canArchiveInSourceSpace = await checkPermission(
+ client,
+ documents.permission.ArchiveDocument,
+ cx.request.sourceSpaceId
+ )
+ const canCreateInTargetSpace = await checkPermission(
+ client,
+ documents.permission.CreateDocument,
+ cx.request.targetSpaceId
+ )
+
+ if (!canArchiveInSourceSpace || !canCreateInTargetSpace) return false
+
+ for (const bundle of cx.bundles) {
+ if (bundle.DocumentMeta.length !== 1) return false
+ if (bundle.ProjectMeta.length !== 1) return false
+ if (bundle.DocumentMeta[0].space !== cx.request.sourceSpaceId) return false
+ if (bundle.ControlledDocument.length < 1) return false
+
+ const isTemplate = hierarchy.hasMixin(bundle.ControlledDocument[0], documents.mixin.DocumentTemplate)
+ if (isTemplate && hierarchy.isDerived(cx.targetSpace._class, documents.class.ExternalSpace)) return false
+ }
+
+ const roots = new Set(cx.request.sourceDocumentIds)
+ const updates = new Map>()
+
+ function update (document: T, update: Partial): void {
+ updates.set(document, { ...updates.get(document), ...update })
+ }
+
+ const parentMeta = cx.targetParentBundle?.ProjectMeta[0]
+ const project = cx.request.targetProjectId ?? documents.ids.NoProject
+
+ if (cx.targetParentBundle !== undefined && parentMeta === undefined) return false
+
+ let lastRank: Rank | undefined
+ if (parentMeta !== undefined) {
+ lastRank = await getFirstRank(client, cx.targetSpace._id, project, parentMeta.meta)
+ }
+
+ for (const bundle of cx.bundles) {
+ const projectMeta = bundle.ProjectMeta[0]
+
+ if (roots.has(projectMeta.meta)) {
+ const path = parentMeta?.path !== undefined ? [parentMeta.meta, ...parentMeta.path] : []
+ const parent = path[0] ?? documents.ids.NoParent
+ const rank = makeRank(lastRank, undefined)
+ update(projectMeta, { parent, path, rank })
+ }
+
+ let key: keyof DocumentBundle
+ for (key in bundle) {
+ bundle[key].forEach((doc) => {
+ update(doc, { space: cx.targetSpace._id })
+ })
+ }
+
+ for (const m of bundle.ProjectMeta) update(m, { project })
+ for (const m of bundle.ProjectDocument) update(m, { project })
+ }
+
+ if (mode === 'check') return true
+
+ const ops = client.apply()
+ for (const u of updates) await ops.update(u[0], u[1])
+
+ const commit = await ops.commit()
+ return commit.result
+}
+
/**
* @public
*/
]