Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Datatable filtering and sorting #192

Merged
merged 3 commits into from
Mar 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 77 additions & 21 deletions components/PoisList/PoisTable.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<script setup lang="ts">
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { localeIncludes } from 'locale-includes'
import { PropertyTranslationsContextEnum } from '~/plugins/property-translations'
import type { FieldsListItem } from '~/lib/apiPois'
import type { ApiPoi, FieldsListItem } from '~/lib/apiPois'
import type { ApiMenuCategory } from '~/lib/apiMenu'
import Field from '~/components/Fields/Field.vue'
import IconButton from '~/components/UI/IconButton.vue'
Expand All @@ -12,27 +13,30 @@ import { getPoiByCategoryId } from '~/lib/apiPois'
import { siteStore as useSiteStore } from '~/stores/site'

interface DataTableHeader {
filterable: boolean
filterable?: boolean
key: string
sortable?: boolean
sort?: (a: string, b: string) => number
title: string
value?: string | Function
}

const props = defineProps<{
category: ApiMenuCategory
}>()

const { t } = useI18n()
const { routeToString, addressToString } = useField()
const { t, locale } = useI18n()
const siteStore = useSiteStore()
const { $propertyTranslations } = useNuxtApp()
const { contribMode, isContribEligible, getContributorFields } = useContrib()
const search = ref('')
const cachedKey = computed(() => `pois-${props.category.id}`)

// Fetch POIs by Cache or API
const pois = ref()
const loadingState = ref(false)
const cachedKey = computed(() => `pois-${props.category.id}`)
const { data: cachedPois } = useNuxtData(cachedKey.value)

if (cachedPois.value) {
pois.value = cachedPois.value
}
Expand All @@ -58,40 +62,92 @@ const fields = computed((): FieldsListItem[] => {
)
})

// Transform non-string values to single string
// Used for sort and filter comparisons
pois.value.features = pois.value.features.map((f: ApiPoi) => {
const fieldEntries = fields.value.map(f => f.field)
const arrayProps: { [key: string]: any } = []

if (fieldEntries.includes('route'))
arrayProps.route = routeToString(f.properties, getContext('route'))

if (fieldEntries.includes('addr'))
arrayProps.addr = addressToString(f.properties)

return {
...f,
properties: {
...f.properties,
...arrayProps,
},
}
})

