diff --git a/app/imports/api/library/LibraryNodes.js b/app/imports/api/library/LibraryNodes.js
index 4f9bb401..8b8ec90c 100644
--- a/app/imports/api/library/LibraryNodes.js
+++ b/app/imports/api/library/LibraryNodes.js
@@ -14,6 +14,7 @@ import { storedIconsSchema } from '/imports/api/icons/Icons.js';
import '/imports/api/library/methods/index.js';
import { updateReferenceNodeWork } from '/imports/api/library/methods/updateReferenceNode.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
+import { restore } from '/imports/api/parenting/softRemove.js';
let LibraryNodes = new Mongo.Collection('libraryNodes');
@@ -186,6 +187,25 @@ const softRemoveLibraryNode = new ValidatedMethod({
}
});
+const restoreLibraryNode = new ValidatedMethod({
+ name: 'libraryNodes.restore',
+ validate: new SimpleSchema({
+ _id: SimpleSchema.RegEx.Id
+ }).validator(),
+ mixins: [RateLimiterMixin],
+ rateLimit: {
+ numRequests: 5,
+ timeInterval: 5000,
+ },
+ run({_id}){
+ // Permissions
+ let node = LibraryNodes.findOne(_id);
+ assertNodeEditPermission(node, this.userId);
+ // Do work
+ restore({_id, collection: LibraryNodes});
+ }
+});
+
export default LibraryNodes;
export {
LibraryNodeSchema,
@@ -194,4 +214,5 @@ export {
pullFromLibraryNode,
pushToLibraryNode,
softRemoveLibraryNode,
+ restoreLibraryNode,
};
diff --git a/app/imports/server/cron/deleteSoftRemovedDocuments.js b/app/imports/server/cron/deleteSoftRemovedDocuments.js
index 779fbed4..3758aa27 100644
--- a/app/imports/server/cron/deleteSoftRemovedDocuments.js
+++ b/app/imports/server/cron/deleteSoftRemovedDocuments.js
@@ -10,17 +10,17 @@ Meteor.startup(() => {
];
/**
- * Deletes all soft removed documents that were removed more than 30 minutes ago
+ * Deletes all soft removed documents that were removed more than 1 day ago
* and were not restored
* @return {Number} Number of documents removed
*/
const deleteOldSoftRemovedDocs = function(){
const now = new Date();
- const thirtyMinutesAgo = new Date(now.getTime() - 30*60000);
+ const yesterday = new Date(now.getTime() - (24 * 60 * 60 * 1000));
collections.forEach(collection => {
collection.remove({
removed: true,
- removedAt: {$lt: thirtyMinutesAgo} // dates *before* 30 minutes ago
+ removedAt: {$lt: yesterday} // dates *before* yesterday
}, function(error){
if (error){
console.error(JSON.stringify(error, null, 2));
diff --git a/app/imports/server/publications/library.js b/app/imports/server/publications/library.js
index 4f94b72b..41e2b080 100644
--- a/app/imports/server/publications/library.js
+++ b/app/imports/server/publications/library.js
@@ -69,6 +69,28 @@ Meteor.publish('libraryNodes', function(libraryId){
});
});
+Meteor.publish('softRemovedLibraryNodes', function(libraryId){
+ if (!libraryId) return [];
+ libraryIdSchema.validate({libraryId});
+ this.autorun(function (){
+ let userId = this.userId;
+ let library = Libraries.findOne(libraryId);
+ try { assertViewPermission(library, userId) }
+ catch(e){
+ return this.error(e);
+ }
+ return [
+ LibraryNodes.find({
+ 'ancestors.0.id': libraryId,
+ removed: true,
+ removedWith: {$exists: false},
+ }, {
+ sort: {order: 1},
+ }),
+ ];
+ });
+});
+
Meteor.publish('descendantLibraryNodes', function(nodeId){
let node = LibraryNodes.findOne(nodeId);
let libraryId = node?.ancestors[0]?.id;
diff --git a/app/imports/ui/library/LibraryEditDialog.vue b/app/imports/ui/library/LibraryEditDialog.vue
index 1c65dba3..145799da 100644
--- a/app/imports/ui/library/LibraryEditDialog.vue
+++ b/app/imports/ui/library/LibraryEditDialog.vue
@@ -27,6 +27,35 @@
@change="updateName"
/>
+
+ Recently Deleted Properties
+
+
+
+
+
+
+
+
+
+ Restore
+
+
+
+
+
+
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
import Libraries, { updateLibraryName, removeLibrary } from '/imports/api/library/Libraries.js';
+import LibraryNodes, { restoreLibraryNode } from '/imports/api/library/LibraryNodes.js';
+import TreeNodeView from '/imports/ui/properties/treeNodeViews/TreeNodeView.vue';
export default {
components: {
DialogBase,
+ TreeNodeView,
},
props: {
_id: String,
@@ -90,11 +122,28 @@ export default {
},
});
},
+ restore(_id){
+ restoreLibraryNode.call({_id});
+ },
},
meteor: {
+ '$subscribe':{
+ softRemovedLibraryNodes(){
+ return [this._id];
+ },
+ },
model(){
return Libraries.findOne(this._id);
},
+ removedDocs(){
+ return LibraryNodes.find({
+ 'ancestors.0.id': this._id,
+ removed: true,
+ removedWith: {$exists: false},
+ }, {
+ sort: {order: 1},
+ });
+ }
}
}
diff --git a/app/imports/ui/library/LibraryNodeDialog.vue b/app/imports/ui/library/LibraryNodeDialog.vue
index d446ef4d..99006a69 100644
--- a/app/imports/ui/library/LibraryNodeDialog.vue
+++ b/app/imports/ui/library/LibraryNodeDialog.vue
@@ -75,6 +75,7 @@
pushToLibraryNode,
pullFromLibraryNode,
softRemoveLibraryNode,
+ restoreLibraryNode,
} from '/imports/api/library/LibraryNodes.js';
import duplicateLibraryNode from '/imports/api/library/methods/duplicateLibraryNode.js';
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
@@ -86,6 +87,8 @@
import { get } from 'lodash';
import { assertDocEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { organizeDoc } from '/imports/api/parenting/organizeMethods.js';
+ import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
+ import getPropertyTitle from '/imports/ui/properties/shared/getPropertyTitle.js';
let formIndex = {};
for (let key in propertyFormIndex){
@@ -212,12 +215,20 @@
});
},
remove(){
- softRemoveLibraryNode.call({_id: this.currentId});
+ let _id = this.currentId;
+ softRemoveLibraryNode.call({_id});
if (this.embedded){
this.$emit('removed');
} else {
this.$store.dispatch('popDialogStack');
}
+ snackbar({
+ text: `Deleted ${getPropertyTitle(this.model)}`,
+ callbackName: 'undo',
+ callback(){
+ restoreLibraryNode.call({_id});
+ },
+ });
},
}
};