From d50ad58526d40a9244f32c9ebe41a9099289c60c Mon Sep 17 00:00:00 2001
From: ThaumRystra <9525416+ThaumRystra@users.noreply.github.com>
Date: Sun, 12 Nov 2023 14:49:15 +0200
Subject: [PATCH] Added the ability to rearrange Docs
---
app/imports/api/docs/Docs.js | 122 +++++++++++++++---
app/imports/client/ui/docs/DocToolbar.vue | 7 +
.../client/ui/docs/DocsRightDrawer.vue | 84 ++++++++++++
app/imports/client/ui/pages/DocsPage.vue | 6 +
app/imports/client/ui/router.js | 2 +
5 files changed, 203 insertions(+), 18 deletions(-)
create mode 100644 app/imports/client/ui/docs/DocsRightDrawer.vue
diff --git a/app/imports/api/docs/Docs.js b/app/imports/api/docs/Docs.js
index cb8aeb4e..6f7e2207 100644
--- a/app/imports/api/docs/Docs.js
+++ b/app/imports/api/docs/Docs.js
@@ -9,8 +9,9 @@ import { storedIconsSchema } from '/imports/api/icons/Icons.js';
import '/imports/api/library/methods/index.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
import { restore } from '/imports/api/parenting/softRemove.js';
-import { reorderDocs } from '/imports/api/parenting/order.js';
-import { getAncestry } from '/imports/api/parenting/parenting.js';
+import { getAncestry, updateParent } from '/imports/api/parenting/parenting.js';
+import { nodeArrayToTree } from '/imports/api/parenting/nodesToTree';
+import { getDocsInDepthFirstOrder } from '/imports/api/parenting/getDescendantsInDepthFirstOrder';
const Docs = new Mongo.Collection('docs');
@@ -184,10 +185,7 @@ const insertDoc = new ValidatedMethod({
}
const docId = Docs.insert(doc);
- reorderDocs({
- collection: Docs,
- ancestorId: 'root',
- });
+ reorderDocs();
return docId;
},
});
@@ -231,10 +229,7 @@ const updateDoc = new ValidatedMethod({
if (pathString === 'name' || pathString === 'urlName') {
rebuildDocAncestors(_id);
}
- reorderDocs({
- collection: Docs,
- ancestorId: 'root',
- });
+ reorderDocs();
return updates;
},
});
@@ -284,10 +279,7 @@ const softRemoveDoc = new ValidatedMethod({
run({ _id }) {
assertDocsEditPermission(this.userId);
softRemove({ _id, collection: Docs });
- reorderDocs({
- collection: Docs,
- ancestorId: 'root',
- });
+ reorderDocs();
}
});
@@ -304,13 +296,105 @@ const restoreDoc = new ValidatedMethod({
run({ _id }) {
assertDocsEditPermission(this.userId);
restore({ _id, collection: Docs });
- reorderDocs({
- collection: Docs,
- ancestorId: 'root',
- });
+ reorderDocs();
}
});
+const organizeDoc = new ValidatedMethod({
+ name: 'docs.organizeDoc',
+ validate: new SimpleSchema({
+ docId: String,
+ parentId: String,
+ order: {
+ type: Number,
+ // Should end in 0.5 to place it reliably between two existing documents
+ },
+ }).validator(),
+ mixins: [RateLimiterMixin],
+ rateLimit: {
+ numRequests: 5,
+ timeInterval: 5000,
+ },
+ run({ docId, parentId, order }) {
+ let doc = Docs.findOne(docId);
+ // The user must be able to edit both the doc and its parent to move it
+ // successfully
+ assertDocsEditPermission(this.userId);
+
+ // Change the doc's parent
+ updateParent({ docRef: { id: docId, collection: 'docs' }, parentRef: { id: parentId, collection: 'docs' } });
+ // Change the doc's order to be a half step ahead of its target location
+ Docs.update(doc._id, { $set: { order } });
+
+ reorderDocs();
+ },
+});
+
+const reorderDoc = new ValidatedMethod({
+ name: 'docs.reorderDoc',
+ validate: new SimpleSchema({
+ docId: String,
+ order: {
+ type: Number,
+ // Should end in 0.5 to place it reliably between two existing documents
+ },
+ }).validator(),
+ mixins: [RateLimiterMixin],
+ rateLimit: {
+ numRequests: 5,
+ timeInterval: 5000,
+ },
+ run({ docId, order }) {
+ assertDocsEditPermission(this.userId);
+ Docs.update(docId, {
+ $set: { order }
+ });
+ reorderDocs();
+ },
+});
+
+function reorderDocs() {
+ const docs = Docs.find({ removed: { $ne: true } }, { sort: { order: 1 } }).fetch();
+ const forest = nodeArrayToTree(docs);
+ const orderedDocs = getDocsInDepthFirstOrder(forest);
+ const bulkWrite = [];
+ orderedDocs.forEach((doc, index) => {
+ if (doc.order !== index) {
+ bulkWrite.push({
+ updateOne: {
+ filter: { _id: doc._id },
+ update: { $set: { order: index } },
+ },
+ });
+ }
+ });
+ if (Meteor.isServer && bulkWrite.length) {
+ Docs.rawCollection().bulkWrite(
+ bulkWrite,
+ { ordered: false },
+ function (e) {
+ if (e) {
+ console.error('Bulk write failed: ');
+ console.error(e);
+ }
+ // Rebuild the ancestors of all the docs
+ // This is a pretty slow way to do anything, but docs hardly ever get rearranged
+ docs.forEach(doc => {
+ rebuildDocAncestors(doc._id);
+ });
+ }
+ );
+ } else {
+ bulkWrite.forEach(op => {
+ Docs.update(
+ op.updateOne.filter,
+ op.updateOne.update,
+ { selector: { type: 'any' } }
+ );
+ });
+ }
+}
+
export {
DocSchema,
insertDoc,
@@ -319,6 +403,8 @@ export {
pullFromDoc,
softRemoveDoc,
restoreDoc,
+ organizeDoc,
+ reorderDoc,
};
export default Docs;
diff --git a/app/imports/client/ui/docs/DocToolbar.vue b/app/imports/client/ui/docs/DocToolbar.vue
index 56c77859..eb65aabf 100644
--- a/app/imports/client/ui/docs/DocToolbar.vue
+++ b/app/imports/client/ui/docs/DocToolbar.vue
@@ -12,6 +12,12 @@
Documentation
+
+ mdi-file-tree
+
+
+
+
+
+
+
+
+
diff --git a/app/imports/client/ui/pages/DocsPage.vue b/app/imports/client/ui/pages/DocsPage.vue
index 875af5d1..53afbfab 100644
--- a/app/imports/client/ui/pages/DocsPage.vue
+++ b/app/imports/client/ui/pages/DocsPage.vue
@@ -112,10 +112,14 @@ export default {
if (!this.doc) return Docs.find({
'parent': undefined,
removed: { $ne: true },
+ }, {
+ sort: { order: 1 }
});
return Docs.find({
'parent.id': this.doc._id,
removed: { $ne: true },
+ }, {
+ sort: { order: 1 }
})
},
siblingDocs() {
@@ -123,6 +127,8 @@ export default {
return Docs.find({
'parent.id': this.doc.parent?.id,
removed: { $ne: true },
+ }, {
+ sort: { order: 1 }
});
},
editing() {
diff --git a/app/imports/client/ui/router.js b/app/imports/client/ui/router.js
index 4f3fe2cc..c2189396 100644
--- a/app/imports/client/ui/router.js
+++ b/app/imports/client/ui/router.js
@@ -40,6 +40,7 @@ const Maintenance = () => import('/imports/client/ui/pages/Maintenance.vue');
const Files = () => import('/imports/client/ui/pages/Files.vue');
const DocsPage = () => import('/imports/client/ui/pages/DocsPage.vue');
const DocToolbar = () => import('/imports/client/ui/docs/DocToolbar.vue');
+const DocsRightDrawer = () => import('/imports/client/ui/docs/DocsRightDrawer.vue');
// Not found
const NotFound = () => import('/imports/client/ui/pages/NotFound.vue');
@@ -285,6 +286,7 @@ RouterFactory.configure(router => {
components: {
default: DocsPage,
toolbar: DocToolbar,
+ rightDrawer: DocsRightDrawer,
},
meta: {
title: 'Documentation',