Fix custom Inputs indexing and expose an explicit scan button
Co-authored-by: MajoorWaldi <129623145+MajoorWaldi@users.noreply.github.com>
This commit is contained in:
co-authored by
MajoorWaldi
parent
228fabf084
commit
14d773f06d
@@ -211,6 +211,7 @@ docs/COMFYUI_FRONTEND_IMPROVEMENTS.md
|
||||
.github/agents/badge-card-qa.agent.md
|
||||
.mjr_index_directory_override
|
||||
.mjr_output_directory_override
|
||||
.mjr_input_directory_override
|
||||
|
||||
coverage/
|
||||
ComfyUI-Majoor-AssetsManager_release.zip
|
||||
|
||||
+25
-25
@@ -1,12 +1,12 @@
|
||||
import { Zt as e, g as t, y as n } from "./viewerRuntimeHosts-DyiuTSfr.js";
|
||||
import { B as r, C as i, I as a, M as o, O as s, P as c, R as l, V as u, j as d, m as f, o as p, r as m, rt as h, v as g, w as _, x as v } from "./events-DrYpMD-I.js";
|
||||
import { a as y, i as b, o as x, s as S } from "./graphTraversal-BIYmTTPW.js";
|
||||
import { _ as C, g as w, m as T, n as E, p as ee, r as D, w as O } from "./Viewer-DXsFcJRb.js";
|
||||
import { r as k, v as A } from "./SidebarWorkflowSection-BEJKYrrz.js";
|
||||
import { _ as j, a as M, c as N, d as P, f as F, g as I, h as L, i as te, l as R, m as z, o as ne, p as B, r as re, s as V, t as ie, u as H, v as U } from "./openMajoorSettings-CjYabCh7.js";
|
||||
import { a as W, n as ae, r as oe } from "./model3dRenderer-CWRtZrSS.js";
|
||||
import { i as se, o as ce, r as le, t as ue } from "./geninfoParser-DS9m_bHm.js";
|
||||
import { t as de } from "./genInfo-D0BtEEbx.js";
|
||||
import { $t as e, g as t, y as n } from "./viewerRuntimeHosts-BBd8uK6j.js";
|
||||
import { B as r, C as i, I as a, M as o, O as s, P as c, R as l, V as u, j as d, m as f, o as p, r as m, rt as h, v as g, w as _, x as v } from "./events-CoI83paj.js";
|
||||
import { a as y, i as b, o as x, s as S } from "./graphTraversal-C94Q5bJN.js";
|
||||
import { _ as C, g as w, m as T, n as E, p as ee, r as D, w as O } from "./Viewer-DWgaGos-.js";
|
||||
import { r as k, v as A } from "./SidebarWorkflowSection-DpoPGJrq.js";
|
||||
import { _ as j, a as M, c as N, d as P, f as F, g as I, h as L, i as te, l as R, m as z, o as ne, p as B, r as re, s as V, t as ie, u as H, v as U } from "./openMajoorSettings-CxqtW_yN.js";
|
||||
import { a as W, n as ae, r as oe } from "./model3dRenderer-e5uWwIMW.js";
|
||||
import { i as se, o as ce, r as le, t as ue } from "./geninfoParser-Bwtqk4p6.js";
|
||||
import { t as de } from "./genInfo-y4P7FXnU.js";
|
||||
//#region ui/features/viewer/floatingViewerConstants.ts
|
||||
var G = Object.freeze({
|
||||
SIMPLE: "simple",
|
||||
@@ -169,7 +169,7 @@ function Ee(e, t = null, { kind: n = "" } = {}) {
|
||||
} catch (e) {
|
||||
console.debug?.(e);
|
||||
}
|
||||
if (!(!i || e?.paused)) {
|
||||
if (i && !e?.paused) {
|
||||
try {
|
||||
let t = e;
|
||||
if (typeof t?.requestVideoFrameCallback == "function") {
|
||||
@@ -186,7 +186,7 @@ function Ee(e, t = null, { kind: n = "" } = {}) {
|
||||
}
|
||||
}
|
||||
}, P = () => {
|
||||
M(), !(!i || e?.paused) && N();
|
||||
M(), i && !e?.paused && N();
|
||||
}, F = (e) => {
|
||||
try {
|
||||
e?.stopPropagation?.();
|
||||
@@ -200,7 +200,7 @@ function Ee(e, t = null, { kind: n = "" } = {}) {
|
||||
}
|
||||
D();
|
||||
}, L = (t, n) => {
|
||||
if (F(n), !(!a || !(e instanceof HTMLVideoElement))) {
|
||||
if (F(n), a && e instanceof HTMLVideoElement) {
|
||||
try {
|
||||
e.pause?.();
|
||||
} catch (e) {
|
||||
@@ -334,7 +334,7 @@ function J(e, t) {
|
||||
return n.className = `mjr-mfv-label label-${t}`, n.textContent = e, n;
|
||||
}
|
||||
function je(e) {
|
||||
if (!(!e || typeof e.play != "function")) try {
|
||||
if (e && typeof e.play == "function") try {
|
||||
let t = e.play();
|
||||
t && typeof t.catch == "function" && t.catch(() => {});
|
||||
} catch (e) {
|
||||
@@ -342,7 +342,7 @@ function je(e) {
|
||||
}
|
||||
}
|
||||
function Me(e) {
|
||||
if (!(!e || typeof e.play != "function")) try {
|
||||
if (e && typeof e.play == "function") try {
|
||||
let t = e.play();
|
||||
t && typeof t.catch == "function" && t.catch(() => {
|
||||
try {
|
||||
@@ -646,7 +646,7 @@ var Xe = class {
|
||||
return this._body = c, t.appendChild(c), this._el = t, this._applyExpandedState(), t;
|
||||
}
|
||||
_applyExpandedState() {
|
||||
if (!(!this._el || !this._body)) {
|
||||
if (this._el && this._body) {
|
||||
if (this._el.classList.toggle("is-collapsible", this._collapsible), this._el.classList.toggle("is-collapsed", this._collapsible && !this._expanded), this._toggleBtn) {
|
||||
let e = this._expanded ? "pi pi-chevron-down" : "pi pi-chevron-right";
|
||||
this._toggleBtn.textContent = "";
|
||||
@@ -939,7 +939,7 @@ function ft(e) {
|
||||
index: o
|
||||
});
|
||||
}
|
||||
!a || c.has(a.index) || te(a.widget, X(e.value), t) && (c.add(a.index), l += 1);
|
||||
a && !c.has(a.index) && te(a.widget, X(e.value), t) && (c.add(a.index), l += 1);
|
||||
}
|
||||
return u(), {
|
||||
ok: l > 0,
|
||||
@@ -1725,7 +1725,7 @@ function Zt(e) {
|
||||
], r = /* @__PURE__ */ new Set(), i = [];
|
||||
for (let [e, t] of n) {
|
||||
let n = le(t?.name || t?.value || t || "");
|
||||
if (!(!n || r.has(n)) && (r.add(n), i.push(`${e}: ${n}`), i.length >= 3)) break;
|
||||
if (n && !r.has(n) && (r.add(n), i.push(`${e}: ${n}`), i.length >= 3)) break;
|
||||
}
|
||||
return i.join(" | ");
|
||||
}
|
||||
@@ -2414,7 +2414,7 @@ function En(e, t) {
|
||||
].map((t) => Tn(e[`_media${t}`])).filter(Boolean));
|
||||
for (let r of t) {
|
||||
let t = Tn(r);
|
||||
if (!(!t || n.has(t))) {
|
||||
if (t && !n.has(t)) {
|
||||
try {
|
||||
URL.revokeObjectURL(t);
|
||||
} catch {}
|
||||
@@ -2515,9 +2515,9 @@ function Mn(e, ...t) {
|
||||
return Array.from(new Set([String(e || ""), ...t].join(" ").split(/\s+/).filter(Boolean))).join(" ");
|
||||
}
|
||||
function Nn(e, t) {
|
||||
if (!(!e || !t)) for (let n of Array.from(e.attributes || [])) {
|
||||
if (e && t) for (let n of Array.from(e.attributes || [])) {
|
||||
let e = String(n?.name || "");
|
||||
if (!(!e || e === "class" || e === "style")) try {
|
||||
if (e && e !== "class" && e !== "style") try {
|
||||
t.setAttribute(e, n.value);
|
||||
} catch (e) {
|
||||
console.debug?.(e);
|
||||
@@ -2745,7 +2745,7 @@ function Wn(e) {
|
||||
}, 250);
|
||||
}
|
||||
function Gn(e) {
|
||||
!e._isPopped || e._popoutRestoreGuard || (e._popoutRestoreGuard = !0, window.setTimeout(() => {
|
||||
e._isPopped && !e._popoutRestoreGuard && (e._popoutRestoreGuard = !0, window.setTimeout(() => {
|
||||
try {
|
||||
e.popIn({ closePopupWindow: !1 });
|
||||
} finally {
|
||||
@@ -3487,7 +3487,7 @@ var dr = 0, fr = class {
|
||||
let i = Math.max(0, Math.min(.92, Number(r) || 0));
|
||||
if (!(i > 0)) return;
|
||||
e.save(), e.fillStyle = `rgba(0,0,0,${i})`, e.fillRect(0, 0, t.width, t.height), e.globalCompositeOperation = "destination-out";
|
||||
for (let t of n) !t || !(t.w > 1 && t.h > 1) || e.fillRect(t.x, t.y, t.w, t.h);
|
||||
for (let t of n) t && t.w > 1 && t.h > 1 && e.fillRect(t.x, t.y, t.w, t.h);
|
||||
e.restore();
|
||||
} catch (e) {
|
||||
console.debug?.(e);
|
||||
@@ -3550,7 +3550,7 @@ var dr = 0, fr = class {
|
||||
_bindLayoutObserver() {
|
||||
this._unbindLayoutObserver();
|
||||
let e = this._contentEl;
|
||||
if (!(!e || typeof ResizeObserver > "u")) try {
|
||||
if (e && typeof ResizeObserver < "u") try {
|
||||
this._layoutObserver = new ResizeObserver(() => {
|
||||
this._applyTransform();
|
||||
}), this._layoutObserver.observe(e);
|
||||
@@ -3881,7 +3881,7 @@ var dr = 0, fr = class {
|
||||
return i.childNodes.length ? r : null;
|
||||
}
|
||||
_appendIdeogramGenInfoCards(e, t) {
|
||||
if (!(!e || !t)) {
|
||||
if (e && t) {
|
||||
t.highLevelDescription && e.appendChild(this._buildGenInfoCard({
|
||||
title: "High Level Description",
|
||||
accent: "#4CAF50",
|
||||
@@ -3967,7 +3967,7 @@ var dr = 0, fr = class {
|
||||
return a.className = "mjr-mfv-gen-field-value", a.textContent = n, r.appendChild(i), r.appendChild(a), r;
|
||||
}
|
||||
_bindGenInfoCopy(e, t) {
|
||||
!e || typeof t != "function" || (e.title = "Click to copy", e.addEventListener("click", async (n) => {
|
||||
e && typeof t == "function" && (e.title = "Click to copy", e.addEventListener("click", async (n) => {
|
||||
n.stopPropagation();
|
||||
let r = String(t() || "").trim();
|
||||
if (r) try {
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { J as e, o as t, r as n } from "./events-DrYpMD-I.js";
|
||||
import { t as r } from "./floatingViewerManager-Crq2oCKD.js";
|
||||
import { J as e, o as t, r as n } from "./events-CoI83paj.js";
|
||||
import { t as r } from "./floatingViewerManager-XH4AFW4G.js";
|
||||
//#region ui/features/viewer/LiveStreamTracker.ts
|
||||
var i = !1, a = null, o = null, s = null, c = null, l = null, u = null, d = null, f = null, p = 0, m = 0, h = !1, g = 400, _ = "kj_preview_override", v = /* @__PURE__ */ new Set([
|
||||
"image/jpeg",
|
||||
Vendored
+3
-3
@@ -1,4 +1,4 @@
|
||||
import { a as e, n as t, r as n, s as r } from "./graphTraversal-BIYmTTPW.js";
|
||||
import { a as e, n as t, r as n, s as r } from "./graphTraversal-C94Q5bJN.js";
|
||||
import { KnownNodesAdapter as i, t as a } from "./KnownNodesAdapter-D3kDh32d.js";
|
||||
//#region ui/features/viewer/nodeStream/NodeStreamRegistry.ts
|
||||
var o = [], s = !1;
|
||||
@@ -454,7 +454,7 @@ function Ce(e) {
|
||||
}
|
||||
if (!(e.depth >= F)) for (let t of ye(e.node)) {
|
||||
let n = String(t?.id ?? "");
|
||||
!n || i.has(n) || (i.add(n), r.push({
|
||||
n && !i.has(n) && (i.add(n), r.push({
|
||||
node: t,
|
||||
depth: e.depth + 1
|
||||
}));
|
||||
@@ -493,7 +493,7 @@ function Z({ force: e = !1 } = {}) {
|
||||
return;
|
||||
}
|
||||
let i = we(r);
|
||||
!e && n === M && i === j || (M = n, j = i, D(r));
|
||||
(e || n !== M || i !== j) && (M = n, j = i, D(r));
|
||||
}
|
||||
function Q() {
|
||||
let e = T;
|
||||
Vendored
+1819
-1778
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
import { F as e, Zt as t, v as n } from "./viewerRuntimeHosts-DyiuTSfr.js";
|
||||
import { St as r, m as i, n as a } from "./events-DrYpMD-I.js";
|
||||
import { Y as o } from "./Viewer-DXsFcJRb.js";
|
||||
import { A as s, C as c, E as l, G as u, J as d, K as f, L as ee, N as p, R as te, T as m, U as ne, V as h, Y as g, b as _, ft as v, j as y, k as b, lt as x, rt as S, ut as C, z as w } from "./mjr-primevue-BlSRoWcG.js";
|
||||
import { $t as e, F as t, v as n } from "./viewerRuntimeHosts-BBd8uK6j.js";
|
||||
import { St as r, m as i, n as a } from "./events-CoI83paj.js";
|
||||
import { Y as o } from "./Viewer-DWgaGos-.js";
|
||||
import { A as s, C as c, E as l, G as u, J as d, K as f, L as ee, N as p, R as te, T as m, U as ne, V as h, Y as g, b as _, ft as v, j as y, k as b, lt as x, rt as S, ut as C, z as w } from "./mjr-primevue-BOf2ACk_.js";
|
||||
//#region ui/vue/components/common/TagsEditor.vue?vue&type=script&setup=true&lang.ts
|
||||
var T = ["aria-busy"], E = ["aria-label"], re = {
|
||||
key: 0,
|
||||
@@ -111,7 +111,7 @@ var T = ["aria-busy"], E = ["aria-label"], re = {
|
||||
q = o;
|
||||
let s = null;
|
||||
try {
|
||||
s = await e(N.asset, a, o ? { signal: o.signal } : {});
|
||||
s = await t(N.asset, a, o ? { signal: o.signal } : {});
|
||||
} catch (e) {
|
||||
console.error("Failed to update tags:", e);
|
||||
}
|
||||
@@ -123,8 +123,8 @@ var T = ["aria-busy"], E = ["aria-label"], re = {
|
||||
}
|
||||
break;
|
||||
}
|
||||
let e = [...J];
|
||||
I.value = e, P("update:modelValue", e), t(s?.error || i("toast.tagsUpdateFailed", "Failed to update tags"), "error"), G = !1, q = null, U.value = !1;
|
||||
let t = [...J];
|
||||
I.value = t, P("update:modelValue", t), e(s?.error || i("toast.tagsUpdateFailed", "Failed to update tags"), "error"), G = !1, q = null, U.value = !1;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -140,18 +140,18 @@ var T = ["aria-busy"], E = ["aria-label"], re = {
|
||||
} catch (e) {
|
||||
console.debug?.(e);
|
||||
}
|
||||
if (K || (I.value = [...c]), fe(c), t(i("toast.tagsUpdated", "Tags updated"), "success", 1e3), !K) break;
|
||||
if (K || (I.value = [...c]), fe(c), e(i("toast.tagsUpdated", "Tags updated"), "success", 1e3), !K) break;
|
||||
K = !1;
|
||||
}
|
||||
if (n >= 10) {
|
||||
let e = [...J];
|
||||
I.value = e;
|
||||
let t = [...J];
|
||||
I.value = t;
|
||||
try {
|
||||
N.asset.tags = [...e];
|
||||
N.asset.tags = [...t];
|
||||
} catch (e) {
|
||||
console.debug?.(e);
|
||||
}
|
||||
P("update:modelValue", e), t(i("toast.tagsUpdateFailed", "Failed to update tags"), "error");
|
||||
P("update:modelValue", t), e(i("toast.tagsUpdateFailed", "Failed to update tags"), "error");
|
||||
}
|
||||
G = !1, q = null, U.value = !1;
|
||||
}
|
||||
@@ -174,14 +174,14 @@ var T = ["aria-busy"], E = ["aria-label"], re = {
|
||||
console.debug?.(e);
|
||||
}
|
||||
});
|
||||
function $(e) {
|
||||
function $(t) {
|
||||
if (N.disabled) return;
|
||||
let n = se(e);
|
||||
let n = se(t);
|
||||
if (!n) return;
|
||||
let r = A(n);
|
||||
if (r && !I.value.some((e) => A(e) === r)) {
|
||||
if (I.value.length >= D) {
|
||||
t(i("toast.maxTagsReached", "Maximum number of tags reached"), "warning");
|
||||
e(i("toast.maxTagsReached", "Maximum number of tags reached"), "warning");
|
||||
return;
|
||||
}
|
||||
I.value = [...I.value, n], Q();
|
||||
+335
-332
File diff suppressed because it is too large
Load Diff
+7
-7
@@ -1,9 +1,9 @@
|
||||
import { a as e, i as t } from "./viewerRuntimeHosts-DyiuTSfr.js";
|
||||
import { r as n } from "./events-DrYpMD-I.js";
|
||||
import { a as r, c as i, i as a, l as o, o as s, s as c, t as l, u } from "./Viewer-DXsFcJRb.js";
|
||||
import { i as d, r as f } from "./floatingViewerManager-Crq2oCKD.js";
|
||||
import { B as p, C as m, D as h, E as g, G as _, J as v, K as y, L as b, N as x, O as S, T as C, U as w, V as T, dt as E, ft as D, j as O, k, lt as A, rt as j, ut as M, w as N, z as P } from "./mjr-primevue-BlSRoWcG.js";
|
||||
import { t as F } from "./TagsEditor-BiRWztXF.js";
|
||||
import { a as e, i as t } from "./viewerRuntimeHosts-BBd8uK6j.js";
|
||||
import { r as n } from "./events-CoI83paj.js";
|
||||
import { a as r, c as i, i as a, l as o, o as s, s as c, t as l, u } from "./Viewer-DWgaGos-.js";
|
||||
import { i as d, r as f } from "./floatingViewerManager-XH4AFW4G.js";
|
||||
import { B as p, C as m, D as h, E as g, G as _, J as v, K as y, L as b, N as x, O as S, T as C, U as w, V as T, dt as E, ft as D, j as O, k, lt as A, rt as j, ut as M, w as N, z as P } from "./mjr-primevue-BOf2ACk_.js";
|
||||
import { t as F } from "./TagsEditor-BXjsZ3IO.js";
|
||||
//#endregion
|
||||
//#region ui/vue/components/viewer/FloatingViewerHost.vue
|
||||
var I = /* @__PURE__ */ x({
|
||||
@@ -112,7 +112,7 @@ var I = /* @__PURE__ */ x({
|
||||
});
|
||||
}
|
||||
async function J(e, t, n = "main") {
|
||||
if (!(!e || e.type !== "item" || e.disabled)) {
|
||||
if (e && e.type === "item" && !e.disabled) {
|
||||
if (Array.isArray(e.submenu) && e.submenu.length) {
|
||||
q(e, t);
|
||||
return;
|
||||
@@ -1,4 +1,4 @@
|
||||
import { a as e, n as t, s as n } from "./graphTraversal-BIYmTTPW.js";
|
||||
import { a as e, n as t, s as n } from "./graphTraversal-C94Q5bJN.js";
|
||||
//#region ui/utils/ids.ts
|
||||
function r(e) {
|
||||
try {
|
||||
@@ -106,6 +106,7 @@ var m = 200, h = 0, g = {
|
||||
COLLECT_FILES: "/mjr/am/collect-files",
|
||||
TOOLS_STATUS: "/mjr/am/tools/status",
|
||||
SETTINGS_OUTPUT_DIRECTORY: "/mjr/am/settings/output-directory",
|
||||
SETTINGS_INPUT_DIRECTORY: "/mjr/am/settings/input-directory",
|
||||
SETTINGS_INDEX_DIRECTORY: "/mjr/am/settings/index-directory",
|
||||
SETTINGS_WORKFLOW_ROOTS: "/mjr/am/settings/workflow-roots",
|
||||
SETTINGS_METADATA_FALLBACK: "/mjr/am/settings/metadata-fallback",
|
||||
@@ -1163,7 +1164,7 @@ function U() {
|
||||
}
|
||||
function W(e, t, n) {
|
||||
let r = B.get(String(e || ""));
|
||||
if (!(!r || !r.size)) for (let i of Array.from(r)) try {
|
||||
if (r && r.size) for (let i of Array.from(r)) try {
|
||||
i(t, n, e);
|
||||
} catch (e) {
|
||||
console.debug?.(e);
|
||||
Vendored
+10
-10
@@ -1,6 +1,6 @@
|
||||
import { t as e } from "./rolldown-runtime-Dy4uBu1J.js";
|
||||
import { _ as t, d as n, o as r, s as i, t as a } from "./viewerRuntimeHosts-DyiuTSfr.js";
|
||||
import { o, r as s, z as c } from "./events-DrYpMD-I.js";
|
||||
import { _ as t, d as n, o as r, s as i, t as a } from "./viewerRuntimeHosts-BBd8uK6j.js";
|
||||
import { o, r as s, z as c } from "./events-CoI83paj.js";
|
||||
//#region ui/features/panel/panelRuntimeRefs.ts
|
||||
var l = null;
|
||||
function u(e) {
|
||||
@@ -98,11 +98,11 @@ var te = /* @__PURE__ */ e({
|
||||
teardownFloatingViewerManager: () => we
|
||||
}), v = null, y = null;
|
||||
async function ne() {
|
||||
return v || (y ||= import("./FloatingViewer-C_N3LZxW.js").then((e) => (v = e.FloatingViewer, v)), y);
|
||||
return v || (y ||= import("./FloatingViewer-38MC8yxJ.js").then((e) => (v = e.FloatingViewer, v)), y);
|
||||
}
|
||||
var b = null, re = null;
|
||||
async function ie() {
|
||||
if (!b) return re ||= import("./NodeStreamController-DpGjUOfx.js").then((e) => {
|
||||
if (!b) return re ||= import("./NodeStreamController-DaqaPtXc.js").then((e) => {
|
||||
b = e.setNodeStreamActive;
|
||||
}), re;
|
||||
}
|
||||
@@ -180,7 +180,7 @@ function se() {
|
||||
}
|
||||
}
|
||||
function F(e) {
|
||||
typeof window > "u" || window.dispatchEvent(new CustomEvent(s.MFV_VISIBILITY_CHANGED, { detail: { visible: !!e } }));
|
||||
typeof window < "u" && window.dispatchEvent(new CustomEvent(s.MFV_VISIBILITY_CHANGED, { detail: { visible: !!e } }));
|
||||
}
|
||||
function I(e) {
|
||||
e && (e.setLiveActive(T), e.setPreviewActive(E), e.setNodeStreamActive?.(D));
|
||||
@@ -310,7 +310,7 @@ function B(e) {
|
||||
}
|
||||
}
|
||||
function V() {
|
||||
k || typeof window > "u" || (window.addEventListener(s.SELECTION_CHANGED, B), k = !0);
|
||||
k || typeof window < "u" && (window.addEventListener(s.SELECTION_CHANGED, B), k = !0);
|
||||
}
|
||||
function H() {
|
||||
typeof window < "u" && window.removeEventListener(s.SELECTION_CHANGED, B), k = !1, N();
|
||||
@@ -409,7 +409,7 @@ var Y = {
|
||||
},
|
||||
async upsertWithContent(e) {
|
||||
let t = await M(), n = !!t.isVisible;
|
||||
!n && o.MFV_LIVE_AUTO_OPEN === !1 || (t.show(), I(t), V(), ce(t, e), n || F(!0));
|
||||
(n || o.MFV_LIVE_AUTO_OPEN !== !1) && (t.show(), I(t), V(), ce(t, e), n || F(!0));
|
||||
},
|
||||
setLiveActive(e) {
|
||||
T = !!e, S?.setLiveActive(T);
|
||||
@@ -433,7 +433,7 @@ var Y = {
|
||||
async feedPreviewBlob(e, t = {}) {
|
||||
if (!Y.canAcceptPreviewBlob()) return;
|
||||
let n = await M(), r = !!n.isVisible;
|
||||
!r && o.MFV_PREVIEW_AUTO_OPEN === !1 || (n.isVisible || n.show(), I(n), n.loadPreviewBlob(e, ...Object.keys(t).length ? [t] : []), r || F(!0));
|
||||
(r || o.MFV_PREVIEW_AUTO_OPEN !== !1) && (n.isVisible || n.show(), I(n), n.loadPreviewBlob(e, ...Object.keys(t).length ? [t] : []), r || F(!0));
|
||||
},
|
||||
toggleNodeStream() {
|
||||
Y.setNodeStreamActive(!D);
|
||||
@@ -458,7 +458,7 @@ var Y = {
|
||||
async feedNodeStream(e) {
|
||||
if (!D) return;
|
||||
let t = await M(), n = !!t.isVisible;
|
||||
!n && o.MFV_NODE_STREAM_AUTO_OPEN === !1 || (t.isVisible || (t.show(), V()), I(t), L(t), ce(t, e), n || F(!0));
|
||||
(n || o.MFV_NODE_STREAM_AUTO_OPEN !== !1) && (t.isVisible || (t.show(), V()), I(t), L(t), ce(t, e), n || F(!0));
|
||||
}
|
||||
}, X = !1, pe = () => Y.open(), me = () => Y.close(), he = () => Y.toggle(), Z = () => Y.toggleLive(), ge = () => Y.togglePreview(), _e = () => Y.toggleNodeStream(), ve = () => Y.popOut(), ye = () => {
|
||||
try {
|
||||
@@ -520,7 +520,7 @@ var Q = (e) => {
|
||||
}
|
||||
};
|
||||
function $() {
|
||||
X || typeof window > "u" || !window?.addEventListener || (window.addEventListener(s.MFV_OPEN, pe), window.addEventListener(s.MFV_CLOSE, me), window.addEventListener(s.MFV_TOGGLE, he), window.addEventListener(s.MFV_LIVE_TOGGLE, Z), window.addEventListener(s.MFV_PREVIEW_TOGGLE, ge), window.addEventListener(s.MFV_NODESTREAM_TOGGLE, _e), window.addEventListener(s.MFV_POPOUT, ve), window.addEventListener(s.SETTINGS_CHANGED, be), window.addEventListener("keydown", Q, !0), window.addEventListener("beforeunload", ye), X = !0);
|
||||
X || typeof window < "u" && window?.addEventListener && (window.addEventListener(s.MFV_OPEN, pe), window.addEventListener(s.MFV_CLOSE, me), window.addEventListener(s.MFV_TOGGLE, he), window.addEventListener(s.MFV_LIVE_TOGGLE, Z), window.addEventListener(s.MFV_PREVIEW_TOGGLE, ge), window.addEventListener(s.MFV_NODESTREAM_TOGGLE, _e), window.addEventListener(s.MFV_POPOUT, ve), window.addEventListener(s.SETTINGS_CHANGED, be), window.addEventListener("keydown", Q, !0), window.addEventListener("beforeunload", ye), X = !0);
|
||||
}
|
||||
function Ce() {
|
||||
if (typeof window > "u" || !window?.removeEventListener) {
|
||||
@@ -1,8 +1,8 @@
|
||||
import { t as e } from "./rolldown-runtime-Dy4uBu1J.js";
|
||||
import { m as t, o as n } from "./events-DrYpMD-I.js";
|
||||
import { h as r } from "./Viewer-DXsFcJRb.js";
|
||||
import { i, n as a, r as o, t as s, u as c } from "./SidebarWorkflowSection-BEJKYrrz.js";
|
||||
import { D as l, E as u, N as d, O as f, T as p, V as m, dt as h, ft as g, k as _, lt as v } from "./mjr-primevue-BlSRoWcG.js";
|
||||
import { m as t, o as n } from "./events-CoI83paj.js";
|
||||
import { h as r } from "./Viewer-DWgaGos-.js";
|
||||
import { i, n as a, r as o, t as s, u as c } from "./SidebarWorkflowSection-DpoPGJrq.js";
|
||||
import { D as l, E as u, N as d, O as f, T as p, V as m, dt as h, ft as g, k as _, lt as v } from "./mjr-primevue-BOf2ACk_.js";
|
||||
//#region ui/vue/components/viewer/ViewerMetadataBlock.vue?vue&type=script&setup=true&lang.ts
|
||||
var y = { style: {
|
||||
display: "flex",
|
||||
@@ -109,7 +109,7 @@ var y = { style: {
|
||||
let t = Object.entries(e || {});
|
||||
if (!t.length) return !1;
|
||||
let n = 0;
|
||||
for (let [, e] of t.slice(0, 50)) if (!(!e || typeof e != "object") && (e.inputs && typeof e.inputs == "object" && (n += 1), n >= 2)) return !0;
|
||||
for (let [, e] of t.slice(0, 50)) if (e && typeof e == "object" && (e.inputs && typeof e.inputs == "object" && (n += 1), n >= 2)) return !0;
|
||||
} catch {
|
||||
return !1;
|
||||
}
|
||||
+5
-5
@@ -22,7 +22,7 @@ function t(e) {
|
||||
let t = Object.entries(e || {});
|
||||
if (!t.length) return !1;
|
||||
let n = 0;
|
||||
for (let [, e] of t.slice(0, 50)) if (!(!e || typeof e != "object") && (e.inputs && typeof e.inputs == "object" && (n += 1), n >= 2)) return !0;
|
||||
for (let [, e] of t.slice(0, 50)) if (e && typeof e == "object" && (e.inputs && typeof e.inputs == "object" && (n += 1), n >= 2)) return !0;
|
||||
return !1;
|
||||
} catch {
|
||||
return !1;
|
||||
@@ -195,7 +195,7 @@ function l(e, t, n) {
|
||||
}
|
||||
function u(e, t) {
|
||||
let n = Number(e), r = Number(t);
|
||||
if (!(!Number.isFinite(n) || !Number.isFinite(r))) return n + r;
|
||||
if (Number.isFinite(n) && Number.isFinite(r)) return n + r;
|
||||
}
|
||||
function d(e, t) {
|
||||
if (e == null || e === "") return "";
|
||||
@@ -227,7 +227,7 @@ function p(e, t, n) {
|
||||
}
|
||||
function m(e) {
|
||||
let t = {};
|
||||
for (let [n, r] of Object.entries(e || {})) r != null && r !== "" && (Array.isArray(r) && r.length === 0 || r && typeof r == "object" && !Array.isArray(r) && Object.keys(r).length === 0 || (t[n] = r));
|
||||
for (let [n, r] of Object.entries(e || {})) r != null && r !== "" && (Array.isArray(r) && r.length === 0 || (!r || typeof r != "object" || Array.isArray(r) || Object.keys(r).length !== 0) && (t[n] = r));
|
||||
return t;
|
||||
}
|
||||
function h(e) {
|
||||
@@ -495,7 +495,7 @@ function R(r) {
|
||||
let i = r.geninfo || r.GenInfo || r.generation || null;
|
||||
if (i && typeof i == "object") {
|
||||
let e = {}, t = /* @__PURE__ */ new Set(), n = (e, n) => {
|
||||
if (!(!n || typeof n != "object") && (n.confidence === "override" || n.source === "majoor_geninfo")) for (let n of Array.isArray(e) ? e : [e]) t.add(n);
|
||||
if (n && typeof n == "object" && (n.confidence === "override" || n.source === "majoor_geninfo")) for (let n of Array.isArray(e) ? e : [e]) t.add(n);
|
||||
}, a = i.positive?.value ?? i.positive?.text ?? null, o = i.negative?.value ?? i.negative?.text ?? null;
|
||||
typeof a == "string" && a.trim() && (e.prompt = a), typeof o == "string" && o.trim() && (e.negative_prompt = o), n("prompt", i.positive), n("negative_prompt", i.negative);
|
||||
let s = i.checkpoint?.name ?? i.checkpoint ?? null;
|
||||
@@ -669,7 +669,7 @@ function H(e, t) {
|
||||
return null;
|
||||
}
|
||||
function U(e, t) {
|
||||
if (!(!t || typeof t != "object")) {
|
||||
if (t && typeof t == "object") {
|
||||
for (let [n, r] of Object.entries(t)) if (r != null && r !== "") {
|
||||
if (Array.isArray(r) && Array.isArray(e[n])) {
|
||||
e[n] = G(e[n], r);
|
||||
+1
-1
@@ -25,7 +25,7 @@ function a(e, t = e?.graph) {
|
||||
return r ? `${r}:${n}` : n;
|
||||
}
|
||||
function o(e, t, n, r) {
|
||||
!n || typeof n != "object" || t.has(n) || (t.add(n), e.push({
|
||||
n && typeof n == "object" && !t.has(n) && (t.add(n), e.push({
|
||||
graph: n,
|
||||
label: r
|
||||
}));
|
||||
+9
-9
@@ -908,7 +908,7 @@ function vn(e, n, i = t) {
|
||||
};
|
||||
}
|
||||
let C = b ? Array(e.length).fill(mn) : mn, w = (e) => {
|
||||
if (!(!(m.flags & 1) || !m.dirty && !e)) {
|
||||
if (m.flags & 1 && (m.dirty || e)) {
|
||||
if (n) {
|
||||
let t = m.run();
|
||||
if (e || o || y || (b ? t.some((e, t) => oe(e, C[t])) : oe(t, C))) {
|
||||
@@ -2442,8 +2442,8 @@ function _a(e, i) {
|
||||
m & 8 ? (u & 16 && ye(l, i, a), d !== l && p(n, d)) : u & 16 ? m & 16 ? me(l, d, n, r, i, a, o, s, c) : ye(l, i, a, !0) : (u & 8 && p(n, ""), m & 16 && T(d, n, r, i, a, o, s, c));
|
||||
}, pe = (e, t, r, i, a, o, s, c, l) => {
|
||||
e ||= n, t ||= n;
|
||||
let u = e.length, d = t.length, f = Math.min(u, d), p;
|
||||
for (p = 0; p < f; p++) {
|
||||
let u = e.length, d = t.length, f = Math.min(u, d), p = 0;
|
||||
for (; p < f; p++) {
|
||||
let n = t[p] = l ? Ka(t[p]) : Ga(t[p]);
|
||||
v(e[p], n, r, null, a, o, s, c, l);
|
||||
}
|
||||
@@ -3033,7 +3033,7 @@ var bo = vo ? (e) => vo.createHTML(e) : (e) => e, xo = "http://www.w3.org/2000/s
|
||||
},
|
||||
insertStaticContent(e, t, n, r, i, a) {
|
||||
let o = n ? n.previousSibling : t.lastChild;
|
||||
if (i && (i === a || i.nextSibling)) for (; t.insertBefore(i.cloneNode(!0), n), !(i === a || !(i = i.nextSibling)););
|
||||
if (i && (i === a || i.nextSibling)) for (; t.insertBefore(i.cloneNode(!0), n), i !== a && (i = i.nextSibling););
|
||||
else {
|
||||
wo.innerHTML = bo(r === "svg" ? `<svg>${e}</svg>` : r === "mathml" ? `<math>${e}</math>` : e);
|
||||
let i = wo.content;
|
||||
@@ -3318,7 +3318,7 @@ function ys(e, t) {
|
||||
}
|
||||
var bs = (e) => e.charCodeAt(0) === 111 && e.charCodeAt(1) === 110 && e.charCodeAt(2) > 96 && e.charCodeAt(2) < 123, xs = (e, t, n, r, i, s) => {
|
||||
let c = i === "svg";
|
||||
t === "class" ? Ko(e, r, c) : t === "style" ? $o(e, n, r) : a(t) ? o(t) || fs(e, t, n, r, s) : (t[0] === "." ? (t = t.slice(1), !0) : t[0] === "^" ? (t = t.slice(1), !1) : Ss(e, t, r, c)) ? (cs(e, t, r), !e.tagName.includes("-") && (t === "value" || t === "checked" || t === "selected") && ss(e, t, r, c, s, t !== "value")) : e._isVueCE && (Cs(e, t) || e._def.__asyncLoader && (/[A-Z]/.test(t) || !g(r))) ? cs(e, T(t), r, s, t) : (t === "true-value" ? e._trueValue = r : t === "false-value" && (e._falseValue = r), ss(e, t, r, c));
|
||||
t === "class" ? Ko(e, r, c) : t === "style" ? $o(e, n, r) : a(t) ? o(t) || fs(e, t, n, r, s) : (t[0] === "." ? (t = t.slice(1), 1) : t[0] === "^" ? (t = t.slice(1), 0) : Ss(e, t, r, c)) ? (cs(e, t, r), !e.tagName.includes("-") && (t === "value" || t === "checked" || t === "selected") && ss(e, t, r, c, s, t !== "value")) : e._isVueCE && (Cs(e, t) || e._def.__asyncLoader && (/[A-Z]/.test(t) || !g(r))) ? cs(e, T(t), r, s, t) : (t === "true-value" ? e._trueValue = r : t === "false-value" && (e._falseValue = r), ss(e, t, r, c));
|
||||
};
|
||||
function Ss(e, t, n, r) {
|
||||
if (r) return !!(t === "innerHTML" || t === "textContent" || t in e && bs(t) && h(n));
|
||||
@@ -3407,7 +3407,7 @@ function Ms(e, t, n) {
|
||||
}
|
||||
function Ns(e, t) {
|
||||
let n = e.multiple, r = d(t);
|
||||
if (!(n && !r && !p(t))) {
|
||||
if (!n || r || p(t)) {
|
||||
for (let i = 0, a = e.options.length; i < a; i++) {
|
||||
let a = e.options[i], o = Ps(a);
|
||||
if (n) {
|
||||
@@ -4818,7 +4818,7 @@ function ru(e) {
|
||||
el: i,
|
||||
css: r,
|
||||
unload: function() {
|
||||
!s || !n.value || (E(), Vc(i.value) && s.head.removeChild(i.value), n.value = !1, i.value = null);
|
||||
s && n.value && (E(), Vc(i.value) && s.head.removeChild(i.value), n.value = !1, i.value = null);
|
||||
},
|
||||
load: ie,
|
||||
isLoaded: /* @__PURE__ */ Gt(n)
|
||||
@@ -6420,7 +6420,7 @@ var Qd = Vd.extend("ripple", {
|
||||
},
|
||||
onMouseDown: function(e) {
|
||||
var t = this, n = e.currentTarget, r = this.getInk(n);
|
||||
if (!(!r || getComputedStyle(r, null).display === "none")) {
|
||||
if (r && getComputedStyle(r, null).display !== "none") {
|
||||
if (!this.isUnstyled() && Dc(r, "p-ink-active"), r.setAttribute("data-p-ink-active", "false"), !Xc(r) && !nl(r)) {
|
||||
var i = Math.max(Rc(n), $c(n));
|
||||
r.style.height = i + "px", r.style.width = i + "px";
|
||||
@@ -11262,7 +11262,7 @@ var ag = {
|
||||
bindOutsideClickListener: function() {
|
||||
var e = this;
|
||||
this.outsideClickListener || (this.outsideClickListener = function(t) {
|
||||
var n = e.container && !e.container.contains(t.target), r = !(e.target && (e.target === t.target || e.target.contains(t.target)));
|
||||
var n = e.container && !e.container.contains(t.target), r = !e.target || e.target !== t.target && !e.target.contains(t.target);
|
||||
e.overlayVisible && n && r ? e.hide() : !e.popup && n && r && (e.focusedOptionIndex = -1);
|
||||
}, document.addEventListener("click", this.outsideClickListener, !0));
|
||||
},
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { $ as e, F as t, I as n, K as r, L as i, Q as a, T as o, X as s, Z as c, et as l, nt as u, ot as d, rt as f, st as p, tt as m } from "./mjr-primevue-BlSRoWcG.js";
|
||||
import { $ as e, F as t, I as n, K as r, L as i, Q as a, T as o, X as s, Z as c, et as l, nt as u, ot as d, rt as f, st as p, tt as m } from "./mjr-primevue-BOf2ACk_.js";
|
||||
//#region node_modules/pinia/dist/pinia.js
|
||||
var h = typeof window < "u", g, _ = (e) => g = e, v = () => t() && n(y) || g, y = Symbol();
|
||||
function b(e) {
|
||||
+7
-7
@@ -1,9 +1,9 @@
|
||||
import { t as e } from "./rolldown-runtime-Dy4uBu1J.js";
|
||||
import { m as t, mt as n, o as r, r as i } from "./events-DrYpMD-I.js";
|
||||
import { m as t, mt as n, o as r, r as i } from "./events-CoI83paj.js";
|
||||
import { n as a } from "./state-DPiaUMw1.js";
|
||||
//#region ui/features/viewer/model3dCore.ts
|
||||
function o(e, t) {
|
||||
if (!(!e || typeof t != "function")) try {
|
||||
if (e && typeof t == "function") try {
|
||||
e.traverse?.((e) => {
|
||||
(e?.isMesh || e?.isPoints || e?.isLine) && t(e);
|
||||
});
|
||||
@@ -1093,7 +1093,7 @@ function F(e, a, o = {}) {
|
||||
let n = Math.max(1e-4, (Number(e) || 1) / Math.max(1, Number(t) || 1)), r = Ce * n;
|
||||
V.left = -r / 2, V.right = r / 2, V.top = Ce / 2, V.bottom = -Ce / 2, V.updateProjectionMatrix();
|
||||
}, Be = () => {
|
||||
if (!(!G || !H || !K)) {
|
||||
if (G && H && K) {
|
||||
if (q === A.ORTHOGRAPHIC) {
|
||||
let e = _.getBoundingClientRect();
|
||||
Ce = m(V, H, K, Math.max(1e-4, (e.width || 1) / Math.max(1, e.height || 1))) || Ce, z = V;
|
||||
@@ -1147,7 +1147,7 @@ function F(e, a, o = {}) {
|
||||
}
|
||||
});
|
||||
}, Ue = (e) => {
|
||||
!Se || !I || (u(Te), l(I, Se, e, we, Te), Fe());
|
||||
Se && I && (u(Te), l(I, Se, e, we, Te), Fe());
|
||||
}, We = (e) => {
|
||||
if (!G) return;
|
||||
let t = ce[e] || [
|
||||
@@ -1201,9 +1201,9 @@ function F(e, a, o = {}) {
|
||||
P.progressSlider.max = String(e);
|
||||
} catch {}
|
||||
}, Je = () => {
|
||||
!X || !Z.length || (je ? (X.timeScale = 0, je = !1, P.playBtn.textContent = t("model3d.play", "Play")) : (X.timeScale = Ae, Q[$]?.isRunning?.() ? (je = !0, P.playBtn.textContent = t("model3d.pause", "Pause")) : qe($)));
|
||||
X && Z.length && (je ? (X.timeScale = 0, je = !1, P.playBtn.textContent = t("model3d.play", "Play")) : (X.timeScale = Ae, Q[$]?.isRunning?.() ? (je = !0, P.playBtn.textContent = t("model3d.pause", "Pause")) : qe($)));
|
||||
}, Ye = (e, t) => {
|
||||
!e || e.length === 0 || !I || (Z = e, X = new I.AnimationMixer(t), ke = new I.Clock(), Q = Array(e.length).fill(null), P.animSel.innerHTML = "", e.forEach((e, t) => {
|
||||
e && e.length !== 0 && I && (Z = e, X = new I.AnimationMixer(t), ke = new I.Clock(), Q = Array(e.length).fill(null), P.animSel.innerHTML = "", e.forEach((e, t) => {
|
||||
let n = document.createElement("option");
|
||||
n.value = String(t), n.textContent = e.name || `Animation ${t + 1}`, P.animSel.appendChild(n);
|
||||
}), P.animSel.style.display = e.length > 1 ? "" : "none", P.bar.style.display = "flex", M.style.bottom = "54px", qe(0), P.playBtn.addEventListener("click", (e) => {
|
||||
@@ -1597,7 +1597,7 @@ function F(e, a, o = {}) {
|
||||
}
|
||||
Fe();
|
||||
let de = () => {
|
||||
if (!(F || ye || !L || !R || !z)) {
|
||||
if (!F && !ye && L && R && z) {
|
||||
try {
|
||||
if (H?.update?.(), X && ke) {
|
||||
let e = ke.getDelta();
|
||||
+9
-9
@@ -1,6 +1,6 @@
|
||||
import { A as e, E as t, J as n, R as r, S as i, Y as a, b as o, j as s, m as c } from "./events-DrYpMD-I.js";
|
||||
import { a as l, n as u } from "./graphTraversal-BIYmTTPW.js";
|
||||
import { S as d, Y as f, b as p, x as m, y as h } from "./SidebarWorkflowSection-BEJKYrrz.js";
|
||||
import { A as e, E as t, J as n, R as r, S as i, Y as a, b as o, j as s, m as c } from "./events-CoI83paj.js";
|
||||
import { a as l, n as u } from "./graphTraversal-C94Q5bJN.js";
|
||||
import { S as d, Y as f, b as p, x as m, y as h } from "./SidebarWorkflowSection-DpoPGJrq.js";
|
||||
//#region ui/features/viewer/floatingViewerProgress.ts
|
||||
var g = "progress-update", _ = "__MJR_MFV_PROGRESS_SERVICE__";
|
||||
function ee() {
|
||||
@@ -100,7 +100,7 @@ var te = class {
|
||||
return r ? (this._attachApi(r), r) : null;
|
||||
}
|
||||
_attachApi(e) {
|
||||
!e || typeof e.addEventListener != "function" || this._api !== e && (this.dispose({
|
||||
e && typeof e.addEventListener == "function" && this._api !== e && (this.dispose({
|
||||
resetPatchedQueuePrompt: !1,
|
||||
keepState: !0
|
||||
}), this._api = e, this._patchQueuePrompt(e), this._attachApiListeners(e));
|
||||
@@ -340,7 +340,7 @@ function k(e) {
|
||||
let t = fe(e), n = e?.properties && typeof e.properties == "object" ? e.properties : null;
|
||||
if (Array.isArray(e?._mjrSubgraphProxyParams)) for (let n of e._mjrSubgraphProxyParams) {
|
||||
let e = String(n?.label || n?.key || "").trim();
|
||||
!e || t.some(([t]) => String(t) === e) || t.push([e, n?.value]);
|
||||
e && !t.some(([t]) => String(t) === e) && t.push([e, n?.value]);
|
||||
}
|
||||
if (n) for (let [e, r] of Object.entries(n)) ge(e) || r != null && typeof r != "object" && t.push([e, r]);
|
||||
return t.slice(0, 160);
|
||||
@@ -370,7 +370,7 @@ function A(e) {
|
||||
});
|
||||
}
|
||||
function pe(e) {
|
||||
return !!(e && typeof e == "object" && !Array.isArray(e) && !j(e));
|
||||
return !(!e || typeof e != "object" || Array.isArray(e) || j(e));
|
||||
}
|
||||
function me(e) {
|
||||
if (Array.isArray(e)) return e;
|
||||
@@ -415,7 +415,7 @@ function P(e) {
|
||||
let n = [];
|
||||
for (let e of ["required", "optional"]) {
|
||||
let r = t[e];
|
||||
if (!(!r || typeof r != "object")) for (let [e, t] of Object.entries(r)) I(t) && n.push(e);
|
||||
if (r && typeof r == "object") for (let [e, t] of Object.entries(r)) I(t) && n.push(e);
|
||||
}
|
||||
return n.length ? n : N(e);
|
||||
}
|
||||
@@ -642,7 +642,7 @@ function Oe(e, t) {
|
||||
let d = u.find(([e]) => V(e) === V(c)) || u.find(([e]) => V(e) === "value") || (u.length === 1 ? u[0] : null);
|
||||
if (!d) continue;
|
||||
let f = `${String(s)}:${je(t, c, d[0])}`, p = a.get(f) || a.get(String(s)) || Me(l, d[0], c);
|
||||
i.size && !i.has(V(p)) || o.push({
|
||||
(!i.size || i.has(V(p))) && o.push({
|
||||
label: p,
|
||||
value: d[1],
|
||||
innerNodeId: s,
|
||||
@@ -666,7 +666,7 @@ function Ae(e) {
|
||||
let n = Array.isArray(e) ? e[1] : e?.origin_id ?? e?.originId ?? e?.from;
|
||||
if (String(n) !== "-10") continue;
|
||||
let i = Number(Array.isArray(e) ? e[2] : e?.origin_slot ?? e?.originSlot ?? e?.fromSlot), a = Array.isArray(e) ? e[3] : e?.target_id ?? e?.targetId ?? e?.to, o = Number(Array.isArray(e) ? e[4] : e?.target_slot ?? e?.targetSlot ?? e?.toSlot), s = Number.isFinite(i) ? t[i] : null, c = String(s?.label || s?.localized_name || s?.name || "").trim();
|
||||
!c || a == null || V(c) !== "value" && (r.set(String(a), c), Number.isFinite(o) && r.set(`${String(a)}:${o}`, c));
|
||||
c && a != null && V(c) !== "value" && (r.set(String(a), c), Number.isFinite(o) && r.set(`${String(a)}:${o}`, c));
|
||||
}
|
||||
return r;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { i as e } from "./model3dRenderer-CWRtZrSS.js";
|
||||
import { i as t, o as n } from "./geninfoParser-DS9m_bHm.js";
|
||||
import { i as e } from "./model3dRenderer-e5uWwIMW.js";
|
||||
import { i as t, o as n } from "./geninfoParser-Bwtqk4p6.js";
|
||||
//#region ui/features/viewer/sideBySide.ts
|
||||
function r(e) {
|
||||
if (!e) return null;
|
||||
@@ -143,7 +143,7 @@ function o(e) {
|
||||
for (let t = 0; t < r.length; t++) {
|
||||
if (t === e) continue;
|
||||
let a = r[t], o = a._mjr3D.camera, s = a._mjr3D.controls;
|
||||
!o || !s || (o.position.copy(n.position), o.quaternion.copy(n.quaternion), n.zoom !== void 0 && (o.zoom = n.zoom), o.updateProjectionMatrix(), s.target.copy(i.target), s.update());
|
||||
o && s && (o.position.copy(n.position), o.quaternion.copy(n.quaternion), n.zoom !== void 0 && (o.zoom = n.zoom), o.updateProjectionMatrix(), s.target.copy(i.target), s.update());
|
||||
}
|
||||
} catch (e) {
|
||||
console.debug?.(e);
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import { t as e } from "./rolldown-runtime-Dy4uBu1J.js";
|
||||
import { r as t } from "./events-DrYpMD-I.js";
|
||||
import { t as n } from "./Viewer-DXsFcJRb.js";
|
||||
import { r as t } from "./events-CoI83paj.js";
|
||||
import { t as n } from "./Viewer-DWgaGos-.js";
|
||||
//#region ui/features/viewer/viewerOpenRequest.ts
|
||||
var r = /* @__PURE__ */ e({ requestViewerOpen: () => o });
|
||||
function i(e) {
|
||||
+280
-273
File diff suppressed because it is too large
Load Diff
Vendored
+6093
-6049
File diff suppressed because it is too large
Load Diff
@@ -184,6 +184,14 @@ Use that line as the quickest confirmation that the current browser session can
|
||||
|
||||
## File System Settings
|
||||
|
||||
### Input Directory
|
||||
|
||||
- **UI location**: Settings -> Majoor Assets Manager -> Advanced -> Paths / Input -> Input Directory
|
||||
- **Purpose**: Overrides the folder indexed and browsed by the Inputs tab. Choose an existing directory.
|
||||
- **Default behavior**: Leave empty to use ComfyUI's input directory, including `--input-directory`.
|
||||
- **After changing it**: Select **Inputs**, then click **Scan** in **Index Status**. The colored status dot also starts a scan.
|
||||
- **Security**: The configured input root is allowlisted; files outside configured roots remain blocked.
|
||||
|
||||
### Workflow Roots
|
||||
|
||||
- **UI location**: Settings -> Majoor Assets Manager -> Advanced -> Paths / Workflows
|
||||
|
||||
@@ -121,6 +121,8 @@ class ComfyCoreAdapter:
|
||||
safe_type = str(item_type or "").strip().lower()
|
||||
if not safe_type:
|
||||
return None
|
||||
if safe_type == "input":
|
||||
return self.get_input_directory()
|
||||
try:
|
||||
import folder_paths # type: ignore
|
||||
|
||||
@@ -133,19 +135,11 @@ class ComfyCoreAdapter:
|
||||
logger.debug("Failed to resolve ComfyUI directory type=%s", safe_type, exc_info=True)
|
||||
return None
|
||||
|
||||
def get_input_directory(self) -> str | None:
|
||||
"""Return ComfyUI input directory when folder_paths is available."""
|
||||
try:
|
||||
import folder_paths # type: ignore
|
||||
def get_input_directory(self) -> str:
|
||||
"""Return the managed input root, falling back to ComfyUI's live root."""
|
||||
from ..config import get_runtime_input_root
|
||||
|
||||
getter = getattr(folder_paths, "get_input_directory", None)
|
||||
if callable(getter):
|
||||
value = getter()
|
||||
if value:
|
||||
return str(value)
|
||||
except Exception:
|
||||
logger.debug("Failed to resolve ComfyUI input directory", exc_info=True)
|
||||
return None
|
||||
return get_runtime_input_root()
|
||||
|
||||
def get_output_directory(self) -> str | None:
|
||||
"""Return ComfyUI output directory when folder_paths is available."""
|
||||
|
||||
@@ -21,7 +21,7 @@ The **first** strategy that returns a valid directory wins:
|
||||
A log line at startup (level INFO) reports which strategy was selected.
|
||||
|
||||
Input/index directory resolution follows a similar pattern; see
|
||||
``_resolve_index_dir()``.
|
||||
``get_runtime_input_root()`` and ``_resolve_index_dir()``.
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
@@ -39,6 +39,8 @@ logger = logging.getLogger(__name__)
|
||||
_INIT_LOCK = threading.Lock()
|
||||
_DIRS_INITIALIZED = False
|
||||
_OUTPUT_DIR_OVERRIDE_FILE_PATH = Path(__file__).resolve().parents[1] / ".mjr_output_directory_override"
|
||||
_INPUT_DIR_OVERRIDE_FILE_PATH = Path(__file__).resolve().parents[1] / ".mjr_input_directory_override"
|
||||
_INPUT_DIR_OVERRIDE_ENV_NAMES = ("MJR_AM_INPUT_DIRECTORY", "MAJOOR_INPUT_DIRECTORY")
|
||||
_INDEX_DIR_OVERRIDE_ENV_NAMES = ("MJR_AM_INDEX_DIRECTORY", "MAJOOR_INDEX_DIRECTORY")
|
||||
_INDEX_DIR_OVERRIDE_FILE_PATH = Path(__file__).resolve().parents[1] / ".mjr_index_directory_override"
|
||||
_INDEX_DIR_LOGGED_SELECTIONS: set[tuple[str, str]] = set()
|
||||
@@ -127,6 +129,48 @@ def _read_output_dir_override_from_file() -> str | None:
|
||||
def _read_index_dir_override_from_file() -> str | None:
|
||||
return _read_override_from_file(_INDEX_DIR_OVERRIDE_FILE_PATH)
|
||||
|
||||
|
||||
def get_runtime_input_root() -> str:
|
||||
"""Resolve the managed override, then ComfyUI's live input root and CLI hints."""
|
||||
override = _env_raw(*_INPUT_DIR_OVERRIDE_ENV_NAMES) or _read_override_from_file(_INPUT_DIR_OVERRIDE_FILE_PATH)
|
||||
if override:
|
||||
try:
|
||||
return str(Path(override).expanduser().resolve())
|
||||
except (OSError, RuntimeError, ValueError):
|
||||
logger.warning("Invalid input directory override, using ComfyUI defaults")
|
||||
try:
|
||||
import folder_paths # type: ignore
|
||||
|
||||
getter = getattr(folder_paths, "get_input_directory", None)
|
||||
value = getter() if callable(getter) else getattr(folder_paths, "input_directory", None)
|
||||
if value:
|
||||
return str(Path(value).expanduser().resolve())
|
||||
except Exception:
|
||||
logger.debug("Failed to resolve live ComfyUI input directory", exc_info=True)
|
||||
try:
|
||||
from comfy.cli_args import args as comfy_args # type: ignore
|
||||
|
||||
value = getattr(comfy_args, "input_directory", None)
|
||||
if value:
|
||||
return str(Path(value).expanduser().resolve())
|
||||
except Exception:
|
||||
logger.debug("ComfyUI input CLI settings unavailable", exc_info=True)
|
||||
try:
|
||||
argv = list(sys.argv or [])
|
||||
for i, token in enumerate(argv):
|
||||
if token == "--input-directory" and i + 1 < len(argv):
|
||||
return str(Path(argv[i + 1]).expanduser().resolve())
|
||||
if isinstance(token, str) and token.startswith("--input-directory="):
|
||||
value = token.split("=", 1)[1].strip()
|
||||
if value:
|
||||
return str(Path(value).expanduser().resolve())
|
||||
except (OSError, RuntimeError, TypeError, ValueError):
|
||||
logger.debug("Invalid input directory CLI argument", exc_info=True)
|
||||
fp_mod = sys.modules.get("folder_paths")
|
||||
base = getattr(fp_mod, "base_path", None) or _detect_comfy_root(Path(__file__).resolve())
|
||||
return str((Path(base) / "input" if base else Path.cwd() / "input").resolve())
|
||||
|
||||
|
||||
def _resolve_output_root() -> Path:
|
||||
resolved = _resolve_output_root_from_env()
|
||||
if resolved is not None:
|
||||
|
||||
@@ -13,6 +13,10 @@ async def apply_startup_settings(
|
||||
) -> None:
|
||||
startup_steps = (
|
||||
("Security bootstrap failed: %s", "ensure_security_bootstrap"),
|
||||
(
|
||||
"Input directory override restore failed: %s",
|
||||
"apply_input_directory_override_on_startup",
|
||||
),
|
||||
(
|
||||
"Output directory override restore failed: %s",
|
||||
"apply_output_directory_override_on_startup",
|
||||
|
||||
@@ -745,6 +745,97 @@ def register_health_routes(routes: web.RouteTableDef) -> None:
|
||||
),
|
||||
}))
|
||||
|
||||
@routes.get("/mjr/am/settings/input-directory")
|
||||
async def get_input_directory_setting(request):
|
||||
user_auth = _require_authenticated_user(request)
|
||||
if not user_auth.ok:
|
||||
return _json_response(
|
||||
Result.Err(user_auth.code or "AUTH_REQUIRED", user_auth.error or "Authentication required"),
|
||||
status=401,
|
||||
)
|
||||
svc, error_result = await _require_services()
|
||||
if error_result:
|
||||
return _json_response(error_result)
|
||||
settings_service = svc.get("settings")
|
||||
if not settings_service:
|
||||
return _json_response(Result.Err("SERVICE_UNAVAILABLE", "Settings service unavailable"))
|
||||
value = await settings_service.get_input_directory()
|
||||
return _json_response(Result.Ok({"input_directory": value or ""}))
|
||||
|
||||
@routes.post("/mjr/am/settings/input-directory")
|
||||
async def update_input_directory_setting(request):
|
||||
csrf = _csrf_error(request)
|
||||
if csrf:
|
||||
return _json_response(Result.Err("CSRF", csrf))
|
||||
auth = _require_write_access(request)
|
||||
if not auth.ok:
|
||||
return _json_response(auth)
|
||||
svc, error_result = await _require_services()
|
||||
if error_result:
|
||||
return _json_response(error_result)
|
||||
settings_service = svc.get("settings")
|
||||
if not settings_service:
|
||||
return _json_response(Result.Err("SERVICE_UNAVAILABLE", "Settings service unavailable"))
|
||||
body_res = await _read_json(request)
|
||||
if not body_res.ok:
|
||||
return _json_response(body_res)
|
||||
raw_value = (body_res.data or {}).get("input_directory")
|
||||
if raw_value is not None and not isinstance(raw_value, str):
|
||||
return _json_response(Result.Err("INVALID_INPUT", "input_directory must be a string"))
|
||||
value = (raw_value or "").strip()
|
||||
if value:
|
||||
try:
|
||||
normalized_path = Path(value).expanduser().resolve(strict=True)
|
||||
if not normalized_path.is_dir():
|
||||
return _json_response(Result.Err("INVALID_INPUT", "input_directory must be a directory"))
|
||||
value = str(normalized_path)
|
||||
except (OSError, RuntimeError, ValueError):
|
||||
return _json_response(Result.Err("INVALID_INPUT", "input_directory must be an existing directory"))
|
||||
old_input_dir = get_input_directory()
|
||||
result = await settings_service.set_input_directory(value)
|
||||
if not result.ok:
|
||||
return _json_response(result)
|
||||
new_input_dir = get_input_directory()
|
||||
watcher = svc.get("watcher")
|
||||
if watcher and old_input_dir != new_input_dir:
|
||||
try:
|
||||
# Watcher registrations are keyed by path, not by source.
|
||||
if Path(old_input_dir).resolve() != Path(get_runtime_output_root()).resolve():
|
||||
watcher.remove_path(old_input_dir)
|
||||
except Exception:
|
||||
logger.debug("Input directory watcher removal failed", exc_info=True)
|
||||
try:
|
||||
watcher.add_path(new_input_dir, source="input", root_id=None)
|
||||
except Exception:
|
||||
logger.debug("Input directory watcher registration failed", exc_info=True)
|
||||
try:
|
||||
await _invalidate_fs_list_cache()
|
||||
except Exception:
|
||||
logger.debug("Input directory listing cache invalidation failed", exc_info=True)
|
||||
if new_input_dir and new_input_dir != old_input_dir:
|
||||
try:
|
||||
await _kickoff_background_scan(
|
||||
new_input_dir,
|
||||
source="input",
|
||||
root_id=None,
|
||||
recursive=True,
|
||||
incremental=True,
|
||||
respect_bg_scan_on_list=False,
|
||||
)
|
||||
except Exception:
|
||||
logger.debug("Input directory initial scan failed", exc_info=True)
|
||||
response_result = Result.Ok({"input_directory": result.data})
|
||||
await _audit_settings_write(
|
||||
svc,
|
||||
request,
|
||||
"settings_input_directory",
|
||||
"settings:input_directory",
|
||||
response_result,
|
||||
previous=old_input_dir,
|
||||
current=str(result.data or ""),
|
||||
)
|
||||
return _json_response(response_result)
|
||||
|
||||
@routes.get("/mjr/am/settings/output-directory")
|
||||
async def get_output_directory_setting(request):
|
||||
user_auth = _require_authenticated_user(request)
|
||||
|
||||
@@ -24,6 +24,8 @@ from typing import Any
|
||||
from mjr_am_shared.runtime_env import get_env, set_env, unset_env
|
||||
|
||||
from .config import (
|
||||
_INPUT_DIR_OVERRIDE_ENV_NAMES,
|
||||
_INPUT_DIR_OVERRIDE_FILE_PATH,
|
||||
_OUTPUT_DIR_OVERRIDE_FILE_PATH,
|
||||
MEDIA_PROBE_BACKEND,
|
||||
OUTPUT_ROOT,
|
||||
@@ -42,6 +44,7 @@ logger = get_logger(__name__)
|
||||
|
||||
_PROBE_BACKEND_KEY = "media_probe_backend"
|
||||
_OUTPUT_DIRECTORY_KEY = "output_directory_override"
|
||||
_INPUT_DIRECTORY_KEY = "input_directory_override"
|
||||
_WORKFLOW_ROOTS_KEY = "workflow_roots"
|
||||
_METADATA_FALLBACK_IMAGE_KEY = "metadata_fallback_image"
|
||||
_METADATA_FALLBACK_MEDIA_KEY = "metadata_fallback_media"
|
||||
@@ -1515,6 +1518,64 @@ class AppSettings:
|
||||
"media": parse_bool(self._cache.get(media_key), self._default_metadata_fallback_media),
|
||||
}
|
||||
|
||||
async def get_input_directory(self) -> str | None:
|
||||
"""Return the persisted input directory override, or None when unset."""
|
||||
async with self._lock:
|
||||
return await self._read_setting(_INPUT_DIRECTORY_KEY) or None
|
||||
|
||||
async def set_input_directory(self, path: str) -> Result[str]:
|
||||
"""Persist a validated input override without changing ComfyUI globals."""
|
||||
from pathlib import Path
|
||||
|
||||
normalized = str(path or "").strip()
|
||||
if normalized:
|
||||
try:
|
||||
target = Path(normalized).expanduser().resolve(strict=True)
|
||||
if not target.is_dir():
|
||||
return Result.Err("INVALID_INPUT", "input_directory must be a directory")
|
||||
normalized = str(target)
|
||||
except (OSError, RuntimeError, ValueError):
|
||||
return Result.Err("INVALID_INPUT", "input_directory must be an existing directory")
|
||||
async with self._lock:
|
||||
result = (
|
||||
await self._write_setting(_INPUT_DIRECTORY_KEY, normalized)
|
||||
if normalized
|
||||
else await self._delete_setting(_INPUT_DIRECTORY_KEY)
|
||||
)
|
||||
if not result.ok:
|
||||
return Result.Err("DB_ERROR", result.error or "Failed to persist input directory")
|
||||
self._apply_input_directory_override(normalized)
|
||||
try:
|
||||
if normalized:
|
||||
_INPUT_DIR_OVERRIDE_FILE_PATH.write_text(normalized + "\n", encoding="utf-8")
|
||||
else:
|
||||
_INPUT_DIR_OVERRIDE_FILE_PATH.unlink(missing_ok=True)
|
||||
except OSError:
|
||||
logger.warning("Failed to update input directory override file", exc_info=True)
|
||||
await self._warn_if_bump_fails("Failed to bump settings version")
|
||||
return Result.Ok(normalized)
|
||||
|
||||
def _apply_input_directory_override(self, normalized: str) -> None:
|
||||
for name in _INPUT_DIR_OVERRIDE_ENV_NAMES:
|
||||
if normalized:
|
||||
set_env(name, normalized)
|
||||
else:
|
||||
unset_env(name)
|
||||
|
||||
async def apply_input_directory_override_on_startup(self) -> None:
|
||||
"""Restore the persisted override before scanning or handling requests."""
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
async with self._lock:
|
||||
raw = await self._read_setting(_INPUT_DIRECTORY_KEY)
|
||||
if raw:
|
||||
target = Path(raw).expanduser().resolve(strict=True)
|
||||
if target.is_dir():
|
||||
self._apply_input_directory_override(str(target))
|
||||
except (OSError, RuntimeError, ValueError) as exc:
|
||||
logger.warning("Failed to restore input directory override on startup: %s", exc)
|
||||
|
||||
async def get_output_directory(self) -> str | None:
|
||||
"""Return persisted output directory override, or None when unset."""
|
||||
async with self._lock:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import sys
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
from mjr_am_backend.adapters.comfy_core import ComfyCoreAdapter
|
||||
|
||||
|
||||
@@ -140,3 +141,69 @@ def test_comfy_core_adapter_base_path_none_when_unavailable(monkeypatch):
|
||||
adapter = ComfyCoreAdapter()
|
||||
|
||||
assert adapter.get_base_path() is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"source",
|
||||
["env", "file", "live", "attribute", "cli", "argv", "argv_equals", "broken_live", "base", "fallback"],
|
||||
)
|
||||
def test_input_directory_resolution_sources(monkeypatch, tmp_path, source):
|
||||
from mjr_am_backend import config
|
||||
|
||||
target = tmp_path / "custom-input"
|
||||
override_file = tmp_path / "override"
|
||||
monkeypatch.setattr(config, "_INPUT_DIR_OVERRIDE_FILE_PATH", override_file)
|
||||
for name in config._INPUT_DIR_OVERRIDE_ENV_NAMES:
|
||||
monkeypatch.delenv(name, raising=False)
|
||||
monkeypatch.setitem(sys.modules, "folder_paths", SimpleNamespace())
|
||||
monkeypatch.setitem(sys.modules, "comfy.cli_args", SimpleNamespace(args=SimpleNamespace()))
|
||||
monkeypatch.setattr(sys, "argv", ["main.py"])
|
||||
monkeypatch.setattr(config, "_detect_comfy_root", lambda _: None)
|
||||
monkeypatch.chdir(tmp_path)
|
||||
if source == "env":
|
||||
monkeypatch.setenv("MAJOOR_INPUT_DIRECTORY", str(target))
|
||||
override_file.write_text(str(tmp_path / "lower-priority"))
|
||||
monkeypatch.setitem(sys.modules, "folder_paths", SimpleNamespace(get_input_directory=lambda: "ignored"))
|
||||
elif source == "file":
|
||||
override_file.write_text(str(target))
|
||||
elif source == "live":
|
||||
monkeypatch.setitem(sys.modules, "folder_paths", SimpleNamespace(get_input_directory=lambda: str(target)))
|
||||
monkeypatch.setitem(sys.modules, "comfy.cli_args", SimpleNamespace(args=SimpleNamespace(input_directory="ignored")))
|
||||
elif source == "attribute":
|
||||
monkeypatch.setitem(sys.modules, "folder_paths", SimpleNamespace(input_directory=str(target)))
|
||||
elif source == "cli":
|
||||
monkeypatch.setitem(sys.modules, "comfy.cli_args", SimpleNamespace(args=SimpleNamespace(input_directory=str(target))))
|
||||
elif source == "argv":
|
||||
monkeypatch.setitem(sys.modules, "folder_paths", None)
|
||||
monkeypatch.setattr(sys, "argv", ["main.py", "--input-directory", str(target)])
|
||||
elif source == "argv_equals":
|
||||
monkeypatch.setattr(sys, "argv", ["main.py", f"--input-directory={target}"])
|
||||
elif source == "broken_live":
|
||||
def broken_getter():
|
||||
raise RuntimeError("ComfyUI not initialized")
|
||||
|
||||
monkeypatch.setitem(sys.modules, "folder_paths", SimpleNamespace(get_input_directory=broken_getter))
|
||||
monkeypatch.setattr(sys, "argv", ["main.py", "--input-directory", str(target)])
|
||||
elif source == "base":
|
||||
monkeypatch.setitem(sys.modules, "folder_paths", SimpleNamespace(base_path=str(tmp_path)))
|
||||
target = tmp_path / "input"
|
||||
else:
|
||||
target = tmp_path / "input"
|
||||
adapter = ComfyCoreAdapter()
|
||||
assert adapter.get_input_directory() == str(target)
|
||||
assert adapter.get_directory_by_type("input") == str(target)
|
||||
|
||||
|
||||
def test_input_directory_tracks_live_changes_without_override(monkeypatch, tmp_path):
|
||||
from mjr_am_backend import config
|
||||
|
||||
monkeypatch.setattr(config, "_INPUT_DIR_OVERRIDE_FILE_PATH", tmp_path / "absent")
|
||||
for name in config._INPUT_DIR_OVERRIDE_ENV_NAMES:
|
||||
monkeypatch.delenv(name, raising=False)
|
||||
module = SimpleNamespace(input_directory=str(tmp_path / "first"))
|
||||
module.get_input_directory = lambda: module.input_directory
|
||||
monkeypatch.setitem(sys.modules, "folder_paths", module)
|
||||
adapter = ComfyCoreAdapter()
|
||||
assert adapter.get_input_directory() == str(tmp_path / "first")
|
||||
module.input_directory = str(tmp_path / "second")
|
||||
assert adapter.get_input_directory() == str(tmp_path / "second")
|
||||
|
||||
@@ -378,6 +378,116 @@ async def test_update_output_directory_real_csrf_and_auth_guards(monkeypatch) ->
|
||||
assert body_missing_auth.get("code") == "AUTH_REQUIRED"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_input_directory_real_security_guards(monkeypatch):
|
||||
from types import SimpleNamespace
|
||||
|
||||
from mjr_am_backend.routes.core import security_auth_context
|
||||
|
||||
monkeypatch.setenv("MAJOOR_REQUIRE_AUTH", "1")
|
||||
for name in ("MAJOOR_API_TOKEN", "MJR_API_TOKEN", "MAJOOR_API_TOKEN_HASH", "MJR_API_TOKEN_HASH"):
|
||||
monkeypatch.delenv(name, raising=False)
|
||||
app = _build_health_app()
|
||||
monkeypatch.setattr(
|
||||
security_auth_context,
|
||||
"_get_comfy_user_manager",
|
||||
lambda _: SimpleNamespace(enabled=True, get_request_user_id=lambda _: None),
|
||||
)
|
||||
for method, headers, code in (
|
||||
("GET", {}, "AUTH_REQUIRED"),
|
||||
("POST", {}, "CSRF"),
|
||||
("POST", {"X-Requested-With": "XMLHttpRequest"}, "AUTH_REQUIRED"),
|
||||
):
|
||||
request = make_mocked_request(method, "/mjr/am/settings/input-directory", headers=headers, app=app)
|
||||
response = await (await app.router.resolve(request)).handler(request)
|
||||
assert json.loads(response.text)["code"] == code
|
||||
if method == "GET":
|
||||
assert response.status == 401
|
||||
monkeypatch.setattr(health_mod, "_csrf_error", lambda _: None)
|
||||
monkeypatch.setattr(health_mod, "_require_write_access", lambda _: Result.Err("FORBIDDEN", "Read only"))
|
||||
request = make_mocked_request("POST", "/mjr/am/settings/input-directory", app=app)
|
||||
response = await (await app.router.resolve(request)).handler(request)
|
||||
assert json.loads(response.text)["code"] == "FORBIDDEN"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("shared_output", [False, True])
|
||||
async def test_input_directory_routes_validation_and_runtime_refresh(monkeypatch, tmp_path, shared_output):
|
||||
from mjr_am_backend.adapters.comfy_core import get_input_directory
|
||||
from mjr_am_backend.settings import AppSettings
|
||||
|
||||
from .test_settings_backend import _DB
|
||||
|
||||
settings = AppSettings(_DB())
|
||||
override_file = tmp_path / "override"
|
||||
monkeypatch.setattr("mjr_am_backend.settings._INPUT_DIR_OVERRIDE_FILE_PATH", override_file)
|
||||
monkeypatch.setattr("mjr_am_backend.config._INPUT_DIR_OVERRIDE_FILE_PATH", override_file)
|
||||
for name in ("MAJOOR_INPUT_DIRECTORY", "MJR_AM_INPUT_DIRECTORY"):
|
||||
monkeypatch.delenv(name, raising=False)
|
||||
original = get_input_directory()
|
||||
output = original + "/." if shared_output else str(tmp_path / "output")
|
||||
monkeypatch.setattr(health_mod, "get_runtime_output_root", lambda: output)
|
||||
custom = tmp_path / "custom"
|
||||
custom.mkdir()
|
||||
watcher = Mock()
|
||||
calls = {"scan": [], "invalidated": 0, "audit": []}
|
||||
payload = {}
|
||||
|
||||
async def services():
|
||||
return {"settings": settings, "watcher": watcher}, None
|
||||
|
||||
async def read_json(_):
|
||||
return Result.Ok(payload)
|
||||
|
||||
async def scan(*args, **kwargs):
|
||||
calls["scan"].append((args, kwargs))
|
||||
|
||||
async def invalidate():
|
||||
calls["invalidated"] += 1
|
||||
|
||||
async def audit(_svc, **kwargs):
|
||||
calls["audit"].append(kwargs)
|
||||
|
||||
monkeypatch.setattr(health_mod, "_require_services", services)
|
||||
monkeypatch.setattr(health_mod, "_require_authenticated_user", lambda _: Result.Ok({}))
|
||||
monkeypatch.setattr(health_mod, "_csrf_error", lambda _: None)
|
||||
monkeypatch.setattr(health_mod, "_require_write_access", lambda _: Result.Ok({}))
|
||||
monkeypatch.setattr(health_mod, "_read_json", read_json)
|
||||
monkeypatch.setattr(health_mod, "_kickoff_background_scan", scan)
|
||||
monkeypatch.setattr(health_mod, "_invalidate_fs_list_cache", invalidate)
|
||||
monkeypatch.setattr(health_mod, "audit_log_write", audit)
|
||||
app = _build_health_app()
|
||||
|
||||
async def request(method):
|
||||
req = make_mocked_request(method, "/mjr/am/settings/input-directory", app=app)
|
||||
return json.loads((await (await app.router.resolve(req)).handler(req)).text)
|
||||
|
||||
assert (await request("GET"))["data"] == {"input_directory": ""}
|
||||
not_dir = tmp_path / "file"
|
||||
not_dir.write_text("x")
|
||||
for invalid in (str(tmp_path / "missing"), str(not_dir), "bad\0path", [], 123):
|
||||
payload["input_directory"] = invalid
|
||||
assert (await request("POST"))["code"] == "INVALID_INPUT"
|
||||
assert await settings.get_input_directory() is None
|
||||
assert not calls["scan"]
|
||||
payload["input_directory"] = str(custom)
|
||||
assert (await request("POST"))["data"] == {"input_directory": str(custom)}
|
||||
assert (await request("GET"))["data"] == {"input_directory": str(custom)}
|
||||
if shared_output:
|
||||
watcher.remove_path.assert_not_called()
|
||||
else:
|
||||
watcher.remove_path.assert_called_once_with(original)
|
||||
watcher.add_path.assert_called_once_with(str(custom), source="input", root_id=None)
|
||||
assert calls["invalidated"] == 1
|
||||
assert calls["scan"][0][0] == (str(custom),)
|
||||
assert calls["scan"][0][1]["source"] == "input"
|
||||
assert calls["audit"][0]["operation"] == "settings_input_directory"
|
||||
payload["input_directory"] = ""
|
||||
assert (await request("POST"))["data"] == {"input_directory": ""}
|
||||
assert get_input_directory() == original
|
||||
assert calls["scan"][-1][0] == (original,)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_workflow_roots_setting_success(monkeypatch, tmp_path) -> None:
|
||||
settings = _Settings()
|
||||
|
||||
@@ -1575,6 +1575,53 @@ async def test_watcher_scope_restart_success(monkeypatch) -> None:
|
||||
assert payload.get("data", {}).get("enabled") is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("configuration", ["override", "cli", "argv"])
|
||||
async def test_scan_scope_input_uses_custom_directory_and_real_allowlist(monkeypatch, tmp_path, configuration):
|
||||
import sys
|
||||
|
||||
from mjr_am_backend import config
|
||||
|
||||
custom = tmp_path / "custom-input"
|
||||
custom.mkdir()
|
||||
seen = []
|
||||
|
||||
class _Index:
|
||||
async def scan_directory(self, directory, recursive, incremental, source, root_id, fast, background_metadata):
|
||||
seen.append((Path(directory), source))
|
||||
return Result.Ok({"scanned": 1, "added": 1, "updated": 0, "skipped": 0, "errors": 0})
|
||||
|
||||
async def services():
|
||||
return {"index": _Index()}, None
|
||||
|
||||
async def read_json(_):
|
||||
return Result.Ok({"scope": "input"})
|
||||
|
||||
for name in config._INPUT_DIR_OVERRIDE_ENV_NAMES:
|
||||
monkeypatch.delenv(name, raising=False)
|
||||
monkeypatch.setattr(config, "_INPUT_DIR_OVERRIDE_FILE_PATH", tmp_path / "absent-override")
|
||||
if configuration == "override":
|
||||
monkeypatch.setenv("MJR_AM_INPUT_DIRECTORY", str(custom))
|
||||
else:
|
||||
monkeypatch.setitem(sys.modules, "folder_paths", None)
|
||||
cli_args = SimpleNamespace(input_directory=str(custom) if configuration == "cli" else None)
|
||||
monkeypatch.setitem(sys.modules, "comfy.cli_args", SimpleNamespace(args=cli_args))
|
||||
monkeypatch.setattr(sys, "argv", ["main.py", "--input-directory", str(custom)])
|
||||
monkeypatch.setattr(scan_mod, "_require_services", services)
|
||||
monkeypatch.setattr(scan_mod, "is_db_maintenance_active", lambda: False)
|
||||
monkeypatch.setattr(scan_mod, "_csrf_error", lambda _: None)
|
||||
monkeypatch.setattr(scan_mod, "_require_write_access", lambda _: Result.Ok({}))
|
||||
monkeypatch.setattr(scan_mod, "_check_rate_limit", lambda *args, **kwargs: (True, None))
|
||||
monkeypatch.setattr(scan_mod, "_read_json", read_json)
|
||||
app = _app()
|
||||
request = make_mocked_request("POST", "/mjr/am/scan", app=app)
|
||||
response = await (await app.router.resolve(request)).handler(request)
|
||||
result = json.loads(response.text)
|
||||
assert result["ok"], result
|
||||
assert seen[0][0] == custom
|
||||
assert seen[0][1] == "input"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_scan_route_scope_output_success(monkeypatch, tmp_path: Path) -> None:
|
||||
out_root = tmp_path / "out"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import asyncio
|
||||
import datetime
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from aiohttp import web
|
||||
@@ -917,6 +918,41 @@ async def test_autocomplete_invalid_limit_and_service_error(monkeypatch) -> None
|
||||
assert json.loads(resp.text).get("code") == "SERVICE_UNAVAILABLE"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_input_uses_custom_directory_without_folder_paths_patch(monkeypatch, tmp_path):
|
||||
custom = tmp_path / "custom-input"
|
||||
custom.mkdir()
|
||||
seen = []
|
||||
|
||||
class _Index:
|
||||
async def search_scoped(self, *args, **kwargs):
|
||||
return Result.Err("DB", "Use filesystem fallback")
|
||||
|
||||
async def services():
|
||||
return {"index": _Index()}, None
|
||||
|
||||
async def filesystem(root, *args, **kwargs):
|
||||
seen.append(Path(root))
|
||||
return Result.Ok({"assets": [{"filename": "custom.png", "filepath": str(custom / "custom.png")}], "total": 1})
|
||||
|
||||
async def scan(*args, **kwargs):
|
||||
return None
|
||||
|
||||
monkeypatch.setenv("MJR_AM_INPUT_DIRECTORY", str(custom))
|
||||
monkeypatch.setattr(search_impl, "_require_services", services)
|
||||
monkeypatch.setattr(search_impl, "_check_rate_limit", lambda *args, **kwargs: (True, None))
|
||||
monkeypatch.setattr(search_impl, "_touch_enrichment_pause", lambda *args, **kwargs: None)
|
||||
monkeypatch.setattr(search_impl, "_list_filesystem_assets", filesystem)
|
||||
monkeypatch.setattr(search_impl, "_kickoff_background_scan", scan)
|
||||
app = _build_search_app()
|
||||
request = make_mocked_request("GET", "/mjr/am/list?scope=input&q=*&offset=0", app=app)
|
||||
response = await (await app.router.resolve(request)).handler(request)
|
||||
result = json.loads(response.text)
|
||||
assert result["ok"]
|
||||
assert seen == [custom]
|
||||
assert result["data"]["assets"][0]["filename"] == "custom.png"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_input_fallback_filesystem(monkeypatch, tmp_path) -> None:
|
||||
class _Index:
|
||||
|
||||
@@ -6,6 +6,7 @@ import pytest
|
||||
from mjr_am_backend.routes.core import security as sec
|
||||
from mjr_am_backend.settings import (
|
||||
_AI_VERBOSE_LOGS_KEY,
|
||||
_INPUT_DIRECTORY_KEY,
|
||||
_METADATA_FALLBACK_IMAGE_KEY,
|
||||
_METADATA_FALLBACK_MEDIA_KEY,
|
||||
_OUTPUT_DIRECTORY_KEY,
|
||||
@@ -421,6 +422,95 @@ async def test_output_directory_db_errors(tmp_path: Path):
|
||||
assert out2.code == "DB_ERROR"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def input_override_file(tmp_path, monkeypatch):
|
||||
path = tmp_path / "input-override.txt"
|
||||
monkeypatch.setattr("mjr_am_backend.settings._INPUT_DIR_OVERRIDE_FILE_PATH", path)
|
||||
monkeypatch.setattr("mjr_am_backend.config._INPUT_DIR_OVERRIDE_FILE_PATH", path)
|
||||
for name in ("MJR_AM_INPUT_DIRECTORY", "MAJOOR_INPUT_DIRECTORY"):
|
||||
monkeypatch.delenv(name, raising=False)
|
||||
return path
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_input_directory_persistence_restart_clear(tmp_path, monkeypatch, input_override_file):
|
||||
from types import SimpleNamespace
|
||||
|
||||
from mjr_am_backend.adapters.comfy_core import get_input_directory
|
||||
from mjr_am_backend.features.runtime.bootstrap import apply_startup_settings
|
||||
from mjr_am_backend.routes.core import paths
|
||||
|
||||
default = tmp_path / "cli-input"
|
||||
custom = tmp_path / "custom"
|
||||
default.mkdir()
|
||||
custom.mkdir()
|
||||
monkeypatch.setitem(sys.modules, "folder_paths", None)
|
||||
monkeypatch.setitem(sys.modules, "comfy.cli_args", None)
|
||||
monkeypatch.setattr(sys, "argv", ["main.py", "--input-directory", str(default)])
|
||||
monkeypatch.setattr(paths, "get_runtime_output_root", lambda: str(tmp_path / "output"))
|
||||
db = _DB()
|
||||
settings = AppSettings(db)
|
||||
assert await settings.get_input_directory() is None
|
||||
assert get_input_directory() == str(default)
|
||||
assert paths._is_path_allowed(default / "a.png")
|
||||
saved = await settings.set_input_directory(str(custom))
|
||||
assert saved.ok and saved.data == str(custom)
|
||||
assert db.store[_INPUT_DIRECTORY_KEY] == str(custom)
|
||||
assert input_override_file.read_text().strip() == str(custom)
|
||||
assert paths._is_path_allowed(custom / "a.png")
|
||||
assert not paths._is_path_allowed(default / "a.png")
|
||||
assert not paths._is_path_allowed(tmp_path / "outside.png")
|
||||
(custom / "escape").symlink_to(default, target_is_directory=True)
|
||||
assert not paths._is_path_allowed(custom / "escape" / "a.png")
|
||||
|
||||
for name in ("MJR_AM_INPUT_DIRECTORY", "MAJOOR_INPUT_DIRECTORY"):
|
||||
monkeypatch.delenv(name)
|
||||
# The early-startup file resolves before the database is initialized.
|
||||
assert get_input_directory() == str(custom)
|
||||
input_override_file.unlink()
|
||||
restarted = AppSettings(db)
|
||||
warnings = []
|
||||
await apply_startup_settings(
|
||||
SimpleNamespace(apply_input_directory_override_on_startup=restarted.apply_input_directory_override_on_startup),
|
||||
warn=lambda *args: warnings.append(args),
|
||||
)
|
||||
assert not warnings
|
||||
assert await restarted.get_input_directory() == str(custom)
|
||||
assert get_input_directory() == str(custom)
|
||||
cleared = await restarted.set_input_directory("")
|
||||
assert cleared.ok
|
||||
assert _INPUT_DIRECTORY_KEY not in db.store
|
||||
assert not input_override_file.exists()
|
||||
assert await restarted.get_input_directory() is None
|
||||
assert get_input_directory() == str(default)
|
||||
assert not paths._is_path_allowed(custom / "a.png")
|
||||
assert paths._is_path_allowed(default / "a.png")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_input_directory_validation_and_db_errors(tmp_path, input_override_file):
|
||||
db = _DB()
|
||||
settings = AppSettings(db)
|
||||
not_dir = tmp_path / "file"
|
||||
not_dir.write_text("x")
|
||||
for invalid in (str(tmp_path / "missing"), str(not_dir), "bad\0path"):
|
||||
result = await settings.set_input_directory(invalid)
|
||||
assert result.code == "INVALID_INPUT"
|
||||
assert _INPUT_DIRECTORY_KEY not in db.store
|
||||
assert not input_override_file.exists()
|
||||
db.fail_write = True
|
||||
result = await settings.set_input_directory(str(tmp_path))
|
||||
assert result.code == "DB_ERROR"
|
||||
assert not input_override_file.exists()
|
||||
db.fail_write = False
|
||||
assert (await settings.set_input_directory(str(tmp_path))).ok
|
||||
db.fail_delete = True
|
||||
result = await settings.set_input_directory("")
|
||||
assert result.code == "DB_ERROR"
|
||||
assert input_override_file.exists()
|
||||
assert await settings.get_input_directory() == str(tmp_path)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_workflow_roots_set_get_clear(tmp_path: Path, monkeypatch):
|
||||
db = _DB()
|
||||
|
||||
@@ -124,6 +124,15 @@ export async function getOutputDirectorySetting() {
|
||||
return get(ENDPOINTS.SETTINGS_OUTPUT_DIRECTORY);
|
||||
}
|
||||
|
||||
export async function getInputDirectorySetting() {
|
||||
return get(ENDPOINTS.SETTINGS_INPUT_DIRECTORY);
|
||||
}
|
||||
|
||||
export async function setInputDirectorySetting(inputDirectory: any, options: Record<string, any> = {}) {
|
||||
const value = String(inputDirectory ?? "").trim();
|
||||
return post(ENDPOINTS.SETTINGS_INPUT_DIRECTORY, { input_directory: value }, options);
|
||||
}
|
||||
|
||||
export async function setOutputDirectorySetting(outputDirectory: any, options: Record<string, any> = {}) {
|
||||
const value = String(outputDirectory ?? "").trim();
|
||||
return post(ENDPOINTS.SETTINGS_OUTPUT_DIRECTORY, { output_directory: value }, options);
|
||||
|
||||
@@ -77,6 +77,7 @@ export const ENDPOINTS = {
|
||||
COLLECT_FILES: "/mjr/am/collect-files",
|
||||
TOOLS_STATUS: "/mjr/am/tools/status",
|
||||
SETTINGS_OUTPUT_DIRECTORY: "/mjr/am/settings/output-directory",
|
||||
SETTINGS_INPUT_DIRECTORY: "/mjr/am/settings/input-directory",
|
||||
SETTINGS_INDEX_DIRECTORY: "/mjr/am/settings/index-directory",
|
||||
SETTINGS_WORKFLOW_ROOTS: "/mjr/am/settings/workflow-roots",
|
||||
SETTINGS_METADATA_FALLBACK: "/mjr/am/settings/metadata-fallback",
|
||||
|
||||
@@ -9,9 +9,11 @@ import {
|
||||
setMetadataFallbackSettings,
|
||||
getIndexDirectorySetting,
|
||||
getOutputDirectorySetting,
|
||||
getInputDirectorySetting,
|
||||
getWorkflowRootsSetting,
|
||||
setIndexDirectorySetting,
|
||||
setOutputDirectorySetting,
|
||||
setInputDirectorySetting,
|
||||
setWorkflowRootsSetting,
|
||||
vectorStats,
|
||||
vectorBackfill,
|
||||
@@ -35,6 +37,7 @@ import {
|
||||
} from "../i18n.js";
|
||||
import { _safeNum, _safeOneOf } from "./settingsUtils.js";
|
||||
import { DEFAULT_SETTINGS, saveMajoorSettings, applySettingsToConfig } from "./settingsCore.js";
|
||||
import { setSettingForApp } from "../hostAdapter.js";
|
||||
|
||||
const SETTINGS_PREFIX = "Majoor";
|
||||
const SETTINGS_CATEGORY = "Majoor Assets Manager";
|
||||
@@ -151,6 +154,77 @@ export function registerAdvancedSettings(safeAddSetting: (def: any) => void, set
|
||||
console.debug?.(e);
|
||||
}
|
||||
|
||||
// -- InputDirectory ----------------------------------------------------
|
||||
|
||||
let inputDirCommittedValue = String(settings.paths?.inputDirectory || "");
|
||||
let inputDirSaveTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let inputDirSaveSeq = 0;
|
||||
let inputDirSaveAbort: AbortController | null = null;
|
||||
const applyInputDirectoryValue = (value: string) => {
|
||||
settings.paths = settings.paths || {};
|
||||
settings.paths.inputDirectory = value;
|
||||
saveMajoorSettings(settings);
|
||||
setSettingForApp(app, `${SETTINGS_PREFIX}.Paths.InputDirectory`, value);
|
||||
notifyApplied("paths.inputDirectory");
|
||||
};
|
||||
|
||||
safeAddSetting({
|
||||
id: `${SETTINGS_PREFIX}.Paths.InputDirectory`,
|
||||
category: cat(t("cat.advanced"), "Paths / Input"),
|
||||
name: "Majoor: Input Directory",
|
||||
tooltip:
|
||||
"Override the folder indexed by the Inputs tab. Leave empty to use ComfyUI's input directory (--input-directory). Run a scan after changing it.",
|
||||
type: "text",
|
||||
defaultValue: String(settings.paths?.inputDirectory || ""),
|
||||
attrs: { placeholder: "D:\\ComfyUI\\input" },
|
||||
onChange: (value: any) => {
|
||||
const next = String(value || "").trim();
|
||||
if (next === String(settings.paths?.inputDirectory || "")) return;
|
||||
settings.paths = settings.paths || {};
|
||||
settings.paths.inputDirectory = next;
|
||||
saveMajoorSettings(settings);
|
||||
if (inputDirSaveTimer) clearTimeout(inputDirSaveTimer);
|
||||
const seq = ++inputDirSaveSeq;
|
||||
inputDirSaveAbort?.abort();
|
||||
inputDirSaveTimer = setTimeout(async () => {
|
||||
inputDirSaveTimer = null;
|
||||
inputDirSaveAbort =
|
||||
typeof AbortController !== "undefined" ? new AbortController() : null;
|
||||
try {
|
||||
const res = await setInputDirectorySetting(
|
||||
next,
|
||||
inputDirSaveAbort ? { signal: inputDirSaveAbort.signal } : {},
|
||||
);
|
||||
if (seq !== inputDirSaveSeq) return;
|
||||
if (!res?.ok) {
|
||||
throw new Error(res?.error || "Failed to set input directory");
|
||||
}
|
||||
const serverValue = String(res?.data?.input_directory ?? next).trim();
|
||||
inputDirCommittedValue = serverValue;
|
||||
applyInputDirectoryValue(serverValue);
|
||||
} catch (error: any) {
|
||||
if (seq !== inputDirSaveSeq) return;
|
||||
if (error?.name === "AbortError" || error?.code === "ABORTED") return;
|
||||
applyInputDirectoryValue(inputDirCommittedValue);
|
||||
comfyToast(error?.message || "Failed to set input directory", "error");
|
||||
}
|
||||
}, 700);
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
getInputDirectorySetting()
|
||||
.then((res) => {
|
||||
if (!res?.ok || inputDirSaveSeq !== 0) return;
|
||||
const serverValue = String(res?.data?.input_directory || "").trim();
|
||||
inputDirCommittedValue = serverValue;
|
||||
applyInputDirectoryValue(serverValue);
|
||||
})
|
||||
.catch(() => {});
|
||||
} catch (e) {
|
||||
console.debug?.(e);
|
||||
}
|
||||
|
||||
// -- IndexDirectory ----------------------------------------------------
|
||||
|
||||
let _indexDirCommittedValue = String(settings.paths?.indexDirectory || "");
|
||||
|
||||
@@ -151,6 +151,7 @@ export const DEFAULT_SETTINGS = {
|
||||
},
|
||||
paths: {
|
||||
outputDirectory: "",
|
||||
inputDirectory: "",
|
||||
indexDirectory: "",
|
||||
workflowRoots: "",
|
||||
},
|
||||
|
||||
@@ -425,6 +425,13 @@ export function createStatusIndicator(options: Record<string, any> = {}): any {
|
||||
header.appendChild(statusDot);
|
||||
header.appendChild(titleSpan);
|
||||
|
||||
const scanButton = document.createElement("button");
|
||||
scanButton.id = "mjr-status-scan";
|
||||
scanButton.type = "button";
|
||||
scanButton.textContent = t("btn.scan", "Scan");
|
||||
scanButton.style.cssText = "margin-left: auto; padding: 4px 10px; cursor: pointer;";
|
||||
header.appendChild(scanButton);
|
||||
|
||||
// Body container
|
||||
const body = document.createElement("div");
|
||||
body.id = "mjr-status-body";
|
||||
@@ -2595,13 +2602,12 @@ export function setupStatusPolling(
|
||||
}
|
||||
scheduleNext();
|
||||
|
||||
const statusDotEl = section.querySelector("#mjr-status-dot");
|
||||
if (statusDotEl) {
|
||||
statusDotEl.onclick = (event: any) => {
|
||||
for (const control of section.querySelectorAll("#mjr-status-dot, #mjr-status-scan")) {
|
||||
control.onclick = (event: any) => {
|
||||
event.stopPropagation();
|
||||
const target = typeof getScanTarget === "function" ? getScanTarget() : null;
|
||||
triggerScan(statusDot, statusText, capabilitiesSection, target);
|
||||
};
|
||||
statusDotEl.title = t("status.clickToScan", "Click to scan");
|
||||
control.title = t("btn.scan", "Scan");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { registerAdvancedSettings } from "../app/settings/settingsAdvanced.js";
|
||||
import { DEFAULT_SETTINGS, saveMajoorSettings } from "../app/settings/settingsCore.js";
|
||||
import { getInputDirectorySetting, setInputDirectorySetting } from "../api/client.js";
|
||||
import { comfyToast } from "../app/toast.js";
|
||||
|
||||
vi.mock("../api/client.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../api/client.js")>();
|
||||
const unavailable = vi.fn().mockResolvedValue({ ok: false });
|
||||
return {
|
||||
...actual,
|
||||
getInputDirectorySetting: vi.fn(),
|
||||
setInputDirectorySetting: vi.fn(),
|
||||
getOutputDirectorySetting: unavailable,
|
||||
getIndexDirectorySetting: unavailable,
|
||||
getWorkflowRootsSetting: unavailable,
|
||||
getMetadataFallbackSettings: unavailable,
|
||||
getRouteLoggingSettings: unavailable,
|
||||
getStartupLoggingSettings: unavailable,
|
||||
getHuggingFaceSettings: unavailable,
|
||||
getAiLoggingSettings: unavailable,
|
||||
vectorStats: unavailable,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../app/settings/settingsCore.js", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("../app/settings/settingsCore.js")>()),
|
||||
saveMajoorSettings: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../app/toast.js", () => ({ comfyToast: vi.fn(), recordToastHistory: vi.fn() }));
|
||||
vi.mock("../app/i18n.js", () => ({
|
||||
t: (key: string, fallback?: string) => fallback || key,
|
||||
getSupportedLanguages: () => [],
|
||||
getCurrentLang: () => "en",
|
||||
}));
|
||||
|
||||
function register() {
|
||||
const settings = structuredClone(DEFAULT_SETTINGS);
|
||||
const definitions: any[] = [];
|
||||
const notify = vi.fn();
|
||||
let input: any;
|
||||
const setSettingValue = vi.fn((_id, value) => input?.onChange(value));
|
||||
registerAdvancedSettings(
|
||||
(definition) => definitions.push(definition), settings, notify,
|
||||
{ ui: { settings: { setSettingValue } } },
|
||||
);
|
||||
input = definitions.find((definition) => definition.id === "Majoor.Paths.InputDirectory");
|
||||
return { settings, input, notify, setSettingValue };
|
||||
}
|
||||
|
||||
describe("Input Directory setting", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(getInputDirectorySetting).mockResolvedValue({
|
||||
ok: true, data: { input_directory: "/custom/inputs" },
|
||||
});
|
||||
vi.mocked(setInputDirectorySetting).mockImplementation(async (value) => ({
|
||||
ok: true, data: { input_directory: value },
|
||||
}));
|
||||
});
|
||||
|
||||
afterEach(() => vi.useRealTimers());
|
||||
|
||||
it("loads the persisted backend override and registers a Paths / Input field", async () => {
|
||||
const { settings, input, notify, setSettingValue } = register();
|
||||
await vi.advanceTimersByTimeAsync(700);
|
||||
expect(input.category).toContain("Paths / Input");
|
||||
expect(input.type).toBe("text");
|
||||
expect(settings.paths.inputDirectory).toBe("/custom/inputs");
|
||||
expect(notify).toHaveBeenCalledWith("paths.inputDirectory");
|
||||
expect(setSettingValue).toHaveBeenCalledWith("Majoor.Paths.InputDirectory", "/custom/inputs");
|
||||
expect(setInputDirectorySetting).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("debounces edits, trims paths, and supports clearing the override", async () => {
|
||||
const { settings, input } = register();
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
input.onChange("/first");
|
||||
input.onChange(" /second ");
|
||||
await vi.advanceTimersByTimeAsync(699);
|
||||
expect(setInputDirectorySetting).not.toHaveBeenCalled();
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
expect(setInputDirectorySetting).toHaveBeenCalledExactlyOnceWith(
|
||||
"/second", expect.objectContaining({ signal: expect.any(AbortSignal) }),
|
||||
);
|
||||
expect(settings.paths.inputDirectory).toBe("/second");
|
||||
input.onChange("");
|
||||
await vi.advanceTimersByTimeAsync(700);
|
||||
expect(settings.paths.inputDirectory).toBe("");
|
||||
expect(saveMajoorSettings).toHaveBeenCalledWith(settings);
|
||||
});
|
||||
|
||||
it("restores the committed value when the backend rejects a path", async () => {
|
||||
const { settings, input, setSettingValue } = register();
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
vi.mocked(setInputDirectorySetting).mockResolvedValue({
|
||||
ok: false, data: null, error: "Directory does not exist",
|
||||
});
|
||||
input.onChange("/missing");
|
||||
await vi.advanceTimersByTimeAsync(700);
|
||||
expect(settings.paths.inputDirectory).toBe("/custom/inputs");
|
||||
expect(setSettingValue).toHaveBeenLastCalledWith("Majoor.Paths.InputDirectory", "/custom/inputs");
|
||||
expect(comfyToast).toHaveBeenCalledWith("Directory does not exist", "error");
|
||||
});
|
||||
|
||||
it("does not let initial backend loading overwrite a new edit", async () => {
|
||||
const { settings, input } = register();
|
||||
input.onChange("/new");
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
expect(settings.paths.inputDirectory).toBe("/new");
|
||||
await vi.advanceTimersByTimeAsync(700);
|
||||
expect(settings.paths.inputDirectory).toBe("/new");
|
||||
});
|
||||
|
||||
it("ignores stale saves when another path is entered while a request is pending", async () => {
|
||||
const { settings, input } = register();
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
let finishFirst!: (value: any) => void;
|
||||
vi.mocked(setInputDirectorySetting).mockImplementationOnce(
|
||||
() => new Promise((resolve) => { finishFirst = resolve; }),
|
||||
);
|
||||
input.onChange("/first");
|
||||
await vi.advanceTimersByTimeAsync(700);
|
||||
const options = vi.mocked(setInputDirectorySetting).mock.calls[0][1];
|
||||
input.onChange("/second");
|
||||
expect(options?.signal.aborted).toBe(true);
|
||||
finishFirst({ ok: true, data: { input_directory: "/first" } });
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
expect(settings.paths.inputDirectory).toBe("/second");
|
||||
await vi.advanceTimersByTimeAsync(700);
|
||||
expect(settings.paths.inputDirectory).toBe("/second");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createStatusIndicator, setupStatusPolling } from "../features/status/StatusDot.js";
|
||||
import { post } from "../api/client.js";
|
||||
import { ENDPOINTS } from "../api/endpoints.js";
|
||||
|
||||
vi.mock("../api/client.js", () => ({
|
||||
get: vi.fn().mockResolvedValue({ ok: true, data: { input_directory: "/custom/inputs" } }),
|
||||
post: vi.fn().mockResolvedValue({ ok: false, error: "Scan stopped for test" }),
|
||||
listDbBackups: vi.fn().mockResolvedValue({ ok: true, data: { items: [] } }),
|
||||
getToolsStatus: vi.fn().mockResolvedValue({ ok: false }),
|
||||
getWatcherStatus: vi.fn().mockResolvedValue({ ok: false }),
|
||||
}));
|
||||
vi.mock("../app/i18n.js", () => ({
|
||||
t: (key: string, fallback?: string) => fallback || key,
|
||||
}));
|
||||
vi.mock("../app/toast.js", () => ({ comfyToast: vi.fn(), recordToastHistory: vi.fn() }));
|
||||
vi.mock("../app/settings.js", () => ({ loadMajoorSettings: () => ({ scan: { fastMode: true } }) }));
|
||||
|
||||
describe("Index Status Scan button", () => {
|
||||
let section: any;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
section?._mjrStatusPollDispose?.();
|
||||
section?.remove();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("exposes a labeled button even when status details are collapsed and scans the current scope", async () => {
|
||||
section = createStatusIndicator();
|
||||
document.body.appendChild(section);
|
||||
const dot = section.querySelector("#mjr-status-dot");
|
||||
const text = section.querySelector("#mjr-status-text");
|
||||
let scope = "input";
|
||||
setupStatusPolling(dot, text, section, null, null, () => ({ scope }));
|
||||
const button = section.querySelector("#mjr-status-scan") as HTMLButtonElement;
|
||||
expect(button.textContent).toBe("Scan");
|
||||
expect(button.type).toBe("button");
|
||||
expect(section.querySelector("#mjr-status-body").style.display).toBe("none");
|
||||
expect(button.closest("#mjr-status-body")).toBeNull();
|
||||
button.click();
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
expect(post).toHaveBeenLastCalledWith(
|
||||
ENDPOINTS.SCAN, expect.objectContaining({ scope: "input", recursive: true }),
|
||||
);
|
||||
scope = "output";
|
||||
button.click();
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
expect(post).toHaveBeenLastCalledWith(
|
||||
ENDPOINTS.SCAN, expect.objectContaining({ scope: "output" }),
|
||||
);
|
||||
dot.click();
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
expect(post).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user