WIP - configure list views
This commit is contained in:
264
frontend/components/ListViewLayoutEditor.vue
Normal file
264
frontend/components/ListViewLayoutEditor.vue
Normal file
@@ -0,0 +1,264 @@
|
||||
<template>
|
||||
<div class="list-view-layout-editor">
|
||||
<div class="flex h-full">
|
||||
<!-- Selected Fields Area -->
|
||||
<div class="flex-1 p-4 overflow-auto">
|
||||
<div class="mb-4 flex justify-between items-center">
|
||||
<h3 class="text-lg font-semibold">{{ layoutName || 'List View Layout' }}</h3>
|
||||
<div class="flex gap-2">
|
||||
<Button variant="outline" size="sm" @click="handleClear">
|
||||
Clear All
|
||||
</Button>
|
||||
<Button size="sm" @click="handleSave">
|
||||
Save Layout
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border rounded-lg bg-slate-50 dark:bg-slate-900 p-4 min-h-[400px]">
|
||||
<p class="text-sm text-muted-foreground mb-4">
|
||||
Drag fields to reorder them. Fields will appear in the list view in this order.
|
||||
</p>
|
||||
|
||||
<div v-if="selectedFields.length === 0" class="text-center py-8 text-muted-foreground">
|
||||
<p>No fields selected.</p>
|
||||
<p class="text-sm">Click or drag fields from the right panel to add them.</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
ref="sortableContainer"
|
||||
class="space-y-2"
|
||||
>
|
||||
<div
|
||||
v-for="(field, index) in selectedFields"
|
||||
:key="field.id"
|
||||
class="p-3 border rounded cursor-move bg-white dark:bg-slate-800 hover:border-primary transition-colors flex items-center justify-between"
|
||||
draggable="true"
|
||||
@dragstart="handleDragStart($event, index)"
|
||||
@dragover.prevent="handleDragOver($event, index)"
|
||||
@drop="handleDrop($event, index)"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-muted-foreground cursor-grab">
|
||||
<GripVertical class="w-4 h-4" />
|
||||
</span>
|
||||
<div>
|
||||
<div class="font-medium text-sm">{{ field.label }}</div>
|
||||
<div class="text-xs text-muted-foreground">
|
||||
{{ field.apiName }} • {{ formatFieldType(field.type) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="text-destructive hover:text-destructive"
|
||||
@click="removeField(field.id)"
|
||||
>
|
||||
<X class="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Available Fields Sidebar -->
|
||||
<div class="w-80 border-l bg-white dark:bg-slate-950 p-4 overflow-auto">
|
||||
<h3 class="text-lg font-semibold mb-4">Available Fields</h3>
|
||||
<p class="text-xs text-muted-foreground mb-4">Click or drag to add field to list</p>
|
||||
|
||||
<div v-if="availableFields.length === 0" class="text-center py-4 text-muted-foreground text-sm">
|
||||
All fields have been added to the layout.
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-2">
|
||||
<div
|
||||
v-for="field in availableFields"
|
||||
:key="field.id"
|
||||
class="p-3 border rounded cursor-pointer bg-white dark:bg-slate-900 hover:border-primary hover:bg-slate-50 dark:hover:bg-slate-800 transition-colors"
|
||||
draggable="true"
|
||||
@dragstart="handleAvailableFieldDragStart($event, field)"
|
||||
@click="addField(field)"
|
||||
>
|
||||
<div class="font-medium text-sm">{{ field.label }}</div>
|
||||
<div class="text-xs text-muted-foreground">
|
||||
{{ field.apiName }} • {{ formatFieldType(field.type) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { GripVertical, X } from 'lucide-vue-next'
|
||||
import type { FieldLayoutItem } from '~/types/page-layout'
|
||||
import type { FieldConfig } from '~/types/field-types'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
const props = defineProps<{
|
||||
fields: FieldConfig[]
|
||||
initialLayout?: FieldLayoutItem[]
|
||||
layoutName?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
save: [layout: { fields: FieldLayoutItem[] }]
|
||||
}>()
|
||||
|
||||
// Selected fields in order
|
||||
const selectedFieldIds = ref<string[]>([])
|
||||
const draggedIndex = ref<number | null>(null)
|
||||
const draggedAvailableField = ref<FieldConfig | null>(null)
|
||||
|
||||
// Initialize with initial layout
|
||||
watch(() => props.initialLayout, (layout) => {
|
||||
if (layout && layout.length > 0) {
|
||||
// Sort by order if available, otherwise use array order
|
||||
const sorted = [...layout].sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
|
||||
selectedFieldIds.value = sorted.map(item => item.fieldId)
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// Computed selected fields in order
|
||||
const selectedFields = computed(() => {
|
||||
return selectedFieldIds.value
|
||||
.map(id => props.fields.find(f => f.id === id))
|
||||
.filter((f): f is FieldConfig => f !== undefined)
|
||||
})
|
||||
|
||||
// Available fields (not selected)
|
||||
const availableFields = computed(() => {
|
||||
const selectedSet = new Set(selectedFieldIds.value)
|
||||
return props.fields.filter(field => !selectedSet.has(field.id))
|
||||
})
|
||||
|
||||
const formatFieldType = (type: string): string => {
|
||||
const typeNames: Record<string, string> = {
|
||||
'TEXT': 'Text',
|
||||
'LONG_TEXT': 'Textarea',
|
||||
'EMAIL': 'Email',
|
||||
'PHONE': 'Phone',
|
||||
'NUMBER': 'Number',
|
||||
'CURRENCY': 'Currency',
|
||||
'PERCENT': 'Percent',
|
||||
'PICKLIST': 'Picklist',
|
||||
'MULTI_PICKLIST': 'Multi-select',
|
||||
'BOOLEAN': 'Checkbox',
|
||||
'DATE': 'Date',
|
||||
'DATE_TIME': 'DateTime',
|
||||
'TIME': 'Time',
|
||||
'URL': 'URL',
|
||||
'LOOKUP': 'Lookup',
|
||||
'FILE': 'File',
|
||||
'IMAGE': 'Image',
|
||||
'JSON': 'JSON',
|
||||
'text': 'Text',
|
||||
'textarea': 'Textarea',
|
||||
'email': 'Email',
|
||||
'number': 'Number',
|
||||
'currency': 'Currency',
|
||||
'select': 'Picklist',
|
||||
'multiSelect': 'Multi-select',
|
||||
'boolean': 'Checkbox',
|
||||
'date': 'Date',
|
||||
'datetime': 'DateTime',
|
||||
'url': 'URL',
|
||||
'lookup': 'Lookup',
|
||||
'belongsTo': 'Lookup',
|
||||
}
|
||||
return typeNames[type] || type
|
||||
}
|
||||
|
||||
const addField = (field: FieldConfig) => {
|
||||
if (!selectedFieldIds.value.includes(field.id)) {
|
||||
selectedFieldIds.value.push(field.id)
|
||||
}
|
||||
}
|
||||
|
||||
const removeField = (fieldId: string) => {
|
||||
selectedFieldIds.value = selectedFieldIds.value.filter(id => id !== fieldId)
|
||||
}
|
||||
|
||||
// Drag and drop for reordering
|
||||
const handleDragStart = (event: DragEvent, index: number) => {
|
||||
draggedIndex.value = index
|
||||
draggedAvailableField.value = null
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.effectAllowed = 'move'
|
||||
}
|
||||
}
|
||||
|
||||
const handleDragOver = (event: DragEvent, index: number) => {
|
||||
event.preventDefault()
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.dropEffect = 'move'
|
||||
}
|
||||
}
|
||||
|
||||
const handleDrop = (event: DragEvent, targetIndex: number) => {
|
||||
event.preventDefault()
|
||||
|
||||
// Handle drop from available fields
|
||||
if (draggedAvailableField.value) {
|
||||
addField(draggedAvailableField.value)
|
||||
// Move the newly added field to the target position
|
||||
const newFieldId = draggedAvailableField.value.id
|
||||
const currentIndex = selectedFieldIds.value.indexOf(newFieldId)
|
||||
if (currentIndex !== -1 && currentIndex !== targetIndex) {
|
||||
const ids = [...selectedFieldIds.value]
|
||||
ids.splice(currentIndex, 1)
|
||||
ids.splice(targetIndex, 0, newFieldId)
|
||||
selectedFieldIds.value = ids
|
||||
}
|
||||
draggedAvailableField.value = null
|
||||
return
|
||||
}
|
||||
|
||||
// Handle reordering within selected fields
|
||||
if (draggedIndex.value === null || draggedIndex.value === targetIndex) {
|
||||
draggedIndex.value = null
|
||||
return
|
||||
}
|
||||
|
||||
const ids = [...selectedFieldIds.value]
|
||||
const [removed] = ids.splice(draggedIndex.value, 1)
|
||||
ids.splice(targetIndex, 0, removed)
|
||||
selectedFieldIds.value = ids
|
||||
draggedIndex.value = null
|
||||
}
|
||||
|
||||
// Drag from available fields
|
||||
const handleAvailableFieldDragStart = (event: DragEvent, field: FieldConfig) => {
|
||||
draggedAvailableField.value = field
|
||||
draggedIndex.value = null
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.effectAllowed = 'copy'
|
||||
}
|
||||
}
|
||||
|
||||
const handleClear = () => {
|
||||
if (confirm('Are you sure you want to clear all fields from the layout?')) {
|
||||
selectedFieldIds.value = []
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
const layout: FieldLayoutItem[] = selectedFieldIds.value.map((fieldId, index) => ({
|
||||
fieldId,
|
||||
order: index,
|
||||
}))
|
||||
|
||||
emit('save', { fields: layout })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.list-view-layout-editor {
|
||||
height: calc(100vh - 300px);
|
||||
min-height: 500px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user