Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ jobs:
- name: Checkout
uses: actions/checkout@master
- name: Setup xmllint
run: sudo apt-get install --no-install-recommends -y libxml2-utils
run: |
sudo apt update
sudo apt install --no-install-recommends -y libxml2-utils
- name: Setup xmllint problem matcher
uses: korelstar/xmllint-problem-matcher@master
- name: lint XML
Expand Down
5 changes: 5 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@
'url' => '/notes',
'verb' => 'POST',
],
[
'name' => 'notes#undo',
'url' => '/notes/undo',
'verb' => 'POST',
],
[
'name' => 'notes#update',
'url' => '/notes/{id}',
Expand Down
28 changes: 28 additions & 0 deletions lib/Controller/NotesController.php
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,34 @@ public function create($content = '', $category = null) {
}


/**
* @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
*
Expand Down
74 changes: 65 additions & 9 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
:category="filter.category"
:search="filter.search"
@category-selected="onSelectCategory"
@note-deleted="routeFirst"
@note-deleted="onNoteDeleted"
/>

<AppSettings v-if="!loading.notes && error !== true" @reload="reloadNotes" />
Expand All @@ -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'
Expand Down Expand Up @@ -70,6 +71,9 @@ export default {
create: false,
},
error: false,
undoNotification: null,
undoTimer: null,
deletedNotes: [],
}
},

Expand All @@ -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 + '/')) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 = '<button class="undo">' + this.t('notes', 'Undo Delete') + '</button>'
this.undoNotification = showSuccess(
'<span class="deletedLabel">' + label + '</span> ' + 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()
Expand Down
34 changes: 24 additions & 10 deletions src/NotesService.js
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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 => {
Expand All @@ -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
})
Expand Down Expand Up @@ -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 => {
Expand All @@ -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) => {
Expand Down
4 changes: 2 additions & 2 deletions src/components/NavigationCategoriesItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
icon="icon-recent"
@click.prevent.stop="onSelectCategory(null)"
>
<AppNavigationCounter slot="counter">
<AppNavigationCounter #counter>
{{ numNotes }}
</AppNavigationCounter>
</AppNavigationItem>
Expand All @@ -25,7 +25,7 @@
:icon="category.name === '' ? 'icon-emptyfolder' : 'icon-files'"
@click.prevent.stop="onSelectCategory(category.name)"
>
<AppNavigationCounter slot="counter">
<AppNavigationCounter #counter>
{{ category.count }}
</AppNavigationCounter>
</AppNavigationItem>
Expand Down
2 changes: 1 addition & 1 deletion src/components/NavigationList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
/>
</template>
<AppNavigationItem
Expand Down
54 changes: 24 additions & 30 deletions src/components/NavigationNoteItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,8 @@
:menu-open.sync="actionsOpen"
:to="{ name: 'note', params: { noteId: note.id.toString() } }"
:class="{ actionsOpen }"
:undo="isPrepareDeleting"
@undo="onUndoDeleteNote"
>
<template v-if="!note.deleting" slot="actions">
<template #actions>
<ActionButton :icon="actionFavoriteIcon" @click="onToggleFavorite">
{{ actionFavoriteText }}
</ActionButton>
Expand All @@ -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',
Expand All @@ -52,7 +51,6 @@ export default {
delete: false,
},
actionsOpen: false,
undoTimer: null,
}
},

Expand All @@ -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() {
Expand Down Expand Up @@ -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
})
},
},
Expand Down
3 changes: 2 additions & 1 deletion src/components/Note.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -141,7 +142,7 @@ export default {
fetchNote(this.noteId)
.then((note) => {
if (note.errorMessage) {
OC.Notification.showTemporary(note.errorMessage)
showError(note.errorMessage)
}
})
.catch(() => {
Expand Down