mirror of
https://github.com/go-vikunja/vikunja.git
synced 2026-07-22 12:06:11 -05:00
fix(quick-add): don't abort task creation when a label can't be created (#3232)
This commit is contained in:
@@ -72,6 +72,7 @@ import {parseSubtasksViaIndention} from '@/helpers/parseSubtasksViaIndention'
|
||||
import TaskRelationService from '@/services/taskRelation'
|
||||
import TaskRelationModel from '@/models/taskRelation'
|
||||
import {getLabelsFromPrefix} from '@/modules/quickAddMagic'
|
||||
import {error} from '@/message'
|
||||
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import {useTaskStore} from '@/stores/tasks'
|
||||
@@ -140,7 +141,15 @@ async function addTask() {
|
||||
// In the store it will only ever see one task at a time so there's no way to reliably
|
||||
// check if a new label was created before (because everything happens async).
|
||||
const allLabels = tasksToCreate.map(({title}) => getLabelsFromPrefix(title, authStore.settings.frontendSettings.quickAddMagicMode) ?? [])
|
||||
await taskStore.ensureLabelsExist(allLabels.flat())
|
||||
const requestedLabels = [...new Set(allLabels.flat())]
|
||||
const resolvedLabels = await taskStore.ensureLabelsExist(requestedLabels)
|
||||
|
||||
// Skipped labels (e.g. link shares may not create them) don't block task creation; just tell the user.
|
||||
const resolvedTitles = new Set(resolvedLabels.map(l => l.title.toLowerCase()))
|
||||
const failedLabels = requestedLabels.filter(title => !resolvedTitles.has(title.toLowerCase()))
|
||||
if (failedLabels.length > 0) {
|
||||
error({message: t('task.label.createFailed', {labels: failedLabels.join(', ')})})
|
||||
}
|
||||
|
||||
const taskCollectionService = new TaskService()
|
||||
const projectIndices = new Map<number, number>()
|
||||
|
||||
@@ -1121,6 +1121,7 @@
|
||||
"addSuccess": "The label has been added successfully.",
|
||||
"removeSuccess": "The label has been removed successfully.",
|
||||
"addCreateSuccess": "The label has been created and added successfully.",
|
||||
"createFailed": "These labels could not be created and were not added: {labels}",
|
||||
"delete": {
|
||||
"header": "Delete this label",
|
||||
"text1": "Are you sure you want to delete this label?",
|
||||
|
||||
@@ -1,6 +1,27 @@
|
||||
import {describe, expect, it} from 'vitest'
|
||||
import {buildDefaultRemindersForQuickAdd, runWrites} from './tasks'
|
||||
import {setActivePinia, createPinia} from 'pinia'
|
||||
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
||||
|
||||
vi.mock('@/router', () => ({
|
||||
default: {
|
||||
currentRoute: {value: {params: {}}},
|
||||
isReady: () => Promise.resolve(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({t: (key: string) => key}),
|
||||
createI18n: () => ({global: {t: (key: string) => key}}),
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/base', () => ({
|
||||
useBaseStore: () => ({setHasTasks: vi.fn()}),
|
||||
}))
|
||||
|
||||
import {buildDefaultRemindersForQuickAdd, runWrites, useTaskStore} from './tasks'
|
||||
import {useLabelStore} from './labels'
|
||||
import LabelModel from '@/models/label'
|
||||
import {REMINDER_PERIOD_RELATIVE_TO_TYPES} from '@/types/IReminderPeriodRelativeTo'
|
||||
import type {ILabel} from '@/modelTypes/ILabel'
|
||||
import type {ITaskReminder} from '@/modelTypes/ITaskReminder'
|
||||
|
||||
const aDefault: ITaskReminder = {
|
||||
@@ -78,3 +99,30 @@ describe('runWrites', () => {
|
||||
expect(completed).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('ensureLabelsExist', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
})
|
||||
|
||||
it('skips labels that fail to create and returns the resolved ones', async () => {
|
||||
const taskStore = useTaskStore()
|
||||
const labelStore = useLabelStore()
|
||||
labelStore.setLabels([{id: 1, title: 'existing'}] as ILabel[])
|
||||
|
||||
vi.spyOn(labelStore, 'createLabel').mockImplementation(async label => {
|
||||
if (label.title === 'forbidden') {
|
||||
throw new Error('403')
|
||||
}
|
||||
return new LabelModel({id: 99, title: label.title})
|
||||
})
|
||||
|
||||
const result = await taskStore.ensureLabelsExist(['existing', 'created', 'forbidden'])
|
||||
const titles = result.map(l => l.title)
|
||||
|
||||
expect(titles).toContain('existing')
|
||||
expect(titles).toContain('created')
|
||||
expect(titles).not.toContain('forbidden')
|
||||
expect(result).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -391,16 +391,22 @@ export const useTaskStore = defineStore('task', () => {
|
||||
const mustCreateLabel = all.map(async labelTitle => {
|
||||
let label = validateLabel(Object.values(labelStore.labels), labelTitle)
|
||||
if (typeof label === 'undefined') {
|
||||
// label not found, create it
|
||||
const labelModel = new LabelModel({
|
||||
title: labelTitle,
|
||||
hexColor: getRandomColorHex(),
|
||||
})
|
||||
label = await labelStore.createLabel(labelModel)
|
||||
try {
|
||||
label = await labelStore.createLabel(labelModel)
|
||||
} catch (e) {
|
||||
// Link shares may not create labels; skip it instead of aborting task creation.
|
||||
console.debug('Could not create label from quick add magic', {labelTitle, e})
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
return label
|
||||
})
|
||||
return Promise.all(mustCreateLabel)
|
||||
const resolved = await Promise.all(mustCreateLabel)
|
||||
return resolved.filter((label): label is LabelModel => typeof label !== 'undefined')
|
||||
}
|
||||
|
||||
// Do everything that is involved in finding, creating and adding the label to the task
|
||||
|
||||
@@ -223,3 +223,32 @@ test.describe('Link share: permission tiers', () => {
|
||||
await expect(page.locator('.input[placeholder="Add a task…"]')).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Link share: quick add magic labels', () => {
|
||||
test.beforeEach(async ({page}) => {
|
||||
await setupApiUrl(page)
|
||||
})
|
||||
|
||||
test('creates the task and shows an error when a label cannot be created', async ({page}) => {
|
||||
await UserFactory.create(1)
|
||||
const projects = await createProjects()
|
||||
await TaskFactory.create(1, {
|
||||
project_id: projects[0].id,
|
||||
})
|
||||
const [share] = await LinkShareFactory.create(1, {
|
||||
project_id: projects[0].id,
|
||||
permission: 1,
|
||||
})
|
||||
|
||||
await page.goto(`/share/${share.hash}/auth`)
|
||||
await expect(page.locator('h1.title')).toContainText(projects[0].title)
|
||||
|
||||
const addTaskInput = page.locator('.input[placeholder="Add a task…"]')
|
||||
await addTaskInput.fill('New task via share *unknownlabel')
|
||||
await addTaskInput.press('Enter')
|
||||
|
||||
// Link shares may not create labels: the label is skipped with an error, the task is still created.
|
||||
await expect(page.locator('.global-notification')).toContainText('could not be created')
|
||||
await expect(page.locator('.tasks')).toContainText('New task via share')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user