Filter BarCommunity Asset

Open in new window
View RTL
<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.


Was this page helpful?

YesNo
Send feedback

We use this feedback to improve our documentation.

 
Edit page
Choose therapy brandChoose veterinary brandAbout Nord Design SystemGet support