Fixed showstopping bugs with tree organize functions

This commit is contained in:
ThaumRystra
2023-12-31 18:06:31 +02:00
parent 9d07953a40
commit e886be8f04
12 changed files with 360 additions and 181 deletions

View File

@@ -115,6 +115,13 @@ for (let key in propertySchemasIndex) {
schema.extend(propertySchemasIndex[key]); schema.extend(propertySchemasIndex[key]);
schema.extend(ChildSchema); schema.extend(ChildSchema);
schema.extend(SoftRemovableSchema); schema.extend(SoftRemovableSchema);
// Use the any schema as a default schema for the collection
if (key === 'any') {
// @ts-expect-error don't have types for .attachSchema
LibraryNodes.attachSchema(schema);
}
// TODO make this an else branch and remove all {selector: {type: any}} options
// @ts-expect-error don't have types for .attachSchema
LibraryNodes.attachSchema(schema, { LibraryNodes.attachSchema(schema, {
selector: { type: key } selector: { type: key }
}); });

View File

@@ -1,5 +1,5 @@
import '/imports/api/simpleSchemaConfig'; import '/imports/api/simpleSchemaConfig';
import { docsToForest, calculateNestedSetOperations, getFilter, moveDocWithinRoot } from '/imports/api/parenting/parentingFunctions' import { docsToForest, calculateNestedSetOperations, getFilter, moveDocWithinRoot, moveDocBetweenRoots } from '/imports/api/parenting/parentingFunctions'
import { TreeDoc } from '/imports/api/parenting/ChildSchema'; import { TreeDoc } from '/imports/api/parenting/ChildSchema';
import { assert } from 'chai'; import { assert } from 'chai';
@@ -188,7 +188,7 @@ describe('Document tree filters can fetch other documents based on their positio
}); });
}); });
describe('Document can be moved without breaking the tree', function () { describe('Document can be moved withing root without breaking the tree', function () {
/** /**
* Test the following structure * Test the following structure
* *
@@ -250,6 +250,37 @@ describe('Document can be moved without breaking the tree', function () {
doc('Mains', 20, 21, 'Vegetarian'), doc('Mains', 20, 21, 'Vegetarian'),
]); ]);
}); });
it('can move a document within its parent to the start of the tree', async function () {
const videosDoc = await treeCollection.findOneAsync({ _id: 'Videos' });
if (!videosDoc) throw new Error('Languages doc not found');
await moveDocWithinRoot(videosDoc, treeCollection, 0.5);
/**
* Expected resulting structure
*
* 1 Videos 12 13 Books 24
* ┃ ┃
* 2 Cooking 11 14 Programming 23
* ┏━━━━━━━━┻━━━━━━━━━┓ ┏━━━━━━━━┻━━━━━━━━━┓
* 3 Meat 4 5 Vegetarian 10 15 Languages 16 17 Databases 22
* ┏━━━━━━━┻━━━━━━━┓ ┏━━━━━━━┻━━━━━━━┓
* 6 Pasta 7 8 Mains 9 18 MongoDB 19 20 dbm 21
**/
const docs = await treeCollection.find({}, { sort: { left: 1 } }).fetchAsync();
assert.deepEqual(docs, [
doc('Videos', 1, 12, undefined),
doc('Cooking', 2, 11, 'Videos'),
doc('Meat', 3, 4, 'Cooking'),
doc('Vegetarian', 5, 10, 'Cooking'),
doc('Pasta', 6, 7, 'Vegetarian'),
doc('Mains', 8, 9, 'Vegetarian'),
doc('Books', 13, 24, undefined),
doc('Programming', 14, 23, 'Books'),
doc('Languages', 15, 16, 'Programming'),
doc('Databases', 17, 22, 'Programming'),
doc('MongoDB', 18, 19, 'Databases'),
doc('dbm', 20, 21, 'Databases'),
]);
});
it('can move a document to a whole new parent', async function () { it('can move a document to a whole new parent', async function () {
const videos = await treeCollection.findOneAsync({ _id: 'Videos' }); const videos = await treeCollection.findOneAsync({ _id: 'Videos' });
if (!videos) throw new Error('Videos doc not found'); if (!videos) throw new Error('Videos doc not found');
@@ -291,4 +322,74 @@ describe('Document can be moved without breaking the tree', function () {
}); });
// TODO test moving between roots describe('Documents can be moved between roots without breaking the trees', function () {
/**
* Test the following structure
* Root 1 Root 2
* 1 Books 12 1 Videos 12
* ┃ ┃
* 2 Programming 11 2 Cooking 11
* ┏━━━━━━━━┻━━━━━━━━━┓ ┏━━━━━━━━┻━━━━━━━━━┓
* 3 Languages 4 5 Databases 10 3 Meat 4 5 Vegetarian 10
* ┏━━━━━━━┻━━━━━━━┓ ┏━━━━━━━┻━━━━━━━┓
* 6 MongoDB 7 8 dbm 9 6 Pasta 7 8 Mains 9
*/
const treeCollection: Mongo.Collection<TreeDoc> = new Mongo.Collection('treeDocsMoveBetween');
const doc = function (_id, left, right, parentId, rootId): TreeDoc {
const doc = { _id, root: { id: rootId, collection: 'someCol' }, left, right, parentId };
if (!parentId) delete doc.parentId;
return doc;
}
beforeEach(function () {
treeCollection.remove({});
[
doc('Books', 1, 12, undefined, 'root1'),
doc('Programming', 2, 11, 'Books', 'root1'),
doc('Languages', 3, 4, 'Programming', 'root1'),
doc('Databases', 5, 10, 'Programming', 'root1'),
doc('MongoDB', 6, 7, 'Databases', 'root1'),
doc('dbm', 8, 9, 'Databases', 'root1'),
doc('Videos', 1, 12, undefined, 'root2'),
doc('Cooking', 2, 11, 'Videos', 'root2'),
doc('Meat', 3, 4, 'Cooking', 'root2'),
doc('Vegetarian', 5, 10, 'Cooking', 'root2'),
doc('Pasta', 6, 7, 'Vegetarian', 'root2'),
doc('Mains', 8, 9, 'Vegetarian', 'root2'),
].map(doc => {
return treeCollection.insert(doc);
});
});
it('can move a document from one root to another', async function () {
/**
* Move veg to languages
* Root 1 Root 2
* 1 Books 18 1 Videos 6
* ┃ ┃
* 2 Programming 17 2 Cooking 5
* ┏━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━┓ ┃
* 3 Languages 10 11 Databases 16 3 Meat 4
* ┃ ┏━━━━━━━┻━━━━━━┓
* 4 Vegetarian 9 12 MongoDB 13 14 dbm 15
* ┏━━━━━━━┻━━━━━━━┓
* 5 Pasta 6 7 Mains 8
*/
const vegDoc = await treeCollection.findOneAsync({ _id: 'Vegetarian' });
if (!vegDoc) throw new Error('Vegetarian doc not found');
await moveDocBetweenRoots(vegDoc, treeCollection, { id: 'root1', collection: 'someCol' }, 3.5);
const docs = await treeCollection.find({}, { sort: { 'root.id': 1, left: 1 } }).fetchAsync();
assert.deepEqual(docs, [
doc('Books', 1, 18, undefined, 'root1'),
doc('Programming', 2, 17, 'Books', 'root1'),
doc('Languages', 3, 10, 'Programming', 'root1'),
doc('Vegetarian', 4, 9, 'Languages', 'root1'),
doc('Pasta', 5, 6, 'Vegetarian', 'root1'),
doc('Mains', 7, 8, 'Vegetarian', 'root1'),
doc('Databases', 11, 16, 'Programming', 'root1'),
doc('MongoDB', 12, 13, 'Databases', 'root1'),
doc('dbm', 14, 15, 'Databases', 'root1'),
doc('Videos', 1, 6, undefined, 'root2'),
doc('Cooking', 2, 5, 'Videos', 'root2'),
doc('Meat', 3, 4, 'Cooking', 'root2'),
]);
});
});

View File

@@ -1,4 +1,4 @@
import { chain, reverse } from 'lodash'; import { chain, reverse, set } from 'lodash';
import { TreeDoc, treeDocFields, Reference } from '/imports/api/parenting/ChildSchema'; import { TreeDoc, treeDocFields, Reference } from '/imports/api/parenting/ChildSchema';
import { getProperties } from '/imports/api/engine/loadCreatures'; import { getProperties } from '/imports/api/engine/loadCreatures';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
@@ -89,20 +89,19 @@ type FilteredDoc = {
_ancestorOfMatchedDocument?: boolean, _ancestorOfMatchedDocument?: boolean,
} & TreeDoc; } & TreeDoc;
let filterToForest: undefined | ((...any) => TreeNode<FilteredDoc>[]) = undefined; export function filterToForest(
if (Meteor.isClient) filterToForest = function (
collection: Mongo.Collection<TreeDoc>, collection: Mongo.Collection<TreeDoc>,
rootId: string, rootId: string,
filter: Mongo.Selector<TreeDoc>, filter?: Mongo.Query<TreeDoc>,
{ {
options = <Mongo.Options<object>>{}, options = <Mongo.Options<object>>{},
includeFilteredDocAncestors = false, includeFilteredDocAncestors = false,
includeFilteredDocDescendants = false includeFilteredDocDescendants = false
} = {} } = {}
): TreeNode<FilteredDoc>[] { ): TreeNode<FilteredDoc>[] {
if (!Meteor.isClient) throw 'Only available on the client';
// Setup the filter // Setup the filter
let collectionFilter = { let collectionFilter: Mongo.Query<TreeDoc> = {
'root.id': rootId, 'root.id': rootId,
'removed': { $ne: true }, 'removed': { $ne: true },
}; };
@@ -173,8 +172,6 @@ if (Meteor.isClient) filterToForest = function (
return docsToForest(nodes); return docsToForest(nodes);
} }
export { filterToForest };
type ForestAndOrphans = { forest: TreeNode<TreeDoc>[], orphanIds: string[] } type ForestAndOrphans = { forest: TreeNode<TreeDoc>[], orphanIds: string[] }
/** /**
* Takes a complete set of documents and builds a forest using just their `.parentIds` * Takes a complete set of documents and builds a forest using just their `.parentIds`
@@ -385,48 +382,33 @@ export async function moveDocWithinRoot(doc: TreeDoc, collection: Mongo.Collecti
}); });
const newParentId = newParent?._id; const newParentId = newParent?._id;
// get ids of the doc and its children, so we can move them even after other docs have been // Use bulk operations with $set only, because using $inc caused a lot of trouble with both
// moved around to potentially overlap the moving doc // latency compensation and oplog tailing
const movedIds = await collection.find({ const bulkOps: any[] = [];
// Move the doc and its children the move distance
await collection.find({
'root.id': doc.root.id, 'root.id': doc.root.id,
left: { $gte: doc.left }, left: { $gte: doc.left },
right: { $lte: doc.right }, right: { $lte: doc.right },
}, { }, {
fields: { _id: 1 } fields: { _id: 1, left: 1, right: 1 },
}).mapAsync(doc => doc._id); }).forEachAsync(moveDoc => {
const update = {
// Move all the lefts and rights of documents between the current doc edge and the destination $set: {
const moveIncludedLeft = collection.updateAsync({ left: (moveDoc.left + move) || 0,
'root.id': doc.root.id, right: (moveDoc.right + move) || 0,
left: { $gt: includedRange.left, $lt: includedRange.right }, }
}, { };
$inc: { left: shiftDistance } bulkOps.push({
}, { updateOne: {
multi: true filter: { _id: moveDoc._id },
}); update,
const moveIncludedRight = collection.updateAsync({ }
'root.id': doc.root.id, });
right: { $gt: includedRange.left, $lt: includedRange.right },
}, {
$inc: { right: shiftDistance }
}, {
multi: true
}); });
// Move the doc and its children to the new location // Change the doc's parent if necessary
const moveDocAndChildren = collection.updateAsync({
_id: { $in: movedIds }
}, {
$inc: {
left: move,
right: move,
}
}, {
multi: true,
});
// Set the doc's parent to the new parentId
let changeParent: Promise<number> | undefined;
if (newParentId !== doc.parentId) { if (newParentId !== doc.parentId) {
let update; let update;
if (newParentId) { if (newParentId) {
@@ -434,9 +416,41 @@ export async function moveDocWithinRoot(doc: TreeDoc, collection: Mongo.Collecti
} else { } else {
update = { $unset: { parentId: 1 } }; update = { $unset: { parentId: 1 } };
} }
changeParent = collection.updateAsync(doc._id, update); bulkOps.push({
updateOne: {
filter: { _id: doc._id },
update,
}
});
} }
return Promise.all([moveIncludedLeft, moveIncludedRight, moveDocAndChildren, changeParent]);
// Move all the lefts and rights of documents between the current doc edge and the destination
await collection.find({
'root.id': doc.root.id,
$or: [
{ left: { $gt: includedRange.left, $lt: includedRange.right } },
{ right: { $gt: includedRange.left, $lt: includedRange.right } },
],
}, {
fields: { _id: 1, left: 1, right: 1 },
}).forEachAsync(doc => {
const $set: { [P in keyof TreeDoc]?: TreeDoc[P] } = {};
if (doc.left > includedRange.left && doc.left < includedRange.right) {
$set.left = (doc.left + shiftDistance) || 0;
}
if (doc.right > includedRange.left && doc.right < includedRange.right) {
$set.right = (doc.right + shiftDistance || 0);
}
bulkOps.push({
updateOne: {
filter: { _id: doc._id },
update: { $set },
}
});
});
await writeBulkOperations(collection, bulkOps);
return rebuildNestedSets(collection, doc.root.id);
} }
export async function moveDocBetweenRoots(doc: TreeDoc, collection: Mongo.Collection<TreeDoc>, newRoot: Reference, newPosition: number) { export async function moveDocBetweenRoots(doc: TreeDoc, collection: Mongo.Collection<TreeDoc>, newRoot: Reference, newPosition: number) {
@@ -444,65 +458,117 @@ export async function moveDocBetweenRoots(doc: TreeDoc, collection: Mongo.Collec
throw new Meteor.Error('invalid-move', 'Document is already in the given root') throw new Meteor.Error('invalid-move', 'Document is already in the given root')
} }
// get ids of the doc and its children, so we can move them even after other docs have been // Use bulk operations with $set only, because using $inc caused a lot of trouble with both
// moved around to potentially overlap the moving doc // latency compensation and oplog tailing
const movedIds = await collection.find({ const bulkOps: {
updateOne: {
filter: Mongo.Query<TreeDoc>,
update: Mongo.Modifier<TreeDoc>
}
}[] = [];
// Get the new parent of the doc after the move
const newParent = await collection.findOneAsync({
'root.id': newRoot.id,
left: { $lt: newPosition },
right: { $gt: newPosition },
}, {
sort: { left: -1 }, // Many ancestors match, taking the right most one gets the immediate parent
fields: { _id: 1 },
});
const newParentId = newParent?._id;
// Change the doc's parent if necessary
if (newParentId !== doc.parentId) {
let update;
if (newParentId) {
update = { $set: { parentId: newParentId } };
} else {
update = { $unset: { parentId: 1 } };
}
bulkOps.push({
updateOne: {
filter: { _id: doc._id },
update,
}
});
}
// Open a gap in the root we are moving to at the new location
const docSize = doc.right - doc.left + 1;
await collection.find({
'root.id': newRoot.id,
$or: [
{ left: { $gt: newPosition } },
{ right: { $gt: newPosition } },
],
}, {
fields: { _id: 1, left: 1, right: 1 },
}).forEachAsync(openGapDoc => {
const $set: { [P in keyof TreeDoc]?: TreeDoc[P] } = {};
if (openGapDoc.left > newPosition) {
$set.left = (openGapDoc.left + docSize) || 0;
}
if (openGapDoc.right > newPosition) {
$set.right = (openGapDoc.right + docSize) || 0;
}
bulkOps.push({
updateOne: {
filter: { _id: openGapDoc._id },
update: { $set },
}
});
});
// Move the doc and its children the move distance, and set their new root
const move = newPosition + 0.5 - doc.left;
await collection.find({
'root.id': doc.root.id, 'root.id': doc.root.id,
left: { $gte: doc.left }, left: { $gte: doc.left },
right: { $lte: doc.right }, right: { $lte: doc.right },
}, { }, {
fields: { _id: 1 } fields: { _id: 1, left: 1, right: 1 },
}).mapAsync(doc => doc._id); }).forEachAsync(moveDoc => {
bulkOps.push({
updateOne: {
filter: { _id: moveDoc._id },
update: {
$set: {
left: (moveDoc.left + move) || 0,
right: (moveDoc.right + move) || 0,
root: newRoot
}
},
}
});
});
// Close the gap in the root we are leaving // Close the gap in the root we are leaving
const docSize = doc.right - doc.left + 1; await collection.find({
const closeGapLeft = collection.updateAsync({
'root.id': doc.root.id, 'root.id': doc.root.id,
left: { $gt: doc.right }, $or: [
{ left: { $gt: doc.right } },
{ right: { $gt: doc.right } },
],
}, { }, {
$inc: { left: -docSize }, fields: { _id: 1, left: 1, right: 1 },
}, { }).forEachAsync(closeGapDoc => {
multi: true, const $set: { [P in keyof TreeDoc]?: TreeDoc[P] } = {};
}); if (closeGapDoc.left > doc.right) {
const closeGapRight = collection.updateAsync({ $set.left = (closeGapDoc.left - docSize) || 0;
'root.id': doc.root.id, }
right: { $gt: doc.right }, if (closeGapDoc.right > doc.right) {
}, { $set.right = (closeGapDoc.right - docSize || 0);
$inc: { right: -docSize }, }
}, { bulkOps.push({
multi: true, updateOne: {
filter: { _id: closeGapDoc._id },
update: { $set },
}
});
}); });
// Open a gap in the root we are moving to at the new location return writeBulkOperations(collection, bulkOps);
const openGapLeft = collection.updateAsync({
'root.id': newRoot.id,
left: { $gt: newPosition },
}, {
$inc: { left: -docSize },
}, {
multi: true,
});
const openGapRight = collection.updateAsync({
'root.id': newRoot.id,
right: { $gt: newPosition },
}, {
$inc: { right: -docSize },
}, {
multi: true,
});
// Move the docs to the new root and update their left and right positions to land in the gap
const moveDistance = newPosition + 0.5 - doc.left;
const moveDocs = collection.updateAsync({
_id: { $in: movedIds },
}, {
$set: { root: newRoot },
$inc: { left: moveDistance, right: moveDistance },
}, {
multi: true,
});
return Promise.all([closeGapLeft, closeGapRight, openGapLeft, openGapRight, moveDocs]);
} }
/** /**
@@ -658,7 +724,16 @@ export function calculateNestedSetOperations(docs: TreeDoc[]) {
visitedNodes.add(top); visitedNodes.add(top);
stack.pop(); stack.pop();
if (top.doc.right !== count) { if (top.doc.right !== count) {
opsById[top.doc._id].updateOne.update.$set.right = count; if (!opsById[top.doc._id]) {
opsById[top.doc._id] = {
updateOne: {
filter: { _id: top.doc._id },
update: { $set: { right: count } }
}
}
} else {
opsById[top.doc._id].updateOne.update.$set.right = count;
}
} }
count += 1; count += 1;
} else { } else {
@@ -753,21 +828,19 @@ async function writeBulkOperations(collection: Mongo.Collection<TreeDoc>, operat
); );
}); });
} else { } else {
// Don't do latency compensation if there are too many operations, it just causes client
// lag without much benefit
const promises = operations.map(op => { const promises = operations.map(op => {
if (op.updateOne) { if (op.updateOne) {
return collection.updateAsync( return collection.updateAsync(
op.updateOne.filter, op.updateOne.filter,
op.updateOne.update, op.updateOne.update,
{ selector: { type: 'any' } }
); );
} else if (op.updateMany) { } else if (op.updateMany) {
return collection.updateAsync( return collection.updateAsync(
op.updateMany.filter, op.updateMany.filter,
op.updateMany.update, op.updateMany.update,
{ { multi: true },
selector: { type: 'any' },
multi: true,
},
) )
} }
}); });

View File

@@ -1,6 +1,5 @@
// Adds accounts-patreon support to bozhao:link-accounts // Adds accounts-patreon support to bozhao:link-accounts
import { Meteor } from 'meteor/meteor'; import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
export default function linkWithPatreon(options, callback) { export default function linkWithPatreon(options, callback) {
if (!Meteor.userId()) { if (!Meteor.userId()) {

View File

@@ -55,14 +55,15 @@
<tree-node-list <tree-node-list
v-if="showExpanded" v-if="showExpanded"
:node="node" :node="node"
:root="node.root"
:children="computedChildren" :children="computedChildren"
:group="group" :group="group"
:organize="organize" :organize="organize"
:selected-node="selectedNode" :selected-node="selectedNode"
:start-expanded="startExpanded" :start-expanded="startExpanded"
:show-external-details="showExternalDetails" :show-external-details="showExternalDetails"
@reordered="e => $emit('reordered', e)" @move-within-root="e => $emit('move-within-root', e)"
@reorganized="e => $emit('reorganized', e)" @move-between-roots="e => $emit('move-between-roots', e)"
@selected="e => $emit('selected', e)" @selected="e => $emit('selected', e)"
/> />
<div v-else> <div v-else>

View File

@@ -25,8 +25,8 @@
:start-expanded="startExpanded" :start-expanded="startExpanded"
:show-external-details="showExternalDetails" :show-external-details="showExternalDetails"
@selected="e => $emit('selected', e)" @selected="e => $emit('selected', e)"
@reordered="e => $emit('reordered', e)" @move-within-root="e => $emit('move-within-root', e)"
@reorganized="e => $emit('reorganized', e)" @move-between-roots="e => $emit('move-between-roots', e)"
/> />
</draggable> </draggable>
</template> </template>
@@ -45,6 +45,10 @@ export default {
type: Object, type: Object,
default: undefined, default: undefined,
}, },
root: {
type: Object,
required: true,
},
group: { group: {
type: String, type: String,
default: undefined, default: undefined,
@@ -93,32 +97,31 @@ export default {
let event = moved || added; let event = moved || added;
if (event) { if (event) {
let doc = event.element.doc; let doc = event.element.doc;
let newIndex; let newPosition;
if (event.newIndex === 0) { if (!this.children.length) {
newIndex = 0.5; if (this.node) {
} else { newPosition = this.node.left + 0.5;
if (event.newIndex < this.children.length) {
let childAtNewIndex = this.children[event.newIndex];
if (event.newIndex > event.oldIndex) {
newIndex = childAtNewIndex.doc.right + 0.5;
} else {
newIndex = childAtNewIndex.doc.left - 0.5;
}
} else { } else {
let childBeforeNewIndex = this.children[event.newIndex - 1]; newPosition = 0.5;
newIndex = childBeforeNewIndex.doc.right + 0.5;
} }
} else if (event.newIndex < this.children.length) {
let childAtNewIndex = this.children[event.newIndex];
if (event.newIndex > event.oldIndex) {
newPosition = childAtNewIndex.doc.right + 0.5;
} else {
newPosition = childAtNewIndex.doc.left - 0.5;
}
} else {
let childBeforeNewIndex = this.children[event.newIndex - 1];
newPosition = childBeforeNewIndex.doc.right + 0.5;
} }
if (moved) { if (doc.root.id === this.root.id) {
this.$emit('reordered', { doc, newIndex }); this.$emit('move-within-root', { doc, newPosition });
} else if (added) { } else {
this.$emit('reorganized', { doc, parent: this.node, newIndex }); this.$emit('move-between-roots', { doc, newPosition, newRootRef: this.root });
} }
} }
}, },
move() {
return true;
},
}, },
}; };
</script> </script>

View File

@@ -6,16 +6,17 @@
:organize="organize" :organize="organize"
:selected-node="selectedNode" :selected-node="selectedNode"
:start-expanded="expanded" :start-expanded="expanded"
:root="root"
@selected="e => $emit('selected', e)" @selected="e => $emit('selected', e)"
@reordered="reordered" @move-within-root="moveWithinRoot"
@reorganized="reorganized" @move-between-roots="moveBetweenRoots"
/> />
</template> </template>
<script lang="js"> <script lang="js">
import { filterToForest } from '/imports/api/parenting/parentingFunctions'; import { filterToForest } from '/imports/api/parenting/parentingFunctions';
import TreeNodeList from '/imports/client/ui/components/tree/TreeNodeList.vue'; import TreeNodeList from '/imports/client/ui/components/tree/TreeNodeList.vue';
import { organizeDoc, reorderDoc } from '/imports/api/parenting/organizeMethods'; import { moveBetweenRoots, moveWithinRoot } from '/imports/api/parenting/organizeMethods';
import { getCollectionByName } from '/imports/api/parenting/parentingFunctions'; import { getCollectionByName } from '/imports/api/parenting/parentingFunctions';
export default { export default {
@@ -57,38 +58,28 @@ export default {
includeFilteredDocDescendants: true, includeFilteredDocDescendants: true,
} }
) || []; ) || [];
console.log(children)
this.$emit('length', children.length); this.$emit('length', children.length);
return children; return children;
}, },
}, },
methods: { methods: {
reordered({ doc, newIndex }) { moveWithinRoot({ doc, newPosition }) {
reorderDoc.callAsync({ moveWithinRoot.callAsync({
docRef: { docRef: {
id: doc._id, id: doc._id,
collection: this.collection, collection: this.collection,
}, },
order: newIndex, newPosition,
}); });
}, },
reorganized({ doc, parent, newIndex }) { moveBetweenRoots({ doc, newPosition, newRootRef }) {
let parentRef; moveBetweenRoots.callAsync({
if (parent) {
parentRef = {
id: parent._id,
collection: this.collection,
};
} else {
parentRef = this.root;
}
organizeDoc.callAsync({
docRef: { docRef: {
id: doc._id, id: doc._id,
collection: this.collection, collection: this.collection,
}, },
parentRef, newPosition,
order: newIndex, newRootRef,
}); });
}, },
}, },

View File

@@ -6,9 +6,10 @@
:children="libraryChildren" :children="libraryChildren"
:organize="organizeMode" :organize="organizeMode"
:selected-node="selectedNode" :selected-node="selectedNode"
:root="{collection: 'libraries', id: libraryId}"
@selected="e => $emit('selected', e)" @selected="e => $emit('selected', e)"
@reordered="reordered" @move-within-root="moveWithinRoot"
@reorganized="reorganized" @move-between-roots="moveBetweenRoots"
/> />
<v-layout <v-layout
v-else v-else
@@ -29,7 +30,7 @@ import Libraries from '/imports/api/library/Libraries';
import LibraryNodes from '/imports/api/library/LibraryNodes'; import LibraryNodes from '/imports/api/library/LibraryNodes';
import { filterToForest } from '/imports/api/parenting/parentingFunctions'; import { filterToForest } from '/imports/api/parenting/parentingFunctions';
import TreeNodeList from '/imports/client/ui/components/tree/TreeNodeList.vue'; import TreeNodeList from '/imports/client/ui/components/tree/TreeNodeList.vue';
import { organizeDoc, reorderDoc } from '/imports/api/parenting/organizeMethods'; import { moveBetweenRoots, moveWithinRoot } from '/imports/api/parenting/organizeMethods';
export default { export default {
components: { components: {
@@ -92,7 +93,7 @@ export default {
if (!this.library) return; if (!this.library) return;
return filterToForest( return filterToForest(
LibraryNodes, LibraryNodes,
this.library._id, this.libraryId,
this.filter, this.filter,
{ {
includeFilteredDocAncestors: true, includeFilteredDocAncestors: true,
@@ -102,35 +103,23 @@ export default {
}, },
}, },
methods: { methods: {
reordered({ doc, newIndex }) { moveWithinRoot({ doc, newPosition }) {
reorderDoc.callAsync({ moveWithinRoot.callAsync({
docRef: { docRef: {
id: doc._id, id: doc._id,
collection: 'libraryNodes', collection: 'libraryNodes',
}, },
order: newIndex, newPosition,
}); });
}, },
reorganized({ doc, parent, newIndex }) { moveBetweenRoots({ doc, newPosition, newRootRef }) {
let parentRef; moveBetweenRoots.callAsync({
if (parent) {
parentRef = {
id: parent._id,
collection: 'libraryNodes',
};
} else {
parentRef = {
id: this.libraryId,
collection: 'libraries',
};
}
organizeDoc.callAsync({
docRef: { docRef: {
id: doc._id, id: doc._id,
collection: 'libraryNodes', collection: 'libraryNodes',
}, },
parentRef, newPosition,
order: newIndex, newRootRef,
}); });
}, },
}, },

View File

@@ -1,12 +1,13 @@
import { Migrations } from 'meteor/percolate:migrations'; import { Migrations } from 'meteor/percolate:migrations';
import Libraries from '/imports/api/library/Libraries';
import { rebuildNestedSets } from '/imports/api/parenting/parentingFunctions';
// Git version 2.0.59 // Git version 2.0.59
// Database version 3 // Database version 3
Migrations.add({ Migrations.add({
version: 3, version: 3,
name: 'Separates creature property tags from library tags', name: 'Separates creature property tags from library tags',
up: Meteor.wrapAsync(async (_, next) => {
up() {
console.log('migrating up library nodes 2 -> 3'); console.log('migrating up library nodes 2 -> 3');
migrateCollection('libraryNodes'); migrateCollection('libraryNodes');
console.log('migrating up creature props 2 -> 3'); console.log('migrating up creature props 2 -> 3');
@@ -14,7 +15,15 @@ Migrations.add({
console.log('migrating up docs 2 -> 3'); console.log('migrating up docs 2 -> 3');
migrateCollection('docs'); migrateCollection('docs');
console.log('New schema fields added, if it was done correctly remove the old fields manually'); console.log('New schema fields added, if it was done correctly remove the old fields manually');
},
console.log('Rebuilding nested sets for all libraries');
const libraryIds = await Mongo.Collection.get('libraries').find().mapAsync((library) => library._id);
for (const [index, libraryId] of libraryIds.entries()) {
console.log('Rebuilding nested sets for library', index + 1, 'of', libraryIds.length);
await rebuildNestedSets(Mongo.Collection.get('libraryNodes'), libraryId);
}
next();
}),
down() { down() {
throw 'Migrating from version 3 down to version 2 is not supported' throw 'Migrating from version 3 down to version 2 is not supported'
@@ -27,6 +36,7 @@ export function migrateCollection(collectionName: string) {
const collection = Mongo.Collection.get(collectionName); const collection = Mongo.Collection.get(collectionName);
// Copy the parent id field and the root ancestor to the new structure // Copy the parent id field and the root ancestor to the new structure
// Using the mongo aggregation API // Using the mongo aggregation API
// Waring: This will destroy parenting data if the old parenting fields are deleted
return collection.rawCollection().updateMany({}, [ return collection.rawCollection().updateMany({}, [
{ {
$addFields: { $addFields: {

View File

@@ -1,4 +1,3 @@
import { Accounts } from 'meteor/accounts-base'
import emailTemplate from './emailTemplate'; import emailTemplate from './emailTemplate';
Accounts.emailTemplates.from = 'no-reply@dicecloud.com'; Accounts.emailTemplates.from = 'no-reply@dicecloud.com';

View File

@@ -11,9 +11,15 @@ const LIBRARY_NODE_TREE_FIELDS = {
type: 1, type: 1,
icon: 1, icon: 1,
color: 1, color: 1,
// Old tree fields
order: 1, order: 1,
parent: 1, parent: 1,
ancestors: 1, ancestors: 1,
// Tree fields
parentId: 1,
left: 1,
right: 1,
root: 1,
removed: 1, removed: 1,
removedAt: 1, removedAt: 1,
// Actions // Actions
@@ -235,7 +241,7 @@ Meteor.publish('libraryNodes', function (libraryId, extraFields) {
}); });
return [ return [
LibraryNodes.find({ LibraryNodes.find({
'ancestors.id': libraryId, 'root.id': libraryId,
}, { }, {
sort: { order: 1 }, sort: { order: 1 },
fields, fields,

View File

@@ -104,9 +104,9 @@ Meteor.publish('searchLibraryNodes', function (creatureId) {
sort: { sort: {
// `score` property specified in the projection fields above. // `score` property specified in the projection fields above.
// score: { $meta: 'textScore' }, // score: { $meta: 'textScore' },
'ancestors.0.id': 1, 'root.id': 1,
name: 1, name: 1,
order: 1, left: 1,
} }
} }
} else { } else {
@@ -114,9 +114,9 @@ Meteor.publish('searchLibraryNodes', function (creatureId) {
delete filter.$and; delete filter.$and;
options = { options = {
sort: { sort: {
'ancestors.0.id': 1, 'root.id': 1,
name: 1, name: 1,
order: 1, left: 1,
} }
}; };
} }