Model photo upload fixes; network devices linked to catalog models

This commit is contained in:
2026-08-05 09:09:19 -04:00
parent d8e3280dfc
commit c51e86e686
6 changed files with 169 additions and 22 deletions

View File

@@ -138,34 +138,51 @@
<div class="image-manage">
<img v-if="form.imageurl" :src="withBase(form.imageurl)" alt="Model image" class="image-thumb" />
<div class="image-actions">
<template v-if="editingModel">
<input
ref="imageFileInput"
type="file"
accept=".png,.jpg,.jpeg,.gif,.webp,.svg"
style="display: none"
@change="onImageSelected"
/>
<button type="button" class="btn btn-secondary btn-sm" :disabled="uploadingImage" @click="triggerImageUpload">
{{ uploadingImage ? 'Uploading...' : (form.imageurl ? 'Replace' : 'Upload') }}
</button>
<button v-if="form.imageurl" type="button" class="btn btn-danger btn-sm" @click="removeImage">Remove</button>
</template>
<small v-else class="text-muted">Save the model first, then upload a photo.</small>
<!--
Offered when CREATING too. The photo is stored as
model-<id>.<ext>, so it cannot be sent before the model has
an id - but making the operator save, reopen and come back
just to add a picture reads as a missing feature. On a new
model the file is held here and uploaded immediately after
the model is created.
-->
<input
ref="imageFileInput"
type="file"
accept=".png,.jpg,.jpeg,.gif,.webp,.svg"
style="display: none"
@change="onImageSelected"
/>
<button type="button" class="btn btn-secondary btn-sm" :disabled="uploadingImage" @click="triggerImageUpload">
{{ uploadingImage ? 'Uploading...' : ((form.imageurl || pendingImageFile) ? 'Replace' : 'Upload') }}
</button>
<button v-if="form.imageurl || pendingImageFile" type="button" class="btn btn-danger btn-sm" @click="removeImage">Remove</button>
<small v-if="pendingImageFile" class="text-muted pending-note">
{{ pendingImageFile.name }} will be uploaded when you save.
</small>
</div>
</div>
</div>
<div class="form-group">
<label for="imageurl">Image URL</label>
<!--
type="text", NOT type="url". An uploaded photo sets this field to
an application path such as /api/models/image/model-120.png, and
native url validation demands an absolute URL with a scheme - so
the browser refused to save the form with "Please enter a URL"
for a value this page had just written itself.
-->
<input
id="imageurl"
v-model="form.imageurl"
type="url"
type="text"
class="form-control"
placeholder="https://..."
placeholder="https://... or /api/models/image/..."
/>
<small class="text-muted">Manual alternative. Uploading a photo overwrites this URL.</small>
<small class="text-muted">
Set automatically when you upload a photo. Accepts a full web address or a path on this server.
</small>
</div>
<div class="form-group">
@@ -224,6 +241,8 @@ const perPage = ref(20)
const showModal = ref(false)
const editingModel = ref(null)
// A photo chosen while CREATING a model, uploaded once the model has an id.
const pendingImageFile = ref(null)
const saving = ref(false)
const error = ref('')
@@ -337,6 +356,9 @@ function openModal(m = null) {
function closeModal() {
showModal.value = false
editingModel.value = null
// Or a photo picked for a model that was never saved would be uploaded onto
// the NEXT model created in this session.
pendingImageFile.value = null
}
async function saveModel() {
@@ -351,7 +373,20 @@ async function saveModel() {
if (editingModel.value) {
await modelsApi.update(editingModel.value.modelnumberid, data)
} else {
await modelsApi.create(data)
const created = await modelsApi.create(data)
// Send a photo chosen before the model existed. A failure here must not
// read as "the model was not saved" - it was.
if (pendingImageFile.value) {
const newId = created?.data?.data?.modelnumberid
if (newId) {
try {
await modelsApi.uploadImage(newId, pendingImageFile.value)
} catch (imgErr) {
console.error('Error uploading image:', imgErr)
toast.error(apiError(imgErr, 'Model saved, but the photo could not be uploaded'))
}
}
}
}
closeModal()
loadModels()
@@ -369,7 +404,14 @@ function triggerImageUpload() {
async function onImageSelected(event) {
const file = event.target.files?.[0]
if (!file || !editingModel.value) return
if (!file) return
// No model id yet, so hold the file and send it once saveModel has created
// the record. Nothing is written to the server until the operator saves.
if (!editingModel.value) {
pendingImageFile.value = file
if (imageFileInput.value) imageFileInput.value.value = ''
return
}
uploadingImage.value = true
try {
const response = await modelsApi.uploadImage(editingModel.value.modelnumberid, file)
@@ -387,7 +429,12 @@ async function onImageSelected(event) {
}
async function removeImage() {
if (!editingModel.value) return
// On a new model there is nothing on the server yet; just drop the choice.
if (!editingModel.value) {
pendingImageFile.value = null
form.value.imageurl = ''
return
}
if (!confirm('Remove this model image?')) return
try {
await modelsApi.removeImage(editingModel.value.modelnumberid)