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
18 changes: 12 additions & 6 deletions frontend/src/modules/events/components/EventsDataTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,31 @@ import { Icon } from '@iconify/vue'
import { Button } from '@/shared/ui/button'
import DataTablePagination from '@/shared/components/data/DataTablePagination.vue'
import DataTable from '@/shared/components/data/DataTable.vue'
import { useDataTable } from '@/shared/composables/useDataTable'
import DataTableLayout from '@/shared/components/data/DataTableLayout.vue'
import { useDataTableWithUrlQuery } from '@/shared/composables/useDataTableWithUrlQuery'
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'

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

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

<template>
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/modules/events/pages/EventsPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { useAsyncTask } from '@/shared/composables/useAsyncTask'
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'

const DeleteModal = defineAsyncComponent(() => import('@/shared/components/modals/DeleteModal.vue'))
const EventEditModal = defineAsyncComponent(
Expand All @@ -28,6 +29,8 @@ const tags = ref<Tag[]>([])
const selectedEventId = ref<number | null>(null)
const editedEvent = ref<Event | null>(null)

const urlFilters = useUrlFilters(eventsFiltersConfig)

const showEditModal = ref(false)
const showDeleteModal = ref(false)

Expand Down Expand Up @@ -99,6 +102,7 @@ onMounted(() => {
:tags="tags"
:isLoading="isLoading"
:isLoadingTags="isLoadingTags"
:url-filters="urlFilters"
/>
</div>

Expand Down
18 changes: 12 additions & 6 deletions frontend/src/modules/fields/components/FieldsDataTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,28 @@ import { Button } from '@/shared/ui/button'
import DataTablePagination from '@/shared/components/data/DataTablePagination.vue'
import { FieldType } from '@/modules/fields/types'
import DataTable from '@/shared/components/data/DataTable.vue'
import { useDataTable } from '@/shared/composables/useDataTable'
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'

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

const { table } = useDataTable({
data: () => props.data,
columns: () => props.columns,
defaultSorting: [{ id: 'id', desc: true }],
})
const { table } = useDataTableWithUrlQuery(
{
data: () => props.data,
columns: () => props.columns,
defaultSorting: [{ id: 'id', desc: true }],
},
props.urlFilters,
'fields'
)

const fieldTypes = Object.values(FieldType)
</script>
Expand Down
10 changes: 9 additions & 1 deletion frontend/src/modules/fields/pages/FieldsPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useAsyncTask } from '@/shared/composables/useAsyncTask'
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'

const DeleteModal = defineAsyncComponent(() => import('@/shared/components/modals/DeleteModal.vue'))
const FieldEditModal = defineAsyncComponent(
Expand All @@ -24,6 +25,8 @@ const fields = ref<Field[]>([])
const selectedFieldId = ref<number | null>(null)
const editedField = ref<Field | null>(null)

const urlFilters = useUrlFilters(fieldsFiltersConfig)

const showEditModal = ref(false)
const showDeleteModal = ref(false)

Expand Down Expand Up @@ -87,7 +90,12 @@ onMounted(() => {
<div>
<Header title="Fields" />
<div class="container mx-auto">
<FieldsDataTable :columns="columns" :data="fields" :isLoading="isLoading" />
<FieldsDataTable
:columns="columns"
:data="fields"
:isLoading="isLoading"
:url-filters="urlFilters"
/>
</div>

<!-- Modals -->
Expand Down
73 changes: 73 additions & 0 deletions frontend/src/modules/tags/components/TagsDataGrid.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<script setup lang="ts">
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'
interface TagsDataGridProps {
filteredData: Tag[]
isLoading: boolean
urlFilters: UrlFiltersReturn<TagsUrlFilters>
onEdit: (tag: Tag) => void
onDelete: (tag: Tag) => void
}

const props = defineProps<TagsDataGridProps>()

const handleSearchKeydown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
props.urlFilters.clearFilters()
}
}
</script>

<template>
<div>
<!-- Toolbar -->
<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))"
placeholder="Search tags..."
class="max-w-xs"
@keydown="handleSearchKeydown"
/>
</div>
<Button as-child>
<RouterLink to="/tags/new">
<Icon icon="radix-icons:plus" class="mr-2 h-4 w-4" />
Add Tag
</RouterLink>
</Button>
</div>

<!-- Tags Grid -->
<div class="grid gap-4 sm:grid-cols-1 md:grid-cols-2">
<template v-if="isLoading">
<ItemSkeleton v-for="n in 4" :key="n" />
</template>

<template v-else-if="filteredData.length > 0">
<TagItem
v-for="tag in filteredData"
:key="tag.id"
:tag="tag"
@updateMe="() => onEdit(tag)"
@deleteMe="() => onDelete(tag)"
/>
</template>

<template v-else>
<div class="col-span-full flex items-center justify-center py-6">
<div class="text-center">
<div class="text-sm">No results.</div>
</div>
</div>
</template>
</div>
</div>
</template>
98 changes: 25 additions & 73 deletions frontend/src/modules/tags/pages/TagsPage.vue
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
<script setup lang="ts">
import TagItem from '../components/TagItem.vue'
import TagsDataGrid from '../components/TagsDataGrid.vue'
import { tagApi } from '@/modules/tags/api'
import type { Tag } from '@/modules/tags/types'
import { ref, onMounted, computed, defineAsyncComponent, watch } from 'vue'
import { ref, onMounted, defineAsyncComponent, computed } from 'vue'
import { useAsyncTask } from '@/shared/composables/useAsyncTask'
import type { TagFormValues } from '@/modules/tags/validation/tagSchema'
import Header from '@/shared/components/layout/PageHeader.vue'
import { useEnhancedToast } from '@/shared/composables/useEnhancedToast'
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 { useDebounceFn } from '@vueuse/core'
import { useUrlFilters, tagsFiltersConfig } from '@/shared/composables/useUrlFilters'
import { filterTags } from '@/shared/utils/tableFilters'

const DeleteModal = defineAsyncComponent(() => import('@/shared/components/modals/DeleteModal.vue'))
Expand All @@ -22,8 +18,6 @@ const TagEditModal = defineAsyncComponent(
const { showUpdated, showDeleted } = useEnhancedToast()

const tags = ref<Tag[]>([])
const searchQuery = ref('')
const debouncedSearchQuery = ref('')
const { run, isLoading } = useAsyncTask()
const { run: runDeleteTask, isLoading: isDeleting } = useAsyncTask()
const { run: runUpdateTask, isLoading: isSaving } = useAsyncTask()
Expand All @@ -34,26 +28,12 @@ const showDeleteModal = ref(false)
const selectedTagId = ref<string | null>(null)
const editedTag = ref<Tag | null>(null)

// Debounced update of search query
const debouncedUpdateSearch = useDebounceFn((query: string) => {
debouncedSearchQuery.value = query
}, 300)

// Watch for search query changes and apply debounce
watch(searchQuery, newQuery => {
debouncedUpdateSearch(newQuery)
})

// Handle escape key to clear search
const handleSearchKeydown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
searchQuery.value = ''
debouncedSearchQuery.value = ''
}
}
// URL filters with search synchronization
const urlFilters = useUrlFilters(tagsFiltersConfig)

