Skip to content

feat: populate tag writer forms from Spoolman spool picker (#32)#65

Merged
sjordan0228 merged 6 commits into
devfrom
feat/writer-spoolman-populate
Apr 1, 2026
Merged

feat: populate tag writer forms from Spoolman spool picker (#32)#65
sjordan0228 merged 6 commits into
devfrom
feat/writer-spoolman-populate

Conversation

@sjordan0228
Copy link
Copy Markdown
Contributor

@sjordan0228 sjordan0228 commented Mar 31, 2026

Summary

  • Add searchable Spoolman spool picker to all 3 tag writer pages (OpenPrintTag, TigerTag, OpenTag3D)
  • Pick a spool from your Spoolman inventory and form fields auto-populate (material, color, temps, weight, manufacturer, density, diameter)
  • Shows "Configure Spoolman in settings" hint when Spoolman is not configured
  • Shared picker functions in SharedJS.h, reuses existing /api/spoolman/spools proxy endpoint

Closes #32

Codex Review Fixes

  • Fixed hidden material/brand ID sync (dispatch input events after fill)
  • Added missing OpenTag3D fields (colorHex, print_temp_c, bed_temp_c)
  • Fixed OpenTag3D diameter SELECT values (1750/2850 vs TigerTag's 56/221)
  • Fixed color field ID on OpenPrintTag and TigerTag (colorHex, not color)

Test Plan

  • Clean compile on esp32s3zero and esp32dev (verified)
  • Open each writer page — picker section appears with spool list
  • Search filters by vendor/material name
  • Select a spool — form fields populate correctly
  • Color picker syncs with hex value
  • Material hidden IDs update correctly (write tag, verify material type)
  • Disable Spoolman — hint message shows instead of picker

Summary by CodeRabbit

  • New Features
    • Added a Spoolman picker to tag-writer forms (searchable picker with live filtering).
    • Selecting a spool auto-populates material, manufacturer, color, weight/density, diameter, and temperature/bed ranges.
    • Shows a “configure in settings” message when spool data is missing and confirms when a spool is loaded.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 31, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

Adds a Spoolman picker UI to three tag-writer pages and new client-side JS to fetch spools from /api/spoolman/spools, filter results, and populate existing form inputs (material, manufacturer, color, weight, diameter, and temperature ranges) when a spool is selected.

Changes

Cohort / File(s) Summary
Spoolman Picker JS
src/SharedJS.h
Introduces module state (_spoolmanPickerSpools, _spoolmanPickerFieldMap) and four new functions: renderSpoolmanPicker(containerId, fieldMap) (fetch + UI init), filterSpoolmanPicker(spools, query, fieldMap) (client filtering + results render), selectSpoolmanSpool(spoolId) (selection handler), and fillFromSpoolman(spool, fieldMap) (populate form inputs, set data-autoFilled, dispatch input events, sync color picker, handle diameter units).
Writer HTML integrations
src/OpenPrintTagWriterHTML.h, src/OpenTag3DWriterHTML.h, src/TigerTagWriterHTML.h
Add a <div id="spoolmanPicker"> container to each writer form and call renderSpoolmanPicker(...) with a field-map mapping spool properties to existing form input IDs. No form submission or payload-building logic changed.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Page as "Writer Page (JS)"
    participant API as "Spoolman API (/api/spoolman/spools)"
    participant Form as "Form Inputs (DOM)"

    User->>Page: open writer page / interact with picker
    Page->>API: GET /api/spoolman/spools
    API-->>Page: JSON spools list
    Page->>Page: render picker UI + handle search
    User->>Page: type query / click spool row
    Page->>Page: filterSpoolmanPicker -> selectSpoolmanSpool(spoolId)
    Page->>Form: fillFromSpoolman(spool) (set values, set data-autoFilled, dispatch input events)
    Form-->>Page: input events trigger existing sync handlers
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description includes a summary of changes, key fixes, and test plan, but is missing the 'Changes' and 'How to Test' sections from the template with proper formatting, and the 'Checklist' section is incomplete. Restructure to match the template: add a bulleted 'Changes' section, expand 'How to Test' with ordered steps, and complete the 'Checklist' with actual checkmarks or status.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main feature addition: a Spoolman spool picker that auto-populates tag writer form fields, matching the core changes across all modified files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/writer-spoolman-populate

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/SharedJS.h`:
- Around line 449-460: The current import code calls fill('material',
fil.material || fil.name || '') and fill('manufacturer', vendor) and only
dispatches synthetic input events, which leaves hidden ID fields set to fallback
values; instead, when the imported label does not resolve to a local ID you must
clear or invalidate the hidden ID field so it won't silently persist a default.
Update the logic around fill(...) and the input dispatch for fieldMap.material
and fieldMap.manufacturer to detect unresolved labels (no exact-match mapping)
and set the corresponding hidden ID element (the element referenced by
document.getElementById(fieldMap.material) /
document.getElementById(fieldMap.manufacturer)) to an empty/invalid value (or
add a data-invalid flag), then dispatch the 'input' event so downstream handlers
know the ID is cleared; keep the existing successful-path behavior unchanged.
- Around line 467-479: The single-temp fields 'nozzle_single' and 'bed_single'
are being populated from the lower bound (extruder.min / bed.min) which causes
ranges like {min:190,max:220} to set the main print temps to 190; change those
to use the primary/high value by using extruder.max || extruder for
'nozzle_single' and bed.max || bed for 'bed_single' (keep the existing fill(...)
calls and logic for 'nozzle_min', 'nozzle_max', 'bed_min', 'bed_max' unchanged).
- Around line 417-427: The result items are rendered as non-focusable divs so
keyboard users can't focus or activate them; update the template in
results.innerHTML to render each item as a real interactive element (use a
<button type="button"> per item) instead of a <div>, keep the inline style
(padding/border) and swatch/escaped label text, keep the onclick handler calling
selectSpoolmanSpool(spoolId), and ensure the button remains keyboard-accessible
(no role/tabindex hacks needed when using a button); locate the HTML
construction around results.innerHTML and the spoolId/selectSpoolmanSpool usage
to make this change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: f27ebfdc-573c-4563-b40a-d88ace855971

📥 Commits

Reviewing files that changed from the base of the PR and between ab79ade and af7a5a9.

📒 Files selected for processing (4)
  • src/OpenPrintTagWriterHTML.h
  • src/OpenTag3DWriterHTML.h
  • src/SharedJS.h
  • src/TigerTagWriterHTML.h

Comment thread src/SharedJS.h
Comment on lines +417 to +427
results.innerHTML = filtered.slice(0, 20).map(function(spool) {
var fil = spool.filament || {};
var vendor = fil.vendor ? fil.vendor.name : '';
var esc = function(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; };
var color = (fil.color_hex || '').replace(/[^0-9a-fA-F]/g, '');
var swatch = color ? '<span style="display:inline-block;width:14px;height:14px;border-radius:50%;background:#' + color + ';vertical-align:middle;margin-right:6px"></span>' : '';
var remaining = spool.remaining_weight ? Math.round(spool.remaining_weight) + 'g' : '?';
var spoolId = spool.id;
return '<div style="padding:8px 10px;border-bottom:1px solid var(--border);cursor:pointer" onclick="selectSpoolmanSpool(' + spoolId + ')">'
+ swatch + '#' + spoolId + ' ' + (vendor ? esc(vendor) + ' ' : '') + esc(fil.material || fil.name || '?') + ' \u2014 ' + remaining
+ '</div>';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Render picker results as real buttons.

These entries are non-focusable <div>s with only onclick, so keyboard users can search but cannot focus a result or activate it with Enter/Space.

♿ Minimal fix
-    return '<div style="padding:8px 10px;border-bottom:1px solid var(--border);cursor:pointer" onclick="selectSpoolmanSpool(' + spoolId + ')">'
+    return '<button type="button" style="display:block;width:100%;padding:8px 10px;border:0;border-bottom:1px solid var(--border);background:transparent;color:inherit;text-align:left;font:inherit;cursor:pointer" onclick="selectSpoolmanSpool(' + spoolId + ')">'
       + swatch + '#' + spoolId + ' ' + (vendor ? esc(vendor) + ' ' : '') + esc(fil.material || fil.name || '?') + ' \u2014 ' + remaining
-      + '</div>';
+      + '</button>';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/SharedJS.h` around lines 417 - 427, The result items are rendered as
non-focusable divs so keyboard users can't focus or activate them; update the
template in results.innerHTML to render each item as a real interactive element
(use a <button type="button"> per item) instead of a <div>, keep the inline
style (padding/border) and swatch/escaped label text, keep the onclick handler
calling selectSpoolmanSpool(spoolId), and ensure the button remains
keyboard-accessible (no role/tabindex hacks needed when using a button); locate
the HTML construction around results.innerHTML and the
spoolId/selectSpoolmanSpool usage to make this change.

Comment thread src/SharedJS.h
Comment on lines +449 to +460
fill('material', fil.material || fil.name || '');
fill('manufacturer', vendor);

// Dispatch input events to trigger hidden ID sync handlers
if (fieldMap.material) {
var matEl = document.getElementById(fieldMap.material);
if (matEl) matEl.dispatchEvent(new Event('input', {bubbles: true}));
}
if (fieldMap.manufacturer) {
var mfgEl = document.getElementById(fieldMap.manufacturer);
if (mfgEl) mfgEl.dispatchEvent(new Event('input', {bubbles: true}));
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fail closed when an imported label cannot be resolved to a local ID.

This path only updates the visible inputs and dispatches synthetic input events. On OpenPrintTag/TigerTag, the downstream handlers reset hidden IDs to default values before an exact-match lookup, and /api/spoolman/spools forwards Spoolman's raw labels unchanged. Any vendor/material miss will therefore write PLA/Generic IDs silently. Clear or invalidate the hidden ID instead of keeping the fallback.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/SharedJS.h` around lines 449 - 460, The current import code calls
fill('material', fil.material || fil.name || '') and fill('manufacturer',
vendor) and only dispatches synthetic input events, which leaves hidden ID
fields set to fallback values; instead, when the imported label does not resolve
to a local ID you must clear or invalidate the hidden ID field so it won't
silently persist a default. Update the logic around fill(...) and the input
dispatch for fieldMap.material and fieldMap.manufacturer to detect unresolved
labels (no exact-match mapping) and set the corresponding hidden ID element (the
element referenced by document.getElementById(fieldMap.material) /
document.getElementById(fieldMap.manufacturer)) to an empty/invalid value (or
add a data-invalid flag), then dispatch the 'input' event so downstream handlers
know the ID is cleared; keep the existing successful-path behavior unchanged.

Comment thread src/SharedJS.h
Comment on lines +467 to +479
// Temps
var extruder = fil.settings_extruder_temp;
var bed = fil.settings_bed_temp;
if (extruder) {
fill('nozzle_min', extruder.min || extruder);
fill('nozzle_max', extruder.max || extruder);
fill('nozzle_single', extruder.min || extruder);
}
if (bed) {
fill('bed_min', bed.min || bed);
fill('bed_max', bed.max || bed);
fill('bed_single', bed.min || bed);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Populate the single-temp fields from the primary value, not the lower bound.

When Spoolman returns {min,max}, nozzle_single and bed_single are filled from .min. OpenTag3D maps those to required print_temp_c / bed_temp_c, so a 190–220 spool currently imports 190 as the main print temperature.

🌡️ Safer range handling
+  function tempMin(value) {
+    return (value && typeof value === 'object' && value.min !== undefined && value.min !== null)
+      ? value.min
+      : value;
+  }
+
+  function tempMax(value) {
+    if (value && typeof value === 'object') {
+      if (value.max !== undefined && value.max !== null) return value.max;
+      if (value.min !== undefined && value.min !== null) return value.min;
+    }
+    return value;
+  }
+
   // Temps
   var extruder = fil.settings_extruder_temp;
   var bed = fil.settings_bed_temp;
   if (extruder) {
-    fill('nozzle_min', extruder.min || extruder);
-    fill('nozzle_max', extruder.max || extruder);
-    fill('nozzle_single', extruder.min || extruder);
+    fill('nozzle_min', tempMin(extruder));
+    fill('nozzle_max', tempMax(extruder));
+    fill('nozzle_single', tempMax(extruder));
   }
   if (bed) {
-    fill('bed_min', bed.min || bed);
-    fill('bed_max', bed.max || bed);
-    fill('bed_single', bed.min || bed);
+    fill('bed_min', tempMin(bed));
+    fill('bed_max', tempMax(bed));
+    fill('bed_single', tempMax(bed));
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Temps
var extruder = fil.settings_extruder_temp;
var bed = fil.settings_bed_temp;
if (extruder) {
fill('nozzle_min', extruder.min || extruder);
fill('nozzle_max', extruder.max || extruder);
fill('nozzle_single', extruder.min || extruder);
}
if (bed) {
fill('bed_min', bed.min || bed);
fill('bed_max', bed.max || bed);
fill('bed_single', bed.min || bed);
}
function tempMin(value) {
return (value && typeof value === 'object' && value.min !== undefined && value.min !== null)
? value.min
: value;
}
function tempMax(value) {
if (value && typeof value === 'object') {
if (value.max !== undefined && value.max !== null) return value.max;
if (value.min !== undefined && value.min !== null) return value.min;
}
return value;
}
// Temps
var extruder = fil.settings_extruder_temp;
var bed = fil.settings_bed_temp;
if (extruder) {
fill('nozzle_min', tempMin(extruder));
fill('nozzle_max', tempMax(extruder));
fill('nozzle_single', tempMax(extruder));
}
if (bed) {
fill('bed_min', tempMin(bed));
fill('bed_max', tempMax(bed));
fill('bed_single', tempMax(bed));
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/SharedJS.h` around lines 467 - 479, The single-temp fields
'nozzle_single' and 'bed_single' are being populated from the lower bound
(extruder.min / bed.min) which causes ranges like {min:190,max:220} to set the
main print temps to 190; change those to use the primary/high value by using
extruder.max || extruder for 'nozzle_single' and bed.max || bed for 'bed_single'
(keep the existing fill(...) calls and logic for 'nozzle_min', 'nozzle_max',
'bed_min', 'bed_max' unchanged).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant