diff --git a/appinfo/routes.php b/appinfo/routes.php
index 69ab49a8..e38f5994 100644
--- a/appinfo/routes.php
+++ b/appinfo/routes.php
@@ -47,6 +47,11 @@ return ['routes' => [
'url' => '/notes',
'verb' => 'POST',
],
+ [
+ 'name' => 'notes#undo',
+ 'url' => '/notes/undo',
+ 'verb' => 'POST',
+ ],
[
'name' => 'notes#update',
'url' => '/notes/{id}',
diff --git a/lib/Controller/NotesController.php b/lib/Controller/NotesController.php
index a58b2f55..e7d9780f 100644
--- a/lib/Controller/NotesController.php
+++ b/lib/Controller/NotesController.php
@@ -140,6 +140,34 @@ class NotesController extends Controller {
}
+ /**
+ * @NoAdminRequired
+ *
+ * @param string $content
+ */
+ public function undo($id, $content, $category, $modified, $favorite) {
+ try {
+ // check if note still exists
+ $note = $this->notesService->get($id, $this->userId);
+ if ($note->getError()) {
+ throw new \Exception();
+ }
+ } catch (\Throwable $e) {
+ // re-create if note doesn't exit anymore
+ $note = $this->notesService->create($this->userId);
+ $note = $this->notesService->update(
+ $note->getId(),
+ $content,
+ $this->userId,
+ $category,
+ $modified
+ );
+ $note->favorite = $this->notesService->favorite($note->getId(), $favorite, $this->userId);
+ }
+ return new DataResponse($note);
+ }
+
+
/**
* @NoAdminRequired
*
diff --git a/src/App.vue b/src/App.vue
index b3a7f569..162600dd 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -14,7 +14,7 @@
:category="filter.category"
:search="filter.search"
@category-selected="onSelectCategory"
- @note-deleted="routeFirst"
+ @note-deleted="onNoteDeleted"
/>
@@ -40,8 +40,9 @@ import {
AppNavigationNew,
Content,
} from '@nextcloud/vue'
+import { showSuccess } from '@nextcloud/dialogs'
-import { fetchNotes, noteExists, createNote } from './NotesService'
+import { fetchNotes, noteExists, createNote, undoDeleteNote } from './NotesService'
import { openNavbar } from './nextcloud'
import AppSettings from './components/AppSettings'
import NavigationList from './components/NavigationList'
@@ -70,6 +71,9 @@ export default {
create: false,
},
error: false,
+ undoNotification: null,
+ undoTimer: null,
+ deletedNotes: [],
}
},
@@ -82,9 +86,6 @@ export default {
const search = this.filter.search.toLowerCase()
const notes = this.notes.filter(note => {
- if (note.deleting === 'deleting') {
- return false
- }
if (this.filter.category !== null
&& this.filter.category !== note.category
&& !note.category.startsWith(this.filter.category + '/')) {
@@ -174,10 +175,12 @@ export default {
},
routeToNote(noteId) {
- this.$router.push({
- name: 'note',
- params: { noteId: noteId.toString() },
- })
+ if (this.$route.name !== 'note' || this.$route.params.noteId !== noteId.toString()) {
+ this.$router.push({
+ name: 'note',
+ params: { noteId: noteId.toString() },
+ })
+ }
},
onSearch(query) {
@@ -215,6 +218,59 @@ export default {
}
},
+ onNoteDeleted(note) {
+ this.deletedNotes.push(note)
+ this.clearUndoTimer()
+ let label
+ if (this.deletedNotes.length === 1) {
+ label = this.t('notes', 'Deleted {title}', { title: note.title })
+ } else {
+ label = this.t('notes', 'Deleted {number} notes', { number: this.deletedNotes.length })
+ }
+ if (this.undoNotification === null) {
+ const action = ''
+ this.undoNotification = showSuccess(
+ '' + label + ' ' + action,
+ { isHTML: true, timeout: -1, onShow: this.onUndoNotificationClosed }
+ )
+ this.undoNotification.toastElement.getElementsByClassName('undo')
+ .forEach(element => { element.onclick = this.onUndoDelete })
+ } else {
+ this.undoNotification.toastElement.getElementsByClassName('deletedLabel')
+ .forEach(element => { element.textContent = label })
+ }
+ this.undoTimer = setTimeout(this.onRemoveUndoNotification, 12000)
+ this.routeFirst()
+ },
+
+ clearUndoTimer() {
+ if (this.undoTimer) {
+ clearTimeout(this.undoTimer)
+ this.undoTimer = null
+ }
+ },
+
+ onUndoDelete() {
+ this.deletedNotes.forEach(note => undoDeleteNote(note))
+ this.onRemoveUndoNotification()
+ },
+
+ onUndoNotificationClosed() {
+ if (this.undoNotification) {
+ this.undoNotification = null
+ this.onRemoveUndoNotification()
+ }
+ },
+
+ onRemoveUndoNotification() {
+ this.deletedNotes = []
+ if (this.undoNotification) {
+ this.undoNotification.hideToast()
+ this.undoNotification = null
+ }
+ this.clearUndoTimer()
+ },
+
onClose(event) {
if (!this.notes.every(note => !note.unsaved)) {
event.preventDefault()
diff --git a/src/NotesService.js b/src/NotesService.js
index 7c7786ae..a56f6989 100644
--- a/src/NotesService.js
+++ b/src/NotesService.js
@@ -1,6 +1,7 @@
import AppGlobal from './mixins/AppGlobal'
import store from './store'
import axios from '@nextcloud/axios'
+import { showError } from '@nextcloud/dialogs'
const t = AppGlobal.methods.t
@@ -10,11 +11,11 @@ function url(url) {
}
function handleSyncError(message) {
- OC.Notification.showTemporary(message + ' ' + t('notes', 'See JavaScript console for details.'))
+ showError(message + ' ' + t('notes', 'See JavaScript console and server log for details.'))
}
function handleInsufficientStorage() {
- OC.Notification.showTemporary(t('notes', 'Saving the note has failed due to insufficient storage.'))
+ showError(t('notes', 'Saving the note has failed due to insufficient storage.'))
}
export const setSettings = settings => {
@@ -41,7 +42,7 @@ export const fetchNotes = () => {
store.dispatch('addAll', response.data.notes)
}
if (response.data.errorMessage) {
- OC.Notification.showTemporary(response.data.errorMessage)
+ showError(response.data.errorMessage)
}
return response.data
})
@@ -119,12 +120,22 @@ function _updateNote(note) {
})
}
-export const prepareDeleteNote = noteId => {
- store.commit('setNoteAttribute', { noteId: noteId, attribute: 'deleting', value: 'prepare' })
-}
-
-export const undoDeleteNote = noteId => {
- store.commit('setNoteAttribute', { noteId: noteId, attribute: 'deleting', value: null })
+export const undoDeleteNote = (note) => {
+ return axios
+ .post(url('/notes/undo'), note)
+ .then(response => {
+ store.commit('add', response.data)
+ return response.data
+ })
+ .catch(err => {
+ console.error(err)
+ if (err.response.status === 507) {
+ handleInsufficientStorage()
+ } else {
+ handleSyncError(t('notes', 'Undo delete has failed for note {title}.', { title: note.title }))
+ }
+ throw err
+ })
}
export const deleteNote = noteId => {
@@ -137,9 +148,12 @@ export const deleteNote = noteId => {
.catch(err => {
console.error(err)
handleSyncError(t('notes', 'Deleting note {id} has failed.', { id: noteId }))
- undoDeleteNote(noteId)
+ // remove note always since we don't know when the error happened
+ store.commit('remove', noteId)
throw err
})
+ .then(() => {
+ })
}
export const setFavorite = (noteId, favorite) => {
diff --git a/src/components/NavigationCategoriesItem.vue b/src/components/NavigationCategoriesItem.vue
index 013a3aa2..6b7857f6 100644
--- a/src/components/NavigationCategoriesItem.vue
+++ b/src/components/NavigationCategoriesItem.vue
@@ -14,7 +14,7 @@
icon="icon-recent"
@click.prevent.stop="onSelectCategory(null)"
>
-
+
{{ numNotes }}
@@ -25,7 +25,7 @@
:icon="category.name === '' ? 'icon-emptyfolder' : 'icon-files'"
@click.prevent.stop="onSelectCategory(category.name)"
>
-
+
{{ category.count }}
diff --git a/src/components/NavigationList.vue b/src/components/NavigationList.vue
index 8ca9ff06..fcd96fe7 100644
--- a/src/components/NavigationList.vue
+++ b/src/components/NavigationList.vue
@@ -42,7 +42,7 @@
:key="note.id"
:note="note"
@category-selected="$emit('category-selected', $event)"
- @note-deleted="$emit('note-deleted')"
+ @note-deleted="$emit('note-deleted', $event)"
/>
-
+
{{ actionFavoriteText }}
@@ -27,8 +25,9 @@ import {
ActionButton,
AppNavigationItem,
} from '@nextcloud/vue'
+import { showError } from '@nextcloud/dialogs'
-import { categoryLabel, setFavorite, prepareDeleteNote, undoDeleteNote, deleteNote } from '../NotesService'
+import { categoryLabel, setFavorite, fetchNote, deleteNote } from '../NotesService'
export default {
name: 'NavigationNoteItem',
@@ -52,7 +51,6 @@ export default {
delete: false,
},
actionsOpen: false,
- undoTimer: null,
}
},
@@ -67,16 +65,8 @@ export default {
return icon
},
- isPrepareDeleting() {
- return this.note.deleting === 'prepare'
- },
-
title() {
- if (this.isPrepareDeleting) {
- return this.t('notes', 'Deleted {title}', { title: this.note.title })
- } else {
- return this.note.title + (this.note.unsaved ? ' *' : '')
- }
+ return this.note.title + (this.note.unsaved ? ' *' : '')
},
actionFavoriteText() {
@@ -118,26 +108,30 @@ export default {
},
onDeleteNote() {
- this.actionsOpen = false
- prepareDeleteNote(this.note.id)
- this.undoTimer = setTimeout(this.onDeleteNoteFinally, 7000)
- this.$emit('note-deleted')
- },
-
- onUndoDeleteNote() {
- clearTimeout(this.undoTimer)
- undoDeleteNote(this.note.id)
- },
-
- onDeleteNoteFinally() {
this.loading.delete = true
- deleteNote(this.note.id)
- .then(() => {
+ fetchNote(this.note.id)
+ .then((note) => {
+ if (note.errorMessage) {
+ throw new Error('Note has errors')
+ }
+ deleteNote(this.note.id)
+ .then(() => {
+ // nothing to do, confirmation is done after event
+ })
+ .catch(() => {
+ // nothing to do, error is already shown by NotesService
+ })
+ .then(() => {
+ // always show undo, since error can relate to response only
+ this.$emit('note-deleted', note)
+ this.loading.delete = false
+ this.actionsOpen = false
+ })
})
.catch(() => {
- })
- .then(() => {
+ showError(this.t('notes', 'Error during preparing note for deletion.'))
this.loading.delete = false
+ this.actionsOpen = false
})
},
},
diff --git a/src/components/Note.vue b/src/components/Note.vue
index cab58fee..b20f4a7e 100644
--- a/src/components/Note.vue
+++ b/src/components/Note.vue
@@ -57,6 +57,7 @@ import {
AppContent,
Tooltip,
} from '@nextcloud/vue'
+import { showError } from '@nextcloud/dialogs'
import { fetchNote, saveNote, saveNoteManually } from '../NotesService'
import { closeNavbar } from '../nextcloud'
@@ -141,7 +142,7 @@ export default {
fetchNote(this.noteId)
.then((note) => {
if (note.errorMessage) {
- OC.Notification.showTemporary(note.errorMessage)
+ showError(note.errorMessage)
}
})
.catch(() => {