- flask plugin new now scaffolds the frontend too: List/Detail/Form views on the global styles, a gated route module (ADR-009), and an api-client snippet emitted into the plugin dir. Views are written before the route file so a partially generated plugin cannot 500 the dev server. - docs/PLUGIN-EXTERNAL-REPO.md + scripts/test-external-plugin.sh: how a sister site develops a plugin in its own repo and runs the framework contract tests in CI against a pinned framework ref (script verified to fail on a broken core_version pin). - docs/CONTRACT-STABILITY.md: settled vs churning contract surface and the provisional 1.0 criteria. - CLAUDE.md active-state refresh (contract 0.6.0, 11 plugins, 340 tests, measuringtools done). Known limitation documented: Path.rglob does not descend symlinks, so the import-surface contract test skips symlinked external plugins. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
229 lines
5.3 KiB
Cheetah
229 lines
5.3 KiB
Cheetah
<template>
|
|
<div>
|
|
<div class="page-header">
|
|
<h2>{{ isEdit ? 'Edit ${Name}' : 'Add ${Name}' }}</h2>
|
|
</div>
|
|
|
|
<div class="card form-card">
|
|
<form @submit.prevent="submitForm">
|
|
<fieldset>
|
|
<legend>Asset Information</legend>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="assetnumber">Asset Number *</label>
|
|
<input
|
|
id="assetnumber"
|
|
v-model="form.assetnumber"
|
|
type="text"
|
|
class="form-control"
|
|
required
|
|
:disabled="isEdit"
|
|
/>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="name">Name</label>
|
|
<input
|
|
id="name"
|
|
v-model="form.name"
|
|
type="text"
|
|
class="form-control"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="serialnumber">Serial Number</label>
|
|
<input
|
|
id="serialnumber"
|
|
v-model="form.serialnumber"
|
|
type="text"
|
|
class="form-control"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</fieldset>
|
|
|
|
<fieldset>
|
|
<legend>${Name} Details</legend>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="examplefield">Example Field</label>
|
|
<input
|
|
id="examplefield"
|
|
v-model="form.examplefield"
|
|
type="text"
|
|
class="form-control"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</fieldset>
|
|
|
|
<div class="form-actions">
|
|
<button type="button" class="btn btn-secondary" @click="cancel">Cancel</button>
|
|
<button type="submit" class="btn btn-primary" :disabled="saving">
|
|
{{ saving ? 'Saving...' : (isEdit ? 'Save Changes' : 'Create') }}
|
|
</button>
|
|
</div>
|
|
|
|
<div v-if="error" class="error-message">{{ error }}</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, onMounted } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import api from '../../api'
|
|
|
|
// local api client. move into src/api/index.js (see
|
|
// plugins/${name}/frontend-api-snippet.js) then swap for:
|
|
// import { ${name}Api } from '../../api'
|
|
const ${name}Api = {
|
|
list(params = {}) { return api.get('/${name}', { params }) },
|
|
get(itemId) { return api.get(`/${name}/${itemId}`) },
|
|
create(data) { return api.post('/${name}', data) },
|
|
update(itemId, data) { return api.put(`/${name}/${itemId}`, data) },
|
|
remove(itemId) { return api.delete(`/${name}/${itemId}`) }
|
|
}
|
|
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
|
|
const itemId = route.params.id
|
|
const isEdit = computed(() => !!itemId)
|
|
|
|
const form = ref({
|
|
assetnumber: '',
|
|
name: '',
|
|
serialnumber: '',
|
|
examplefield: ''
|
|
})
|
|
const saving = ref(false)
|
|
const error = ref('')
|
|
|
|
onMounted(async () => {
|
|
if (isEdit.value) {
|
|
await loadItem()
|
|
}
|
|
})
|
|
|
|
async function loadItem() {
|
|
try {
|
|
const response = await ${name}Api.get(itemId)
|
|
const data = response.data.data
|
|
form.value.assetnumber = data.assetnumber || ''
|
|
form.value.name = data.name || ''
|
|
form.value.serialnumber = data.serialnumber || ''
|
|
form.value.examplefield = data.examplefield || ''
|
|
} catch (loadError) {
|
|
console.error('Error loading ${name}:', loadError)
|
|
error.value = 'Failed to load record'
|
|
}
|
|
}
|
|
|
|
async function submitForm() {
|
|
saving.value = true
|
|
error.value = ''
|
|
|
|
try {
|
|
const payload = {
|
|
assetnumber: form.value.assetnumber,
|
|
name: form.value.name || null,
|
|
serialnumber: form.value.serialnumber || null,
|
|
examplefield: form.value.examplefield || null
|
|
}
|
|
|
|
let redirectId = itemId
|
|
if (isEdit.value) {
|
|
await ${name}Api.update(itemId, payload)
|
|
} else {
|
|
const response = await ${name}Api.create(payload)
|
|
redirectId = response.data.data?.assetid
|
|
}
|
|
|
|
router.push(redirectId ? `/${name}/${redirectId}` : '/${name}')
|
|
} catch (submitError) {
|
|
console.error('Error saving ${name}:', submitError)
|
|
error.value = 'Failed to save record'
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
|
|
function cancel() {
|
|
if (isEdit.value) {
|
|
router.push(`/${name}/${itemId}`)
|
|
} else {
|
|
router.push('/${name}')
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.form-card {
|
|
max-width: 800px;
|
|
}
|
|
|
|
fieldset {
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
padding: 1.5rem;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
legend {
|
|
font-weight: 600;
|
|
padding: 0 0.5rem;
|
|
color: var(--text);
|
|
}
|
|
|
|
.form-row {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 1rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.form-row:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.form-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.form-group label {
|
|
margin-bottom: 0.375rem;
|
|
font-weight: 500;
|
|
color: var(--text);
|
|
}
|
|
|
|
.form-actions {
|
|
display: flex;
|
|
gap: 1rem;
|
|
justify-content: flex-end;
|
|
margin-top: 1.5rem;
|
|
padding-top: 1.5rem;
|
|
border-top: 1px solid var(--border);
|
|
}
|
|
|
|
.error-message {
|
|
margin-top: 1rem;
|
|
padding: 0.75rem 1rem;
|
|
background: var(--danger);
|
|
color: white;
|
|
border-radius: 6px;
|
|
}
|
|
|
|
@media (max-width: 600px) {
|
|
.form-row {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
</style>
|