Skip to content
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