Added page layouts
This commit is contained in:
@@ -4,8 +4,8 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
import { useApi } from '@/composables/useApi'
|
||||
import { useFields, useViewState } from '@/composables/useFieldViews'
|
||||
import ListView from '@/components/views/ListView.vue'
|
||||
import DetailView from '@/components/views/DetailView.vue'
|
||||
import EditView from '@/components/views/EditView.vue'
|
||||
import DetailView from '@/components/views/DetailViewEnhanced.vue'
|
||||
import EditView from '@/components/views/EditViewEnhanced.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -273,6 +273,7 @@ onMounted(async () => {
|
||||
:config="detailConfig"
|
||||
:data="currentRecord"
|
||||
:loading="dataLoading"
|
||||
:object-id="objectDefinition?.id"
|
||||
@edit="handleEdit"
|
||||
@delete="() => handleDelete([currentRecord])"
|
||||
@back="handleBack"
|
||||
@@ -285,6 +286,7 @@ onMounted(async () => {
|
||||
:data="currentRecord || {}"
|
||||
:loading="dataLoading"
|
||||
:saving="saving"
|
||||
:object-id="objectDefinition?.id"
|
||||
@save="handleSaveRecord"
|
||||
@cancel="handleCancel"
|
||||
@back="handleBack"
|
||||
@@ -295,7 +297,6 @@ onMounted(async () => {
|
||||
|
||||
<style scoped>
|
||||
.object-view-container {
|
||||
min-height: 100vh;
|
||||
padding: 2rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -13,38 +13,119 @@
|
||||
|
||||
<h1 class="text-3xl font-bold mb-6">{{ object.label }}</h1>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-2xl font-semibold mb-4">Fields</h2>
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
v-for="field in object.fields"
|
||||
:key="field.id"
|
||||
class="p-4 border rounded-lg bg-card"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 class="font-semibold">{{ field.label }}</h3>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Type: {{ field.type }} | API Name: {{ field.apiName }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex gap-2 text-xs">
|
||||
<span
|
||||
v-if="field.isRequired"
|
||||
class="px-2 py-1 bg-destructive/10 text-destructive rounded"
|
||||
>
|
||||
Required
|
||||
</span>
|
||||
<span
|
||||
v-if="field.isUnique"
|
||||
class="px-2 py-1 bg-primary/10 text-primary rounded"
|
||||
>
|
||||
Unique
|
||||
</span>
|
||||
<Tabs v-model="activeTab" default-value="fields" class="w-full">
|
||||
<TabsList class="grid w-full grid-cols-2 max-w-md">
|
||||
<TabsTrigger value="fields">Fields</TabsTrigger>
|
||||
<TabsTrigger value="layouts">Page Layouts</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<!-- Fields Tab -->
|
||||
<TabsContent value="fields" class="mt-6">
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
v-for="field in object.fields"
|
||||
:key="field.id"
|
||||
class="p-4 border rounded-lg bg-card"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 class="font-semibold">{{ field.label }}</h3>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Type: {{ field.type }} | API Name: {{ field.apiName }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex gap-2 text-xs">
|
||||
<span
|
||||
v-if="field.isRequired"
|
||||
class="px-2 py-1 bg-destructive/10 text-destructive rounded"
|
||||
>
|
||||
Required
|
||||
</span>
|
||||
<span
|
||||
v-if="field.isUnique"
|
||||
class="px-2 py-1 bg-primary/10 text-primary rounded"
|
||||
>
|
||||
Unique
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<!-- Page Layouts Tab -->
|
||||
<TabsContent value="layouts" class="mt-6">
|
||||
<div v-if="!selectedLayout" class="space-y-4">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-xl font-semibold">Page Layouts</h2>
|
||||
<Button @click="handleCreateLayout">
|
||||
<Plus class="w-4 h-4 mr-2" />
|
||||
New Layout
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div v-if="loadingLayouts" class="text-center py-8">
|
||||
Loading layouts...
|
||||
</div>
|
||||
|
||||
<div v-else-if="layouts.length === 0" class="text-center py-8 text-muted-foreground">
|
||||
No page layouts yet. Create one to get started.
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-2">
|
||||
<div
|
||||
v-for="layout in layouts"
|
||||
:key="layout.id"
|
||||
class="p-4 border rounded-lg bg-card hover:border-primary cursor-pointer transition-colors"
|
||||
@click="handleSelectLayout(layout)"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 class="font-semibold">{{ layout.name }}</h3>
|
||||
<p v-if="layout.description" class="text-sm text-muted-foreground">
|
||||
{{ layout.description }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
v-if="layout.isDefault"
|
||||
class="px-2 py-1 bg-primary/10 text-primary rounded text-xs"
|
||||
>
|
||||
Default
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@click.stop="handleDeleteLayout(layout.id)"
|
||||
>
|
||||
<Trash2 class="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Layout Editor -->
|
||||
<div v-else>
|
||||
<div class="mb-4">
|
||||
<Button variant="outline" @click="selectedLayout = null">
|
||||
<ArrowLeft class="w-4 h-4 mr-2" />
|
||||
Back to Layouts
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<PageLayoutEditor
|
||||
:fields="object.fields"
|
||||
:initial-layout="(selectedLayout.layoutConfig || selectedLayout.layout_config)?.fields || []"
|
||||
:layout-name="selectedLayout.name"
|
||||
@save="handleSaveLayout"
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
@@ -53,12 +134,26 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Plus, Trash2, ArrowLeft } from 'lucide-vue-next'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import PageLayoutEditor from '@/components/PageLayoutEditor.vue'
|
||||
import type { PageLayout, FieldLayoutItem } from '~/types/page-layout'
|
||||
|
||||
const route = useRoute()
|
||||
const { api } = useApi()
|
||||
const { getPageLayouts, createPageLayout, updatePageLayout, deletePageLayout } = usePageLayouts()
|
||||
const { toast } = useToast()
|
||||
|
||||
const object = ref<any>(null)
|
||||
const loading = ref(true)
|
||||
const error = ref<string | null>(null)
|
||||
const activeTab = ref('fields')
|
||||
|
||||
// Page layouts state
|
||||
const layouts = ref<PageLayout[]>([])
|
||||
const loadingLayouts = ref(false)
|
||||
const selectedLayout = ref<PageLayout | null>(null)
|
||||
|
||||
const fetchObject = async () => {
|
||||
try {
|
||||
@@ -72,7 +167,92 @@ const fetchObject = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchObject()
|
||||
const fetchLayouts = async () => {
|
||||
if (!object.value) return
|
||||
|
||||
try {
|
||||
loadingLayouts.value = true
|
||||
layouts.value = await getPageLayouts(object.value.id)
|
||||
} catch (e: any) {
|
||||
console.error('Error fetching layouts:', e)
|
||||
toast.error('Failed to load page layouts')
|
||||
} finally {
|
||||
loadingLayouts.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleCreateLayout = async () => {
|
||||
const name = prompt('Enter a name for the new layout:')
|
||||
if (!name) return
|
||||
|
||||
try {
|
||||
const newLayout = await createPageLayout({
|
||||
name,
|
||||
objectId: object.value.id,
|
||||
isDefault: layouts.value.length === 0,
|
||||
layoutConfig: { fields: [] },
|
||||
})
|
||||
|
||||
layouts.value.push(newLayout)
|
||||
selectedLayout.value = newLayout
|
||||
toast.success('Layout created successfully')
|
||||
} catch (e: any) {
|
||||
console.error('Error creating layout:', e)
|
||||
toast.error('Failed to create layout')
|
||||
}
|
||||
}
|
||||
|
||||
const handleSelectLayout = (layout: PageLayout) => {
|
||||
selectedLayout.value = layout
|
||||
}
|
||||
|
||||
const handleSaveLayout = async (fields: FieldLayoutItem[]) => {
|
||||
if (!selectedLayout.value) return
|
||||
|
||||
try {
|
||||
const updated = await updatePageLayout(selectedLayout.value.id, {
|
||||
layoutConfig: { fields },
|
||||
})
|
||||
|
||||
// Update the layout in the list
|
||||
const index = layouts.value.findIndex(l => l.id === selectedLayout.value!.id)
|
||||
if (index !== -1) {
|
||||
layouts.value[index] = updated
|
||||
}
|
||||
|
||||
selectedLayout.value = updated
|
||||
toast.success('Layout saved successfully')
|
||||
} catch (e: any) {
|
||||
console.error('Error saving layout:', e)
|
||||
toast.error('Failed to save layout')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteLayout = async (layoutId: string) => {
|
||||
if (!confirm('Are you sure you want to delete this layout?')) return
|
||||
|
||||
try {
|
||||
await deletePageLayout(layoutId)
|
||||
layouts.value = layouts.value.filter(l => l.id !== layoutId)
|
||||
toast.success('Layout deleted successfully')
|
||||
} catch (e: any) {
|
||||
console.error('Error deleting layout:', e)
|
||||
toast.error('Failed to delete layout')
|
||||
}
|
||||
}
|
||||
|
||||
// Watch for tab changes to load layouts
|
||||
watch(activeTab, (newTab) => {
|
||||
if (newTab === 'layouts' && layouts.value.length === 0 && !loadingLayouts.value) {
|
||||
fetchLayouts()
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchObject()
|
||||
// If we start on layouts tab, load them
|
||||
if (activeTab.value === 'layouts') {
|
||||
await fetchLayouts()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user