Skip to content
Snippets Groups Projects

Refactor facet autocomplete

Merged Remi PLANEL requested to merge refactor-facet-autocomplete into dev
3 files
+ 100
2
Compare changes
  • Side-by-side
  • Inline
Files
3
+ 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