From 7e642cc7b883c7009bf9c27a596e0878dad4d9db Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Sun, 14 Jun 2026 21:19:47 -0400 Subject: [PATCH] feat: clearer album-source display, gated on InvokeAI backend Replace the disabled source-type radios in the album edit form with a static "Album Source: " line plus the small-font immutability hint, so the selected backing is easy to read. Also hide the entire InvokeAI album-source UI unless an InvokeAI backend is configured in settings: the new-album radio chooser stays hidden (and the source is forced to a directory of image files) and the edit form's source line is hidden. The edit line still shows for existing board albums so their (immutable) type never silently disappears. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../static/javascript/album-manager.js | 47 +++++++++++++++++-- .../templates/modules/album-manager.html | 18 ++----- tests/frontend/invokeai-album-source.test.js | 31 ++++++++++++ 3 files changed, 79 insertions(+), 17 deletions(-) diff --git a/photomap/frontend/static/javascript/album-manager.js b/photomap/frontend/static/javascript/album-manager.js index c9eed328..4d68f81f 100644 --- a/photomap/frontend/static/javascript/album-manager.js +++ b/photomap/frontend/static/javascript/album-manager.js @@ -110,6 +110,7 @@ export class AlbumManager { slideshowTitle: document.getElementById("slideshow_title"), albumManagementContent: document.querySelector("#albumManagementContent"), // InvokeAI board-album source controls (add form) + newAlbumSourceGroup: document.getElementById("newAlbumSourceGroup"), newAlbumDirectorySection: document.getElementById("newAlbumDirectorySection"), newAlbumInvokeAISection: document.getElementById("newAlbumInvokeAISection"), newAlbumInvokeUrl: document.getElementById("newAlbumInvokeUrl"), @@ -323,6 +324,16 @@ export class AlbumManager { return checked ? checked.value : "directory"; } + // Whether a global InvokeAI backend URL is configured in settings. + async _isInvokeAIConfigured() { + try { + const config = await fetchJson("invokeai/config"); + return !!(config.url && config.url.trim()); + } catch { + return false; + } + } + setupNewAlbumSourceControls() { document.querySelectorAll('input[name="newAlbumSourceType"]').forEach((radio) => { radio.addEventListener("change", () => this.toggleNewAlbumSourceSections()); @@ -366,6 +377,22 @@ export class AlbumManager { } } + // Show or hide the album-source chooser based on whether a global InvokeAI + // backend is configured. With no backend the board option is meaningless, so + // we hide the radios and force the directory source. + _setNewAlbumInvokeAvailable(available) { + if (this.elements.newAlbumSourceGroup) { + this.elements.newAlbumSourceGroup.hidden = !available; + } + if (!available) { + const directoryRadio = document.querySelector('input[name="newAlbumSourceType"][value="directory"]'); + if (directoryRadio) { + directoryRadio.checked = true; + } + this.toggleNewAlbumSourceSections(); + } + } + // Build the InvokeAI-root input row: a text input plus the same 📁 // filetree-picker button used by the image-path rows. _createInvokeRootRow(container, initialValue = "") { @@ -499,10 +526,12 @@ export class AlbumManager { // Prefill the URL — and surface the credential fallback — from the // global InvokeAI settings when available. this._settingsInvokeDefaults = null; + let hasBackend = false; if (this.elements.newAlbumInvokeUrl) { this.elements.newAlbumInvokeUrl.value = ""; try { const config = await fetchJson("invokeai/config"); + hasBackend = !!(config.url && config.url.trim()); if (config.url) { this.elements.newAlbumInvokeUrl.value = config.url; } @@ -516,6 +545,8 @@ export class AlbumManager { } this._applySettingsCredentialDefaults(); } + // The board-album option only makes sense with a configured backend. + this._setNewAlbumInvokeAvailable(hasBackend); } // The backend reuses the settings-panel credentials when the album form @@ -1261,9 +1292,19 @@ export class AlbumManager { // Reflect the (immutable) source type and show the matching section const isBoardAlbum = album.source_type === "invokeai_board"; - editForm.querySelectorAll(".edit-album-source-radio").forEach((radio) => { - radio.checked = radio.value === (album.source_type || "directory"); - }); + const sourceDisplay = editForm.querySelector(".edit-album-source-display"); + if (sourceDisplay) { + sourceDisplay.textContent = isBoardAlbum ? "An InvokeAI Image Gallery Board" : "Directory of Image Files"; + } + // Show the (immutable) source line only when an InvokeAI backend is + // configured — or when this album is itself a board album, so its type is + // never silently hidden. + const sourceGroup = editForm.querySelector(".edit-album-source-group"); + if (sourceGroup) { + this._isInvokeAIConfigured().then((available) => { + sourceGroup.hidden = !(available || isBoardAlbum); + }); + } const directorySection = editForm.querySelector(".edit-album-directory-section"); const invokeSection = editForm.querySelector(".edit-album-invokeai-section"); if (directorySection) { diff --git a/photomap/frontend/templates/modules/album-manager.html b/photomap/frontend/templates/modules/album-manager.html index f97e2678..18f14f92 100644 --- a/photomap/frontend/templates/modules/album-manager.html +++ b/photomap/frontend/templates/modules/album-manager.html @@ -34,7 +34,7 @@

