Enhance library browser with symbol/footprint resolution and datasheet links

- Add library path resolution for symbols and footprints using KiCad library tables
- Parse sym-lib-table and fp-lib-table with multi-line format support
- Resolve environment variables (KICAD9_SYMBOL_DIR, UM_KICAD, etc.)
- Auto-select symbol and footprint when loading part from database
- Add Datasheet column to parts search results with clickable links
- Make IPN, Description, Class, Manufacturer, and MPN fields read-only
- Remove file extensions (.kicad_sym, .pretty) from library dropdown display
- Fix column access in search results using index-based retrieval

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
brentperteet
2026-02-27 12:44:10 -06:00
parent 0de59dabfc
commit 5869d2c693
2 changed files with 357 additions and 32 deletions

View File

@@ -411,20 +411,24 @@
</div>
</div>
<!-- Create Part View -->
<!-- Create/Edit Part View -->
<div id="libraryCreateView" style="display: none;">
<div style="margin: 20px 0; padding: 20px; background: #f8f9fa; border-radius: 8px;">
<h3>Create New Part</h3>
<h3 id="formTitle">Create New Part</h3>
<div id="createPartFormLoading" style="padding: 20px; text-align: center;">
<p>Loading missing IPNs from spreadsheet...</p>
</div>
<div id="createPartForm" style="display: none;">
<div style="margin-bottom: 15px;">
<div id="ipnDropdownContainer" style="margin-bottom: 15px;">
<label style="display: block; font-weight: bold; margin-bottom: 5px;">Select IPN:</label>
<select id="ipnDropdown" style="width: 100%; padding: 8px; border: 1px solid #dee2e6; border-radius: 5px; font-size: 14px;">
<option value="">-- Select an IPN --</option>
</select>
</div>
<div id="ipnDisplayContainer" style="margin-bottom: 15px; display: none;">
<label style="display: block; font-weight: bold; margin-bottom: 5px;">IPN:</label>
<input type="text" id="ipnDisplay" readonly style="width: 100%; padding: 8px; border: 1px solid #dee2e6; border-radius: 5px; font-size: 14px; background-color: #e9ecef;">
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; font-weight: bold; margin-bottom: 5px;">Description:</label>
<input type="text" id="descriptionInput" readonly style="width: 100%; padding: 8px; border: 1px solid #dee2e6; border-radius: 5px; font-size: 14px; background-color: #e9ecef;">
@@ -1003,18 +1007,26 @@
<th style="padding: 10px; text-align: left; border: 1px solid #dee2e6;">MPN</th>
<th style="padding: 10px; text-align: left; border: 1px solid #dee2e6;">Manufacturer</th>
<th style="padding: 10px; text-align: left; border: 1px solid #dee2e6;">Description</th>
<th style="padding: 10px; text-align: center; border: 1px solid #dee2e6;">Datasheet</th>
</tr>
</thead>
<tbody>
`;
for (const part of data.parts) {
const datasheetCell = part.datasheet
? `<a href="${part.datasheet}" target="_blank" style="color: #007bff; text-decoration: none;">Datasheet</a>`
: '';
html += `
<tr style="border-bottom: 1px solid #dee2e6;">
<td style="padding: 8px; border: 1px solid #dee2e6;">${part.ipn}</td>
<td style="padding: 8px; border: 1px solid #dee2e6;">
<a href="#" onclick="editPart('${part.ipn}'); return false;" style="color: #007bff; text-decoration: none;">${part.ipn}</a>
</td>
<td style="padding: 8px; border: 1px solid #dee2e6;">${part.mpn}</td>
<td style="padding: 8px; border: 1px solid #dee2e6;">${part.manufacturer}</td>
<td style="padding: 8px; border: 1px solid #dee2e6;">${part.description}</td>
<td style="padding: 8px; text-align: center; border: 1px solid #dee2e6;">${datasheetCell}</td>
</tr>
`;
}
@@ -1060,6 +1072,13 @@
let selectedFootprintLibraryPath = '';
let selectedSymbol = '';
let selectedFootprint = '';
let isEditMode = false;
let editingIpn = '';
const formTitle = document.getElementById('formTitle');
const ipnDropdownContainer = document.getElementById('ipnDropdownContainer');
const ipnDisplayContainer = document.getElementById('ipnDisplayContainer');
const ipnDisplay = document.getElementById('ipnDisplay');
function showCreatePartMessage(text, type) {
createPartMessage.textContent = text;
@@ -1067,8 +1086,39 @@
createPartMessage.style.display = 'block';
}
// Edit part function (called from search results)
window.editPart = function(ipn) {
isEditMode = true;
editingIpn = ipn;
formTitle.textContent = 'Edit Part';
ipnDropdownContainer.style.display = 'none';
ipnDisplayContainer.style.display = 'block';
ipnDisplay.value = ipn;
createPartBtn.textContent = 'Update Part';
libraryBrowserView.style.display = 'none';
libraryCreateView.style.display = 'block';
createPartFormLoading.style.display = 'block';
createPartForm.style.display = 'none';
createPartMessage.style.display = 'none';
// Load symbol and footprint libraries
socket.emit('get_symbol_libraries');
socket.emit('get_footprint_libraries');
// Request part details
socket.emit('get_part_details', { ipn: ipn });
};
// Show create part view
showCreatePartBtn.addEventListener('click', () => {
isEditMode = false;
editingIpn = '';
formTitle.textContent = 'Create New Part';
ipnDropdownContainer.style.display = 'block';
ipnDisplayContainer.style.display = 'none';
createPartBtn.textContent = 'Create Part';
libraryBrowserView.style.display = 'none';
libraryCreateView.style.display = 'block';
createPartFormLoading.style.display = 'block';
@@ -1275,24 +1325,41 @@
});
createPartBtn.addEventListener('click', () => {
const selectedIpn = ipnDropdown.value;
if (!selectedIpn) return;
if (isEditMode) {
// Edit mode - update existing part
createPartBtn.disabled = true;
showCreatePartMessage('Updating part in database...', 'info');
socket.emit('update_part', {
ipn: editingIpn,
description: descriptionInput.value.trim(),
class: classInput.value.trim(),
manufacturer: manufacturerInput.value.trim(),
mpn: mpnInput.value.trim(),
datasheet: datasheetInput.value.trim(),
symbol: selectedSymbol,
footprint: selectedFootprint
});
} else {
// Create mode - add new part
const selectedIpn = ipnDropdown.value;
if (!selectedIpn) return;
const part = missingPartsData.find(p => p.ipn === selectedIpn);
if (!part) return;
const part = missingPartsData.find(p => p.ipn === selectedIpn);
if (!part) return;
createPartBtn.disabled = true;
showCreatePartMessage('Creating part in database...', 'info');
socket.emit('create_part', {
ipn: part.ipn,
description: part.description,
class: part.class,
manufacturer: part.manufacturer,
mpn: part.mpn,
datasheet: datasheetInput.value.trim(),
symbol: selectedSymbol,
footprint: selectedFootprint
});
createPartBtn.disabled = true;
showCreatePartMessage('Creating part in database...', 'info');
socket.emit('create_part', {
ipn: part.ipn,
description: descriptionInput.value.trim(),
class: classInput.value.trim(),
manufacturer: manufacturerInput.value.trim(),
mpn: mpnInput.value.trim(),
datasheet: datasheetInput.value.trim(),
symbol: selectedSymbol,
footprint: selectedFootprint
});
}
});
socket.on('part_created', (data) => {
@@ -1334,6 +1401,71 @@
}
});
socket.on('part_updated', (data) => {
showCreatePartMessage(data.message, 'success');
createPartBtn.disabled = false;
// Return to browser view after delay
setTimeout(() => {
libraryCreateView.style.display = 'none';
libraryBrowserView.style.display = 'block';
}, 1500);
});
socket.on('part_details_result', (data) => {
createPartFormLoading.style.display = 'none';
createPartForm.style.display = 'block';
// Populate form with part details
descriptionInput.value = data.part.description || '';
classInput.value = data.part.class || '';
manufacturerInput.value = data.part.manufacturer || '';
mpnInput.value = data.part.mpn || '';
datasheetInput.value = data.part.datasheet || '';
// Use resolved symbol info from server
if (data.symbol_info) {
selectedSymbol = data.part.symbol;
const libPath = data.symbol_info.library_path;
const symbolName = data.symbol_info.symbol_name;
// Find and select the library by path
const lib = symbolLibraries.find(l => l.path === libPath);
if (lib) {
symbolLibrarySelect.value = lib.path;
selectedSymbolLibraryPath = lib.path;
socket.emit('get_symbols_in_library', { library_path: lib.path });
// Symbol will be selected when symbols_in_library_result is received
setTimeout(() => {
symbolSelect.value = symbolName;
socket.emit('render_symbol', { library_path: lib.path, symbol_name: symbolName });
}, 500);
}
}
// Use resolved footprint info from server
if (data.footprint_info) {
selectedFootprint = data.part.footprint;
const libPath = data.footprint_info.library_path;
const footprintName = data.footprint_info.footprint_name;
// Find and select the library by path
const lib = footprintLibraries.find(l => l.path === libPath);
if (lib) {
footprintLibrarySelect.value = lib.path;
selectedFootprintLibraryPath = lib.path;
socket.emit('get_footprints_in_library', { library_path: lib.path });
// Footprint will be selected when footprints_in_library_result is received
setTimeout(() => {
footprintSelect.value = footprintName;
socket.emit('render_footprint', { library_path: lib.path, footprint_name: footprintName });
}, 500);
}
}
createPartBtn.disabled = false;
});
// ========================================
// Build Pipeline
// ========================================