Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/tasks.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorRaimund Schlüßler <raimund.schluessler@mailbox.org>2019-01-27 09:24:23 +0300
committerRaimund Schlüßler <raimund.schluessler@mailbox.org>2020-05-09 21:37:15 +0300
commit64fc9b27af2f794ec89ca4d789b6d712447a1b81 (patch)
tree877ded2215a1814f154fb78f3a250728f0c51cc7 /src
parent0b75a29dcf83b56d48189d99e9aa189b1306e6d4 (diff)
Implement manual sorting
Signed-off-by: Raimund Schlüßler <raimund.schluessler@mailbox.org>
Diffstat (limited to 'src')
-rw-r--r--src/components/SortorderDropdown.vue23
-rw-r--r--src/components/TaskBody.vue17
-rw-r--r--src/components/TaskDragContainer.vue130
-rw-r--r--src/components/TheCollections/Calendar.vue24
-rw-r--r--src/components/TheCollections/General.vue20
-rw-r--r--src/components/TheCollections/Week.vue19
-rw-r--r--src/models/task.js52
-rw-r--r--src/store/settings.js5
-rw-r--r--src/store/storeHelper.js15
-rw-r--r--src/store/tasks.js26
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