Added auth functionality, initial work with views and field types
This commit is contained in:
234
frontend/components/views/ListView.vue
Normal file
234
frontend/components/views/ListView.vue
Normal file
@@ -0,0 +1,234 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import FieldRenderer from '@/components/fields/FieldRenderer.vue'
|
||||
import { ListViewConfig, ViewMode, FieldType } from '@/types/field-types'
|
||||
import { ChevronDown, ChevronUp, Search, Plus, Download, Trash2, Edit } from 'lucide-vue-next'
|
||||
|
||||
interface Props {
|
||||
config: ListViewConfig
|
||||
data?: any[]
|
||||
loading?: boolean
|
||||
selectable?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
data: () => [],
|
||||
loading: false,
|
||||
selectable: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'row-click': [row: any]
|
||||
'row-select': [rows: any[]]
|
||||
'create': []
|
||||
'edit': [row: any]
|
||||
'delete': [rows: any[]]
|
||||
'action': [actionId: string, rows: any[]]
|
||||
'sort': [field: string, direction: 'asc' | 'desc']
|
||||
'search': [query: string]
|
||||
'refresh': []
|
||||
}>()
|
||||
|
||||
// State
|
||||
const selectedRows = ref<Set<string>>(new Set())
|
||||
const searchQuery = ref('')
|
||||
const sortField = ref<string>('')
|
||||
const sortDirection = ref<'asc' | 'desc'>('asc')
|
||||
|
||||
// Computed
|
||||
const visibleFields = computed(() =>
|
||||
props.config.fields.filter(f => f.showOnList !== false)
|
||||
)
|
||||
|
||||
const allSelected = computed({
|
||||
get: () => props.data.length > 0 && selectedRows.value.size === props.data.length,
|
||||
set: (val: boolean) => {
|
||||
if (val) {
|
||||
selectedRows.value = new Set(props.data.map(row => row.id))
|
||||
} else {
|
||||
selectedRows.value.clear()
|
||||
}
|
||||
emit('row-select', getSelectedRows())
|
||||
},
|
||||
})
|
||||
|
||||
const getSelectedRows = () => {
|
||||
return props.data.filter(row => selectedRows.value.has(row.id))
|
||||
}
|
||||
|
||||
const toggleRowSelection = (rowId: string) => {
|
||||
if (selectedRows.value.has(rowId)) {
|
||||
selectedRows.value.delete(rowId)
|
||||
} else {
|
||||
selectedRows.value.add(rowId)
|
||||
}
|
||||
emit('row-select', getSelectedRows())
|
||||
}
|
||||
|
||||
const handleSort = (field: string) => {
|
||||
if (sortField.value === field) {
|
||||
sortDirection.value = sortDirection.value === 'asc' ? 'desc' : 'asc'
|
||||
} else {
|
||||
sortField.value = field
|
||||
sortDirection.value = 'asc'
|
||||
}
|
||||
emit('sort', field, sortDirection.value)
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
emit('search', searchQuery.value)
|
||||
}
|
||||
|
||||
const handleAction = (actionId: string) => {
|
||||
emit('action', actionId, getSelectedRows())
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="list-view space-y-4">
|
||||
<!-- Toolbar -->
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<!-- Search -->
|
||||
<div v-if="config.searchable !== false" class="flex-1 max-w-sm">
|
||||
<div class="relative">
|
||||
<Search class="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
v-model="searchQuery"
|
||||
placeholder="Search..."
|
||||
class="pl-8"
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- Bulk Actions -->
|
||||
<template v-if="selectedRows.size > 0">
|
||||
<Badge variant="secondary" class="px-3 py-1">
|
||||
{{ selectedRows.size }} selected
|
||||
</Badge>
|
||||
<Button variant="outline" size="sm" @click="emit('delete', getSelectedRows())">
|
||||
<Trash2 class="h-4 w-4 mr-2" />
|
||||
Delete
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<!-- Custom Actions -->
|
||||
<Button
|
||||
v-for="action in config.actions"
|
||||
:key="action.id"
|
||||
:variant="action.variant || 'outline'"
|
||||
size="sm"
|
||||
@click="handleAction(action.id)"
|
||||
>
|
||||
{{ action.label }}
|
||||
</Button>
|
||||
|
||||
<!-- Export -->
|
||||
<Button v-if="config.exportable" variant="outline" size="sm">
|
||||
<Download class="h-4 w-4 mr-2" />
|
||||
Export
|
||||
</Button>
|
||||
|
||||
<!-- Create -->
|
||||
<Button size="sm" @click="emit('create')">
|
||||
<Plus class="h-4 w-4 mr-2" />
|
||||
New
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="border rounded-lg">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead v-if="selectable" class="w-12">
|
||||
<Checkbox v-model:checked="allSelected" />
|
||||
</TableHead>
|
||||
<TableHead
|
||||
v-for="field in visibleFields"
|
||||
:key="field.id"
|
||||
:class="{ 'cursor-pointer hover:bg-muted/50': field.sortable !== false }"
|
||||
@click="field.sortable !== false && handleSort(field.apiName)"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
{{ field.label }}
|
||||
<template v-if="field.sortable !== false && sortField === field.apiName">
|
||||
<ChevronUp v-if="sortDirection === 'asc'" class="h-4 w-4" />
|
||||
<ChevronDown v-else class="h-4 w-4" />
|
||||
</template>
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead class="w-20">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow v-if="loading">
|
||||
<TableCell :colspan="visibleFields.length + (selectable ? 2 : 1)" class="text-center py-8">
|
||||
<div class="flex items-center justify-center">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow v-else-if="data.length === 0">
|
||||
<TableCell :colspan="visibleFields.length + (selectable ? 2 : 1)" class="text-center py-8 text-muted-foreground">
|
||||
No records found
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow
|
||||
v-else
|
||||
v-for="row in data"
|
||||
:key="row.id"
|
||||
class="cursor-pointer hover:bg-muted/50"
|
||||
@click="emit('row-click', row)"
|
||||
>
|
||||
<TableCell v-if="selectable" @click.stop>
|
||||
<Checkbox
|
||||
:checked="selectedRows.has(row.id)"
|
||||
@update:checked="toggleRowSelection(row.id)"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell v-for="field in visibleFields" :key="field.id">
|
||||
<FieldRenderer
|
||||
:field="field"
|
||||
:model-value="row[field.apiName]"
|
||||
:mode="ViewMode.LIST"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell @click.stop>
|
||||
<div class="flex items-center gap-1">
|
||||
<Button variant="ghost" size="sm" @click="emit('edit', row)">
|
||||
<Edit class="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" @click="emit('delete', [row])">
|
||||
<Trash2 class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination would go here -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.list-view {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user