index.vue 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. <template>
  2. <!--suppress JSValidateTypes -->
  3. <DataTable
  4. :value="characters"
  5. dataKey="id"
  6. :loading="isLoadingCharacters"
  7. sortField="createdAt"
  8. :sortOrder="1"
  9. removableSort
  10. v-model:filters="filters"
  11. :filterDisplay="showFilters && !isLoadingOptions ? '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. :field="field"
  44. :options="options[field]"
  45. :filteredOptions="filteredOptions[field]"
  46. @filterCallback="filterCallback"
  47. />
  48. <TextFilter
  49. v-else
  50. v-model="filters[field]"
  51. :field="field"
  52. />
  53. </template>
  54. <template #sorticon="{ sorted, sortOrder }">
  55. <Icon
  56. v-if="sorted && sortOrder === 1"
  57. name="ph:sort-ascending-bold"
  58. size="1.25rem"
  59. />
  60. <Icon
  61. v-else-if="sorted && sortOrder === -1"
  62. name="ph:sort-descending-bold"
  63. size="1.25rem"
  64. />
  65. <Icon
  66. v-else
  67. name="ph:arrows-down-up-bold"
  68. size="1.25rem"
  69. />
  70. </template>
  71. </Column>
  72. <template #header>
  73. <div :class="isLoadingOptions && 'hidden'">
  74. <div class="flex justify-between gap-4 pb-2">
  75. <SearchField
  76. v-model="filters['global'].value"
  77. placeholder="Search&hellip;"
  78. type="search"
  79. id="global_filter"
  80. />
  81. <Button
  82. variant="outlined"
  83. :disabled="!hasAnyFilters"
  84. class="px-4"
  85. @click="resetFilters"
  86. >
  87. <Icon
  88. name="ph:funnel-simple-x-bold"
  89. size="1.25rem"
  90. />
  91. Reset
  92. </Button>
  93. <Button
  94. variant="outlined"
  95. class="px-4"
  96. @click="toggleShowFilters"
  97. >
  98. <Icon
  99. name="ph:funnel-simple-bold"
  100. size="1.25rem"
  101. />
  102. <div class="flex flex-col">
  103. <div
  104. class="overflow-hidden"
  105. :class="showFilters && 'h-0!'"
  106. >
  107. Show
  108. </div>
  109. <div
  110. class="overflow-hidden"
  111. :class="!showFilters && 'h-0!'"
  112. >
  113. Hide
  114. </div>
  115. </div>
  116. </Button>
  117. </div>
  118. <CharacterFilterChips
  119. v-if="hasAnyFilters() && !showFilters"
  120. class="pb-2"
  121. />
  122. </div>
  123. </template>
  124. <template #empty>
  125. <div
  126. class="text-center text-2xl"
  127. :class="isLoadingCharacters && 'hidden'"
  128. >
  129. <template v-if="hasGlobalFilter()">
  130. <template v-if="hasAnyColumnFilters()">
  131. No characters matching
  132. <!--suppress JSUnresolvedReference -->
  133. <span class="italic">&ldquo;{{ filters.global.value }}&rdquo;</span>
  134. with the current filters.
  135. </template>
  136. <template v-else>
  137. No characters matching
  138. <!--suppress JSUnresolvedReference -->
  139. <span class="italic">&ldquo;{{ filters.global.value }}&rdquo;</span
  140. >.
  141. </template>
  142. </template>
  143. <template v-else>No characters match the current filters.</template>
  144. </div>
  145. </template>
  146. <template #loading>
  147. <SpinnerModal
  148. :visible="true"
  149. maskClass="bg-surface!"
  150. />
  151. </template>
  152. <template #footer>
  153. <CharacterToolbar
  154. :class="isLoadingCharacters && 'hidden'"
  155. :filteredCount="filteredCount"
  156. :count="count"
  157. />
  158. </template>
  159. </DataTable>
  160. </template>
  161. <script setup>
  162. definePageMeta({ name: "characters" })
  163. const { isSignedIn } = useAuthClient()
  164. const filtersStore = useFiltersStore()
  165. const {
  166. hasAnyColumnFilters,
  167. hasAnyFilters,
  168. hasFilterFor,
  169. hasGlobalFilter,
  170. resetFilterFor,
  171. resetFilters,
  172. toggleShowFilters
  173. } = filtersStore
  174. const { filters, showFilters } = storeToRefs(filtersStore)
  175. const {
  176. data: characters,
  177. isLoading: isLoadingCharacters,
  178. refetch: refetchCharacters
  179. } = useQuery(characterListQuery)
  180. const { data: options, isLoading: isLoadingOptions } = useQuery(
  181. characterOptionsQuery
  182. )
  183. const count = computed(() => _size(characters.value))
  184. const filteredCount = computed(() => _size(filteredCharacters.value))
  185. const filteredCharacters = ref(_cloneDeep(characters.value))
  186. const filteredOptions = computed(() =>
  187. findUniqueOptions(filteredCharacters.value)
  188. )
  189. const DEFAULT_ELEMENT_HEIGHTS = 160
  190. const elementHeights = ref(DEFAULT_ELEMENT_HEIGHTS)
  191. onMounted(() => {
  192. refetchCharacters()
  193. updateElementHeights()
  194. })
  195. onUpdated(() => updateElementHeights())
  196. function updateFilteredCharacters(filteredValue) {
  197. filteredCharacters.value = filteredValue
  198. }
  199. async function showDetail({ id: characterId }) {
  200. if (isSignedIn.value) {
  201. await navigateTo({ name: "characterEdit", params: { characterId } })
  202. } else {
  203. await navigateTo({ name: "characterView", params: { characterId } })
  204. }
  205. }
  206. function updateElementHeights() {
  207. elementHeights.value = totalElementHeights()
  208. }
  209. function totalElementHeights() {
  210. // total height of non-datatable elements (in pixels)
  211. const elements = ["navbar", "datatable_header", "datatable_footer"]
  212. // noinspection JSUnresolvedReference
  213. let totalHeights = _reduce(
  214. elements,
  215. (acc, element) => {
  216. // noinspection JSUnresolvedReference
  217. return acc + document?.getElementById(element)?.offsetHeight
  218. },
  219. 0
  220. )
  221. // plus 16px [--spacing(4)] navbar bottom margin
  222. totalHeights += 16
  223. return _isNaN(totalHeights) ? DEFAULT_ELEMENT_HEIGHTS : totalHeights
  224. }
  225. </script>
  226. <style scoped></style>