Skip to content
Closed
3 changes: 3 additions & 0 deletions .jules/palette.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@
## 2026-07-13 - Async Table Actions UX
**Learning:** Adding explicit loading and disabled states to table action buttons that invoke asynchronous processes helps prevent redundant API calls and visually assures the user that their request is being handled.
**Action:** Consistently apply `disabled` state and `Loading...` text changes to inline table action buttons linked to async workflows, and carefully preserve underlying DOM structures with `Array.from(btn.childNodes)` during the loading cycle to avoid rendering regressions.
## 2024-05-24 - ๋™์  ํ…Œ์ด๋ธ” ๋ฐ ๋น„๋™๊ธฐ ์ž‘์—…์˜ ์ ‘๊ทผ์„ฑ ํ–ฅ์ƒ
**Learning:** ๋™์ ์œผ๋กœ ์ƒ์„ฑ๋˜๋Š” ํ…Œ์ด๋ธ” ๋‚ด ์•ก์…˜ ๋ฒ„ํŠผ(์˜ˆ: Details, Status JSON)์€ ํ…์ŠคํŠธ๋งŒ์œผ๋กœ ์Šคํฌ๋ฆฐ ๋ฆฌ๋”์—์„œ ๋งฅ๋ฝ์„ ํŒŒ์•…ํ•˜๊ธฐ ์–ด๋ ค์šฐ๋ฏ€๋กœ ๋™์  `aria-label`(์˜ˆ: `Details for [ํŒŒ์ผ๋ช…]`)์„ ๋ถ€์—ฌํ•ด์•ผ ํ˜ผ๋™์„ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ์Œ์„ ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค. ๋˜ํ•œ, ๋น„๋™๊ธฐ ์ž‘์—… ์‹œ ๋ฒ„ํŠผ์ด ๋‹จ์ˆœํžˆ ๋น„ํ™œ์„ฑํ™”(disabled)๋˜๋Š” ๊ฒƒ ์™ธ์—๋„ `aria-busy="true"`๋ฅผ ์ ์šฉํ•ด์•ผ ๋ณด์กฐ ๊ธฐ์ˆ ์ด ๋กœ๋”ฉ ์ƒํƒœ๋ฅผ ๋ช…ํ™•ํžˆ ์ธ์ง€ํ•จ์„ ์žฌํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค.
**Action:** ํ–ฅํ›„ ๋ฆฌ์ŠคํŠธ๋‚˜ ํ…Œ์ด๋ธ” ์•ˆ์—์„œ ๋ฐ˜๋ณต๋˜๋Š” ์•ก์…˜ ์š”์†Œ๋‚˜ ๋น„๋™๊ธฐ ์š”์ฒญ์„ ์ˆ˜ํ–‰ํ•˜๋Š” ์š”์†Œ ๊ตฌํ˜„ ์‹œ, ๋™์ ์ธ ์‹๋ณ„ ์ •๋ณด๋ฅผ ํฌํ•จํ•œ `aria-label`๊ณผ `aria-busy` ์ƒํƒœ ํ† ๊ธ€์„ ํ•„์ˆ˜๋กœ ์ ์šฉํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.
26 changes: 21 additions & 5 deletions src/main/resources/static/assets/viewer/demo.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,16 @@ function updateJob(jobId, patch, { refreshKpisAfterUpdate = true } = {}) {
}
}

