Skip to content

Commit c649d88

Browse files
committed
pdf-server: fix Reset bug; show cleared baseline fields w/ revert
Reset-all bug: clearUserFormStorage skipped any field whose name was in pdfBaselineFormValues — but if the user had EDITED a baseline field, the edit sits in storage under that name. Skipping it left the widget showing the stale edit while the panel showed the restored baseline. Fix: remove ALL storage overrides on reset — every field reverts to the PDF's /V, which IS baseline. Removed the now-dead helper. Panel: cleared baseline fields now stay visible instead of vanishing. State is derived per-field by comparing formFieldValues to pdfBaselineFormValues (no new data structures — both maps already exist, the comparison was just never rendered): - unchanged: current === baseline → solid swatch, trash clears - modified: baseline exists, current differs → solid swatch, revert - cleared: baseline exists, current empty/absent → outlined cross swatch, struck-out label/value, revert restores - added: no baseline → solid swatch, trash removes Panel iterates union(formFieldValues, pdfBaselineFormValues) so cleared items don't disappear. sidebarItemCount uses the same union so the toolbar button stays visible as long as there are baseline items OR edits. Per-item revert: formFieldValues.set(name, baseline) + storage.remove(id) → widget reverts to /V.
1 parent c099442 commit c649d88

2 files changed

Lines changed: 124 additions & 39 deletions

File tree

examples/pdf-server/src/mcp-app.css

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1046,6 +1046,23 @@ body {
10461046
border: 1px solid rgba(0, 0, 0, 0.15);
10471047
}
10481048

