Skip to content
Snippets Groups Projects

Refactor facet autocomplete

Merged Remi PLANEL requested to merge refactor-facet-autocomplete into dev
1 file
+ 8
2
Compare changes
  • Side-by-side
  • Inline
+ 255
0
<script setup lang="ts">
import { filter } from '@observablehq/plot'
export interface FilterItem {
type: 'facet' | 'innerOperator' | 'outerOperator' | 'value'
value: string
title: string
count?: number
deletable: boolean
props: Record<string, any>
}
export interface FacetItem {
title: string
value: string
type: "facet"
icon?: string
count?: number
}
export interface OperatorItem {
title: string,
type: 'operator'
}
export interface FacetCategory {
title: string
type: "subheader"
}
export interface FacetDivider {
type: "divider"
}
export type FacetInputItem = FacetItem | FacetCategory | FacetDivider
export interface Props {
db: string
modelValue: FilterItem[] | undefined
facets: MaybeRef<FacetInputItem[] | undefined>
facetDistribution: MaybeRef<Record<string, Record<string, number>> | undefined>
isValidFilters?: MaybeRef<boolean>
autocompleteProps?: Record<string, any>
}
const emit = defineEmits(['update:modelValue', "meiliFilters"])
const filterId = ref<number>(0)
const props = withDefaults(defineProps<Props>(), {
modelValue: undefined,
autocompleteProps: () => {
return {
chips: true,
clearable: true,
multiple: true,
"auto-select-first": true,
"return-object": true,
"prepend-inner-icon": "md:filter_alt",
"hide-details": "auto",
"item-value": "value", "item-title": "title",
label: "Filter results...",
"single-line": true,
}
},
isValidFilters: false
});
// const { result: msResult } = useMeiliSearch(props.db)
const isAutocompleteFocused = ref<boolean>(false)
// const facetDistribution: Ref<Record<string, Record<string, number>>> = useState('facetDistribution')
const autocompleteProps = computed(() => {
return {
...props.autocompleteProps,
items: toValue(autocompleteItems)
}
})
const filterStep = computed(() => {
const toValFilterItems = toValue(props.modelValue)
if (toValFilterItems !== undefined) {
return toValFilterItems.length % 4
}
})
const innerOperatorItems = ref<FilterItem[]>([
{
type: "innerOperator", value: '=', title: "is", deletable: false, props: {
type: "innerOperator", deletable: false
}
}, {
type: "innerOperator", value: '!=', title: "is not", deletable: false, props: {
type: "innerOperator",
deletable: false
}
},
])
const outerOperatorItems = ref<FilterItem[]>([
{
type: "outerOperator", value: 'AND', title: "AND", deletable: false, props: {
type: "outerOperator", deletable: false
}
}, {
type: "outerOperator", value: 'OR', title: "OR", deletable: false, props: {
type: "outerOperator",
deletable: false
}
},
])
const autocompleteItems = computed(() => {
const toValFilterItems = toValue(props.modelValue)
// const index = toValFilterItems?.length ?? 0
if (filterStep.value === undefined || filterStep.value === 0) {
filterId.value++
return toValue(props.facets)?.map(facetItem => {
switch (facetItem.type) {
case "facet":
return {
type: "facet",
value: `${facetItem.value}-${filterId.value}`,
title: facetItem.title,
deletable: false,
icon: facetItem?.icon,
count: facetItem?.count,
props: {
deletable: false,
type: "facet"
}
}
case "subheader":
return {
type: "subheader",
title: facetItem.title,
deletable: false,
props: {
type: "subheader"
}
}
case "divider":
return { type: "divider" }
default:
break;
}
})
}
if (filterStep.value === 1) {
filterId.value++
return innerOperatorItems.value.map(it => { return { ...it, value: `${it.value}-${filterId.value}`, } })
}
if (filterStep.value === 2) {
filterId.value++
// get the facet value
if (Array.isArray(toValFilterItems)) {
const { type, value } = toValFilterItems?.slice(-2, -1)[0]
const sanitizedValue = value.split("-")[0]
const facetDistri = toValue(props.facetDistribution)
return facetDistri?.[sanitizedValue] ? Object.entries(facetDistri[sanitizedValue]).map(([key, val]) => {
return {
type: "value", value: `${key}-${filterId.value}`, title: key, count: val, deletable: true, props: {
type: "value", count: val, deletable: true
}
}
}) : []
}
}
if (filterStep.value === 3) {
filterId.value++
return outerOperatorItems.value.map(it => { return { ...it, value: `${it.value}-${filterId.value}`, } })
}
})
const hasFacetDistribution = computed(() => {
const toValFacetDistribution = toValue(props.facetDistribution)
return toValFacetDistribution !== undefined && Object.keys(toValFacetDistribution).length > 0
})
function updateAutocompleteFocused(isFocused: boolean) {
isAutocompleteFocused.value = isFocused
}
function emitUpdateModelValue(filters: MaybeRef<FilterItem[] | undefined>) {
emit('update:modelValue', toValue(filters))
}
function clearFilters() {
emitUpdateModelValue(undefined)
}
function deleteOneFilter(index: number) {
const toValFilterItems = toValue(props.modelValue)
// check if the next item is an outeroperator
const nextFilterItem = toValFilterItems?.slice(index + 1, index + 2)
if (index + 1 === toValFilterItems?.length && toValFilterItems?.length >= 7) {
// need to remove the previous outer operator
toValFilterItems?.splice(index - 3, 4)
}
else if (nextFilterItem?.length === 1 && nextFilterItem[0].type === 'outerOperator') {
toValFilterItems?.splice(index - 2, 4)
}
else {
toValFilterItems?.splice(index - 2, 3)
}
emitUpdateModelValue(toValFilterItems)
}
function isItemFilter(type: string | undefined) {
return type === "facet" || type === "innerOperator" || type === "outerOperator" || type === "value"
}
const hint = ref<string>('All <span class="font-weight-bold">OR</span> in a row are grouped together. Example: <span class="font-weight-bold">brex OR avs AND Archea</span> &rArr; <span class="font-weight-bold">(brex OR avs) AND Archea</span>')
</script>
<template>
<v-autocomplete :model-value="props.modelValue" @click:clear="clearFilters" v-bind="autocompleteProps" :hint="hint"
persistent-hint @update:focused="updateAutocompleteFocused" @update:modelValue="emitUpdateModelValue"
:loading="!hasFacetDistribution" :disabled="!hasFacetDistribution">
<template #message="{ message }">
<span v-html="message"></span>
</template>
<template #item="{ props, item }">
<v-list-item v-if="isItemFilter(item?.raw?.type)" v-bind="{ ...props, active: false }" :title="item.title"
:prepend-icon="item?.raw?.icon ? item.raw.icon : undefined"
:subtitle="item.raw?.count ? item.raw.count : ''" :value="props.value">
</v-list-item>
<v-divider v-if="item.raw.type === 'divider'"></v-divider>
<v-list-subheader v-if="item.raw.type === 'subheader'" :title="item.raw.title"></v-list-subheader>
</template>
<template #chip="{ props, item, index }">
<v-chip v-bind="props" :text="item.raw.title" :closable="item.props.deletable"
@click:close="item.props.type === deleteOneFilter(index)"></v-chip>
</template>
<!-- <template #append>
<v-btn variant="text" icon="md:filter_alt" @click="emitUpdateModelValue(props.modelValue)"
:disabled="!isValidFilters"></v-btn>
</template> -->
</v-autocomplete>
</template>
\ No newline at end of file