AW: theCure
This commit is contained in:
102
frontend/components/DynamicForm.vue
Normal file
102
frontend/components/DynamicForm.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
type FieldDef = {
|
||||
name: string
|
||||
label?: string
|
||||
type?: string
|
||||
required?: boolean
|
||||
options?: Array<{ label: string; value: string }>
|
||||
}
|
||||
|
||||
type LayoutSection = {
|
||||
title?: string
|
||||
fields: string[]
|
||||
}
|
||||
|
||||
type FormLayout = {
|
||||
sections: LayoutSection[]
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
layout: FormLayout
|
||||
fieldsMeta?: Record<string, FieldDef>
|
||||
modelValue?: Record<string, any>
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', v: Record<string, any>): void
|
||||
(e: 'submit', v: Record<string, any>): void
|
||||
}>()
|
||||
|
||||
const state = ref<Record<string, any>>({ ...(props.modelValue || {}) })
|
||||
|
||||
watch(() => props.modelValue, (v) => {
|
||||
if (v) state.value = { ...v }
|
||||
})
|
||||
|
||||
function updateField(name: string, value: any) {
|
||||
state.value[name] = value
|
||||
emit('update:modelValue', state.value)
|
||||
}
|
||||
|
||||
function onSubmit() {
|
||||
emit('submit', state.value)
|
||||
}
|
||||
|
||||
const resolvedLabel = (name: string) => props.fieldsMeta?.[name]?.label || name
|
||||
const resolvedType = (name: string) => props.fieldsMeta?.[name]?.type || 'text'
|
||||
const isRequired = (name: string) => !!props.fieldsMeta?.[name]?.required
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form class="space-y-6" @submit.prevent="onSubmit">
|
||||
<div v-for="(section, i) in layout.sections" :key="i" class="space-y-4">
|
||||
<h3 v-if="section.title" class="text-lg font-semibold">{{ section.title }}</h3>
|
||||
<div class="grid md:grid-cols-2 gap-4">
|
||||
<div v-for="field in section.fields" :key="field" class="space-y-1">
|
||||
<label class="text-sm font-medium">{{ resolvedLabel(field) }}</label>
|
||||
|
||||
<UInput
|
||||
v-if="['text','string','number'].includes(resolvedType(field))"
|
||||
:type="resolvedType(field) === 'number' ? 'number' : 'text'"
|
||||
v-model="state[field]"
|
||||
:required="isRequired(field)"
|
||||
@update:model-value="v => updateField(field, v)"
|
||||
/>
|
||||
|
||||
<UTextarea
|
||||
v-else-if="['text','textarea'].includes(resolvedType(field))"
|
||||
v-model="state[field]"
|
||||
:required="isRequired(field)"
|
||||
@update:model-value="v => updateField(field, v)"
|
||||
/>
|
||||
|
||||
<USelect
|
||||
v-else-if="resolvedType(field) === 'select'"
|
||||
:options="props.fieldsMeta?.[field]?.options || []"
|
||||
v-model="state[field]"
|
||||
@update:model-value="v => updateField(field, v)"
|
||||
/>
|
||||
|
||||
<UCheckbox
|
||||
v-else-if="resolvedType(field) === 'checkbox'"
|
||||
v-model="state[field]"
|
||||
@update:model-value="v => updateField(field, v)"
|
||||
/>
|
||||
|
||||
<UInput
|
||||
v-else
|
||||
type="text"
|
||||
v-model="state[field]"
|
||||
@update:model-value="v => updateField(field, v)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<UButton type="submit">Submit</UButton>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
6
frontend/nuxt.config.ts
Normal file
6
frontend/nuxt.config.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export default defineNuxtConfig({
|
||||
modules: ['@nuxt/ui'],
|
||||
compatibilityDate: '2024-10-01',
|
||||
devtools: { enabled: true },
|
||||
typescript: { strict: true }
|
||||
})
|
||||
14
frontend/package.json
Normal file
14
frontend/package.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "codeless-platform-frontend",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "nuxt dev",
|
||||
"build": "nuxt build",
|
||||
"start": "nuxt start"
|
||||
},
|
||||
"dependencies": {
|
||||
"nuxt": "^4.0.0-rc.0",
|
||||
"@nuxt/ui": "^2.12.0"
|
||||
}
|
||||
}
|
||||
8
frontend/pages/admin/apps.vue
Normal file
8
frontend/pages/admin/apps.vue
Normal file
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<div class="p-6">
|
||||
<UCard>
|
||||
<template #header><h2 class="text-xl font-semibold">Admin / Apps</h2></template>
|
||||
<p>Scaffold placeholder. Add CRUD UI for applications.</p>
|
||||
</UCard>
|
||||
</div>
|
||||
</template>
|
||||
8
frontend/pages/admin/entities/[id]/fields.vue
Normal file
8
frontend/pages/admin/entities/[id]/fields.vue
Normal file
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<div class="p-6">
|
||||
<UCard>
|
||||
<template #header><h2 class="text-xl font-semibold">Admin / Entity Fields</h2></template>
|
||||
<p>Scaffold placeholder. Add fields to the selected entity.</p>
|
||||
</UCard>
|
||||
</div>
|
||||
</template>
|
||||
8
frontend/pages/admin/entities/index.vue
Normal file
8
frontend/pages/admin/entities/index.vue
Normal file
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<div class="p-6">
|
||||
<UCard>
|
||||
<template #header><h2 class="text-xl font-semibold">Admin / Entities</h2></template>
|
||||
<p>Scaffold placeholder. List and manage entities here.</p>
|
||||
</UCard>
|
||||
</div>
|
||||
</template>
|
||||
8
frontend/pages/admin/forms.vue
Normal file
8
frontend/pages/admin/forms.vue
Normal file
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<div class="p-6">
|
||||
<UCard>
|
||||
<template #header><h2 class="text-xl font-semibold">Admin / Forms</h2></template>
|
||||
<p>Scaffold placeholder. Manage form layouts (layout_json) here.</p>
|
||||
</UCard>
|
||||
</div>
|
||||
</template>
|
||||
8
frontend/pages/admin/workflows.vue
Normal file
8
frontend/pages/admin/workflows.vue
Normal file
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<div class="p-6">
|
||||
<UCard>
|
||||
<template #header><h2 class="text-xl font-semibold">Admin / Workflows</h2></template>
|
||||
<p>Scaffold placeholder. Manage workflows (trigger_event, condition_json, action_json).</p>
|
||||
</UCard>
|
||||
</div>
|
||||
</template>
|
||||
16
frontend/pages/index.vue
Normal file
16
frontend/pages/index.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<div class="p-6">
|
||||
<UCard>
|
||||
<h1 class="text-2xl font-semibold">Codeless Platform</h1>
|
||||
<p class="mt-2">Nuxt 4 + Nuxt UI starter running.</p>
|
||||
<div class="mt-4 flex gap-3">
|
||||
<UButton to="/admin/apps" color="primary" variant="solid">Admin Studio</UButton>
|
||||
<UButton to="/runtime/tickets/list" variant="soft">Tickets (Demo)</UButton>
|
||||
</div>
|
||||
<p class="mt-4 text-xs text-gray-500">
|
||||
Tip: set your backend base URL in localStorage:
|
||||
<code>localStorage.setItem('API_BASE','http://localhost:8000')</code>
|
||||
</p>
|
||||
</UCard>
|
||||
</div>
|
||||
</template>
|
||||
8
frontend/pages/runtime/[entity]/[id]/edit.vue
Normal file
8
frontend/pages/runtime/[entity]/[id]/edit.vue
Normal file
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<div class="p-6">
|
||||
<UCard>
|
||||
<template #header><h2 class="text-xl font-semibold">Edit Record</h2></template>
|
||||
<p>Placeholder. Implement edit form honoring field update permissions.</p>
|
||||
</UCard>
|
||||
</div>
|
||||
</template>
|
||||
8
frontend/pages/runtime/[entity]/[id]/view.vue
Normal file
8
frontend/pages/runtime/[entity]/[id]/view.vue
Normal file
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<div class="p-6">
|
||||
<UCard>
|
||||
<template #header><h2 class="text-xl font-semibold">Record View</h2></template>
|
||||
<p>Placeholder. Implement detail view honoring field read permissions.</p>
|
||||
</UCard>
|
||||
</div>
|
||||
</template>
|
||||
53
frontend/pages/runtime/tickets/list.vue
Normal file
53
frontend/pages/runtime/tickets/list.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<script setup lang="ts">
|
||||
const entity = 'tickets'
|
||||
const baseURL = process.client ? (localStorage.getItem('API_BASE') || 'http://localhost:8000') : ''
|
||||
const items = ref<any[]>([])
|
||||
const error = ref<string | null>(null)
|
||||
const loading = ref(true)
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await fetch(`${baseURL}/api/${entity}/records`)
|
||||
if (!res.ok) throw new Error(await res.text())
|
||||
items.value = await res.json()
|
||||
} catch (e: any) {
|
||||
error.value = e.message
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-6 space-y-4">
|
||||
<UCard>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-xl font-semibold">Tickets</h2>
|
||||
<div class="flex gap-2">
|
||||
<UButton to="/runtime/tickets/new">New</UButton>
|
||||
<UButton to="/">Home</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="loading" class="py-8"><ULoading /></div>
|
||||
|
||||
<ul v-else class="space-y-2">
|
||||
<li v-for="it in items" :key="it.id" class="p-3 border rounded">
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<strong>{{ it.title }}</strong>
|
||||
<div class="text-sm text-gray-600">{{ it.status }}</div>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500">{{ new Date(it.created_at).toLocaleString() }}</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<template #footer>
|
||||
<p v-if="error" class="text-red-600 text-sm">{{ error }}</p>
|
||||
</template>
|
||||
</UCard>
|
||||
</div>
|
||||
</template>
|
||||
55
frontend/pages/runtime/tickets/new.vue
Normal file
55
frontend/pages/runtime/tickets/new.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<script setup lang="ts">
|
||||
const entity = 'tickets'
|
||||
const baseURL = process.client ? (localStorage.getItem('API_BASE') || 'http://localhost:8000') : ''
|
||||
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const formLayout = ref<{ sections: Array<{ title?: string; fields: string[] }> }>({
|
||||
sections: [{ title: 'Ticket Info', fields: ['title', 'description', 'status'] }]
|
||||
})
|
||||
const fieldsMeta = ref<Record<string, any>>({
|
||||
title: { label: 'Title', type: 'string', required: true },
|
||||
description: { label: 'Description', type: 'textarea' },
|
||||
status: { label: 'Status', type: 'select', options: [{ label: 'Open', value: 'open' }, { label: 'Closed', value: 'closed' }] }
|
||||
})
|
||||
|
||||
async function handleSubmit(payload: Record<string, any>) {
|
||||
try {
|
||||
loading.value = true
|
||||
const res = await fetch(`${baseURL}/api/${entity}/records`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
if (!res.ok) throw new Error(await res.text())
|
||||
navigateTo(`/runtime/${entity}/list`)
|
||||
} catch (e: any) {
|
||||
error.value = e.message
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-6 space-y-4">
|
||||
<UCard>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-xl font-semibold">New Ticket</h2>
|
||||
<UButton variant="ghost" to="/runtime/tickets/list">Back to list</UButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="loading" class="py-8"><ULoading /></div>
|
||||
<div v-else>
|
||||
<DynamicForm :layout="formLayout" :fields-meta="fieldsMeta" @submit="handleSubmit" />
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<p v-if="error" class="text-red-600 text-sm">{{ error }}</p>
|
||||
<p class="text-xs text-gray-500">Set localStorage API_BASE to your backend URL if not localhost:8000</p>
|
||||
</template>
|
||||
</UCard>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user