Filter BarCommunity Asset
<script lang="ts" setup>
import { z } from 'zod'
import FilterDropdown from '@/components/FilterDropdown.vue'
const { speciesOptions } = usePatientDetailsFormFieldOptions()
// Additional Filters
type AdditionalFilter = {
id: string
label: string
value: Ref<string | Array<string>>
options: Array<{ label: string; value: string }>
}
const additionalFilters: AdditionalFilter[] = [
{
id: 'client',
label: 'Client',
value: ref([]),
options: [
{ label: 'Donald Johnson', value: '17' },
{ label: 'Claudia Olson', value: '32' },
{ label: 'Jesse Wheeler', value: '123' },
],
},
{
id: 'patient',
label: 'Patient',
value: ref([]),
options: [
{ label: 'Pixie', value: '3' },
{ label: 'Nimba', value: '33' },
{ label: 'Bink', value: '77' },
{ label: 'Luna', value: '99' },
],
},
]
function getLabelForAddedFilter(filterId: string) {
return additionalFilters.find((filter) => filter.id === filterId)?.label ?? ''
}
function getOptionsForAddedFilter(filterId: string) {
return (
additionalFilters.find((filter) => filter.id === filterId)?.options ?? []
)
}
function getValueForAddedFilter(filterId: string) {
return additionalFilters.find((filter) => filter.id === filterId)?.value
}
function updateValueForAddedFilter(filterId: string, value: string | string[]) {
const filter = additionalFilters.find((filter) => filter.id === filterId)
if (filter) {
filter.value.value = value
}
}
const addedFilterIds = ref<string[]>([])
const availableFiltersToAdd = computed(() =>
additionalFilters.filter((filter) => {
return !addedFilterIds.value.some(
(addedFilterId) => addedFilterId === filter.id,
)
}),
)
async function addFilter(filterId: string) {
addedFilterIds.value.push(filterId)
await nextTick()
if (addedFilterDropdownRefs.value.length) {
addedFilterDropdownRefs.value[
addedFilterDropdownRefs.value.length - 1
].open()
}
}
function removeFilter(filterId: string) {
addedFilterIds.value = addedFilterIds.value.filter(
(addedFilterId) => addedFilterId !== filterId,
)
}
const addedFilterDropdownRefs = ref<InstanceType<typeof FilterDropdown>[]>([])
// Filters Schema
const filterSchema = z.object({
search: z.string().default(''),
statusIn: z.array(z.string()).default([]),
speciesIn: z.array(z.string()).default([]),
clientIn: z.array(z.string()).default([]),
patientIn: z.array(z.string()).default([]),
addedFilters: z.array(z.string()).default([]),
})
const { filters, isInitialFilters, onFiltersChange, onResetFilters } =
useFiltersURLState(filterSchema)
// Search Filter
const search = ref(filters?.value?.search)
const searchDebounced = refDebounced(search)
// Status Filter
const statusIn = ref(filters?.value?.statusIn || [])
const status = ['ACTIVE', 'ARRIVED', 'CONSULTATION', 'DISCHARGED'] as const
type Status = (typeof status)[number]
const statusTranslations = {
ACTIVE: 'Active',
ARRIVED: 'Arrived',
CONSULTATION: 'Consultation',
DISCHARGED: 'Discharged',
}
const statusOptions = Object.entries(statusTranslations).map(
([value, label]) => ({
value,
label,
}),
)
function toggleStatus(toggled: Status) {
const set = new Set(statusIn.value)
if (set.has(toggled)) {
set.delete(toggled)
} else {
set.add(toggled)
}
statusIn.value = [...set]
}
// Species Filter
const speciesIn = ref(filters?.value?.speciesIn || [])
// If filters URL state changes, update internal values accordingly
watchDebounced(
() => filters?.value,
(newFilters) => {
search.value = newFilters?.search
statusIn.value = newFilters?.statusIn
speciesIn.value = newFilters?.speciesIn
updateValueForAddedFilter('client', newFilters?.clientIn)
updateValueForAddedFilter('patient', newFilters?.patientIn)
addedFilterIds.value = newFilters?.addedFilters
},
{ deep: true, immediate: true },
)
// If any of the internal values change, update filters URL changes accordingly
watch(
[
searchDebounced,
statusIn,
speciesIn,
getValueForAddedFilter('client'),
getValueForAddedFilter('patient'),
addedFilterIds,
],
() => {
return onFiltersChange({
search: search.value,
statusIn: statusIn.value,
speciesIn: speciesIn.value,
clientIn: getValueForAddedFilter('client')?.value,
patientIn: getValueForAddedFilter('patient')?.value,
addedFilters: addedFilterIds.value,
})
},
{ deep: true, immediate: true },
)
// Narrow breakpoint
const cardElement = ref<HTMLElement>()
const { width } = useElementSize(cardElement)
// based the breakpoint on some manual testing
const isNarrow = computed(() => width.value <= 1090)
</script>
<template>
<provet-stack direction="horizontal" gap="s" wrap>
<provet-input v-model="search" size="s" label="Search items" hide-label>
<provet-icon
slot="start"
name="navigation-search"
size="xs"
></provet-icon>
</provet-input>
<!-- Render a filter dropdown for the status filter at narrow widths... -->
<FilterDropdown
v-if="isNarrow"
v-model="statusIn"
name="status"
label="Status"
:options="statusOptions"
multiple
/>
<!-- ...or a tag group if space allows -->
<template v-else>
<provet-visually-hidden id="status-label">
Time frame
</provet-visually-hidden>
<provet-tag-group aria-labelledby="status-label">
<provet-tag
v-for="value in status"
:key="value"
size="s"
variant="selectable"
:checked="statusIn.includes(value)"
@change="toggleStatus(value)"
>
{{ statusTranslations[value] }}
</provet-tag>
</provet-tag-group>
</template>
<FilterDropdown
v-model="speciesIn"
name="species"
label="Species"
:options="speciesOptions"
multiple
/>
<provet-divider direction="vertical"></provet-divider>
<template v-for="filterId in addedFilterIds" :key="filterId">
<FilterDropdown
ref="addedFilterDropdownRefs"
name="add-filter"
:model-value="getValueForAddedFilter(filterId)?.value"
:label="getLabelForAddedFilter(filterId)"
:options="getOptionsForAddedFilter(filterId)"
multiple
removable
@remove="removeFilter(filterId)"
@update:model-value="updateValueForAddedFilter(filterId, $event || '')"
/>
</template>
<FilterAddButton
v-if="availableFiltersToAdd.length"
:options="availableFiltersToAdd"
@select="addFilter"
/>
<FiltersResetButton :disabled="isInitialFilters" @click="onResetFilters" />
</provet-stack>
</template>
Integration
This community asset is currently only available to use in the New Frontend for Provet Cloud.
Troubleshooting
If you experience any issues while using this community asset, please ask for support in the #vet-frontend Slack channel.