1
0
Fork 0

Compare commits

...

18 Commits

Author SHA1 Message Date
kolaente 7e50b6e270
fix: lint 2023-05-31 16:22:02 +02:00
kolaente 4aad488707 chore: group return parameter 2023-05-31 14:18:58 +00:00
kolaente 61f0f5210a chore: make fuzzy matching a paramater 2023-05-31 14:18:58 +00:00
kolaente 8ea27a6655 fix: make type singular 2023-05-31 14:18:58 +00:00
kolaente 4993673d5c chore(i18n): clarify translation string 2023-05-31 14:18:58 +00:00
kolaente 1510807164 chore: use startsWith for prefix matching 2023-05-31 14:18:58 +00:00
kolaente a99eb820eb fix: clarify user search setting 2023-05-31 14:18:58 +00:00
kolaente ef65853278 chore: remove user margin from the component 2023-05-31 14:18:58 +00:00
kolaente 0bb6085fb1 chore: remove user margin from the component 2023-05-31 14:18:58 +00:00
kolaente ede1069577 feat(quick add magic): allow fuzzy matching of assignees when the api results are unambigous 2023-05-31 14:18:58 +00:00
kolaente 15a5aedcf9 fix: ensure all matched quick add magic parts are correctly removed from the task 2023-05-31 14:18:58 +00:00
kolaente 5f29624414 fix: lint 2023-05-31 14:18:58 +00:00
kolaente 502241a173 feat(assignees): show user avatar in search results 2023-05-31 14:18:58 +00:00
kolaente 7df5a0ba2e feat: show initial list of users when opening the assignees view 2023-05-31 14:18:58 +00:00
kolaente 1d05675cbd chore: clarify users when can still be found even if they disabled it 2023-05-31 14:18:58 +00:00
kolaente 0fcf949653 fix(quick add magic): cleanup all assignee properties 2023-05-31 14:18:58 +00:00
kolaente 22c10d935a fix(quick add magic): use the project user service to find assignees for quick add magic 2023-05-31 14:18:58 +00:00
kolaente 3d0753ebc8 fix(quick add magic): don't replace the prefix in every occurrence when it is present in the matched part 2023-05-31 14:18:58 +00:00
8 changed files with 66 additions and 32 deletions

View File

@ -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;

View File

@ -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">

View File

@ -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

View File

@ -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 -->

View File

@ -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",

View File

@ -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', () => {

View File

@ -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
} }

View File

@ -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)
} }
} }