fix(dashboard): restore shift-click suggestion submit
Default domain was retired slug "custodian", so POST /technical-debt/ returned 422 and the modal failed after open. Use infotech, resolve apiBase like config.js, and surface API error detail in the toast.
This commit is contained in:
parent
224584d379
commit
c363549388
3 changed files with 116 additions and 7 deletions
|
|
@ -10,7 +10,9 @@ const _configDir = dirname(fileURLToPath(import.meta.url));
|
|||
const _modalScript = readFileSync(
|
||||
join(_configDir, "src/components/improvement-modal.js"), "utf-8"
|
||||
)
|
||||
.replace(/^export function /gm, "function ") // strip ES module export
|
||||
// Strip ES module exports so the file can run as a plain <script>.
|
||||
.replace(/^export function /gm, "function ")
|
||||
.replace(/^export const /gm, "const ")
|
||||
+ "\ninitImprovementModal();\n"; // auto-initialise
|
||||
|
||||
export default {
|
||||
|
|
|
|||
|
|
@ -12,10 +12,59 @@
|
|||
* Submissions are stored as technical-debt items with debt_type="dashboard-improvement".
|
||||
*
|
||||
* Interaction:
|
||||
* - Hold Shift → cursor changes to crosshair across the entire page
|
||||
* - Hold Shift 1s → cursor changes to copy/highlight mode (shift-wait-click)
|
||||
* - Shift+click any element (except form controls) → opens suggestion modal
|
||||
*/
|
||||
|
||||
/** Domain slug for dashboard-improvement TD records (state-hub lives under infotech). */
|
||||
export const DEFAULT_IMPROVEMENT_DOMAIN = "infotech";
|
||||
const DEFAULT_API = "http://127.0.0.1:8000";
|
||||
const API_STORAGE_KEY = "stateHubApiBase";
|
||||
|
||||
/**
|
||||
* Resolve API base when init is called without an explicit apiBase.
|
||||
* Mirrors dashboard/src/components/config.js so the modal works when the
|
||||
* dashboard is not on 127.0.0.1 (remote host, tunnel, storage override).
|
||||
*/
|
||||
export function resolveImprovementApiBase({
|
||||
location = globalThis.location,
|
||||
storage = globalThis.localStorage,
|
||||
} = {}) {
|
||||
const clean = (value) => {
|
||||
if (typeof value !== "string") return null;
|
||||
const cleaned = value.trim().replace(/\/+$/, "");
|
||||
return cleaned || null;
|
||||
};
|
||||
try {
|
||||
if (location?.href) {
|
||||
const url = new URL(location.href);
|
||||
for (const name of ["api_base", "apiBase"]) {
|
||||
const q = clean(url.searchParams.get(name));
|
||||
if (q) return q;
|
||||
}
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
const fromGlobal = clean(globalThis.STATE_HUB_API_BASE);
|
||||
if (fromGlobal) return fromGlobal;
|
||||
try {
|
||||
const fromStorage = clean(storage?.getItem?.(API_STORAGE_KEY));
|
||||
if (fromStorage) return fromStorage;
|
||||
} catch { /* ignore */ }
|
||||
try {
|
||||
if (location?.href) {
|
||||
const url = new URL(location.href);
|
||||
if (!["http:", "https:"].includes(url.protocol)) return DEFAULT_API;
|
||||
if (url.hostname === "::1" || url.hostname === "[::1]") return DEFAULT_API;
|
||||
url.port = globalThis.STATE_HUB_API_PORT || "8000";
|
||||
url.pathname = "";
|
||||
url.search = "";
|
||||
url.hash = "";
|
||||
return url.origin;
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
return DEFAULT_API;
|
||||
}
|
||||
|
||||
const _STYLE_ID = "improvement-modal-styles";
|
||||
|
||||
function _ensureStyles() {
|
||||
|
|
@ -239,10 +288,13 @@ let _initialized = false;
|
|||
* Safe to call multiple times — only the first call takes effect.
|
||||
*
|
||||
* @param {object} opts
|
||||
* @param {string} opts.apiBase State Hub API base URL (default: "http://127.0.0.1:8000")
|
||||
* @param {string} opts.domain Domain slug for the TD record (default: "custodian")
|
||||
* @param {string} [opts.apiBase] State Hub API base URL (resolved like config.js if omitted)
|
||||
* @param {string} [opts.domain] Domain slug for the TD record (default: "infotech")
|
||||
*/
|
||||
export function initImprovementModal({ apiBase = "http://127.0.0.1:8000", domain = "custodian" } = {}) {
|
||||
export function initImprovementModal({
|
||||
apiBase = resolveImprovementApiBase(),
|
||||
domain = DEFAULT_IMPROVEMENT_DOMAIN,
|
||||
} = {}) {
|
||||
if (_initialized) return;
|
||||
_initialized = true;
|
||||
_ensureStyles();
|
||||
|
|
@ -391,7 +443,10 @@ export function initImprovementModal({ apiBase = "http://127.0.0.1:8000", domain
|
|||
try {
|
||||
const r = await fetch(`${apiBase}/technical-debt/`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-StateHub-Component": "state-hub.dashboard.improvement-modal",
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (r.ok) {
|
||||
|
|
@ -400,7 +455,13 @@ export function initImprovementModal({ apiBase = "http://127.0.0.1:8000", domain
|
|||
} else {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = "Submit suggestion";
|
||||
_toast(`⚠ Submission failed (HTTP ${r.status})`);
|
||||
let detail = `HTTP ${r.status}`;
|
||||
try {
|
||||
const body = await r.json();
|
||||
if (typeof body?.detail === "string") detail = body.detail;
|
||||
else if (body?.detail != null) detail = JSON.stringify(body.detail);
|
||||
} catch { /* keep status-only detail */ }
|
||||
_toast(`⚠ Submission failed (${detail})`);
|
||||
}
|
||||
} catch {
|
||||
submitBtn.disabled = false;
|
||||
|
|
|
|||
46
dashboard/test/improvement-modal.test.mjs
Normal file
46
dashboard/test/improvement-modal.test.mjs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import {readFileSync} from "node:fs";
|
||||
import {dirname, join} from "node:path";
|
||||
import {fileURLToPath} from "node:url";
|
||||
|
||||
import {
|
||||
DEFAULT_IMPROVEMENT_DOMAIN,
|
||||
resolveImprovementApiBase,
|
||||
} from "../src/components/improvement-modal.js";
|
||||
|
||||
const root = dirname(fileURLToPath(import.meta.url));
|
||||
const modalSource = readFileSync(
|
||||
join(root, "../src/components/improvement-modal.js"),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
test("dashboard-improvement default domain is the live infotech slug", () => {
|
||||
assert.equal(DEFAULT_IMPROVEMENT_DOMAIN, "infotech");
|
||||
// Regression: custodian was retired from the domain registry; POST /technical-debt/
|
||||
// returns 422 for unknown domain, so the shift-wait-click modal must not use it.
|
||||
assert.doesNotMatch(modalSource, /domain\s*=\s*["']custodian["']/);
|
||||
assert.match(modalSource, /domain\s*=\s*DEFAULT_IMPROVEMENT_DOMAIN/);
|
||||
assert.match(modalSource, /debt_type:\s*["']dashboard-improvement["']/);
|
||||
});
|
||||
|
||||
test("improvement modal api base mirrors dashboard config resolution", () => {
|
||||
assert.equal(
|
||||
resolveImprovementApiBase({location: null, storage: null}),
|
||||
"http://127.0.0.1:8000",
|
||||
);
|
||||
assert.equal(
|
||||
resolveImprovementApiBase({
|
||||
location: new URL("http://localhost:3000/workstreams"),
|
||||
storage: null,
|
||||
}),
|
||||
"http://localhost:8000",
|
||||
);
|
||||
assert.equal(
|
||||
resolveImprovementApiBase({
|
||||
location: new URL("http://localhost:3000/?api_base=http%3A%2F%2F127.0.0.1%3A18000%2F"),
|
||||
storage: {getItem: () => "http://ignored.example:8000"},
|
||||
}),
|
||||
"http://127.0.0.1:18000",
|
||||
);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue