Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .jules/palette.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@
## 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.

## 2026-07-14 - 동적 ν…Œμ΄λΈ” 및 비동기 λ²„νŠΌ μ ‘κ·Όμ„± ν–₯상
**Learning:** λ™μ μœΌλ‘œ μƒμ„±λ˜λŠ” ν…Œμ΄λΈ” ν–‰ λ‚΄μ˜ μ•„μ΄μ½˜/ν…μŠ€νŠΈ λ²„νŠΌμ΄λ‚˜ 링크(예: "Details", "Status JSON")λŠ” 슀크린 리더 μ‚¬μš©μžμ—κ²Œ μ»¨ν…μŠ€νŠΈλ₯Ό μ œκ³΅ν•˜μ§€ λͺ»ν•˜λŠ” κ²½μš°κ°€ λ§ŽμŠ΅λ‹ˆλ‹€. λ˜ν•œ, 비동기 μž‘μ—… μ‹œ λ‘œλ”© μƒνƒœλ₯Ό ν‘œμ‹œν•  λ•Œ κΈ°μ‘΄ λ…Έλ“œλ₯Ό λ‹¨μˆœνžˆ μ‚­μ œ/μž¬μ„±μ„±ν•˜λŠ” λŒ€μ‹  `innerHTML`을 λ°±μ—…/λ³΅μ›ν•˜λ©΄μ„œ `aria-busy="true"` 속성을 λΆ€μ—¬ν•˜λŠ” νŒ¨ν„΄μ΄ 이 ν”„λ‘œμ νŠΈμ˜ μ»΄ν¬λ„ŒνŠΈ μ ‘κ·Όμ„± 및 μƒνƒœ μœ μ§€μ— 더 적합함을 ν™•μΈν–ˆμŠ΅λ‹ˆλ‹€.
**Action:** μ•žμœΌλ‘œ 동적 ν…Œμ΄λΈ” μ•‘μ…˜ λ²„νŠΌμ„ 생성할 λ•ŒλŠ” λ°˜λ“œμ‹œ row의 고유 정보(예: 파일λͺ…)λ₯Ό ν¬ν•¨ν•œ `aria-label`을 μ „λ‹¬ν•˜κ³ , 비동기 μƒνƒœ ν† κΈ€ μ‹œμ—λŠ” `innerHTML` 기반 볡원과 `aria-busy` 속성을 κΈ°λ³Έ νŒ¨ν„΄μœΌλ‘œ μ μš©ν•˜κ² μŠ΅λ‹ˆλ‹€.
46 changes: 31 additions & 15 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,11 +113,14 @@ 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";
if (ariaLabel) {
button.setAttribute("aria-label", ariaLabel);
}
button.addEventListener("click", onClick);
return button;
}
Expand Down Expand Up @@ -146,20 +152,22 @@ function renderHistory(history = loadHistory()) {
if (job.statusUrl) {
actionsCell.appendChild(createActionButton("Details", (e) => {
const btn = e.currentTarget;
const initialChildren = Array.from(btn.childNodes);
const initialHtml = btn.innerHTML;
btn.disabled = true;
btn.textContent = "Loading...";
btn.setAttribute("aria-busy", "true");
openJobDetail(job).finally(() => {
btn.replaceChildren(...initialChildren);
btn.removeAttribute("aria-busy");
btn.innerHTML = initialHtml;
btn.disabled = false;
});
}));
}, `View details for ${job.fileName || "Document"}`));
actionsCell.appendChild(createActionButton("Status JSON", () => {
void openJsonDocument(job.statusUrl, "Clearfolio status JSON");
}));
}, `View 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 @@ -297,9 +305,10 @@ async function retryActiveJob() {
}

const jobId = activeJobDetail.jobId;
const initialChildren = Array.from(el.retryJobBtn.childNodes);
const initialHtml = el.retryJobBtn.innerHTML;
el.retryJobBtn.disabled = true;
el.retryJobBtn.textContent = "Retrying...";
el.retryJobBtn.setAttribute("aria-busy", "true");
setStatus("Requesting operator retry...");

try {
Expand Down Expand Up @@ -338,7 +347,8 @@ async function retryActiveJob() {
} catch (err) {
setError("Network error while requesting retry. Retry when the service is reachable.");
} finally {
el.retryJobBtn.replaceChildren(...initialChildren);
el.retryJobBtn.removeAttribute("aria-busy");
el.retryJobBtn.innerHTML = initialHtml;
el.retryJobBtn.disabled = false;
}
}
Expand Down Expand Up @@ -412,9 +422,10 @@ async function refreshKpis() {
}

async function refreshKpiEvidence() {
const initialChildren = Array.from(el.refreshEvidenceBtn.childNodes);
const initialHtml = el.refreshEvidenceBtn.innerHTML;
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 @@ -427,15 +438,17 @@ async function refreshKpiEvidence() {
} catch (err) {
el.kpiExportStatus.textContent = "Snapshot evidence is unavailable while the service is unreachable.";
} finally {
el.refreshEvidenceBtn.replaceChildren(...initialChildren);
el.refreshEvidenceBtn.removeAttribute("aria-busy");
el.refreshEvidenceBtn.innerHTML = initialHtml;
el.refreshEvidenceBtn.disabled = false;
}
}

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

try {
Expand All @@ -458,7 +471,8 @@ async function loadDemoData() {
} catch (err) {
setError("Unable to load seeded demo story.");
} finally {
el.loadDemoDataBtn.replaceChildren(...initialChildren);
el.loadDemoDataBtn.removeAttribute("aria-busy");
el.loadDemoDataBtn.innerHTML = initialHtml;
el.loadDemoDataBtn.disabled = false;
}
}
Expand Down Expand Up @@ -505,9 +519,10 @@ async function submitDocument(event) {
return;
}

const initialChildren = Array.from(el.submitBtn.childNodes);
const initialHtml = el.submitBtn.innerHTML;
el.submitBtn.disabled = true;
el.submitBtn.textContent = "Submitting...";
el.submitBtn.setAttribute("aria-busy", "true");
setStatus("Submitting document...");

try {
Expand Down Expand Up @@ -550,7 +565,8 @@ async function submitDocument(event) {
addFailedHistory(file.name, "FAILED");
setError("Network error while submitting. Retry when the service is reachable.");
} finally {
el.submitBtn.replaceChildren(...initialChildren);
el.submitBtn.removeAttribute("aria-busy");
el.submitBtn.innerHTML = initialHtml;
el.submitBtn.disabled = false;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@ void demoScriptUsesExistingApiAndSessionHistory() throws Exception {
assertTrue(script.contains("X-Clearfolio-Tenant-Id"));
assertTrue(script.contains("X-Clearfolio-Permissions"));
assertTrue(script.contains("deadLettered"));
assertTrue(script.contains("ariaLabel"));
assertTrue(script.contains("aria-label"));
assertTrue(script.contains("aria-busy"));
assertTrue(script.contains("initialHtml = btn.innerHTML"));
}
}

Expand Down
Loading