feat(frontend): apply quick add magic when creating related tasks

Route the create flow through taskStore.createNewTask so titles typed
into the related-task input get parsed for labels, priority, assignees,
due dates and cross-project targets - matching the main add-task input.
Also surface the quick-add-magic hint next to the field.
This commit is contained in:
Tink bot
2026-05-11 12:20:47 +00:00
committed by kolaente
parent 572edd431d
commit f495a792b2
2 changed files with 111 additions and 2 deletions

View File

@@ -36,7 +36,7 @@
</label>
<div
key="field-search"
class="field"
class="field task-relation-search-field"
>
<Multiselect
v-model="newTaskRelation.task"
@@ -77,6 +77,7 @@
</span>
</template>
</Multiselect>
<QuickAddMagic class="task-relation-quick-add-magic" />
</div>
<div
key="field-kind"
@@ -200,6 +201,7 @@ import CustomTransition from '@/components/misc/CustomTransition.vue'
import BaseButton from '@/components/base/BaseButton.vue'
import Multiselect from '@/components/input/Multiselect.vue'
import FancyCheckbox from '@/components/input/FancyCheckbox.vue'
import QuickAddMagic from '@/components/tasks/partials/QuickAddMagic.vue'
import {error, success} from '@/message'
import {useTaskStore} from '@/stores/tasks'
@@ -362,7 +364,7 @@ async function removeTaskRelation() {
}
async function createAndRelateTask(title: string) {
const newTask = await taskService.create(new TaskModel({title, projectId: props.projectId}))
const newTask = await taskStore.createNewTask({title, projectId: props.projectId})
newTaskRelation.task = newTask
await addTaskRelation()
}
@@ -459,6 +461,17 @@ async function toggleTaskDone(task: ITask) {
padding: 0.5rem;
}
.task-relation-search-field {
position: relative;
}
.task-relation-quick-add-magic {
position: absolute;
inset-block-start: .5rem;
inset-inline-end: .75rem;
z-index: 4;
}
// FIXME: The height of the actual checkbox in the <FancyCheckbox/> component is too much resulting in a
// weired positioning of the checkbox. Setting the height here is a workaround until we fix the styling
// of the component.

View File

@@ -0,0 +1,96 @@
import {test, expect} from '../../support/fixtures'
import {ProjectFactory} from '../../factories/project'
import {TaskFactory} from '../../factories/task'
import {UserFactory} from '../../factories/user'
import {createDefaultViews} from '../project/prepareProjects'
import {login} from '../../support/authenticateUser'
async function openRelatedTasksForm(page) {
await page.locator('.task-view .action-buttons .button').filter({hasText: 'Add Relation'}).click()
const input = page.locator('.task-relations .multiselect input').first()
await expect(input).toBeVisible()
return input
}
test.describe('Related tasks quick add magic', () => {
test('Applies a label parsed via *prefix to the new related task', async ({authenticatedPage: page}) => {
const project = (await ProjectFactory.create(1, {id: 1, title: 'Project A'}))[0]
await createDefaultViews(project.id)
const parent = (await TaskFactory.create(1, {id: 1, title: 'Parent task', project_id: project.id}, false))[0]
await page.goto(`/tasks/${parent.id}`)
const input = await openRelatedTasksForm(page)
await input.fill('Subtask one *Urgent')
await input.press('Enter')
const relatedTaskLink = page.locator('.task-relations .related-tasks .task a').filter({hasText: 'Subtask one'})
await expect(relatedTaskLink).toBeVisible({timeout: 10000})
// Quick add magic strips the *Urgent prefix from the title
await expect(relatedTaskLink).not.toContainText('*Urgent')
await relatedTaskLink.click()
await expect(page).toHaveURL(/\/tasks\/\d+/)
await expect(page.locator('.task-view .details.labels-list .multiselect .input-wrapper span.tag').filter({hasText: 'Urgent'}))
.toBeVisible({timeout: 10000})
})
test('Applies a priority parsed via !prefix to the new related task', async ({authenticatedPage: page}) => {
const project = (await ProjectFactory.create(1, {id: 1, title: 'Project A'}))[0]
await createDefaultViews(project.id)
const parent = (await TaskFactory.create(1, {id: 1, title: 'Parent task', project_id: project.id}, false))[0]
await page.goto(`/tasks/${parent.id}`)
const input = await openRelatedTasksForm(page)
await input.fill('Important work !4')
await input.press('Enter')
const relatedTaskLink = page.locator('.task-relations .related-tasks .task a').filter({hasText: 'Important work'})
await expect(relatedTaskLink).toBeVisible({timeout: 10000})
await expect(relatedTaskLink).not.toContainText('!4')
await relatedTaskLink.click()
// Priority 4 is "Urgent"
await expect(page.locator('.task-view .columns.details select').first()).toHaveValue('4', {timeout: 10000})
})
test('Creates the related task in another project via +project prefix', async ({authenticatedPage: page}) => {
const projectA = (await ProjectFactory.create(1, {id: 1, title: 'Source'}))[0]
await createDefaultViews(projectA.id)
const projectB = (await ProjectFactory.create(1, {id: 2, title: 'TargetProject'}, false))[0]
await createDefaultViews(projectB.id, 5)
const parent = (await TaskFactory.create(1, {id: 1, title: 'Parent task', project_id: projectA.id}, false))[0]
await page.goto(`/tasks/${parent.id}`)
const input = await openRelatedTasksForm(page)
await input.fill('Cross task +TargetProject')
await input.press('Enter')
const relatedTaskRow = page.locator('.task-relations .related-tasks .task').filter({hasText: 'Cross task'})
await expect(relatedTaskRow).toBeVisible({timeout: 10000})
await expect(relatedTaskRow.locator('a')).not.toContainText('+TargetProject')
// Cross-project marker shows the other project name
await expect(relatedTaskRow.locator('.different-project')).toContainText('TargetProject')
})
test('Keeps the title literal when quick add magic is disabled', async ({page, apiContext}) => {
const user = (await UserFactory.create(1, {
frontend_settings: JSON.stringify({
quickAddMagicMode: 'disabled',
}),
}))[0]
const project = (await ProjectFactory.create(1, {id: 1, title: 'Project A', owner_id: user.id}))[0]
await createDefaultViews(project.id)
const parent = (await TaskFactory.create(1, {id: 1, title: 'Parent task', project_id: project.id, created_by_id: user.id}, false))[0]
await login(page, apiContext, user)
await page.goto(`/tasks/${parent.id}`)
const input = await openRelatedTasksForm(page)
await input.fill('Buy milk *Urgent')
await input.press('Enter')
// With magic disabled, the prefix stays in the title verbatim
await expect(page.locator('.task-relations .related-tasks .task a').filter({hasText: 'Buy milk *Urgent'}))
.toBeVisible({timeout: 10000})
})
})