Skip to content

Commit 78f573d

Browse files
authored
feat: url query filters for tables and grids (events, fields, tags)
1 parent 066d5ee commit 78f573d

File tree

11 files changed

+635
-87
lines changed

11 files changed

+635
-87
lines changed

frontend/src/modules/events/components/EventsDataTable.vue

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,31 @@ import { Icon } from '@iconify/vue'
55
import { Button } from '@/shared/ui/button'
66
import DataTablePagination from '@/shared/components/data/DataTablePagination.vue'
77
import DataTable from '@/shared/components/data/DataTable.vue'
8-
import { useDataTable } from '@/shared/composables/useDataTable'
98
import DataTableLayout from '@/shared/components/data/DataTableLayout.vue'
9+
import { useDataTableWithUrlQuery } from '@/shared/composables/useDataTableWithUrlQuery'
1010
import DataTableSkeleton from '@/shared/components/skeletons/DataTableSkeleton.vue'
1111
import DataTableSingleSelectFilter from '@/shared/components/data/DataTableSingleSelectFilter.vue'
1212
import type { Tag } from '@/modules/tags/types'
13+
import type { UrlFiltersReturn, EventsUrlFilters } from '@/shared/types/urlFilters'
1314
1415
const props = defineProps<{
1516
columns: ColumnDef<TData, TValue>[]
1617
data: TData[]
1718
tags: Tag[]
1819
isLoading: boolean
1920
isLoadingTags: boolean
21+
urlFilters: UrlFiltersReturn<EventsUrlFilters>
2022
}>()
2123
22-
const { table } = useDataTable({
23-
data: () => props.data,
24-
columns: () => props.columns,
25-
defaultSorting: [{ id: 'id', desc: true }],
26-
})
24+
const { table } = useDataTableWithUrlQuery(
25+
{
26+
data: () => props.data,
27+
columns: () => props.columns,
28+
defaultSorting: [{ id: 'id', desc: true }],
29+
},
30+
props.urlFilters,
31+
'events'
32+
)
2733
</script>
2834

2935
<template>

