+
+
+
+
+
diff --git a/src/components/Markdown.vue b/src/components/Markdown.vue
deleted file mode 100644
index 7fdacdcf4..000000000
--- a/src/components/Markdown.vue
+++ /dev/null
@@ -1,72 +0,0 @@
-
-
-
-
-
-
-
diff --git a/src/main.js b/src/main.js
index 9388c59a2..a05871192 100644
--- a/src/main.js
+++ b/src/main.js
@@ -27,6 +27,17 @@ import store from './store/store.js'
import { subscribe, unsubscribe } from '@nextcloud/event-bus'
import { linkTo } from '@nextcloud/router'
+import Eye from 'vue-material-design-icons/Eye.vue'
+import EyeOff from 'vue-material-design-icons/EyeOff.vue'
+import CalendarRemove from 'vue-material-design-icons/CalendarRemove.vue'
+import Cancel from 'vue-material-design-icons/Cancel.vue'
+import Check from 'vue-material-design-icons/Check.vue'
+import TrendingUp from 'vue-material-design-icons/TrendingUp.vue'
+import AlertBoxOutline from 'vue-material-design-icons/AlertBoxOutline.vue'
+import Pulse from 'vue-material-design-icons/Pulse.vue'
+import Tag from 'vue-material-design-icons/Tag.vue'
+import Delete from 'vue-material-design-icons/Delete.vue'
+
import Vue from 'vue'
import { sync } from 'vuex-router-sync'
import VTooltip from 'v-tooltip'
@@ -52,6 +63,32 @@ sync(store, router)
Vue.use(VTooltip)
Vue.use(VueClipboard)
+/**
+ * We have to globally register these material design icons
+ * so we can use them dynamically via ``
+ * in the MultiselectOption component.
+ */
+// eslint-disable-next-line vue/match-component-file-name
+Vue.component('IconEye', Eye)
+// eslint-disable-next-line vue/match-component-file-name
+Vue.component('IconEyeOff', EyeOff)
+// eslint-disable-next-line vue/match-component-file-name
+Vue.component('IconCalendarRemove', CalendarRemove)
+// eslint-disable-next-line vue/match-component-file-name
+Vue.component('IconCancel', Cancel)
+// eslint-disable-next-line vue/match-component-file-name
+Vue.component('IconCheck', Check)
+// eslint-disable-next-line vue/match-component-file-name
+Vue.component('IconTrendingUp', TrendingUp)
+// eslint-disable-next-line vue/match-component-file-name
+Vue.component('IconAlertBoxOutline', AlertBoxOutline)
+// eslint-disable-next-line vue/match-component-file-name
+Vue.component('IconPulse', Pulse)
+// eslint-disable-next-line vue/match-component-file-name
+Vue.component('IconTag', Tag)
+// eslint-disable-next-line vue/match-component-file-name
+Vue.component('IconDelete', Delete)
+
if (!OCA.Tasks) {
/**
* @namespace OCA.Tasks
diff --git a/src/mixins/editableItem.js b/src/mixins/editableItem.js
new file mode 100644
index 000000000..b47199be8
--- /dev/null
+++ b/src/mixins/editableItem.js
@@ -0,0 +1,141 @@
+/**
+ * Nextcloud - Tasks
+ *
+ * @author Raimund Schlüßler
+ * @copyright 2021 Raimund Schlüßler
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this library. If not, see .
+ *
+ */
+import Actions from '@nextcloud/vue/dist/Components/Actions'
+import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
+
+import Check from 'vue-material-design-icons/Check.vue'
+import Delete from 'vue-material-design-icons/Delete.vue'
+
+import ClickOutside from 'v-click-outside'
+
+export default {
+ components: {
+ Actions,
+ ActionButton,
+ Check,
+ Delete,
+ },
+ directives: {
+ clickOutside: ClickOutside.directive,
+ },
+ props: {
+ /**
+ * Is the value read only?
+ */
+ readOnly: {
+ type: Boolean,
+ default: false,
+ },
+ /**
+ * The string to show when
+ * editing is not active
+ */
+ propertyString: {
+ type: String,
+ default: '',
+ },
+ /**
+ * The task to edit
+ */
+ task: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ newValue: this.value,
+ editing: false,
+ }
+ },
+ /**
+ * Save possible edits before destroying the component
+ * (e.g. when the sidebar is hidden)
+ */
+ beforeDestroy() {
+ this.setValue()
+ },
+ watch: {
+ /**
+ * We have to watch the task for changes,
+ * so we can save possible edits before
+ * navigating away.
+ *
+ * @param {Task} newTask The task to navigate to
+ * @param {Task} oldTask The task to navigate from
+ */
+ task(newTask, oldTask) {
+ this.setValue(oldTask)
+ },
+ },
+ methods: {
+ /**
+ * Emits the current value to the parent component
+ * when editing ends
+ *
+ * @param {Task} task The task for which to set the value
+ */
+ setValue(task = this.task) {
+ // Set the property if editing is active.
+ if (this.editing) {
+ this.$emit('setValue', { task, value: this.newValue })
+ }
+ this.setEditing(false)
+ },
+ /**
+ * Removes the value
+ */
+ clearValue() {
+ this.$emit('setValue', { task: this.task, value: null })
+ this.setEditing(false)
+ },
+ /**
+ * Sets the editing mode if allowed.
+ *
+ * @param {Boolean} editing If editing is enabled
+ * @param {Object} $event The event which triggered the function
+ */
+ setEditing(editing, $event) {
+ if (this.readOnly) { return }
+
+ if ($event?.target.tagName === 'A') {
+ return
+ }
+
+ // If we just start editing, we sync value
+ // and new value
+ if (!this.editing && editing) {
+ this.newValue = this.value
+ }
+
+ this.editing = editing
+ this.$emit('editing', this.editing)
+
+ this.editingEnabled()
+ },
+
+ /**
+ * Function to call after editing is enabled
+ * (important for notes item)
+ */
+ editingEnabled() {},
+ },
+}
diff --git a/src/store/tasks.js b/src/store/tasks.js
index f82bba03d..be9dc1aa7 100644
--- a/src/store/tasks.js
+++ b/src/store/tasks.js
@@ -1021,6 +1021,9 @@ const actions = {
* @param {Task} task The task to update
*/
async setNote(context, { task, note }) {
+ if (note === task.note) {
+ return
+ }
context.commit('setNote', { task, note })
context.dispatch('scheduleTaskUpdate', task)
},
diff --git a/src/views/AppSidebar.vue b/src/views/AppSidebar.vue
index 3b093f0c5..e3d15e5db 100644
--- a/src/views/AppSidebar.vue
+++ b/src/views/AppSidebar.vue
@@ -20,457 +20,422 @@ License along with this library. If not, see .
-->
-
+
+
+ setPriority({ task, priority: value })">
+
+
+ setPercentComplete({ task, complete: value })">
+
+
+
-
-
-
-
-
-
-
-
- {{ startDateString }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ dueDateString }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ priorityString }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ completeString }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
{{ tmpTask.note }}
-
-
-
-
-
-
-
-
-
-
-
-
- {{ $t('tasks', 'Loading task from server.') }}
- {{ $t('tasks', 'Task not found!') }}
-
-
+
+
+ {{ taskStatusLabel }}
+
+
+ setNote({ task, note: value })" />
+
+
+
+
+
diff --git a/tests/javascript/unit/setupStore.js b/tests/javascript/unit/setupStore.js
index de6d95293..f214f9b90 100644
--- a/tests/javascript/unit/setupStore.js
+++ b/tests/javascript/unit/setupStore.js
@@ -3,11 +3,13 @@ import collections from 'Store/collections.js'
import tasks from 'Store/tasks.js'
import settings from 'Store/settings.js'
import Task from 'Models/task.js'
+import router from '@/router.js'
import { loadICS } from '../../assets/loadAsset.js'
import { createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex'
+import { sync } from 'vuex-router-sync'
const localVue = createLocalVue()
localVue.use(Vuex)
@@ -21,6 +23,10 @@ const store = new Vuex.Store({
},
})
+// Sync router and store so that we can access
+// the router from within the store
+sync(store, router)
+
const calendarsData = [
{
url: 'calendar-1/tmp',
@@ -71,6 +77,7 @@ calendarsData.forEach(calendarData => {
const task = new Task(taskData, calendar)
const response = {
url: `${calendar.id}/${task.uid}.ics`,
+ update: () => { return Promise.resolve(response) },
}
localVue.set(task, 'dav', response)
return task
diff --git a/tests/javascript/unit/views/AppSidebar.spec.js b/tests/javascript/unit/views/AppSidebar.spec.js
new file mode 100644
index 000000000..3a121cce4
--- /dev/null
+++ b/tests/javascript/unit/views/AppSidebar.spec.js
@@ -0,0 +1,51 @@
+/**
+ * Nextcloud - Tasks
+ *
+ * @author Raimund Schlüßler
+ * @copyright 2021 Raimund Schlüßler
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this library. If not, see .
+ *
+ */
+import AppSidebar from 'Views/AppSidebar.vue'
+import router from '@/router.js'
+
+import { store, localVue } from '../setupStore.js'
+
+import { shallowMount } from '@vue/test-utils'
+
+describe('AppSidebar.vue', () => {
+ 'use strict'
+
+ // We set the route before mounting AppSidebar to prevent messages that the task was not found
+ // Could be adjusted with future tests
+ router.push({ name: 'calendarsTask', params: { calendarId: 'calendar-1', taskId: 'pwen4kz18g.ics' } })
+
+ it('Returns the correct value for the new dates', () => {
+ const wrapper = shallowMount(AppSidebar, { localVue, store, router })
+
+ let actual = wrapper.vm.newStartDate
+ let expected = new Date('2019-01-01T12:00:00')
+ expect(actual.getTime()).toBe(expected.getTime())
+
+ actual = wrapper.vm.newDueDate
+ expected = new Date('2019-01-01T12:34:00')
+ expect(actual.getTime()).toBe(expected.getTime())
+
+ const newDueDate = new Date('2019-01-01T15:01:00')
+ wrapper.vm.setDueDate({ task: wrapper.vm.task, value: newDueDate })
+ actual = wrapper.vm.newDueDate
+ expect(actual.getTime()).toBe(newDueDate.getTime())
+ })
+})