+
+
+
+
+
diff --git a/plugins/applications/frontend/views/ApplicationForm.vue b/plugins/applications/frontend/views/ApplicationForm.vue
index d6fc1a6..5a7736a 100644
--- a/plugins/applications/frontend/views/ApplicationForm.vue
+++ b/plugins/applications/frontend/views/ApplicationForm.vue
@@ -111,18 +111,59 @@
class="form-control"
placeholder="Network path or URL to install files"
/>
+
+ A share path or URL, or upload the installer below and this fills
+ itself in.
+
-
+
+
+ {{ uploadedPackage }}
+
+
+
+
+
+ {{ packageProgress }}%
+
+
+ Up to 500MB. Anything larger belongs on the share - put its path in
+ Install Path instead. Downloads require a login.
+
+
+
+
+
+
+
+
+
+
+
+ PNG, JPG, GIF, WEBP, SVG or ICO. Replaces whatever is there now.
+
+
+
+
+
- Image should be placed in /images/applications/
+
+ 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.
+
Notes
@@ -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() {