Add New Album

-
+ -
- -
- - -
- An album's source type cannot be changed after creation. +
diff --git a/tests/frontend/invokeai-album-source.test.js b/tests/frontend/invokeai-album-source.test.js index 2e30feaf..797ea1b8 100644 --- a/tests/frontend/invokeai-album-source.test.js +++ b/tests/frontend/invokeai-album-source.test.js @@ -221,6 +221,12 @@ describe("AlbumManager add-album payloads", () => { describe("AlbumManager settings-credential surfacing", () => { function makeInvokeSectionStub() { document.body.innerHTML = ` + +
+
@@ -231,6 +237,9 @@ describe("AlbumManager settings-credential surfacing", () => { `; const elements = {}; [ + "newAlbumSourceGroup", + "newAlbumDirectorySection", + "newAlbumInvokeAISection", "newAlbumInvokeUrl", "newAlbumInvokeUsername", "newAlbumInvokePassword", @@ -246,6 +255,9 @@ describe("AlbumManager settings-credential surfacing", () => { _setInvokeHint: AlbumManager.prototype._setInvokeHint, _createInvokeRootRow: AlbumManager.prototype._createInvokeRootRow, _applySettingsCredentialDefaults: AlbumManager.prototype._applySettingsCredentialDefaults, + _setNewAlbumInvokeAvailable: AlbumManager.prototype._setNewAlbumInvokeAvailable, + getNewAlbumSourceType: AlbumManager.prototype.getNewAlbumSourceType, + toggleNewAlbumSourceSections: AlbumManager.prototype.toggleNewAlbumSourceSections, initializeNewAlbumInvokeSection: AlbumManager.prototype.initializeNewAlbumInvokeSection, }; } @@ -311,6 +323,25 @@ describe("AlbumManager settings-credential surfacing", () => { manager._applySettingsCredentialDefaults(); expect(manager.elements.newAlbumInvokeUsername.value).toBe("bob"); }); + + test("reveals the album-source chooser when a backend is configured", async () => { + const manager = makeInvokeSectionStub(); + fetchJson.mockResolvedValue({ url: "http://localhost:9090", username: "", has_password: false, board_id: "" }); + + await manager.initializeNewAlbumInvokeSection(); + + expect(manager.elements.newAlbumSourceGroup.hidden).toBe(false); + }); + + test("hides the chooser and forces the directory source with no backend", async () => { + const manager = makeInvokeSectionStub(); + fetchJson.mockResolvedValue({ url: "", username: "", has_password: false, board_id: "" }); + + await manager.initializeNewAlbumInvokeSection(); + + expect(manager.elements.newAlbumSourceGroup.hidden).toBe(true); + expect(document.querySelector('input[name="newAlbumSourceType"][value="directory"]').checked).toBe(true); + }); }); describe("AlbumManager indexing-progress robustness", () => {