Added auth functionality, initial work with views and field types
This commit is contained in:
202
frontend/components/fields/FieldRenderer.vue
Normal file
202
frontend/components/fields/FieldRenderer.vue
Normal file
@@ -0,0 +1,202 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { DatePicker } from '@/components/ui/date-picker'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { FieldConfig, FieldType, ViewMode } from '@/types/field-types'
|
||||
import { Label } from '@/components/ui/label'
|
||||
|
||||
interface Props {
|
||||
field: FieldConfig
|
||||
modelValue: any
|
||||
mode: ViewMode
|
||||
readonly?: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: any]
|
||||
}>()
|
||||
|
||||
const value = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val),
|
||||
})
|
||||
|
||||
const isReadOnly = computed(() => props.readonly || props.field.isReadOnly || props.mode === ViewMode.DETAIL)
|
||||
const isEditMode = computed(() => props.mode === ViewMode.EDIT)
|
||||
const isListMode = computed(() => props.mode === ViewMode.LIST)
|
||||
const isDetailMode = computed(() => props.mode === ViewMode.DETAIL)
|
||||
|
||||
const formatValue = (val: any): string => {
|
||||
if (val === null || val === undefined) return '-'
|
||||
|
||||
switch (props.field.type) {
|
||||
case FieldType.DATE:
|
||||
return val instanceof Date ? val.toLocaleDateString() : new Date(val).toLocaleDateString()
|
||||
case FieldType.DATETIME:
|
||||
return val instanceof Date ? val.toLocaleString() : new Date(val).toLocaleString()
|
||||
case FieldType.BOOLEAN:
|
||||
return val ? 'Yes' : 'No'
|
||||
case FieldType.CURRENCY:
|
||||
return `${props.field.prefix || '$'}${Number(val).toFixed(2)}${props.field.suffix || ''}`
|
||||
case FieldType.SELECT:
|
||||
const option = props.field.options?.find(opt => opt.value === val)
|
||||
return option?.label || val
|
||||
case FieldType.MULTI_SELECT:
|
||||
if (!Array.isArray(val)) return '-'
|
||||
return val.map(v => {
|
||||
const opt = props.field.options?.find(o => o.value === v)
|
||||
return opt?.label || v
|
||||
}).join(', ')
|
||||
default:
|
||||
return String(val)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="field-renderer space-y-2">
|
||||
<!-- Label (shown in edit and detail modes) -->
|
||||
<Label v-if="!isListMode" :for="field.id" class="flex items-center gap-2">
|
||||
{{ field.label }}
|
||||
<span v-if="field.isRequired && isEditMode" class="text-destructive">*</span>
|
||||
</Label>
|
||||
|
||||
<!-- Help Text -->
|
||||
<p v-if="field.helpText && !isListMode" class="text-sm text-muted-foreground">
|
||||
{{ field.helpText }}
|
||||
</p>
|
||||
|
||||
<!-- List View - Simple text display -->
|
||||
<div v-if="isListMode" class="text-sm truncate">
|
||||
<Badge v-if="field.type === FieldType.BOOLEAN" :variant="value ? 'default' : 'secondary'">
|
||||
{{ formatValue(value) }}
|
||||
</Badge>
|
||||
<template v-else>
|
||||
{{ formatValue(value) }}
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Detail View - Formatted display -->
|
||||
<div v-else-if="isDetailMode" class="space-y-1">
|
||||
<div v-if="field.type === FieldType.BOOLEAN" class="flex items-center gap-2">
|
||||
<Checkbox :checked="value" disabled />
|
||||
<span class="text-sm">{{ formatValue(value) }}</span>
|
||||
</div>
|
||||
<div v-else-if="field.type === FieldType.MULTI_SELECT" class="flex flex-wrap gap-2">
|
||||
<Badge v-for="(item, idx) in value" :key="idx" variant="secondary">
|
||||
{{ props.field.options?.find(opt => opt.value === item)?.label || item }}
|
||||
</Badge>
|
||||
</div>
|
||||
<div v-else-if="field.type === FieldType.URL && value" class="text-sm">
|
||||
<a :href="value" target="_blank" class="text-primary hover:underline">
|
||||
{{ value }}
|
||||
</a>
|
||||
</div>
|
||||
<div v-else-if="field.type === FieldType.EMAIL && value" class="text-sm">
|
||||
<a :href="`mailto:${value}`" class="text-primary hover:underline">
|
||||
{{ value }}
|
||||
</a>
|
||||
</div>
|
||||
<div v-else-if="field.type === FieldType.MARKDOWN && value" class="prose prose-sm">
|
||||
<div v-html="value" />
|
||||
</div>
|
||||
<div v-else class="text-sm font-medium">
|
||||
{{ formatValue(value) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit View - Input components -->
|
||||
<div v-else-if="isEditMode && !isReadOnly">
|
||||
<!-- Text Input -->
|
||||
<Input
|
||||
v-if="[FieldType.TEXT, FieldType.EMAIL, FieldType.URL, FieldType.PASSWORD].includes(field.type)"
|
||||
:id="field.id"
|
||||
v-model="value"
|
||||
:type="field.type === FieldType.PASSWORD ? 'password' : field.type === FieldType.EMAIL ? 'email' : field.type === FieldType.URL ? 'url' : 'text'"
|
||||
:placeholder="field.placeholder"
|
||||
:required="field.isRequired"
|
||||
:disabled="field.isReadOnly"
|
||||
/>
|
||||
|
||||
<!-- Textarea -->
|
||||
<Textarea
|
||||
v-else-if="field.type === FieldType.TEXTAREA || field.type === FieldType.MARKDOWN"
|
||||
:id="field.id"
|
||||
v-model="value"
|
||||
:placeholder="field.placeholder"
|
||||
:rows="field.rows || 4"
|
||||
:required="field.isRequired"
|
||||
:disabled="field.isReadOnly"
|
||||
/>
|
||||
|
||||
<!-- Number Input -->
|
||||
<Input
|
||||
v-else-if="[FieldType.NUMBER, FieldType.CURRENCY].includes(field.type)"
|
||||
:id="field.id"
|
||||
v-model.number="value"
|
||||
type="number"
|
||||
:placeholder="field.placeholder"
|
||||
:min="field.min"
|
||||
:max="field.max"
|
||||
:step="field.step || (field.type === FieldType.CURRENCY ? 0.01 : 1)"
|
||||
:required="field.isRequired"
|
||||
:disabled="field.isReadOnly"
|
||||
/>
|
||||
|
||||
<!-- Select -->
|
||||
<Select v-else-if="field.type === FieldType.SELECT" v-model="value">
|
||||
<SelectTrigger :id="field.id">
|
||||
<SelectValue :placeholder="field.placeholder || 'Select an option'" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem v-for="option in field.options" :key="String(option.value)" :value="String(option.value)">
|
||||
{{ option.label }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<!-- Boolean - Checkbox -->
|
||||
<div v-else-if="field.type === FieldType.BOOLEAN" class="flex items-center gap-2">
|
||||
<Checkbox :id="field.id" v-model:checked="value" :disabled="field.isReadOnly" />
|
||||
<Label :for="field.id" class="text-sm font-normal cursor-pointer">
|
||||
{{ field.placeholder || field.label }}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<!-- Date Picker -->
|
||||
<DatePicker
|
||||
v-else-if="[FieldType.DATE, FieldType.DATETIME].includes(field.type)"
|
||||
v-model="value"
|
||||
:placeholder="field.placeholder"
|
||||
:disabled="field.isReadOnly"
|
||||
/>
|
||||
|
||||
<!-- Fallback -->
|
||||
<Input
|
||||
v-else
|
||||
:id="field.id"
|
||||
v-model="value"
|
||||
:placeholder="field.placeholder"
|
||||
:required="field.isRequired"
|
||||
:disabled="field.isReadOnly"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Read-only Edit View -->
|
||||
<div v-else-if="isEditMode && isReadOnly" class="text-sm text-muted-foreground">
|
||||
{{ formatValue(value) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.field-renderer {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user