diff --git a/src/components/TaskBody.vue b/src/components/TaskBody.vue index 3e302503e..a6d4079aa 100644 --- a/src/components/TaskBody.vue +++ b/src/components/TaskBody.vue @@ -44,6 +44,7 @@ License along with this library. If not, see . class="no-nav" :cancelled="task.status === 'CANCELLED'" :read-only="readOnly" + :recurring="task.recurring" :priority-class="priorityClass" @toggle-completed="toggleCompleted(task)" /> diff --git a/src/components/TaskCheckbox.vue b/src/components/TaskCheckbox.vue index 7d86a9c62..b5b9e480d 100644 --- a/src/components/TaskCheckbox.vue +++ b/src/components/TaskCheckbox.vue @@ -33,6 +33,7 @@ License along with this library. If not, see . + @@ -45,6 +46,7 @@ import CheckboxBlank from 'vue-material-design-icons/CheckboxBlank.vue' import CheckboxBlankOffOutline from 'vue-material-design-icons/CheckboxBlankOffOutline.vue' import CheckboxBlankOutline from 'vue-material-design-icons/CheckboxBlankOutline.vue' import CheckboxOutline from 'vue-material-design-icons/CheckboxOutline.vue' +import Repeat from 'vue-material-design-icons/Repeat.vue' export default { components: { @@ -52,6 +54,7 @@ export default { CheckboxBlankOffOutline, CheckboxBlankOutline, CheckboxOutline, + Repeat, }, props: { completed: { @@ -66,6 +69,10 @@ export default { type: Boolean, required: true, }, + recurring: { + type: Boolean, + required: true + }, priorityClass: { type: String, default: '', diff --git a/src/models/task.js b/src/models/task.js index 402065206..37064d420 100644 --- a/src/models/task.js +++ b/src/models/task.js @@ -95,6 +95,7 @@ export default class Task { this._completed = !!comp this._completedDate = comp ? comp.toJSDate() : null this._completedDateMoment = moment(this._completedDate, 'YYYYMMDDTHHmmss') + this._recurrence = this.vtodo.getFirstPropertyValue('rrule') this._status = this.vtodo.getFirstPropertyValue('status') this._note = this.vtodo.getFirstPropertyValue('description') || '' this._related = this.getParent()?.getFirstValue() || null @@ -329,6 +330,19 @@ export default class Task { return this._completedDateMoment.clone() } + get recurrence() { + return this._recurrence + } + + get recurring() { + if (this._start === null || this._recurrence === null) { + return false + } + const iter = this._recurrence.iterator(this.start); + iter.next(); + return iter.next() !== null + } + get status() { return this._status } @@ -674,6 +688,27 @@ export default class Task { ).toSeconds() } + /** + * For completing a recurring task, tries to set the task start date to the next recurrence date. + * + * Does nothing if we are at the end of the recurrence (RRULE:UNTIL was reached). + */ + completeRecurring() { + // Get recurrence iterator, starting at start date + const iter = this.recurrence.iterator(this.start); + // Skip the start date itself + iter.next(); + // If there is a next recurrence, update the start date to next recurrence date + let nextRecurrence = iter.next(); + if (nextRecurrence !== null) { + this.start = nextRecurrence; + // If the due date now lies before start date, clear it + if (this.due !== null && this.due.compare(this.start) < 0) { + this.due = null + } + } + } + /** * Checks if the task matches the search query * diff --git a/src/store/tasks.js b/src/store/tasks.js index 8bcb5ae62..fd8250b4d 100644 --- a/src/store/tasks.js +++ b/src/store/tasks.js @@ -1038,6 +1038,12 @@ const actions = { if (task.calendar.isSharedWithMe && task.class !== 'PUBLIC') { return } + // Don't complete a task if it is still recurring, but update its start date instead + if (task.recurring) { + task.completeRecurring() + await context.dispatch('updateTask', task) + return + } if (task.completed) { await context.dispatch('setPercentComplete', { task, complete: 0 }) } else { diff --git a/src/views/AppSidebar.vue b/src/views/AppSidebar.vue index 3ff10a770..78b1e13bf 100644 --- a/src/views/AppSidebar.vue +++ b/src/views/AppSidebar.vue @@ -129,6 +129,7 @@ License along with this library. If not, see . @@ -518,6 +519,14 @@ export default { readOnly() { return this.task.calendar.readOnly || (this.task.calendar.isSharedWithMe && this.task.class !== 'PUBLIC') }, + /** + * Whether this is a recurring task. + * + * @return {boolean} Is the task recurring + */ + recurring() { + return this.task.recurring + }, /** * Whether the dates of a task are all-day * When no dates are set, we consider the last used value.