feat: improve user assignments via quick add magic (#3348)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/3348
This commit is contained in:
commit
d9f608e8b4
@ -48,7 +48,6 @@ const displayName = computed(() => getDisplayName(props.user))
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.user {
|
.user {
|
||||||
margin: .5rem;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-items: center;
|
justify-items: center;
|
||||||
|
|
||||||
|
@ -20,7 +20,8 @@
|
|||||||
:user="n.notification.doer"
|
:user="n.notification.doer"
|
||||||
:show-username="false"
|
:show-username="false"
|
||||||
:avatar-size="16"
|
:avatar-size="16"
|
||||||
v-if="n.notification.doer"/>
|
v-if="n.notification.doer"
|
||||||
|
/>
|
||||||
<div class="detail">
|
<div class="detail">
|
||||||
<div>
|
<div>
|
||||||
<span class="has-text-weight-bold mr-1" v-if="n.notification.doer">
|
<span class="has-text-weight-bold mr-1" v-if="n.notification.doer">
|
||||||
|
@ -12,12 +12,15 @@
|
|||||||
>
|
>
|
||||||
<template #tag="{item: user}">
|
<template #tag="{item: user}">
|
||||||
<span class="assignee">
|
<span class="assignee">
|
||||||
<user :avatar-size="32" :show-username="false" :user="user"/>
|
<user :avatar-size="32" :show-username="false" :user="user" class="m-2"/>
|
||||||
<BaseButton @click="removeAssignee(user)" class="remove-assignee" v-if="!disabled">
|
<BaseButton @click="removeAssignee(user)" class="remove-assignee" v-if="!disabled">
|
||||||
<icon icon="times"/>
|
<icon icon="times"/>
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
<template #searchResult="{option: user}">
|
||||||
|
<user :avatar-size="24" :show-username="true" :user="user"/>
|
||||||
|
</template>
|
||||||
</Multiselect>
|
</Multiselect>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -104,11 +107,6 @@ async function removeAssignee(user: IUser) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function findUser(query: string) {
|
async function findUser(query: string) {
|
||||||
if (query === '') {
|
|
||||||
foundUsers.value = []
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await projectUserService.getAll({projectId: props.projectId}, {s: query}) as IUser[]
|
const response = await projectUserService.getAll({projectId: props.projectId}, {s: query}) as IUser[]
|
||||||
|
|
||||||
// Filter the results to not include users who are already assigned
|
// Filter the results to not include users who are already assigned
|
||||||
|
@ -56,6 +56,7 @@
|
|||||||
:key="task.id + 'assignee' + a.id + i"
|
:key="task.id + 'assignee' + a.id + i"
|
||||||
:show-username="false"
|
:show-username="false"
|
||||||
:user="a"
|
:user="a"
|
||||||
|
class="m-2"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- FIXME: use popup -->
|
<!-- FIXME: use popup -->
|
||||||
|
@ -76,8 +76,8 @@
|
|||||||
"savedSuccess": "The settings were successfully updated.",
|
"savedSuccess": "The settings were successfully updated.",
|
||||||
"emailReminders": "Send me reminders for tasks via Email",
|
"emailReminders": "Send me reminders for tasks via Email",
|
||||||
"overdueReminders": "Send me a summary of my undone overdue tasks every day",
|
"overdueReminders": "Send me a summary of my undone overdue tasks every day",
|
||||||
"discoverableByName": "Let other users find me when they search for my name",
|
"discoverableByName": "Allow other users to add me as a member to teams or projects when they search for my name",
|
||||||
"discoverableByEmail": "Let other users find me when they search for my full email",
|
"discoverableByEmail": "Allow other users to add me as a member to teams or projects when they search for my full email",
|
||||||
"playSoundWhenDone": "Play a sound when marking tasks as done",
|
"playSoundWhenDone": "Play a sound when marking tasks as done",
|
||||||
"weekStart": "Week starts on",
|
"weekStart": "Week starts on",
|
||||||
"weekStartSunday": "Sunday",
|
"weekStartSunday": "Sunday",
|
||||||
|
@ -691,6 +691,14 @@ describe('Parse Task Text', () => {
|
|||||||
expect(result.assignees).toHaveLength(1)
|
expect(result.assignees).toHaveLength(1)
|
||||||
expect(result.assignees[0]).toBe('today')
|
expect(result.assignees[0]).toBe('today')
|
||||||
})
|
})
|
||||||
|
it('should recognize an email address', () => {
|
||||||
|
const text = 'Lorem Ipsum @email@example.com'
|
||||||
|
const result = parseTaskText(text)
|
||||||
|
|
||||||
|
expect(result.text).toBe('Lorem Ipsum @email@example.com')
|
||||||
|
expect(result.assignees).toHaveLength(1)
|
||||||
|
expect(result.assignees[0]).toBe('email@example.com')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Recurring Dates', () => {
|
describe('Recurring Dates', () => {
|
||||||
|
@ -109,7 +109,9 @@ const getItemsFromPrefix = (text: string, prefix: string): string[] => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
p = p.replace(prefix, '')
|
if (p.startsWith(prefix)) {
|
||||||
|
p = p.substring(1)
|
||||||
|
}
|
||||||
|
|
||||||
let itemText
|
let itemText
|
||||||
if (p.charAt(0) === '\'') {
|
if (p.charAt(0) === '\'') {
|
||||||
@ -120,8 +122,8 @@ const getItemsFromPrefix = (text: string, prefix: string): string[] => {
|
|||||||
// Only until the next space
|
// Only until the next space
|
||||||
itemText = p.split(' ')[0]
|
itemText = p.split(' ')[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
if(itemText !== '') {
|
if (itemText !== '') {
|
||||||
items.push(itemText)
|
items.push(itemText)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -278,13 +280,16 @@ const getRepeats = (text: string): repeatParsedResult => {
|
|||||||
|
|
||||||
export const cleanupItemText = (text: string, items: string[], prefix: string): string => {
|
export const cleanupItemText = (text: string, items: string[], prefix: string): string => {
|
||||||
items.forEach(l => {
|
items.forEach(l => {
|
||||||
|
if (l === '') {
|
||||||
|
return
|
||||||
|
}
|
||||||
text = text
|
text = text
|
||||||
.replace(`${prefix}'${l}' `, '')
|
.replace(new RegExp(`\\${prefix}'${l}' `, 'ig'), '')
|
||||||
.replace(`${prefix}'${l}'`, '')
|
.replace(new RegExp(`\\${prefix}'${l}'`, 'ig'), '')
|
||||||
.replace(`${prefix}"${l}" `, '')
|
.replace(new RegExp(`\\${prefix}"${l}" `, 'ig'), '')
|
||||||
.replace(`${prefix}"${l}"`, '')
|
.replace(new RegExp(`\\${prefix}"${l}"`, 'ig'), '')
|
||||||
.replace(`${prefix}${l} `, '')
|
.replace(new RegExp(`\\${prefix}${l} `, 'ig'), '')
|
||||||
.replace(`${prefix}${l}`, '')
|
.replace(new RegExp(`\\${prefix}${l}`, 'ig'), '')
|
||||||
})
|
})
|
||||||
return text
|
return text
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,6 @@ import router from '@/router'
|
|||||||
import TaskService from '@/services/task'
|
import TaskService from '@/services/task'
|
||||||
import TaskAssigneeService from '@/services/taskAssignee'
|
import TaskAssigneeService from '@/services/taskAssignee'
|
||||||
import LabelTaskService from '@/services/labelTask'
|
import LabelTaskService from '@/services/labelTask'
|
||||||
import UserService from '@/services/user'
|
|
||||||
|
|
||||||
import {playPop} from '@/helpers/playPop'
|
import {playPop} from '@/helpers/playPop'
|
||||||
import {getQuickAddMagicMode} from '@/helpers/quickAddMagicMode'
|
import {getQuickAddMagicMode} from '@/helpers/quickAddMagicMode'
|
||||||
@ -29,12 +28,21 @@ import {useProjectStore} from '@/stores/projects'
|
|||||||
import {useAttachmentStore} from '@/stores/attachments'
|
import {useAttachmentStore} from '@/stores/attachments'
|
||||||
import {useKanbanStore} from '@/stores/kanban'
|
import {useKanbanStore} from '@/stores/kanban'
|
||||||
import {useBaseStore} from '@/stores/base'
|
import {useBaseStore} from '@/stores/base'
|
||||||
|
import ProjectUserService from '@/services/projectUsers'
|
||||||
|
|
||||||
|
interface MatchedAssignee extends IUser {
|
||||||
|
match: string,
|
||||||
|
}
|
||||||
|
|
||||||
// IDEA: maybe use a small fuzzy search here to prevent errors
|
// IDEA: maybe use a small fuzzy search here to prevent errors
|
||||||
function findPropertyByValue(object, key, value) {
|
function findPropertyByValue(object, key, value, fuzzy = false) {
|
||||||
return Object.values(object).find(
|
return Object.values(object).find(l => {
|
||||||
(l) => l[key]?.toLowerCase() === value.toLowerCase(),
|
if (fuzzy) {
|
||||||
)
|
return l[key]?.toLowerCase().includes(value.toLowerCase())
|
||||||
|
}
|
||||||
|
|
||||||
|
return l[key]?.toLowerCase() === value.toLowerCase()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the user exists in the search results
|
// Check if the user exists in the search results
|
||||||
@ -42,9 +50,19 @@ function validateUser(
|
|||||||
users: IUser[],
|
users: IUser[],
|
||||||
query: IUser['username'] | IUser['name'] | IUser['email'],
|
query: IUser['username'] | IUser['name'] | IUser['email'],
|
||||||
) {
|
) {
|
||||||
return findPropertyByValue(users, 'username', query) ||
|
if (users.length === 1) {
|
||||||
|
return (
|
||||||
|
findPropertyByValue(users, 'username', query, true) ||
|
||||||
|
findPropertyByValue(users, 'name', query, true) ||
|
||||||
|
findPropertyByValue(users, 'email', query, true)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
findPropertyByValue(users, 'username', query) ||
|
||||||
findPropertyByValue(users, 'name', query) ||
|
findPropertyByValue(users, 'name', query) ||
|
||||||
findPropertyByValue(users, 'email', query)
|
findPropertyByValue(users, 'email', query)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the label exists
|
// Check if the label exists
|
||||||
@ -63,14 +81,18 @@ async function addLabelToTask(task: ITask, label: ILabel) {
|
|||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
async function findAssignees(parsedTaskAssignees: string[]): Promise<IUser[]> {
|
async function findAssignees(parsedTaskAssignees: string[], projectId: number): Promise<MatchedAssignee[]> {
|
||||||
if (parsedTaskAssignees.length <= 0) {
|
if (parsedTaskAssignees.length <= 0) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
const userService = new UserService()
|
const userService = new ProjectUserService()
|
||||||
const assignees = parsedTaskAssignees.map(async a => {
|
const assignees = parsedTaskAssignees.map(async a => {
|
||||||
const users = await userService.getAll({}, {s: a})
|
const users = (await userService.getAll({projectId}, {s: a}))
|
||||||
|
.map(u => ({
|
||||||
|
...u,
|
||||||
|
match: a,
|
||||||
|
}))
|
||||||
return validateUser(users, a)
|
return validateUser(users, a)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -388,15 +410,15 @@ export const useTaskStore = defineStore('task', () => {
|
|||||||
cancel()
|
cancel()
|
||||||
throw new Error('NO_PROJECT')
|
throw new Error('NO_PROJECT')
|
||||||
}
|
}
|
||||||
|
|
||||||
const assignees = await findAssignees(parsedTask.assignees)
|
const assignees = await findAssignees(parsedTask.assignees, foundProjectId)
|
||||||
|
|
||||||
// Only clean up those assignees from the task title which actually exist
|
// Only clean up those assignees from the task title which actually exist
|
||||||
let cleanedTitle = parsedTask.text
|
let cleanedTitle = parsedTask.text
|
||||||
if (assignees.length > 0) {
|
if (assignees.length > 0) {
|
||||||
const assigneePrefix = PREFIXES[quickAddMagicMode]?.assignee
|
const assigneePrefix = PREFIXES[quickAddMagicMode]?.assignee
|
||||||
if (assigneePrefix) {
|
if (assigneePrefix) {
|
||||||
cleanedTitle = cleanupItemText(cleanedTitle, assignees.map(a => a.username), assigneePrefix)
|
cleanedTitle = cleanupItemText(cleanedTitle, assignees.map(a => a.match), assigneePrefix)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user