Skip to content

Commit

Permalink
Merge pull request #2686 from creightonfrance/1184-feature-request-ed…
Browse files Browse the repository at this point in the history
…itable-completion-date

feat: allow editing completion date
  • Loading branch information
raimund-schluessler authored Jan 5, 2025
2 parents 9907d72 + a440671 commit e83edce
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 6 deletions.
9 changes: 8 additions & 1 deletion src/components/AppSidebar/DateTimePickerItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,13 @@ export default {
type: Boolean,
default: false,
},
/**
* Whether the date can be considered 'overdue'
*/
checkOverdue: {
type: Boolean,
default: true,
}
},
data() {
return {
Expand All @@ -139,7 +146,7 @@ export default {
return this.date.isValid()
},
isOverdue() {
return overdue(this.date)
return this.checkOverdue && overdue(this.date)
},
},
methods: {
Expand Down
26 changes: 21 additions & 5 deletions src/models/task.js
Original file line number Diff line number Diff line change
Expand Up @@ -306,12 +306,14 @@ export default class Task {
}
}

setCompleted(completed) {
setCompleted(completed, completedDate = null) {
if (completed) {
const now = ICAL.Time.fromJSDate(new Date(), true)
this.vtodo.updatePropertyWithValue('completed', now)
this._completedDate = now
this._completedDateMoment = moment(now, 'YYYYMMDDTHHmmssZ')
if (completedDate === null) {
completedDate = ICAL.Time.fromJSDate(new Date(), true)
}
this.vtodo.updatePropertyWithValue('completed', completedDate)
this._completedDate = completedDate
this._completedDateMoment = moment(completedDate, 'YYYYMMDDTHHmmssZ')
} else {
this.vtodo.removeProperty('completed')
this._completedDate = null
Expand All @@ -325,6 +327,20 @@ export default class Task {
return this._completedDate
}

set completedDate(completedDate) {
if (completedDate) {
this.setCompleted(true, completedDate)
this.setComplete(100)
this.setStatus('COMPLETED')
} else {
this.setCompleted(false)
if (this.complete === 100) {
this.setComplete(99)
this.setStatus('IN-PROCESS')
}
}
}

get completedDateMoment() {
return this._completedDateMoment.clone()
}
Expand Down
34 changes: 34 additions & 0 deletions src/store/tasks.js
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,29 @@ const mutations = {
}
},

/**
* Sets the completed date of a task
*
* @param {object} state The store data
* @param {object} data Destructuring object
* @param {Task} data.task The task
* @param {moment|null} data.completedDate The completed date moment
*/
setCompletedDate(state, { task, completedDate }) {
if (completedDate !== null) {
// Check that the completed date is in the past.
const now = moment(ICAL.Time.fromJSDate(new Date(), true), 'YYYYMMDDTHHmmssZ')
if (completedDate.isAfter(now)) {
showError(t('tasks', 'Completion date must be in the past.'))
return
}
// Convert completed date to ICALTime first
completedDate = momentToICALTime(completedDate, false)
}
// Set the completed date
task.completedDate = completedDate
},

/**
* Toggles if the start and due dates of a task are all day
*
Expand Down Expand Up @@ -1318,6 +1341,17 @@ const actions = {
context.dispatch('updateTask', task)
},

/**
* Sets the completed date of a task
*
* @param {object} context The store context
* @param {Task} task The task to update
*/
async setCompletedDate(context, { task, completedDate }) {
context.commit('setCompletedDate', { task, completedDate })
context.dispatch('updateTask', task)
},

/**
* Sets the start or due date to the given day
*
Expand Down
44 changes: 44 additions & 0 deletions src/views/AppSidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,18 @@ License along with this library. If not, see <http://www.gnu.org/licenses/>.
:placeholder="t('tasks', 'Select a status')"
icon="IconPulse"
@change-value="changeStatus" />
<DateTimePickerItem v-show="task.completed"
:date="task.completedDateMoment"
:value="newCompletedDate"
:property-string="completedString"
:read-only="readOnly"
:task="task"
:check-overdue=false
@set-value="changeCompletedDate">
<template #icon>
<CalendarCheck :size="20" />
</template>
</DateTimePickerItem>
<SliderItem v-show="!readOnly || task.priority"
:value="task.priority"
:property-string="priorityString"
Expand Down Expand Up @@ -289,6 +301,7 @@ import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
import { generateUrl } from '@nextcloud/router'

import Calendar from 'vue-material-design-icons/Calendar.vue'
import CalendarCheck from 'vue-material-design-icons/CalendarCheck.vue'
import CalendarEnd from 'vue-material-design-icons/CalendarEnd.vue'
import CalendarStart from 'vue-material-design-icons/CalendarStart.vue'
import Delete from 'vue-material-design-icons/Delete.vue'
Expand Down Expand Up @@ -319,6 +332,7 @@ export default {
Calendar,
CalendarEnd,
CalendarStart,
CalendarCheck,
Delete,
Download,
InformationOutline,
Expand Down Expand Up @@ -549,6 +563,18 @@ export default {
}
return reference.toDate()
},
/**
* Initializes the completed date of a task
*
* @return {Date|null} The completed date moment
*/
newCompletedDate() {
const completedDate = this.task.completedDateMoment
if (completedDate.isValid()) {
return completedDate.toDate()
}
return null
},
taskStatusLabel() {
return this.loading ? t('tasks', 'Loading task from server.') : t('tasks', 'Task not found!')
},
Expand Down Expand Up @@ -709,6 +735,7 @@ export default {
'addTag',
'setDue',
'setStart',
'setCompletedDate',
'toggleAllDay',
'moveTask',
'setClassification',
Expand Down Expand Up @@ -817,6 +844,23 @@ export default {
this.setDue({ task, due, allDay: this.allDay })
},

/**
* Sets the completed date to the given Date or to null
*
* @param {object} context The data object
* @param {Task} context.task The task for which to set the date
* @param {Date|null} context.value The new completed date
*/
changeCompletedDate({ task, value: completedDate }) {
if (completedDate) {
completedDate = moment(completedDate)
}
if (this.task.completedDateMoment.isSame(completedDate)) {
return
}
this.setCompletedDate({ task, completedDate })
},

changeClass(classification) {
this.setClassification({ task: this.task, classification: classification.type })
},
Expand Down
35 changes: 35 additions & 0 deletions tests/javascript/unit/views/AppSidebar.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,39 @@ describe('AppSidebar.vue', () => {
actual = wrapper.vm.newDueDate
expect(actual.getTime()).toBe(newDueDate.getTime())
})

it('Task completed date is set correctly', () => {
const wrapper = shallowMount(AppSidebar, {
global: {
plugins: [store, router],
},
})

let actual = wrapper.vm.newCompletedDate
expect(actual).toBe(null)

const newCompletedDate = new Date('2019-01-01T12:00:00')
wrapper.vm.changeCompletedDate({ task: wrapper.vm.task, value: newCompletedDate })

actual = wrapper.vm.newCompletedDate
expect(actual.getTime()).toBe(newCompletedDate.getTime())
})

it('Setting completed date to future is ignored', () => {
const wrapper = shallowMount(AppSidebar, {
global: {
plugins: [store, router],
},
})

let actual = wrapper.vm.newCompletedDate
const expected = new Date('2019-01-01T12:00:00')
expect(actual.getTime()).toBe(expected.getTime())

const newCompletedDate = new Date('2020-01-01T12:00:01')
wrapper.vm.changeCompletedDate({ task: wrapper.vm.task, value: newCompletedDate })

actual = wrapper.vm.newCompletedDate
expect(actual.getTime()).toBe(expected.getTime())
})
})

0 comments on commit e83edce

Please sign in to comment.