Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/components/TaskBody.vue
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,15 @@ License along with this library. If not, see <http://www.gnu.org/licenses/>.
</template>
{{ task.hideCompletedSubtasks ? t('tasks', 'Show closed subtasks') : t('tasks', 'Hide closed subtasks') }}
</NcActionButton>
<NcActionButton v-if="!readOnly"
:close-after-click="true"
class="reactive no-nav"
@click="duplicateTask({ task })">
<template #icon>
<ContentDuplicate :size="20" />
</template>
{{ t('tasks', 'Duplicate task') }}
</NcActionButton>
<NcActionButton v-if="!readOnly"
class="reactive no-nav"
@click="scheduleTaskDeletion(task)">
Expand Down Expand Up @@ -205,6 +214,7 @@ import NcTextField from '@nextcloud/vue/components/NcTextField'
import Linkify from '@nextcloud/vue/directives/Linkify'

import Bell from 'vue-material-design-icons/BellOutline.vue'
import ContentDuplicate from 'vue-material-design-icons/ContentDuplicate.vue'
import Delete from 'vue-material-design-icons/TrashCanOutline.vue'
import Eye from 'vue-material-design-icons/EyeOutline.vue'
import Pin from 'vue-material-design-icons/PinOutline.vue'
Expand Down Expand Up @@ -237,6 +247,7 @@ export default {
NcProgressBar,
NcTextField,
Bell,
ContentDuplicate,
Delete,
Eye,
Pin,
Expand Down Expand Up @@ -487,6 +498,7 @@ export default {
'toggleCompleted',
'toggleStarred',
'createTask',
'duplicateTask',
'getTasksFromCalendar',
'toggleSubtasksVisibility',
'toggleCompletedSubtasksVisibility',
Expand Down
77 changes: 77 additions & 0 deletions src/store/tasks.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import dayjs from 'dayjs'

import ICAL from 'ical.js'
import { randomUUID } from '../utils/crypto.js'

const state = {
tasks: {},
Expand Down Expand Up @@ -859,6 +860,82 @@
}
},

/**
* Duplicates an existing task. Subtasks are duplicated recursively.
*
* @param {object} context The store context
* @param {object} payload Destructuring object
* @param {Task} payload.task The task to duplicate
* @param {Calendar} [payload.calendar] The calendar to create the duplicate in (defaults to original task calendar)
* @param {Task|null} [payload.parent] The parent task to attach the duplicate to (optional)
* @return {Promise<Task>} The newly created duplicate task
*/
async duplicateTask(context, payload) {
// Support being called with either the Task directly or a payload object
// called as: duplicateTask(task) or duplicateTask({ task, calendar, parent })
const task = payload && payload.task ? payload.task : payload
const calendar = payload && payload.calendar ? payload.calendar : (task ? task.calendar : null)
const parent = payload && Object.prototype.hasOwnProperty.call(payload, 'parent') ? payload.parent : null

// Don't try to duplicate non-existing tasks
if (!task) {
return null
}
// Don't try to duplicate tasks into read-only calendars
if (!calendar || calendar.readOnly) {
return null
}
// Don't duplicate tasks with access class not PUBLIC into calendars shared with me
if (calendar.isSharedWithMe && task.class !== 'PUBLIC') {
return null
}

// Create a new Task from the existing task's jCal
const vData = ICAL.stringify(task.jCal)
const newTask = new Task(vData, calendar)

// Assign a new UID and created timestamp
newTask.uid = randomUUID()

Check failure

Code scanning / CodeQL

Insecure randomness High

This uses a cryptographically insecure random number generated at
Math.random()
in a security context.
newTask.created = ICAL.Time.fromJSDate(new Date(), true)
newTask.dav = null
newTask.conflict = false

// If a parent was provided, link to it. Otherwise, if the original task had
// a related parent and that parent exists in the target calendar, keep relation.
if (parent) {
newTask.related = parent.uid
} else if (task.related) {
const existingParent = context.getters.getTaskByUid(task.related)
if (existingParent && existingParent.calendar && existingParent.calendar.id === calendar.id) {
newTask.related = task.related
} else {
newTask.related = null
}
}

// Create the new vObject on the server
try {
const response = await calendar.dav.createVObject(ICAL.stringify(newTask.jCal))
newTask.dav = response
newTask.syncStatus = new SyncStatus('success', t('tasks', 'Successfully duplicated the task.'))
context.commit('appendTask', newTask)
context.commit('addTaskToCalendar', newTask)
const parentLocal = context.getters.getTaskByUid(newTask.related)
context.commit('addTaskToParent', { task: newTask, parent: parentLocal })
} catch (error) {
console.error(error)
showError(t('tasks', 'Could not duplicate the task.'))
return null
}

// Duplicate subtasks recursively, attaching them to the new parent
await Promise.all(Object.values(task.subTasks).map(async (subTask) => {
await context.dispatch('duplicateTask', { task: subTask, calendar, parent: newTask })
}))

return newTask
},

/**
* Deletes a task
*
Expand Down
11 changes: 11 additions & 0 deletions src/views/AppSidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,14 @@ License along with this library. If not, see <http://www.gnu.org/licenses/>.
</template>
{{ t('tasks', 'Export') }}
</NcActionLink>
<NcActionButton v-if="!readOnly"
:close-after-click="true"
@click="duplicateTask({ task })">
<template #icon>
<ContentDuplicate :size="20" />
</template>
{{ t('tasks', 'Duplicate task') }}
</NcActionButton>
<NcActionButton v-if="!readOnly"
@click="scheduleTaskDeletion(task)">
<template #icon>
Expand Down Expand Up @@ -304,6 +312,7 @@ 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 ContentDuplicate from 'vue-material-design-icons/ContentDuplicate.vue'
import Delete from 'vue-material-design-icons/TrashCanOutline.vue'
import Download from 'vue-material-design-icons/TrayArrowDown.vue'
import InformationOutline from 'vue-material-design-icons/InformationOutline.vue'
Expand Down Expand Up @@ -337,6 +346,7 @@ export default {
CalendarEnd,
CalendarStart,
CalendarCheck,
ContentDuplicate,
Delete,
Download,
InformationOutline,
Expand Down Expand Up @@ -751,6 +761,7 @@ export default {
'setStatus',
'getTaskByUri',
'togglePinned',
'duplicateTask',
]),

async loadTask() {
Expand Down
Loading