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
41 changes: 26 additions & 15 deletions frontend/src/modules/events/components/EventForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -53,18 +53,6 @@ const { handleSubmit, values, setValues, setFieldValue } = useForm<EventFormValu
validationSchema: toTypedSchema(eventSchema),
})

function toggleFieldSelection(id: number) {
const current = values.fields || []
if (current.includes(id)) {
setFieldValue(
'fields',
current.filter(f => f !== id)
)
} else {
setFieldValue('fields', [...current, id])
}
}

watchEffect(() => {
if (props.event) {
setValues({
Expand Down Expand Up @@ -230,16 +218,39 @@ function removeTag(tagId: string) {
</FormField>

<!-- Linked Fields -->
<FormField name="fields">
<FormField name="fields" v-slot="{ componentField }">
<FormItem>
<FormLabel>Linked Fields</FormLabel>
<FormDescription>Choose one or more fields this event uses.</FormDescription>
<FormControl>
<LinkedFieldsSelector
:fields="availableFields"
:selected-ids="values.fields"
:selected-ids="componentField.modelValue || []"
:is-loading="isLoadingFields"
@toggle="toggleFieldSelection"
@toggle="
(id: number) => {
const current: number[] = componentField.modelValue || []
const updated = current.includes(id)
? current.filter((f: number) => f !== id)
: [...current, id]
componentField.onChange(updated)
}
"
@toggle-all="
(ids: number[], selected: boolean) => {
const current: number[] = componentField.modelValue || []
let updated: number[]
if (selected) {
// Add all missing IDs
const toAdd = ids.filter(id => !current.includes(id))
updated = [...current, ...toAdd]
} else {
// Remove all provided IDs
updated = current.filter(id => !ids.includes(id))
}
componentField.onChange(updated)
}
"
/>
</FormControl>
<FormMessage />
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/modules/events/pages/EventCreatePage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const onSubmit = (values: EventFormValues) => {
<div>
<Header title="Create new event" backLink fallbackBackLink="/events" />

<Card class="mx-auto max-w-md">
<Card class="mx-auto max-w-lg">
<CardContent>
<EventForm
:availableFields="fields ?? []"
Expand Down
134 changes: 109 additions & 25 deletions frontend/src/modules/fields/components/LinkedFieldsSelector.vue
Original file line number Diff line number Diff line change
@@ -1,41 +1,125 @@
<script setup lang="ts">
import { h, computed } from 'vue'
import type { Field } from '@/modules/fields/types'
import Skeleton from '@/shared/ui/skeleton/Skeleton.vue'
import Checkbox from '@/shared/ui/checkbox/Checkbox.vue'
import type { ColumnDef } from '@tanstack/vue-table'
import { Checkbox } from '@/shared/ui/checkbox'
import { Input } from '@/shared/ui/input'
import { useDataTable } from '@/shared/composables/useDataTable'
import DataTable from '@/shared/components/data/DataTable.vue'
import DataTableSkeleton from '@/shared/components/skeletons/DataTableSkeleton.vue'
import CompactDataTablePagination from '@/shared/components/data/CompactDataTablePagination.vue'
import DataTableColumnHeader from '@/shared/components/data/DataTableColumnHeader.vue'

defineProps<{
const props = defineProps<{
fields: Field[]
selectedIds: number[]
selectedIds?: number[]
isLoading?: boolean
}>()

const emit = defineEmits<{
(e: 'toggle', id: number): void
(e: 'toggleAll', ids: number[], selected: boolean): void
}>()

const safeSelectedIds = computed(() => props.selectedIds || [])

const columns = computed<ColumnDef<Field>[]>(() => [
{
accessorKey: 'id',
enableHiding: false,
meta: {
class: 'w-[6ch] text-center',
headerClass: 'w-[6ch] text-center',
},
header: ({ column }) =>
h(DataTableColumnHeader<Field, unknown>, {
column,
title: 'ID',
align: 'center',
}),
cell: ({ row }) => h('div', { class: 'text-center font-medium' }, row.original.id),
},
{
accessorKey: 'name',
enableHiding: false,
header: ({ column }) => h(DataTableColumnHeader<Field, unknown>, { column, title: 'Name' }),
cell: ({ row }) =>
h(
'div',
{
class: 'truncate whitespace-nowrap overflow-hidden text-left font-medium',
title: row.original.name,
},
row.original.name
),
},
{
id: 'selection',
enableHiding: false,
header: ({ table }) => {
const rows = table.getFilteredRowModel().rows
const allIds = rows.map(r => r.original.id)
const isAllSelected =
allIds.length > 0 && allIds.every(id => safeSelectedIds.value.includes(id))
const isSomeSelected = allIds.some(id => safeSelectedIds.value.includes(id))

return h(
'div',
{ class: 'flex justify-end pr-6' },
h(Checkbox, {
modelValue: isAllSelected || (isSomeSelected && 'indeterminate'),
'onUpdate:modelValue': (value: boolean | 'indeterminate') =>
emit('toggleAll', allIds, value === true),
ariaLabel: 'Select all',
})
)
},
cell: ({ row }) =>
h(
'div',
{ class: 'flex justify-end pr-6' },
h(Checkbox, {
modelValue: safeSelectedIds.value.includes(row.original.id),
'onUpdate:modelValue': () => emit('toggle', row.original.id),
ariaLabel: 'Select field',
})
),
},
])

const { table } = useDataTable({
data: () => props.fields,
columns: () => columns.value,
defaultPageSize: 5,
})
</script>

<template>
<div class="max-h-24 space-y-2 overflow-y-auto rounded-md border p-4 shadow-xs">
<template v-if="isLoading">
<Skeleton v-for="i in 4" :key="i" class="h-5 w-[70%] rounded-md shadow-xs" />
</template>

<Transition name="fade" appear>
<div v-if="!isLoading" class="space-y-2">
<div v-for="field in fields" :key="field.id" class="flex items-center gap-2">
<Checkbox
:id="`field-${field.id}`"
:model-value="selectedIds.includes(field.id)"
@update:model-value="() => emit('toggle', field.id)"
/>
<label
:for="`field-${field.id}`"
class="text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
{{ field.name }}
</label>
</div>
<div class="flex flex-col gap-3">
<!-- Full-width Search -->
<div class="relative w-full">
<Input
class="h-9 w-full"
placeholder="Search fields..."
:model-value="table.getColumn('name')?.getFilterValue() as string"
@update:model-value="table.getColumn('name')?.setFilterValue($event)"
autocomplete="off"
name="field-search"
/>
</div>

<!-- Table Container -->
<div class="rounded-md border shadow-xs">
<DataTableSkeleton v-if="isLoading" :columns="3" :rows="5" />
<DataTable v-else :table="table" class="border-none shadow-none" />
</div>

<!-- Footer with Pagination and Summary -->
<div class="flex items-center justify-between px-1">
<div class="text-muted-foreground text-xs font-medium">
{{ safeSelectedIds.length }} field(s) selected
</div>
</Transition>
<CompactDataTablePagination v-if="table.getPageCount() > 1" :table="table" />
</div>
</div>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ defineProps<{
<Icon icon="radix-icons:chevron-left" class="h-4 w-4" />
</Button>

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

Expand Down
1 change: 0 additions & 1 deletion frontend/tsconfig.app.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
Expand Down
3 changes: 3 additions & 0 deletions frontend/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ export default defineConfig({
server: {
host: true,
port: 5173,
watch: {
usePolling: true,
},
},

resolve: {
Expand Down
Loading