diff options
author | Raimund Schlüßler <raimund.schluessler@mailbox.org> | 2019-01-27 09:24:23 +0300 |
---|---|---|
committer | Raimund Schlüßler <raimund.schluessler@mailbox.org> | 2020-05-09 21:37:15 +0300 |
commit | 64fc9b27af2f794ec89ca4d789b6d712447a1b81 (patch) | |
tree | 877ded2215a1814f154fb78f3a250728f0c51cc7 /src | |
parent | 0b75a29dcf83b56d48189d99e9aa189b1306e6d4 (diff) |
Implement manual sorting
Signed-off-by: Raimund Schlüßler <raimund.schluessler@mailbox.org>
Diffstat (limited to 'src')
-rw-r--r-- | src/components/SortorderDropdown.vue | 23 | ||||
-rw-r--r-- | src/components/TaskBody.vue | 17 | ||||
-rw-r--r-- | src/components/TaskDragContainer.vue | 130 | ||||
-rw-r--r-- | src/components/TheCollections/Calendar.vue | 24 | ||||
-rw-r--r-- | src/components/TheCollections/General.vue | 20 | ||||
-rw-r--r-- | src/components/TheCollections/Week.vue | 19 | ||||
-rw-r--r-- | src/models/task.js | 52 | ||||
-rw-r--r-- | src/store/settings.js | 5 | ||||
-rw-r--r-- | src/store/storeHelper.js | 15 | ||||
-rw-r--r-- | src/store/tasks.js | 26 |
10 files changed, 256 insertions, 75 deletions
diff --git a/src/components/SortorderDropdown.vue b/src/components/SortorderDropdown.vue index a86d6559..570b6f79 100644 --- a/src/components/SortorderDropdown.vue +++ b/src/components/SortorderDropdown.vue @@ -43,6 +43,7 @@ License along with this library. If not, see <http://www.gnu.org/licenses/>. <script> import Actions from '@nextcloud/vue/dist/Components/Actions' import ActionButton from '@nextcloud/vue/dist/Components/ActionButton' +import { mapGetters } from 'vuex' export default { name: 'SortorderDropdown', @@ -96,26 +97,30 @@ export default { text: this.$t('tasks', 'Priority'), hint: this.$t('tasks', 'Sort by priority and summary.'), }, - // Manual sorting is not yet implemented - // { - // id: 'manual', - // icon: 'icon-manual', - // text: this.$t('tasks', 'Manually'), - // hint: this.$t('tasks', 'Sort by manual order.') - // }, { id: 'alphabetically', icon: 'icon-alphabetically', text: this.$t('tasks', 'Alphabetically'), hint: this.$t('tasks', 'Sort by summary and priority.'), }, + { + id: 'manual', + icon: 'icon-manual', + text: this.$t('tasks', 'Manually'), + hint: this.$t('tasks', 'Sort by manual order.'), + }, ], } }, computed: { + ...mapGetters({ + sortOrderGetter: 'sortOrder', + sortDirectionGetter: 'sortDirection', + }), + sortOrder: { get() { - return this.$store.state.settings.settings.sortOrder + return this.sortOrderGetter }, set(order) { this.$store.dispatch('setSetting', { type: 'sortOrder', value: order }) @@ -123,7 +128,7 @@ export default { }, sortDirection: { get() { - return this.$store.state.settings.settings.sortDirection + return this.sortDirectionGetter }, set(direction) { this.$store.dispatch('setSetting', { type: 'sortDirection', value: +direction }) diff --git a/src/components/TaskBody.vue b/src/components/TaskBody.vue index bc0f2e4c..3cfe097f 100644 --- a/src/components/TaskBody.vue +++ b/src/components/TaskBody.vue @@ -138,15 +138,11 @@ License along with this library. If not, see <http://www.gnu.org/licenses/>. </form> </div> <TaskDragContainer v-if="showSubtasks" + :tasks="filteredSubtasks" + :disabled="task.calendar.readOnly" + :collection-string="collectionString" :task-id="task.uri" - :calendar-id="task.calendar.uri" - :disabled="task.calendar.readOnly"> - <TaskBody v-for="subtask in filteredSubtasks" - :key="subtask.uid" - :task="subtask" - :collection-string="collectionString" - class="subtask" /> - </TaskDragContainer> + :calendar-id="task.calendar.uri" /> </div> </li> </template> @@ -186,7 +182,6 @@ export default { collectionString: { type: String, default: null, - required: false, }, }, data() { @@ -203,8 +198,6 @@ export default { }, computed: { ...mapGetters({ - sortOrder: 'sortOrder', - sortDirection: 'sortDirection', searchQuery: 'searchQuery', }), @@ -305,7 +298,7 @@ export default { return isTaskInList(task, this.collectionString) || this.isTaskOpen(task) || this.isDescendantOpen(task) }) } - return sort([...subTasks], this.sortOrder, this.sortDirection) + return subTasks }, /** diff --git a/src/components/TaskDragContainer.vue b/src/components/TaskDragContainer.vue index 2d14bd64..d51e48e4 100644 --- a/src/components/TaskDragContainer.vue +++ b/src/components/TaskDragContainer.vue @@ -25,30 +25,61 @@ License along with this library. If not, see <http://www.gnu.org/licenses/>. :set-data="setDragData" v-bind="{group: 'tasks', swapThreshold: 0.30, delay: 500, delayOnTouchOnly: true, touchStartThreshold: 3, disabled: disabled, filter: '.readOnly'}" :move="onMove" - @add="onAdd"> - <slot /> + @add="onAdd" + @end="onEnd"> + <TaskBody v-for="task in sortedTasks" + :key="task.key" + :task="task" + :collection-string="collectionString" /> </draggable> </template> <script> +import { sort } from '../store/storeHelper' + import draggable from 'vuedraggable' -import { mapGetters, mapActions } from 'vuex' +import { mapGetters, mapActions, mapMutations } from 'vuex' export default { + name: 'TaskDragContainer', components: { + /** + * We asynchronously import here, because we have a circular dependency + * between TaskDragContainer and TaskBody which otherwise cannot be resolved. + * See https://vuejs.org/v2/guide/components-edge-cases.html#Circular-References-Between-Components + * + * We load it "eager", because the TaskBody will always be required. + * + * @returns {Object} The TaskBody component + */ + TaskBody: () => import(/* webpackMode: "eager" */ './TaskBody'), draggable, }, props: { + tasks: { + type: Array, + default: () => [], + }, disabled: { type: Boolean, default: false, }, + collectionString: { + type: String, + default: null, + }, }, computed: { ...mapGetters({ getCalendar: 'getCalendarById', getTask: 'getTaskByUri', + sortOrder: 'sortOrder', + sortDirection: 'sortDirection', }), + + sortedTasks() { + return sort([...this.tasks], this.sortOrder, this.sortDirection) + }, }, methods: { ...mapActions([ @@ -56,16 +87,104 @@ export default { 'setPriority', 'setPercentComplete', 'setDate', + 'setSortOrder', ]), + ...mapMutations({ + commitSortOrder: 'setSortOrder', + }), + setDragData: (dataTransfer) => { // We do nothing here, this just prevents // vue.draggable from setting data on the // dataTransfer object. }, + adjustSortOrder(task, newIndex, oldIndex = -1) { + // Only change the sort order if we sort manually + if (this.sortOrder !== 'manual') { + return + } + // If the tasks array has no entry, we don't need to sort. + if (this.sortedTasks.length === 0) { + return + } + // If the task is inserted at its current position, don't sort. + if (newIndex === oldIndex) { + return + } + + // Get a copy of the sorted tasks array + const sortedTasks = [...this.sortedTasks] + + // In case the task to move is already in the array, move it to the new position + if (oldIndex > -1) { + sortedTasks.splice(newIndex, 0, sortedTasks.splice(oldIndex, 1)[0]) + // Otherwise insert it + } else { + sortedTasks.splice(newIndex, 0, task) + } + // Get the new sort order for the moved task and apply it. + // We just do that to minimize the number of other tasks to be changed. + let newSortOrder + if (newIndex + 1 < sortedTasks.length) { + newSortOrder = sortedTasks[newIndex + 1].sortOrder - Math.pow(-1, +this.sortDirection) + } else { + newSortOrder = sortedTasks[newIndex - 1].sortOrder + Math.pow(-1, +this.sortDirection) + } + if (newSortOrder < 0) { + newSortOrder = 0 + } + // If we moved the task from a different list, don't schedule a request to the server, + // this will be done afterwards. + const newOrder = { task: sortedTasks[newIndex], order: newSortOrder } + if (oldIndex > -1) { + this.setSortOrder(newOrder) + } else { + this.commitSortOrder(newOrder) + } + + // Check the sort orders to be strictly monotonous + if (this.sortDirection) { + sortedTasks.reverse() + } + let currentIndex = 1 + while (currentIndex < sortedTasks.length) { + if (sortedTasks[currentIndex].sortOrder <= sortedTasks[currentIndex - 1].sortOrder) { + const order = { task: sortedTasks[currentIndex], order: sortedTasks[currentIndex - 1].sortOrder + 1 } + if (sortedTasks[currentIndex] === task) { + this.commitSortOrder(order) + } else { + this.setSortOrder(order) + } + } + currentIndex++ + } + }, + + /** + * Called when a task is dropped. + * We only handle sorting tasks here. + * + * @param {Object} $event The event which caused the drop + */ + onEnd($event) { + // Don't do anything if the tasks are not sorted but moved. + if ($event.to !== $event.from) { + return + } + /** + * We have to adjust the sortOrder property of the tasks + * to achieve the desired sort order. + */ + this.adjustSortOrder(null, $event.newIndex, $event.oldIndex) + }, + /** * Called when a task is dropped. + * We handle changing the parent task, calendar or collection here + * and also have to sort a task to the correct position + * in case of manual sort order. * * @param {Object} $event The event which caused the drop */ @@ -76,6 +195,11 @@ export default { if (taskAttribute) { task = this.getTask(taskAttribute.value) } + /** + * We have to adjust the sortOrder property of the tasks + * to achieve the desired sort order. + */ + this.adjustSortOrder(task, $event.newIndex, -1) // Move the task to a new calendar or parent. this.prepareMoving(task, $event) this.prepareCollecting(task, $event) diff --git a/src/components/TheCollections/Calendar.vue b/src/components/TheCollections/Calendar.vue index e6165ecc..669efc8a 100644 --- a/src/components/TheCollections/Calendar.vue +++ b/src/components/TheCollections/Calendar.vue @@ -40,28 +40,18 @@ License along with this library. If not, see <http://www.gnu.org/licenses/>. <div class="task-list"> <div class="grouped-tasks"> <TaskDragContainer + :tasks="uncompletedRootTasks(calendar.tasks)" :calendar-id="calendarId" :disabled="calendar.readOnly" - class="tasks" - collection-id="uncompleted" - type="list"> - <TaskBody v-for="task in sort(uncompletedRootTasks(calendar.tasks), sortOrder, sortDirection)" - :key="task.key" - :task="task" /> - </TaskDragContainer> + collection-id="uncompleted" /> <h2 v-show="completedCount(calendarId)" class="heading heading--hiddentasks reactive" @click="toggleHidden"> <span class="heading__title icon-triangle-s">{{ completedCountString }}</span> </h2> <TaskDragContainer v-if="showHidden" + :tasks="completedRootTasks(calendar.tasks)" :calendar-id="calendarId" :disabled="calendar.readOnly" - class="completed-tasks" - collection-id="completed" - type="list"> - <TaskBody v-for="task in sort(completedRootTasks(calendar.tasks), sortOrder, sortDirection)" - :key="task.key" - :task="task" /> - </TaskDragContainer> + collection-id="completed" /> <LoadCompletedButton :calendar="calendar" /> <DeleteCompletedModal v-if="calendar.loadedCompleted && !calendar.readOnly" :calendar="calendar" /> </div> @@ -71,16 +61,13 @@ License along with this library. If not, see <http://www.gnu.org/licenses/>. <script> import { mapGetters, mapActions } from 'vuex' -import { sort } from '../../store/storeHelper' import SortorderDropdown from '../SortorderDropdown' import LoadCompletedButton from '../LoadCompletedButton' import DeleteCompletedModal from '../DeleteCompletedModal' -import TaskBody from '../TaskBody' import TaskDragContainer from '../TaskDragContainer' export default { components: { - TaskBody, SortorderDropdown, LoadCompletedButton, TaskDragContainer, @@ -129,15 +116,12 @@ export default { calendar: 'getCalendarByRoute', uncompletedRootTasks: 'findUncompletedRootTasks', completedRootTasks: 'findCompletedRootTasks', - sortOrder: 'sortOrder', - sortDirection: 'sortDirection', }), }, methods: { ...mapActions([ 'createTask', ]), - sort, toggleHidden() { this.showHidden = +!this.showHidden }, diff --git a/src/components/TheCollections/General.vue b/src/components/TheCollections/General.vue index 00e39d9a..ba95f07e 100644 --- a/src/components/TheCollections/General.vue +++ b/src/components/TheCollections/General.vue @@ -46,16 +46,11 @@ License along with this library. If not, see <http://www.gnu.org/licenses/>. <span class="heading__title">{{ calendar.displayName }}</span> </h2> <TaskDragContainer - :calendar-id="calendar.id" - :collection-id="collectionId" + :tasks="calendar.filteredTasks" :disabled="calendar.readOnly" - class="tasks" - type="list"> - <TaskBody v-for="task in sort(calendar.filteredTasks, sortOrder, sortDirection)" - :key="task.key" - :task="task" - :collection-string="collectionId" /> - </TaskDragContainer> + :collection-string="collectionId" + :calendar-id="calendar.id" + :collection-id="collectionId" /> <LoadCompletedButton v-if="collectionId === 'completed'" :calendar="calendar" /> </div> </div> @@ -65,15 +60,13 @@ License along with this library. If not, see <http://www.gnu.org/licenses/>. <script> import moment from '@nextcloud/moment' import { mapGetters, mapActions } from 'vuex' -import { sort, isTaskInList, isParentInList } from '../../store/storeHelper' +import { isTaskInList, isParentInList } from '../../store/storeHelper' import SortorderDropdown from '../SortorderDropdown' import LoadCompletedButton from '../LoadCompletedButton' -import TaskBody from '../TaskBody' import TaskDragContainer from '../TaskDragContainer' export default { components: { - TaskBody, SortorderDropdown, LoadCompletedButton, TaskDragContainer, @@ -129,15 +122,12 @@ export default { ...mapGetters({ calendar: 'getDefaultCalendar', calendars: 'getSortedCalendars', - sortOrder: 'sortOrder', - sortDirection: 'sortDirection', }), }, methods: { ...mapActions([ 'createTask', ]), - sort, clearNewTask(event) { event.target.blur() this.newTaskName = '' diff --git a/src/components/TheCollections/Week.vue b/src/components/TheCollections/Week.vue index 1faaf0cb..a4055402 100644 --- a/src/components/TheCollections/Week.vue +++ b/src/components/TheCollections/Week.vue @@ -45,14 +45,9 @@ License along with this library. If not, see <http://www.gnu.org/licenses/>. <span class="heading__title">{{ dayString(day.diff) }}</span> </h2> <TaskDragContainer - :collection-id="'week-' + day.diff" - class="tasks" - type="list"> - <TaskBody v-for="task in sort(day.tasks, sortOrder, sortDirection)" - :key="task.key" - :task="task" - :collection-string="'week-' + day.diff" /> - </TaskDragContainer> + :tasks="day.tasks" + :collection-string="`week-${day.diff}`" + :collection-id="`week-${day.diff}`" /> </div> </div> </div> @@ -61,14 +56,12 @@ License along with this library. If not, see <http://www.gnu.org/licenses/>. <script> import moment from '@nextcloud/moment' import { mapGetters, mapActions } from 'vuex' -import { sort, isTaskInList } from '../../store/storeHelper' +import { isTaskInList } from '../../store/storeHelper' import SortorderDropdown from '../SortorderDropdown' -import TaskBody from '../TaskBody' import TaskDragContainer from '../TaskDragContainer' export default { components: { - TaskBody, SortorderDropdown, TaskDragContainer, }, @@ -83,8 +76,6 @@ export default { calendar: 'getDefaultCalendar', tasks: 'getAllTasks', uncompletedRootTasks: 'findUncompletedRootTasks', - sortOrder: 'sortOrder', - sortDirection: 'sortDirection', }), /** @@ -122,8 +113,6 @@ export default { 'createTask', ]), - sort, - dayString(day) { const date = moment().add(day, 'day') let dayString diff --git a/src/models/task.js b/src/models/task.js index d43e6766..85e89ef6 100644 --- a/src/models/task.js +++ b/src/models/task.js @@ -112,6 +112,12 @@ export default class Task { this._class = this.vtodo.getFirstPropertyValue('class') || 'PUBLIC' this._pinned = this.vtodo.getFirstPropertyValue('x-pinned') === 'true' + let sortOrder = this.vtodo.getFirstPropertyValue('x-apple-sort-order') + if (sortOrder === null) { + sortOrder = this.getSortOrder() + } + this._sortOrder = +sortOrder + this._searchQuery = '' this._matchesSearchQuery = true } @@ -555,6 +561,10 @@ export default class Task { this.updateLastModified() this._created = this.vtodo.getFirstPropertyValue('created') this._createdMoment = moment(this._created, 'YYYYMMDDTHHmmss') + // Update the sortorder if necessary + if (this.vtodo.getFirstPropertyValue('x-apple-sort-order') === null) { + this._sortOrder = this.getSortOrder() + } } get class() { @@ -571,6 +581,48 @@ export default class Task { this._class = this.vtodo.getFirstPropertyValue('class') || 'PUBLIC' } + get sortOrder() { + return this._sortOrder + } + + set sortOrder(sortOrder) { + // We expect an integer for the sort order. + sortOrder = parseInt(sortOrder) + if (isNaN(sortOrder)) { + this.vtodo.removeProperty('x-apple-sort-order') + // Get the default sort order. + sortOrder = this.getSortOrder() + } else { + this.vtodo.updatePropertyWithValue('x-apple-sort-order', sortOrder) + } + this.updateLastModified() + this._sortOrder = sortOrder + } + + /** + * Construct the default value for the sort order + * from the created date. + * + * @returns {Integer} The sort order + */ + getSortOrder() { + // If there is no created date we return 0. + if (this._created === null) { + return 0 + } + return this._created.subtractDate( + new ICAL.Time({ + year: 2001, + month: 1, + day: 1, + hour: 0, + minute: 0, + second: 0, + isDate: false, + }) + ).toSeconds() + } + /** * Checks if the task matches the search query * diff --git a/src/store/settings.js b/src/store/settings.js index b5f00ca3..d0d9f6e5 100644 --- a/src/store/settings.js +++ b/src/store/settings.js @@ -31,7 +31,10 @@ import Vuex from 'vuex' Vue.use(Vuex) const state = { - settings: {}, + settings: { + sortOrder: 'default', + sortDirection: false, + }, } const getters = { diff --git a/src/store/storeHelper.js b/src/store/storeHelper.js index c50a79e5..19d9aa03 100644 --- a/src/store/storeHelper.js +++ b/src/store/storeHelper.js @@ -243,6 +243,10 @@ function sort(tasks, sortOrder, sortDirection) { comparators = [sortByPinned, sortByCompletedDate, sortAlphabetically] break } + case 'manual': { + comparators = [sortBySortOrder] + break + } default: comparators = [sortByPinned, sortByCompleted, sortByDue, sortByPriority, sortByStart, sortAlphabetically] } @@ -391,6 +395,17 @@ function sortByDate(taskA, taskB, date) { } /** + * Comparator to compare two tasks by sort order in ascending order + * + * @param {Task} taskA The first task + * @param {Task} taskB The second task + * @returns {Integer} + */ +function sortBySortOrder(taskA, taskB) { + return taskA.sortOrder - taskB.sortOrder +} + +/** * Function to convert a moment to a ICAL Time * * @param {Moment} moment The moment to convert diff --git a/src/store/tasks.js b/src/store/tasks.js index 6458dc13..9e7c807e 100644 --- a/src/store/tasks.js +++ b/src/store/tasks.js @@ -463,6 +463,17 @@ const mutations = { }, /** + * Sets the sort order of a task + * + * @param {Object} state The store data + * @param {Task} task The task + * @param {Integer} order The sort order + */ + setSortOrder(state, { task, order }) { + Vue.set(task, 'sortOrder', order) + }, + + /** * Sets the due date of a task * * @param {Object} state The store data @@ -1057,6 +1068,21 @@ const actions = { }, /** + * Sets the sort order of a task + * + * @param {Object} context The store context + * @param {Task} task The task to update + * @param {Integer} order The sort order + */ + async setSortOrder(context, { task, order }) { + if (task.sortOrder === order) { + return + } + context.commit('setSortOrder', { task, order }) + context.dispatch('scheduleTaskUpdate', task) + }, + + /** * Sets the due date of a task * * @param {Object} context The store context |