fix: tags deep link renders and admin modals close

admin.html loaded tags-page.js after admin.js, so opening the page at #/tags
found no TagsPageManager and rendered nothing; the script order is now
theme-bridge, html-escape, api-client, png-metadata, (list-state, sse-stream,
data-actions), viewer, comfy-metadata, prompt-list-sort, view-router,
tags-page, admin. Route resolution lives in the pure ViewRouter.resolveView().

The auto-tag modal's close buttons called a bare hideModal(), which only
gallery.js defines; they call window.admin.hideModal(). PromptAdmin is
constructed from startAdmin() on DOMContentLoaded (or immediately when the
document is already parsed) instead of at script evaluation time.
This commit is contained in:
Vito Sansevero
2026-10-08 18:01:34 -07:00
parent de194af8bc
commit 247abc3475
4 changed files with 97 additions and 31 deletions
+30
View File
@@ -0,0 +1,30 @@
// Run with: node --test tests/js/
const test = require("node:test");
const assert = require("node:assert/strict");
const { resolveView, VIEWS } = require("../../web/js/view-router.js");
test("the tags deep link and its sub-routes resolve to the tags view", () => {
assert.equal(resolveView("#/tags"), "tags");
assert.equal(resolveView("#/tags/"), "tags");
assert.equal(resolveView("#/tags/landscape,portrait?mode=or"), "tags");
});
test("an empty, bare or unknown hash resolves to the prompts dashboard", () => {
assert.equal(resolveView(""), "prompts");
assert.equal(resolveView("#"), "prompts");
assert.equal(resolveView("#/"), "prompts");
assert.equal(resolveView("#/other"), "prompts");
assert.equal(resolveView("#/tagsX"), "prompts", "prefix must end at a path boundary");
assert.equal(resolveView("#tags"), "prompts");
});
test("non-string input resolves to the prompts dashboard", () => {
assert.equal(resolveView(undefined), "prompts");
assert.equal(resolveView(null), "prompts");
assert.equal(resolveView(42), "prompts");
});
test("every resolved value is a listed view", () => {
assert.ok(Object.isFrozen(VIEWS));
for (const hash of ["#/tags", "", "#/x"]) assert.ok(VIEWS.includes(resolveView(hash)));
});
+4 -3
View File
@@ -1043,7 +1043,7 @@
<div class="bg-pm-surface rounded-pm-lg p-4 max-w-lg w-full mx-4 border border-pm shadow-pm">
<div class="flex items-center justify-between mb-4">
<h3 class="text-sm font-semibold text-pm">Auto Tag Images</h3>
<button onclick="hideModal('autoTagModal')" class="p-1 rounded-pm-sm hover:bg-pm-hover transition-colors">
<button onclick="window.admin.hideModal('autoTagModal')" class="p-1 rounded-pm-sm hover:bg-pm-hover transition-colors">
<svg class="w-4 h-4 text-pm-secondary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
@@ -1157,7 +1157,7 @@ No sentences, no commentary, no captions. Only tags. Keep length short but descr
<!-- Action Buttons -->
<div class="flex justify-end gap-2">
<button onclick="hideModal('autoTagModal')"
<button onclick="window.admin.hideModal('autoTagModal')"
class="px-3 py-1.5 bg-pm-surface border border-pm hover:bg-pm-hover text-pm text-xs font-medium rounded-pm-sm transition-colors">
Cancel
</button>
@@ -1399,7 +1399,8 @@ No sentences, no commentary, no captions. Only tags. Keep length short but descr
<script src="/prompt_manager/js/comfy-metadata.js"></script>
<script src="/prompt_manager/js/prompt-list-sort.js"></script>
<script src="/prompt_manager/js/admin.js"></script>
<script src="/prompt_manager/js/view-router.js"></script>
<script src="/prompt_manager/js/tags-page.js"></script>
<script src="/prompt_manager/js/admin.js"></script>
</body>
</html>
+36 -28
View File
@@ -60,8 +60,7 @@
}
handleRoute() {
const hash = window.location.hash;
if (hash === '#/tags' || hash.startsWith('#/tags/')) {
if (ViewRouter.resolveView(window.location.hash) === "tags") {
this.showTagsPage();
} else {
this.showDashboard();
@@ -3866,33 +3865,42 @@ Seed: ${this.currentMetadata.seed || 'Unknown'}`;
}
}
// Initialize the admin interface
const admin = new PromptAdmin();
window.admin = admin;
/** Build the dashboard; needs the DOM, so it runs on DOMContentLoaded. */
function startAdmin() {
// Initialize the admin interface
const admin = new PromptAdmin();
window.admin = admin;
// Add event listeners for prompt selection
document.addEventListener("change", function (e) {
if (e.target.classList.contains("prompt-checkbox")) {
const promptId = parseInt(e.target.dataset.id);
if (e.target.checked) {
admin.selectedPrompts.add(promptId);
} else {
admin.selectedPrompts.delete(promptId);
// Add event listeners for prompt selection
document.addEventListener("change", function (e) {
if (e.target.classList.contains("prompt-checkbox")) {
const promptId = parseInt(e.target.dataset.id);
if (e.target.checked) {
admin.selectedPrompts.add(promptId);
} else {
admin.selectedPrompts.delete(promptId);
}
admin.updateBulkActionButtons();
const allCheckboxes = document.querySelectorAll(".prompt-checkbox");
const checkedCheckboxes = document.querySelectorAll(".prompt-checkbox:checked");
const selectAllCheckbox = document.getElementById("selectAll");
selectAllCheckbox.checked = allCheckboxes.length === checkedCheckboxes.length;
selectAllCheckbox.indeterminate = checkedCheckboxes.length > 0 && checkedCheckboxes.length < allCheckboxes.length;
}
admin.updateBulkActionButtons();
});
const allCheckboxes = document.querySelectorAll(".prompt-checkbox");
const checkedCheckboxes = document.querySelectorAll(".prompt-checkbox:checked");
const selectAllCheckbox = document.getElementById("selectAll");
selectAllCheckbox.checked = allCheckboxes.length === checkedCheckboxes.length;
selectAllCheckbox.indeterminate = checkedCheckboxes.length > 0 && checkedCheckboxes.length < allCheckboxes.length;
}
});
// Escape closes the gallery modal
document.addEventListener('keydown', function (e) {
const galleryModal = document.getElementById('galleryModal');
if (e.key === 'Escape' && galleryModal && !galleryModal.classList.contains('hidden')) {
admin.closeGallery();
}
});
}
// Escape closes the gallery modal
document.addEventListener('keydown', function (e) {
const galleryModal = document.getElementById('galleryModal');
if (e.key === 'Escape' && galleryModal && !galleryModal.classList.contains('hidden')) {
admin.closeGallery();
}
});
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", startAdmin);
} else {
startAdmin();
}
+27
View File
@@ -0,0 +1,27 @@
/**
* Hash-route resolution for the admin page: which top-level view a
* location.hash selects. Kept pure so the #/tags deep link is testable.
*
* Loadable in the browser (window.ViewRouter) and in Node for tests.
*/
(function (root) {
"use strict";
const VIEWS = Object.freeze(["prompts", "tags"]);
const TAGS_PREFIX = "#/tags";
/**
* @param {string} hash - window.location.hash, including the leading "#"
* @returns {"prompts"|"tags"}
*/
function resolveView(hash) {
if (typeof hash !== "string") return "prompts";
if (hash === TAGS_PREFIX) return "tags";
if (hash.startsWith(TAGS_PREFIX + "/")) return "tags";
return "prompts";
}
const api = { resolveView, VIEWS };
if (typeof module !== "undefined" && module.exports) module.exports = api;
else root.ViewRouter = api;
})(typeof window !== "undefined" ? window : globalThis);