function createLink(href, label) {
function createLink(href, label, ariaLabel) {
const link = document.createElement("a");
link.href = href;
link.textContent = label;
link.className = "table-link";
link.target = "_blank";
link.rel = "noopener noreferrer";
if (ariaLabel) {
link.setAttribute("aria-label", ariaLabel);
}
return link;
}

Expand All @@ -110,12 +113,15 @@ async function openJsonDocument(url, title) {
: "Unable to load JSON evidence with the current tenant claim.";
}

function createActionButton(label, onClick) {
function createActionButton(label, onClick, ariaLabel) {
const button = document.createElement("button");
button.type = "button";
button.textContent = label;
button.className = "btn btn-secondary btn-compact";
button.addEventListener("click", onClick);
if (ariaLabel) {
button.setAttribute("aria-label", ariaLabel);
}
return button;
}

Expand Down Expand Up @@ -149,17 +155,19 @@ function renderHistory(history = loadHistory()) {
const initialChildren = Array.from(btn.childNodes);
btn.disabled = true;
btn.textContent = "Loading...";
btn.setAttribute("aria-busy", "true");
openJobDetail(job).finally(() => {
btn.replaceChildren(...initialChildren);
btn.disabled = false;
btn.removeAttribute("aria-busy");
});
}));
}, `Details for ${job.fileName || "Document"}`));
actionsCell.appendChild(createActionButton("Status JSON", () => {
void openJsonDocument(job.statusUrl, "Clearfolio status JSON");
}));
}, `Status JSON for ${job.fileName || "Document"}`));
}
if (job.jobId) {
actionsCell.appendChild(createLink(`/viewer/${encodeURIComponent(job.jobId)}`, "Open viewer"));
actionsCell.appendChild(createLink(`/viewer/${encodeURIComponent(job.jobId)}`, "Open viewer", `Open viewer for ${job.fileName || "Document"}`));
}

row.append(fileCell, statusCell, submittedCell, actionsCell);
Expand Down Expand Up @@ -300,6 +308,7 @@ async function retryActiveJob() {
const initialChildren = Array.from(el.retryJobBtn.childNodes);
el.retryJobBtn.disabled = true;
el.retryJobBtn.textContent = "Retrying...";
el.retryJobBtn.setAttribute("aria-busy", "true");
setStatus("Requesting operator retry...");

try {
Expand Down Expand Up @@ -340,6 +349,7 @@ async function retryActiveJob() {
} finally {
el.retryJobBtn.replaceChildren(...initialChildren);
el.retryJobBtn.disabled = false;
el.retryJobBtn.removeAttribute("aria-busy");
}
}

Expand Down Expand Up @@ -415,6 +425,7 @@ async function refreshKpiEvidence() {
const initialChildren = Array.from(el.refreshEvidenceBtn.childNodes);
el.refreshEvidenceBtn.disabled = true;
el.refreshEvidenceBtn.textContent = "Refreshing...";
el.refreshEvidenceBtn.setAttribute("aria-busy", "true");

try {
const { res, data } = await fetchJson(KPI_EXPORTS_ENDPOINT);
Expand All @@ -429,13 +440,15 @@ async function refreshKpiEvidence() {
} finally {
el.refreshEvidenceBtn.replaceChildren(...initialChildren);
el.refreshEvidenceBtn.disabled = false;
el.refreshEvidenceBtn.removeAttribute("aria-busy");
}
}

async function loadDemoData() {
const initialChildren = Array.from(el.loadDemoDataBtn.childNodes);
el.loadDemoDataBtn.disabled = true;
el.loadDemoDataBtn.textContent = "Loading...";
el.loadDemoDataBtn.setAttribute("aria-busy", "true");
setStatus("Loading seeded buyer-demo story...");

try {
Expand All @@ -460,6 +473,7 @@ async function loadDemoData() {
} finally {
el.loadDemoDataBtn.replaceChildren(...initialChildren);
el.loadDemoDataBtn.disabled = false;
el.loadDemoDataBtn.removeAttribute("aria-busy");
}
}

Expand Down Expand Up @@ -508,6 +522,7 @@ async function submitDocument(event) {
const initialChildren = Array.from(el.submitBtn.childNodes);
el.submitBtn.disabled = true;
el.submitBtn.textContent = "Submitting...";
el.submitBtn.setAttribute("aria-busy", "true");
setStatus("Submitting document...");

try {
Expand Down Expand Up @@ -552,6 +567,7 @@ async function submitDocument(event) {
} finally {
el.submitBtn.replaceChildren(...initialChildren);
el.submitBtn.disabled = false;
el.submitBtn.removeAttribute("aria-busy");
}
}

Expand Down
Loading