// Filter tags based on URL search
const filteredTags = computed(() => {
return filterTags(tags.value, debouncedSearchQuery.value)
return filterTags(tags.value, urlFilters.filters.search)
})

const handleDelete = () => {
Expand All @@ -79,6 +59,17 @@ const handleUpdate = (values: TagFormValues) => {
)
}

// Handlers for TagsDataGrid
const selectEditTag = (tag: Tag) => {
editedTag.value = tag
showEditModal.value = true
}

const selectDeleteTag = (tag: Tag) => {
selectedTagId.value = tag.id
showDeleteModal.value = true
}

onMounted(() => {
run(
() => tagApi.getAll(),
Expand All @@ -93,52 +84,13 @@ onMounted(() => {
<div class="container mx-auto">
<Header title="Tags" />

<!-- Toolbar -->
<div class="mb-6 flex flex-wrap items-center justify-between gap-4">
<div class="flex-1">
<Input
v-model="searchQuery"
placeholder="Search tags..."
class="max-w-xs"
:disabled="tags.length === 0"
@keydown="handleSearchKeydown"
>
</Input>
</div>
<Button as-child>
<RouterLink to="/tags/new">
<Icon icon="radix-icons:plus" class="mr-2 h-4 w-4" />
Add Tag
</RouterLink>
</Button>
</div>

<!-- Tags Grid -->
<div class="grid gap-4 sm:grid-cols-1 md:grid-cols-2">
<template v-if="isLoading">
<ItemSkeleton v-for="n in 4" :key="n" />
</template>

<template v-else>
<TagItem
v-for="tag in filteredTags"
:key="tag.id"
:tag="tag"
@updateMe="
() => {
showEditModal = true
editedTag = tag
}
"
@deleteMe="
id => {
showDeleteModal = true
selectedTagId = id
}
"
/>
</template>
</div>
<TagsDataGrid
:filtered-data="filteredTags"
:isLoading="isLoading"
:url-filters="urlFilters"
:onEdit="selectEditTag"
:onDelete="selectDeleteTag"
/>

<!-- Modals -->
<TagEditModal
Expand Down
Loading