ADR-013 Phase 4: frontend staging machinery + relocate printedparts; fix router crash
The staging step that makes lean per-site frontend builds possible, plus the
first plugin relocated as the pilot.
- scripts/stage-frontend.mjs: copies each chosen plugin's plugins/<name>/frontend/
into frontend/src/.plugins-staged/<name>/ and codegens routes.gen.js. Plugin
selection via SITE_PLUGINS (comma-separated); empty = all plugins that have a
frontend/ (the full build). Wired as npm predev/prebuild; outputs gitignored.
- Router imports routes.gen.js and merges staged routes with the in-tree
./routes/*.js glob - dual-location during the transition.
- printedparts relocated: its 6 views (list/detail/form/kiosk + the settings and
labels views from the shared dirs) moved into plugins/printedparts/frontend/
views/, core imports rewritten to the @/ alias; routes.js is the self-contained
route module. Its old in-tree route file is removed.
Also fixes a crash the previous commit (37c764b) shipped: slides.js exports only
`toplevel` (its child routes live in core.js), so the router's
flatMap(m => m.default) produced an undefined child and threw
"Cannot read properties of undefined (reading 'path')" at load - the whole SPA
went blank. Guarded with `m.default || []`. (The earlier "print pages are blank"
reading was this crash, not page nature.)
Verified live: /machines renders again; the relocated /printedparts list renders
identically from the staged plugin frontend; SITE_PLUGINS=machines excludes
printedparts from routes.gen. Build (via npm, runs stage) + vitest + naming green.
This commit is contained in:
175
plugins/printedparts/frontend/views/PrintedItemForm.vue
Normal file
175
plugins/printedparts/frontend/views/PrintedItemForm.vue
Normal file
@@ -0,0 +1,175 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="page-header">
|
||||
<h2>{{ isEdit ? 'Edit Part' : 'Add Part' }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card form-card">
|
||||
<div v-if="error" class="error-message">{{ error }}</div>
|
||||
|
||||
<form @submit.prevent="save">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Name *</label>
|
||||
<input v-model="form.itemname" type="text" class="form-control" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Bin location</label>
|
||||
<input v-model="form.binlocation" type="text" class="form-control"
|
||||
placeholder="e.g., Bin A3" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Description</label>
|
||||
<input v-model="form.itemdescription" type="text" class="form-control"
|
||||
maxlength="500" placeholder="Brief description shown on the storefront" />
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Low-stock threshold</label>
|
||||
<input v-model.number="form.lowstockthreshold" type="number" min="0"
|
||||
class="form-control" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Gage lab asset tag</label>
|
||||
<input v-model="form.gagelabtag" type="text" class="form-control"
|
||||
placeholder="e.g. WJRP0117" />
|
||||
<p class="form-hint">
|
||||
Assigned by the gage lab; optional. The kiosk finds parts by
|
||||
this tag or by the internal code.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Print notes</label>
|
||||
<textarea v-model="form.printnotes" class="form-control" rows="3"
|
||||
placeholder="Material, print time, slicer file path"></textarea>
|
||||
</div>
|
||||
|
||||
<div v-if="isEdit" class="form-group">
|
||||
<label>Photo</label>
|
||||
<div class="image-row">
|
||||
<img v-if="imageurl" :src="withBase(imageurl)" class="image-preview" />
|
||||
<input type="file" accept="image/*" @change="onImagePicked" />
|
||||
<button v-if="imageurl" type="button" class="btn btn-secondary btn-sm"
|
||||
@click="removeImage">Remove photo</button>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else class="form-hint">Save first, then add a photo from the edit page.</p>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary" :disabled="saving">
|
||||
{{ saving ? 'Saving...' : 'Save' }}
|
||||
</button>
|
||||
<router-link :to="cancelTarget" class="btn btn-secondary">Cancel</router-link>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { printedpartsApi } from '@/api'
|
||||
import { withBase } from '@/utils/basePath'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const isEdit = computed(() => !!route.params.id)
|
||||
const cancelTarget = computed(() =>
|
||||
isEdit.value ? `/printedparts/${route.params.id}` : '/printedparts')
|
||||
|
||||
const form = ref({
|
||||
itemname: '',
|
||||
gagelabtag: '',
|
||||
itemdescription: '',
|
||||
lowstockthreshold: null,
|
||||
binlocation: '',
|
||||
printnotes: ''
|
||||
})
|
||||
const imageurl = ref(null)
|
||||
const saving = ref(false)
|
||||
const error = ref('')
|
||||
|
||||
onMounted(async () => {
|
||||
if (!isEdit.value) return
|
||||
try {
|
||||
const response = await printedpartsApi.get(route.params.id)
|
||||
const item = response.data.data
|
||||
for (const key of Object.keys(form.value)) {
|
||||
form.value[key] = item[key]
|
||||
}
|
||||
imageurl.value = item.imageurl
|
||||
} catch (loadError) {
|
||||
error.value = 'Could not load the item'
|
||||
console.error(loadError)
|
||||
}
|
||||
})
|
||||
|
||||
async function save() {
|
||||
saving.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
const payload = { ...form.value }
|
||||
if (payload.lowstockthreshold === null || payload.lowstockthreshold === '') {
|
||||
delete payload.lowstockthreshold
|
||||
}
|
||||
if (!payload.gagelabtag) payload.gagelabtag = ''
|
||||
let printeditemid
|
||||
if (isEdit.value) {
|
||||
await printedpartsApi.update(route.params.id, payload)
|
||||
printeditemid = route.params.id
|
||||
} else {
|
||||
const response = await printedpartsApi.create(payload)
|
||||
printeditemid = response.data.data.printeditemid
|
||||
}
|
||||
router.push(`/printedparts/${printeditemid}`)
|
||||
} catch (saveError) {
|
||||
error.value = saveError.response?.data?.error?.message || 'Save failed'
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onImagePicked(event) {
|
||||
const file = event.target.files?.[0]
|
||||
if (!file) return
|
||||
try {
|
||||
const response = await printedpartsApi.uploadImage(route.params.id, file)
|
||||
imageurl.value = response.data.data.imageurl
|
||||
} catch (uploadError) {
|
||||
error.value = uploadError.response?.data?.error?.message || 'Image upload failed'
|
||||
}
|
||||
}
|
||||
|
||||
async function removeImage() {
|
||||
try {
|
||||
await printedpartsApi.deleteImage(route.params.id)
|
||||
imageurl.value = null
|
||||
} catch (deleteError) {
|
||||
error.value = 'Could not remove the image'
|
||||
console.error(deleteError)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.image-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
.image-preview {
|
||||
width: 6rem;
|
||||
height: 6rem;
|
||||
object-fit: cover;
|
||||
border-radius: 0.35rem;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.form-hint { color: var(--text-light); }
|
||||
</style>
|
||||
Reference in New Issue
Block a user