feat(filter): autocomplete for assignees
This commit is contained in:
parent
8fa2f6686a
commit
b1d9dc6fc3
@ -8,28 +8,33 @@ import {createRandomID} from '@/helpers/randomId'
|
|||||||
import AutocompleteDropdown from '@/components/input/AutocompleteDropdown.vue'
|
import AutocompleteDropdown from '@/components/input/AutocompleteDropdown.vue'
|
||||||
import {useLabelStore} from '@/stores/labels'
|
import {useLabelStore} from '@/stores/labels'
|
||||||
import XLabel from '@/components/tasks/partials/label.vue'
|
import XLabel from '@/components/tasks/partials/label.vue'
|
||||||
|
import User from '@/components/misc/user.vue'
|
||||||
|
import ProjectUserService from '@/services/projectUsers'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
modelValue,
|
projectId,
|
||||||
} = defineProps<{
|
} = defineProps<{
|
||||||
modelValue: string,
|
projectId?: number,
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const filterQuery = ref('')
|
const model = defineModel<string>()
|
||||||
|
|
||||||
|
const filterQuery = ref<string>('')
|
||||||
const {
|
const {
|
||||||
textarea: filterInput,
|
textarea: filterInput,
|
||||||
height,
|
height,
|
||||||
} = useAutoHeightTextarea(filterQuery)
|
} = useAutoHeightTextarea(filterQuery)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => modelValue,
|
() => model.value,
|
||||||
() => {
|
() => {
|
||||||
filterQuery.value = modelValue
|
filterQuery.value = model.value
|
||||||
},
|
},
|
||||||
{immediate: true},
|
{immediate: true},
|
||||||
)
|
)
|
||||||
|
|
||||||
const userService = new UserService()
|
const userService = new UserService()
|
||||||
|
const projectUserService = new ProjectUserService()
|
||||||
|
|
||||||
const dateFields = [
|
const dateFields = [
|
||||||
'dueDate',
|
'dueDate',
|
||||||
@ -47,6 +52,11 @@ const labelFields = [
|
|||||||
'labels',
|
'labels',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const autocompleteFields = [
|
||||||
|
...labelFields,
|
||||||
|
...assigneeFields,
|
||||||
|
]
|
||||||
|
|
||||||
const availableFilterFields = [
|
const availableFilterFields = [
|
||||||
'done',
|
'done',
|
||||||
'priority',
|
'priority',
|
||||||
@ -210,15 +220,27 @@ function handleFieldInput(e, autocompleteOnInput) {
|
|||||||
const cursorPosition = filterInput.value.selectionStart
|
const cursorPosition = filterInput.value.selectionStart
|
||||||
const textUpToCursor = filterQuery.value.substring(0, cursorPosition)
|
const textUpToCursor = filterQuery.value.substring(0, cursorPosition)
|
||||||
|
|
||||||
labelFields.forEach(l => {
|
autocompleteFields.forEach(field => {
|
||||||
const pattern = new RegExp('(' + l + '\\s*' + FILTER_OPERATORS_REGEX + '\\s*)([\'"]?)([^\'"&\|\(\)]+\\1?)?$', 'ig')
|
const pattern = new RegExp('(' + field + '\\s*' + FILTER_OPERATORS_REGEX + '\\s*)([\'"]?)([^\'"&\|\(\)]+\\1?)?$', 'ig')
|
||||||
const match = pattern.exec(textUpToCursor)
|
const match = pattern.exec(textUpToCursor)
|
||||||
|
|
||||||
if (match !== null) {
|
if (match !== null) {
|
||||||
const [matched, prefix, operator, space, keyword] = match
|
const [matched, prefix, operator, space, keyword] = match
|
||||||
if (keyword) {
|
if (keyword) {
|
||||||
|
if (matched.startsWith('label')) {
|
||||||
autocompleteResultType.value = 'labels'
|
autocompleteResultType.value = 'labels'
|
||||||
autocompleteResults.value = labelStore.filterLabelsByQuery([], keyword)
|
autocompleteResults.value = labelStore.filterLabelsByQuery([], keyword)
|
||||||
|
}
|
||||||
|
if (matched.startsWith('assignee')) {
|
||||||
|
autocompleteResultType.value = 'assignees'
|
||||||
|
if (projectId) {
|
||||||
|
projectUserService.getAll({projectId}, {s: keyword})
|
||||||
|
.then(users => autocompleteResults.value = users.length > 1 ? users : [])
|
||||||
|
} else {
|
||||||
|
userService.getAll({}, {s: keyword})
|
||||||
|
.then(users => autocompleteResults.value = users.length > 1 ? users : [])
|
||||||
|
}
|
||||||
|
}
|
||||||
autocompleteMatchText.value = keyword
|
autocompleteMatchText.value = keyword
|
||||||
autocompleteMatchPosition.value = prefix.length - 1
|
autocompleteMatchPosition.value = prefix.length - 1
|
||||||
}
|
}
|
||||||
@ -228,7 +250,9 @@ function handleFieldInput(e, autocompleteOnInput) {
|
|||||||
|
|
||||||
function autocompleteSelect(value) {
|
function autocompleteSelect(value) {
|
||||||
filterQuery.value = filterQuery.value.substring(0, autocompleteMatchPosition.value + 1) +
|
filterQuery.value = filterQuery.value.substring(0, autocompleteMatchPosition.value + 1) +
|
||||||
value.title +
|
(autocompleteResultType.value === 'labels'
|
||||||
|
? value.title
|
||||||
|
: value.username) +
|
||||||
filterQuery.value.substring(autocompleteMatchPosition.value + autocompleteMatchText.value.length + 1)
|
filterQuery.value.substring(autocompleteMatchPosition.value + autocompleteMatchText.value.length + 1)
|
||||||
|
|
||||||
autocompleteResults.value = []
|
autocompleteResults.value = []
|
||||||
@ -281,6 +305,11 @@ function autocompleteSelect(value) {
|
|||||||
v-if="autocompleteResultType === 'labels'"
|
v-if="autocompleteResultType === 'labels'"
|
||||||
:label="item"
|
:label="item"
|
||||||
/>
|
/>
|
||||||
|
<User
|
||||||
|
v-else-if="autocompleteResultType === 'assignees'"
|
||||||
|
:user="item"
|
||||||
|
:avatar-size="25"
|
||||||
|
/>
|
||||||
<template v-else> {{ item }}</template>
|
<template v-else> {{ item }}</template>
|
||||||
</template>
|
</template>
|
||||||
</AutocompleteDropdown>
|
</AutocompleteDropdown>
|
||||||
|
@ -31,7 +31,10 @@
|
|||||||
</Fancycheckbox>
|
</Fancycheckbox>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FilterInput v-model="filterQuery"/>
|
<FilterInput
|
||||||
|
v-model="filterQuery"
|
||||||
|
:project-id="projectId"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">{{ $t('misc.search') }}</label>
|
<label class="label">{{ $t('misc.search') }}</label>
|
||||||
@ -231,6 +234,7 @@ import ProjectService from '@/services/project'
|
|||||||
// FIXME: do not use this here for now. instead create new version from DEFAULT_PARAMS
|
// FIXME: do not use this here for now. instead create new version from DEFAULT_PARAMS
|
||||||
import {getDefaultParams} from '@/composables/useTaskList'
|
import {getDefaultParams} from '@/composables/useTaskList'
|
||||||
import FilterInput from '@/components/project/partials/FilterInput.vue'
|
import FilterInput from '@/components/project/partials/FilterInput.vue'
|
||||||
|
import {useRoute} from 'vue-router'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
@ -244,6 +248,15 @@ const props = defineProps({
|
|||||||
|
|
||||||
const emit = defineEmits(['update:modelValue'])
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const projectId = computed(() => {
|
||||||
|
if (route.name?.startsWith('project.')) {
|
||||||
|
return Number(route.params.projectId)
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
})
|
||||||
|
|
||||||
// FIXME: merge with DEFAULT_PARAMS in taskProject.js
|
// FIXME: merge with DEFAULT_PARAMS in taskProject.js
|
||||||
const DEFAULT_PARAMS = {
|
const DEFAULT_PARAMS = {
|
||||||
sort_by: [],
|
sort_by: [],
|
||||||
|
Loading…
x
Reference in New Issue
Block a user