From 55dd7d298187dcc8393ae67340117d66d45dc4ef Mon Sep 17 00:00:00 2001 From: kolaente Date: Wed, 11 Sep 2024 17:58:42 +0200 Subject: [PATCH] fix(task): specify task index when creating multiple tasks at once This change allows to specify the task index when creating a task, which will then be checked to avoid duplicates and used. This allows us to calculate the indexes for all tasks beforehand when creating them at once using quick add magic. The method is not bulletproof, but already fixes a problem where multiple tasks would have the same index when created that way. Resolves https://community.vikunja.io/t/add-multiple-tasks-at-once/333/16 --- frontend/src/components/input/Password.vue | 2 +- frontend/src/components/tasks/AddTask.vue | 47 ++++++++++++++++++++-- frontend/src/stores/tasks.ts | 3 ++ pkg/models/tasks.go | 23 +++++++++-- 4 files changed, 66 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/input/Password.vue b/frontend/src/components/input/Password.vue index ed1a0700c..9b490d3da 100644 --- a/frontend/src/components/input/Password.vue +++ b/frontend/src/components/input/Password.vue @@ -36,7 +36,7 @@ import {ref, watchEffect} from 'vue' import {useDebounceFn} from '@vueuse/core' import {useI18n} from 'vue-i18n' import BaseButton from '@/components/base/BaseButton.vue' -import {validatePassword} from '@/helpers/validatePasswort'; +import {validatePassword} from '@/helpers/validatePasswort' const props = withDefaults(defineProps<{ modelValue: string, diff --git a/frontend/src/components/tasks/AddTask.vue b/frontend/src/components/tasks/AddTask.vue index 4a022bda2..d7aaf173e 100644 --- a/frontend/src/components/tasks/AddTask.vue +++ b/frontend/src/components/tasks/AddTask.vue @@ -51,6 +51,7 @@ import {computed, ref} from 'vue' import {useI18n} from 'vue-i18n' import {useElementHover} from '@vueuse/core' +import { useRouter } from 'vue-router' import {RELATION_KIND} from '@/types/IRelationKind' import type {ITask} from '@/modelTypes/ITask' @@ -66,6 +67,8 @@ import {useAuthStore} from '@/stores/auth' import {useTaskStore} from '@/stores/tasks' import {useAutoHeightTextarea} from '@/composables/useAutoHeightTextarea' +import TaskService from '@/services/task' +import TaskModel from '@/models/task' const props = withDefaults(defineProps<{ defaultPosition?: number, @@ -81,6 +84,7 @@ const {textarea: newTaskInput} = useAutoHeightTextarea(newTaskTitle) const {t} = useI18n({useScope: 'global'}) const authStore = useAuthStore() const taskStore = useTaskStore() +const router = useRouter() // enable only if we don't have a modal // onStartTyping(() => { @@ -129,21 +133,56 @@ async function addTask() { const allLabels = tasksToCreate.map(({title}) => getLabelsFromPrefix(title, authStore.settings.frontendSettings.quickAddMagicMode) ?? []) await taskStore.ensureLabelsExist(allLabels.flat()) - const newTasks = tasksToCreate.map(async ({title, project}) => { + const taskCollectionService = new TaskService() + const projectIndices = new Map() + + let currentProjectId = authStore.settings.defaultProjectId + if (typeof router.currentRoute.value.params.projectId !== 'undefined') { + currentProjectId = Number(router.currentRoute.value.params.projectId) + } + + // Create a map of project indices before creating tasks + if (tasksToCreate.length > 1) { + for (const {project} of tasksToCreate) { + const projectId = project !== null + ? await taskStore.findProjectId({project, projectId: 0}) + : currentProjectId + + if (!projectIndices.has(projectId)) { + const newestTask = await taskCollectionService.getAll(new TaskModel({}), { + sort_by: ['id'], + order_by: ['desc'], + per_page: 1, + filter: `project_id = ${projectId}`, + }) + projectIndices.set(projectId, newestTask[0]?.index || 0) + } + } +} + + const newTasks = tasksToCreate.map(async ({title, project}, index) => { if (title === '') { return } // If the task has a project specified, make sure to use it - let projectId = null - if (project !== null) { - projectId = await taskStore.findProjectId({project, projectId: 0}) + const projectId = project !== null + ? await taskStore.findProjectId({project, projectId: 0}) + : currentProjectId + + // Calculate new index for this task per project + let taskIndex: number | undefined + if (tasksToCreate.length > 1) { + const lastIndex = projectIndices.get(projectId) + taskIndex = lastIndex + index + 1 } + console.log('many tasks to create', taskIndex) const task = await taskStore.createNewTask({ title, projectId: projectId || authStore.settings.defaultProjectId, position: props.defaultPosition, + index: taskIndex, }) createdTasks[title] = task return task diff --git a/frontend/src/stores/tasks.ts b/frontend/src/stores/tasks.ts index 67b092f9e..ab124c8b2 100644 --- a/frontend/src/stores/tasks.ts +++ b/frontend/src/stores/tasks.ts @@ -414,6 +414,7 @@ export const useTaskStore = defineStore('task', () => { bucketId, projectId, position, + index, } : Partial, ) { @@ -429,6 +430,7 @@ export const useTaskStore = defineStore('task', () => { projectId, bucketId, position, + index, })) } finally { cancel() @@ -467,6 +469,7 @@ export const useTaskStore = defineStore('task', () => { assignees, bucketId: bucketId || 0, position, + index, }) task.repeatAfter = parsedTask.repeats diff --git a/pkg/models/tasks.go b/pkg/models/tasks.go index 5de2d32df..78bc47b4b 100644 --- a/pkg/models/tasks.go +++ b/pkg/models/tasks.go @@ -738,10 +738,25 @@ func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool, setB t.UID = uuid.NewString() } - // Get the index for this task - t.Index, err = getNextTaskIndex(s, t.ProjectID) - if err != nil { - return err + // Check if an index was provided, otherwise calculate a new one + if t.Index == 0 { + t.Index, err = getNextTaskIndex(s, t.ProjectID) + if err != nil { + return err + } + } else { + // Check if the provided index is already taken + exists, err := s.Where("project_id = ? AND `index` = ?", t.ProjectID, t.Index).Exist(&Task{}) + if err != nil { + return err + } + if exists { + // If the index is taken, calculate a new one + t.Index, err = getNextTaskIndex(s, t.ProjectID) + if err != nil { + return err + } + } } t.HexColor = utils.NormalizeHex(t.HexColor)