Tech Tools section with a code generator, toner report that names the part, toner forecasting, warranty proof of cover, application uploads
This commit is contained in:
@@ -111,18 +111,59 @@
|
||||
class="form-control"
|
||||
placeholder="Network path or URL to install files"
|
||||
/>
|
||||
<small class="form-hint">
|
||||
A share path or URL, or upload the installer below and this fills
|
||||
itself in.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="image">Image Filename</label>
|
||||
<label for="packagefile">Installer</label>
|
||||
<div v-if="uploadedPackage" class="upload-current">
|
||||
<span class="upload-name">{{ uploadedPackage }}</span>
|
||||
<button type="button" class="btn btn-sm btn-secondary" @click="removePackage"
|
||||
:disabled="busyPackage">Remove installer</button>
|
||||
</div>
|
||||
<input id="packagefile" type="file" class="form-control"
|
||||
:accept="PACKAGE_ACCEPT" @change="onPackagePicked" />
|
||||
<div v-if="packageProgress !== null" class="upload-progress">
|
||||
<div class="upload-bar"><span :style="{ width: packageProgress + '%' }"></span></div>
|
||||
<span>{{ packageProgress }}%</span>
|
||||
</div>
|
||||
<small class="form-hint">
|
||||
Up to 500MB. Anything larger belongs on the share - put its path in
|
||||
Install Path instead. Downloads require a login.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="imagefile">Image</label>
|
||||
<div v-if="imagePreview" class="upload-current">
|
||||
<img :src="imagePreview" alt="" class="upload-thumb" />
|
||||
<button type="button" class="btn btn-sm btn-secondary" @click="removeImage"
|
||||
:disabled="busyImage">Remove image</button>
|
||||
</div>
|
||||
<input id="imagefile" type="file" class="form-control"
|
||||
accept="image/*" @change="onImagePicked" />
|
||||
<small class="form-hint">
|
||||
PNG, JPG, GIF, WEBP, SVG or ICO. Replaces whatever is there now.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="image">Image path</label>
|
||||
<input
|
||||
id="image"
|
||||
v-model="form.image"
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="e.g., myapp.png"
|
||||
placeholder="filled in by the upload above"
|
||||
/>
|
||||
<small class="form-hint">Image should be placed in /images/applications/</small>
|
||||
<small class="form-hint">
|
||||
Set by the upload. Older entries hold a bare filename served from
|
||||
/images/applications/, and those still work - leave them alone
|
||||
unless you are replacing the image.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<h4 style="margin-top: 1.5rem; margin-bottom: 1rem;">Notes</h4>
|
||||
@@ -184,6 +225,76 @@ const form = ref({
|
||||
|
||||
const supportTeams = ref([])
|
||||
|
||||
// Files are chosen before the application exists, so they are held here and
|
||||
// uploaded once there is an id to attach them to (see saveApplication).
|
||||
const pendingImage = ref(null)
|
||||
const pendingPackage = ref(null)
|
||||
const imagePreview = ref('')
|
||||
const uploadedPackage = ref('')
|
||||
const packageProgress = ref(null)
|
||||
const busyImage = ref(false)
|
||||
const busyPackage = ref(false)
|
||||
|
||||
const PACKAGE_ACCEPT = '.exe,.msi,.msp,.zip,.7z,.cab,.iso,.appx,.msix,.ps1,.bat,.txt,.pdf'
|
||||
const MAX_PACKAGE_BYTES = 500 * 1024 * 1024
|
||||
|
||||
function onImagePicked(event) {
|
||||
const file = event.target.files?.[0]
|
||||
if (!file) return
|
||||
pendingImage.value = file
|
||||
// Show the chosen file immediately rather than after a round trip.
|
||||
imagePreview.value = URL.createObjectURL(file)
|
||||
}
|
||||
|
||||
function onPackagePicked(event) {
|
||||
const file = event.target.files?.[0]
|
||||
if (!file) return
|
||||
if (file.size > MAX_PACKAGE_BYTES) {
|
||||
error.value = `${file.name} is ${(file.size / 1048576).toFixed(0)}MB; the limit is 500MB. `
|
||||
+ 'Put it on the share and use Install Path instead.'
|
||||
event.target.value = ''
|
||||
return
|
||||
}
|
||||
error.value = ''
|
||||
pendingPackage.value = file
|
||||
uploadedPackage.value = file.name
|
||||
}
|
||||
|
||||
async function removeImage() {
|
||||
pendingImage.value = null
|
||||
imagePreview.value = ''
|
||||
form.value.image = ''
|
||||
if (isEdit.value) {
|
||||
busyImage.value = true
|
||||
try { await applicationsApi.removeImage(route.params.id) }
|
||||
catch (err) { error.value = apiError(err, 'Failed to remove the image') }
|
||||
finally { busyImage.value = false }
|
||||
}
|
||||
}
|
||||
|
||||
async function removePackage() {
|
||||
pendingPackage.value = null
|
||||
uploadedPackage.value = ''
|
||||
if (isEdit.value) {
|
||||
busyPackage.value = true
|
||||
try {
|
||||
const response = await applicationsApi.removePackage(route.params.id)
|
||||
form.value.installpath = response.data.data.installpath || ''
|
||||
} catch (err) {
|
||||
error.value = apiError(err, 'Failed to remove the installer')
|
||||
} finally { busyPackage.value = false }
|
||||
}
|
||||
}
|
||||
|
||||
// The image field holds either a served URL (new) or a bare filename from the
|
||||
// classic site, which is still rendered out of /images/applications/.
|
||||
function imageSrcFor(value) {
|
||||
if (!value) return ''
|
||||
return value.startsWith('/api/') || value.startsWith('http')
|
||||
? value
|
||||
: `/images/applications/${value}`
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
// Load support teams
|
||||
@@ -210,6 +321,10 @@ onMounted(async () => {
|
||||
image: app.image || '',
|
||||
applicationnotes: app.applicationnotes || ''
|
||||
}
|
||||
imagePreview.value = imageSrcFor(app.image)
|
||||
if ((app.installpath || '').startsWith('/api/applications/package/')) {
|
||||
uploadedPackage.value = app.installpath.split('/').pop()
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading data:', err)
|
||||
@@ -240,16 +355,34 @@ async function saveApplication() {
|
||||
applicationnotes: form.value.applicationnotes || null
|
||||
}
|
||||
|
||||
let appid = route.params.id
|
||||
if (isEdit.value) {
|
||||
await applicationsApi.update(route.params.id, appData)
|
||||
await applicationsApi.update(appid, appData)
|
||||
} else {
|
||||
await applicationsApi.create(appData)
|
||||
const created = await applicationsApi.create(appData)
|
||||
appid = created.data.data.appid
|
||||
}
|
||||
|
||||
// Uploads come after the save: a new application has no id until it exists,
|
||||
// and both endpoints key on it. A failed upload must not lose the record
|
||||
// that saved fine, so it reports and stays on the form.
|
||||
if (pendingImage.value) {
|
||||
await applicationsApi.uploadImage(appid, pendingImage.value)
|
||||
pendingImage.value = null
|
||||
}
|
||||
if (pendingPackage.value) {
|
||||
packageProgress.value = 0
|
||||
await applicationsApi.uploadPackage(appid, pendingPackage.value,
|
||||
percent => { packageProgress.value = percent })
|
||||
pendingPackage.value = null
|
||||
packageProgress.value = null
|
||||
}
|
||||
|
||||
router.push('/applications')
|
||||
} catch (err) {
|
||||
console.error('Error saving application:', err)
|
||||
error.value = apiError(err, 'Failed to save application')
|
||||
packageProgress.value = null
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
@@ -257,6 +390,46 @@ async function saveApplication() {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.upload-current {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.upload-thumb {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
object-fit: contain;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.25rem;
|
||||
background: var(--bg);
|
||||
}
|
||||
.upload-name {
|
||||
font-family: ui-monospace, monospace;
|
||||
font-size: 0.85rem;
|
||||
word-break: break-all;
|
||||
}
|
||||
.upload-progress {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.upload-bar {
|
||||
flex: 1;
|
||||
height: 0.5rem;
|
||||
background: var(--border);
|
||||
border-radius: 999px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.upload-bar span {
|
||||
display: block;
|
||||
height: 100%;
|
||||
background: var(--primary);
|
||||
transition: width 0.2s ease;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
|
||||
Reference in New Issue
Block a user