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>
|
||||
.user {
|
||||
margin: .5rem;
|
||||
display: flex;
|
||||
justify-items: center;
|
||||
|
||||
|
@ -20,7 +20,8 @@
|
||||
:user="n.notification.doer"
|
||||
:show-username="false"
|
||||
:avatar-size="16"
|
||||
v-if="n.notification.doer"/>
|
||||
v-if="n.notification.doer"
|
||||
/>
|
||||
<div class="detail">
|
||||
<div>
|
||||
<span class="has-text-weight-bold mr-1" v-if="n.notification.doer">
|
||||
|
@ -12,12 +12,15 @@
|
||||
>
|
||||
<template #tag="{item: user}">
|
||||
<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">
|
||||
<icon icon="times"/>
|
||||
</BaseButton>
|
||||
</span>
|
||||
</template>
|
||||
<template #searchResult="{option: user}">
|
||||
<user :avatar-size="24" :show-username="true" :user="user"/>
|
||||
</template>
|
||||
</Multiselect>
|
||||
</template>
|
||||
|
||||
@ -104,11 +107,6 @@ async function removeAssignee(user: IUser) {
|
||||
}
|
||||
|
||||
async function findUser(query: string) {
|
||||
if (query === '') {
|
||||
foundUsers.value = []
|
||||
return
|
||||
}
|
||||
|
||||
const response = await projectUserService.getAll({projectId: props.projectId}, {s: query}) as IUser[]
|
||||
|
||||
// Filter the results to not include users who are already assigned
|
||||
|
@ -56,6 +56,7 @@
|
||||
:key="task.id + 'assignee' + a.id + i"
|
||||
:show-username="false"
|
||||
:user="a"
|
||||
class="m-2"
|
||||
/>
|
||||
|
||||
<!-- FIXME: use popup -->
|
||||
|
@ -76,8 +76,8 @@
|
||||
"savedSuccess": "The settings were successfully updated.",
|
||||
"emailReminders": "Send me reminders for tasks via Email",
|
||||
"overdueReminders": "Send me a summary of my undone overdue tasks every day",
|
||||
"discoverableByName": "Let other users find me when they search for my name",
|
||||
"discoverableByEmail": "Let other users find me when they search for my full email",
|
||||
"discoverableByName": "Allow other users to add me as a member to teams or projects when they search for my name",
|
||||
"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",
|
||||
"weekStart": "Week starts on",
|
||||
"weekStartSunday": "Sunday",
|
||||
|
@ -691,6 +691,14 @@ describe('Parse Task Text', () => {
|
||||
expect(result.assignees).toHaveLength(1)
|
||||
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', () => {
|
||||
|
@ -109,7 +109,9 @@ const getItemsFromPrefix = (text: string, prefix: string): string[] => {
|
||||
return
|
||||
}
|
||||
|
||||
p = p.replace(prefix, '')
|
||||
if (p.startsWith(prefix)) {
|
||||
p = p.substring(1)
|
||||
}
|
||||
|
||||
let itemText
|
||||
if (p.charAt(0) === '\'') {
|
||||
@ -121,7 +123,7 @@ const getItemsFromPrefix = (text: string, prefix: string): string[] => {
|
||||
itemText = p.split(' ')[0]
|
||||
}
|
||||
|
||||
if(itemText !== '') {
|
||||
if (itemText !== '') {
|
||||
items.push(itemText)
|
||||
}
|
||||
})
|
||||
@ -278,13 +280,16 @@ const getRepeats = (text: string): repeatParsedResult => {
|
||||
|
||||
export const cleanupItemText = (text: string, items: string[], prefix: string): string => {
|
||||
items.forEach(l => {
|
||||
if (l === '') {
|
||||
return
|
||||
}
|
||||
text = text
|
||||
.replace(`${prefix}'${l}' `, '')
|
||||
.replace(`${prefix}'${l}'`, '')
|
||||
.replace(`${prefix}"${l}" `, '')
|
||||
.replace(`${prefix}"${l}"`, '')
|
||||
.replace(`${prefix}${l} `, '')
|
||||
.replace(`${prefix}${l}`, '')
|
||||
.replace(new RegExp(`\\${prefix}'${l}' `, 'ig'), '')
|
||||
.replace(new RegExp(`\\${prefix}'${l}'`, 'ig'), '')
|
||||
.replace(new RegExp(`\\${prefix}"${l}" `, 'ig'), '')
|
||||
.replace(new RegExp(`\\${prefix}"${l}"`, 'ig'), '')
|
||||
.replace(new RegExp(`\\${prefix}${l} `, 'ig'), '')
|
||||
.replace(new RegExp(`\\${prefix}${l}`, 'ig'), '')
|
||||
})
|
||||
return text
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ import router from '@/router'
|
||||
import TaskService from '@/services/task'
|
||||
import TaskAssigneeService from '@/services/taskAssignee'
|
||||
import LabelTaskService from '@/services/labelTask'
|
||||
import UserService from '@/services/user'
|
||||
|
||||
import {playPop} from '@/helpers/playPop'
|
||||
import {getQuickAddMagicMode} from '@/helpers/quickAddMagicMode'
|
||||
@ -29,12 +28,21 @@ import {useProjectStore} from '@/stores/projects'
|
||||
import {useAttachmentStore} from '@/stores/attachments'
|
||||
import {useKanbanStore} from '@/stores/kanban'
|
||||
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
|
||||
function findPropertyByValue(object, key, value) {
|
||||
return Object.values(object).find(
|
||||
(l) => l[key]?.toLowerCase() === value.toLowerCase(),
|
||||
)
|
||||
function findPropertyByValue(object, key, value, fuzzy = false) {
|
||||
return Object.values(object).find(l => {
|
||||
if (fuzzy) {
|
||||
return l[key]?.toLowerCase().includes(value.toLowerCase())
|
||||
}
|
||||
|
||||
return l[key]?.toLowerCase() === value.toLowerCase()
|
||||
})
|
||||
}
|
||||
|
||||
// Check if the user exists in the search results
|
||||
@ -42,9 +50,19 @@ function validateUser(
|
||||
users: IUser[],
|
||||
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, 'email', query)
|
||||
)
|
||||
}
|
||||
|
||||
// Check if the label exists
|
||||
@ -63,14 +81,18 @@ async function addLabelToTask(task: ITask, label: ILabel) {
|
||||
return response
|
||||
}
|
||||
|
||||
async function findAssignees(parsedTaskAssignees: string[]): Promise<IUser[]> {
|
||||
async function findAssignees(parsedTaskAssignees: string[], projectId: number): Promise<MatchedAssignee[]> {
|
||||
if (parsedTaskAssignees.length <= 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
const userService = new UserService()
|
||||
const userService = new ProjectUserService()
|
||||
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)
|
||||
})
|
||||
|
||||
@ -389,14 +411,14 @@ export const useTaskStore = defineStore('task', () => {
|
||||
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
|
||||
let cleanedTitle = parsedTask.text
|
||||
if (assignees.length > 0) {
|
||||
const assigneePrefix = PREFIXES[quickAddMagicMode]?.assignee
|
||||
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