frontend/src/modules/events/pages/EventsPage.vue

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { useAsyncTask } from '@/shared/composables/useAsyncTask'
1010
import { getEventColumns } from '@/modules/events/components/eventColumns'
1111
import { useEnhancedToast } from '@/shared/composables/useEnhancedToast'
1212
import type { EventFormValues } from '@/modules/events/validation/eventSchema'
13+
import { useUrlFilters, eventsFiltersConfig } from '@/shared/composables/useUrlFilters'
1314
1415
const DeleteModal = defineAsyncComponent(() => import('@/shared/components/modals/DeleteModal.vue'))
1516
const EventEditModal = defineAsyncComponent(
@@ -28,6 +29,8 @@ const tags = ref<Tag[]>([])
2829
const selectedEventId = ref<number | null>(null)
2930
const editedEvent = ref<Event | null>(null)
3031
32+
const urlFilters = useUrlFilters(eventsFiltersConfig)
33+
3134
const showEditModal = ref(false)
3235
const showDeleteModal = ref(false)
3336
@@ -99,6 +102,7 @@ onMounted(() => {
99102
:tags="tags"
100103
:isLoading="isLoading"
101104
:isLoadingTags="isLoadingTags"
105+
:url-filters="urlFilters"
102106
/>
103107
</div>
104108

frontend/src/modules/fields/components/FieldsDataTable.vue

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,28 @@ import { Button } from '@/shared/ui/button'
66
import DataTablePagination from '@/shared/components/data/DataTablePagination.vue'
77
import { FieldType } from '@/modules/fields/types'
88
import DataTable from '@/shared/components/data/DataTable.vue'
9-
import { useDataTable } from '@/shared/composables/useDataTable'
109
import DataTableLayout from '@/shared/components/data/DataTableLayout.vue'
1110
import DataTableSkeleton from '@/shared/components/skeletons/DataTableSkeleton.vue'
1211
import DataTableMultiSelectFilter from '@/shared/components/data/DataTableMultiSelectFilter.vue'
12+
import type { UrlFiltersReturn, FieldsUrlFilters } from '@/shared/types/urlFilters'
13+
import { useDataTableWithUrlQuery } from '@/shared/composables/useDataTableWithUrlQuery'
1314
1415
const props = defineProps<{
1516
columns: ColumnDef<TData, TValue>[]
1617
data: TData[]
1718
isLoading: boolean
19+
urlFilters: UrlFiltersReturn<FieldsUrlFilters>
1820
}>()
1921
20-
const { table } = useDataTable({
21-
data: () => props.data,
22-
columns: () => props.columns,
23-
defaultSorting: [{ id: 'id', desc: true }],
24-
})
22+
const { table } = useDataTableWithUrlQuery(
23+
{
24+
data: () => props.data,
25+
columns: () => props.columns,
26+
defaultSorting: [{ id: 'id', desc: true }],
27+
},
28+
props.urlFilters,
29+
'fields'
30+
)
2531
2632
const fieldTypes = Object.values(FieldType)
2733
</script>

frontend/src/modules/fields/pages/FieldsPage.vue

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { useAsyncTask } from '@/shared/composables/useAsyncTask'
88
import { getFieldColumns } from '@/modules/fields/components/fieldColumns'
99
import { useEnhancedToast } from '@/shared/composables/useEnhancedToast'
1010
import type { FieldFormValues } from '@/modules/fields/validation/fieldSchema'
11+
import { useUrlFilters, fieldsFiltersConfig } from '@/shared/composables/useUrlFilters'
1112
1213
const DeleteModal = defineAsyncComponent(() => import('@/shared/components/modals/DeleteModal.vue'))
1314
const FieldEditModal = defineAsyncComponent(
@@ -24,6 +25,8 @@ const fields = ref<Field[]>([])
2425
const selectedFieldId = ref<number | null>(null)
2526
const editedField = ref<Field | null>(null)
2627
28+
const urlFilters = useUrlFilters(fieldsFiltersConfig)
29+
2730
const showEditModal = ref(false)
2831
const showDeleteModal = ref(false)
2932
@@ -87,7 +90,12 @@ onMounted(() => {
8790
<div>
8891
<Header title="Fields" />
8992
<div class="container mx-auto">
90-
<FieldsDataTable :columns="columns" :data="fields" :isLoading="isLoading" />
93+
<FieldsDataTable
94+
:columns="columns"
95+
:data="fields"
96+
:isLoading="isLoading"
97+
:url-filters="urlFilters"
98+
/>
9199
</div>
92100

93101
<!-- Modals -->
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<script setup lang="ts">
2+
import TagItem from './TagItem.vue'
3+
import type { Tag } from '@/modules/tags/types'
4+
import type { UrlFiltersReturn, TagsUrlFilters } from '@/shared/types/urlFilters'
5+
import { Input } from '@/shared/ui/input'
6+
import { Button } from '@/shared/ui/button'
7+
import { Icon } from '@iconify/vue'
8+
import ItemSkeleton from '@/shared/components/skeletons/ItemSkeleton.vue'
9+
import { RouterLink } from 'vue-router'
10+
interface TagsDataGridProps {
11+
filteredData: Tag[]
12+
isLoading: boolean
13+
urlFilters: UrlFiltersReturn<TagsUrlFilters>
14+
onEdit: (tag: Tag) => void
15+
onDelete: (tag: Tag) => void
16+
}
17+
18+
const props = defineProps<TagsDataGridProps>()
19+
20+
const handleSearchKeydown = (event: KeyboardEvent) => {
21+
if (event.key === 'Escape') {
22+
props.urlFilters.clearFilters()
23+
}
24+
}
25+
</script>
26+
27+
<template>
28+
<div>
29+
<!-- Toolbar -->
30+
<div class="mb-6 flex flex-wrap items-center justify-between gap-4">
31+
<div class="flex-1">
32+
<Input
33+
:model-value="urlFilters.filters.search"
34+
@update:model-value="(value: string | number) => urlFilters.updateSearch?.(String(value))"
35+
placeholder="Search tags..."
36+
class="max-w-xs"
37+
@keydown="handleSearchKeydown"
38+
/>
39+
</div>
40+
<Button as-child>
41+
<RouterLink to="/tags/new">
42+
<Icon icon="radix-icons:plus" class="mr-2 h-4 w-4" />
43+
Add Tag
44+
</RouterLink>
45+
</Button>
46+
</div>
47+
48+
<!-- Tags Grid -->
49+
<div class="grid gap-4 sm:grid-cols-1 md:grid-cols-2">
50+
<template v-if="isLoading">
51+
<ItemSkeleton v-for="n in 4" :key="n" />
52+
</template>
53+
54+
<template v-else-if="filteredData.length > 0">
55+
<TagItem
56+
v-for="tag in filteredData"
57+
:key="tag.id"
58+
:tag="tag"
59+
@updateMe="() => onEdit(tag)"
60+
@deleteMe="() => onDelete(tag)"
61+
/>
62+
</template>
63+
64+
<template v-else>
65+
<div class="col-span-full flex items-center justify-center py-6">
66+
<div class="text-center">
67+
<div class="text-sm">No results.</div>
68+
</div>
69+
</div>
70+
</template>
71+
</div>
72+
</div>
73+
</template>

frontend/src/modules/tags/pages/TagsPage.vue

Lines changed: 25 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,13 @@
11
<script setup lang="ts">
2-
import TagItem from '../components/TagItem.vue'
2+
import TagsDataGrid from '../components/TagsDataGrid.vue'
33
import { tagApi } from '@/modules/tags/api'
44
import type { Tag } from '@/modules/tags/types'
5-
import { ref, onMounted, computed, defineAsyncComponent, watch } from 'vue'
5+
import { ref, onMounted, defineAsyncComponent, computed } from 'vue'
66
import { useAsyncTask } from '@/shared/composables/useAsyncTask'
77
import type { TagFormValues } from '@/modules/tags/validation/tagSchema'
88
import Header from '@/shared/components/layout/PageHeader.vue'
99
import { useEnhancedToast } from '@/shared/composables/useEnhancedToast'
10-
import { Input } from '@/shared/ui/input'
11-
import { Button } from '@/shared/ui/button'
12-
import { Icon } from '@iconify/vue'
13-
import ItemSkeleton from '@/shared/components/skeletons/ItemSkeleton.vue'
14-
import { useDebounceFn } from '@vueuse/core'
10+
import { useUrlFilters, tagsFiltersConfig } from '@/shared/composables/useUrlFilters'
1511
import { filterTags } from '@/shared/utils/tableFilters'
1612
1713
const DeleteModal = defineAsyncComponent(() => import('@/shared/components/modals/DeleteModal.vue'))
@@ -22,8 +18,6 @@ const TagEditModal = defineAsyncComponent(
2218
const { showUpdated, showDeleted } = useEnhancedToast()
2319
2420
const tags = ref<Tag[]>([])
25-
const searchQuery = ref('')
26-
const debouncedSearchQuery = ref('')
2721
const { run, isLoading } = useAsyncTask()
2822
const { run: runDeleteTask, isLoading: isDeleting } = useAsyncTask()
2923
const { run: runUpdateTask, isLoading: isSaving } = useAsyncTask()
@@ -34,26 +28,12 @@ const showDeleteModal = ref(false)
3428
const selectedTagId = ref<string | null>(null)
3529
const editedTag = ref<Tag | null>(null)
3630
37-
// Debounced update of search query
38-
const debouncedUpdateSearch = useDebounceFn((query: string) => {
39-
debouncedSearchQuery.value = query
40-
}, 300)
41-
42-
// Watch for search query changes and apply debounce
43-
watch(searchQuery, newQuery => {
44-
debouncedUpdateSearch(newQuery)
45-
})
46-
47-
// Handle escape key to clear search
48-
const handleSearchKeydown = (event: KeyboardEvent) => {
49-
if (event.key === 'Escape') {
50-
searchQuery.value = ''
51-
debouncedSearchQuery.value = ''
52-
}
53-
}
31+
// URL filters with search synchronization
32+
const urlFilters = useUrlFilters(tagsFiltersConfig)
5433
34+
// Filter tags based on URL search
5535
const filteredTags = computed(() => {
56-
return filterTags(tags.value, debouncedSearchQuery.value)
36+
return filterTags(tags.value, urlFilters.filters.search)
5737
})
5838
5939
const handleDelete = () => {
@@ -79,6 +59,17 @@ const handleUpdate = (values: TagFormValues) => {
7959
)
8060
}
8161
62+
// Handlers for TagsDataGrid
63+
const selectEditTag = (tag: Tag) => {
64+
editedTag.value = tag
65+
showEditModal.value = true
66+
}
67+
68+
const selectDeleteTag = (tag: Tag) => {
69+
selectedTagId.value = tag.id
70+
showDeleteModal.value = true
71+
}
72+
8273
onMounted(() => {
8374
run(
8475
() => tagApi.getAll(),
@@ -93,52 +84,13 @@ onMounted(() => {
9384
<div class="container mx-auto">
9485
<Header title="Tags" />
9586

96-
<!-- Toolbar -->
97-
<div class="mb-6 flex flex-wrap items-center justify-between gap-4">
98-
<div class="flex-1">
99-
<Input
100-
v-model="searchQuery"
101-
placeholder="Search tags..."
102-
class="max-w-xs"
103-
:disabled="tags.length === 0"
104-
@keydown="handleSearchKeydown"
105-
>
106-
</Input>
107-
</div>
108-
<Button as-child>
109-
<RouterLink to="/tags/new">
110-
<Icon icon="radix-icons:plus" class="mr-2 h-4 w-4" />
111-
Add Tag
112-
</RouterLink>
113-
</Button>
114-
</div>
115-
116-
<!-- Tags Grid -->
117-
<div class="grid gap-4 sm:grid-cols-1 md:grid-cols-2">
118-
<template v-if="isLoading">
119-
<ItemSkeleton v-for="n in 4" :key="n" />
120-
</template>
121-
122-
<template v-else>
123-
<TagItem
124-
v-for="tag in filteredTags"
125-
:key="tag.id"
126-
:tag="tag"
127-
@updateMe="
128-
() => {
129-
showEditModal = true
130-
editedTag = tag
131-
}
132-
"
133-
@deleteMe="
134-
id => {
135-
showDeleteModal = true
136-
selectedTagId = id
137-
}
138-
"
139-
/>
140-
</template>
141-
</div>
87+
<TagsDataGrid
88+
:filtered-data="filteredTags"
89+
:isLoading="isLoading"
90+
:url-filters="urlFilters"
91+
:onEdit="selectEditTag"
92+
:onDelete="selectDeleteTag"
93+
/>
14294

14395
<!-- Modals -->
14496
<TagEditModal

0 commit comments

Comments
 (0)