`flask plugin new` wrote views to frontend/src/views/<name>/ and a route file to frontend/src/router/routes/<name>.js. Every one of the 16 bundled plugins keeps its frontend in plugins/<name>/frontend/ instead, staged into .plugins-staged by stage-frontend.mjs and aggregated into routes.gen.js. It was not visibly broken, which is why it lasted: the router still globs ./routes/*.js during the Phase 4 dual-location transition, so a scaffolded plugin's pages loaded. What a new plugin author silently lost was ADR-013 lean builds. Pruning works off the staged tree, so a frontend in the core tree cannot be excluded from a site build, and deleting the plugin strands its views behind. The templates for the correct layout already existed; only the output paths were wrong. Also fixed the imports they carry: core imports now use the @ alias (@/api, @/components/...), which resolves both before and after staging, where the previous ../../api was correct only in the old location. Sibling views stay relative and travel with the route file. frontend_dir is gone from scaffold_plugin. There is no second output tree to point at, and a plugin developed in its own repo now gets the same frontend as a bundled one rather than being skipped. Proven end to end: scaffolded a throwaway plugin, confirmed it stages, builds real chunks, and that SITE_PLUGINS=network prunes it, which is what the old layout could not do.
150 lines
4.3 KiB
Cheetah
150 lines
4.3 KiB
Cheetah
<template>
|
|
<div class="detail-page" v-if="item">
|
|
<div class="hero-card">
|
|
<div class="hero-content">
|
|
<div class="hero-title-row">
|
|
<h1 class="hero-title">{{ item.name || item.assetnumber || '${Name}' }}</h1>
|
|
<router-link
|
|
v-if="authStore.isAuthenticated"
|
|
:to="`/${name}/${itemId}/edit`"
|
|
class="btn btn-secondary"
|
|
>
|
|
Edit
|
|
</router-link>
|
|
</div>
|
|
<div class="hero-details">
|
|
<div class="detail-item" v-if="item.assetnumber">
|
|
<span class="label">Asset #</span>
|
|
<span class="value">{{ item.assetnumber }}</span>
|
|
</div>
|
|
<div class="detail-item" v-if="item.serialnumber">
|
|
<span class="label">Serial</span>
|
|
<span class="value mono">{{ item.serialnumber }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="content-grid">
|
|
<div class="content-column">
|
|
<div class="section-card">
|
|
<h3 class="section-title">${Name} Information</h3>
|
|
<div class="info-list">
|
|
<div class="info-row">
|
|
<span class="info-label">Example Field</span>
|
|
<span class="info-value">{{ item.examplefield || '-' }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="content-column">
|
|
<div class="section-card">
|
|
<h3 class="section-title">Asset Information</h3>
|
|
<div class="info-list">
|
|
<div class="info-row">
|
|
<span class="info-label">Asset Number</span>
|
|
<span class="info-value">{{ item.assetnumber || '-' }}</span>
|
|
</div>
|
|
<div class="info-row">
|
|
<span class="info-label">Name</span>
|
|
<span class="info-value">{{ item.name || '-' }}</span>
|
|
</div>
|
|
<div class="info-row">
|
|
<span class="info-label">Serial Number</span>
|
|
<span class="info-value mono">{{ item.serialnumber || '-' }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="action-bar" v-if="authStore.isAuthenticated">
|
|
<router-link :to="`/${name}/${itemId}/edit`" class="btn btn-primary">Edit</router-link>
|
|
<button @click="confirmDelete" class="btn btn-danger">Delete</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else-if="loading" class="loading-container">
|
|
<div class="loading">Loading...</div>
|
|
</div>
|
|
|
|
<div v-else class="error-container">
|
|
<p>Record not found</p>
|
|
<router-link to="/${name}" class="btn btn-secondary">Back to ${Name}</router-link>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import api from '@/api'
|
|
import { useAuthStore } from '@/stores/auth'
|
|
|
|
// 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 authStore = useAuthStore()
|
|
|
|
const itemId = route.params.id
|
|
const item = ref(null)
|
|
const loading = ref(true)
|
|
|
|
onMounted(loadItem)
|
|
|
|
async function loadItem() {
|
|
loading.value = true
|
|
try {
|
|
const response = await ${name}Api.get(itemId)
|
|
item.value = response.data.data
|
|
} catch (error) {
|
|
console.error('Error loading ${name}:', error)
|
|
item.value = null
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
async function confirmDelete() {
|
|
if (confirm('Delete this record?')) {
|
|
try {
|
|
await ${name}Api.remove(itemId)
|
|
router.push('/${name}')
|
|
} catch (error) {
|
|
console.error('Error deleting ${name}:', error)
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.mono {
|
|
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
|
|
}
|
|
|
|
.action-bar {
|
|
display: flex;
|
|
gap: 1rem;
|
|
margin-top: 2rem;
|
|
padding-top: 1.5rem;
|
|
border-top: 1px solid var(--border);
|
|
}
|
|
|
|
.loading-container,
|
|
.error-container {
|
|
text-align: center;
|
|
padding: 3rem;
|
|
color: var(--text-light);
|
|
}
|
|
</style>
|