fix: list filter logic

This commit is contained in:
Shariq Ansari 2023-08-11 02:57:29 +05:30
parent a60301f786
commit cd88c0d0e4
4 changed files with 178 additions and 42 deletions

View File

@ -3,11 +3,11 @@
<template #target> <template #target>
<Button label="Filter"> <Button label="Filter">
<template #prefix><FilterIcon class="h-4" /></template> <template #prefix><FilterIcon class="h-4" /></template>
<template v-if="filterValues.length" #suffix> <template v-if="storage.size" #suffix>
<div <div
class="flex justify-center items-center w-5 h-5 text-2xs font-medium pt-[1px] bg-gray-900 text-white rounded" class="flex justify-center items-center w-5 h-5 text-2xs font-medium pt-[1px] bg-gray-900 text-white rounded"
> >
{{ filterValues.length }} {{ storage.size }}
</div> </div>
</template> </template>
</Button> </Button>
@ -16,8 +16,9 @@
<div class="rounded-lg border border-gray-100 bg-white shadow-xl my-2"> <div class="rounded-lg border border-gray-100 bg-white shadow-xl my-2">
<div class="p-2 min-w-[400px]"> <div class="p-2 min-w-[400px]">
<div <div
v-if="filterValues.length" v-if="storage.size"
v-for="(filter, i) in filterValues" v-for="(f, i) in storage"
:key="f.field.fieldname"
id="filter-list" id="filter-list"
class="flex flex-col gap-2 mb-3" class="flex flex-col gap-2 mb-3"
> >
@ -27,20 +28,29 @@
</div> </div>
<Autocomplete <Autocomplete
class="!min-w-[140px]" class="!min-w-[140px]"
:value="filter.field" :value="f.field.fieldname"
:options="filterableFields.data" :options="filterableFields.data"
@change="(e) => updateFilter(e, i)" @change="(e) => updateFilter(e, i)"
placeholder="Filter by..." placeholder="Filter by..."
/> />
<FormControl <FormControl
type="select" type="select"
v-model="filter.operator" v-model="f.operator"
:options="getOperators(filter.fieldtype)" :options="getOperators(f.field.fieldtype)"
placeholder="Operator" placeholder="Operator"
/> />
<SearchComplete
v-if="typeLink.includes(f.field.fieldtype)"
:doctype="f.field.options"
:value="f.value"
@change="(v) => (f.value = v.value)"
placeholder="Value"
class="!min-w-[140px]"
/>
<component <component
:is="getValSelect(filter.fieldtype, filter.options)" v-else
v-model="filter.value" :is="getValSelect(f.field.fieldtype, f.field.options)"
v-model="f.value"
placeholder="Value" placeholder="Value"
class="!min-w-[140px]" class="!min-w-[140px]"
/> />
@ -55,10 +65,10 @@
</div> </div>
<div class="flex items-center justify-between gap-2"> <div class="flex items-center justify-between gap-2">
<Autocomplete <Autocomplete
:options="filterableFields.data"
value="" value=""
placeholder="filter by" :options="filterableFields.data"
@change="(e) => setfilter(e)" @change="(e) => setfilter(e)"
placeholder="Filter by..."
> >
<template #target="{ togglePopover }"> <template #target="{ togglePopover }">
<Button <Button
@ -74,7 +84,7 @@
</template> </template>
</Autocomplete> </Autocomplete>
<Button <Button
v-if="filterValues.length" v-if="storage.size"
class="!text-gray-600" class="!text-gray-600"
variant="ghost" variant="ghost"
label="Clear all filter" label="Clear all filter"
@ -90,14 +100,15 @@
import NestedPopover from '@/components/NestedPopover.vue' import NestedPopover from '@/components/NestedPopover.vue'
import FilterIcon from '@/components/Icons/FilterIcon.vue' import FilterIcon from '@/components/Icons/FilterIcon.vue'
import SearchComplete from '@/components/SearchComplete.vue' import SearchComplete from '@/components/SearchComplete.vue'
import { useDebounceFn } from '@vueuse/core'
import { useFilter } from '@/composables/filter'
import { import {
FeatherIcon, FeatherIcon,
Button,
Autocomplete, Autocomplete,
FormControl, FormControl,
createResource, createResource,
} from 'frappe-ui' } from 'frappe-ui'
import { ref, h } from 'vue' import { h, watch } from 'vue'
const props = defineProps({ const props = defineProps({
doctype: { doctype: {
@ -106,11 +117,10 @@ const props = defineProps({
}, },
}) })
const filterValues = ref([])
const filterableFields = createResource({ const filterableFields = createResource({
url: 'crm.api.doc.get_filterable_fields', url: 'crm.api.doc.get_filterable_fields',
auto: true, auto: true,
cache: ['filterableFields', props.doctype],
params: { params: {
doctype: props.doctype, doctype: props.doctype,
}, },
@ -126,12 +136,19 @@ const filterableFields = createResource({
}, },
}) })
const { apply, storage } = useFilter(() => filterableFields.data)
const typeCheck = ['Check'] const typeCheck = ['Check']
const typeLink = ['Link'] const typeLink = ['Link']
const typeNumber = ['Float', 'Int'] const typeNumber = ['Float', 'Int']
const typeSelect = ['Select'] const typeSelect = ['Select']
const typeString = ['Data', 'Long Text', 'Small Text', 'Text Editor', 'Text'] const typeString = ['Data', 'Long Text', 'Small Text', 'Text Editor', 'Text']
watch(
storage,
useDebounceFn(() => apply(), 300),
{ deep: true }
)
function getOperators(fieldtype) { function getOperators(fieldtype) {
let options = [] let options = []
if (typeString.includes(fieldtype)) { if (typeString.includes(fieldtype)) {
@ -171,9 +188,7 @@ function getOperators(fieldtype) {
} }
function getValSelect(fieldtype, options) { function getValSelect(fieldtype, options) {
if (typeLink.includes(fieldtype)) { if (typeSelect.includes(fieldtype) || typeCheck.includes(fieldtype)) {
return h(SearchComplete, { doctype: options })
} else if (typeSelect.includes(fieldtype) || typeCheck.includes(fieldtype)) {
const _options = const _options =
fieldtype == 'Check' ? ['Yes', 'No'] : getSelectOptions(options) fieldtype == 'Check' ? ['Yes', 'No'] : getSelectOptions(options)
return h(FormControl, { return h(FormControl, {
@ -213,36 +228,40 @@ function getSelectOptions(options) {
} }
function setfilter(data) { function setfilter(data) {
filterValues.value = [ storage.value.add({
...filterValues.value, field: {
{
field: data.value,
operator: getDefaultOperator(data.fieldtype),
value: getDefaultValue(data),
label: data.label, label: data.label,
fieldname: data.value,
fieldtype: data.fieldtype, fieldtype: data.fieldtype,
options: data.options, options: data.options,
}, },
] fieldname: data.value,
operator: getDefaultOperator(data.fieldtype),
value: getDefaultValue(data),
})
} }
function updateFilter(data, index) { function updateFilter(data, index) {
filterValues.value[index] = { storage.value.delete(Array.from(storage.value)[index])
field: data.value, storage.value.add({
fieldname: data.value,
operator: getDefaultOperator(data.fieldtype), operator: getDefaultOperator(data.fieldtype),
value: getDefaultValue(data), value: getDefaultValue(data),
label: data.label, field: {
fieldtype: data.fieldtype, label: data.label,
options: data.options, fieldname: data.value,
} fieldtype: data.fieldtype,
options: data.options,
},
})
} }
function removeFilter(index) { function removeFilter(index) {
filterValues.value.splice(index, 1) storage.value.delete(Array.from(storage.value)[index])
} }
function clearfilter(close) { function clearfilter(close) {
filterValues.value = [] storage.value.clear()
close() close()
} }
</script> </script>

View File

@ -9,10 +9,7 @@
</template> </template>
<script setup> <script setup>
import { import { Autocomplete, createListResource } from 'frappe-ui'
Autocomplete,
createListResource,
} from 'frappe-ui'
import { computed, ref, watch } from 'vue' import { computed, ref, watch } from 'vue'
const props = defineProps({ const props = defineProps({

View File

@ -0,0 +1,104 @@
import { ref, watchEffect } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { toValue } from '@vueuse/core'
import { usersStore } from '@/stores/users'
const operatorMap = {
is: '=',
'is not': '!=',
equals: '=',
'not equals': '!=',
yes: true,
no: false,
like: 'LIKE',
'not like': 'NOT LIKE',
'>': '>',
'<': '<',
'>=': '>=',
'<=': '<=',
}
export function useFilter(fields) {
const route = useRoute()
const router = useRouter()
const { getUser } = usersStore()
const storage = ref(new Set())
watchEffect(() => {
const f__ = toValue(fields)
if (fields && !f__) return
storage.value = new Set()
const q = route.query.q || ''
q.split(' ')
.map((f) => {
const [fieldname, operator, value] = f
.split(':')
.map(decodeURIComponent)
const field = (f__ || []).find((f) => f.fieldname === fieldname)
return {
field,
fieldname,
operator,
value,
}
})
.filter((f) => !f__ || (f__ && f.field))
.filter((f) => operatorMap[f.operator])
.forEach((f) => storage.value.add(f))
})
function getArgs(old) {
old = old || {}
const l__ = Array.from(storage.value)
const obj = l__.map(transformIn).reduce((p, c) => {
p[c.fieldname] = [operatorMap[c.operator.toLowerCase()], c.value]
return p
}, {})
const merged = { ...old, ...obj }
return merged
}
function apply(r) {
r = r || route
const l__ = Array.from(storage.value)
const q = l__
.map(transformOut)
.map((f) =>
[f.fieldname, f.operator.toLowerCase(), f.value]
.map(encodeURIComponent)
.join(':')
)
.join(' ')
if (!q && !r.query.q) {
router.push({ ...r, query: { ...r.query } })
} else {
router.push({ ...r, query: { ...r.query, q } })
}
}
/**
* Used to set fields internally. These will not reflect in URL.
* Can be used for APIs
*/
function transformIn(f) {
if (f.fieldname === '_assign') {
f.operator = f.operator === 'is' ? 'like' : 'not like'
}
if (f.operator.includes('like') && !f.value.includes('%')) {
f.value = `%${f.value}%`
}
return f
}
/**
* Used to set fields in URL query
*/
function transformOut(f) {
if (f.value === '@me') {
f.value = getUser()
}
return f
}
return { apply, getArgs, storage }
}

View File

@ -25,7 +25,7 @@
</Dropdown> </Dropdown>
</template> </template>
<template #right-subheader> <template #right-subheader>
<Filter doctype="CRM Lead"/> <Filter doctype="CRM Lead" />
<SortBy doctype="CRM Lead" /> <SortBy doctype="CRM Lead" />
<Button icon="more-horizontal" /> <Button icon="more-horizontal" />
</template> </template>
@ -59,6 +59,8 @@ import SortBy from '@/components/SortBy.vue'
import Filter from '@/components/Filter.vue' import Filter from '@/components/Filter.vue'
import { usersStore } from '@/stores/users' import { usersStore } from '@/stores/users'
import { useOrderBy } from '@/composables/orderby' import { useOrderBy } from '@/composables/orderby'
import { useFilter } from '@/composables/filter'
import { useDebounceFn } from '@vueuse/core'
import { import {
FeatherIcon, FeatherIcon,
Dialog, Dialog,
@ -77,6 +79,7 @@ const list = {
} }
const { getUser } = usersStore() const { getUser } = usersStore()
const { get: getOrderBy } = useOrderBy() const { get: getOrderBy } = useOrderBy()
const { getArgs, storage } = useFilter()
const currentView = ref({ const currentView = ref({
label: 'List', label: 'List',
@ -99,6 +102,7 @@ const leads = createListResource({
'lead_owner', 'lead_owner',
'modified', 'modified',
], ],
filters: getArgs() || {},
orderBy: 'modified desc', orderBy: 'modified desc',
cache: 'Leads', cache: 'Leads',
pageLength: 20, pageLength: 20,
@ -107,10 +111,22 @@ const leads = createListResource({
watch( watch(
() => getOrderBy(), () => getOrderBy(),
(value) => { (value, old_value) => {
leads.orderBy = value || 'modified desc' if (!value && !old_value) return
leads.orderBy = getOrderBy() || 'modified desc'
leads.reload() leads.reload()
} },
{ immediate: true }
)
watch(
storage,
useDebounceFn((value, old_value) => {
if (JSON.stringify([...value]) === JSON.stringify([...old_value])) return
leads.filters = getArgs() || {}
leads.reload()
}, 300),
{ deep: true }
) )
const columns = [ const columns = [