diff --git a/plugins/controlled-documents-assets/lang/cs.json b/plugins/controlled-documents-assets/lang/cs.json index 4241ef4869..fef51c8a70 100644 --- a/plugins/controlled-documents-assets/lang/cs.json +++ b/plugins/controlled-documents-assets/lang/cs.json @@ -302,7 +302,10 @@ "MakeDocumentObsoleteDialog": "Označit {count, plural, one {dokument jako zastaralý} other {dokumenty jako zastaralé}}", "MakeDocumentObsoleteConfirm": "Opravdu chcete označit následující dokumenty jako zastaralé: {titles}?", - "LatestVersionHint": "nejnovější" + "LatestVersionHint": "nejnovější", + + "CannotDeleteFolder": "Složku nelze smazat", + "CannotDeleteFolderHint": "Před odstraněním složky prosím přesuňte všechny podřízené dokumenty na jiné místo." }, "controlledDocStates": { "Empty": "", diff --git a/plugins/controlled-documents-assets/lang/de.json b/plugins/controlled-documents-assets/lang/de.json index f1866edaf9..d0885596ca 100644 --- a/plugins/controlled-documents-assets/lang/de.json +++ b/plugins/controlled-documents-assets/lang/de.json @@ -309,7 +309,10 @@ "MakeDocumentObsoleteDialog": "{count, plural, one {Dokument als veraltet markieren} other {Dokumente als veraltet markieren}}", "MakeDocumentObsoleteConfirm": "Möchten Sie die folgenden Dokumente wirklich als veraltet markieren: {titles}?", - "LatestVersionHint": "neueste" + "LatestVersionHint": "neueste", + + "CannotDeleteFolder": "Der Ordner kann nicht gelöscht werden", + "CannotDeleteFolderHint": "Bitte verschieben Sie alle untergeordneten Dokumente an einen anderen Ort, bevor Sie den Ordner löschen." }, "controlledDocStates": { "Empty": "", diff --git a/plugins/controlled-documents-assets/lang/en.json b/plugins/controlled-documents-assets/lang/en.json index a98bc2d854..feefcd63f8 100644 --- a/plugins/controlled-documents-assets/lang/en.json +++ b/plugins/controlled-documents-assets/lang/en.json @@ -311,7 +311,10 @@ "RenameFolder": "Rename folder", "CreateChildFolder": "Create child folder", - "LatestVersionHint": "latest" + "LatestVersionHint": "latest", + + "CannotDeleteFolder": "The folder cannot be deleted", + "CannotDeleteFolderHint": "Please move all child documents to another location before deleting the folder." }, "controlledDocStates": { "Empty": "", diff --git a/plugins/controlled-documents-assets/lang/fr.json b/plugins/controlled-documents-assets/lang/fr.json index 82b93dbd9b..162e4e31b5 100644 --- a/plugins/controlled-documents-assets/lang/fr.json +++ b/plugins/controlled-documents-assets/lang/fr.json @@ -269,7 +269,10 @@ "MakeDocumentObsoleteDialog": "Marquer {count, plural, one {le document comme obsolète} other {les documents comme obsolètes}}", "MakeDocumentObsoleteConfirm": "Voulez-vous vraiment marquer les documents suivants comme obsolètes : {titles} ?", - "LatestVersionHint": "dernier" + "LatestVersionHint": "dernier", + + "CannotDeleteFolder": "Le dossier ne peut pas être supprimé", + "CannotDeleteFolderHint": "Veuillez déplacer tous les documents enfants vers un autre emplacement avant de supprimer le dossier." }, "controlledDocStates": { "Empty": "", diff --git a/plugins/controlled-documents-assets/lang/it.json b/plugins/controlled-documents-assets/lang/it.json index 79b28decd0..f6701c1beb 100644 --- a/plugins/controlled-documents-assets/lang/it.json +++ b/plugins/controlled-documents-assets/lang/it.json @@ -267,7 +267,10 @@ "MakeDocumentObsoleteDialog": "Segna {count, plural, one {il documento come obsoleto} other {i documenti come obsoleti}}", "MakeDocumentObsoleteConfirm": "Vuoi davvero segnare i seguenti documenti come obsoleti: {titles}?", - "LatestVersionHint": "ultimo" + "LatestVersionHint": "ultimo", + + "CannotDeleteFolder": "Impossibile eliminare la cartella", + "CannotDeleteFolderHint": "Sposta tutti i documenti figli in un'altra posizione prima di eliminare la cartella." }, "controlledDocStates": { "Empty": "", diff --git a/plugins/controlled-documents-assets/lang/ru.json b/plugins/controlled-documents-assets/lang/ru.json index 37fc54813b..6278b7a4ac 100644 --- a/plugins/controlled-documents-assets/lang/ru.json +++ b/plugins/controlled-documents-assets/lang/ru.json @@ -311,7 +311,10 @@ "MakeDocumentObsoleteDialog": "Пометить {count, plural, one {документ как устаревший} other {документы как устаревшие}}", "MakeDocumentObsoleteConfirm": "Вы действительно хотите пометить следующие документы как устаревшие: {titles}?", - "LatestVersionHint": "последняя" + "LatestVersionHint": "последняя", + + "CannotDeleteFolder": "Папка не может быть удалена", + "CannotDeleteFolderHint": "Пожалуйста, переместите все дочерние документы в другое место перед удалением папки." }, "controlledDocStates": { "Empty": "", diff --git a/plugins/controlled-documents-assets/lang/zh.json b/plugins/controlled-documents-assets/lang/zh.json index 7b4c2f584e..8791c59c12 100644 --- a/plugins/controlled-documents-assets/lang/zh.json +++ b/plugins/controlled-documents-assets/lang/zh.json @@ -308,7 +308,10 @@ "MakeDocumentObsoleteDialog": "标记 {count, plural, one {文档为过时} other {文档为过时}}", "MakeDocumentObsoleteConfirm": "您确定要将以下文档标记为过时吗:{titles}?", - "LatestVersionHint": "最新" + "LatestVersionHint": "最新", + + "CannotDeleteFolder": "无法删除文件夹", + "CannotDeleteFolderHint": "请在删除文件夹之前将所有子文档移动到其他位置。" }, "controlledDocStates": { "Empty": "", diff --git a/plugins/controlled-documents-resources/src/plugin.ts b/plugins/controlled-documents-resources/src/plugin.ts index 04a794ca23..548550e46f 100644 --- a/plugins/controlled-documents-resources/src/plugin.ts +++ b/plugins/controlled-documents-resources/src/plugin.ts @@ -214,7 +214,10 @@ export default mergeIds(documentsId, documents, { CreateDocumentTemplateFailed: '' as IntlString, TryAgain: '' as IntlString, - LatestVersionHint: '' as IntlString + LatestVersionHint: '' as IntlString, + + CannotDeleteFolder: '' as IntlString, + CannotDeleteFolderHint: '' as IntlString }, controlledDocStates: { Empty: '' as IntlString, diff --git a/plugins/controlled-documents-resources/src/utils.ts b/plugins/controlled-documents-resources/src/utils.ts index bd411abe75..fc2497a336 100644 --- a/plugins/controlled-documents-resources/src/utils.ts +++ b/plugins/controlled-documents-resources/src/utils.ts @@ -33,7 +33,8 @@ import documents, { compareDocumentVersions, emptyBundle, getDocumentName, - getFirstRank + getFirstRank, + transferDocuments } from '@hcengineering/controlled-documents' import core, { type Class, @@ -53,7 +54,7 @@ import core, { getCurrentAccount } from '@hcengineering/core' import { type IntlString, translate } from '@hcengineering/platform' -import { createQuery, getClient } from '@hcengineering/presentation' +import { createQuery, getClient, MessageBox } from '@hcengineering/presentation' import request, { type Request, RequestStatus } from '@hcengineering/request' import { isEmptyMarkup } from '@hcengineering/text' import { type Location, getUserTimezone, showPopup } from '@hcengineering/ui' @@ -551,46 +552,22 @@ export async function canRenameFolder ( return await isEditableProject(doc.project) } -export async function canDeleteFolder (obj?: Doc | Doc[]): Promise { - if (obj == null) { - return false - } +export async function canDeleteFolder (doc: ProjectDocument): Promise { + if (doc?._class === undefined) return false const client = getClient() const hierarchy = client.getHierarchy() - const objs = (Array.isArray(obj) ? obj : [obj]) as Document[] - - const isFolders = objs.every((doc) => isFolder(hierarchy, doc)) - if (!isFolders) { - return false - } - - const folders = objs as unknown as ProjectDocument[] - - const pjMeta = await client.findAll(documents.class.ProjectMeta, { _id: { $in: folders.map((f) => f.attachedTo) } }) - const directChildren = await client.findAll(documents.class.ProjectMeta, { - parent: { $in: pjMeta.map((p) => p.meta) } - }) - - if (directChildren.length > 0) { + if (!isFolder(hierarchy, doc)) { return false } const currentUser = getCurrentAccount() as PersonAccount - const isOwner = objs.every((doc) => doc.owner === currentUser.person) - - if (isOwner) { + if (doc.createdBy === currentUser._id) { return true } - 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)) + return await checkPermission(getClient(), documents.permission.ArchiveDocument, doc.space) } export async function canDeleteDocumentCategory (doc?: Doc | Doc[]): Promise { @@ -835,25 +812,70 @@ export async function renameFolder (doc: ProjectDocument): Promise { showPopup(documents.component.CreateFolder, props) } -export async function deleteFolder (obj: ProjectDocument | ProjectDocument[]): Promise { +export async function deleteFolder (obj: ProjectDocument): Promise { + const success = await _deleteFolder(obj) + if (!success) { + showPopup(MessageBox, { + label: documentsResources.string.CannotDeleteFolder, + message: documentsResources.string.CannotDeleteFolderHint, + canSubmit: false + }) + } +} + +async function _deleteFolder (obj: ProjectDocument): Promise { const client = getClient() if (!(await canDeleteFolder(obj))) { - return + return false } - const objs = Array.isArray(obj) ? obj : [obj] + const space = obj.space + const project = obj.project - const pjmeta = await client.findAll(documents.class.ProjectMeta, { _id: { $in: objs.map((p) => p.attachedTo) } }) - const meta = await client.findAll(documents.class.DocumentMeta, { _id: { $in: pjmeta.map((p) => p.meta) } }) + const bundle: DocumentBundle = { + ...emptyBundle(), + ProjectMeta: await client.findAll(documents.class.ProjectMeta, { space, project }), + ProjectDocument: await client.findAll(documents.class.ProjectDocument, { space, project }), + DocumentMeta: await client.findAll(documents.class.DocumentMeta, { space }), + ControlledDocument: await client.findAll(documents.class.ControlledDocument, { space }) + } + + const prjMeta = bundle.ProjectMeta.find((m) => m._id === obj.attachedTo) + if (prjMeta === undefined) return false + + const tree = new ProjectDocumentTree(bundle, { keepRemoved: true }) + + const movableStates = [DocumentState.Deleted, DocumentState.Obsolete] + const descendants = tree.descendantsOf(prjMeta.meta) + for (const meta of descendants) { + const bundle = tree.bundleOf(meta) + const docs = bundle?.ControlledDocument ?? [] + const movable = docs.every((d) => movableStates.includes(d.state)) + if (!movable) return false + } + + const children = tree.childrenOf(prjMeta.meta) + if (children.length > 0) { + await transferDocuments(client, { + sourceDocumentIds: children, + sourceSpaceId: obj.space, + sourceProjectId: obj.project, + + targetSpaceId: obj.space, + targetProjectId: obj.project + }) + } + + const toRemoval = [obj, prjMeta] - const docsToRemove = [...objs, ...pjmeta, ...meta] const ops = client.apply() - for (const doc of docsToRemove) { + for (const doc of toRemoval) { await ops.remove(doc) } await ops.commit() + return true } export async function createDocument (space: DocumentSpace): Promise { diff --git a/plugins/controlled-documents/src/utils.ts b/plugins/controlled-documents/src/utils.ts index f080cf494f..d08fbad53f 100644 --- a/plugins/controlled-documents/src/utils.ts +++ b/plugins/controlled-documents/src/utils.ts @@ -197,13 +197,17 @@ function extractPresentableStateFromDocumentBundle (bundle: DocumentBundle, prjm return bundle } +export interface ProjectDocumentTreeOptions { + keepRemoved?: boolean +} + export class ProjectDocumentTree { parents: Map, Ref> nodesChildren: Map, DocumentBundle[]> nodes: Map, DocumentBundle> links: Map, Ref> - constructor (bundle?: DocumentBundle) { + constructor (bundle?: DocumentBundle, options?: ProjectDocumentTreeOptions) { bundle = { ...emptyBundle(), ...bundle } const { bundles, links } = compileBundles(bundle) this.links = links @@ -211,6 +215,8 @@ export class ProjectDocumentTree { this.nodesChildren = new Map() this.parents = new Map() + const keepRemoved = options?.keepRemoved ?? false + bundles.sort((a, b) => { const rankA = a.ProjectMeta[0]?.rank ?? '' const rankB = b.ProjectMeta[0]?.rank ?? '' @@ -224,7 +230,7 @@ export class ProjectDocumentTree { const presentable = extractPresentableStateFromDocumentBundle(bundle, prjmeta) this.nodes.set(prjmeta.meta, presentable) - const parent = prjmeta.path[0] ?? documents.ids.NoParent + const parent = prjmeta.parent ?? documents.ids.NoParent this.parents.set(prjmeta.meta, parent) if (!this.nodesChildren.has(parent)) { @@ -234,10 +240,12 @@ export class ProjectDocumentTree { } const nodesForRemoval = new Set>() - for (const [id, node] of this.nodes) { - const state = node.ControlledDocument[0]?.state - const isRemoved = state === DocumentState.Obsolete || state === DocumentState.Deleted - if (isRemoved) nodesForRemoval.add(id) + if (!keepRemoved) { + for (const [id, node] of this.nodes) { + const state = node.ControlledDocument[0]?.state + const isRemoved = state === DocumentState.Obsolete || state === DocumentState.Deleted + if (isRemoved) nodesForRemoval.add(id) + } } for (const id of this.nodes.keys()) { @@ -320,8 +328,12 @@ export async function findProjectDocsHierarchy ( space: Ref, project?: Ref> ): Promise { - const ProjectMeta = await client.findAll(documents.class.ProjectMeta, { space, project }) - return new ProjectDocumentTree({ ...emptyBundle(), ProjectMeta }) + const bundle: DocumentBundle = { + ...emptyBundle(), + DocumentMeta: await client.findAll(documents.class.DocumentMeta, { space }), + ProjectMeta: await client.findAll(documents.class.ProjectMeta, { space, project }) + } + return new ProjectDocumentTree(bundle, { keepRemoved: true }) } export interface DocumentBundle { @@ -507,20 +519,30 @@ async function _buildDocumentTransferContext ( client: TxOperations, request: DocumentTransferRequest ): Promise { + const isSameSpace = request.sourceSpaceId === request.targetSpaceId + const sourceTree = await findProjectDocsHierarchy(client, request.sourceSpaceId, request.sourceProjectId) - const targetTree = await findProjectDocsHierarchy(client, request.targetSpaceId, request.targetProjectId) + const targetTree = isSameSpace + ? sourceTree + : await findProjectDocsHierarchy(client, request.targetSpaceId, request.targetProjectId) const docIds = new Set>(request.sourceDocumentIds) for (const id of request.sourceDocumentIds) { sourceTree.descendantsOf(id).forEach((d) => docIds.add(d)) } + if (request.targetParentId !== undefined && docIds.has(request.targetParentId)) { + return + } + 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 }) + const targetSpace = isSameSpace + ? sourceSpace + : await client.findOne(documents.class.DocumentSpace, { _id: request.targetSpaceId }) if (sourceSpace === undefined || targetSpace === undefined) return @@ -565,7 +587,6 @@ async function _transferDocuments ( 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() @@ -621,13 +642,17 @@ async function _transferDocuments ( let key: keyof DocumentBundle for (key in bundle) { - bundle[key].forEach((doc) => { - update(doc, { space: cx.targetSpace._id }) - }) + for (const doc of bundle[key]) { + const space = cx.targetSpace._id + if (doc.space !== space) update(doc, { space }) + } + } + for (const m of bundle.ProjectMeta) { + if (m.project !== project) update(m, { project }) + } + for (const m of bundle.ProjectDocument) { + if (m.project !== project) update(m, { project }) } - - for (const m of bundle.ProjectMeta) update(m, { project }) - for (const m of bundle.ProjectDocument) update(m, { project }) } if (mode === 'check') return true