// Transform headers data to be compliant with Vuetify's standards
const headers = computed((): Array<DataTableHeader> => {
const headers = computed(() => {
// Basic Fields
const headers: Array<DataTableHeader> = fields.value.map(f => ({
const h: DataTableHeader[] = fields.value.map(f => ({
filterable: true,
key: f.field,
key: `properties.${f.field}`,
sortable: true,
title: $propertyTranslations.p(
f.field,
PropertyTranslationsContextEnum.List,
),
value: `properties.${f.field}`,
sort: customSort,
}))

// Contrib Field
if (contribMode) {
headers.push({
h.push({
filterable: false,
sortable: false,
key: 'contrib',
title: t('fields.contrib.heading'),
})
}

// Details Field
headers.push({
h.push({
filterable: false,
sortable: false,
key: 'details',
title: 'Actions',
})

return headers
return h
})

function valueToString(item: any) {
if (Array.isArray(item))
return item.join(' ')

return item === undefined || typeof item === 'object' ? '' : item
}

function customSort(a: string, b: string) {
const first = valueToString(a)
const second = valueToString(b)

if (!first && second)
return -1

if (first && !second)
return 1

if (!first && !second)
return 0

return first.localeCompare(second, locale.value, { sensitivity: 'base' })
}

function customFilter(value: any, query: string): boolean {
return query !== null && value !== null && typeof value === 'string' && value.toLowerCase().includes(query.toLowerCase())
value = valueToString(value)

if (!value)
return false

return localeIncludes(value, query, { locales: locale.value, sensitivity: 'base' })
}

function getContext(key: string) {
Expand Down Expand Up @@ -139,13 +195,13 @@ function getContext(key: string) {
<tr>
<td v-for="col in columns" :key="col.key">
<ContribFieldGroup
v-if="col.key === 'contrib' && isContribEligible(item.raw.properties)"
v-bind="getContributorFields(item.raw)"
v-if="col.key === 'contrib' && isContribEligible(item.properties)"
v-bind="getContributorFields(item)"
/>
<IconButton
v-else-if="col.key === 'details'"
class="tw-h-10"
:href="`/poi/${item.raw.properties.metadata.id}/details`"
:href="`/poi/${item.properties.metadata.id}/details`"
:label="t('poisTable.details')"
target="_self"
>
Expand All @@ -154,12 +210,12 @@ function getContext(key: string) {
</IconButton>
<Field
v-else
:context="getContext(col.key)"
:recursion-stack="[col.key]"
:field="{ field: col.key }"
:context="getContext(col.key.split('.').pop())"
:recursion-stack="[col.key.split('.').pop()]"
:field="{ field: col.key.split('.').pop() }"
:details="t('poisTable.details')"
:properties="item.raw.properties"
:geom="item.raw.geometry"
:properties="item.properties"
:geom="item.geometry"
/>
</td>
</tr>
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
"vue-matomo": "^4.2.0",
"vue-screen": "^2.2.0",
"vue3-touch-events": "^4.1.3",
"vuetify": "^3.3.4"
"vuetify": "3.4.0"
},
"devDependencies": {
"@antfu/eslint-config": "^2.6.2",
Expand Down
6 changes: 0 additions & 6 deletions plugins/vuetify.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
import { createVuetify } from 'vuetify'
import { aliases, mdi } from 'vuetify/iconsets/mdi-svg'
import { VDataTable } from 'vuetify/labs/VDataTable'
import { VSkeletonLoader } from 'vuetify/labs/VSkeletonLoader'
import 'vuetify/styles'

import { defineNuxtPlugin } from '#app/nuxt'

export default defineNuxtPlugin((nuxtApp) => {
const vuetify = createVuetify({
components: {
VDataTable,
VSkeletonLoader,
},
icons: {
defaultSet: 'mdi',
aliases,
Expand Down
12 changes: 6 additions & 6 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3214,7 +3214,7 @@ __metadata:
vue-screen: ^2.2.0
vue-tsc: ^1.8.27
vue3-touch-events: ^4.1.3
vuetify: ^3.3.4
vuetify: 3.4.0
languageName: unknown
linkType: soft

Expand Down Expand Up @@ -16304,13 +16304,13 @@ __metadata:
languageName: node
linkType: hard

"vuetify@npm:^3.3.4":
version: 3.3.4
resolution: "vuetify@npm:3.3.4"
"vuetify@npm:3.4.0":
version: 3.4.0
resolution: "vuetify@npm:3.4.0"
peerDependencies:
typescript: ">=4.7"
vite-plugin-vuetify: ^1.0.0-alpha.12
vue: ^3.2.0
vue: ^3.3.0
vue-i18n: ^9.0.0
webpack-plugin-vuetify: ^2.0.0-alpha.11
peerDependenciesMeta:
Expand All @@ -16322,7 +16322,7 @@ __metadata:
optional: true
webpack-plugin-vuetify:
optional: true
checksum: adf51155a353f8cf21e88e96d72f89aa9f504ba4f0213326237595139e75918eecb7133eb084a6ec0d0a36f7a28b8cc0e1f4125e3e148c4d8c0137d0131b66c7
checksum: c28b901a575b75110d05c1cd4a9c596573af5245d11a2e02dcfb5137ef88d3375d20a281a9ac849f6ee2895a74195f18a091aa5f44e0d177a18e6d8ed9cd77a6
languageName: node
linkType: hard

Expand Down
Loading