| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256 |
- <!--suppress VueUnrecognizedSlot -->
- <template>
- <!--suppress JSValidateTypes -->
- <DataTable
- :value="characters"
- dataKey="id"
- :loading="isLoading"
- sortField="createdAt"
- :sortOrder="1"
- removableSort
- v-model:filters="filters"
- :filterDisplay="showFilters ? 'row' : null"
- :globalFilterFields="globalFilterFieldNames"
- selectionMode="single"
- resizableColumns
- stateStorage="session"
- stateKey="datatable-state"
- scrollable
- :scrollHeight="`calc(100vh - ${elementHeights}px)`"
- size="small"
- :pt="{
- header: { id: 'datatable_header' },
- footer: { id: 'datatable_footer' },
- column: {
- headerCell: 'max-w-[8rem]',
- bodyCell: 'max-w-[8rem]'
- }
- }"
- @filter="(event) => updateFilteredCharacters(event.filteredValue)"
- @rowSelect="(event) => showDetail(event.data)"
- >
- <Column
- v-for="field in _keys(columnFields)"
- :key="field"
- :field="field"
- :header="_upperCase(field)"
- :showFilterMenu="false"
- :sortable="true"
- >
- <template #filter="{ filterModel, filterCallback }">
- <OptionsFilter
- v-if="_isMatch(characterFields[field], { type: 'autocomplete' })"
- v-model="filterModel.value"
- :field="field"
- :options="options[field]"
- :filteredOptions="filteredOptions[field]"
- @filterCallback="filterCallback"
- />
- <TextFilter
- v-else
- v-model="filters[field]"
- :field="field"
- />
- </template>
- <template #sorticon="{ sorted, sortOrder }">
- <Icon
- v-if="sorted && sortOrder === 1"
- name="ph:sort-ascending-bold"
- size="1.25rem"
- />
- <Icon
- v-else-if="sorted && sortOrder === -1"
- name="ph:sort-descending-bold"
- size="1.25rem"
- />
- <Icon
- v-else
- name="ph:arrows-down-up-bold"
- size="1.25rem"
- />
- </template>
- </Column>
- <template #header>
- <div :class="isLoading && 'hidden'">
- <div class="flex justify-between gap-4 pb-2">
- <SearchField
- v-model="filters['global'].value"
- placeholder="Search…"
- type="search"
- id="global_filter"
- />
- <Button
- variant="outlined"
- :disabled="!hasAnyFilters"
- class="px-4"
- @click="resetFilters"
- >
- <Icon
- name="ph:funnel-simple-x-bold"
- size="1.25rem"
- />
- Reset
- </Button>
- <Button
- variant="outlined"
- class="px-4"
- @click="toggleShowFilters"
- >
- <Icon
- name="ph:funnel-simple-bold"
- size="1.25rem"
- />
- <div class="flex flex-col">
- <div
- class="overflow-hidden"
- :class="showFilters && 'h-0!'"
- >
- Show
- </div>
- <div
- class="overflow-hidden"
- :class="!showFilters && 'h-0!'"
- >
- Hide
- </div>
- </div>
- </Button>
- </div>
- <CharacterFilterChips
- v-if="hasAnyFilters() && !showFilters"
- class="pb-2"
- />
- </div>
- </template>
- <template #empty>
- <div
- class="text-center text-2xl"
- :class="isLoading && 'hidden'"
- >
- <template v-if="hasGlobalFilter()">
- <template v-if="hasAnyColumnFilters()">
- No characters matching
- <!--suppress JSUnresolvedReference -->
- <span class="italic">“{{ filters.global.value }}”</span>
- with the current filters.
- </template>
- <template v-else>
- No characters matching
- <!--suppress JSUnresolvedReference -->
- <span class="italic">“{{ filters.global.value }}”</span
- >.
- </template>
- </template>
- <template v-else>No characters match the current filters.</template>
- </div>
- </template>
- <template #loading>
- <SpinnerModal
- :visible="true"
- maskClass="bg-surface!"
- />
- </template>
- <template #footer>
- <CharacterToolbar
- :class="isLoading && 'hidden'"
- :filteredCount="filteredCount"
- :count="count"
- />
- </template>
- </DataTable>
- </template>
- <script setup>
- definePageMeta({ name: "characters" })
- const { isSignedIn } = useAuthClient()
- const filtersStore = useFiltersStore()
- const {
- hasAnyColumnFilters,
- hasAnyFilters,
- hasFilterFor,
- hasGlobalFilter,
- resetFilterFor,
- resetFilters,
- toggleShowFilters
- } = filtersStore
- const { filters, showFilters } = storeToRefs(filtersStore)
- const { data: characters, isLoading: isLoadingCharacters } =
- useQuery(characterListQuery)
- const { data: options, isLoading: isLoadingOptions } = useQuery(
- characterOptionsQuery
- )
- const isLoading = computed(
- () => isLoadingCharacters.value || isLoadingOptions.value
- )
- const count = computed(() => _size(characters.value))
- const filteredCount = computed(() => _size(filteredCharacters.value))
- const filteredCharacters = ref(_cloneDeep(characters.value))
- const filteredOptions = computed(() =>
- findUniqueOptions(filteredCharacters.value)
- )
- const DEFAULT_ELEMENT_HEIGHTS = 160
- const elementHeights = ref(DEFAULT_ELEMENT_HEIGHTS)
- onMounted(() => updateElementHeights())
- onUpdated(() => updateElementHeights())
- function updateFilteredCharacters(filteredValue) {
- filteredCharacters.value = filteredValue
- }
- async function showDetail({ id: characterId }) {
- if (isSignedIn.value) {
- await navigateTo({ name: "characterEdit", params: { characterId } })
- } else {
- await navigateTo({ name: "characterView", params: { characterId } })
- }
- }
- function updateElementHeights() {
- elementHeights.value = totalElementHeights()
- }
- function totalElementHeights() {
- // total height of non-datatable elements (in pixels)
- const elements = ["navbar", "datatable_header", "datatable_footer"]
- let totalHeights = _reduce(
- elements,
- (acc, element) => acc + document?.getElementById(element)?.offsetHeight,
- 0
- )
- // plus 16px [--spacing(4)] navbar bottom margin
- totalHeights += 16
- return _isNaN(totalHeights) ? DEFAULT_ELEMENT_HEIGHTS : totalHeights
- }
- </script>
- <style scoped></style>
|