Skip to content
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
23 changes: 11 additions & 12 deletions frontend/src/modules/events/components/EventsDataTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,29 @@ import { Button } from '@/shared/ui/button'
import DataTablePagination from '@/shared/components/data/DataTablePagination.vue'
import DataTable from '@/shared/components/data/DataTable.vue'
import DataTableLayout from '@/shared/components/data/DataTableLayout.vue'
import { useDataTableWithUrlQuery } from '@/shared/composables/useDataTableWithUrlQuery'
import { useDataTable } from '@/shared/composables/useDataTable'
import DataTableSkeleton from '@/shared/components/skeletons/DataTableSkeleton.vue'
import DataTableSingleSelectFilter from '@/shared/components/data/DataTableSingleSelectFilter.vue'
import type { Tag } from '@/modules/tags/types'
import type { UrlFiltersReturn, EventsUrlFilters } from '@/shared/types/urlFilters'
import type { useDataTableUrlSync } from '@/shared/composables/useDataTableUrlSync'

const props = defineProps<{
columns: ColumnDef<TData, TValue>[]
data: TData[]
tags: Tag[]
isLoading: boolean
isLoadingTags: boolean
urlFilters: UrlFiltersReturn<EventsUrlFilters>
urlSync: ReturnType<typeof useDataTableUrlSync>
}>()

const { table } = useDataTableWithUrlQuery(
{
data: () => props.data,
columns: () => props.columns,
defaultSorting: [{ id: 'id', desc: true }],
},
props.urlFilters,
'events'
)
const { table } = useDataTable({
data: () => props.data,
columns: () => props.columns,
sorting: props.urlSync.state.sorting,
columnFilters: props.urlSync.state.columnFilters,
onSortingChange: props.urlSync.updaters.onSortingChange,
onColumnFiltersChange: props.urlSync.updaters.onColumnFiltersChange,
})
</script>

<template>
Expand Down
15 changes: 12 additions & 3 deletions frontend/src/modules/events/pages/EventsPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import Header from '@/shared/components/layout/PageHeader.vue'
import { getEventColumns } from '@/modules/events/components/eventColumns'
import { useEnhancedToast } from '@/shared/composables/useEnhancedToast'
import type { EventFormValues } from '@/modules/events/validation/eventSchema'
import { useUrlFilters, eventsFiltersConfig } from '@/shared/composables/useUrlFilters'
import { useDataTableUrlSync } from '@/shared/composables/useDataTableUrlSync'

