Skip to content

Commit 150c203

Browse files
Merge pull request #3006 from max65482/feat/task-duplication
Task duplication
2 parents 4980372 + cc4f055 commit 150c203

File tree

3 files changed

+100
-0
lines changed

3 files changed

+100
-0
lines changed

src/components/TaskBody.vue

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,15 @@ License along with this library. If not, see <http://www.gnu.org/licenses/>.
118118
</template>
119119
{{ task.hideCompletedSubtasks ? t('tasks', 'Show closed subtasks') : t('tasks', 'Hide closed subtasks') }}
120120
</NcActionButton>
121+
<NcActionButton v-if="!readOnly"
122+
:close-after-click="true"
123+
class="reactive no-nav"
124+
@click="duplicateTask({ task })">
125+
<template #icon>
126+
<ContentDuplicate :size="20" />
127+
</template>
128+
{{ t('tasks', 'Duplicate task') }}
129+
</NcActionButton>
121130
<NcActionButton v-if="!readOnly"
122131
class="reactive no-nav"
123132
@click="scheduleTaskDeletion(task)">
@@ -205,6 +214,7 @@ import NcTextField from '@nextcloud/vue/components/NcTextField'
205214
import Linkify from '@nextcloud/vue/directives/Linkify'
206215
207216
import Bell from 'vue-material-design-icons/BellOutline.vue'
217+
import ContentDuplicate from 'vue-material-design-icons/ContentDuplicate.vue'
208218
import Delete from 'vue-material-design-icons/TrashCanOutline.vue'
209219
import Eye from 'vue-material-design-icons/EyeOutline.vue'
210220
import Pin from 'vue-material-design-icons/PinOutline.vue'
@@ -237,6 +247,7 @@ export default {
237247
NcProgressBar,
238248
NcTextField,
239249
Bell,
250+
ContentDuplicate,
240251
Delete,
241252
Eye,
242253
Pin,
@@ -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: 77 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,82 @@ 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+
} else {
912+
newTask.related = null
913+
}
914+
}
915+
916+
// Create the new vObject on the server
917+
try {
918+
const response = await calendar.dav.createVObject(ICAL.stringify(newTask.jCal))
919+
newTask.dav = response
920+
newTask.syncStatus = new SyncStatus('success', t('tasks', 'Successfully duplicated the task.'))
921+
context.commit('appendTask', newTask)
922+
context.commit('addTaskToCalendar', newTask)
923+
const parentLocal = context.getters.getTaskByUid(newTask.related)
924+
context.commit('addTaskToParent', { task: newTask, parent: parentLocal })
925+
} catch (error) {
926+
console.error(error)
927+
showError(t('tasks', 'Could not duplicate the task.'))
928+
return null
929+
}
930+
931+
// Duplicate subtasks recursively, attaching them to the new parent
932+
await Promise.all(Object.values(task.subTasks).map(async (subTask) => {
933+
await context.dispatch('duplicateTask', { task: subTask, calendar, parent: newTask })
934+
}))
935+
936+
return newTask
937+
},
938+
862939
/**
863940
* Deletes a task
864941
*

src/views/AppSidebar.vue

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,14 @@ License along with this library. If not, see <http://www.gnu.org/licenses/>.
116116
</template>
117117
{{ t('tasks', 'Export') }}
118118
</NcActionLink>
119+
<NcActionButton v-if="!readOnly"
120+
:close-after-click="true"
121+
@click="duplicateTask({ task })">
122+
<template #icon>
123+
<ContentDuplicate :size="20" />
124+
</template>
125+
{{ t('tasks', 'Duplicate task') }}
126+
</NcActionButton>
119127
<NcActionButton v-if="!readOnly"
120128
@click="scheduleTaskDeletion(task)">
121129
<template #icon>
@@ -303,6 +311,7 @@ import Calendar from 'vue-material-design-icons/Calendar.vue'
303311
import CalendarCheck from 'vue-material-design-icons/CalendarCheck.vue'
304312
import CalendarEnd from 'vue-material-design-icons/CalendarEnd.vue'
305313
import CalendarStart from 'vue-material-design-icons/CalendarStart.vue'
314+
import ContentDuplicate from 'vue-material-design-icons/ContentDuplicate.vue'
306315
import Delete from 'vue-material-design-icons/TrashCanOutline.vue'
307316
import Download from 'vue-material-design-icons/TrayArrowDown.vue'
308317
import InformationOutline from 'vue-material-design-icons/InformationOutline.vue'
@@ -334,6 +343,7 @@ export default {
334343
CalendarEnd,
335344
CalendarStart,
336345
CalendarCheck,
346+
ContentDuplicate,
337347
Delete,
338348
Download,
339349
InformationOutline,
@@ -748,6 +758,7 @@ export default {
748758
'setStatus',
749759
'getTaskByUri',
750760
'togglePinned',
761+
'duplicateTask',
751762
]),
752763
753764
async loadTask() {

0 commit comments

Comments
 (0)