Skip to content

Commit af1838f

Browse files
authored
feat(frontend): upgrade linked fields selector (#46)
* feat(frontend): improve CompactDataTablePagination with compact text sizing * feat(frontend): upgrade LinkedFieldsSelector to paginated data table interface * refactor(frontend): polish form layouts and improve input attributes for security * fix(frontend): enable Vite polling for reliable Docker HMR on macOS * fix(frontend): remove deprecated tsconfig options
1 parent ead5006 commit af1838f

6 files changed

Lines changed: 140 additions & 43 deletions

File tree

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

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -53,18 +53,6 @@ const { handleSubmit, values, setValues, setFieldValue } = useForm<EventFormValu
5353
validationSchema: toTypedSchema(eventSchema),
5454
})
5555
56-
function toggleFieldSelection(id: number) {
57-
const current = values.fields || []
58-
if (current.includes(id)) {
59-
setFieldValue(
60-
'fields',
61-
current.filter(f => f !== id)
62-
)
63-
} else {
64-
setFieldValue('fields', [...current, id])
65-
}
66-
}
67-
6856
watchEffect(() => {
6957
if (props.event) {
7058
setValues({
@@ -230,16 +218,39 @@ function removeTag(tagId: string) {
230218
</FormField>
231219

232220
<!-- Linked Fields -->
233-
<FormField name="fields">
221+
<FormField name="fields" v-slot="{ componentField }">
234222
<FormItem>
235223
<FormLabel>Linked Fields</FormLabel>
236224
<FormDescription>Choose one or more fields this event uses.</FormDescription>
237225
<FormControl>
238226
<LinkedFieldsSelector
239227
:fields="availableFields"
240-
:selected-ids="values.fields"
228+
:selected-ids="componentField.modelValue || []"
241229
:is-loading="isLoadingFields"
242-
@toggle="toggleFieldSelection"
230+
@toggle="
231+
(id: number) => {
232+
const current: number[] = componentField.modelValue || []
233+
const updated = current.includes(id)
234+
? current.filter((f: number) => f !== id)
235+
: [...current, id]
236+
componentField.onChange(updated)
237+
}
238+
"
239+
@toggle-all="
240+
(ids: number[], selected: boolean) => {
241+
const current: number[] = componentField.modelValue || []
242+
let updated: number[]
243+
if (selected) {
244+
// Add all missing IDs
245+
const toAdd = ids.filter(id => !current.includes(id))
246+
updated = [...current, ...toAdd]
247+
} else {
248+
// Remove all provided IDs
249+
updated = current.filter(id => !ids.includes(id))
250+
}
251+
componentField.onChange(updated)
252+
}
253+
"
243254
/>
244255
</FormControl>
245256
<FormMessage />

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ const onSubmit = (values: EventFormValues) => {
4545
<div>
4646
<Header title="Create new event" backLink fallbackBackLink="/events" />
4747

48-
<Card class="mx-auto max-w-md">
48+
<Card class="mx-auto max-w-lg">
4949
<CardContent>
5050
<EventForm
5151
:availableFields="fields ?? []"
Lines changed: 109 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,125 @@
11
<script setup lang="ts">
2+
import { h, computed } from 'vue'
23
import type { Field } from '@/modules/fields/types'
3-
import Skeleton from '@/shared/ui/skeleton/Skeleton.vue'
4-
import Checkbox from '@/shared/ui/checkbox/Checkbox.vue'
4+
import type { ColumnDef } from '@tanstack/vue-table'
5+
import { Checkbox } from '@/shared/ui/checkbox'
6+
import { Input } from '@/shared/ui/input'
7+
import { useDataTable } from '@/shared/composables/useDataTable'
8+
import DataTable from '@/shared/components/data/DataTable.vue'
9+
import DataTableSkeleton from '@/shared/components/skeletons/DataTableSkeleton.vue'
10+
import CompactDataTablePagination from '@/shared/components/data/CompactDataTablePagination.vue'
11+
import DataTableColumnHeader from '@/shared/components/data/DataTableColumnHeader.vue'
512
6-
defineProps<{
13+
const props = defineProps<{
714
fields: Field[]
8-
selectedIds: number[]
15+
selectedIds?: number[]
916
isLoading?: boolean
1017
}>()
1118
1219
const emit = defineEmits<{
1320
(e: 'toggle', id: number): void
21+
(e: 'toggleAll', ids: number[], selected: boolean): void
1422
}>()
23+
24+
const safeSelectedIds = computed(() => props.selectedIds || [])
25+
26+
const columns = computed<ColumnDef<Field>[]>(() => [
27+
{
28+
accessorKey: 'id',
29+
enableHiding: false,
30+
meta: {
31+
class: 'w-[6ch] text-center',
32+
headerClass: 'w-[6ch] text-center',
33+
},
34+
header: ({ column }) =>
35+
h(DataTableColumnHeader<Field, unknown>, {
36+
column,
37+
title: 'ID',
38+
align: 'center',
39+
}),
40+
cell: ({ row }) => h('div', { class: 'text-center font-medium' }, row.original.id),
41+
},
42+
{
43+
accessorKey: 'name',
44+
enableHiding: false,
45+
header: ({ column }) => h(DataTableColumnHeader<Field, unknown>, { column, title: 'Name' }),
46+
cell: ({ row }) =>
47+
h(
48+
'div',
49+
{
50+
class: 'truncate whitespace-nowrap overflow-hidden text-left font-medium',
51+
title: row.original.name,
52+
},
53+
row.original.name
54+
),
55+
},
56+
{
57+
id: 'selection',
58+
enableHiding: false,
59+
header: ({ table }) => {
60+
const rows = table.getFilteredRowModel().rows
61+
const allIds = rows.map(r => r.original.id)
62+
const isAllSelected =
63+
allIds.length > 0 && allIds.every(id => safeSelectedIds.value.includes(id))
64+
const isSomeSelected = allIds.some(id => safeSelectedIds.value.includes(id))
65+
66+
return h(
67+
'div',
68+
{ class: 'flex justify-end pr-6' },
69+
h(Checkbox, {
70+
modelValue: isAllSelected || (isSomeSelected && 'indeterminate'),
71+
'onUpdate:modelValue': (value: boolean | 'indeterminate') =>
72+
emit('toggleAll', allIds, value === true),
73+
ariaLabel: 'Select all',
74+
})
75+
)
76+
},
77+
cell: ({ row }) =>
78+
h(
79+
'div',
80+
{ class: 'flex justify-end pr-6' },
81+
h(Checkbox, {
82+
modelValue: safeSelectedIds.value.includes(row.original.id),
83+
'onUpdate:modelValue': () => emit('toggle', row.original.id),
84+
ariaLabel: 'Select field',
85+
})
86+
),
87+
},
88+
])
89+
90+
const { table } = useDataTable({
91+
data: () => props.fields,
92+
columns: () => columns.value,
93+
defaultPageSize: 5,
94+
})
1595
</script>
1696

1797
<template>
18-
<div class="max-h-24 space-y-2 overflow-y-auto rounded-md border p-4 shadow-xs">
19-
<template v-if="isLoading">
20-
<Skeleton v-for="i in 4" :key="i" class="h-5 w-[70%] rounded-md shadow-xs" />
21-
</template>
22-
23-
<Transition name="fade" appear>
24-
<div v-if="!isLoading" class="space-y-2">
25-
<div v-for="field in fields" :key="field.id" class="flex items-center gap-2">
26-
<Checkbox
27-
:id="`field-${field.id}`"
28-
:model-value="selectedIds.includes(field.id)"
29-
@update:model-value="() => emit('toggle', field.id)"
30-
/>
31-
<label
32-
:for="`field-${field.id}`"
33-
class="text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
34-
>
35-
{{ field.name }}
36-
</label>
37-
</div>
98+
<div class="flex flex-col gap-3">
99+
<!-- Full-width Search -->
100+
<div class="relative w-full">
101+
<Input
102+
class="h-9 w-full"
103+
placeholder="Search fields..."
104+
:model-value="table.getColumn('name')?.getFilterValue() as string"
105+
@update:model-value="table.getColumn('name')?.setFilterValue($event)"
106+
autocomplete="off"
107+
name="field-search"
108+
/>
109+
</div>
110+
111+
<!-- Table Container -->
112+
<div class="rounded-md border shadow-xs">
113+
<DataTableSkeleton v-if="isLoading" :columns="3" :rows="5" />
114+
<DataTable v-else :table="table" class="border-none shadow-none" />
115+
</div>
116+
117+
<!-- Footer with Pagination and Summary -->
118+
<div class="flex items-center justify-between px-1">
119+
<div class="text-muted-foreground text-xs font-medium">
120+
{{ safeSelectedIds.length }} field(s) selected
38121
</div>
39-
</Transition>
122+
<CompactDataTablePagination v-if="table.getPageCount() > 1" :table="table" />
123+
</div>
40124
</div>
41125
</template>

frontend/src/shared/components/data/CompactDataTablePagination.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ defineProps<{
2020
<Icon icon="radix-icons:chevron-left" class="h-4 w-4" />
2121
</Button>
2222

23-
<span class="text-muted-foreground text-sm">
23+
<span class="text-muted-foreground text-xs">
2424
Page {{ table.getState().pagination.pageIndex + 1 }} of {{ table.getPageCount() }}
2525
</span>
2626

frontend/tsconfig.app.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
"noUnusedParameters": true,
1010
"noFallthroughCasesInSwitch": true,
1111
"noUncheckedSideEffectImports": true,
12-
"baseUrl": ".",
1312
"paths": {
1413
"@/*": ["./src/*"]
1514
}

frontend/vite.config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ export default defineConfig({
1111
server: {
1212
host: true,
1313
port: 5173,
14+
watch: {
15+
usePolling: true,
16+
},
1417
},
1518

1619
resolve: {

0 commit comments

Comments
 (0)