const DeleteModal = defineAsyncComponent(() => import('@/shared/components/modals/DeleteModal.vue'))
const EventEditModal = defineAsyncComponent(
Expand All @@ -25,7 +25,16 @@ const editedEvent = ref<Event | null>(null)
const showEditModal = ref(false)
const showDeleteModal = ref(false)

const urlFilters = useUrlFilters(eventsFiltersConfig)
const urlSync = useDataTableUrlSync({
columnFilters: {
name: { param: 'search' },
tags: { param: 'tag' },
},
sorting: {
sortParam: 'sort',
orderParam: 'order',
},
})

const { data: tags, isLoading: isLoadingTags } = useQuery({
queryKey: ['tags'],
Expand Down Expand Up @@ -90,7 +99,7 @@ const columns = getEventColumns(selectEditEvent, selectDeleteEvent)
:tags="tags ?? []"
:isLoading="isLoading"
:isLoadingTags="isLoadingTags"
:url-filters="urlFilters"
:url-sync="urlSync"
/>
</div>

Expand Down
23 changes: 11 additions & 12 deletions frontend/src/modules/fields/components/FieldsDataTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,24 @@ import DataTable from '@/shared/components/data/DataTable.vue'
import DataTableLayout from '@/shared/components/data/DataTableLayout.vue'
import DataTableSkeleton from '@/shared/components/skeletons/DataTableSkeleton.vue'
import DataTableMultiSelectFilter from '@/shared/components/data/DataTableMultiSelectFilter.vue'
import type { UrlFiltersReturn, FieldsUrlFilters } from '@/shared/types/urlFilters'
import { useDataTableWithUrlQuery } from '@/shared/composables/useDataTableWithUrlQuery'
import { useDataTable } from '@/shared/composables/useDataTable'
import type { useDataTableUrlSync } from '@/shared/composables/useDataTableUrlSync'

const props = defineProps<{
columns: ColumnDef<TData, TValue>[]
data: TData[]
isLoading: boolean
urlFilters: UrlFiltersReturn<FieldsUrlFilters>
urlSync: ReturnType<typeof useDataTableUrlSync>
}>()

const { table } = useDataTableWithUrlQuery(
{
data: () => props.data,
columns: () => props.columns,
defaultSorting: [{ id: 'id', desc: true }],
},
props.urlFilters,
'fields'
)
const { table } = useDataTable({
data: () => props.data,
columns: () => props.columns,
sorting: props.urlSync.state.sorting,
columnFilters: props.urlSync.state.columnFilters,
onSortingChange: props.urlSync.updaters.onSortingChange,
onColumnFiltersChange: props.urlSync.updaters.onColumnFiltersChange,
})

const fieldTypes = Object.values(FieldType)
</script>
Expand Down
15 changes: 12 additions & 3 deletions frontend/src/modules/fields/pages/FieldsPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import Header from '@/shared/components/layout/PageHeader.vue'
import { getFieldColumns } from '@/modules/fields/components/fieldColumns'
import { useEnhancedToast } from '@/shared/composables/useEnhancedToast'
import type { FieldFormValues } from '@/modules/fields/validation/fieldSchema'
import { useUrlFilters, fieldsFiltersConfig } from '@/shared/composables/useUrlFilters'
import { useDataTableUrlSync } from '@/shared/composables/useDataTableUrlSync'

const DeleteModal = defineAsyncComponent(() => import('@/shared/components/modals/DeleteModal.vue'))
const FieldEditModal = defineAsyncComponent(
Expand All @@ -24,7 +24,16 @@ const editedField = ref<Field | null>(null)
const showEditModal = ref(false)
const showDeleteModal = ref(false)

const urlFilters = useUrlFilters(fieldsFiltersConfig)
const urlSync = useDataTableUrlSync({
columnFilters: {
name: { param: 'search' },
field_type: { param: 'type', type: 'array' },
},
sorting: {
sortParam: 'sort',
orderParam: 'order',
},
})

const { data: fields, isLoading } = useQuery({
queryKey: ['fields'],
Expand Down Expand Up @@ -81,7 +90,7 @@ const columns = getFieldColumns(selectEditField, selectDeleteField)
:columns="columns"
:data="fields ?? []"
:isLoading="isLoading"
:url-filters="urlFilters"
:url-sync="urlSync"
/>
</div>

Expand Down
33 changes: 28 additions & 5 deletions frontend/src/modules/tags/components/TagsDataGrid.vue
Original file line number Diff line number Diff line change
@@ -1,25 +1,49 @@
<script setup lang="ts">
import { ref, watch } from 'vue'
import TagItem from './TagItem.vue'
import type { Tag } from '@/modules/tags/types'
import type { UrlFiltersReturn, TagsUrlFilters } from '@/shared/types/urlFilters'
import { Input } from '@/shared/ui/input'
import { Button } from '@/shared/ui/button'
import { Icon } from '@iconify/vue'
import ItemSkeleton from '@/shared/components/skeletons/ItemSkeleton.vue'
import { RouterLink } from 'vue-router'
import { useDebounceFn } from '@vueuse/core'

interface TagsDataGridProps {
filteredData: Tag[]
isLoading: boolean
urlFilters: UrlFiltersReturn<TagsUrlFilters>
searchValue: string
onEdit: (tag: Tag) => void
onDelete: (tag: Tag) => void
}

const props = defineProps<TagsDataGridProps>()
const emit = defineEmits<{
'update:searchValue': [value: string]
}>()

const localSearchValue = ref(props.searchValue)

const debouncedEmit = useDebounceFn((value: string) => {
emit('update:searchValue', value)
}, 300)

watch(localSearchValue, newValue => {
debouncedEmit(newValue)
})

watch(
() => props.searchValue,
newValue => {
if (newValue !== localSearchValue.value) {
localSearchValue.value = newValue
}
}
)

const handleSearchKeydown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
props.urlFilters.clearFilters()
localSearchValue.value = ''
}
}
</script>
Expand All @@ -30,8 +54,7 @@ const handleSearchKeydown = (event: KeyboardEvent) => {
<div class="mb-6 flex flex-wrap items-center justify-between gap-4">
<div class="flex-1">
<Input
:model-value="urlFilters.filters.search"
@update:model-value="(value: string | number) => urlFilters.updateSearch?.(String(value))"
v-model="localSearchValue"
placeholder="Search tags..."
class="max-w-xs"
@keydown="handleSearchKeydown"
Expand Down
20 changes: 16 additions & 4 deletions frontend/src/modules/tags/pages/TagsPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { ref, defineAsyncComponent, computed } from 'vue'
import type { TagFormValues } from '@/modules/tags/validation/tagSchema'
import Header from '@/shared/components/layout/PageHeader.vue'
import { useEnhancedToast } from '@/shared/composables/useEnhancedToast'
import { useUrlFilters, tagsFiltersConfig } from '@/shared/composables/useUrlFilters'
import { useDataTableUrlSync } from '@/shared/composables/useDataTableUrlSync'
import { filterTags } from '@/shared/utils/tableFilters'

const DeleteModal = defineAsyncComponent(() => import('@/shared/components/modals/DeleteModal.vue'))
Expand All @@ -24,7 +24,18 @@ const editedTag = ref<Tag | null>(null)
const showEditModal = ref(false)
const showDeleteModal = ref(false)

const urlFilters = useUrlFilters(tagsFiltersConfig)
const { state, updaters } = useDataTableUrlSync({
columnFilters: { search: { param: 'search' } },
})

const searchValue = computed(() => {
const filter = state.columnFilters.value.find(f => f.id === 'search')
return (filter?.value as string) || ''
})

const handleSearchUpdate = (value: string) => {
updaters.onColumnFiltersChange([{ id: 'search', value }])
}

const { data: tags, isLoading } = useQuery({
queryKey: ['tags'],
Expand Down Expand Up @@ -70,7 +81,7 @@ const selectDeleteTag = (tag: Tag) => {
}

const filteredTags = computed(() => {
return filterTags(tags.value ?? [], urlFilters.filters.search)
return filterTags(tags.value ?? [], searchValue.value)
})
</script>

Expand All @@ -81,7 +92,8 @@ const filteredTags = computed(() => {
<TagsDataGrid
:filtered-data="filteredTags"
:isLoading="isLoading"
:url-filters="urlFilters"
:search-value="searchValue"
@update:search-value="handleSearchUpdate"
:onEdit="selectEditTag"
:onDelete="selectDeleteTag"
/>
Expand Down
36 changes: 25 additions & 11 deletions frontend/src/shared/composables/useDataTable.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { ref } from 'vue'
import { ref, type Ref } from 'vue'
import type {
ColumnDef,
SortingState,
ColumnFiltersState,
VisibilityState,
PaginationState,
Updater,
} from '@tanstack/vue-table'
import {
getCoreRowModel,
Expand All @@ -17,26 +18,36 @@ import {
} from '@tanstack/vue-table'
import { valueUpdater } from '@/shared/utils/general'

type SortingOption = {
id: string
desc: boolean
}

type UseDataTableOptions<TData, TValue> = {
data: () => TData[]
columns: () => ColumnDef<TData, TValue>[]
defaultSorting?: SortingOption[]
// Optional controlled state
sorting?: Ref<SortingState>
columnFilters?: Ref<ColumnFiltersState>
onSortingChange?: (updater: Updater<SortingState>) => void
onColumnFiltersChange?: (updater: Updater<ColumnFiltersState>) => void
// Default values for internal state
defaultSorting?: SortingState
defaultPageSize?: number
}

export function useDataTable<TData, TValue>({
data,
columns,
sorting: externalSorting,
columnFilters: externalColumnFilters,
onSortingChange: externalOnSortingChange,
onColumnFiltersChange: externalOnColumnFiltersChange,
defaultSorting = [],
defaultPageSize = 10,
}: UseDataTableOptions<TData, TValue>) {
const sorting = ref<SortingState>(defaultSorting ?? [])
const columnFilters = ref<ColumnFiltersState>([])
// Use external state if provided, otherwise create internal state
const internalSorting = ref<SortingState>(defaultSorting)
const internalColumnFilters = ref<ColumnFiltersState>([])

const sorting = externalSorting || internalSorting
const columnFilters = externalColumnFilters || internalColumnFilters

const columnVisibility = ref<VisibilityState>({})
const rowSelection = ref({})
const pagination = ref<PaginationState>({
Expand All @@ -57,8 +68,11 @@ export function useDataTable<TData, TValue>({
getFilteredRowModel: getFilteredRowModel(),
getFacetedRowModel: getFacetedRowModel(),
getFacetedUniqueValues: getFacetedUniqueValues(),
onSortingChange: updaterOrValue => valueUpdater(updaterOrValue, sorting),
onColumnFiltersChange: updaterOrValue => valueUpdater(updaterOrValue, columnFilters),
onSortingChange:
externalOnSortingChange || (updaterOrValue => valueUpdater(updaterOrValue, internalSorting)),
onColumnFiltersChange:
externalOnColumnFiltersChange ||
(updaterOrValue => valueUpdater(updaterOrValue, internalColumnFilters)),
onColumnVisibilityChange: updaterOrValue => valueUpdater(updaterOrValue, columnVisibility),
onRowSelectionChange: updaterOrValue => valueUpdater(updaterOrValue, rowSelection),
onPaginationChange: updaterOrValue => valueUpdater(updaterOrValue, pagination),
Expand Down
Loading
Loading