1049+
/* Swatch for baseline items the user cleared — outlined with a cross
1050+
instead of solid fill, so the panel still shows "this existed in the
1051+
file and you removed it" rather than vanishing. */
1052+
.annotation-card-swatch-cleared {
1053+
background: transparent;
1054+
border-color: #4a90d9;
1055+
display: flex;
1056+
align-items: center;
1057+
justify-content: center;
1058+
}
1059+
1060+
.annotation-card-cleared .annotation-card-type,
1061+
.annotation-card-cleared .annotation-card-preview {
1062+
opacity: 0.55;
1063+
text-decoration: line-through;
1064+
}
1065+
10491066
.annotation-card-type {
10501067
font-size: 0.7rem;
10511068
color: var(--text200);

examples/pdf-server/src/mcp-app.ts

Lines changed: 107 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2194,9 +2194,36 @@ function toggleAnnotationPanel(): void {
21942194
setAnnotationPanelOpen(annotationPanelUserPref);
21952195
}
21962196

2197-
/** Total count of annotations + filled form fields for the sidebar badge. */
2197+
/**
2198+
* Derived state of a form field relative to the PDF baseline.
2199+
* Not stored — computed on demand by comparing formFieldValues to
2200+
* pdfBaselineFormValues.
2201+
*/
2202+
type FieldState =
2203+
| "unchanged" // current === baseline (came from the PDF, untouched)
2204+
| "modified" // baseline exists but current differs
2205+
| "cleared" // baseline exists but current is absent/empty
2206+
| "added"; // no baseline — user-filled or fill_form
2207+
2208+
function fieldState(name: string): FieldState {
2209+
const cur = formFieldValues.get(name);
2210+
const base = pdfBaselineFormValues.get(name);
2211+
if (base === undefined) return "added";
2212+
if (cur === undefined || cur === "" || cur === false) return "cleared";
2213+
return cur === base ? "unchanged" : "modified";
2214+
}
2215+
2216+
/** All field names that should appear in the panel: current ∪ baseline.
2217+
* Cleared baseline fields remain visible (crossed out) so they can be
2218+
* reverted individually. */
2219+
function panelFieldNames(): Set<string> {
2220+
return new Set([...formFieldValues.keys(), ...pdfBaselineFormValues.keys()]);
2221+
}
2222+
2223+
/** Total count of annotations + form fields for the sidebar badge.
2224+
* Uses the union so cleared baseline items still contribute. */
21982225
function sidebarItemCount(): number {
2199-
return annotationMap.size + formFieldValues.size;
2226+
return annotationMap.size + panelFieldNames().size;
22002227
}
22012228

22022229
function updateAnnotationsBadge(): void {
@@ -2327,12 +2354,19 @@ function renderAnnotationPanel(): void {
23272354
byPage.get(page)!.push(tracked);
23282355
}
23292356

2330-
// Group form fields by page
2331-
const fieldsByPage = new Map<number, [string, string | boolean][]>();
2332-
for (const [name, value] of formFieldValues) {
2357+
// Group form fields by page — iterate the UNION so cleared baseline
2358+
// fields remain visible (crossed out) with a per-item revert button.
2359+
const fieldsByPage = new Map<number, string[]>();
2360+
for (const name of panelFieldNames()) {
23332361
const page = fieldNameToPage.get(name) ?? 1;
23342362
if (!fieldsByPage.has(page)) fieldsByPage.set(page, []);
2335-
fieldsByPage.get(page)!.push([name, value]);
2363+
fieldsByPage.get(page)!.push(name);
2364+
}
2365+
// Sort fields by their intrinsic document order within each page
2366+
for (const names of fieldsByPage.values()) {
2367+
names.sort(
2368+
(a, b) => (fieldNameToOrder.get(a) ?? 0) - (fieldNameToOrder.get(b) ?? 0),
2369+
);
23362370
}
23372371

23382372
// Collect all pages that have annotations or form fields
@@ -2369,8 +2403,8 @@ function renderAnnotationPanel(): void {
23692403
pageNum === currentPage,
23702404
(body) => {
23712405
// Form fields first
2372-
for (const [name, value] of fields) {
2373-
body.appendChild(createFormFieldCard(name, value));
2406+
for (const name of fields) {
2407+
body.appendChild(createFormFieldCard(name));
23742408
}
23752409
// Then annotations
23762410
for (const tracked of annotations) {
@@ -2541,54 +2575,85 @@ function createAnnotationCard(tracked: TrackedAnnotation): HTMLElement {
25412575
return card;
25422576
}
25432577

2544-
function createFormFieldCard(
2545-
name: string,
2546-
value: string | boolean,
2547-
): HTMLElement {
2578+
const TRASH_SVG = `<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M2 3h8M4.5 3V2a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v1M5 5.5v3M7 5.5v3M3 3l.5 7a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1L9 3"/></svg>`;
2579+
const REVERT_SVG = `<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2 6a4 4 0 1 1 1.2 2.85"/><path d="M2 9V6h3"/></svg>`;
2580+
2581+
/** Revert one field to its PDF-stored baseline value. */
2582+
function revertFieldToBaseline(name: string): void {
2583+
const base = pdfBaselineFormValues.get(name);
2584+
if (base === undefined) return;
2585+
formFieldValues.set(name, base);
2586+
// Remove our storage override → widget falls back to PDF's /V = baseline
2587+
if (pdfDocument) {
2588+
const ids = fieldNameToIds.get(name);
2589+
if (ids) for (const id of ids) pdfDocument.annotationStorage.remove(id);
2590+
}
2591+
}
2592+
2593+
function createFormFieldCard(name: string): HTMLElement {
2594+
const state = fieldState(name);
2595+
const value = formFieldValues.get(name);
2596+
const baseValue = pdfBaselineFormValues.get(name);
2597+
25482598
const card = document.createElement("div");
25492599
card.className = "annotation-card";
2600+
if (state === "cleared") card.classList.add("annotation-card-cleared");
25502601

25512602
const row = document.createElement("div");
25522603
row.className = "annotation-card-row";
25532604

2554-
// Color swatch (blue for form fields)
2605+
// Swatch: solid blue normally; crossed-out for cleared baseline fields
25552606
const swatch = document.createElement("div");
25562607
swatch.className = "annotation-card-swatch";
2557-
swatch.style.background = "#4a90d9";
2608+
if (state === "cleared") {
2609+
swatch.classList.add("annotation-card-swatch-cleared");
2610+
swatch.innerHTML = `<svg width="10" height="10" viewBox="0 0 10 10" stroke="#4a90d9" stroke-width="1.5" stroke-linecap="round"><path d="M2 2l6 6M8 2L2 8"/></svg>`;
2611+
} else {
2612+
swatch.style.background = "#4a90d9";
2613+
}
2614+
// Subtle modified marker
2615+
if (state === "modified") swatch.title = "Modified from file";
25582616
row.appendChild(swatch);
25592617

25602618
// Field label
2561-
const label = getFormFieldLabel(name);
25622619
const nameEl = document.createElement("span");
25632620
nameEl.className = "annotation-card-type";
2564-
nameEl.textContent = label;
2621+
nameEl.textContent = getFormFieldLabel(name);
25652622
row.appendChild(nameEl);
25662623

2567-
// Field value preview
2624+
// Value preview: show current, or struck-out baseline when cleared
2625+
const shown = state === "cleared" ? baseValue : value;
25682626
const displayValue =
2569-
typeof value === "boolean" ? (value ? "checked" : "unchecked") : value;
2627+
typeof shown === "boolean" ? (shown ? "checked" : "unchecked") : shown;
25702628
if (displayValue) {
25712629
const valueEl = document.createElement("span");
25722630
valueEl.className = "annotation-card-preview";
25732631
valueEl.textContent = displayValue;
25742632
row.appendChild(valueEl);
25752633
}
25762634

2577-
// Delete button
2578-
const deleteBtn = document.createElement("button");
2579-
deleteBtn.className = "annotation-card-delete";
2580-
deleteBtn.title = "Clear field";
2581-
deleteBtn.innerHTML = `<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M2 3h8M4.5 3V2a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v1M5 5.5v3M7 5.5v3M3 3l.5 7a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1L9 3"/></svg>`;
2582-
deleteBtn.addEventListener("click", (e) => {
2635+
// Action button: revert for modified/cleared baseline fields, trash otherwise
2636+
const isRevertable = state === "modified" || state === "cleared";
2637+
const actionBtn = document.createElement("button");
2638+
actionBtn.className = "annotation-card-delete";
2639+
actionBtn.title = isRevertable
2640+
? "Revert to value stored in file"
2641+
: "Clear field";
2642+
actionBtn.innerHTML = isRevertable ? REVERT_SVG : TRASH_SVG;
2643+
actionBtn.addEventListener("click", (e) => {
25832644
e.stopPropagation();
2584-
formFieldValues.delete(name);
2585-
clearFieldInStorage(name);
2645+
if (isRevertable) {
2646+
revertFieldToBaseline(name);
2647+
} else {
2648+
formFieldValues.delete(name);
2649+
clearFieldInStorage(name);
2650+
}
25862651
updateAnnotationsBadge();
25872652
renderAnnotationPanel();
25882653
renderPage();
25892654
persistAnnotations();
25902655
});
2591-
row.appendChild(deleteBtn);
2656+
row.appendChild(actionBtn);
25922657

25932658
// Click handler: navigate to page and focus form input
25942659
card.addEventListener("click", () => {
@@ -2853,28 +2918,31 @@ function clearFieldInStorage(name: string): void {
28532918
for (const id of ids) storage.setValue(id, { value: clearValue });
28542919
}
28552920

2856-
/** Remove all user-sourced entries from annotationStorage, leaving the
2857-
* PDF's own stored values intact. */
2858-
function clearUserFormStorage(): void {
2859-
if (!pdfDocument) return;
2860-
for (const [name] of formFieldValues) {
2861-
if (pdfBaselineFormValues.has(name)) continue; // PDF-native — leave alone
2862-
const ids = fieldNameToIds.get(name);
2863-
if (ids) for (const id of ids) pdfDocument.annotationStorage.remove(id);
2864-
}
2865-
}
2866-
28672921
/**
28682922
* Revert to what's in the PDF file: restore baseline annotations, restore
28692923
* baseline form values, discard all user edits. Result: diff is empty, clean.
2924+
*
2925+
* Form fields: remove ALL storage overrides — every field reverts to the
2926+
* PDF's /V (which IS baseline). We can't skip baseline-named fields: if the
2927+
* user edited one, our override is in storage under that name, and skipping
2928+
* it leaves the widget showing the stale edit.
28702929
*/
28712930
function resetToBaseline(): void {
28722931
clearAnnotationMap();
28732932
for (const def of pdfBaselineAnnotations) {
28742933
annotationMap.set(def.id, { def: { ...def }, elements: [] });
28752934
}
28762935

2877-
clearUserFormStorage();
2936+
if (pdfDocument) {
2937+
const storage = pdfDocument.annotationStorage;
2938+
for (const name of new Set([
2939+
...formFieldValues.keys(),
2940+
...pdfBaselineFormValues.keys(),
2941+
])) {
2942+
const ids = fieldNameToIds.get(name);
2943+
if (ids) for (const id of ids) storage.remove(id);
2944+
}
2945+
}
28782946
formFieldValues.clear();
28792947
for (const [name, value] of pdfBaselineFormValues) {
28802948
formFieldValues.set(name, value);

0 commit comments

Comments
 (0)