From c946e4aa89b6ac26cb1fa3e0fa0578877dfe8374 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Wed, 29 Jul 2026 10:03:47 +0100 Subject: [PATCH 1/4] add url prompt dialog --- static/components/photo-gallery-form.js | 41 +++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/static/components/photo-gallery-form.js b/static/components/photo-gallery-form.js index 21bf9c5..a31813a 100644 --- a/static/components/photo-gallery-form.js +++ b/static/components/photo-gallery-form.js @@ -99,6 +99,39 @@ window.app.component('photo-gallery-form', { } }, + addFromUrl() { + if (!this.canAdd) { + LNbits.utils.notifyError(`Maximum ${this.max} photos allowed`) + return + } + + this.$q + .dialog({ + title: 'Add Photo from URL', + color: 'secondary', + prompt: { + model: '', + type: 'text', + isValid: val => val.trim() !== '', + label: 'Image URL', + attrs: { + placeholder: 'https://example.com/photo.jpg' + } + }, + cancel: true, + persistent: true + }) + .onOk(url => { + const preview = url + this.gallery.push({ + file: null, + preview, + assetId: null, + isNew: false + }) + }) + }, + remove(index) { URL.revokeObjectURL(this.gallery[index].preview) this.gallery.splice(index, 1) @@ -111,6 +144,14 @@ window.app.component('photo-gallery-form', {
+ Date: Wed, 29 Jul 2026 12:32:42 +0100 Subject: [PATCH 2/4] ignore test artifacts --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 0152b6e..ddbf3f7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ __pycache__ node_modules .mypy_cache +.pytest_cache +.ruff_cache .venv +data/ From ccadba9e6fec8ebbb430f775d88579c8265401dc Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Wed, 29 Jul 2026 12:34:28 +0100 Subject: [PATCH 3/4] owner and manager dialogs --- static/components/item-card.js | 2 + static/components/photo-gallery-form.js | 6 +-- static/js/index.js | 55 +++++++++---------------- static/js/utils.js | 8 ++++ templates/inventory/manager.html | 33 ++++++++------- 5 files changed, 49 insertions(+), 55 deletions(-) diff --git a/static/components/item-card.js b/static/components/item-card.js index cce5ae0..73258e2 100644 --- a/static/components/item-card.js +++ b/static/components/item-card.js @@ -38,6 +38,8 @@ window.app.component('item-card', { return 'In Stock' }, itemImgUrl() { + if (!this.item.images || !this.item.images.length) return null + if (isURLimg(this.item.images[0])) return this.item.images[0] return isBase64String(this.item.images[0]) ? this.item.images[0] : `/api/v1/assets/${this.item.images[0]}/thumbnail` diff --git a/static/components/photo-gallery-form.js b/static/components/photo-gallery-form.js index a31813a..9404837 100644 --- a/static/components/photo-gallery-form.js +++ b/static/components/photo-gallery-form.js @@ -112,7 +112,7 @@ window.app.component('photo-gallery-form', { prompt: { model: '', type: 'text', - isValid: val => val.trim() !== '', + isValid: isURLimg, label: 'Image URL', attrs: { placeholder: 'https://example.com/photo.jpg' @@ -122,12 +122,12 @@ window.app.component('photo-gallery-form', { persistent: true }) .onOk(url => { - const preview = url + const preview = url.trim() this.gallery.push({ file: null, preview, assetId: null, - isNew: false + isNew: true }) }) }, diff --git a/static/js/index.js b/static/js/index.js index b0efb55..ff169a3 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -481,7 +481,10 @@ window.app = Vue.createApp({ this.itemDialog.gallery = item.images.map(id => { return { assetId: id, - preview: isBase64String(id) ? id : `/api/v1/assets/${id}/thumbnail`, + preview: + isBase64String(id) || isURLimg(id) + ? id + : `/api/v1/assets/${id}/thumbnail`, file: null, isNew: false } @@ -531,9 +534,9 @@ window.app = Vue.createApp({ this.itemDialog.data.inventory_id = this.inventory.id try { const assetIds = await Promise.all( - this.itemDialog.gallery - .filter(p => p.file) - .map(p => this.uploadPhoto(p.file)) + this.itemDialog.gallery.map(p => + p.file ? this.uploadPhoto(p.file) : p.preview + ) ) if (assetIds.includes(null)) { LNbits.utils.notifyError('One or more photo uploads failed') @@ -555,40 +558,20 @@ window.app = Vue.createApp({ } }, async updateItem(data) { - const filesToUpload = this.itemDialog.gallery - .filter( - p => - (p.isNew && p.file) || - (!p.isNew && p.assetId && isBase64String(p.assetId)) - ) - .map(p => { - if (p.isNew && p.file) return p.file - if (!p.isNew && p.assetId && isBase64String(p.assetId)) - return base64ToFile(p.assetId) - return null + const assetIds = await Promise.all( + this.itemDialog.gallery.map(p => { + if (p.file) return this.uploadPhoto(p.file) + if (p.assetId && isBase64String(p.assetId)) { + return this.uploadPhoto(base64ToFile(p.assetId)) + } + return p.assetId || p.preview }) - .filter(Boolean) - - let uploadedAssetIds = [] - if (filesToUpload.length > 0) { - try { - uploadedAssetIds = await Promise.all( - filesToUpload.map(file => this.uploadPhoto(file)) - ) - } catch (error) { - LNbits.utils.notifyError('Failed to upload photos') - return - } + ) + if (assetIds.includes(null)) { + LNbits.utils.notifyError('One or more photo uploads failed') + return } - - // Asset IDs from gallery that are not new and not base64 - const existingAssetIds = this.itemDialog.gallery - .filter(p => !p.isNew && p.assetId && !isBase64String(p.assetId)) - .map(p => p.assetId) - - const finalIds = [...existingAssetIds, ...uploadedAssetIds] - - data.images = toCsv(finalIds) + data.images = toCsv(assetIds) try { const {data: updatedItem} = await LNbits.api.request( diff --git a/static/js/utils.js b/static/js/utils.js index fc1c1b1..df0e3bb 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -51,3 +51,11 @@ function isBase64String(str) { if (typeof str !== 'string') return false return str.includes('data:') && str.includes('base64') } +function isURLimg(str) { + if (typeof str !== 'string') return false + try { + return ['http:', 'https:'].includes(new URL(str.trim()).protocol) + } catch { + return false + } +} diff --git a/templates/inventory/manager.html b/templates/inventory/manager.html index fe05623..582e4b7 100644 --- a/templates/inventory/manager.html +++ b/templates/inventory/manager.html @@ -358,11 +358,17 @@ }, async addItem() { this.itemDialog.data.inventory_id = this.inventory.id - const base64List = await Promise.all( - this.itemDialog.gallery.map(p => fileToBase64(p.file)) + const newPhotos = await Promise.all( + this.itemDialog.gallery.map(p => + p.file ? fileToBase64(p.file) : p.preview + ) ) + try { - this.itemDialog.data.images = toCsv(base64List, '|||') + this.itemDialog.data.images = toCsv( + newPhotos, + newPhotos.some(isBase64String) ? '|||' : ',' + ) const {data} = await LNbits.api.request( 'POST', `/inventory/api/v1/managers/${this.manager.id}/item`, @@ -385,21 +391,16 @@ } }, async updateItem(data) { - const newPhotos = this.itemDialog.gallery.filter(p => p.isNew && p.file) - let newBase64 = [] - - if (newPhotos.length > 0) { - newBase64 = await Promise.all( - newPhotos.map(p => fileToBase64(p.file)) + const finalImages = await Promise.all( + this.itemDialog.gallery.map(p => + p.file ? fileToBase64(p.file) : p.preview ) - } - - const finalImages = [ - ...this.itemDialog.gallery.filter(p => !p.isNew).map(p => p.file), - ...newBase64 - ] + ) - data.images = toCsv(finalImages, '|||') + data.images = toCsv( + finalImages, + finalImages.some(isBase64String) ? '|||' : ',' + ) try { const {data: updatedItem} = await LNbits.api.request( From cc66a1e8a31106a36b22ead0be29f5d979e4c4c6 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Wed, 29 Jul 2026 13:02:46 +0100 Subject: [PATCH 4/4] fix the import delimiter handling --- static/js/utils.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/static/js/utils.js b/static/js/utils.js index df0e3bb..ebf0231 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -21,9 +21,10 @@ function mapItems(obj) { } obj.tags = fromCsv(obj.tags) obj.omit_tags = fromCsv(obj.omit_tags) - obj.images = isBase64String(obj.images) - ? fromCsv(obj.images, '|||') - : fromCsv(obj.images) + obj.images = + isBase64String(obj.images) || obj.images?.includes('|||') + ? fromCsv(obj.images, '|||') + : fromCsv(obj.images) return obj }