index.vue 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. <!--suppress VueUnrecognizedSlot -->
  2. <template>
  3. <DataTable
  4. :value="characters"
  5. dataKey="id"
  6. :loading="isLoading"
  7. sortField="createdAt"
  8. :sortOrder="1"
  9. removableSort
  10. v-model:filters="filters"
  11. :filterDisplay="showFilters ? 'row' : null"
  12. :globalFilterFields="globalFilterFieldNames"
  13. selectionMode="single"
  14. resizableColumns
  15. stateStorage="session"
  16. stateKey="datatable-state"
  17. scrollable
  18. :scrollHeight="`calc(100vh - ${elementHeights}px)`"
  19. size="small"
  20. :pt="{
  21. header: { id: 'datatable_header' },
  22. footer: { id: 'datatable_footer' },
  23. column: {
  24. headerCell: 'max-w-[8rem]',
  25. bodyCell: 'max-w-[8rem]'
  26. }
  27. }"
  28. @filter="(event) => updateFilteredCharacters(event.filteredValue)"
  29. @rowSelect="(event) => showDetail(event.data)"
  30. >
  31. <Column
  32. v-for="field in _keys(columnFields)"
  33. :key="field"
  34. :field="field"
  35. :header="_upperCase(field)"
  36. :showFilterMenu="false"
  37. :sortable="true"
  38. >
  39. <template #filter="{ filterModel, filterCallback }">
  40. <OptionsFilter
  41. v-if="_isMatch(characterFields[field], { type: 'autocomplete' })"
  42. v-model="filterModel.value"
  43. :options="options[field]"
  44. :filteredOptions="filteredOptions[field]"
  45. :filterCallback="filterCallback"
  46. :hasFilter="() => hasFilterFor(field)"
  47. :resetFilter="() => resetFilterFor(field)"
  48. />
  49. <TextFilter
  50. v-else
  51. v-model="filters[field]"
  52. :filterCallback="filterCallback"
  53. :hasFilter="() => hasFilterFor(field)"
  54. :resetFilter="() => resetFilterFor(field)"
  55. />
  56. </template>
  57. <template #sorticon="{ sorted, sortOrder }">
  58. <Icon
  59. v-if="sorted && sortOrder === 1"
  60. name="ph:sort-ascending-bold"
  61. size="1.25rem"
  62. />
  63. <Icon
  64. v-else-if="sorted && sortOrder === -1"
  65. name="ph:sort-descending-bold"
  66. size="1.25rem"
  67. />
  68. <Icon
  69. v-else
  70. name="ph:arrows-down-up-bold"
  71. size="1.25rem"
  72. />
  73. </template>
  74. </Column>
  75. <template #header>
  76. <div :class="isLoading && 'hidden'">
  77. <div class="flex justify-between gap-4 pb-2">
  78. <SearchField
  79. v-model="filters['global'].value"
  80. placeholder="Search"
  81. type="search"
  82. id="global_filter"
  83. />
  84. <ClearFiltersButton
  85. :disabled="!hasAnyFilters"
  86. @click="resetFilters"
  87. class="px-4"
  88. />
  89. <ToggleFilterButton
  90. @click="toggleShowFilters"
  91. class="px-4"
  92. :showFilters="showFilters"
  93. />
  94. </div>
  95. <CharacterFilterChips
  96. v-if="hasAnyFilters && !showFilters"
  97. class="pb-2"
  98. />
  99. </div>
  100. </template>
  101. <template #empty>
  102. <div
  103. class="text-center text-2xl"
  104. :class="isLoading && 'hidden'"
  105. >
  106. <template v-if="hasGlobalFilter">
  107. <template v-if="hasAnyColumnFilters">
  108. No characters matching
  109. <!--suppress JSUnresolvedReference -->
  110. <span class="italic">&ldquo;{{ filters.global.value }}&rdquo;</span>
  111. with the current filters.
  112. </template>
  113. <template v-else>
  114. No characters matching
  115. <!--suppress JSUnresolvedReference -->
  116. <span class="italic">&ldquo;{{ filters.global.value }}&rdquo;</span
  117. >.
  118. </template>
  119. </template>
  120. <template v-else>No characters match the current filters.</template>
  121. </div>
  122. </template>
  123. <template #loading>
  124. <SpinnerModal
  125. :visible="true"
  126. maskClass="bg-surface!"
  127. />
  128. </template>
  129. <template #footer>
  130. <CharacterToolbar
  131. :class="isLoading && 'hidden'"
  132. :filteredCount="filteredCount"
  133. :count="count"
  134. />
  135. </template>
  136. </DataTable>
  137. </template>
  138. <script setup>
  139. definePageMeta({ name: "characters" })
  140. const { isSignedIn } = useAuthClient()
  141. const filtersStore = useFiltersStore()
  142. const {
  143. hasAnyColumnFilters,
  144. hasAnyFilters,
  145. hasFilterFor,
  146. hasGlobalFilter,
  147. resetFilterFor,
  148. resetFilters,
  149. toggleShowFilters
  150. } = filtersStore
  151. const { filters, showFilters } = storeToRefs(filtersStore)
  152. const { data: characters, isLoading: isLoadingCharacters } =
  153. useQuery(characterListQuery)
  154. const { data: options, isLoading: isLoadingOptions } = useQuery(
  155. characterOptionsQuery
  156. )
  157. const isLoading = computed(
  158. () => isLoadingCharacters.value || isLoadingOptions.value
  159. )
  160. const count = computed(() => _size(characters.value))
  161. const filteredCount = computed(() => _size(filteredCharacters.value))
  162. const filteredCharacters = ref(_cloneDeep(characters.value))
  163. const filteredOptions = computed(() =>
  164. findUniqueOptions(filteredCharacters.value)
  165. )
  166. const DEFAULT_ELEMENT_HEIGHTS = 168
  167. const elementHeights = ref(DEFAULT_ELEMENT_HEIGHTS)
  168. onMounted(() => updateElementHeights())
  169. onUpdated(() => updateElementHeights())
  170. function updateFilteredCharacters(filteredValue) {
  171. filteredCharacters.value = filteredValue
  172. }
  173. async function showDetail({ id: characterId }) {
  174. if (isSignedIn.value) {
  175. await navigateTo({ name: "characterEdit", params: { characterId } })
  176. } else {
  177. await navigateTo({ name: "characterView", params: { characterId } })
  178. }
  179. }
  180. function updateElementHeights() {
  181. elementHeights.value = totalElementHeights()
  182. }
  183. function totalElementHeights() {
  184. // total height of non-datatable elements (in pixels)
  185. const elements = ["navbar", "datatable_header", "datatable_footer"]
  186. // noinspection JSUnresolvedReference
  187. let totalHeights = _reduce(
  188. elements,
  189. (acc, element) => acc + document?.getElementById(element)?.offsetHeight,
  190. 0
  191. )
  192. // plus 16px [--spacing(4)] bottom navbar margin
  193. totalHeights += 16
  194. return _isNaN(totalHeights) ? DEFAULT_ELEMENT_HEIGHTS : totalHeights
  195. }
  196. </script>
  197. <style scoped></style>