diff --git a/app/assets/javascripts/app/controllers/editor.js b/app/assets/javascripts/app/controllers/editor.js index a4d35eeab..ed7975894 100644 --- a/app/assets/javascripts/app/controllers/editor.js +++ b/app/assets/javascripts/app/controllers/editor.js @@ -56,7 +56,7 @@ angular.module('app') // Before checking if isMappingSourceRetrieved, we check if this item was deleted via a local source, // such as alternating uuids during sign in. Otherwise, we only want to make interface updates if it's a // remote retrieved source. - if(this.note.deleted) { + if(this.note.deleted || this.note.content.trashed) { $rootScope.notifyDelete(); return; } @@ -381,7 +381,7 @@ angular.module('app') } } - this.deleteNote = async function() { + this.deleteNote = async function(permanently) { let run = () => { $timeout(() => { if(this.note.locked) { @@ -390,8 +390,15 @@ angular.module('app') } let title = this.note.safeTitle().length ? `'${this.note.title}'` : "this note"; - if(confirm(`Are you sure you want to delete ${title}?`)) { - this.remove()(this.note); + let message = permanently ? `Are you sure you want to permanently delete ${title}?` + : `Are you sure you want to move ${title} to the trash?` + if(confirm(message)) { + if(permanently) { + this.remove()(this.note); + } else { + this.note.content.trashed = true; + this.changesMade({dontUpdateClientModified: true, dontUpdatePreviews: true}); + } this.showMenu = false; } }); @@ -406,6 +413,27 @@ angular.module('app') } } + this.restoreTrashedNote = function() { + this.note.content.trashed = false; + this.changesMade({dontUpdateClientModified: true, dontUpdatePreviews: true}); + } + + this.deleteNotePermanantely = function() { + this.deleteNote(true); + } + + this.getTrashCount = function() { + return modelManager.trashedItems().length; + } + + this.emptyTrash = function() { + let count = this.getTrashCount(); + if(confirm(`Are you sure you want to permanently delete ${count} note(s)?`)) { + modelManager.emptyTrash(); + syncManager.sync(); + } + } + this.togglePin = function() { this.note.setAppDataItem("pinned", !this.note.pinned); this.changesMade({dontUpdatePreviews: true}); diff --git a/app/assets/javascripts/app/controllers/notes.js b/app/assets/javascripts/app/controllers/notes.js index e49ab5eed..b17ea8a6e 100644 --- a/app/assets/javascripts/app/controllers/notes.js +++ b/app/assets/javascripts/app/controllers/notes.js @@ -198,6 +198,13 @@ angular.module('app') }) } + if(note.content.trashed) { + flags.push({ + text: "Deleted", + class: "danger" + }) + } + note.flags = flags; return flags; @@ -301,10 +308,17 @@ angular.module('app') this.noteFilter = {text : ''}; this.filterNotes = function(note) { - var canShowArchived = false, canShowPinned = true; + let canShowArchived = false, canShowPinned = true; + let isTrash = this.tag.content.isTrashTag; + + if(!isTrash && note.content.trashed) { + note.visible = false; + return note.visible; + } + var isSmartTag = this.tag.isSmartTag(); if(isSmartTag) { - canShowArchived = this.tag.isReferencingArchivedNotes(); + canShowArchived = this.tag.content.isArchiveTag || isTrash; } else { canShowArchived = this.showArchived; canShowPinned = !this.hidePinned; diff --git a/app/assets/javascripts/app/controllers/tags.js b/app/assets/javascripts/app/controllers/tags.js index c1c4ed66e..1a8577857 100644 --- a/app/assets/javascripts/app/controllers/tags.js +++ b/app/assets/javascripts/app/controllers/tags.js @@ -81,7 +81,7 @@ angular.module('app') if(tag.isSmartTag()) { Object.defineProperty(tag, "notes", { get: () => { - return modelManager.notesMatchingPredicate(tag.content.predicate); + return modelManager.notesMatchingSmartTag(tag); } }); } diff --git a/app/assets/javascripts/app/services/modelManager.js b/app/assets/javascripts/app/services/modelManager.js index d2a65ff8b..1509dfb70 100644 --- a/app/assets/javascripts/app/services/modelManager.js +++ b/app/assets/javascripts/app/services/modelManager.js @@ -116,9 +116,29 @@ class ModelManager extends SFModelManager { } } - notesMatchingPredicate(predicate) { + notesMatchingSmartTag(tag) { let contentTypePredicate = new SFPredicate("content_type", "=", "Note"); - return this.itemsMatchingPredicates([contentTypePredicate, predicate]); + let predicates = [contentTypePredicate, tag.content.predicate]; + if(!tag.content.isTrashTag) { + let notTrashedPredicate = new SFPredicate("content.trashed", "=", false); + predicates.push(notTrashedPredicate); + } + return this.itemsMatchingPredicates(predicates); + } + + trashSmartTag() { + return this.systemSmartTags.find((tag) => tag.content.isTrashTag); + } + + trashedItems() { + return this.notesMatchingSmartTag(this.trashSmartTag()); + } + + emptyTrash() { + let notes = this.trashedItems(); + for(let note of notes) { + this.setItemToBeDeleted(note); + } } buildSystemSmartTags() { diff --git a/app/assets/templates/editor.html.haml b/app/assets/templates/editor.html.haml index 25386c77a..f0e6cd73c 100644 --- a/app/assets/templates/editor.html.haml +++ b/app/assets/templates/editor.html.haml @@ -38,7 +38,14 @@ %menu-row{"label" => "ctrl.note.locked ? 'Unlock' : 'Lock'", "action" => "ctrl.selectedMenuItem(true); ctrl.toggleLockNote()", "desc" => "'Locking notes prevents unintentional editing'"} %menu-row{"label" => "ctrl.note.content.protected ? 'Unprotect' : 'Protect'", "action" => "ctrl.selectedMenuItem(true); ctrl.toggleProtectNote()", "desc" => "'Protecting a note will require credentials to view it (Manage Privileges via Account menu)'"} %menu-row{"label" => "'Preview'", "circle" => "ctrl.note.content.hidePreview ? 'danger' : 'success'", "circle-align" => "'right'", "action" => "ctrl.selectedMenuItem(true); ctrl.toggleNotePreview()", "desc" => "'Hide or unhide the note preview from the list of notes'"} - %menu-row{"label" => "'Delete'", "action" => "ctrl.selectedMenuItem(); ctrl.deleteNote()", "desc" => "'Delete this note permanently from all your devices'"} + %menu-row{"ng-show" => "!ctrl.note.content.trashed", "label" => "'Move to Trash'", "action" => "ctrl.selectedMenuItem(); ctrl.deleteNote()", "desc" => "'Send this note to the trash'"} + + .sk-menu-panel-section{"ng-if" => "ctrl.note.content.trashed"} + .sk-menu-panel-header + .sk-menu-panel-header-title Trash + %menu-row{"label" => "'Restore Note'", "action" => "ctrl.selectedMenuItem(true); ctrl.restoreTrashedNote()", "desc" => "'Undelete this note and restore it back into your notes'"} + %menu-row{"label" => "'Delete Forever'", "action" => "ctrl.selectedMenuItem(true); ctrl.deleteNotePermanantely()", "desc" => "'Delete this note permanently from all your devices'"} + %menu-row{"label" => "'Empty Trash'", "subtitle" => "ctrl.getTrashCount() + ' notes in trash'", "action" => "ctrl.selectedMenuItem(true); ctrl.emptyTrash()", "desc" => "'Permanently delete all notes in the trash'"} .sk-menu-panel-section .sk-menu-panel-header diff --git a/app/assets/templates/tags.html.haml b/app/assets/templates/tags.html.haml index 6674a13fe..666a65baa 100644 --- a/app/assets/templates/tags.html.haml +++ b/app/assets/templates/tags.html.haml @@ -13,7 +13,8 @@ .scrollable .infinite-scroll - .tag{"ng-repeat" => "tag in ctrl.smartTags", "ng-click" => "ctrl.selectTag(tag)", "ng-class" => "{'selected' : ctrl.selectedTag == tag}"} + .tag{"ng-repeat" => "tag in ctrl.smartTags", "ng-click" => "ctrl.selectTag(tag)", + "ng-class" => "{'selected' : ctrl.selectedTag == tag, 'faded' : !tag.content.isAllTag}"} .info %input.title{"ng-disabled" => "true", "ng-model" => "tag.title"}