feat: search in quick actions #943

Merged
konrad merged 10 commits from feature/search-in-quick-actions into main 2021-11-13 20:26:03 +00:00
3 changed files with 81 additions and 45 deletions

View File

@ -62,7 +62,10 @@ import TeamModel from '@/models/team'
import {CURRENT_LIST, LOADING, LOADING_MODULE, QUICK_ACTIONS_ACTIVE} from '@/store/mutation-types'
import ListModel from '@/models/list'
import QuickAddMagic from '@/components/tasks/partials/quick-add-magic.vue'
import {getHistory} from '../../modules/listHistory'
import {getHistory} from '@/modules/listHistory'
import {parseTaskText, PrefixMode} from '@/modules/parseTaskText'
import {getQuickAddMagicMode} from '@/helpers/quickAddMagicMode'
import {PREFIXES} from '@/modules/parseTaskText'
const TYPE_LIST = 'list'
const TYPE_TASK = 'task'
@ -107,11 +110,6 @@ export default {
results() {
let lists = []
if (this.searchMode === SEARCH_MODE_ALL || this.searchMode === SEARCH_MODE_LISTS) {
let query = this.query
if (this.searchMode === SEARCH_MODE_LISTS) {
query = query.substr(1)
}
const ncache = {}
const history = getHistory()
@ -122,25 +120,31 @@ export default {
}),
...Object.values(this.$store.state.lists)])]
lists = (allLists.filter(l => {
if (typeof l === 'undefined' || l === null) {
return false
}
const {list} = this.parsedQuery
if (l.isArchived) {
return false
}
if (list === null) {
konrad marked this conversation as resolved Outdated

There is too much happening in this tenary which makes it hard to read.

There is too much happening in this tenary which makes it hard to read.

I've moved one out, hope it is clearer now.

I've moved one out, hope it is clearer now.
lists = []
} else {
lists = allLists.filter(l => {
if (typeof l === 'undefined' || l === null) {
return false
}
if (typeof ncache[l.namespaceId] === 'undefined') {
ncache[l.namespaceId] = this.$store.getters['namespaces/getNamespaceById'](l.namespaceId)
}
if (l.isArchived) {
return false
}
if (ncache[l.namespaceId].isArchived) {
return false
}
if (typeof ncache[l.namespaceId] === 'undefined') {
ncache[l.namespaceId] = this.$store.getters['namespaces/getNamespaceById'](l.namespaceId)
}
return l.title.toLowerCase().includes(query.toLowerCase())
}) ?? [])
if (ncache[l.namespaceId].isArchived) {
return false
}
return l.title.toLowerCase().includes(list.toLowerCase())
}) ?? []
}
}
const cmds = this.availableCmds
@ -207,7 +211,9 @@ export default {
}
}
return this.$t('quickActions.hint')
const prefixes = PREFIXES[getQuickAddMagicMode()] ?? PREFIXES[PrefixMode.Default]
return this.$t('quickActions.hint', prefixes)
},
currentList() {
return Object.keys(this.$store.state[CURRENT_LIST]).length === 0 ? null : this.$store.state[CURRENT_LIST]
@ -236,18 +242,23 @@ export default {
return cmds
},
parsedQuery() {
return parseTaskText(this.query, getQuickAddMagicMode())
},
searchMode() {
if (this.query === '') {
return SEARCH_MODE_ALL
}
if (this.query.startsWith('#')) {
const {text, list, labels, assignees} = this.parsedQuery
if (assignees.length === 0 && text !== '') {
return SEARCH_MODE_TASKS
}
if (this.query.startsWith('*')) {
if (assignees.length === 0 && list !== null && text === '' && labels.length === 0) {
return SEARCH_MODE_LISTS
}
if (this.query.startsWith('@')) {
if (assignees.length > 0 && list === null && text === '' && labels.length === 0) {
return SEARCH_MODE_TEAMS
}
@ -268,12 +279,7 @@ export default {
return
}
let query = this.query
if (this.searchMode === SEARCH_MODE_TASKS) {
query = query.substr(1)
}
if (query === '' || this.selectedCmd !== null) {
if (this.selectedCmd !== null) {
return
}
@ -282,8 +288,35 @@ export default {
this.taskSearchTimeout = null
}
const {text, list, labels} = this.parsedQuery
const params = {
s: text,
filter_by: [],
filter_value: [],
filter_comparator: [],
}
if (list !== null) {
const l = this.$store.getters['lists/findListByExactname'](list)
if (l !== null) {
params.filter_by.push('list_id')
params.filter_value.push(l.id)
params.filter_comparator.push('equals')
}
}
if (labels.length > 0) {
const labelIds = this.$store.getters['labels/getLabelsByExactTitles'](labels).map(l => l.id)
if (labelIds.length > 0) {
params.filter_by.push('labels')
params.filter_value.push(labelIds.join())
params.filter_comparator.push('in')
dpschen marked this conversation as resolved Outdated

Having this timeout deep hidden inside the searchTasks makes it hard to follow.
We should use a common pattern debounce for these cases.

Having this timeout deep hidden inside the searchTasks makes it hard to follow. We should use a common pattern debounce for these cases.
}
}
this.taskSearchTimeout = setTimeout(async () => {
const r = await this.taskService.getAll({}, {s: query})
const r = await this.taskService.getAll({}, params)
this.foundTasks = r.map(t => {
t.type = TYPE_TASK
const list = this.$store.getters['lists/getListById'](t.listId)
@ -301,12 +334,7 @@ export default {
return
}
let query = this.query
if (this.searchMode === SEARCH_MODE_TEAMS) {
query = query.substr(1)
}
if (query === '' || this.selectedCmd !== null) {
if (this.query === '' || this.selectedCmd !== null) {
return
}
@ -315,11 +343,14 @@ export default {
this.teamSearchTimeout = null
}
konrad marked this conversation as resolved Outdated

Not happy with this deep nesting here (hard to follow what happens).
Tried to untangle it a bit with async, but still not happy (untested):

const {assignees} = this.parsedQuery

this.teamSearchTimeout = setTimeout(async () => {
    // search team for each assignee
    const teamServicePromises = assignees.map((t) => this.teamService.getAll({}, {s: t}))
    const teamServiceResult = await Promise.all(teamServicePromises)
    this.foundTeams = teamServiceResult.flatMap(team => {
        team.title = team.name
        return team
    })
}, 150)

While working on this I always had to think of proposal pipeline operator...
Then I found https://www.freecodecamp.org/news/pipe-and-compose-in-javascript-5b04004ac937/ 🤔 😆

Not happy with this deep nesting here (hard to follow what happens). Tried to untangle it a bit with async, but still not happy (untested): ```js const {assignees} = this.parsedQuery this.teamSearchTimeout = setTimeout(async () => { // search team for each assignee const teamServicePromises = assignees.map((t) => this.teamService.getAll({}, {s: t})) const teamServiceResult = await Promise.all(teamServicePromises) this.foundTeams = teamServiceResult.flatMap(team => { team.title = team.name return team }) }, 150) ``` While working on this I always had to think of [proposal pipeline operator](https://github.com/tc39/proposal-pipeline-operator)... Then I found https://www.freecodecamp.org/news/pipe-and-compose-in-javascript-5b04004ac937/ 🤔 😆

Maybe we could use something like this: https://vueuse.org/shared/useDebounceFn/#usage

Maybe we could use something like this: https://vueuse.org/shared/useDebounceFn/#usage

That component is a prime candidate for using the composition api.
Let's do this later in another issue =)

For now: what do you think about the above as a temporary solution?

That component is a prime candidate for using the composition api. Let's do this later in another issue =) For now: what do you think about the above as a temporary solution?

For now: what do you think about the above as a temporary solution?

I think that makes sense, refactored it.

> For now: what do you think about the above as a temporary solution? I think that makes sense, refactored it.
const {assignees} = this.parsedQuery
this.teamSearchTimeout = setTimeout(async () => {
const r = await this.teamService.getAll({}, {s: query})
this.foundTeams = r.map(t => {
t.title = t.name
return t
const teamSearchPromises = assignees.map((t) => this.teamService.getAll({}, {s: t}))
const teamsResult = await Promise.all(teamSearchPromises)
this.foundTeams = teamsResult.flatMap(team => {
team.title = team.name
return team
})
}, 150)
},
@ -348,7 +379,7 @@ export default {
this.doAction(this.results[0].type, this.results[0].items[0])
return
}
if (this.selectedCmd === null) {
return
}

View File

@ -806,7 +806,7 @@
"quickActions": {
"commands": "Commands",
"placeholder": "Type a command or search…",
"hint": "You can use # to only search for tasks, * to only search for lists and @ to only search for teams.",
"hint": "You can use {list} to limit the search to a list. Combine {list} or {label} (labels) with a search query to search for a task with these labels or on that list. Use {assignee} to only search for teams.",
"tasks": "Tasks",
"lists": "Lists",
"teams": "Teams",

View File

@ -1,6 +1,6 @@
import LabelService from '@/services/label'
import {setLoading} from '@/store/helper'
import { success } from '@/message'
import {success} from '@/message'
import {i18n} from '@/i18n'
import {getLabelsByIds, filterLabelsByQuery} from '@/helpers/labels'
@ -45,6 +45,11 @@ export default {
filterLabelsByQuery(state) {
return (labelsToHide, query) => filterLabelsByQuery(state, labelsToHide, query)
},
getLabelsByExactTitles(state) {
return labelTitles => Object
.values(state.labels)
.filter(({title}) => labelTitles.some(l => l.toLowerCase() === title.toLowerCase()))
konrad marked this conversation as resolved Outdated
.filter(({title}) => labelTitles.some(l => l.toLowerCase() === title.toLowerCase())
``` .filter(({title}) => labelTitles.some(l => l.toLowerCase() === title.toLowerCase()) ```

Done.

Done.
},
},
actions: {
async loadAllLabels(ctx, {forceLoad} = {}) {