index.vue 6.4 KB

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