Skip to content

Commit db26b4f

Browse files
committed
feat: task duplication
Signed-off-by: Maximilian Martin <maximilian_martin@gmx.de>
1 parent 831714f commit db26b4f

File tree

2 files changed

+90
-0
lines changed

2 files changed

+90
-0
lines changed

src/components/TaskBody.vue

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,15 @@ License along with this library. If not, see <http://www.gnu.org/licenses/>.
126126
</template>
127127
{{ t('tasks', 'Delete task') }}
128128
</NcActionButton>
129+
<NcActionButton v-if="!readOnly"
130+
:close-after-click="true"
131+
class="reactive no-nav"
132+
@click="duplicateTask({ task })">
133+
<template #icon>
134+
<ContentDuplicate :size="20" />
135+
</template>
136+
{{ t('tasks', 'Duplicate task') }}
137+
</NcActionButton>
129138
</NcActions>
130139
<NcActions v-if="task.deleteCountdown !== null">
131140
<NcActionButton class="reactive no-nav"
@@ -206,6 +215,7 @@ import Linkify from '@nextcloud/vue/directives/Linkify'
206215
207216
import Bell from 'vue-material-design-icons/BellOutline.vue'
208217
import Delete from 'vue-material-design-icons/TrashCanOutline.vue'
218+
import ContentDuplicate from 'vue-material-design-icons/ContentDuplicate.vue'
209219
import Eye from 'vue-material-design-icons/EyeOutline.vue'
210220
import Pin from 'vue-material-design-icons/PinOutline.vue'
211221
import Plus from 'vue-material-design-icons/Plus.vue'
@@ -238,6 +248,7 @@ export default {
238248
NcTextField,
239249
Bell,
240250
Delete,
251+
ContentDuplicate,
241252
Eye,
242253
Pin,
243254
Plus,
@@ -487,6 +498,7 @@ export default {
487498
'toggleCompleted',
488499
'toggleStarred',
489500
'createTask',
501+
'duplicateTask',
490502
'getTasksFromCalendar',
491503
'toggleSubtasksVisibility',
492504
'toggleCompletedSubtasksVisibility',

src/store/tasks.js

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { translate as t } from '@nextcloud/l10n'
3333
import moment from '@nextcloud/moment'
3434

3535
import ICAL from 'ical.js'
36+
import { randomUUID } from '../utils/crypto.js'
3637

3738
const state = {
3839
tasks: {},
@@ -859,6 +860,83 @@ const actions = {
859860
}
860861
},
861862

863+
/**
864+
* Duplicates an existing task. Subtasks are duplicated recursively.
865+
*
866+
* @param {object} context The store context
867+
* @param {object} payload Destructuring object
868+
* @param {Task} payload.task The task to duplicate
869+
* @param {Calendar} [payload.calendar] The calendar to create the duplicate in (defaults to original task calendar)
870+
* @param {Task|null} [payload.parent] The parent task to attach the duplicate to (optional)
871+
* @return {Promise<Task>} The newly created duplicate task
872+
*/
873+
async duplicateTask(context, payload) {
874+
// Support being called with either the Task directly or a payload object
875+
// called as: duplicateTask(task) or duplicateTask({ task, calendar, parent })
876+
const task = payload && payload.task ? payload.task : payload
877+
const calendar = payload && payload.calendar ? payload.calendar : (task ? task.calendar : null)
878+
const parent = payload && Object.prototype.hasOwnProperty.call(payload, 'parent') ? payload.parent : null
879+
880+
// Don't try to duplicate non-existing tasks
881+
if (!task) {
882+
return null
883+
}
884+
// Don't try to duplicate tasks into read-only calendars
885+
if (!calendar || calendar.readOnly) {
886+
return null
887+
}
888+
// Don't duplicate tasks with access class not PUBLIC into calendars shared with me
889+
if (calendar.isSharedWithMe && task.class !== 'PUBLIC') {
890+
return null
891+
}
892+
893+
// Create a new Task from the existing task's jCal
894+
const vData = ICAL.stringify(task.jCal)
895+
const newTask = new Task(vData, calendar)
896+
897+
// Assign a new UID and created timestamp
898+
newTask.uid = randomUUID()
899+
newTask.created = ICAL.Time.fromJSDate(new Date(), true)
900+
newTask.dav = null
901+
newTask.conflict = false
902+
903+
// If a parent was provided, link to it. Otherwise, if the original task had
904+
// a related parent and that parent exists in the target calendar, keep relation.
905+
if (parent) {
906+
newTask.related = parent.uid
907+
} else if (task.related) {
908+
const existingParent = context.getters.getTaskByUid(task.related)
909+
if (existingParent && existingParent.calendar && existingParent.calendar.id === calendar.id) {
910+
newTask.related = task.related
911+
}
912+
else {
913+
newTask.related = null
914+
}
915+
}
916+
917+
// Create the new vObject on the server
918+
try {
919+
const response = await calendar.dav.createVObject(ICAL.stringify(newTask.jCal))
920+
newTask.dav = response
921+
newTask.syncStatus = new SyncStatus('success', t('tasks', 'Successfully duplicated the task.'))
922+
context.commit('appendTask', newTask)
923+
context.commit('addTaskToCalendar', newTask)
924+
const parentLocal = context.getters.getTaskByUid(newTask.related)
925+
context.commit('addTaskToParent', { task: newTask, parent: parentLocal })
926+
} catch (error) {
927+
console.error(error)
928+
showError(t('tasks', 'Could not duplicate the task.'))
929+
return null
930+
}
931+
932+
// Duplicate subtasks recursively, attaching them to the new parent
933+
await Promise.all(Object.values(task.subTasks).map(async (subTask) => {
934+
await context.dispatch('duplicateTask', { task: subTask, calendar, parent: newTask })
935+
}))
936+
937+
return newTask
938+
},
939+
862940
/**
863941
* Deletes a task
864942
*

0 commit comments

Comments
 (0)