feat: automatically create subtask relations based on indention (#2443)
continuous-integration/drone/push Build is failing Details

Reviewed-on: #2443
Reviewed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
This commit is contained in:
konrad 2022-09-30 11:47:20 +00:00
commit ec227a6872
3 changed files with 199 additions and 9 deletions

View File

@ -41,18 +41,19 @@
</template>
<script setup lang="ts">
import {ref, watch, unref, computed} from 'vue'
import {computed, ref, unref, watch} from 'vue'
import {useI18n} from 'vue-i18n'
import {tryOnMounted, debouncedWatch, useWindowSize, type MaybeRef} from '@vueuse/core'
import {debouncedWatch, type MaybeRef, tryOnMounted, useWindowSize} from '@vueuse/core'
import QuickAddMagic from '@/components/tasks/partials/quick-add-magic.vue'
import type {ITask} from '@/modelTypes/ITask'
import {parseSubtasksViaIndention} from '@/helpers/parseSubtasksViaIndention'
import TaskRelationService from '@/services/taskRelation'
import TaskRelationModel from '@/models/taskRelation'
import {RELATION_KIND} from '@/types/IRelationKind'
import {useAuthStore} from '@/stores/auth'
import {useTaskStore} from '@/stores/tasks'
function cleanupTitle(title: string) {
return title.replace(/^((\* |\+ |- )(\[ \] )?)/g, '')
}
function useAutoHeightTextarea(value: MaybeRef<string>) {
const textarea = ref<HTMLInputElement>()
const minHeight = ref(0)
@ -161,8 +162,9 @@ async function addTask() {
}
const taskTitleBackup = newTaskTitle.value
const newTasks = newTaskTitle.value.split(/[\r\n]+/).map(async uncleanedTitle => {
const title = cleanupTitle(uncleanedTitle)
const createdTasks: ITask[] = []
const tasksToCreate = parseSubtasksViaIndention(newTaskTitle.value)
const newTasks = tasksToCreate.map(async ({title}) => {
if (title === '') {
return
}
@ -172,13 +174,44 @@ async function addTask() {
listId: authStore.settings.defaultListId,
position: props.defaultPosition,
})
emit('taskAdded', task)
createdTasks.push(task)
return task
})
try {
newTaskTitle.value = ''
await Promise.all(newTasks)
const taskRelationService = new TaskRelationService()
const relations = tasksToCreate.map(async t => {
const createdTask = createdTasks.find(ct => ct.title === t.title)
if (typeof createdTask === 'undefined') {
return
}
if (t.parent === null) {
emit('taskAdded', createdTask)
return
}
const createdParentTask = createdTasks.find(ct => ct.title === t.parent)
if (typeof createdTask === 'undefined' || typeof createdParentTask === 'undefined') {
return
}
const rel = await taskRelationService.create(new TaskRelationModel({
taskId: createdTask.id,
otherTaskId: createdParentTask.id,
relationKind: RELATION_KIND.PARENTTASK,
}))
createdTask.relatedTasks[RELATION_KIND.PARENTTASK] = [createdParentTask]
// we're only emitting here so that the relation shows up in the task list
emit('taskAdded', createdTask)
return rel
})
await Promise.all(relations)
} catch (e: any) {
newTaskTitle.value = taskTitleBackup
if (e?.message === 'NO_LIST') {

View File

@ -0,0 +1,109 @@
import {describe, it, expect} from 'vitest'
import {parseSubtasksViaIndention} from '@/helpers/parseSubtasksViaIndention'
describe('Parse Subtasks via Relation', () => {
it('Should not return a parent for a single task', () => {
const tasks = parseSubtasksViaIndention('single task')
expect(tasks).to.have.length(1)
expect(tasks[0].parent).toBeNull()
})
it('Should not return a parent for multiple tasks without indention', () => {
const tasks = parseSubtasksViaIndention(`task one
task two`)
expect(tasks).to.have.length(2)
expect(tasks[0].parent).toBeNull()
expect(tasks[1].parent).toBeNull()
})
it('Should return a parent for two tasks with indention', () => {
const tasks = parseSubtasksViaIndention(`parent task
sub task`)
expect(tasks).to.have.length(2)
expect(tasks[0].parent).toBeNull()
expect(tasks[0].title).to.eq('parent task')
expect(tasks[1].parent).to.eq('parent task')
expect(tasks[1].title).to.eq('sub task')
})
it('Should return a parent for multiple subtasks', () => {
const tasks = parseSubtasksViaIndention(`parent task
sub task one
sub task two`)
expect(tasks).to.have.length(3)
expect(tasks[0].parent).toBeNull()
expect(tasks[0].title).to.eq('parent task')
expect(tasks[1].title).to.eq('sub task one')
expect(tasks[1].parent).to.eq('parent task')
expect(tasks[2].title).to.eq('sub task two')
expect(tasks[2].parent).to.eq('parent task')
})
it('Should work with multiple indention levels', () => {
const tasks = parseSubtasksViaIndention(`parent task
sub task
sub sub task`)
expect(tasks).to.have.length(3)
expect(tasks[0].parent).toBeNull()
expect(tasks[0].title).to.eq('parent task')
expect(tasks[1].title).to.eq('sub task')
expect(tasks[1].parent).to.eq('parent task')
expect(tasks[2].title).to.eq('sub sub task')
expect(tasks[2].parent).to.eq('sub task')
})
it('Should work with multiple indention levels and multiple tasks', () => {
const tasks = parseSubtasksViaIndention(`parent task
sub task
sub sub task one
sub sub task two`)
expect(tasks).to.have.length(4)
expect(tasks[0].parent).toBeNull()
expect(tasks[0].title).to.eq('parent task')
expect(tasks[1].title).to.eq('sub task')
expect(tasks[1].parent).to.eq('parent task')
expect(tasks[2].title).to.eq('sub sub task one')
expect(tasks[2].parent).to.eq('sub task')
expect(tasks[3].title).to.eq('sub sub task two')
expect(tasks[3].parent).to.eq('sub task')
})
it('Should work with multiple indention levels and multiple tasks', () => {
const tasks = parseSubtasksViaIndention(`parent task
sub task
sub sub task one
sub sub sub task
sub sub task two`)
expect(tasks).to.have.length(5)
expect(tasks[0].parent).toBeNull()
expect(tasks[0].title).to.eq('parent task')
expect(tasks[1].title).to.eq('sub task')
expect(tasks[1].parent).to.eq('parent task')
expect(tasks[2].title).to.eq('sub sub task one')
expect(tasks[2].parent).to.eq('sub task')
expect(tasks[3].title).to.eq('sub sub sub task')
expect(tasks[3].parent).to.eq('sub sub task one')
expect(tasks[4].title).to.eq('sub sub task two')
expect(tasks[4].parent).to.eq('sub task')
})
it('Should return a parent for multiple subtasks with special stuff', () => {
const tasks = parseSubtasksViaIndention(`* parent task
* sub task one
sub task two`)
expect(tasks).to.have.length(3)
expect(tasks[0].parent).toBeNull()
expect(tasks[0].title).to.eq('parent task')
expect(tasks[1].title).to.eq('sub task one')
expect(tasks[1].parent).to.eq('parent task')
expect(tasks[2].title).to.eq('sub task two')
expect(tasks[2].parent).to.eq('parent task')
})
it('Should not break when the first line is indented', () => {
const tasks = parseSubtasksViaIndention(' single task')
expect(tasks).to.have.length(1)
expect(tasks[0].parent).toBeNull()
})
})

View File

@ -0,0 +1,48 @@
export interface TaskWithParent {
title: string,
parent: string | null,
}
function cleanupTitle(title: string) {
return title.replace(/^((\* |\+ |- )(\[ \] )?)/g, '')
}
const spaceRegex = /^ */
/**
* @param taskTitles should be multiple lines of task tiles with indention to declare their parent/subtask
* relation between each other.
*/
export function parseSubtasksViaIndention(taskTitles: string): TaskWithParent[] {
const titles = taskTitles.split(/[\r\n]+/)
return titles.map((title, index) => {
const task: TaskWithParent = {
title: cleanupTitle(title),
parent: null,
}
if (index === 0) {
return task
}
const matched = spaceRegex.exec(title)
const matchedSpaces = matched ? matched[0].length : 0
if (matchedSpaces > 0) {
// Go up the tree to find the first task with less indention than the current one
let pi = 1
let parentSpaces = 0
do {
task.parent = cleanupTitle(titles[index - pi])
pi++
const parentMatched = spaceRegex.exec(task.parent)
parentSpaces = parentMatched ? parentMatched[0].length : 0
} while (parentSpaces >= matchedSpaces)
task.title = cleanupTitle(title.replace(spaceRegex, ''))
task.parent = task.parent.replace(spaceRegex, '')
}
return task
})
}