mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-03-02 13:35:25 +01:00
Compare commits
16 Commits
update-kic
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32a666f6c3 | ||
|
|
1ee998853f | ||
|
|
9ae585d2b7 | ||
|
|
8f92615491 | ||
|
|
e5dcfad3ff | ||
|
|
b7cc14fa14 | ||
|
|
b9d940ae33 | ||
|
|
46617f01a4 | ||
|
|
f097b79103 | ||
|
|
a8f9f9832e | ||
|
|
f3dab36bbe | ||
|
|
bebd603117 | ||
|
|
2660f4ee82 | ||
|
|
eb2bbdd633 | ||
|
|
24966230ea | ||
|
|
477cc1c0bb |
206
assets/commands/kicad_populate_default_mappings.json
Normal file
206
assets/commands/kicad_populate_default_mappings.json
Normal file
@@ -0,0 +1,206 @@
|
||||
{
|
||||
"_comment": "Default KiCad footprint/symbol mappings for partdb:kicad:populate command. Based on KiCad 9.x standard libraries. Use --mapping-file to override or extend these mappings.",
|
||||
"footprints": {
|
||||
"SOT-23": "Package_TO_SOT_SMD:SOT-23",
|
||||
"SOT-23-3": "Package_TO_SOT_SMD:SOT-23",
|
||||
"SOT-23-5": "Package_TO_SOT_SMD:SOT-23-5",
|
||||
"SOT-23-6": "Package_TO_SOT_SMD:SOT-23-6",
|
||||
"SOT-223": "Package_TO_SOT_SMD:SOT-223-3_TabPin2",
|
||||
"SOT-223-3": "Package_TO_SOT_SMD:SOT-223-3_TabPin2",
|
||||
"SOT-89": "Package_TO_SOT_SMD:SOT-89-3",
|
||||
"SOT-89-3": "Package_TO_SOT_SMD:SOT-89-3",
|
||||
"SOT-323": "Package_TO_SOT_SMD:SOT-323_SC-70",
|
||||
"SOT-363": "Package_TO_SOT_SMD:SOT-363_SC-70-6",
|
||||
"TSOT-25": "Package_TO_SOT_SMD:SOT-23-5",
|
||||
"SC-70-5": "Package_TO_SOT_SMD:SOT-353_SC-70-5",
|
||||
"SC-70-6": "Package_TO_SOT_SMD:SOT-363_SC-70-6",
|
||||
"TO-220": "Package_TO_SOT_THT:TO-220-3_Vertical",
|
||||
"TO-220AB": "Package_TO_SOT_THT:TO-220-3_Vertical",
|
||||
"TO-220AB-3": "Package_TO_SOT_THT:TO-220-3_Vertical",
|
||||
"TO-220FP": "Package_TO_SOT_THT:TO-220F-3_Vertical",
|
||||
"TO-247-3": "Package_TO_SOT_THT:TO-247-3_Vertical",
|
||||
"TO-92": "Package_TO_SOT_THT:TO-92_Inline",
|
||||
"TO-92-3": "Package_TO_SOT_THT:TO-92_Inline",
|
||||
"TO-252": "Package_TO_SOT_SMD:TO-252-2",
|
||||
"TO-252-2L": "Package_TO_SOT_SMD:TO-252-2",
|
||||
"TO-252-3L": "Package_TO_SOT_SMD:TO-252-3",
|
||||
"TO-263": "Package_TO_SOT_SMD:TO-263-2",
|
||||
"TO-263-2": "Package_TO_SOT_SMD:TO-263-2",
|
||||
"D2PAK": "Package_TO_SOT_SMD:TO-252-2",
|
||||
"DPAK": "Package_TO_SOT_SMD:TO-252-2",
|
||||
"SOIC-8": "Package_SO:SOIC-8_3.9x4.9mm_P1.27mm",
|
||||
"ESOP-8": "Package_SO:SOIC-8_3.9x4.9mm_P1.27mm",
|
||||
"SOIC-14": "Package_SO:SOIC-14_3.9x8.7mm_P1.27mm",
|
||||
"SOIC-16": "Package_SO:SOIC-16_3.9x9.9mm_P1.27mm",
|
||||
"TSSOP-8": "Package_SO:TSSOP-8_3x3mm_P0.65mm",
|
||||
"TSSOP-14": "Package_SO:TSSOP-14_4.4x5mm_P0.65mm",
|
||||
"TSSOP-16": "Package_SO:TSSOP-16_4.4x5mm_P0.65mm",
|
||||
"TSSOP-16L": "Package_SO:TSSOP-16_4.4x5mm_P0.65mm",
|
||||
"TSSOP-20": "Package_SO:TSSOP-20_4.4x6.5mm_P0.65mm",
|
||||
"MSOP-8": "Package_SO:MSOP-8_3x3mm_P0.65mm",
|
||||
"MSOP-10": "Package_SO:MSOP-10_3x3mm_P0.5mm",
|
||||
"MSOP-16": "Package_SO:MSOP-16_3x4mm_P0.5mm",
|
||||
"SO-5": "Package_TO_SOT_SMD:SOT-23-5",
|
||||
"DIP-4": "Package_DIP:DIP-4_W7.62mm",
|
||||
"DIP-6": "Package_DIP:DIP-6_W7.62mm",
|
||||
"DIP-8": "Package_DIP:DIP-8_W7.62mm",
|
||||
"DIP-14": "Package_DIP:DIP-14_W7.62mm",
|
||||
"DIP-16": "Package_DIP:DIP-16_W7.62mm",
|
||||
"DIP-18": "Package_DIP:DIP-18_W7.62mm",
|
||||
"DIP-20": "Package_DIP:DIP-20_W7.62mm",
|
||||
"DIP-24": "Package_DIP:DIP-24_W7.62mm",
|
||||
"DIP-28": "Package_DIP:DIP-28_W7.62mm",
|
||||
"DIP-40": "Package_DIP:DIP-40_W15.24mm",
|
||||
"QFN-8": "Package_DFN_QFN:QFN-8-1EP_3x3mm_P0.65mm_EP1.55x1.55mm",
|
||||
"QFN-12(3x3)": "Package_DFN_QFN:QFN-12-1EP_3x3mm_P0.5mm_EP1.65x1.65mm",
|
||||
"QFN-16": "Package_DFN_QFN:QFN-16-1EP_3x3mm_P0.5mm_EP1.45x1.45mm",
|
||||
"QFN-20": "Package_DFN_QFN:QFN-20-1EP_4x4mm_P0.5mm_EP2.5x2.5mm",
|
||||
"QFN-24": "Package_DFN_QFN:QFN-24-1EP_4x4mm_P0.5mm_EP2.45x2.45mm",
|
||||
"QFN-32": "Package_DFN_QFN:QFN-32-1EP_5x5mm_P0.5mm_EP3.45x3.45mm",
|
||||
"QFN-48": "Package_DFN_QFN:QFN-48-1EP_7x7mm_P0.5mm_EP5.3x5.3mm",
|
||||
"TQFP-32": "Package_QFP:TQFP-32_7x7mm_P0.8mm",
|
||||
"TQFP-44": "Package_QFP:TQFP-44_10x10mm_P0.8mm",
|
||||
"TQFP-48": "Package_QFP:TQFP-48_7x7mm_P0.5mm",
|
||||
"TQFP-48(7x7)": "Package_QFP:TQFP-48_7x7mm_P0.5mm",
|
||||
"TQFP-64": "Package_QFP:TQFP-64_10x10mm_P0.5mm",
|
||||
"TQFP-100": "Package_QFP:TQFP-100_14x14mm_P0.5mm",
|
||||
"LQFP-32": "Package_QFP:LQFP-32_7x7mm_P0.8mm",
|
||||
"LQFP-48": "Package_QFP:LQFP-48_7x7mm_P0.5mm",
|
||||
"LQFP-64": "Package_QFP:LQFP-64_10x10mm_P0.5mm",
|
||||
"LQFP-100": "Package_QFP:LQFP-100_14x14mm_P0.5mm",
|
||||
|
||||
"SOD-123": "Diode_SMD:D_SOD-123",
|
||||
"SOD-123F": "Diode_SMD:D_SOD-123F",
|
||||
"SOD-123FL": "Diode_SMD:D_SOD-123F",
|
||||
"SOD-323": "Diode_SMD:D_SOD-323",
|
||||
"SOD-523": "Diode_SMD:D_SOD-523",
|
||||
"SOD-882": "Diode_SMD:D_SOD-882",
|
||||
"SOD-882D": "Diode_SMD:D_SOD-882",
|
||||
"SMA(DO-214AC)": "Diode_SMD:D_SMA",
|
||||
"SMA": "Diode_SMD:D_SMA",
|
||||
"SMB": "Diode_SMD:D_SMB",
|
||||
"SMC": "Diode_SMD:D_SMC",
|
||||
|
||||
"DO-35": "Diode_THT:D_DO-35_SOD27_P7.62mm_Horizontal",
|
||||
"DO-35(DO-204AH)": "Diode_THT:D_DO-35_SOD27_P7.62mm_Horizontal",
|
||||
"DO-41": "Diode_THT:D_DO-41_SOD81_P10.16mm_Horizontal",
|
||||
"DO-201": "Diode_THT:D_DO-201_P15.24mm_Horizontal",
|
||||
|
||||
"DFN-2(0.6x1)": "Package_DFN_QFN:DFN-2-1EP_0.6x1.0mm_P0.65mm_EP0.2x0.55mm",
|
||||
"DFN1006-2": "Package_DFN_QFN:DFN-2_1.0x0.6mm",
|
||||
"DFN-6": "Package_DFN_QFN:DFN-6-1EP_2x2mm_P0.65mm_EP1x1.6mm",
|
||||
"DFN-8": "Package_DFN_QFN:DFN-8-1EP_3x2mm_P0.5mm_EP1.3x1.5mm",
|
||||
|
||||
"0201": "Resistor_SMD:R_0201_0603Metric",
|
||||
"0402": "Resistor_SMD:R_0402_1005Metric",
|
||||
"0603": "Resistor_SMD:R_0603_1608Metric",
|
||||
"0805": "Resistor_SMD:R_0805_2012Metric",
|
||||
"1206": "Resistor_SMD:R_1206_3216Metric",
|
||||
"1210": "Resistor_SMD:R_1210_3225Metric",
|
||||
"1812": "Resistor_SMD:R_1812_4532Metric",
|
||||
"2010": "Resistor_SMD:R_2010_5025Metric",
|
||||
"2512": "Resistor_SMD:R_2512_6332Metric",
|
||||
"2917": "Resistor_SMD:R_2917_7343Metric",
|
||||
"2920": "Resistor_SMD:R_2920_7350Metric",
|
||||
|
||||
"CASE-A-3216-18(mm)": "Capacitor_Tantalum_SMD:CP_EIA-3216-18_Kemet-A",
|
||||
"CASE-B-3528-21(mm)": "Capacitor_Tantalum_SMD:CP_EIA-3528-21_Kemet-B",
|
||||
"CASE-C-6032-28(mm)": "Capacitor_Tantalum_SMD:CP_EIA-6032-28_Kemet-C",
|
||||
"CASE-D-7343-31(mm)": "Capacitor_Tantalum_SMD:CP_EIA-7343-31_Kemet-D",
|
||||
"CASE-E-7343-43(mm)": "Capacitor_Tantalum_SMD:CP_EIA-7343-43_Kemet-E",
|
||||
|
||||
"SMD,D4xL5.4mm": "Capacitor_SMD:CP_Elec_4x5.4",
|
||||
"SMD,D5xL5.4mm": "Capacitor_SMD:CP_Elec_5x5.4",
|
||||
"SMD,D6.3xL5.4mm": "Capacitor_SMD:CP_Elec_6.3x5.4",
|
||||
"SMD,D6.3xL7.7mm": "Capacitor_SMD:CP_Elec_6.3x7.7",
|
||||
"SMD,D8xL6.5mm": "Capacitor_SMD:CP_Elec_8x6.5",
|
||||
"SMD,D8xL10mm": "Capacitor_SMD:CP_Elec_8x10",
|
||||
"SMD,D10xL10mm": "Capacitor_SMD:CP_Elec_10x10",
|
||||
"SMD,D10xL10.5mm": "Capacitor_SMD:CP_Elec_10x10.5",
|
||||
|
||||
"Through Hole,D5xL11mm": "Capacitor_THT:CP_Radial_D5.0mm_P2.00mm",
|
||||
"Through Hole,D6.3xL11mm": "Capacitor_THT:CP_Radial_D6.3mm_P2.50mm",
|
||||
"Through Hole,D8xL11mm": "Capacitor_THT:CP_Radial_D8.0mm_P3.50mm",
|
||||
"Through Hole,D10xL16mm": "Capacitor_THT:CP_Radial_D10.0mm_P5.00mm",
|
||||
"Through Hole,D10xL20mm": "Capacitor_THT:CP_Radial_D10.0mm_P5.00mm",
|
||||
"Through Hole,D12.5xL20mm": "Capacitor_THT:CP_Radial_D12.5mm_P5.00mm",
|
||||
|
||||
"LED 3mm": "LED_THT:LED_D3.0mm",
|
||||
"LED 5mm": "LED_THT:LED_D5.0mm",
|
||||
"LED 0603": "LED_SMD:LED_0603_1608Metric",
|
||||
"LED 0805": "LED_SMD:LED_0805_2012Metric",
|
||||
"SMD5050-4P": "LED_SMD:LED_WS2812B_PLCC4_5.0x5.0mm_P3.2mm",
|
||||
"SMD5050-6P": "LED_SMD:LED_WS2812B_PLCC4_5.0x5.0mm_P3.2mm",
|
||||
|
||||
"HC-49": "Crystal:Crystal_HC49-4H_Vertical",
|
||||
"HC-49/U": "Crystal:Crystal_HC49-4H_Vertical",
|
||||
"HC-49/S": "Crystal:Crystal_HC49-U_Vertical",
|
||||
"HC-49/US": "Crystal:Crystal_HC49-U_Vertical",
|
||||
|
||||
"USB-A": "Connector_USB:USB_A_Stewart_SS-52100-001_Horizontal",
|
||||
"USB-B": "Connector_USB:USB_B_OST_USB-B1HSxx_Horizontal",
|
||||
"USB-Mini-B": "Connector_USB:USB_Mini-B_Lumberg_2486_01_Horizontal",
|
||||
"USB-Micro-B": "Connector_USB:USB_Micro-B_Molex-105017-0001",
|
||||
"USB-C": "Connector_USB:USB_C_Receptacle_GCT_USB4085",
|
||||
|
||||
"1x2 P2.54mm": "Connector_PinHeader_2.54mm:PinHeader_1x02_P2.54mm_Vertical",
|
||||
"1x3 P2.54mm": "Connector_PinHeader_2.54mm:PinHeader_1x03_P2.54mm_Vertical",
|
||||
"1x4 P2.54mm": "Connector_PinHeader_2.54mm:PinHeader_1x04_P2.54mm_Vertical",
|
||||
"1x5 P2.54mm": "Connector_PinHeader_2.54mm:PinHeader_1x05_P2.54mm_Vertical",
|
||||
"1x6 P2.54mm": "Connector_PinHeader_2.54mm:PinHeader_1x06_P2.54mm_Vertical",
|
||||
"1x8 P2.54mm": "Connector_PinHeader_2.54mm:PinHeader_1x08_P2.54mm_Vertical",
|
||||
"1x10 P2.54mm": "Connector_PinHeader_2.54mm:PinHeader_1x10_P2.54mm_Vertical",
|
||||
"2x2 P2.54mm": "Connector_PinHeader_2.54mm:PinHeader_2x02_P2.54mm_Vertical",
|
||||
"2x3 P2.54mm": "Connector_PinHeader_2.54mm:PinHeader_2x03_P2.54mm_Vertical",
|
||||
"2x4 P2.54mm": "Connector_PinHeader_2.54mm:PinHeader_2x04_P2.54mm_Vertical",
|
||||
"2x5 P2.54mm": "Connector_PinHeader_2.54mm:PinHeader_2x05_P2.54mm_Vertical",
|
||||
"2x10 P2.54mm": "Connector_PinHeader_2.54mm:PinHeader_2x10_P2.54mm_Vertical",
|
||||
"2x20 P2.54mm": "Connector_PinHeader_2.54mm:PinHeader_2x20_P2.54mm_Vertical",
|
||||
"SIP-3-2.54mm": "Package_SIP:SIP-3_P2.54mm",
|
||||
"SIP-4-2.54mm": "Package_SIP:SIP-4_P2.54mm",
|
||||
"SIP-5-2.54mm": "Package_SIP:SIP-5_P2.54mm"
|
||||
},
|
||||
"categories": {
|
||||
"Electrolytic": "Device:C_Polarized",
|
||||
"Polarized": "Device:C_Polarized",
|
||||
"Tantalum": "Device:C_Polarized",
|
||||
"Zener": "Device:D_Zener",
|
||||
"Schottky": "Device:D_Schottky",
|
||||
"TVS": "Device:D_TVS",
|
||||
"LED": "Device:LED",
|
||||
"NPN": "Device:Q_NPN_BCE",
|
||||
"PNP": "Device:Q_PNP_BCE",
|
||||
"N-MOSFET": "Device:Q_NMOS_GDS",
|
||||
"NMOS": "Device:Q_NMOS_GDS",
|
||||
"N-MOS": "Device:Q_NMOS_GDS",
|
||||
"P-MOSFET": "Device:Q_PMOS_GDS",
|
||||
"PMOS": "Device:Q_PMOS_GDS",
|
||||
"P-MOS": "Device:Q_PMOS_GDS",
|
||||
"MOSFET": "Device:Q_NMOS_GDS",
|
||||
"JFET": "Device:Q_NJFET_DSG",
|
||||
"Ferrite": "Device:Ferrite_Bead",
|
||||
"Crystal": "Device:Crystal",
|
||||
"Oscillator": "Oscillator:Oscillator_Crystal",
|
||||
"Fuse": "Device:Fuse",
|
||||
"Transformer": "Device:Transformer_1P_1S",
|
||||
"Resistor": "Device:R",
|
||||
"Capacitor": "Device:C",
|
||||
"Inductor": "Device:L",
|
||||
"Diode": "Device:D",
|
||||
"Transistor": "Device:Q_NPN_BCE",
|
||||
"Voltage Regulator": "Regulator_Linear:LM317_TO-220",
|
||||
"LDO": "Regulator_Linear:AMS1117-3.3",
|
||||
"Op-Amp": "Amplifier_Operational:LM358",
|
||||
"Comparator": "Comparator:LM393",
|
||||
"Optocoupler": "Isolator:PC817",
|
||||
"Relay": "Relay:Relay_DPDT",
|
||||
"Connector": "Connector:Conn_01x02",
|
||||
"Switch": "Switch:SW_Push",
|
||||
"Button": "Switch:SW_Push",
|
||||
"Potentiometer": "Device:R_POT",
|
||||
"Trimpot": "Device:R_POT",
|
||||
"Thermistor": "Device:Thermistor",
|
||||
"Varistor": "Device:Varistor",
|
||||
"Photo": "Device:LED"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Controller} from "@hotwired/stimulus";
|
||||
|
||||
/**
|
||||
* Purpose of this controller is to allow users to input non-printable characters like EOT, FS, etc. in a form field and submit them correctly with the form.
|
||||
* The visible input field encodes non-printable characters via their Unicode Control picture representation, e.g. \n becomes ␊ and \t becomes ␉, so that they can be displayed in the input field without breaking the form submission.
|
||||
* The actual value of the field, which is submitted with the form, is stored in a hidden input and contains the non-printable characters in their original form.
|
||||
*/
|
||||
export default class extends Controller {
|
||||
|
||||
_hiddenInput;
|
||||
|
||||
connect() {
|
||||
this.element.addEventListener("input", this._update.bind(this));
|
||||
|
||||
// We use a hidden input to store the actual value of the field, which is submitted with the form.
|
||||
// The visible input is just for user interaction and can contain non-printable characters, which are not allowed in the hidden input.
|
||||
this._hiddenInput = document.createElement("input");
|
||||
this._hiddenInput.type = "hidden";
|
||||
this._hiddenInput.name = this.element.name;
|
||||
this.element.removeAttribute("name");
|
||||
this.element.parentNode.insertBefore(this._hiddenInput, this.element.nextSibling);
|
||||
|
||||
this.element.addEventListener("keypress", this._onKeyPress.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that non-printable characters like EOT, FS, etc. gets added to the input value when the user types them
|
||||
* @param event
|
||||
* @private
|
||||
*/
|
||||
_onKeyPress(event) {
|
||||
const ALLOWED_INPUT_CODES = [4, 28, 29, 30, 31]; //EOT, FS, GS, RS, US
|
||||
|
||||
if (!ALLOWED_INPUT_CODES.includes(event.keyCode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const char = String.fromCharCode(event.keyCode);
|
||||
this.element.value += char;
|
||||
|
||||
this._update();
|
||||
|
||||
|
||||
}
|
||||
|
||||
_update() {
|
||||
//Chrome workaround: Remove a leading ∠ character (U+2220) that appears when the user types a non-printable character at the beginning of the input field.
|
||||
if (this.element.value.startsWith("∠")) {
|
||||
this.element.value = this.element.value.substring(1);
|
||||
}
|
||||
|
||||
// Remove non-printable characters from the input value and store them in the hidden input
|
||||
const normalizedValue = this.decodeNonPrintableChars(this.element.value);
|
||||
this._hiddenInput.value = normalizedValue;
|
||||
|
||||
// Encode non-printable characters in the visible input to their Unicode Control picture representation
|
||||
const encodedValue = this.encodeNonPrintableChars(normalizedValue);
|
||||
if (encodedValue !== this.element.value) {
|
||||
this.element.value = encodedValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes non-printable characters in the given string via their Unicode Control picture representation, e.g. \n becomes ␊ and \t becomes ␉.
|
||||
* This allows us to display non-printable characters in the input field without breaking the form submission.
|
||||
* @param str
|
||||
*/
|
||||
encodeNonPrintableChars(str) {
|
||||
return str.replace(/[\x00-\x1F\x7F]/g, (char) => {
|
||||
const code = char.charCodeAt(0);
|
||||
return String.fromCharCode(0x2400 + code);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes the Unicode Control picture representation of non-printable characters back to their original form, e.g. ␊ becomes \n and ␉ becomes \t.
|
||||
* @param str
|
||||
*/
|
||||
decodeNonPrintableChars(str) {
|
||||
return str.replace(/[\u2400-\u241F\u2421]/g, (char) => {
|
||||
const code = char.charCodeAt(0) - 0x2400;
|
||||
return String.fromCharCode(code);
|
||||
});
|
||||
}
|
||||
}
|
||||
136
assets/controllers/helpers/scan_special_char_controller.js
Normal file
136
assets/controllers/helpers/scan_special_char_controller.js
Normal file
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
/**
|
||||
* This controller listens for a special non-printable character (SOH / ASCII 1) to be entered anywhere on the page,
|
||||
* which is then used as a trigger to submit the following characters as a barcode / scan input.
|
||||
*/
|
||||
export default class extends Controller {
|
||||
connect() {
|
||||
// Optional: Log to confirm global attachment
|
||||
console.log("Scanner listener active")
|
||||
|
||||
this.isCapturing = false
|
||||
this.buffer = ""
|
||||
|
||||
window.addEventListener("keypress", this.handleKeydown.bind(this))
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.isCapturing = false
|
||||
this.buffer = ""
|
||||
this.timeoutId = null
|
||||
}
|
||||
|
||||
handleKeydown(event) {
|
||||
|
||||
// Ignore if the user is typing in a form field
|
||||
const isInput = ["INPUT", "TEXTAREA", "SELECT"].includes(event.target.tagName) ||
|
||||
event.target.isContentEditable;
|
||||
if (isInput) return
|
||||
|
||||
// 1. Detect Start of Header (SOH / Ctrl+A)
|
||||
if (event.key === "\x01" || event.keyCode === 1) {
|
||||
this.startCapturing(event)
|
||||
return
|
||||
}
|
||||
|
||||
// 2. Process characters if in capture mode
|
||||
if (this.isCapturing) {
|
||||
this.resetTimeout() // Push the expiration back with every keypress
|
||||
|
||||
if (event.key === "Enter" || event.keyCode === 13) {
|
||||
|
||||
this.finishCapturing(event)
|
||||
} else if (event.key.length === 1) {
|
||||
this.buffer += event.key
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
startCapturing(event) {
|
||||
this.isCapturing = true
|
||||
this.buffer = ""
|
||||
this.resetTimeout()
|
||||
event.preventDefault()
|
||||
console.debug("Scan character detected. Capture started...")
|
||||
}
|
||||
|
||||
finishCapturing(event) {
|
||||
event.preventDefault()
|
||||
const data = this.buffer;
|
||||
this.stopCapturing()
|
||||
this.processCapture(data)
|
||||
}
|
||||
|
||||
stopCapturing() {
|
||||
this.isCapturing = false
|
||||
this.buffer = ""
|
||||
if (this.timeoutId) clearTimeout(this.timeoutId)
|
||||
console.debug("Capture cleared/finished.")
|
||||
}
|
||||
|
||||
resetTimeout() {
|
||||
if (this.timeoutId) clearTimeout(this.timeoutId)
|
||||
|
||||
this.timeoutId = setTimeout(() => {
|
||||
if (this.isCapturing) {
|
||||
console.warn("Capture timed out. Resetting buffer.")
|
||||
this.stopCapturing()
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
|
||||
processCapture(data) {
|
||||
if (!data) return
|
||||
|
||||
console.debug("Captured scan data: " + data)
|
||||
|
||||
const scanInput = document.getElementById("scan_dialog_input");
|
||||
if (scanInput) { //When we are on the scan dialog page, submit the form there
|
||||
this._submitScanForm(data);
|
||||
} else { //Otherwise use our own form (e.g. on the part list page)
|
||||
this.element.querySelector("input[name='input']").value = data;
|
||||
this.element.requestSubmit();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
_submitScanForm(data) {
|
||||
const scanInput = document.getElementById("scan_dialog_input");
|
||||
if (!scanInput) {
|
||||
console.error("Scan input field not found!")
|
||||
return;
|
||||
}
|
||||
|
||||
scanInput.value = data;
|
||||
scanInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
|
||||
const form = document.getElementById("scan_dialog_form");
|
||||
if (!form) {
|
||||
console.error("Scan form not found!")
|
||||
return;
|
||||
}
|
||||
|
||||
form.requestSubmit();
|
||||
}
|
||||
}
|
||||
@@ -114,7 +114,11 @@ export default class extends Controller {
|
||||
// Mark as handled immediately (prevents spam even if callback fires repeatedly)
|
||||
this._lastDecodedText = normalized;
|
||||
|
||||
document.getElementById('scan_dialog_input').value = decodedText;
|
||||
const input = document.getElementById('scan_dialog_input');
|
||||
input.value = decodedText;
|
||||
//Trigger nonprintable char input controller to update the hidden input value
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
|
||||
//Submit form
|
||||
document.getElementById('scan_dialog_form').requestSubmit();
|
||||
}
|
||||
|
||||
740
composer.lock
generated
740
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -208,29 +208,29 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* initial_marking?: list<scalar|Param|null>,
|
||||
* events_to_dispatch?: list<string|Param>|null,
|
||||
* places?: list<array{ // Default: []
|
||||
* name: scalar|Param|null,
|
||||
* metadata?: list<mixed>,
|
||||
* name?: scalar|Param|null,
|
||||
* metadata?: array<string, mixed>,
|
||||
* }>,
|
||||
* transitions: list<array{ // Default: []
|
||||
* name: string|Param,
|
||||
* transitions?: list<array{ // Default: []
|
||||
* name?: string|Param,
|
||||
* guard?: string|Param, // An expression to block the transition.
|
||||
* from?: list<array{ // Default: []
|
||||
* place: string|Param,
|
||||
* place?: string|Param,
|
||||
* weight?: int|Param, // Default: 1
|
||||
* }>,
|
||||
* to?: list<array{ // Default: []
|
||||
* place: string|Param,
|
||||
* place?: string|Param,
|
||||
* weight?: int|Param, // Default: 1
|
||||
* }>,
|
||||
* weight?: int|Param, // Default: 1
|
||||
* metadata?: list<mixed>,
|
||||
* metadata?: array<string, mixed>,
|
||||
* }>,
|
||||
* metadata?: list<mixed>,
|
||||
* metadata?: array<string, mixed>,
|
||||
* }>,
|
||||
* },
|
||||
* router?: bool|array{ // Router configuration
|
||||
* enabled?: bool|Param, // Default: false
|
||||
* resource: scalar|Param|null,
|
||||
* resource?: scalar|Param|null,
|
||||
* type?: scalar|Param|null,
|
||||
* cache_dir?: scalar|Param|null, // Deprecated: Setting the "framework.router.cache_dir.cache_dir" configuration option is deprecated. It will be removed in version 8.0. // Default: "%kernel.build_dir%"
|
||||
* default_uri?: scalar|Param|null, // The default URI used to generate URLs in a non-HTTP context. // Default: null
|
||||
@@ -360,10 +360,10 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* mapping?: array{
|
||||
* paths?: list<scalar|Param|null>,
|
||||
* },
|
||||
* default_context?: list<mixed>,
|
||||
* default_context?: array<string, mixed>,
|
||||
* named_serializers?: array<string, array{ // Default: []
|
||||
* name_converter?: scalar|Param|null,
|
||||
* default_context?: list<mixed>,
|
||||
* default_context?: array<string, mixed>,
|
||||
* include_built_in_normalizers?: bool|Param, // Whether to include the built-in normalizers // Default: true
|
||||
* include_built_in_encoders?: bool|Param, // Whether to include the built-in encoders // Default: true
|
||||
* }>,
|
||||
@@ -427,7 +427,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* },
|
||||
* messenger?: bool|array{ // Messenger configuration
|
||||
* enabled?: bool|Param, // Default: false
|
||||
* routing?: array<string, array{ // Default: []
|
||||
* routing?: array<string, string|array{ // Default: []
|
||||
* senders?: list<scalar|Param|null>,
|
||||
* }>,
|
||||
* serializer?: array{
|
||||
@@ -440,7 +440,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* transports?: array<string, string|array{ // Default: []
|
||||
* dsn?: scalar|Param|null,
|
||||
* serializer?: scalar|Param|null, // Service id of a custom serializer to use. // Default: null
|
||||
* options?: list<mixed>,
|
||||
* options?: array<string, mixed>,
|
||||
* failure_transport?: scalar|Param|null, // Transport name to send failed messages to (after all retries have failed). // Default: null
|
||||
* retry_strategy?: string|array{
|
||||
* service?: scalar|Param|null, // Service id to override the retry strategy entirely. // Default: null
|
||||
@@ -462,7 +462,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* allow_no_senders?: bool|Param, // Default: true
|
||||
* },
|
||||
* middleware?: list<string|array{ // Default: []
|
||||
* id: scalar|Param|null,
|
||||
* id?: scalar|Param|null,
|
||||
* arguments?: list<mixed>,
|
||||
* }>,
|
||||
* }>,
|
||||
@@ -634,7 +634,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* lock_factory?: scalar|Param|null, // The service ID of the lock factory used by this limiter (or null to disable locking). // Default: "auto"
|
||||
* cache_pool?: scalar|Param|null, // The cache pool to use for storing the current limiter state. // Default: "cache.rate_limiter"
|
||||
* storage_service?: scalar|Param|null, // The service ID of a custom storage implementation, this precedes any configured "cache_pool". // Default: null
|
||||
* policy: "fixed_window"|"token_bucket"|"sliding_window"|"compound"|"no_limit"|Param, // The algorithm to be used by this limiter.
|
||||
* policy?: "fixed_window"|"token_bucket"|"sliding_window"|"compound"|"no_limit"|Param, // The algorithm to be used by this limiter.
|
||||
* limiters?: list<scalar|Param|null>,
|
||||
* limit?: int|Param, // The maximum allowed hits in a fixed interval or burst.
|
||||
* interval?: scalar|Param|null, // Configures the fixed interval if "policy" is set to "fixed_window" or "sliding_window". The value must be a number followed by "second", "minute", "hour", "day", "week" or "month" (or their plural equivalent).
|
||||
@@ -679,7 +679,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* enabled?: bool|Param, // Default: false
|
||||
* message_bus?: scalar|Param|null, // The message bus to use. // Default: "messenger.default_bus"
|
||||
* routing?: array<string, array{ // Default: []
|
||||
* service: scalar|Param|null,
|
||||
* service?: scalar|Param|null,
|
||||
* secret?: scalar|Param|null, // Default: ""
|
||||
* }>,
|
||||
* },
|
||||
@@ -694,7 +694,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* dbal?: array{
|
||||
* default_connection?: scalar|Param|null,
|
||||
* types?: array<string, string|array{ // Default: []
|
||||
* class: scalar|Param|null,
|
||||
* class?: scalar|Param|null,
|
||||
* commented?: bool|Param, // Deprecated: The doctrine-bundle type commenting features were removed; the corresponding config parameter was deprecated in 2.0 and will be dropped in 3.0.
|
||||
* }>,
|
||||
* driver_schemes?: array<string, scalar|Param|null>,
|
||||
@@ -910,7 +910,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* datetime_functions?: array<string, scalar|Param|null>,
|
||||
* },
|
||||
* filters?: array<string, string|array{ // Default: []
|
||||
* class: scalar|Param|null,
|
||||
* class?: scalar|Param|null,
|
||||
* enabled?: bool|Param, // Default: false
|
||||
* parameters?: array<string, mixed>,
|
||||
* }>,
|
||||
@@ -975,7 +975,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* providers?: list<scalar|Param|null>,
|
||||
* },
|
||||
* entity?: array{
|
||||
* class: scalar|Param|null, // The full entity class name of your user class.
|
||||
* class?: scalar|Param|null, // The full entity class name of your user class.
|
||||
* property?: scalar|Param|null, // Default: null
|
||||
* manager_name?: scalar|Param|null, // Default: null
|
||||
* },
|
||||
@@ -986,8 +986,8 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* }>,
|
||||
* },
|
||||
* ldap?: array{
|
||||
* service: scalar|Param|null,
|
||||
* base_dn: scalar|Param|null,
|
||||
* service?: scalar|Param|null,
|
||||
* base_dn?: scalar|Param|null,
|
||||
* search_dn?: scalar|Param|null, // Default: null
|
||||
* search_password?: scalar|Param|null, // Default: null
|
||||
* extra_fields?: list<scalar|Param|null>,
|
||||
@@ -998,11 +998,11 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* password_attribute?: scalar|Param|null, // Default: null
|
||||
* },
|
||||
* saml?: array{
|
||||
* user_class: scalar|Param|null,
|
||||
* user_class?: scalar|Param|null,
|
||||
* default_roles?: list<scalar|Param|null>,
|
||||
* },
|
||||
* }>,
|
||||
* firewalls: array<string, array{ // Default: []
|
||||
* firewalls?: array<string, array{ // Default: []
|
||||
* pattern?: scalar|Param|null,
|
||||
* host?: scalar|Param|null,
|
||||
* methods?: list<scalar|Param|null>,
|
||||
@@ -1136,9 +1136,9 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* failure_path_parameter?: scalar|Param|null, // Default: "_failure_path"
|
||||
* },
|
||||
* login_link?: array{
|
||||
* check_route: scalar|Param|null, // Route that will validate the login link - e.g. "app_login_link_verify".
|
||||
* check_route?: scalar|Param|null, // Route that will validate the login link - e.g. "app_login_link_verify".
|
||||
* check_post_only?: scalar|Param|null, // If true, only HTTP POST requests to "check_route" will be handled by the authenticator. // Default: false
|
||||
* signature_properties: list<scalar|Param|null>,
|
||||
* signature_properties?: list<scalar|Param|null>,
|
||||
* lifetime?: int|Param, // The lifetime of the login link in seconds. // Default: 600
|
||||
* max_uses?: int|Param, // Max number of times a login link can be used - null means unlimited within lifetime. // Default: null
|
||||
* used_link_cache?: scalar|Param|null, // Cache service id used to expired links of max_uses is set.
|
||||
@@ -1240,13 +1240,13 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* failure_handler?: scalar|Param|null,
|
||||
* realm?: scalar|Param|null, // Default: null
|
||||
* token_extractors?: list<scalar|Param|null>,
|
||||
* token_handler: string|array{
|
||||
* token_handler?: string|array{
|
||||
* id?: scalar|Param|null,
|
||||
* oidc_user_info?: string|array{
|
||||
* base_uri: scalar|Param|null, // Base URI of the userinfo endpoint on the OIDC server, or the OIDC server URI to use the discovery (require "discovery" to be configured).
|
||||
* base_uri?: scalar|Param|null, // Base URI of the userinfo endpoint on the OIDC server, or the OIDC server URI to use the discovery (require "discovery" to be configured).
|
||||
* discovery?: array{ // Enable the OIDC discovery.
|
||||
* cache?: array{
|
||||
* id: scalar|Param|null, // Cache service id to use to cache the OIDC discovery configuration.
|
||||
* id?: scalar|Param|null, // Cache service id to use to cache the OIDC discovery configuration.
|
||||
* },
|
||||
* },
|
||||
* claim?: scalar|Param|null, // Claim which contains the user identifier (e.g. sub, email, etc.). // Default: "sub"
|
||||
@@ -1254,27 +1254,27 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* },
|
||||
* oidc?: array{
|
||||
* discovery?: array{ // Enable the OIDC discovery.
|
||||
* base_uri: list<scalar|Param|null>,
|
||||
* base_uri?: list<scalar|Param|null>,
|
||||
* cache?: array{
|
||||
* id: scalar|Param|null, // Cache service id to use to cache the OIDC discovery configuration.
|
||||
* id?: scalar|Param|null, // Cache service id to use to cache the OIDC discovery configuration.
|
||||
* },
|
||||
* },
|
||||
* claim?: scalar|Param|null, // Claim which contains the user identifier (e.g.: sub, email..). // Default: "sub"
|
||||
* audience: scalar|Param|null, // Audience set in the token, for validation purpose.
|
||||
* issuers: list<scalar|Param|null>,
|
||||
* audience?: scalar|Param|null, // Audience set in the token, for validation purpose.
|
||||
* issuers?: list<scalar|Param|null>,
|
||||
* algorithm?: array<mixed>,
|
||||
* algorithms: list<scalar|Param|null>,
|
||||
* algorithms?: list<scalar|Param|null>,
|
||||
* key?: scalar|Param|null, // Deprecated: The "key" option is deprecated and will be removed in 8.0. Use the "keyset" option instead. // JSON-encoded JWK used to sign the token (must contain a "kty" key).
|
||||
* keyset?: scalar|Param|null, // JSON-encoded JWKSet used to sign the token (must contain a list of valid public keys).
|
||||
* encryption?: bool|array{
|
||||
* enabled?: bool|Param, // Default: false
|
||||
* enforce?: bool|Param, // When enabled, the token shall be encrypted. // Default: false
|
||||
* algorithms: list<scalar|Param|null>,
|
||||
* keyset: scalar|Param|null, // JSON-encoded JWKSet used to decrypt the token (must contain a list of valid private keys).
|
||||
* algorithms?: list<scalar|Param|null>,
|
||||
* keyset?: scalar|Param|null, // JSON-encoded JWKSet used to decrypt the token (must contain a list of valid private keys).
|
||||
* },
|
||||
* },
|
||||
* cas?: array{
|
||||
* validation_url: scalar|Param|null, // CAS server validation URL
|
||||
* validation_url?: scalar|Param|null, // CAS server validation URL
|
||||
* prefix?: scalar|Param|null, // CAS prefix // Default: "cas"
|
||||
* http_client?: scalar|Param|null, // HTTP Client service // Default: null
|
||||
* },
|
||||
@@ -1379,7 +1379,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* use_microseconds?: scalar|Param|null, // Default: true
|
||||
* channels?: list<scalar|Param|null>,
|
||||
* handlers?: array<string, array{ // Default: []
|
||||
* type: scalar|Param|null,
|
||||
* type?: scalar|Param|null,
|
||||
* id?: scalar|Param|null,
|
||||
* enabled?: bool|Param, // Default: true
|
||||
* priority?: scalar|Param|null, // Default: 0
|
||||
@@ -1502,7 +1502,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* headers?: list<scalar|Param|null>,
|
||||
* mailer?: scalar|Param|null, // Default: null
|
||||
* email_prototype?: string|array{
|
||||
* id: scalar|Param|null,
|
||||
* id?: scalar|Param|null,
|
||||
* method?: scalar|Param|null, // Default: null
|
||||
* },
|
||||
* verbosity_levels?: array{
|
||||
@@ -1531,7 +1531,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* generate_final_entities?: bool|Param, // Default: false
|
||||
* }
|
||||
* @psalm-type WebpackEncoreConfig = array{
|
||||
* output_path: scalar|Param|null, // The path where Encore is building the assets - i.e. Encore.setOutputPath()
|
||||
* output_path?: scalar|Param|null, // The path where Encore is building the assets - i.e. Encore.setOutputPath()
|
||||
* crossorigin?: false|"anonymous"|"use-credentials"|Param, // crossorigin value when Encore.enableIntegrityHashes() is used, can be false (default), anonymous or use-credentials // Default: false
|
||||
* preload?: bool|Param, // preload all rendered script and link tags automatically via the http2 Link header. // Default: false
|
||||
* cache?: bool|Param, // Enable caching of the entry point file(s) // Default: false
|
||||
@@ -1561,27 +1561,27 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* cache_prefix?: scalar|Param|null, // Default: "media/cache"
|
||||
* },
|
||||
* aws_s3?: array{
|
||||
* bucket: scalar|Param|null,
|
||||
* bucket?: scalar|Param|null,
|
||||
* cache?: scalar|Param|null, // Default: false
|
||||
* use_psr_cache?: bool|Param, // Default: false
|
||||
* acl?: scalar|Param|null, // Default: "public-read"
|
||||
* cache_prefix?: scalar|Param|null, // Default: ""
|
||||
* client_id?: scalar|Param|null, // Default: null
|
||||
* client_config: list<mixed>,
|
||||
* client_config?: list<mixed>,
|
||||
* get_options?: array<string, scalar|Param|null>,
|
||||
* put_options?: array<string, scalar|Param|null>,
|
||||
* proxies?: array<string, scalar|Param|null>,
|
||||
* },
|
||||
* flysystem?: array{
|
||||
* filesystem_service: scalar|Param|null,
|
||||
* filesystem_service?: scalar|Param|null,
|
||||
* cache_prefix?: scalar|Param|null, // Default: ""
|
||||
* root_url: scalar|Param|null,
|
||||
* root_url?: scalar|Param|null,
|
||||
* visibility?: "public"|"private"|"noPredefinedVisibility"|Param, // Default: "public"
|
||||
* },
|
||||
* }>,
|
||||
* loaders?: array<string, array{ // Default: []
|
||||
* stream?: array{
|
||||
* wrapper: scalar|Param|null,
|
||||
* wrapper?: scalar|Param|null,
|
||||
* context?: scalar|Param|null, // Default: null
|
||||
* },
|
||||
* filesystem?: array{
|
||||
@@ -1595,11 +1595,11 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* },
|
||||
* },
|
||||
* flysystem?: array{
|
||||
* filesystem_service: scalar|Param|null,
|
||||
* filesystem_service?: scalar|Param|null,
|
||||
* },
|
||||
* asset_mapper?: array<mixed>,
|
||||
* chain?: array{
|
||||
* loaders: list<scalar|Param|null>,
|
||||
* loaders?: list<scalar|Param|null>,
|
||||
* },
|
||||
* }>,
|
||||
* driver?: scalar|Param|null, // Default: "gd"
|
||||
@@ -1746,23 +1746,23 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* providers?: array{
|
||||
* apilayer_fixer?: array{
|
||||
* priority?: int|Param, // Default: 0
|
||||
* api_key: scalar|Param|null,
|
||||
* api_key?: scalar|Param|null,
|
||||
* },
|
||||
* apilayer_currency_data?: array{
|
||||
* priority?: int|Param, // Default: 0
|
||||
* api_key: scalar|Param|null,
|
||||
* api_key?: scalar|Param|null,
|
||||
* },
|
||||
* apilayer_exchange_rates_data?: array{
|
||||
* priority?: int|Param, // Default: 0
|
||||
* api_key: scalar|Param|null,
|
||||
* api_key?: scalar|Param|null,
|
||||
* },
|
||||
* abstract_api?: array{
|
||||
* priority?: int|Param, // Default: 0
|
||||
* api_key: scalar|Param|null,
|
||||
* api_key?: scalar|Param|null,
|
||||
* },
|
||||
* fixer?: array{
|
||||
* priority?: int|Param, // Default: 0
|
||||
* access_key: scalar|Param|null,
|
||||
* access_key?: scalar|Param|null,
|
||||
* enterprise?: bool|Param, // Default: false
|
||||
* },
|
||||
* cryptonator?: array{
|
||||
@@ -1770,7 +1770,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* },
|
||||
* exchange_rates_api?: array{
|
||||
* priority?: int|Param, // Default: 0
|
||||
* access_key: scalar|Param|null,
|
||||
* access_key?: scalar|Param|null,
|
||||
* enterprise?: bool|Param, // Default: false
|
||||
* },
|
||||
* webservicex?: array{
|
||||
@@ -1805,38 +1805,38 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* },
|
||||
* currency_data_feed?: array{
|
||||
* priority?: int|Param, // Default: 0
|
||||
* api_key: scalar|Param|null,
|
||||
* api_key?: scalar|Param|null,
|
||||
* },
|
||||
* currency_layer?: array{
|
||||
* priority?: int|Param, // Default: 0
|
||||
* access_key: scalar|Param|null,
|
||||
* access_key?: scalar|Param|null,
|
||||
* enterprise?: bool|Param, // Default: false
|
||||
* },
|
||||
* forge?: array{
|
||||
* priority?: int|Param, // Default: 0
|
||||
* api_key: scalar|Param|null,
|
||||
* api_key?: scalar|Param|null,
|
||||
* },
|
||||
* open_exchange_rates?: array{
|
||||
* priority?: int|Param, // Default: 0
|
||||
* app_id: scalar|Param|null,
|
||||
* app_id?: scalar|Param|null,
|
||||
* enterprise?: bool|Param, // Default: false
|
||||
* },
|
||||
* xignite?: array{
|
||||
* priority?: int|Param, // Default: 0
|
||||
* token: scalar|Param|null,
|
||||
* token?: scalar|Param|null,
|
||||
* },
|
||||
* xchangeapi?: array{
|
||||
* priority?: int|Param, // Default: 0
|
||||
* api_key: scalar|Param|null,
|
||||
* api_key?: scalar|Param|null,
|
||||
* },
|
||||
* currency_converter?: array{
|
||||
* priority?: int|Param, // Default: 0
|
||||
* access_key: scalar|Param|null,
|
||||
* access_key?: scalar|Param|null,
|
||||
* enterprise?: bool|Param, // Default: false
|
||||
* },
|
||||
* array?: array{
|
||||
* priority?: int|Param, // Default: 0
|
||||
* latestRates: mixed,
|
||||
* latestRates?: mixed,
|
||||
* historicalRates?: mixed,
|
||||
* },
|
||||
* },
|
||||
@@ -2098,9 +2098,9 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* counter_checker?: scalar|Param|null, // This service will check if the counter is valid. By default it throws an exception (recommended). // Default: "Webauthn\\Counter\\ThrowExceptionIfInvalid"
|
||||
* top_origin_validator?: scalar|Param|null, // For cross origin (e.g. iframe), this service will be in charge of verifying the top origin. // Default: null
|
||||
* creation_profiles?: array<string, array{ // Default: []
|
||||
* rp: array{
|
||||
* rp?: array{
|
||||
* id?: scalar|Param|null, // Default: null
|
||||
* name: scalar|Param|null,
|
||||
* name?: scalar|Param|null,
|
||||
* icon?: scalar|Param|null, // Deprecated: The child node "icon" at path "webauthn.creation_profiles..rp.icon" is deprecated and has no effect. // Default: null
|
||||
* },
|
||||
* challenge_length?: int|Param, // Default: 32
|
||||
@@ -2124,21 +2124,21 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* }>,
|
||||
* metadata?: bool|array{ // Enable the support of the Metadata Statements. Please read the documentation for this feature.
|
||||
* enabled?: bool|Param, // Default: false
|
||||
* mds_repository: scalar|Param|null, // The Metadata Statement repository.
|
||||
* status_report_repository: scalar|Param|null, // The Status Report repository.
|
||||
* mds_repository?: scalar|Param|null, // The Metadata Statement repository.
|
||||
* status_report_repository?: scalar|Param|null, // The Status Report repository.
|
||||
* certificate_chain_checker?: scalar|Param|null, // A Certificate Chain checker. // Default: "Webauthn\\MetadataService\\CertificateChain\\PhpCertificateChainValidator"
|
||||
* },
|
||||
* controllers?: bool|array{
|
||||
* enabled?: bool|Param, // Default: false
|
||||
* creation?: array<string, array{ // Default: []
|
||||
* options_method?: scalar|Param|null, // Default: "POST"
|
||||
* options_path: scalar|Param|null,
|
||||
* options_path?: scalar|Param|null,
|
||||
* result_method?: scalar|Param|null, // Default: "POST"
|
||||
* result_path?: scalar|Param|null, // Default: null
|
||||
* host?: scalar|Param|null, // Default: null
|
||||
* profile?: scalar|Param|null, // Default: "default"
|
||||
* options_builder?: scalar|Param|null, // When set, corresponds to the ID of the Public Key Credential Creation Builder. The profile-based ebuilder is ignored. // Default: null
|
||||
* user_entity_guesser: scalar|Param|null,
|
||||
* user_entity_guesser?: scalar|Param|null,
|
||||
* hide_existing_credentials?: scalar|Param|null, // In order to prevent username enumeration, the existing credentials can be hidden. This is highly recommended when the attestation ceremony is performed by anonymous users. // Default: false
|
||||
* options_storage?: scalar|Param|null, // Deprecated: The child node "options_storage" at path "webauthn.controllers.creation..options_storage" is deprecated. Please use the root option "options_storage" instead. // Service responsible of the options/user entity storage during the ceremony // Default: null
|
||||
* success_handler?: scalar|Param|null, // Default: "Webauthn\\Bundle\\Service\\DefaultSuccessHandler"
|
||||
@@ -2150,7 +2150,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* }>,
|
||||
* request?: array<string, array{ // Default: []
|
||||
* options_method?: scalar|Param|null, // Default: "POST"
|
||||
* options_path: scalar|Param|null,
|
||||
* options_path?: scalar|Param|null,
|
||||
* result_method?: scalar|Param|null, // Default: "POST"
|
||||
* result_path?: scalar|Param|null, // Default: null
|
||||
* host?: scalar|Param|null, // Default: null
|
||||
@@ -2171,10 +2171,10 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* baseurl?: scalar|Param|null, // Default: "<request_scheme_and_host>/saml/"
|
||||
* strict?: bool|Param,
|
||||
* debug?: bool|Param,
|
||||
* idp: array{
|
||||
* entityId: scalar|Param|null,
|
||||
* singleSignOnService: array{
|
||||
* url: scalar|Param|null,
|
||||
* idp?: array{
|
||||
* entityId?: scalar|Param|null,
|
||||
* singleSignOnService?: array{
|
||||
* url?: scalar|Param|null,
|
||||
* binding?: scalar|Param|null,
|
||||
* },
|
||||
* singleLogoutService?: array{
|
||||
@@ -2245,30 +2245,30 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* },
|
||||
* contactPerson?: array{
|
||||
* technical?: array{
|
||||
* givenName: scalar|Param|null,
|
||||
* emailAddress: scalar|Param|null,
|
||||
* givenName?: scalar|Param|null,
|
||||
* emailAddress?: scalar|Param|null,
|
||||
* },
|
||||
* support?: array{
|
||||
* givenName: scalar|Param|null,
|
||||
* emailAddress: scalar|Param|null,
|
||||
* givenName?: scalar|Param|null,
|
||||
* emailAddress?: scalar|Param|null,
|
||||
* },
|
||||
* administrative?: array{
|
||||
* givenName: scalar|Param|null,
|
||||
* emailAddress: scalar|Param|null,
|
||||
* givenName?: scalar|Param|null,
|
||||
* emailAddress?: scalar|Param|null,
|
||||
* },
|
||||
* billing?: array{
|
||||
* givenName: scalar|Param|null,
|
||||
* emailAddress: scalar|Param|null,
|
||||
* givenName?: scalar|Param|null,
|
||||
* emailAddress?: scalar|Param|null,
|
||||
* },
|
||||
* other?: array{
|
||||
* givenName: scalar|Param|null,
|
||||
* emailAddress: scalar|Param|null,
|
||||
* givenName?: scalar|Param|null,
|
||||
* emailAddress?: scalar|Param|null,
|
||||
* },
|
||||
* },
|
||||
* organization?: list<array{ // Default: []
|
||||
* name: scalar|Param|null,
|
||||
* displayname: scalar|Param|null,
|
||||
* url: scalar|Param|null,
|
||||
* name?: scalar|Param|null,
|
||||
* displayname?: scalar|Param|null,
|
||||
* url?: scalar|Param|null,
|
||||
* }>,
|
||||
* }>,
|
||||
* use_proxy_vars?: bool|Param, // Default: false
|
||||
@@ -2304,7 +2304,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* },
|
||||
* auto_install?: bool|Param, // Default: false
|
||||
* fonts?: list<array{ // Default: []
|
||||
* normal: scalar|Param|null,
|
||||
* normal?: scalar|Param|null,
|
||||
* bold?: scalar|Param|null,
|
||||
* italic?: scalar|Param|null,
|
||||
* bold_italic?: scalar|Param|null,
|
||||
@@ -2455,7 +2455,9 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* enabled?: bool|Param, // Default: true
|
||||
* },
|
||||
* max_query_depth?: int|Param, // Default: 20
|
||||
* graphql_playground?: array<mixed>,
|
||||
* graphql_playground?: bool|array{ // Deprecated: The "graphql_playground" configuration is deprecated and will be ignored.
|
||||
* enabled?: bool|Param, // Default: false
|
||||
* },
|
||||
* max_query_complexity?: int|Param, // Default: 500
|
||||
* nesting_separator?: scalar|Param|null, // The separator to use to filter nested fields. // Default: "_"
|
||||
* collection?: array{
|
||||
@@ -2512,7 +2514,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* },
|
||||
* termsOfService?: scalar|Param|null, // A URL to the Terms of Service for the API. MUST be in the format of a URL. // Default: null
|
||||
* tags?: list<array{ // Default: []
|
||||
* name: scalar|Param|null,
|
||||
* name?: scalar|Param|null,
|
||||
* description?: scalar|Param|null, // Default: null
|
||||
* }>,
|
||||
* license?: array{
|
||||
@@ -2804,7 +2806,10 @@ final class App
|
||||
*/
|
||||
public static function config(array $config): array
|
||||
{
|
||||
return AppReference::config($config);
|
||||
/** @var ConfigType $config */
|
||||
$config = AppReference::config($config);
|
||||
|
||||
return $config;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -88,3 +88,6 @@ The value of the environment variable is copied to the settings database, so the
|
||||
* `php bin/console partdb:attachments:download`: Download all attachments that are not already downloaded to the
|
||||
local filesystem. This is useful to create local backups of the attachments, no matter what happens on the remote, and
|
||||
also makes picture thumbnails available for the frontend for them.
|
||||
|
||||
## EDA integration commands
|
||||
* `php bin/console partdb:kicad:populate`: Populate KiCad footprint paths and symbol paths for footprints and categories based on their names. Use `--dry-run` to preview changes without applying them, and `--list` to list current values. See the [EDA integration documentation](eda_integration.md) for more details.
|
||||
|
||||
@@ -87,3 +87,31 @@ To show more levels of categories, you can set this value to a higher number.
|
||||
If you set this value to -1, all parts are shown inside a single category in KiCad, without any subcategories.
|
||||
|
||||
You can view the "real" category path of a part in the part details dialog in KiCad.
|
||||
|
||||
### Kicad:populate command
|
||||
|
||||
Part-DB also provides a command that attempts to automatically populate the KiCad symbol and footprint fields based on the part's category and footprint names.
|
||||
This is especially useful if you have a large database and want to quickly assign symbols and footprints to parts without doing it manually.
|
||||
|
||||
For this run `bin/console partdb:kicad:populate --dry-run` in the terminal, it will show you a list of suggestions for mappings for your existing categories and footprints.
|
||||
It uses names and alternative names, when the primary name doesn't match, to find the right mapping.
|
||||
If you are happy with the suggestions, you can run the command without the `--dry-run` option to apply the changes to your database. By default, only empty values are updated, but you can use the `--force` option to overwrite existing values as well.
|
||||
|
||||
It uses the mapping under `assets/commands/kicad_populate_default_mappings.json` by default, but you can extend/override it by providing your own mapping file
|
||||
with the `--mapping-file` option.
|
||||
The mapping file is a JSON file with the following structure, where the key is the name of the footprint or category, and the value is the corresponding KiCad library path:
|
||||
```json
|
||||
{
|
||||
"footprints": {
|
||||
"MyCustomPackage": "MyLibrary:MyFootprint",
|
||||
"0805": "Capacitor_SMD:C_0805_2012Metric"
|
||||
},
|
||||
"categories": {
|
||||
"Sensor": "Sensor:Sensor_Temperature",
|
||||
"MCU": "MCU_Microchip:PIC16F877A"
|
||||
}
|
||||
}
|
||||
```
|
||||
Its okay if the file contains just one of the `footprints` or `categories` keys, so you can choose to only provide mappings for one of them if you want.
|
||||
|
||||
It is recommended to take a backup of your database before running this command.
|
||||
|
||||
51
docs/usage/scanner.md
Normal file
51
docs/usage/scanner.md
Normal file
@@ -0,0 +1,51 @@
|
||||
---
|
||||
title: Barcode Scanner
|
||||
layout: default
|
||||
parent: Usage
|
||||
---
|
||||
|
||||
# Barcode scanner
|
||||
|
||||
When the user has the correct permission there will be a barcode scanner button in the navbar.
|
||||
On this page you can either input a barcode code by hand, use an external barcode scanner, or use your devices camera to
|
||||
scan a barcode.
|
||||
|
||||
In info mode (when the "Info" toggle is enabled) you can scan a barcode and Part-DB will parse it and show information
|
||||
about it.
|
||||
|
||||
Without info mode, the barcode will directly redirect you to the corresponding page.
|
||||
|
||||
### Barcode matching
|
||||
|
||||
When you scan a barcode, Part-DB will try to match it to an existing part, part lot or storage location first.
|
||||
For Part-DB generated barcodes, it will use the internal ID of a part. Alternatively you can also scan a barcode that contains the part's IPN.
|
||||
|
||||
You can set a GTIN/EAN code in the part properties and Part-DB will open the part page when you scan the corresponding GTIN/EAN barcode.
|
||||
|
||||
On a part lot you can under "Advanced" set a user barcode, that will redirect you to the part lot page when scanned. This allows to reuse
|
||||
arbitrary existing barcodes that already exist on the part lots (for example, from the manufacturer) and link them to the part lot in Part-DB.
|
||||
|
||||
Part-DB can also parse various distributor barcodes (for example from Digikey and Mouser) and will try to redirect you to the corresponding
|
||||
part page based on the distributor part number in the barcode.
|
||||
|
||||
### Part creation from barcodes
|
||||
For certain barcodes Part-DB can automatically create a new part, when it cannot find a matching part.
|
||||
Part-DB will try to retrieve the part information from an information provider and redirects you to the part creation page
|
||||
with the retrieved information pre-filled.
|
||||
|
||||
## Using an external barcode scanner
|
||||
|
||||
Part-DB supports the use of external barcode scanners that emulate keyboard input. To use a barcode scanner with Part-DB,
|
||||
simply connect the scanner to your computer and scan a barcode while the cursor is in a text field in Part-DB.
|
||||
The scanned barcode will be entered into the text field as if you had typed it on the keyboard.
|
||||
|
||||
In scanner fields, it will also try to insert special non-printable characters the scanner send via Alt + key combinations.
|
||||
This is required for EIGP114 datamatrix codes.
|
||||
|
||||
### Automatically redirect on barcode scanning
|
||||
|
||||
If you configure your barcode scanner to send a <SOH> (Start of heading, 0x01) non-printable character at the beginning
|
||||
of the scanned barcode, Part-DB will automatically scan the barcode that comes afterward (and is ended with an enter key)
|
||||
and redirects you to the corresponding page.
|
||||
This allows you to quickly scan a barcode from anywhere in Part-DB without the need to first open the scanner page.
|
||||
If an input field is focused, the barcode will be entered into the field as usual and no redirection will happen.
|
||||
52
migrations/Version20260211000000.php
Normal file
52
migrations/Version20260211000000.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use App\Migration\AbstractMultiPlatformMigration;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
|
||||
final class Version20260211000000 extends AbstractMultiPlatformMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add eda_visibility nullable boolean column to parameters and orderdetails tables';
|
||||
}
|
||||
|
||||
public function mySQLUp(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE parameters ADD eda_visibility TINYINT(1) DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE `orderdetails` ADD eda_visibility TINYINT(1) DEFAULT NULL');
|
||||
}
|
||||
|
||||
public function mySQLDown(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE parameters DROP COLUMN eda_visibility');
|
||||
$this->addSql('ALTER TABLE `orderdetails` DROP COLUMN eda_visibility');
|
||||
}
|
||||
|
||||
public function sqLiteUp(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE parameters ADD COLUMN eda_visibility BOOLEAN DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE orderdetails ADD COLUMN eda_visibility BOOLEAN DEFAULT NULL');
|
||||
}
|
||||
|
||||
public function sqLiteDown(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE parameters DROP COLUMN eda_visibility');
|
||||
$this->addSql('ALTER TABLE orderdetails DROP COLUMN eda_visibility');
|
||||
}
|
||||
|
||||
public function postgreSQLUp(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE parameters ADD eda_visibility BOOLEAN DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE orderdetails ADD eda_visibility BOOLEAN DEFAULT NULL');
|
||||
}
|
||||
|
||||
public function postgreSQLDown(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE parameters DROP COLUMN eda_visibility');
|
||||
$this->addSql('ALTER TABLE orderdetails DROP COLUMN eda_visibility');
|
||||
}
|
||||
}
|
||||
364
src/Command/PopulateKicadCommand.php
Normal file
364
src/Command/PopulateKicadCommand.php
Normal file
@@ -0,0 +1,364 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Entity\Parts\Category;
|
||||
use App\Entity\Parts\Footprint;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
|
||||
#[AsCommand('partdb:kicad:populate', 'Populate KiCad footprint paths and symbol paths for footprints and categories')]
|
||||
final class PopulateKicadCommand extends Command
|
||||
{
|
||||
private const DEFAULT_MAPPING_FILE = 'assets/commands/kicad_populate_default_mappings.json';
|
||||
|
||||
public function __construct(private readonly EntityManagerInterface $entityManager, #[Autowire("%kernel.project_dir%")] private readonly string $projectDir)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setHelp('This command populates KiCad footprint paths on Footprint entities and KiCad symbol paths on Category entities based on their names.');
|
||||
|
||||
$this
|
||||
->addOption('dry-run', null, InputOption::VALUE_NONE, 'Preview changes without applying them')
|
||||
->addOption('footprints', null, InputOption::VALUE_NONE, 'Only update footprint entities')
|
||||
->addOption('categories', null, InputOption::VALUE_NONE, 'Only update category entities')
|
||||
->addOption('force', null, InputOption::VALUE_NONE, 'Overwrite existing values (by default, only empty values are updated)')
|
||||
->addOption('list', null, InputOption::VALUE_NONE, 'List all footprints and categories with their current KiCad values')
|
||||
->addOption('mapping-file', null, InputOption::VALUE_REQUIRED, 'Path to a JSON file with custom mappings (merges with built-in defaults)')
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$dryRun = $input->getOption('dry-run');
|
||||
$footprintsOnly = $input->getOption('footprints');
|
||||
$categoriesOnly = $input->getOption('categories');
|
||||
$force = $input->getOption('force');
|
||||
$list = $input->getOption('list');
|
||||
$mappingFile = $input->getOption('mapping-file');
|
||||
|
||||
// If neither specified, do both
|
||||
$doFootprints = !$categoriesOnly || $footprintsOnly;
|
||||
$doCategories = !$footprintsOnly || $categoriesOnly;
|
||||
|
||||
if ($list) {
|
||||
$this->listCurrentValues($io);
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
// Load mappings: start with built-in defaults, then merge user-supplied file
|
||||
['footprints' => $footprintMappings, 'categories' => $categoryMappings] = $this->getDefaultMappings();
|
||||
|
||||
if ($mappingFile !== null) {
|
||||
$customMappings = $this->loadMappingFile($mappingFile, $io);
|
||||
if ($customMappings === null) {
|
||||
return Command::FAILURE;
|
||||
}
|
||||
if (isset($customMappings['footprints']) && is_array($customMappings['footprints'])) {
|
||||
// User mappings take priority (overwrite defaults)
|
||||
$footprintMappings = array_merge($footprintMappings, $customMappings['footprints']);
|
||||
$io->text(sprintf('Loaded %d custom footprint mappings from %s', count($customMappings['footprints']), $mappingFile));
|
||||
}
|
||||
if (isset($customMappings['categories']) && is_array($customMappings['categories'])) {
|
||||
$categoryMappings = array_merge($categoryMappings, $customMappings['categories']);
|
||||
$io->text(sprintf('Loaded %d custom category mappings from %s', count($customMappings['categories']), $mappingFile));
|
||||
}
|
||||
}
|
||||
|
||||
if ($dryRun) {
|
||||
$io->note('DRY RUN MODE - No changes will be made');
|
||||
}
|
||||
|
||||
$totalUpdated = 0;
|
||||
|
||||
if ($doFootprints) {
|
||||
$updated = $this->updateFootprints($io, $dryRun, $force, $footprintMappings);
|
||||
$totalUpdated += $updated;
|
||||
}
|
||||
|
||||
if ($doCategories) {
|
||||
$updated = $this->updateCategories($io, $dryRun, $force, $categoryMappings);
|
||||
$totalUpdated += $updated;
|
||||
}
|
||||
|
||||
if (!$dryRun && $totalUpdated > 0) {
|
||||
$this->entityManager->flush();
|
||||
$io->success(sprintf('Updated %d entities. Run "php bin/console cache:clear" to clear the cache.', $totalUpdated));
|
||||
} elseif ($dryRun && $totalUpdated > 0) {
|
||||
$io->info(sprintf('DRY RUN: Would update %d entities. Run without --dry-run to apply changes.', $totalUpdated));
|
||||
} else {
|
||||
$io->info('No entities needed updating.');
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
private function listCurrentValues(SymfonyStyle $io): void
|
||||
{
|
||||
$io->section('Current Footprint KiCad Values');
|
||||
|
||||
$footprintRepo = $this->entityManager->getRepository(Footprint::class);
|
||||
/** @var Footprint[] $footprints */
|
||||
$footprints = $footprintRepo->findAll();
|
||||
|
||||
$rows = [];
|
||||
foreach ($footprints as $footprint) {
|
||||
$kicadValue = $footprint->getEdaInfo()->getKicadFootprint();
|
||||
$rows[] = [
|
||||
$footprint->getId(),
|
||||
$footprint->getName(),
|
||||
$kicadValue ?? '(empty)',
|
||||
];
|
||||
}
|
||||
|
||||
$io->table(['ID', 'Name', 'KiCad Footprint'], $rows);
|
||||
|
||||
$io->section('Current Category KiCad Values');
|
||||
|
||||
$categoryRepo = $this->entityManager->getRepository(Category::class);
|
||||
/** @var Category[] $categories */
|
||||
$categories = $categoryRepo->findAll();
|
||||
|
||||
$rows = [];
|
||||
foreach ($categories as $category) {
|
||||
$kicadValue = $category->getEdaInfo()->getKicadSymbol();
|
||||
$rows[] = [
|
||||
$category->getId(),
|
||||
$category->getName(),
|
||||
$kicadValue ?? '(empty)',
|
||||
];
|
||||
}
|
||||
|
||||
$io->table(['ID', 'Name', 'KiCad Symbol'], $rows);
|
||||
}
|
||||
|
||||
private function updateFootprints(SymfonyStyle $io, bool $dryRun, bool $force, array $mappings): int
|
||||
{
|
||||
$io->section('Updating Footprint Entities');
|
||||
|
||||
$footprintRepo = $this->entityManager->getRepository(Footprint::class);
|
||||
/** @var Footprint[] $footprints */
|
||||
$footprints = $footprintRepo->findAll();
|
||||
|
||||
$updated = 0;
|
||||
$skipped = [];
|
||||
|
||||
foreach ($footprints as $footprint) {
|
||||
$name = $footprint->getName();
|
||||
$currentValue = $footprint->getEdaInfo()->getKicadFootprint();
|
||||
|
||||
// Skip if already has value and not forcing
|
||||
if (!$force && $currentValue !== null && $currentValue !== '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for exact match on name first, then try alternative names
|
||||
$matchedValue = $this->findFootprintMapping($mappings, $name, $footprint->getAlternativeNames());
|
||||
|
||||
if ($matchedValue !== null) {
|
||||
$io->text(sprintf(' %s: %s -> %s', $name, $currentValue ?? '(empty)', $matchedValue));
|
||||
|
||||
if (!$dryRun) {
|
||||
$footprint->getEdaInfo()->setKicadFootprint($matchedValue);
|
||||
}
|
||||
$updated++;
|
||||
} else {
|
||||
// No mapping found
|
||||
$skipped[] = $name;
|
||||
}
|
||||
}
|
||||
|
||||
$io->newLine();
|
||||
$io->text(sprintf('Updated: %d footprints', $updated));
|
||||
|
||||
if (count($skipped) > 0) {
|
||||
$io->warning(sprintf('No mapping found for %d footprints:', count($skipped)));
|
||||
foreach ($skipped as $name) {
|
||||
$io->text(' - ' . $name);
|
||||
}
|
||||
}
|
||||
|
||||
return $updated;
|
||||
}
|
||||
|
||||
private function updateCategories(SymfonyStyle $io, bool $dryRun, bool $force, array $mappings): int
|
||||
{
|
||||
$io->section('Updating Category Entities');
|
||||
|
||||
$categoryRepo = $this->entityManager->getRepository(Category::class);
|
||||
/** @var Category[] $categories */
|
||||
$categories = $categoryRepo->findAll();
|
||||
|
||||
$updated = 0;
|
||||
$skipped = [];
|
||||
|
||||
foreach ($categories as $category) {
|
||||
$name = $category->getName();
|
||||
$currentValue = $category->getEdaInfo()->getKicadSymbol();
|
||||
|
||||
// Skip if already has value and not forcing
|
||||
if (!$force && $currentValue !== null && $currentValue !== '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for matches using the pattern-based mappings (also check alternative names)
|
||||
$matchedValue = $this->findCategoryMapping($mappings, $name, $category->getAlternativeNames());
|
||||
|
||||
if ($matchedValue !== null) {
|
||||
$io->text(sprintf(' %s: %s -> %s', $name, $currentValue ?? '(empty)', $matchedValue));
|
||||
|
||||
if (!$dryRun) {
|
||||
$category->getEdaInfo()->setKicadSymbol($matchedValue);
|
||||
}
|
||||
$updated++;
|
||||
} else {
|
||||
$skipped[] = $name;
|
||||
}
|
||||
}
|
||||
|
||||
$io->newLine();
|
||||
$io->text(sprintf('Updated: %d categories', $updated));
|
||||
|
||||
if (count($skipped) > 0) {
|
||||
$io->note(sprintf('No mapping found for %d categories (this is often expected):', count($skipped)));
|
||||
foreach ($skipped as $name) {
|
||||
$io->text(' - ' . $name);
|
||||
}
|
||||
}
|
||||
|
||||
return $updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a JSON mapping file and returns the parsed data.
|
||||
* Expected format: {"footprints": {"Name": "KiCad:Path"}, "categories": {"Pattern": "KiCad:Path"}}
|
||||
*
|
||||
* @return array|null The parsed mappings, or null on error
|
||||
*/
|
||||
private function loadMappingFile(string $path, SymfonyStyle $io): ?array
|
||||
{
|
||||
if (!file_exists($path)) {
|
||||
$io->error(sprintf('Mapping file not found: %s', $path));
|
||||
return null;
|
||||
}
|
||||
|
||||
$content = file_get_contents($path);
|
||||
if ($content === false) {
|
||||
$io->error(sprintf('Could not read mapping file: %s', $path));
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = json_decode($content, true);
|
||||
if (!is_array($data)) {
|
||||
$io->error(sprintf('Invalid JSON in mapping file: %s', $path));
|
||||
return null;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function matchesPattern(string $name, string $pattern): bool
|
||||
{
|
||||
// Check for exact match
|
||||
if ($pattern === $name) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for case-insensitive contains
|
||||
if (stripos($name, $pattern) !== false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a footprint mapping by checking the entity name and its alternative names.
|
||||
* Footprints use exact matching.
|
||||
*
|
||||
* @param array<string, string> $mappings
|
||||
* @param string $name The primary name of the footprint
|
||||
* @param string|null $alternativeNames Comma-separated alternative names
|
||||
* @return string|null The matched KiCad path, or null if no match found
|
||||
*/
|
||||
private function findFootprintMapping(array $mappings, string $name, ?string $alternativeNames): ?string
|
||||
{
|
||||
// Check primary name
|
||||
if (isset($mappings[$name])) {
|
||||
return $mappings[$name];
|
||||
}
|
||||
|
||||
// Check alternative names
|
||||
if ($alternativeNames !== null && $alternativeNames !== '') {
|
||||
foreach (explode(',', $alternativeNames) as $altName) {
|
||||
$altName = trim($altName);
|
||||
if ($altName !== '' && isset($mappings[$altName])) {
|
||||
return $mappings[$altName];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a category mapping by checking the entity name and its alternative names.
|
||||
* Categories use pattern-based matching (case-insensitive contains).
|
||||
*
|
||||
* @param array<string, string> $mappings
|
||||
* @param string $name The primary name of the category
|
||||
* @param string|null $alternativeNames Comma-separated alternative names
|
||||
* @return string|null The matched KiCad symbol path, or null if no match found
|
||||
*/
|
||||
private function findCategoryMapping(array $mappings, string $name, ?string $alternativeNames): ?string
|
||||
{
|
||||
// Check primary name against all patterns
|
||||
foreach ($mappings as $pattern => $kicadSymbol) {
|
||||
if ($this->matchesPattern($name, $pattern)) {
|
||||
return $kicadSymbol;
|
||||
}
|
||||
}
|
||||
|
||||
// Check alternative names against all patterns
|
||||
if ($alternativeNames !== null && $alternativeNames !== '') {
|
||||
foreach (explode(',', $alternativeNames) as $altName) {
|
||||
$altName = trim($altName);
|
||||
if ($altName === '') {
|
||||
continue;
|
||||
}
|
||||
foreach ($mappings as $pattern => $kicadSymbol) {
|
||||
if ($this->matchesPattern($altName, $pattern)) {
|
||||
return $kicadSymbol;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default mappings for footprints and categories.
|
||||
* @return array{footprints: array<string, string>, categories: array<string, string>}
|
||||
* @throws \JsonException
|
||||
*/
|
||||
private function getDefaultMappings(): array
|
||||
{
|
||||
$path = $this->projectDir . '/' . self::DEFAULT_MAPPING_FILE;
|
||||
$content = file_get_contents($path);
|
||||
|
||||
return json_decode($content, true, 512, JSON_THROW_ON_ERROR);
|
||||
}
|
||||
}
|
||||
117
src/Controller/BatchEdaController.php
Normal file
117
src/Controller/BatchEdaController.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Parts\Part;
|
||||
use App\Form\Part\EDA\BatchEdaType;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
class BatchEdaController extends AbstractController
|
||||
{
|
||||
public function __construct(private readonly EntityManagerInterface $entityManager)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute shared EDA values across all parts. If all parts have the same value for a field, return it.
|
||||
* @param Part[] $parts
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function getSharedEdaValues(array $parts): array
|
||||
{
|
||||
$fields = [
|
||||
'reference_prefix' => static fn (Part $p) => $p->getEdaInfo()->getReferencePrefix(),
|
||||
'value' => static fn (Part $p) => $p->getEdaInfo()->getValue(),
|
||||
'kicad_symbol' => static fn (Part $p) => $p->getEdaInfo()->getKicadSymbol(),
|
||||
'kicad_footprint' => static fn (Part $p) => $p->getEdaInfo()->getKicadFootprint(),
|
||||
'visibility' => static fn (Part $p) => $p->getEdaInfo()->getVisibility(),
|
||||
'exclude_from_bom' => static fn (Part $p) => $p->getEdaInfo()->getExcludeFromBom(),
|
||||
'exclude_from_board' => static fn (Part $p) => $p->getEdaInfo()->getExcludeFromBoard(),
|
||||
'exclude_from_sim' => static fn (Part $p) => $p->getEdaInfo()->getExcludeFromSim(),
|
||||
];
|
||||
|
||||
$data = [];
|
||||
foreach ($fields as $key => $getter) {
|
||||
$values = array_map($getter, $parts);
|
||||
$unique = array_unique($values, SORT_REGULAR);
|
||||
if (count($unique) === 1) {
|
||||
$data[$key] = $unique[array_key_first($unique)];
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
#[Route('/tools/batch_eda_edit', name: 'batch_eda_edit')]
|
||||
public function batchEdaEdit(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('@parts.edit');
|
||||
|
||||
$ids = $request->query->getString('ids', '');
|
||||
$redirectUrl = $request->query->getString('_redirect', '');
|
||||
|
||||
//Parse part IDs and load parts
|
||||
$idArray = array_filter(array_map(intval(...), explode(',', $ids)), static fn (int $id): bool => $id > 0);
|
||||
$parts = $this->entityManager->getRepository(Part::class)->findBy(['id' => $idArray]);
|
||||
|
||||
if ($parts === []) {
|
||||
$this->addFlash('error', 'batch_eda.no_parts_selected');
|
||||
|
||||
return $redirectUrl !== '' ? $this->redirect($redirectUrl) : $this->redirectToRoute('parts_show_all');
|
||||
}
|
||||
|
||||
//Pre-populate form with shared values (when all parts have the same value)
|
||||
$initialData = $this->getSharedEdaValues($parts);
|
||||
$form = $this->createForm(BatchEdaType::class, $initialData);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
foreach ($parts as $part) {
|
||||
$this->denyAccessUnlessGranted('edit', $part);
|
||||
$edaInfo = $part->getEdaInfo();
|
||||
|
||||
if ($form->get('apply_reference_prefix')->getData()) {
|
||||
$edaInfo->setReferencePrefix($form->get('reference_prefix')->getData() ?: null);
|
||||
}
|
||||
if ($form->get('apply_value')->getData()) {
|
||||
$edaInfo->setValue($form->get('value')->getData() ?: null);
|
||||
}
|
||||
if ($form->get('apply_kicad_symbol')->getData()) {
|
||||
$edaInfo->setKicadSymbol($form->get('kicad_symbol')->getData() ?: null);
|
||||
}
|
||||
if ($form->get('apply_kicad_footprint')->getData()) {
|
||||
$edaInfo->setKicadFootprint($form->get('kicad_footprint')->getData() ?: null);
|
||||
}
|
||||
if ($form->get('apply_visibility')->getData()) {
|
||||
$edaInfo->setVisibility($form->get('visibility')->getData());
|
||||
}
|
||||
if ($form->get('apply_exclude_from_bom')->getData()) {
|
||||
$edaInfo->setExcludeFromBom($form->get('exclude_from_bom')->getData());
|
||||
}
|
||||
if ($form->get('apply_exclude_from_board')->getData()) {
|
||||
$edaInfo->setExcludeFromBoard($form->get('exclude_from_board')->getData());
|
||||
}
|
||||
if ($form->get('apply_exclude_from_sim')->getData()) {
|
||||
$edaInfo->setExcludeFromSim($form->get('exclude_from_sim')->getData());
|
||||
}
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
$this->addFlash('success', 'batch_eda.success');
|
||||
|
||||
return $redirectUrl !== '' ? $this->redirect($redirectUrl) : $this->redirectToRoute('parts_show_all');
|
||||
}
|
||||
|
||||
return $this->render('parts/batch_eda_edit.html.twig', [
|
||||
'form' => $form->createView(),
|
||||
'parts' => $parts,
|
||||
'redirect_url' => $redirectUrl,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,8 @@ use App\Entity\Parts\Category;
|
||||
use App\Entity\Parts\Part;
|
||||
use App\Services\EDA\KiCadHelper;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
@@ -55,15 +57,16 @@ class KiCadApiController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route('/categories.json', name: 'kicad_api_categories')]
|
||||
public function categories(): Response
|
||||
public function categories(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('@categories.read');
|
||||
|
||||
return $this->json($this->kiCADHelper->getCategories());
|
||||
$data = $this->kiCADHelper->getCategories();
|
||||
return $this->createCacheableJsonResponse($request, $data, 300);
|
||||
}
|
||||
|
||||
#[Route('/parts/category/{category}.json', name: 'kicad_api_category')]
|
||||
public function categoryParts(?Category $category): Response
|
||||
public function categoryParts(Request $request, ?Category $category): Response
|
||||
{
|
||||
if ($category !== null) {
|
||||
$this->denyAccessUnlessGranted('read', $category);
|
||||
@@ -72,14 +75,31 @@ class KiCadApiController extends AbstractController
|
||||
}
|
||||
$this->denyAccessUnlessGranted('@parts.read');
|
||||
|
||||
return $this->json($this->kiCADHelper->getCategoryParts($category));
|
||||
$minimal = $request->query->getBoolean('minimal', false);
|
||||
$data = $this->kiCADHelper->getCategoryParts($category, $minimal);
|
||||
return $this->createCacheableJsonResponse($request, $data, 300);
|
||||
}
|
||||
|
||||
#[Route('/parts/{part}.json', name: 'kicad_api_part')]
|
||||
public function partDetails(Part $part): Response
|
||||
public function partDetails(Request $request, Part $part): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('read', $part);
|
||||
|
||||
return $this->json($this->kiCADHelper->getKiCADPart($part));
|
||||
$data = $this->kiCADHelper->getKiCADPart($part);
|
||||
return $this->createCacheableJsonResponse($request, $data, 60);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a JSON response with HTTP cache headers (ETag and Cache-Control).
|
||||
* Returns 304 Not Modified if the client's ETag matches.
|
||||
*/
|
||||
private function createCacheableJsonResponse(Request $request, array $data, int $maxAge): Response
|
||||
{
|
||||
$response = new JsonResponse($data);
|
||||
$response->setEtag(md5(json_encode($data)));
|
||||
$response->headers->set('Cache-Control', 'private, max-age=' . $maxAge);
|
||||
$response->isNotModified($request);
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -115,6 +115,61 @@ class PartDataTableHelper
|
||||
return implode('<br>', $tmp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders an EDA/KiCad completeness indicator for the given part.
|
||||
* Shows icons for symbol, footprint, and value status.
|
||||
*/
|
||||
public function renderEdaStatus(Part $context): string
|
||||
{
|
||||
$edaInfo = $context->getEdaInfo();
|
||||
$category = $context->getCategory();
|
||||
$footprint = $context->getFootprint();
|
||||
|
||||
// Determine effective values (direct or inherited)
|
||||
$hasSymbol = $edaInfo->getKicadSymbol() !== null || $category?->getEdaInfo()->getKicadSymbol() !== null;
|
||||
$hasFootprint = $edaInfo->getKicadFootprint() !== null || $footprint?->getEdaInfo()->getKicadFootprint() !== null;
|
||||
$hasReference = $edaInfo->getReferencePrefix() !== null || $category?->getEdaInfo()->getReferencePrefix() !== null;
|
||||
|
||||
$symbolInherited = $edaInfo->getKicadSymbol() === null && $category?->getEdaInfo()->getKicadSymbol() !== null;
|
||||
$footprintInherited = $edaInfo->getKicadFootprint() === null && $footprint?->getEdaInfo()->getKicadFootprint() !== null;
|
||||
|
||||
$icons = [];
|
||||
|
||||
// Symbol status
|
||||
if ($hasSymbol) {
|
||||
$title = $this->translator->trans('eda.status.symbol_set');
|
||||
$class = $symbolInherited ? 'text-info' : 'text-success';
|
||||
$icons[] = sprintf('<i class="fa-solid fa-microchip fa-fw %s" title="%s"></i>', $class, $title);
|
||||
}
|
||||
|
||||
// Footprint status
|
||||
if ($hasFootprint) {
|
||||
$title = $this->translator->trans('eda.status.footprint_set');
|
||||
$class = $footprintInherited ? 'text-info' : 'text-success';
|
||||
$icons[] = sprintf('<i class="fa-solid fa-stamp fa-fw %s" title="%s"></i>', $class, $title);
|
||||
}
|
||||
|
||||
// Reference prefix status
|
||||
if ($hasReference) {
|
||||
$icons[] = sprintf('<i class="fa-solid fa-font fa-fw text-success" title="%s"></i>',
|
||||
$this->translator->trans('eda.status.reference_set'));
|
||||
}
|
||||
|
||||
if (empty($icons)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Overall status: all 3 = green check, partial = yellow
|
||||
$allSet = $hasSymbol && $hasFootprint && $hasReference;
|
||||
$statusIcon = $allSet
|
||||
? sprintf('<i class="fa-solid fa-bolt fa-fw text-success" title="%s"></i>', $this->translator->trans('eda.status.complete'))
|
||||
: sprintf('<i class="fa-solid fa-bolt fa-fw text-warning" title="%s"></i>', $this->translator->trans('eda.status.partial'));
|
||||
|
||||
// Wrap in link to EDA settings tab (data-turbo=false to ensure hash is read on page load)
|
||||
$editUrl = $this->entityURLGenerator->editURL($context) . '#eda';
|
||||
return sprintf('<a href="%s" data-turbo="false">%s</a>', $editUrl, $statusIcon);
|
||||
}
|
||||
|
||||
public function renderAmount(Part $context): string
|
||||
{
|
||||
$amount = $context->getAmountSum();
|
||||
|
||||
@@ -89,6 +89,10 @@ final class PartsDataTable implements DataTableTypeInterface
|
||||
$this->configureOptions($resolver);
|
||||
$options = $resolver->resolve($options);
|
||||
|
||||
/*************************************************************************************************************
|
||||
* When adding columns here, add them also to PartTableColumns enum, to make them configurable in the settings!
|
||||
*************************************************************************************************************/
|
||||
|
||||
$this->csh
|
||||
//Color the table rows depending on the review and favorite status
|
||||
->add('row_color', RowClassColumn::class, [
|
||||
@@ -228,6 +232,21 @@ final class PartsDataTable implements DataTableTypeInterface
|
||||
])
|
||||
->add('attachments', PartAttachmentsColumn::class, [
|
||||
'label' => $this->translator->trans('part.table.attachments'),
|
||||
])
|
||||
->add('eda_reference', TextColumn::class, [
|
||||
'label' => $this->translator->trans('part.table.eda_reference'),
|
||||
'render' => static fn($value, Part $context) => htmlspecialchars($context->getEdaInfo()->getReferencePrefix() ?? ''),
|
||||
'orderField' => 'NATSORT(part.eda_info.reference_prefix)'
|
||||
])
|
||||
->add('eda_value', TextColumn::class, [
|
||||
'label' => $this->translator->trans('part.table.eda_value'),
|
||||
'render' => static fn($value, Part $context) => htmlspecialchars($context->getEdaInfo()->getValue() ?? ''),
|
||||
'orderField' => 'NATSORT(part.eda_info.value)'
|
||||
])
|
||||
->add('eda_status', TextColumn::class, [
|
||||
'label' => $this->translator->trans('part.table.eda_status'),
|
||||
'render' => fn($value, Part $context) => $this->partDataTableHelper->renderEdaStatus($context),
|
||||
'className' => 'text-center',
|
||||
]);
|
||||
|
||||
//Add a column to list the projects where the part is used, when the user has the permission to see the projects
|
||||
|
||||
@@ -172,6 +172,13 @@ abstract class AbstractParameter extends AbstractNamedDBElement implements Uniqu
|
||||
#[Assert\Length(max: 255)]
|
||||
protected string $group = '';
|
||||
|
||||
/**
|
||||
* @var bool|null Whether this parameter should be exported as a field in the EDA HTTP library API. Null means use system default.
|
||||
*/
|
||||
#[Groups(['full', 'parameter:read', 'parameter:write', 'import'])]
|
||||
#[ORM\Column(type: Types::BOOLEAN, nullable: true, options: ['default' => null])]
|
||||
protected ?bool $eda_visibility = null;
|
||||
|
||||
/**
|
||||
* Mapping is done in subclasses.
|
||||
*
|
||||
@@ -471,6 +478,21 @@ abstract class AbstractParameter extends AbstractNamedDBElement implements Uniqu
|
||||
return static::ALLOWED_ELEMENT_CLASS;
|
||||
}
|
||||
|
||||
public function isEdaVisibility(): ?bool
|
||||
{
|
||||
return $this->eda_visibility;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setEdaVisibility(?bool $eda_visibility): self
|
||||
{
|
||||
$this->eda_visibility = $eda_visibility;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getComparableFields(): array
|
||||
{
|
||||
return ['name' => $this->name, 'group' => $this->group, 'element' => $this->element?->getId()];
|
||||
|
||||
@@ -122,6 +122,13 @@ class Orderdetail extends AbstractDBElement implements TimeStampableInterface, N
|
||||
#[ORM\Column(type: Types::BOOLEAN)]
|
||||
protected bool $obsolete = false;
|
||||
|
||||
/**
|
||||
* @var bool|null Whether this orderdetail's supplier part number should be exported as an EDA field. Null means use system default.
|
||||
*/
|
||||
#[Groups(['full', 'import', 'orderdetail:read', 'orderdetail:write'])]
|
||||
#[ORM\Column(type: Types::BOOLEAN, nullable: true, options: ['default' => null])]
|
||||
protected ?bool $eda_visibility = null;
|
||||
|
||||
/**
|
||||
* @var string The URL to the product on the supplier's website
|
||||
*/
|
||||
@@ -418,6 +425,21 @@ class Orderdetail extends AbstractDBElement implements TimeStampableInterface, N
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isEdaVisibility(): ?bool
|
||||
{
|
||||
return $this->eda_visibility;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setEdaVisibility(?bool $eda_visibility): self
|
||||
{
|
||||
$this->eda_visibility = $eda_visibility;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->getSupplierPartNr();
|
||||
|
||||
@@ -61,6 +61,8 @@ class ScanDialogType extends AbstractType
|
||||
'attr' => [
|
||||
'autofocus' => true,
|
||||
'id' => 'scan_dialog_input',
|
||||
'style' => 'font-family: var(--bs-font-monospace)',
|
||||
'data-controller' => 'elements--nonprintable-char-input',
|
||||
],
|
||||
]);
|
||||
|
||||
|
||||
@@ -54,7 +54,9 @@ use App\Entity\Parameters\StorageLocationParameter;
|
||||
use App\Entity\Parameters\SupplierParameter;
|
||||
use App\Entity\Parts\MeasurementUnit;
|
||||
use App\Form\Type\ExponentialNumberType;
|
||||
use App\Form\Type\TriStateCheckboxType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\NumberType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
@@ -147,6 +149,14 @@ class ParameterType extends AbstractType
|
||||
'class' => 'form-control-sm',
|
||||
],
|
||||
]);
|
||||
|
||||
// Only show the EDA visibility field for part parameters, as it has no function for other entities
|
||||
if ($options['data_class'] === PartParameter::class) {
|
||||
$builder->add('eda_visibility', TriStateCheckboxType::class, [
|
||||
'label' => false,
|
||||
'required' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function finishView(FormView $view, FormInterface $form, array $options): void
|
||||
|
||||
116
src/Form/Part/EDA/BatchEdaType.php
Normal file
116
src/Form/Part/EDA/BatchEdaType.php
Normal file
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Part\EDA;
|
||||
|
||||
use App\Form\Type\TriStateCheckboxType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
use function Symfony\Component\Translation\t;
|
||||
|
||||
/**
|
||||
* Form type for batch editing EDA/KiCad fields on multiple parts at once.
|
||||
* Each field has an "apply" checkbox — only checked fields are applied.
|
||||
*/
|
||||
class BatchEdaType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
->add('reference_prefix', TextType::class, [
|
||||
'label' => 'eda_info.reference_prefix',
|
||||
'required' => false,
|
||||
'attr' => ['placeholder' => t('eda_info.reference_prefix.placeholder')],
|
||||
])
|
||||
->add('apply_reference_prefix', CheckboxType::class, [
|
||||
'label' => 'batch_eda.apply',
|
||||
'required' => false,
|
||||
'mapped' => false,
|
||||
])
|
||||
->add('value', TextType::class, [
|
||||
'label' => 'eda_info.value',
|
||||
'required' => false,
|
||||
'attr' => ['placeholder' => t('eda_info.value.placeholder')],
|
||||
])
|
||||
->add('apply_value', CheckboxType::class, [
|
||||
'label' => 'batch_eda.apply',
|
||||
'required' => false,
|
||||
'mapped' => false,
|
||||
])
|
||||
->add('kicad_symbol', KicadFieldAutocompleteType::class, [
|
||||
'label' => 'eda_info.kicad_symbol',
|
||||
'type' => KicadFieldAutocompleteType::TYPE_SYMBOL,
|
||||
'required' => false,
|
||||
'attr' => ['placeholder' => t('eda_info.kicad_symbol.placeholder')],
|
||||
])
|
||||
->add('apply_kicad_symbol', CheckboxType::class, [
|
||||
'label' => 'batch_eda.apply',
|
||||
'required' => false,
|
||||
'mapped' => false,
|
||||
])
|
||||
->add('kicad_footprint', KicadFieldAutocompleteType::class, [
|
||||
'label' => 'eda_info.kicad_footprint',
|
||||
'type' => KicadFieldAutocompleteType::TYPE_FOOTPRINT,
|
||||
'required' => false,
|
||||
'attr' => ['placeholder' => t('eda_info.kicad_footprint.placeholder')],
|
||||
])
|
||||
->add('apply_kicad_footprint', CheckboxType::class, [
|
||||
'label' => 'batch_eda.apply',
|
||||
'required' => false,
|
||||
'mapped' => false,
|
||||
])
|
||||
->add('visibility', TriStateCheckboxType::class, [
|
||||
'label' => 'eda_info.visibility',
|
||||
'required' => false,
|
||||
])
|
||||
->add('apply_visibility', CheckboxType::class, [
|
||||
'label' => 'batch_eda.apply',
|
||||
'required' => false,
|
||||
'mapped' => false,
|
||||
])
|
||||
->add('exclude_from_bom', TriStateCheckboxType::class, [
|
||||
'label' => 'eda_info.exclude_from_bom',
|
||||
'required' => false,
|
||||
])
|
||||
->add('apply_exclude_from_bom', CheckboxType::class, [
|
||||
'label' => 'batch_eda.apply',
|
||||
'required' => false,
|
||||
'mapped' => false,
|
||||
])
|
||||
->add('exclude_from_board', TriStateCheckboxType::class, [
|
||||
'label' => 'eda_info.exclude_from_board',
|
||||
'required' => false,
|
||||
])
|
||||
->add('apply_exclude_from_board', CheckboxType::class, [
|
||||
'label' => 'batch_eda.apply',
|
||||
'required' => false,
|
||||
'mapped' => false,
|
||||
])
|
||||
->add('exclude_from_sim', TriStateCheckboxType::class, [
|
||||
'label' => 'eda_info.exclude_from_sim',
|
||||
'required' => false,
|
||||
])
|
||||
->add('apply_exclude_from_sim', CheckboxType::class, [
|
||||
'label' => 'batch_eda.apply',
|
||||
'required' => false,
|
||||
'mapped' => false,
|
||||
])
|
||||
->add('submit', SubmitType::class, [
|
||||
'label' => 'batch_eda.submit',
|
||||
'attr' => ['class' => 'btn btn-primary'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -79,6 +79,11 @@ class OrderdetailType extends AbstractType
|
||||
'label' => 'orderdetails.edit.prices_includes_vat',
|
||||
]);
|
||||
|
||||
$builder->add('eda_visibility', TriStateCheckboxType::class, [
|
||||
'required' => false,
|
||||
'label' => 'orderdetails.edit.eda_visibility',
|
||||
]);
|
||||
|
||||
//Add pricedetails after we know the data, so we can set the default currency
|
||||
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($options): void {
|
||||
/** @var Orderdetail $orderdetail */
|
||||
|
||||
100
src/Helpers/RandomizeUseragentHttpClient.php
Normal file
100
src/Helpers/RandomizeUseragentHttpClient.php
Normal file
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
|
||||
|
||||
/**
|
||||
* HttpClient wrapper that randomizes the user agent for each request, to make it harder for servers to detect and block us.
|
||||
* When we get a 503, 403 or 429, we assume that the server is blocking us and try again with a different user agent, until we run out of retries.
|
||||
*/
|
||||
final class RandomizeUseragentHttpClient implements HttpClientInterface
|
||||
{
|
||||
public const USER_AGENTS = [
|
||||
"Mozilla/5.0 (Windows; U; Windows NT 10.0; Win64; x64) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/52.0.1359.302 Safari/600.6 Edge/15.25690",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36 Edge/16.16299",
|
||||
"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 8_8_3) Gecko/20100101 Firefox/51.6",
|
||||
"Mozilla/5.0 (Android; Android 4.4.4; E:number:20-23:00 Build/24.0.B.1.34) AppleWebKit/603.18 (KHTML, like Gecko) Chrome/47.0.1559.384 Mobile Safari/600.5",
|
||||
"Mozilla/5.0 (compatible; MSIE 9.0; Windows; Windows NT 6.3; WOW64 Trident/5.0)",
|
||||
"Mozilla/5.0 (Windows; Windows NT 6.0; Win64; x64) AppleWebKit/602.21 (KHTML, like Gecko) Chrome/51.0.3187.154 Safari/536",
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 9_4_2; like Mac OS X) AppleWebKit/537.24 (KHTML, like Gecko) Chrome/51.0.2432.275 Mobile Safari/535.6",
|
||||
"Mozilla/5.0 (U; Linux i680 ) Gecko/20100101 Firefox/57.5",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 8_8_6; en-US) Gecko/20100101 Firefox/53.9",
|
||||
"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 8_6_7) AppleWebKit/534.46 (KHTML, like Gecko) Chrome/55.0.3276.345 Safari/535",
|
||||
"Mozilla/5.0 (Windows; Windows NT 10.5;) AppleWebKit/535.42 (KHTML, like Gecko) Chrome/53.0.1176.353 Safari/534.0 Edge/11.95743",
|
||||
"Mozilla/5.0 (Linux; Android 5.1.1; MOTO G Build/LPH223) AppleWebKit/600.27 (KHTML, like Gecko) Chrome/47.0.1604.204 Mobile Safari/535.1",
|
||||
"Mozilla/5.0 (iPod; CPU iPod OS 7_4_8; like Mac OS X) AppleWebKit/534.17 (KHTML, like Gecko) Chrome/50.0.1632.146 Mobile Safari/600.4",
|
||||
"Mozilla/5.0 (Linux; U; Linux i570 ; en-US) Gecko/20100101 Firefox/49.9",
|
||||
"Mozilla/5.0 (Windows NT 10.2; WOW64; en-US) AppleWebKit/603.2 (KHTML, like Gecko) Chrome/55.0.1299.311 Safari/535",
|
||||
"Mozilla/5.0 (Windows; Windows NT 10.5; x64; en-US) AppleWebKit/603.39 (KHTML, like Gecko) Chrome/52.0.1443.139 Safari/536.6 Edge/13.79436",
|
||||
"Mozilla/5.0 (Linux; U; Android 5.1; SM-G9350T Build/MMB29M) AppleWebKit/537.15 (KHTML, like Gecko) Chrome/55.0.2552.307 Mobile Safari/600.8",
|
||||
"Mozilla/5.0 (Android; Android 6.0; SAMSUNG SM-D9350V Build/MDB08L) AppleWebKit/535.30 (KHTML, like Gecko) Chrome/53.0.1345.278 Mobile Safari/537.4",
|
||||
"Mozilla/5.0 (Windows; Windows NT 10.0;) AppleWebKit/534.44 (KHTML, like Gecko) Chrome/47.0.3503.387 Safari/601",
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private readonly HttpClientInterface $client,
|
||||
private readonly array $userAgents = self::USER_AGENTS,
|
||||
private readonly int $repeatOnFailure = 1,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getRandomUserAgent(): string
|
||||
{
|
||||
return $this->userAgents[array_rand($this->userAgents)];
|
||||
}
|
||||
|
||||
public function request(string $method, string $url, array $options = []): ResponseInterface
|
||||
{
|
||||
$repeatsLeft = $this->repeatOnFailure;
|
||||
do {
|
||||
$modifiedOptions = $options;
|
||||
if (!isset($modifiedOptions['headers']['User-Agent'])) {
|
||||
$modifiedOptions['headers']['User-Agent'] = $this->getRandomUserAgent();
|
||||
}
|
||||
$response = $this->client->request($method, $url, $modifiedOptions);
|
||||
|
||||
//When we get a 503, 403 or 429, we assume that the server is blocking us and try again with a different user agent
|
||||
if (!in_array($response->getStatusCode(), [403, 429, 503], true)) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
//Otherwise we try again with a different user agent, until we run out of retries
|
||||
} while ($repeatsLeft-- > 0);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function stream(iterable|ResponseInterface $responses, ?float $timeout = null): ResponseStreamInterface
|
||||
{
|
||||
return $this->client->stream($responses, $timeout);
|
||||
}
|
||||
|
||||
public function withOptions(array $options): static
|
||||
{
|
||||
return new self($this->client->withOptions($options), $this->userAgents, $this->repeatOnFailure);
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,15 @@ class PartNormalizer implements NormalizerInterface, DenormalizerInterface, Norm
|
||||
'spn' => 'supplier_part_number',
|
||||
'supplier_product_number' => 'supplier_part_number',
|
||||
'storage_location' => 'storelocation',
|
||||
//EDA/KiCad field aliases
|
||||
'kicad_symbol' => 'eda_kicad_symbol',
|
||||
'kicad_footprint' => 'eda_kicad_footprint',
|
||||
'kicad_reference' => 'eda_reference_prefix',
|
||||
'kicad_value' => 'eda_value',
|
||||
'eda_exclude_bom' => 'eda_exclude_from_bom',
|
||||
'eda_exclude_board' => 'eda_exclude_from_board',
|
||||
'eda_exclude_sim' => 'eda_exclude_from_sim',
|
||||
'eda_invisible' => 'eda_visibility',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
@@ -190,9 +199,45 @@ class PartNormalizer implements NormalizerInterface, DenormalizerInterface, Norm
|
||||
}
|
||||
}
|
||||
|
||||
//Handle EDA/KiCad fields
|
||||
$this->applyEdaFields($object, $data);
|
||||
|
||||
return $object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply EDA/KiCad fields from CSV data to the Part's EDAPartInfo.
|
||||
*/
|
||||
private function applyEdaFields(Part $part, array $data): void
|
||||
{
|
||||
$edaInfo = $part->getEdaInfo();
|
||||
|
||||
if (!empty($data['eda_kicad_symbol'])) {
|
||||
$edaInfo->setKicadSymbol(trim((string) $data['eda_kicad_symbol']));
|
||||
}
|
||||
if (!empty($data['eda_kicad_footprint'])) {
|
||||
$edaInfo->setKicadFootprint(trim((string) $data['eda_kicad_footprint']));
|
||||
}
|
||||
if (!empty($data['eda_reference_prefix'])) {
|
||||
$edaInfo->setReferencePrefix(trim((string) $data['eda_reference_prefix']));
|
||||
}
|
||||
if (!empty($data['eda_value'])) {
|
||||
$edaInfo->setValue(trim((string) $data['eda_value']));
|
||||
}
|
||||
if (isset($data['eda_exclude_from_bom']) && $data['eda_exclude_from_bom'] !== '') {
|
||||
$edaInfo->setExcludeFromBom(filter_var($data['eda_exclude_from_bom'], FILTER_VALIDATE_BOOLEAN));
|
||||
}
|
||||
if (isset($data['eda_exclude_from_board']) && $data['eda_exclude_from_board'] !== '') {
|
||||
$edaInfo->setExcludeFromBoard(filter_var($data['eda_exclude_from_board'], FILTER_VALIDATE_BOOLEAN));
|
||||
}
|
||||
if (isset($data['eda_exclude_from_sim']) && $data['eda_exclude_from_sim'] !== '') {
|
||||
$edaInfo->setExcludeFromSim(filter_var($data['eda_exclude_from_sim'], FILTER_VALIDATE_BOOLEAN));
|
||||
}
|
||||
if (isset($data['eda_visibility']) && $data['eda_visibility'] !== '') {
|
||||
$edaInfo->setVisibility(filter_var($data['eda_visibility'], FILTER_VALIDATE_BOOLEAN));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool[]
|
||||
*/
|
||||
|
||||
@@ -23,6 +23,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Services\EDA;
|
||||
|
||||
use App\Entity\Attachments\Attachment;
|
||||
use App\Entity\Parts\Category;
|
||||
use App\Entity\Parts\Footprint;
|
||||
use App\Entity\Parts\Part;
|
||||
@@ -43,6 +44,9 @@ class KiCadHelper
|
||||
/** @var int The maximum level of the shown categories. 0 Means only the top level categories are shown. -1 means only a single one containing */
|
||||
private readonly int $category_depth;
|
||||
|
||||
/** @var bool Whether to resolve actual datasheet PDF URLs (true) or use Part-DB page links (false) */
|
||||
private readonly bool $datasheetAsPdf;
|
||||
|
||||
public function __construct(
|
||||
private readonly NodesListBuilder $nodesListBuilder,
|
||||
private readonly TagAwareCacheInterface $kicadCache,
|
||||
@@ -51,9 +55,10 @@ class KiCadHelper
|
||||
private readonly UrlGeneratorInterface $urlGenerator,
|
||||
private readonly EntityURLGenerator $entityURLGenerator,
|
||||
private readonly TranslatorInterface $translator,
|
||||
KiCadEDASettings $kiCadEDASettings,
|
||||
private readonly KiCadEDASettings $kiCadEDASettings,
|
||||
) {
|
||||
$this->category_depth = $kiCadEDASettings->categoryDepth;
|
||||
$this->datasheetAsPdf = $kiCadEDASettings->datasheetAsPdf ?? true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,11 +120,16 @@ class KiCadHelper
|
||||
}
|
||||
|
||||
//Format the category for KiCAD
|
||||
// Use the category comment as description if available, otherwise use the Part-DB URL
|
||||
$description = $category->getComment();
|
||||
if ($description === null || $description === '') {
|
||||
$description = $this->entityURLGenerator->listPartsURL($category);
|
||||
}
|
||||
|
||||
$result[] = [
|
||||
'id' => (string)$category->getId(),
|
||||
'name' => $category->getFullPath('/'),
|
||||
//Show the category link as the category description, this also fixes an segfault in KiCad see issue #878
|
||||
'description' => $this->entityURLGenerator->listPartsURL($category),
|
||||
'description' => $description,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -131,11 +141,13 @@ class KiCadHelper
|
||||
* Returns an array of objects containing all parts for the given category in the format required by KiCAD.
|
||||
* The result is cached for performance and invalidated on category or part changes.
|
||||
* @param Category|null $category
|
||||
* @param bool $minimal If true, only return id and name (faster for symbol chooser listing)
|
||||
* @return array
|
||||
*/
|
||||
public function getCategoryParts(?Category $category): array
|
||||
public function getCategoryParts(?Category $category, bool $minimal = false): array
|
||||
{
|
||||
return $this->kicadCache->get('kicad_category_parts_'.($category?->getID() ?? 0) . '_' . $this->category_depth,
|
||||
$cacheKey = 'kicad_category_parts_'.($category?->getID() ?? 0) . '_' . $this->category_depth . ($minimal ? '_min' : '');
|
||||
return $this->kicadCache->get($cacheKey,
|
||||
function (ItemInterface $item) use ($category) {
|
||||
$item->tag([
|
||||
$this->tagGenerator->getElementTypeCacheTag(Category::class),
|
||||
@@ -198,14 +210,22 @@ class KiCadHelper
|
||||
$result["fields"]["value"] = $this->createField($part->getEdaInfo()->getValue() ?? $part->getName(), true);
|
||||
$result["fields"]["keywords"] = $this->createField($part->getTags());
|
||||
|
||||
//Use the part info page as datasheet link. It must be an absolute URL.
|
||||
$result["fields"]["datasheet"] = $this->createField(
|
||||
$this->urlGenerator->generate(
|
||||
'part_info',
|
||||
['id' => $part->getId()],
|
||||
UrlGeneratorInterface::ABSOLUTE_URL)
|
||||
//Use the part info page as Part-DB link. It must be an absolute URL.
|
||||
$partUrl = $this->urlGenerator->generate(
|
||||
'part_info',
|
||||
['id' => $part->getId()],
|
||||
UrlGeneratorInterface::ABSOLUTE_URL
|
||||
);
|
||||
|
||||
//Try to find an actual datasheet attachment (configurable: PDF URL vs Part-DB page link)
|
||||
if ($this->datasheetAsPdf) {
|
||||
$datasheetUrl = $this->findDatasheetUrl($part);
|
||||
$result["fields"]["datasheet"] = $this->createField($datasheetUrl ?? $partUrl);
|
||||
} else {
|
||||
$result["fields"]["datasheet"] = $this->createField($partUrl);
|
||||
}
|
||||
$result["fields"]["Part-DB URL"] = $this->createField($partUrl);
|
||||
|
||||
//Add basic fields
|
||||
$result["fields"]["description"] = $this->createField($part->getDescription());
|
||||
if ($part->getCategory() !== null) {
|
||||
@@ -245,32 +265,7 @@ class KiCadHelper
|
||||
$result["fields"]["Part-DB IPN"] = $this->createField($part->getIpn());
|
||||
}
|
||||
|
||||
// Add supplier information from orderdetails (include obsolete orderdetails)
|
||||
if ($part->getOrderdetails(false)->count() > 0) {
|
||||
$supplierCounts = [];
|
||||
|
||||
foreach ($part->getOrderdetails(false) as $orderdetail) {
|
||||
if ($orderdetail->getSupplier() !== null && $orderdetail->getSupplierPartNr() !== '') {
|
||||
$supplierName = $orderdetail->getSupplier()->getName();
|
||||
|
||||
$supplierName .= " SPN"; // Append "SPN" to the supplier name to indicate Supplier Part Number
|
||||
|
||||
if (!isset($supplierCounts[$supplierName])) {
|
||||
$supplierCounts[$supplierName] = 0;
|
||||
}
|
||||
$supplierCounts[$supplierName]++;
|
||||
|
||||
// Create field name with sequential number if more than one from same supplier (e.g. "Mouser", "Mouser 2", etc.)
|
||||
$fieldName = $supplierCounts[$supplierName] > 1
|
||||
? $supplierName . ' ' . $supplierCounts[$supplierName]
|
||||
: $supplierName;
|
||||
|
||||
$result["fields"][$fieldName] = $this->createField($orderdetail->getSupplierPartNr());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Add fields for KiCost:
|
||||
//Add KiCost manufacturer fields (always present, independent of orderdetails)
|
||||
if ($part->getManufacturer() !== null) {
|
||||
$result["fields"]["manf"] = $this->createField($part->getManufacturer()->getName());
|
||||
}
|
||||
@@ -278,13 +273,74 @@ class KiCadHelper
|
||||
$result['fields']['manf#'] = $this->createField($part->getManufacturerProductNumber());
|
||||
}
|
||||
|
||||
//For each supplier, add a field with the supplier name and the supplier part number for KiCost
|
||||
if ($part->getOrderdetails(false)->count() > 0) {
|
||||
foreach ($part->getOrderdetails(false) as $orderdetail) {
|
||||
// Add supplier information from orderdetails (include obsolete orderdetails)
|
||||
// If any orderdetail has eda_visibility explicitly set to true, only export those;
|
||||
// otherwise export all (backward compat when no flags are set)
|
||||
$allOrderdetails = $part->getOrderdetails(false);
|
||||
if ($allOrderdetails->count() > 0) {
|
||||
$hasExplicitEdaVisibility = false;
|
||||
foreach ($allOrderdetails as $od) {
|
||||
if ($od->isEdaVisibility() !== null) {
|
||||
$hasExplicitEdaVisibility = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$supplierCounts = [];
|
||||
foreach ($allOrderdetails as $orderdetail) {
|
||||
if ($orderdetail->getSupplier() !== null && $orderdetail->getSupplierPartNr() !== '') {
|
||||
$fieldName = mb_strtolower($orderdetail->getSupplier()->getName()) . '#';
|
||||
// When explicit flags exist, filter by resolved visibility
|
||||
$resolvedVisibility = $orderdetail->isEdaVisibility() ?? $this->kiCadEDASettings->defaultOrderdetailsVisibility;
|
||||
if ($hasExplicitEdaVisibility && !$resolvedVisibility) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$supplierName = $orderdetail->getSupplier()->getName() . ' SPN';
|
||||
|
||||
if (!isset($supplierCounts[$supplierName])) {
|
||||
$supplierCounts[$supplierName] = 0;
|
||||
}
|
||||
$supplierCounts[$supplierName]++;
|
||||
|
||||
// Create field name with sequential number if more than one from same supplier
|
||||
$fieldName = $supplierCounts[$supplierName] > 1
|
||||
? $supplierName . ' ' . $supplierCounts[$supplierName]
|
||||
: $supplierName;
|
||||
|
||||
$result["fields"][$fieldName] = $this->createField($orderdetail->getSupplierPartNr());
|
||||
|
||||
//Also add a KiCost-compatible field (supplier_name# = SPN)
|
||||
$kicostFieldName = mb_strtolower($orderdetail->getSupplier()->getName()) . '#';
|
||||
$result["fields"][$kicostFieldName] = $this->createField($orderdetail->getSupplierPartNr());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Add stock quantity and storage locations (only count non-expired lots with known quantity)
|
||||
$totalStock = 0;
|
||||
$locations = [];
|
||||
foreach ($part->getPartLots() as $lot) {
|
||||
$isAvailable = !$lot->isInstockUnknown() && $lot->isExpired() !== true;
|
||||
if ($isAvailable) {
|
||||
$totalStock += $lot->getAmount();
|
||||
if ($lot->getAmount() > 0 && $lot->getStorageLocation() !== null) {
|
||||
$locations[] = $lot->getStorageLocation()->getName();
|
||||
}
|
||||
}
|
||||
}
|
||||
$result['fields']['Stock'] = $this->createField($totalStock);
|
||||
if ($locations !== []) {
|
||||
$result['fields']['Storage Location'] = $this->createField(implode(', ', array_unique($locations)));
|
||||
}
|
||||
|
||||
//Add parameters marked for EDA export (explicit true, or system default when null)
|
||||
foreach ($part->getParameters() as $parameter) {
|
||||
$paramVisibility = $parameter->isEdaVisibility() ?? $this->kiCadEDASettings->defaultParameterVisibility;
|
||||
if ($paramVisibility && $parameter->getName() !== '') {
|
||||
$fieldName = $parameter->getName();
|
||||
//Don't overwrite hardcoded fields
|
||||
if (!isset($result['fields'][$fieldName])) {
|
||||
$result['fields'][$fieldName] = $this->createField($parameter->getFormattedValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -344,7 +400,7 @@ class KiCadHelper
|
||||
|
||||
//If the user set a visibility, then use it
|
||||
if ($eda_info->getVisibility() !== null) {
|
||||
return $part->getEdaInfo()->getVisibility();
|
||||
return $eda_info->getVisibility();
|
||||
}
|
||||
|
||||
//If the part has a category, then use the category visibility if possible
|
||||
@@ -395,4 +451,64 @@ class KiCadHelper
|
||||
'visible' => $this->boolToKicadBool($visible),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the URL to the actual datasheet file for the given part.
|
||||
* Searches attachments by type name, attachment name, and file extension.
|
||||
* @return string|null The datasheet URL, or null if no datasheet was found.
|
||||
*/
|
||||
private function findDatasheetUrl(Part $part): ?string
|
||||
{
|
||||
$firstPdf = null;
|
||||
|
||||
foreach ($part->getAttachments() as $attachment) {
|
||||
//Check if the attachment type name contains "datasheet"
|
||||
$typeName = $attachment->getAttachmentType()?->getName() ?? '';
|
||||
if (str_contains(mb_strtolower($typeName), 'datasheet')) {
|
||||
return $this->getAttachmentUrl($attachment);
|
||||
}
|
||||
|
||||
//Check if the attachment name contains "datasheet"
|
||||
$name = mb_strtolower($attachment->getName());
|
||||
if (str_contains($name, 'datasheet') || str_contains($name, 'data sheet')) {
|
||||
return $this->getAttachmentUrl($attachment);
|
||||
}
|
||||
|
||||
//Track first PDF as fallback (check internal extension or external URL path)
|
||||
if ($firstPdf === null) {
|
||||
$extension = $attachment->getExtension();
|
||||
if ($extension === null && $attachment->hasExternal()) {
|
||||
$urlPath = parse_url($attachment->getExternalPath(), PHP_URL_PATH);
|
||||
$extension = is_string($urlPath) ? strtolower(pathinfo($urlPath, PATHINFO_EXTENSION)) : null;
|
||||
}
|
||||
if ($extension === 'pdf') {
|
||||
$firstPdf = $attachment;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Use first PDF attachment as fallback
|
||||
if ($firstPdf !== null) {
|
||||
return $this->getAttachmentUrl($firstPdf);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an absolute URL for viewing the given attachment.
|
||||
* Prefers the external URL (direct link) over the internal view route.
|
||||
*/
|
||||
private function getAttachmentUrl(Attachment $attachment): string
|
||||
{
|
||||
if ($attachment->hasExternal()) {
|
||||
return $attachment->getExternalPath();
|
||||
}
|
||||
|
||||
return $this->urlGenerator->generate(
|
||||
'attachment_view',
|
||||
['id' => $attachment->getId()],
|
||||
UrlGeneratorInterface::ABSOLUTE_URL
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -396,10 +396,14 @@ class BOMImporter
|
||||
}
|
||||
}
|
||||
|
||||
// Create unique key for this entry (name + part ID)
|
||||
$entry_key = $name . '|' . ($part ? $part->getID() : 'null');
|
||||
// Create unique key for this entry.
|
||||
// When linked to a Part-DB part, use the part ID as key (merges footprint variants).
|
||||
// Otherwise, use name (which includes package) to avoid merging unrelated components.
|
||||
$entry_key = $part !== null
|
||||
? 'part:' . $part->getID()
|
||||
: 'name:' . $name;
|
||||
|
||||
// Check if we already have an entry with the same name and part
|
||||
// Check if we already have an entry with the same key
|
||||
if (isset($entries_by_key[$entry_key])) {
|
||||
// Merge with existing entry
|
||||
$existing_entry = $entries_by_key[$entry_key];
|
||||
@@ -413,14 +417,22 @@ class BOMImporter
|
||||
$existing_quantity = $existing_entry->getQuantity();
|
||||
$existing_entry->setQuantity($existing_quantity + $quantity);
|
||||
|
||||
// Track footprint variants in comment when merging entries with different packages
|
||||
$currentPackage = trim($mapped_entry['Package'] ?? '');
|
||||
if ($currentPackage !== '' && !str_contains($existing_entry->getComment(), $currentPackage)) {
|
||||
$comment = $existing_entry->getComment();
|
||||
$existing_entry->setComment($comment . ', Footprint variant: ' . $currentPackage);
|
||||
}
|
||||
|
||||
$this->logger->info('Merged duplicate BOM entry', [
|
||||
'name' => $name,
|
||||
'part_id' => $part ? $part->getID() : null,
|
||||
'part_id' => $part?->getID(),
|
||||
'original_quantity' => $existing_quantity,
|
||||
'added_quantity' => $quantity,
|
||||
'new_quantity' => $existing_quantity + $quantity,
|
||||
'original_mountnames' => $existing_mountnames,
|
||||
'added_mountnames' => $designator,
|
||||
'package' => $currentPackage,
|
||||
]);
|
||||
|
||||
continue; // Skip creating new entry
|
||||
|
||||
@@ -24,6 +24,7 @@ declare(strict_types=1);
|
||||
namespace App\Services\InfoProviderSystem\Providers;
|
||||
|
||||
use App\Exceptions\ProviderIDNotSupportedException;
|
||||
use App\Helpers\RandomizeUseragentHttpClient;
|
||||
use App\Services\InfoProviderSystem\DTOs\ParameterDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\PriceDTO;
|
||||
@@ -54,11 +55,8 @@ class GenericWebProvider implements InfoProviderInterface
|
||||
private readonly ProviderRegistry $providerRegistry, private readonly PartInfoRetriever $infoRetriever,
|
||||
)
|
||||
{
|
||||
$this->httpClient = $httpClient->withOptions(
|
||||
$this->httpClient = (new RandomizeUseragentHttpClient($httpClient))->withOptions(
|
||||
[
|
||||
'headers' => [
|
||||
'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36',
|
||||
],
|
||||
'timeout' => 15,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -23,6 +23,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Services\InfoProviderSystem\Providers;
|
||||
|
||||
use App\Helpers\RandomizeUseragentHttpClient;
|
||||
use App\Services\InfoProviderSystem\DTOs\FileDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\ParameterDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
|
||||
@@ -30,7 +31,6 @@ use App\Services\InfoProviderSystem\DTOs\PriceDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
|
||||
use App\Settings\InfoProviderSystem\ReicheltSettings;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
@@ -39,10 +39,13 @@ class ReicheltProvider implements InfoProviderInterface
|
||||
|
||||
public const DISTRIBUTOR_NAME = "Reichelt";
|
||||
|
||||
public function __construct(private readonly HttpClientInterface $client,
|
||||
private readonly HttpClientInterface $client;
|
||||
|
||||
public function __construct(HttpClientInterface $client,
|
||||
private readonly ReicheltSettings $settings,
|
||||
)
|
||||
{
|
||||
$this->client = new RandomizeUseragentHttpClient($client);
|
||||
}
|
||||
|
||||
public function getProviderInfo(): array
|
||||
|
||||
@@ -127,6 +127,15 @@ implode(',', array_map(static fn (PartLot $lot) => $lot->getID(), $part->getPart
|
||||
);
|
||||
}
|
||||
|
||||
if ($action === 'batch_edit_eda') {
|
||||
$ids = implode(',', array_map(static fn (Part $part) => $part->getID(), $selected_parts));
|
||||
return new RedirectResponse(
|
||||
$this->urlGenerator->generate('batch_eda_edit', [
|
||||
'ids' => $ids,
|
||||
'_redirect' => $redirect_url
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
//Iterate over the parts and apply the action to it:
|
||||
foreach ($selected_parts as $part) {
|
||||
|
||||
@@ -51,6 +51,13 @@ enum PartTableColumns : string implements TranslatableInterface
|
||||
case GTIN = "gtin";
|
||||
case TAGS = "tags";
|
||||
case ATTACHMENTS = "attachments";
|
||||
|
||||
case EDA_REFERENCE = "eda_reference";
|
||||
|
||||
case EDA_VALUE = "eda_value";
|
||||
|
||||
case EDA_STATUS = "eda_status";
|
||||
|
||||
case EDIT = "edit";
|
||||
|
||||
public function trans(TranslatorInterface $translator, ?string $locale = null): string
|
||||
|
||||
@@ -43,4 +43,23 @@ class KiCadEDASettings
|
||||
envVar: "int:EDA_KICAD_CATEGORY_DEPTH", envVarMode: EnvVarMode::OVERWRITE)]
|
||||
#[Assert\Range(min: -1)]
|
||||
public int $categoryDepth = 0;
|
||||
}
|
||||
|
||||
#[SettingsParameter(label: new TM("settings.misc.kicad_eda.datasheet_link"),
|
||||
description: new TM("settings.misc.kicad_eda.datasheet_link.help")
|
||||
)]
|
||||
public ?bool $datasheetAsPdf = true;
|
||||
|
||||
#[SettingsParameter(
|
||||
label: new TM("settings.misc.kicad_eda.default_parameter_visibility"),
|
||||
description: new TM("settings.misc.kicad_eda.default_parameter_visibility.help"),
|
||||
|
||||
)]
|
||||
public bool $defaultParameterVisibility = false;
|
||||
|
||||
#[SettingsParameter(
|
||||
label: new TM("settings.misc.kicad_eda.default_orderdetails_visibility"),
|
||||
description: new TM("settings.misc.kicad_eda.default_orderdetails_visibility.help"),
|
||||
|
||||
)]
|
||||
public bool $defaultOrderdetailsVisibility = false;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ app.request.locale | replace({"_": "-"}) }}"
|
||||
{# For the UX translator, just use the language part (before the _. should be 2 chars), otherwise it finds no translations #}
|
||||
{# For the UX translator, just use the language part (before the _. should be 2 chars), otherwise it finds no translations #}
|
||||
data-symfony-ux-translator-locale="{{ app.request.locale|u.truncate(2) }}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
@@ -73,9 +73,17 @@
|
||||
{{ encore_entry_script_tags('webauthn_tfa') }}
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body data-base-url="{{ path('homepage', {'_locale': app.request.locale}) }}"
|
||||
<body data-base-url="{{ path('homepage', {'_locale': app.request.locale}) }}"
|
||||
data-locale="{{ app.request.locale|default("en")|slice(0,2) }}"
|
||||
data-keybindings-special-characters="{{ settings_instance('keybindings').enableSpecialCharacters ? 'true' : 'false' }}">
|
||||
|
||||
{# Listen for the special #}
|
||||
{% if is_granted("@tools.label_scanner") %}
|
||||
<form class="d-none" {{ stimulus_controller('helpers/scan_special_char') }} action="{{ path("scan_dialog") }}" data-turbo-frame="content">
|
||||
<input name="input" type="hidden">
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
{% block body %}
|
||||
<header>
|
||||
<turbo-frame id="navbar-frame" target="content" data-turbo-action="advance">
|
||||
@@ -121,13 +129,13 @@
|
||||
|
||||
<!-- Back to top button -->
|
||||
<button id="back-to-top" class="btn btn-primary back-to-top btn-sm" role="button" title="{% trans %}back_to_top{% endtrans %}"
|
||||
{{ stimulus_controller('common/back_to_top') }} {{ stimulus_action('common/back_to_top', 'backToTop') }}>
|
||||
{{ stimulus_controller('common/back_to_top') }} {{ stimulus_action('common/back_to_top', 'backToTop') }}>
|
||||
<i class="fas fa-angle-up fa-fw"></i>
|
||||
</button>
|
||||
|
||||
{# Must be outside of the sidebar or it will be hidden too #}
|
||||
<button class="btn btn-outline-secondary btn-sm p-0 d-md-block d-none" type="button" id="sidebar-toggle-button" title="{% trans %}sidebar.big.toggle{% endtrans %}"
|
||||
{{ stimulus_controller('common/hide_sidebar') }} {{ stimulus_action('common/hide_sidebar', 'toggleSidebar') }} style="--fa-width: 10px;">
|
||||
{{ stimulus_controller('common/hide_sidebar') }} {{ stimulus_action('common/hide_sidebar', 'toggleSidebar') }} style="--fa-width: 10px;">
|
||||
<i class="fas fa-angle-left"></i>
|
||||
</button>
|
||||
|
||||
|
||||
@@ -62,6 +62,9 @@
|
||||
<option {% if not is_granted('@projects.read') %}disabled{% endif %} value="add_to_project" data-url="{{ path('select_project')}}">{% trans %}part_list.action.projects.add_to_project{% endtrans %}</option>
|
||||
</optgroup>
|
||||
|
||||
<optgroup label="{% trans %}part_list.action.group.eda{% endtrans %}">
|
||||
<option {% if not is_granted('@parts.edit') %}disabled{% endif %} value="batch_edit_eda" data-turbo="false">{% trans %}part_list.action.batch_edit_eda{% endtrans %}</option>
|
||||
</optgroup>
|
||||
<optgroup label="{% trans %}part_list.action.action.delete{% endtrans %}">
|
||||
<option {% if not is_granted('@parts.delete') %}disabled{% endif %} value="delete">{% trans %}part_list.action.action.delete{% endtrans %}</option>
|
||||
</optgroup>
|
||||
|
||||
88
templates/parts/batch_eda_edit.html.twig
Normal file
88
templates/parts/batch_eda_edit.html.twig
Normal file
@@ -0,0 +1,88 @@
|
||||
{% extends "main_card.html.twig" %}
|
||||
|
||||
{% block title %}{% trans %}batch_eda.title{% endtrans %}{% endblock %}
|
||||
|
||||
{% block card_title %}
|
||||
<i class="fas fa-bolt"></i> {% trans %}batch_eda.title{% endtrans %}
|
||||
{% endblock %}
|
||||
|
||||
{% block card_content %}
|
||||
<div class="mb-3">
|
||||
<p>{% trans with {'%count%': parts|length} %}batch_eda.description{% endtrans %}</p>
|
||||
<details>
|
||||
<summary>{% trans %}batch_eda.show_parts{% endtrans %}</summary>
|
||||
<ul class="list-unstyled ms-3 mt-1">
|
||||
{% for part in parts %}
|
||||
<li><a href="{{ path('part_edit', {id: part.id}) }}">{{ part.name }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
{{ form_start(form) }}
|
||||
|
||||
<p class="text-muted small">{% trans %}batch_eda.apply_hint{% endtrans %}</p>
|
||||
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 30px;">{% trans %}batch_eda.apply{% endtrans %}</th>
|
||||
<th>{% trans %}batch_eda.field{% endtrans %}</th>
|
||||
<th>{% trans %}batch_eda.value{% endtrans %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="text-center align-middle">{{ form_widget(form.apply_reference_prefix) }}</td>
|
||||
<td class="align-middle">{{ form_label(form.reference_prefix) }}</td>
|
||||
<td>{{ form_widget(form.reference_prefix, {'attr': {'class': 'form-control-sm'}}) }}{{ form_errors(form.reference_prefix) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-center align-middle">{{ form_widget(form.apply_value) }}</td>
|
||||
<td class="align-middle">{{ form_label(form.value) }}</td>
|
||||
<td>{{ form_widget(form.value, {'attr': {'class': 'form-control-sm'}}) }}{{ form_errors(form.value) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-center align-middle">{{ form_widget(form.apply_kicad_symbol) }}</td>
|
||||
<td class="align-middle">{{ form_label(form.kicad_symbol) }}</td>
|
||||
<td>{{ form_widget(form.kicad_symbol) }}{{ form_errors(form.kicad_symbol) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-center align-middle">{{ form_widget(form.apply_kicad_footprint) }}</td>
|
||||
<td class="align-middle">{{ form_label(form.kicad_footprint) }}</td>
|
||||
<td>{{ form_widget(form.kicad_footprint) }}{{ form_errors(form.kicad_footprint) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-center align-middle">{{ form_widget(form.apply_visibility) }}</td>
|
||||
<td class="align-middle">{{ form_label(form.visibility) }}</td>
|
||||
<td>{{ form_widget(form.visibility) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-center align-middle">{{ form_widget(form.apply_exclude_from_bom) }}</td>
|
||||
<td class="align-middle">{{ form_label(form.exclude_from_bom) }}</td>
|
||||
<td>{{ form_widget(form.exclude_from_bom) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-center align-middle">{{ form_widget(form.apply_exclude_from_board) }}</td>
|
||||
<td class="align-middle">{{ form_label(form.exclude_from_board) }}</td>
|
||||
<td>{{ form_widget(form.exclude_from_board) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-center align-middle">{{ form_widget(form.apply_exclude_from_sim) }}</td>
|
||||
<td class="align-middle">{{ form_label(form.exclude_from_sim) }}</td>
|
||||
<td>{{ form_widget(form.exclude_from_sim) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="d-flex justify-content-between">
|
||||
{% if redirect_url %}
|
||||
<a href="{{ redirect_url }}" class="btn btn-secondary">{% trans %}batch_eda.cancel{% endtrans %}</a>
|
||||
{% else %}
|
||||
<a href="{{ path('parts_show_all') }}" class="btn btn-secondary">{% trans %}batch_eda.cancel{% endtrans %}</a>
|
||||
{% endif %}
|
||||
{{ form_widget(form.submit) }}
|
||||
</div>
|
||||
|
||||
{{ form_end(form) }}
|
||||
{% endblock %}
|
||||
@@ -14,6 +14,7 @@
|
||||
<th>{% trans %}specifications.unit{% endtrans %}</th>
|
||||
<th>{% trans %}specifications.text{% endtrans %}</th>
|
||||
<th>{% trans %}specifications.group{% endtrans %}</th>
|
||||
<th title="{% trans %}specifications.eda_visibility.help{% endtrans %}"><i class="fas fa-bolt fa-fw"></i></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
{{ form_row(form.supplier_product_url, {'attr': {'class': 'form-control-sm'}}) }}
|
||||
{{ form_widget(form.obsolete) }}
|
||||
{{ form_widget(form.pricesIncludesVAT) }}
|
||||
{{ form_widget(form.eda_visibility) }}
|
||||
</td>
|
||||
<td>
|
||||
<div {{ collection.controller(form.pricedetails, 'pricedetails.edit.delete.confirm') }}>
|
||||
@@ -79,6 +80,9 @@
|
||||
<td {{ stimulus_controller('pages/latex_preview', {"unit": true}) }}>{{ form_widget(form.unit, {"attr": {"data-pages--parameters-autocomplete-target": "unit", "data-pages--latex-preview-target": "input"}}) }}{{ form_errors(form.unit) }}<span {{ stimulus_target('pages/latex_preview', 'preview') }}></span></td>
|
||||
<td>{{ form_widget(form.value_text) }}{{ form_errors(form.value_text) }}</td>
|
||||
<td>{{ form_widget(form.group) }}{{ form_errors(form.group) }}</td>
|
||||
{% if form.eda_visibility is defined %}
|
||||
<td class="text-center">{{ form_widget(form.eda_visibility) }}</td>
|
||||
{% endif %}
|
||||
<td>
|
||||
<button type="button" class="btn btn-danger btn-sm order_btn_delete position-relative {% if form.parent.vars.allow_delete is defined and not form.parent.vars.allow_delete %}disabled{% endif %}"
|
||||
{{ collection.delete_btn() }} title="{% trans %}orderdetail.delete{% endtrans %}">
|
||||
|
||||
478
tests/Command/PopulateKicadCommandTest.php
Normal file
478
tests/Command/PopulateKicadCommandTest.php
Normal file
@@ -0,0 +1,478 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Command;
|
||||
|
||||
use App\Command\PopulateKicadCommand;
|
||||
use App\Entity\Parts\Category;
|
||||
use App\Entity\Parts\Footprint;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
final class PopulateKicadCommandTest extends KernelTestCase
|
||||
{
|
||||
private CommandTester $commandTester;
|
||||
private EntityManagerInterface $entityManager;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
$application = new Application(self::$kernel);
|
||||
|
||||
$command = $application->find('partdb:kicad:populate');
|
||||
$this->commandTester = new CommandTester($command);
|
||||
$this->entityManager = self::getContainer()->get(EntityManagerInterface::class);
|
||||
}
|
||||
|
||||
public function testListOption(): void
|
||||
{
|
||||
$this->commandTester->execute(['--list' => true]);
|
||||
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
// Should show footprints and categories tables
|
||||
$this->assertStringContainsString('Current Footprint KiCad Values', $output);
|
||||
$this->assertStringContainsString('Current Category KiCad Values', $output);
|
||||
$this->assertStringContainsString('ID', $output);
|
||||
$this->assertStringContainsString('Name', $output);
|
||||
|
||||
$this->assertEquals(0, $this->commandTester->getStatusCode());
|
||||
}
|
||||
|
||||
public function testDryRunDoesNotModifyDatabase(): void
|
||||
{
|
||||
// Create a test footprint without KiCad value
|
||||
$footprint = new Footprint();
|
||||
$footprint->setName('SOT-23');
|
||||
$this->entityManager->persist($footprint);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$footprintId = $footprint->getId();
|
||||
|
||||
// Run in dry-run mode
|
||||
$this->commandTester->execute(['--dry-run' => true, '--footprints' => true]);
|
||||
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertStringContainsString('DRY RUN MODE', $output);
|
||||
$this->assertStringContainsString('SOT-23', $output);
|
||||
|
||||
// Clear entity manager to force reload from DB
|
||||
$this->entityManager->clear();
|
||||
|
||||
// Verify footprint was NOT updated in the database
|
||||
$reloadedFootprint = $this->entityManager->find(Footprint::class, $footprintId);
|
||||
$this->assertNull($reloadedFootprint->getEdaInfo()->getKicadFootprint());
|
||||
|
||||
// Cleanup
|
||||
$this->entityManager->remove($reloadedFootprint);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
public function testFootprintMappingUpdatesCorrectly(): void
|
||||
{
|
||||
// Create test footprints
|
||||
$footprint1 = new Footprint();
|
||||
$footprint1->setName('SOT-23');
|
||||
|
||||
$footprint2 = new Footprint();
|
||||
$footprint2->setName('0805');
|
||||
|
||||
$footprint3 = new Footprint();
|
||||
$footprint3->setName('DIP-8');
|
||||
|
||||
$this->entityManager->persist($footprint1);
|
||||
$this->entityManager->persist($footprint2);
|
||||
$this->entityManager->persist($footprint3);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$ids = [$footprint1->getId(), $footprint2->getId(), $footprint3->getId()];
|
||||
|
||||
// Run the command
|
||||
$this->commandTester->execute(['--footprints' => true]);
|
||||
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertEquals(0, $this->commandTester->getStatusCode());
|
||||
|
||||
// Clear and reload
|
||||
$this->entityManager->clear();
|
||||
|
||||
// Verify mappings were applied
|
||||
$reloaded1 = $this->entityManager->find(Footprint::class, $ids[0]);
|
||||
$this->assertEquals('Package_TO_SOT_SMD:SOT-23', $reloaded1->getEdaInfo()->getKicadFootprint());
|
||||
|
||||
$reloaded2 = $this->entityManager->find(Footprint::class, $ids[1]);
|
||||
$this->assertEquals('Resistor_SMD:R_0805_2012Metric', $reloaded2->getEdaInfo()->getKicadFootprint());
|
||||
|
||||
$reloaded3 = $this->entityManager->find(Footprint::class, $ids[2]);
|
||||
$this->assertEquals('Package_DIP:DIP-8_W7.62mm', $reloaded3->getEdaInfo()->getKicadFootprint());
|
||||
|
||||
// Cleanup
|
||||
$this->entityManager->remove($reloaded1);
|
||||
$this->entityManager->remove($reloaded2);
|
||||
$this->entityManager->remove($reloaded3);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
public function testSkipsExistingValuesWithoutForce(): void
|
||||
{
|
||||
// Create footprint with existing value
|
||||
$footprint = new Footprint();
|
||||
$footprint->setName('SOT-23');
|
||||
$footprint->getEdaInfo()->setKicadFootprint('Custom:MyFootprint');
|
||||
$this->entityManager->persist($footprint);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$footprintId = $footprint->getId();
|
||||
|
||||
// Run without --force
|
||||
$this->commandTester->execute(['--footprints' => true]);
|
||||
|
||||
$this->entityManager->clear();
|
||||
|
||||
// Should keep original value
|
||||
$reloaded = $this->entityManager->find(Footprint::class, $footprintId);
|
||||
$this->assertEquals('Custom:MyFootprint', $reloaded->getEdaInfo()->getKicadFootprint());
|
||||
|
||||
// Cleanup
|
||||
$this->entityManager->remove($reloaded);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
public function testForceOptionOverwritesExistingValues(): void
|
||||
{
|
||||
// Create footprint with existing value
|
||||
$footprint = new Footprint();
|
||||
$footprint->setName('SOT-23');
|
||||
$footprint->getEdaInfo()->setKicadFootprint('Custom:MyFootprint');
|
||||
$this->entityManager->persist($footprint);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$footprintId = $footprint->getId();
|
||||
|
||||
// Run with --force
|
||||
$this->commandTester->execute(['--footprints' => true, '--force' => true]);
|
||||
|
||||
$this->entityManager->clear();
|
||||
|
||||
// Should overwrite with mapped value
|
||||
$reloaded = $this->entityManager->find(Footprint::class, $footprintId);
|
||||
$this->assertEquals('Package_TO_SOT_SMD:SOT-23', $reloaded->getEdaInfo()->getKicadFootprint());
|
||||
|
||||
// Cleanup
|
||||
$this->entityManager->remove($reloaded);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
public function testCategoryMappingUpdatesCorrectly(): void
|
||||
{
|
||||
// Create test categories
|
||||
$category1 = new Category();
|
||||
$category1->setName('Resistors');
|
||||
|
||||
$category2 = new Category();
|
||||
$category2->setName('LED Indicators');
|
||||
|
||||
$category3 = new Category();
|
||||
$category3->setName('Zener Diodes');
|
||||
|
||||
$this->entityManager->persist($category1);
|
||||
$this->entityManager->persist($category2);
|
||||
$this->entityManager->persist($category3);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$ids = [$category1->getId(), $category2->getId(), $category3->getId()];
|
||||
|
||||
// Run the command
|
||||
$this->commandTester->execute(['--categories' => true]);
|
||||
|
||||
$this->assertEquals(0, $this->commandTester->getStatusCode());
|
||||
|
||||
// Clear and reload
|
||||
$this->entityManager->clear();
|
||||
|
||||
// Verify mappings were applied (using pattern matching)
|
||||
$reloaded1 = $this->entityManager->find(Category::class, $ids[0]);
|
||||
$this->assertEquals('Device:R', $reloaded1->getEdaInfo()->getKicadSymbol());
|
||||
|
||||
$reloaded2 = $this->entityManager->find(Category::class, $ids[1]);
|
||||
$this->assertEquals('Device:LED', $reloaded2->getEdaInfo()->getKicadSymbol());
|
||||
|
||||
$reloaded3 = $this->entityManager->find(Category::class, $ids[2]);
|
||||
$this->assertEquals('Device:D_Zener', $reloaded3->getEdaInfo()->getKicadSymbol());
|
||||
|
||||
// Cleanup
|
||||
$this->entityManager->remove($reloaded1);
|
||||
$this->entityManager->remove($reloaded2);
|
||||
$this->entityManager->remove($reloaded3);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
public function testUnmappedFootprintsAreListed(): void
|
||||
{
|
||||
// Create footprint with no mapping
|
||||
$footprint = new Footprint();
|
||||
$footprint->setName('CustomPackage-XYZ');
|
||||
$this->entityManager->persist($footprint);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$footprintId = $footprint->getId();
|
||||
|
||||
// Run the command
|
||||
$this->commandTester->execute(['--footprints' => true]);
|
||||
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
// Should list the unmapped footprint
|
||||
$this->assertStringContainsString('No mapping found', $output);
|
||||
$this->assertStringContainsString('CustomPackage-XYZ', $output);
|
||||
|
||||
// Cleanup
|
||||
$this->entityManager->clear();
|
||||
$reloaded = $this->entityManager->find(Footprint::class, $footprintId);
|
||||
$this->entityManager->remove($reloaded);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
public function testMappingFileOverridesDefaults(): void
|
||||
{
|
||||
// Create a footprint that has a built-in mapping (SOT-23 -> Package_TO_SOT_SMD:SOT-23)
|
||||
$footprint = new Footprint();
|
||||
$footprint->setName('SOT-23');
|
||||
$this->entityManager->persist($footprint);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$footprintId = $footprint->getId();
|
||||
|
||||
// Create a temporary JSON mapping file that overrides SOT-23
|
||||
$mappingFile = sys_get_temp_dir() . '/partdb_test_mappings_' . uniqid() . '.json';
|
||||
file_put_contents($mappingFile, json_encode([
|
||||
'footprints' => [
|
||||
'SOT-23' => 'Custom_Library:Custom_SOT-23',
|
||||
],
|
||||
]));
|
||||
|
||||
try {
|
||||
// Run with mapping file
|
||||
$this->commandTester->execute(['--footprints' => true, '--mapping-file' => $mappingFile]);
|
||||
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertEquals(0, $this->commandTester->getStatusCode());
|
||||
$this->assertStringContainsString('custom footprint mappings', $output);
|
||||
|
||||
$this->entityManager->clear();
|
||||
|
||||
// Should use the custom mapping, not the built-in one
|
||||
$reloaded = $this->entityManager->find(Footprint::class, $footprintId);
|
||||
$this->assertEquals('Custom_Library:Custom_SOT-23', $reloaded->getEdaInfo()->getKicadFootprint());
|
||||
|
||||
// Cleanup
|
||||
$this->entityManager->remove($reloaded);
|
||||
$this->entityManager->flush();
|
||||
} finally {
|
||||
@unlink($mappingFile);
|
||||
}
|
||||
}
|
||||
|
||||
public function testMappingFileInvalidJsonReturnsFailure(): void
|
||||
{
|
||||
$mappingFile = sys_get_temp_dir() . '/partdb_test_invalid_' . uniqid() . '.json';
|
||||
file_put_contents($mappingFile, 'not valid json{{{');
|
||||
|
||||
try {
|
||||
$this->commandTester->execute(['--mapping-file' => $mappingFile]);
|
||||
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertEquals(1, $this->commandTester->getStatusCode());
|
||||
$this->assertStringContainsString('Invalid JSON', $output);
|
||||
} finally {
|
||||
@unlink($mappingFile);
|
||||
}
|
||||
}
|
||||
|
||||
public function testMappingFileNotFoundReturnsFailure(): void
|
||||
{
|
||||
$this->commandTester->execute(['--mapping-file' => '/nonexistent/path/mappings.json']);
|
||||
|
||||
$this->assertEquals(1, $this->commandTester->getStatusCode());
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertStringContainsString('Mapping file not found', $output);
|
||||
}
|
||||
|
||||
public function testFootprintAlternativeNameMatching(): void
|
||||
{
|
||||
// Create a footprint with a primary name that has no mapping,
|
||||
// but an alternative name that does
|
||||
$footprint = new Footprint();
|
||||
$footprint->setName('MyCustomSOT23');
|
||||
$footprint->setAlternativeNames('SOT-23, SOT23-3L');
|
||||
$this->entityManager->persist($footprint);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$footprintId = $footprint->getId();
|
||||
|
||||
$this->commandTester->execute(['--footprints' => true]);
|
||||
|
||||
$this->entityManager->clear();
|
||||
|
||||
// Should match via alternative name "SOT-23"
|
||||
$reloaded = $this->entityManager->find(Footprint::class, $footprintId);
|
||||
$this->assertEquals('Package_TO_SOT_SMD:SOT-23', $reloaded->getEdaInfo()->getKicadFootprint());
|
||||
|
||||
// Cleanup
|
||||
$this->entityManager->remove($reloaded);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
public function testCategoryAlternativeNameMatching(): void
|
||||
{
|
||||
// Create a category with a primary name that has no mapping,
|
||||
// but an alternative name that matches a pattern
|
||||
$category = new Category();
|
||||
$category->setName('SMD Components');
|
||||
$category->setAlternativeNames('Resistor SMD, Chip Resistors');
|
||||
$this->entityManager->persist($category);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$categoryId = $category->getId();
|
||||
|
||||
$this->commandTester->execute(['--categories' => true]);
|
||||
|
||||
$this->entityManager->clear();
|
||||
|
||||
// Should match via alternative name "Resistor SMD" matching pattern "Resistor"
|
||||
$reloaded = $this->entityManager->find(Category::class, $categoryId);
|
||||
$this->assertEquals('Device:R', $reloaded->getEdaInfo()->getKicadSymbol());
|
||||
|
||||
// Cleanup
|
||||
$this->entityManager->remove($reloaded);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
public function testBothFootprintsAndCategoriesUpdatedByDefault(): void
|
||||
{
|
||||
// Create one of each
|
||||
$footprint = new Footprint();
|
||||
$footprint->setName('TO-220');
|
||||
$this->entityManager->persist($footprint);
|
||||
|
||||
$category = new Category();
|
||||
$category->setName('Capacitors');
|
||||
$this->entityManager->persist($category);
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
$footprintId = $footprint->getId();
|
||||
$categoryId = $category->getId();
|
||||
|
||||
// Run without specific options (should do both)
|
||||
$this->commandTester->execute([]);
|
||||
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertStringContainsString('Updating Footprint Entities', $output);
|
||||
$this->assertStringContainsString('Updating Category Entities', $output);
|
||||
|
||||
$this->entityManager->clear();
|
||||
|
||||
// Both should be updated
|
||||
$reloadedFootprint = $this->entityManager->find(Footprint::class, $footprintId);
|
||||
$this->assertEquals('Package_TO_SOT_THT:TO-220-3_Vertical', $reloadedFootprint->getEdaInfo()->getKicadFootprint());
|
||||
|
||||
$reloadedCategory = $this->entityManager->find(Category::class, $categoryId);
|
||||
$this->assertEquals('Device:C', $reloadedCategory->getEdaInfo()->getKicadSymbol());
|
||||
|
||||
// Cleanup
|
||||
$this->entityManager->remove($reloadedFootprint);
|
||||
$this->entityManager->remove($reloadedCategory);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
public function testMappingFileWithBothFootprintsAndCategories(): void
|
||||
{
|
||||
$footprint = new Footprint();
|
||||
$footprint->setName('CustomPkg');
|
||||
$this->entityManager->persist($footprint);
|
||||
|
||||
$category = new Category();
|
||||
$category->setName('CustomType');
|
||||
$this->entityManager->persist($category);
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
$footprintId = $footprint->getId();
|
||||
$categoryId = $category->getId();
|
||||
|
||||
$mappingFile = sys_get_temp_dir() . '/partdb_test_both_' . uniqid() . '.json';
|
||||
file_put_contents($mappingFile, json_encode([
|
||||
'footprints' => [
|
||||
'CustomPkg' => 'Custom:Footprint',
|
||||
],
|
||||
'categories' => [
|
||||
'CustomType' => 'Custom:Symbol',
|
||||
],
|
||||
]));
|
||||
|
||||
try {
|
||||
$this->commandTester->execute(['--mapping-file' => $mappingFile]);
|
||||
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertEquals(0, $this->commandTester->getStatusCode());
|
||||
$this->assertStringContainsString('custom footprint mappings', $output);
|
||||
$this->assertStringContainsString('custom category mappings', $output);
|
||||
|
||||
$this->entityManager->clear();
|
||||
|
||||
$reloadedFp = $this->entityManager->find(Footprint::class, $footprintId);
|
||||
$this->assertEquals('Custom:Footprint', $reloadedFp->getEdaInfo()->getKicadFootprint());
|
||||
|
||||
$reloadedCat = $this->entityManager->find(Category::class, $categoryId);
|
||||
$this->assertEquals('Custom:Symbol', $reloadedCat->getEdaInfo()->getKicadSymbol());
|
||||
|
||||
// Cleanup
|
||||
$this->entityManager->remove($reloadedFp);
|
||||
$this->entityManager->remove($reloadedCat);
|
||||
$this->entityManager->flush();
|
||||
} finally {
|
||||
@unlink($mappingFile);
|
||||
}
|
||||
}
|
||||
|
||||
public function testMappingFileWithOnlyCategoriesSection(): void
|
||||
{
|
||||
$category = new Category();
|
||||
$category->setName('OnlyCatType');
|
||||
$this->entityManager->persist($category);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$categoryId = $category->getId();
|
||||
|
||||
$mappingFile = sys_get_temp_dir() . '/partdb_test_catonly_' . uniqid() . '.json';
|
||||
file_put_contents($mappingFile, json_encode([
|
||||
'categories' => [
|
||||
'OnlyCatType' => 'Custom:CatSymbol',
|
||||
],
|
||||
]));
|
||||
|
||||
try {
|
||||
$this->commandTester->execute(['--categories' => true, '--mapping-file' => $mappingFile]);
|
||||
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertEquals(0, $this->commandTester->getStatusCode());
|
||||
$this->assertStringContainsString('custom category mappings', $output);
|
||||
// Should NOT mention footprint mappings since they weren't in the file
|
||||
$this->assertStringNotContainsString('custom footprint mappings', $output);
|
||||
|
||||
$this->entityManager->clear();
|
||||
|
||||
$reloaded = $this->entityManager->find(Category::class, $categoryId);
|
||||
$this->assertEquals('Custom:CatSymbol', $reloaded->getEdaInfo()->getKicadSymbol());
|
||||
|
||||
$this->entityManager->remove($reloaded);
|
||||
$this->entityManager->flush();
|
||||
} finally {
|
||||
@unlink($mappingFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
171
tests/Controller/BatchEdaControllerTest.php
Normal file
171
tests/Controller/BatchEdaControllerTest.php
Normal file
@@ -0,0 +1,171 @@
|
||||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Controller;
|
||||
|
||||
use App\Entity\UserSystem\User;
|
||||
use PHPUnit\Framework\Attributes\Group;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
|
||||
#[Group("slow")]
|
||||
#[Group("DB")]
|
||||
final class BatchEdaControllerTest extends WebTestCase
|
||||
{
|
||||
private function loginAsUser($client, string $username): void
|
||||
{
|
||||
$entityManager = $client->getContainer()->get('doctrine')->getManager();
|
||||
$userRepository = $entityManager->getRepository(User::class);
|
||||
$user = $userRepository->findOneBy(['name' => $username]);
|
||||
|
||||
if (!$user) {
|
||||
$this->markTestSkipped("User {$username} not found");
|
||||
}
|
||||
|
||||
$client->loginUser($user);
|
||||
}
|
||||
|
||||
public function testBatchEdaPageLoads(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$this->loginAsUser($client, 'admin');
|
||||
|
||||
$client->request('GET', '/en/tools/batch_eda_edit', ['ids' => '1,2,3']);
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
}
|
||||
|
||||
public function testBatchEdaPageWithoutPartsRedirects(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$this->loginAsUser($client, 'admin');
|
||||
|
||||
$client->request('GET', '/en/tools/batch_eda_edit');
|
||||
|
||||
self::assertResponseRedirects();
|
||||
}
|
||||
|
||||
public function testBatchEdaPageWithoutPartsRedirectsToCustomUrl(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$this->loginAsUser($client, 'admin');
|
||||
|
||||
// Empty IDs with a custom redirect URL
|
||||
$client->request('GET', '/en/tools/batch_eda_edit', [
|
||||
'ids' => '',
|
||||
'_redirect' => '/en/parts',
|
||||
]);
|
||||
|
||||
self::assertResponseRedirects('/en/parts');
|
||||
}
|
||||
|
||||
public function testBatchEdaFormSubmission(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$this->loginAsUser($client, 'admin');
|
||||
|
||||
$crawler = $client->request('GET', '/en/tools/batch_eda_edit', ['ids' => '1,2']);
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
|
||||
$form = $crawler->selectButton('batch_eda[submit]')->form();
|
||||
$form['batch_eda[apply_reference_prefix]'] = true;
|
||||
$form['batch_eda[reference_prefix]'] = 'R';
|
||||
|
||||
$client->submit($form);
|
||||
|
||||
self::assertResponseRedirects();
|
||||
}
|
||||
|
||||
public function testBatchEdaFormSubmissionAppliesAllFields(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$this->loginAsUser($client, 'admin');
|
||||
|
||||
$crawler = $client->request('GET', '/en/tools/batch_eda_edit', ['ids' => '1,2']);
|
||||
self::assertResponseIsSuccessful();
|
||||
|
||||
$form = $crawler->selectButton('batch_eda[submit]')->form();
|
||||
|
||||
// Apply all text fields
|
||||
$form['batch_eda[apply_reference_prefix]'] = true;
|
||||
$form['batch_eda[reference_prefix]'] = 'C';
|
||||
$form['batch_eda[apply_value]'] = true;
|
||||
$form['batch_eda[value]'] = '100nF';
|
||||
$form['batch_eda[apply_kicad_symbol]'] = true;
|
||||
$form['batch_eda[kicad_symbol]'] = 'Device:C';
|
||||
$form['batch_eda[apply_kicad_footprint]'] = true;
|
||||
$form['batch_eda[kicad_footprint]'] = 'Capacitor_SMD:C_0402';
|
||||
|
||||
// Apply all tri-state checkboxes
|
||||
$form['batch_eda[apply_visibility]'] = true;
|
||||
$form['batch_eda[apply_exclude_from_bom]'] = true;
|
||||
$form['batch_eda[apply_exclude_from_board]'] = true;
|
||||
$form['batch_eda[apply_exclude_from_sim]'] = true;
|
||||
|
||||
$client->submit($form);
|
||||
|
||||
// All field branches in the controller are now exercised; redirect confirms success
|
||||
self::assertResponseRedirects();
|
||||
}
|
||||
|
||||
public function testBatchEdaFormSubmissionWithRedirectUrl(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$this->loginAsUser($client, 'admin');
|
||||
|
||||
$crawler = $client->request('GET', '/en/tools/batch_eda_edit', [
|
||||
'ids' => '1',
|
||||
'_redirect' => '/en/parts',
|
||||
]);
|
||||
self::assertResponseIsSuccessful();
|
||||
|
||||
$form = $crawler->selectButton('batch_eda[submit]')->form();
|
||||
$form['batch_eda[apply_reference_prefix]'] = true;
|
||||
$form['batch_eda[reference_prefix]'] = 'U';
|
||||
|
||||
$client->submit($form);
|
||||
|
||||
// Should redirect to the custom URL, not the default route
|
||||
self::assertResponseRedirects('/en/parts');
|
||||
}
|
||||
|
||||
public function testBatchEdaFormWithPartialFields(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$this->loginAsUser($client, 'admin');
|
||||
|
||||
$crawler = $client->request('GET', '/en/tools/batch_eda_edit', ['ids' => '3']);
|
||||
self::assertResponseIsSuccessful();
|
||||
|
||||
$form = $crawler->selectButton('batch_eda[submit]')->form();
|
||||
// Only apply value and kicad_footprint, leave other apply checkboxes unchecked
|
||||
$form['batch_eda[apply_value]'] = true;
|
||||
$form['batch_eda[value]'] = 'TestValue';
|
||||
$form['batch_eda[apply_kicad_footprint]'] = true;
|
||||
$form['batch_eda[kicad_footprint]'] = 'Package_SO:SOIC-8';
|
||||
|
||||
$client->submit($form);
|
||||
|
||||
// Redirect confirms the partial submission was processed
|
||||
self::assertResponseRedirects();
|
||||
}
|
||||
}
|
||||
@@ -148,6 +148,11 @@ final class KiCadApiControllerTest extends WebTestCase
|
||||
'value' => 'http://localhost/en/part/1/info',
|
||||
'visible' => 'False',
|
||||
),
|
||||
'Part-DB URL' =>
|
||||
array(
|
||||
'value' => 'http://localhost/en/part/1/info',
|
||||
'visible' => 'False',
|
||||
),
|
||||
'description' =>
|
||||
array(
|
||||
'value' => '',
|
||||
@@ -168,6 +173,11 @@ final class KiCadApiControllerTest extends WebTestCase
|
||||
'value' => '1',
|
||||
'visible' => 'False',
|
||||
),
|
||||
'Stock' =>
|
||||
array(
|
||||
'value' => '0',
|
||||
'visible' => 'False',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -177,20 +187,19 @@ final class KiCadApiControllerTest extends WebTestCase
|
||||
public function testPartDetailsPart2(): void
|
||||
{
|
||||
$client = $this->createClientWithCredentials();
|
||||
$client->request('GET', self::BASE_URL.'/parts/1.json');
|
||||
$client->request('GET', self::BASE_URL.'/parts/2.json');
|
||||
|
||||
//Response should still be successful, but the result should be empty
|
||||
self::assertResponseIsSuccessful();
|
||||
$content = $client->getResponse()->getContent();
|
||||
self::assertJson($content);
|
||||
|
||||
$data = json_decode($content, true);
|
||||
|
||||
//For part 2 things info should be taken from the category and footprint
|
||||
//For part 2, EDA info should be inherited from category and footprint (no part-level overrides)
|
||||
$expected = array (
|
||||
'id' => '1',
|
||||
'name' => 'Part 1',
|
||||
'symbolIdStr' => 'Part:1',
|
||||
'id' => '2',
|
||||
'name' => 'Part 2',
|
||||
'symbolIdStr' => 'Category:1',
|
||||
'exclude_from_bom' => 'False',
|
||||
'exclude_from_board' => 'True',
|
||||
'exclude_from_sim' => 'False',
|
||||
@@ -198,27 +207,32 @@ final class KiCadApiControllerTest extends WebTestCase
|
||||
array (
|
||||
'footprint' =>
|
||||
array (
|
||||
'value' => 'Part:1',
|
||||
'value' => 'Footprint:1',
|
||||
'visible' => 'False',
|
||||
),
|
||||
'reference' =>
|
||||
array (
|
||||
'value' => 'P',
|
||||
'value' => 'C',
|
||||
'visible' => 'True',
|
||||
),
|
||||
'value' =>
|
||||
array (
|
||||
'value' => 'Part 1',
|
||||
'value' => 'Part 2',
|
||||
'visible' => 'True',
|
||||
),
|
||||
'keywords' =>
|
||||
array (
|
||||
'value' => '',
|
||||
'value' => 'test, Test, Part2',
|
||||
'visible' => 'False',
|
||||
),
|
||||
'datasheet' =>
|
||||
array (
|
||||
'value' => 'http://localhost/en/part/1/info',
|
||||
'value' => 'http://localhost/en/part/2/info',
|
||||
'visible' => 'False',
|
||||
),
|
||||
'Part-DB URL' =>
|
||||
array (
|
||||
'value' => 'http://localhost/en/part/2/info',
|
||||
'visible' => 'False',
|
||||
),
|
||||
'description' =>
|
||||
@@ -231,14 +245,44 @@ final class KiCadApiControllerTest extends WebTestCase
|
||||
'value' => 'Node 1',
|
||||
'visible' => 'False',
|
||||
),
|
||||
'Manufacturer' =>
|
||||
array (
|
||||
'value' => 'Node 1',
|
||||
'visible' => 'False',
|
||||
),
|
||||
'Manufacturing Status' =>
|
||||
array (
|
||||
'value' => '',
|
||||
'value' => 'Active',
|
||||
'visible' => 'False',
|
||||
),
|
||||
'Part-DB Footprint' =>
|
||||
array (
|
||||
'value' => 'Node 1',
|
||||
'visible' => 'False',
|
||||
),
|
||||
'Mass' =>
|
||||
array (
|
||||
'value' => '100.2 g',
|
||||
'visible' => 'False',
|
||||
),
|
||||
'Part-DB ID' =>
|
||||
array (
|
||||
'value' => '1',
|
||||
'value' => '2',
|
||||
'visible' => 'False',
|
||||
),
|
||||
'Part-DB IPN' =>
|
||||
array (
|
||||
'value' => 'IPN123',
|
||||
'visible' => 'False',
|
||||
),
|
||||
'manf' =>
|
||||
array (
|
||||
'value' => 'Node 1',
|
||||
'visible' => 'False',
|
||||
),
|
||||
'Stock' =>
|
||||
array (
|
||||
'value' => '0',
|
||||
'visible' => 'False',
|
||||
),
|
||||
),
|
||||
@@ -247,4 +291,31 @@ final class KiCadApiControllerTest extends WebTestCase
|
||||
self::assertEquals($expected, $data);
|
||||
}
|
||||
|
||||
public function testCategoriesHasCacheHeaders(): void
|
||||
{
|
||||
$client = $this->createClientWithCredentials();
|
||||
$client->request('GET', self::BASE_URL.'/categories.json');
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
$response = $client->getResponse();
|
||||
self::assertNotNull($response->headers->get('ETag'));
|
||||
self::assertStringContainsString('max-age=', $response->headers->get('Cache-Control'));
|
||||
}
|
||||
|
||||
public function testConditionalRequestReturns304(): void
|
||||
{
|
||||
$client = $this->createClientWithCredentials();
|
||||
$client->request('GET', self::BASE_URL.'/categories.json');
|
||||
|
||||
$etag = $client->getResponse()->headers->get('ETag');
|
||||
self::assertNotNull($etag);
|
||||
|
||||
//Make a conditional request with the ETag
|
||||
$client->request('GET', self::BASE_URL.'/categories.json', [], [], [
|
||||
'HTTP_IF_NONE_MATCH' => $etag,
|
||||
]);
|
||||
|
||||
self::assertResponseStatusCodeSame(304);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -136,4 +136,44 @@ final class PartNormalizerTest extends WebTestCase
|
||||
$this->assertEqualsWithDelta(1.0, $priceDetail->getPriceRelatedQuantity(), PHP_FLOAT_EPSILON);
|
||||
$this->assertEqualsWithDelta(1.0, $priceDetail->getMinDiscountQuantity(), PHP_FLOAT_EPSILON);
|
||||
}
|
||||
|
||||
public function testDenormalizeEdaFields(): void
|
||||
{
|
||||
$input = [
|
||||
'name' => 'EDA Test Part',
|
||||
'kicad_symbol' => 'Device:R',
|
||||
'kicad_footprint' => 'Resistor_SMD:R_0805_2012Metric',
|
||||
'kicad_reference' => 'R',
|
||||
'kicad_value' => '10k',
|
||||
'eda_exclude_bom' => 'true',
|
||||
'eda_exclude_board' => 'false',
|
||||
];
|
||||
|
||||
$part = $this->service->denormalize($input, Part::class, 'json', ['groups' => ['import'], 'partdb_import' => true]);
|
||||
$this->assertInstanceOf(Part::class, $part);
|
||||
$this->assertSame('EDA Test Part', $part->getName());
|
||||
|
||||
$edaInfo = $part->getEdaInfo();
|
||||
$this->assertSame('Device:R', $edaInfo->getKicadSymbol());
|
||||
$this->assertSame('Resistor_SMD:R_0805_2012Metric', $edaInfo->getKicadFootprint());
|
||||
$this->assertSame('R', $edaInfo->getReferencePrefix());
|
||||
$this->assertSame('10k', $edaInfo->getValue());
|
||||
$this->assertTrue($edaInfo->getExcludeFromBom());
|
||||
$this->assertFalse($edaInfo->getExcludeFromBoard());
|
||||
}
|
||||
|
||||
public function testDenormalizeEdaFieldsEmptyValuesIgnored(): void
|
||||
{
|
||||
$input = [
|
||||
'name' => 'Part Without EDA',
|
||||
'kicad_symbol' => '',
|
||||
'kicad_footprint' => '',
|
||||
];
|
||||
|
||||
$part = $this->service->denormalize($input, Part::class, 'json', ['groups' => ['import'], 'partdb_import' => true]);
|
||||
|
||||
$edaInfo = $part->getEdaInfo();
|
||||
$this->assertNull($edaInfo->getKicadSymbol());
|
||||
$this->assertNull($edaInfo->getKicadFootprint());
|
||||
}
|
||||
}
|
||||
|
||||
604
tests/Services/EDA/KiCadHelperTest.php
Normal file
604
tests/Services/EDA/KiCadHelperTest.php
Normal file
@@ -0,0 +1,604 @@
|
||||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2024 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Services\EDA;
|
||||
|
||||
use App\Entity\Attachments\AttachmentType;
|
||||
use App\Entity\Attachments\PartAttachment;
|
||||
use App\Entity\Parameters\PartParameter;
|
||||
use App\Entity\Parts\Category;
|
||||
use App\Entity\Parts\Manufacturer;
|
||||
use App\Entity\Parts\Part;
|
||||
use App\Entity\Parts\PartLot;
|
||||
use App\Entity\Parts\StorageLocation;
|
||||
use App\Entity\Parts\Supplier;
|
||||
use App\Entity\PriceInformations\Orderdetail;
|
||||
use App\Services\EDA\KiCadHelper;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use PHPUnit\Framework\Attributes\Group;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
|
||||
#[Group('DB')]
|
||||
final class KiCadHelperTest extends KernelTestCase
|
||||
{
|
||||
private KiCadHelper $helper;
|
||||
private EntityManagerInterface $em;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
$this->helper = self::getContainer()->get(KiCadHelper::class);
|
||||
$this->em = self::getContainer()->get(EntityManagerInterface::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Part 1 (from fixtures) has no stock lots. Stock should be 0.
|
||||
*/
|
||||
public function testPartWithoutStockHasZeroStock(): void
|
||||
{
|
||||
$part = $this->em->find(Part::class, 1);
|
||||
$result = $this->helper->getKiCADPart($part);
|
||||
|
||||
self::assertArrayHasKey('Stock', $result['fields']);
|
||||
self::assertSame('0', $result['fields']['Stock']['value']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Part 3 (from fixtures) has a lot with amount=1.0 in StorageLocation 1.
|
||||
*/
|
||||
public function testPartWithStockShowsCorrectQuantity(): void
|
||||
{
|
||||
$part = $this->em->find(Part::class, 3);
|
||||
$result = $this->helper->getKiCADPart($part);
|
||||
|
||||
self::assertArrayHasKey('Stock', $result['fields']);
|
||||
self::assertSame('1', $result['fields']['Stock']['value']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Part 3 has a lot with amount > 0 in StorageLocation "Node 1".
|
||||
*/
|
||||
public function testPartWithStorageLocationShowsLocation(): void
|
||||
{
|
||||
$part = $this->em->find(Part::class, 3);
|
||||
$result = $this->helper->getKiCADPart($part);
|
||||
|
||||
self::assertArrayHasKey('Storage Location', $result['fields']);
|
||||
self::assertSame('Node 1', $result['fields']['Storage Location']['value']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Part 1 has no stock lots, so no storage location should be shown.
|
||||
*/
|
||||
public function testPartWithoutStorageLocationOmitsField(): void
|
||||
{
|
||||
$part = $this->em->find(Part::class, 1);
|
||||
$result = $this->helper->getKiCADPart($part);
|
||||
|
||||
self::assertArrayNotHasKey('Storage Location', $result['fields']);
|
||||
}
|
||||
|
||||
/**
|
||||
* All parts should have a "Part-DB URL" field pointing to the part info page.
|
||||
*/
|
||||
public function testPartDbUrlFieldIsPresent(): void
|
||||
{
|
||||
$part = $this->em->find(Part::class, 1);
|
||||
$result = $this->helper->getKiCADPart($part);
|
||||
|
||||
self::assertArrayHasKey('Part-DB URL', $result['fields']);
|
||||
self::assertStringContainsString('/part/1/info', $result['fields']['Part-DB URL']['value']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Part 1 has no attachments, so the datasheet should fall back to the Part-DB page URL.
|
||||
*/
|
||||
public function testDatasheetFallbackToPartUrlWhenNoAttachments(): void
|
||||
{
|
||||
$part = $this->em->find(Part::class, 1);
|
||||
$result = $this->helper->getKiCADPart($part);
|
||||
|
||||
// With no attachments, datasheet should equal Part-DB URL
|
||||
self::assertSame(
|
||||
$result['fields']['Part-DB URL']['value'],
|
||||
$result['fields']['datasheet']['value']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Part 3 has attachments but none named "datasheet" and none are PDFs,
|
||||
* so the datasheet should fall back to the Part-DB page URL.
|
||||
*/
|
||||
public function testDatasheetFallbackWhenNoMatchingAttachments(): void
|
||||
{
|
||||
$part = $this->em->find(Part::class, 3);
|
||||
$result = $this->helper->getKiCADPart($part);
|
||||
|
||||
// "TestAttachment" (url: www.foo.bar) and "Test2" (internal: invalid) don't match datasheet patterns
|
||||
self::assertSame(
|
||||
$result['fields']['Part-DB URL']['value'],
|
||||
$result['fields']['datasheet']['value']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that an attachment with type name containing "Datasheet" is found.
|
||||
*/
|
||||
public function testDatasheetFoundByAttachmentTypeName(): void
|
||||
{
|
||||
$category = $this->em->find(Category::class, 1);
|
||||
|
||||
// Create an attachment type named "Datasheets"
|
||||
$datasheetType = new AttachmentType();
|
||||
$datasheetType->setName('Datasheets');
|
||||
$this->em->persist($datasheetType);
|
||||
|
||||
// Create a part with a datasheet attachment
|
||||
$part = new Part();
|
||||
$part->setName('Part with Datasheet Type');
|
||||
$part->setCategory($category);
|
||||
|
||||
$attachment = new PartAttachment();
|
||||
$attachment->setName('Component Spec');
|
||||
$attachment->setURL('https://example.com/spec.pdf');
|
||||
$attachment->setAttachmentType($datasheetType);
|
||||
$part->addAttachment($attachment);
|
||||
|
||||
$this->em->persist($part);
|
||||
$this->em->flush();
|
||||
|
||||
$result = $this->helper->getKiCADPart($part);
|
||||
|
||||
self::assertSame('https://example.com/spec.pdf', $result['fields']['datasheet']['value']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that an attachment named "Datasheet" is found (regardless of type).
|
||||
*/
|
||||
public function testDatasheetFoundByAttachmentName(): void
|
||||
{
|
||||
$category = $this->em->find(Category::class, 1);
|
||||
$attachmentType = $this->em->find(AttachmentType::class, 1);
|
||||
|
||||
$part = new Part();
|
||||
$part->setName('Part with Named Datasheet');
|
||||
$part->setCategory($category);
|
||||
|
||||
$attachment = new PartAttachment();
|
||||
$attachment->setName('Datasheet BC547');
|
||||
$attachment->setURL('https://example.com/bc547-datasheet.pdf');
|
||||
$attachment->setAttachmentType($attachmentType);
|
||||
$part->addAttachment($attachment);
|
||||
|
||||
$this->em->persist($part);
|
||||
$this->em->flush();
|
||||
|
||||
$result = $this->helper->getKiCADPart($part);
|
||||
|
||||
self::assertSame('https://example.com/bc547-datasheet.pdf', $result['fields']['datasheet']['value']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that a PDF attachment is used as fallback when no "datasheet" match exists.
|
||||
*/
|
||||
public function testDatasheetFallbackToFirstPdfAttachment(): void
|
||||
{
|
||||
$category = $this->em->find(Category::class, 1);
|
||||
$attachmentType = $this->em->find(AttachmentType::class, 1);
|
||||
|
||||
$part = new Part();
|
||||
$part->setName('Part with PDF');
|
||||
$part->setCategory($category);
|
||||
|
||||
// Non-PDF attachment first
|
||||
$attachment1 = new PartAttachment();
|
||||
$attachment1->setName('Photo');
|
||||
$attachment1->setURL('https://example.com/photo.jpg');
|
||||
$attachment1->setAttachmentType($attachmentType);
|
||||
$part->addAttachment($attachment1);
|
||||
|
||||
// PDF attachment second
|
||||
$attachment2 = new PartAttachment();
|
||||
$attachment2->setName('Specifications');
|
||||
$attachment2->setURL('https://example.com/specs.pdf');
|
||||
$attachment2->setAttachmentType($attachmentType);
|
||||
$part->addAttachment($attachment2);
|
||||
|
||||
$this->em->persist($part);
|
||||
$this->em->flush();
|
||||
|
||||
$result = $this->helper->getKiCADPart($part);
|
||||
|
||||
// Should find the .pdf file as fallback
|
||||
self::assertSame('https://example.com/specs.pdf', $result['fields']['datasheet']['value']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that a "data sheet" variant (with space) is also matched by name.
|
||||
*/
|
||||
public function testDatasheetMatchesDataSheetWithSpace(): void
|
||||
{
|
||||
$category = $this->em->find(Category::class, 1);
|
||||
$attachmentType = $this->em->find(AttachmentType::class, 1);
|
||||
|
||||
$part = new Part();
|
||||
$part->setName('Part with Data Sheet');
|
||||
$part->setCategory($category);
|
||||
|
||||
$attachment = new PartAttachment();
|
||||
$attachment->setName('Data Sheet v1.2');
|
||||
$attachment->setURL('https://example.com/data-sheet.pdf');
|
||||
$attachment->setAttachmentType($attachmentType);
|
||||
$part->addAttachment($attachment);
|
||||
|
||||
$this->em->persist($part);
|
||||
$this->em->flush();
|
||||
|
||||
$result = $this->helper->getKiCADPart($part);
|
||||
|
||||
self::assertSame('https://example.com/data-sheet.pdf', $result['fields']['datasheet']['value']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test stock calculation excludes expired lots.
|
||||
*/
|
||||
public function testStockExcludesExpiredLots(): void
|
||||
{
|
||||
$category = $this->em->find(Category::class, 1);
|
||||
|
||||
$part = new Part();
|
||||
$part->setName('Part with Expired Stock');
|
||||
$part->setCategory($category);
|
||||
|
||||
// Active lot
|
||||
$lot1 = new PartLot();
|
||||
$lot1->setAmount(10.0);
|
||||
$part->addPartLot($lot1);
|
||||
|
||||
// Expired lot
|
||||
$lot2 = new PartLot();
|
||||
$lot2->setAmount(5.0);
|
||||
$lot2->setExpirationDate(new \DateTimeImmutable('-1 day'));
|
||||
$part->addPartLot($lot2);
|
||||
|
||||
$this->em->persist($part);
|
||||
$this->em->flush();
|
||||
|
||||
$result = $this->helper->getKiCADPart($part);
|
||||
|
||||
// Only the active lot should be counted
|
||||
self::assertSame('10', $result['fields']['Stock']['value']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test stock calculation excludes lots with unknown stock.
|
||||
*/
|
||||
public function testStockExcludesUnknownLots(): void
|
||||
{
|
||||
$category = $this->em->find(Category::class, 1);
|
||||
|
||||
$part = new Part();
|
||||
$part->setName('Part with Unknown Stock');
|
||||
$part->setCategory($category);
|
||||
|
||||
// Known lot
|
||||
$lot1 = new PartLot();
|
||||
$lot1->setAmount(7.0);
|
||||
$part->addPartLot($lot1);
|
||||
|
||||
// Unknown lot
|
||||
$lot2 = new PartLot();
|
||||
$lot2->setInstockUnknown(true);
|
||||
$part->addPartLot($lot2);
|
||||
|
||||
$this->em->persist($part);
|
||||
$this->em->flush();
|
||||
|
||||
$result = $this->helper->getKiCADPart($part);
|
||||
|
||||
self::assertSame('7', $result['fields']['Stock']['value']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test stock sums across multiple lots.
|
||||
*/
|
||||
public function testStockSumsMultipleLots(): void
|
||||
{
|
||||
$category = $this->em->find(Category::class, 1);
|
||||
$location1 = $this->em->find(StorageLocation::class, 1);
|
||||
$location2 = $this->em->find(StorageLocation::class, 2);
|
||||
|
||||
$part = new Part();
|
||||
$part->setName('Part in Multiple Locations');
|
||||
$part->setCategory($category);
|
||||
|
||||
$lot1 = new PartLot();
|
||||
$lot1->setAmount(15.0);
|
||||
$lot1->setStorageLocation($location1);
|
||||
$part->addPartLot($lot1);
|
||||
|
||||
$lot2 = new PartLot();
|
||||
$lot2->setAmount(25.0);
|
||||
$lot2->setStorageLocation($location2);
|
||||
$part->addPartLot($lot2);
|
||||
|
||||
$this->em->persist($part);
|
||||
$this->em->flush();
|
||||
|
||||
$result = $this->helper->getKiCADPart($part);
|
||||
|
||||
self::assertSame('40', $result['fields']['Stock']['value']);
|
||||
self::assertArrayHasKey('Storage Location', $result['fields']);
|
||||
// Both locations should be listed
|
||||
self::assertStringContainsString('Node 1', $result['fields']['Storage Location']['value']);
|
||||
self::assertStringContainsString('Node 2', $result['fields']['Storage Location']['value']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that the Stock field visibility is "False" (not visible in schematic by default).
|
||||
*/
|
||||
public function testStockFieldIsNotVisible(): void
|
||||
{
|
||||
$part = $this->em->find(Part::class, 1);
|
||||
$result = $this->helper->getKiCADPart($part);
|
||||
|
||||
self::assertSame('False', $result['fields']['Stock']['visible']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that a parameter with eda_visibility=true appears in the KiCad fields.
|
||||
*/
|
||||
public function testParameterWithEdaVisibilityAppearsInFields(): void
|
||||
{
|
||||
$category = $this->em->find(Category::class, 1);
|
||||
|
||||
$part = new Part();
|
||||
$part->setName('Part with Exported Parameter');
|
||||
$part->setCategory($category);
|
||||
|
||||
$param = new PartParameter();
|
||||
$param->setName('Voltage Rating');
|
||||
$param->setValueTypical(3.3);
|
||||
$param->setUnit('V');
|
||||
$param->setEdaVisibility(true);
|
||||
$part->addParameter($param);
|
||||
|
||||
$this->em->persist($part);
|
||||
$this->em->flush();
|
||||
|
||||
$result = $this->helper->getKiCADPart($part);
|
||||
|
||||
self::assertArrayHasKey('Voltage Rating', $result['fields']);
|
||||
self::assertSame('3.3 V', $result['fields']['Voltage Rating']['value']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that a parameter with eda_visibility=false does NOT appear in the KiCad fields.
|
||||
*/
|
||||
public function testParameterWithoutEdaVisibilityDoesNotAppear(): void
|
||||
{
|
||||
$category = $this->em->find(Category::class, 1);
|
||||
|
||||
$part = new Part();
|
||||
$part->setName('Part with Non-exported Parameter');
|
||||
$part->setCategory($category);
|
||||
|
||||
$param = new PartParameter();
|
||||
$param->setName('Internal Note');
|
||||
$param->setValueText('for testing only');
|
||||
$param->setEdaVisibility(false);
|
||||
$part->addParameter($param);
|
||||
|
||||
$this->em->persist($part);
|
||||
$this->em->flush();
|
||||
|
||||
$result = $this->helper->getKiCADPart($part);
|
||||
|
||||
self::assertArrayNotHasKey('Internal Note', $result['fields']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that a parameter with eda_visibility=null (system default) does NOT appear in the KiCad fields.
|
||||
*/
|
||||
public function testParameterWithNullEdaVisibilityDoesNotAppear(): void
|
||||
{
|
||||
$category = $this->em->find(Category::class, 1);
|
||||
|
||||
$part = new Part();
|
||||
$part->setName('Part with Default Parameter');
|
||||
$part->setCategory($category);
|
||||
|
||||
$param = new PartParameter();
|
||||
$param->setName('Default Param');
|
||||
$param->setValueText('some value');
|
||||
// eda_visibility is null by default
|
||||
$part->addParameter($param);
|
||||
|
||||
$this->em->persist($part);
|
||||
$this->em->flush();
|
||||
|
||||
$result = $this->helper->getKiCADPart($part);
|
||||
|
||||
self::assertArrayNotHasKey('Default Param', $result['fields']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that an exported parameter named "description" does NOT overwrite the hardcoded description field.
|
||||
*/
|
||||
public function testExportedParameterDoesNotOverwriteHardcodedField(): void
|
||||
{
|
||||
$category = $this->em->find(Category::class, 1);
|
||||
|
||||
$part = new Part();
|
||||
$part->setName('Part with Conflicting Parameter');
|
||||
$part->setDescription('The real description');
|
||||
$part->setCategory($category);
|
||||
|
||||
$param = new PartParameter();
|
||||
$param->setName('description');
|
||||
$param->setValueText('should not overwrite');
|
||||
$param->setEdaVisibility(true);
|
||||
$part->addParameter($param);
|
||||
|
||||
$this->em->persist($part);
|
||||
$this->em->flush();
|
||||
|
||||
$result = $this->helper->getKiCADPart($part);
|
||||
|
||||
// The hardcoded description should win
|
||||
self::assertSame('The real description', $result['fields']['description']['value']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that orderdetails without explicit eda_visibility are all exported (backward compat).
|
||||
*/
|
||||
public function testOrderdetailsExportedWhenNoEdaVisibilitySet(): void
|
||||
{
|
||||
$category = $this->em->find(Category::class, 1);
|
||||
|
||||
$supplier = new Supplier();
|
||||
$supplier->setName('TestSupplier');
|
||||
$this->em->persist($supplier);
|
||||
|
||||
$part = new Part();
|
||||
$part->setName('Part with Supplier');
|
||||
$part->setCategory($category);
|
||||
|
||||
$od = new Orderdetail();
|
||||
$od->setSupplier($supplier);
|
||||
$od->setSupplierpartnr('TS-001');
|
||||
// eda_visibility is null (default)
|
||||
$part->addOrderdetail($od);
|
||||
|
||||
$this->em->persist($part);
|
||||
$this->em->flush();
|
||||
|
||||
$result = $this->helper->getKiCADPart($part);
|
||||
|
||||
// Should export since no explicit flags are set (backward compat)
|
||||
self::assertArrayHasKey('TestSupplier SPN', $result['fields']);
|
||||
self::assertSame('TS-001', $result['fields']['TestSupplier SPN']['value']);
|
||||
// KiCost field should also be present
|
||||
self::assertArrayHasKey('testsupplier#', $result['fields']);
|
||||
self::assertSame('TS-001', $result['fields']['testsupplier#']['value']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that only orderdetails with eda_visibility=true are exported when explicit flags exist.
|
||||
*/
|
||||
public function testOrderdetailsFilteredByExplicitEdaVisibility(): void
|
||||
{
|
||||
$category = $this->em->find(Category::class, 1);
|
||||
|
||||
$supplier1 = new Supplier();
|
||||
$supplier1->setName('VisibleSupplier');
|
||||
$this->em->persist($supplier1);
|
||||
|
||||
$supplier2 = new Supplier();
|
||||
$supplier2->setName('HiddenSupplier');
|
||||
$this->em->persist($supplier2);
|
||||
|
||||
$part = new Part();
|
||||
$part->setName('Part with Mixed Visibility');
|
||||
$part->setCategory($category);
|
||||
|
||||
$od1 = new Orderdetail();
|
||||
$od1->setSupplier($supplier1);
|
||||
$od1->setSupplierpartnr('VIS-001');
|
||||
$od1->setEdaVisibility(true);
|
||||
$part->addOrderdetail($od1);
|
||||
|
||||
$od2 = new Orderdetail();
|
||||
$od2->setSupplier($supplier2);
|
||||
$od2->setSupplierpartnr('HID-001');
|
||||
$od2->setEdaVisibility(false);
|
||||
$part->addOrderdetail($od2);
|
||||
|
||||
$this->em->persist($part);
|
||||
$this->em->flush();
|
||||
|
||||
$result = $this->helper->getKiCADPart($part);
|
||||
|
||||
// Visible supplier should be exported
|
||||
self::assertArrayHasKey('VisibleSupplier SPN', $result['fields']);
|
||||
self::assertSame('VIS-001', $result['fields']['VisibleSupplier SPN']['value']);
|
||||
|
||||
// Hidden supplier should NOT be exported
|
||||
self::assertArrayNotHasKey('HiddenSupplier SPN', $result['fields']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that manufacturer fields (manf, manf#) are always exported.
|
||||
*/
|
||||
public function testManufacturerFieldsExported(): void
|
||||
{
|
||||
$category = $this->em->find(Category::class, 1);
|
||||
|
||||
$manufacturer = new Manufacturer();
|
||||
$manufacturer->setName('Acme Corp');
|
||||
$this->em->persist($manufacturer);
|
||||
|
||||
$part = new Part();
|
||||
$part->setName('Acme Widget');
|
||||
$part->setCategory($category);
|
||||
$part->setManufacturer($manufacturer);
|
||||
$part->setManufacturerProductNumber('ACM-1234');
|
||||
|
||||
$this->em->persist($part);
|
||||
$this->em->flush();
|
||||
|
||||
$result = $this->helper->getKiCADPart($part);
|
||||
|
||||
self::assertArrayHasKey('manf', $result['fields']);
|
||||
self::assertSame('Acme Corp', $result['fields']['manf']['value']);
|
||||
self::assertArrayHasKey('manf#', $result['fields']);
|
||||
self::assertSame('ACM-1234', $result['fields']['manf#']['value']);
|
||||
self::assertArrayHasKey('Manufacturer', $result['fields']);
|
||||
self::assertArrayHasKey('MPN', $result['fields']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that a parameter with empty name is not exported even with eda_visibility=true.
|
||||
*/
|
||||
public function testParameterWithEmptyNameIsSkipped(): void
|
||||
{
|
||||
$category = $this->em->find(Category::class, 1);
|
||||
|
||||
$part = new Part();
|
||||
$part->setName('Part with Empty Param Name');
|
||||
$part->setCategory($category);
|
||||
|
||||
$param = new PartParameter();
|
||||
$param->setName('');
|
||||
$param->setValueText('some value');
|
||||
$param->setEdaVisibility(true);
|
||||
$part->addParameter($param);
|
||||
|
||||
$this->em->persist($part);
|
||||
$this->em->flush();
|
||||
|
||||
$result = $this->helper->getKiCADPart($part);
|
||||
|
||||
// Empty-named parameter should not appear
|
||||
self::assertArrayNotHasKey('', $result['fields']);
|
||||
}
|
||||
}
|
||||
@@ -641,6 +641,12 @@ Subelemente werden beim Löschen nach oben verschoben.</target>
|
||||
<target>Sektion</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="8rz303Z" name="specifications.eda_visibility.help">
|
||||
<segment state="translated">
|
||||
<source>specifications.eda_visibility.help</source>
|
||||
<target>Diesen Parameter als EDA Feld exportieren</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="XclPxI9" name="specification.create">
|
||||
<segment state="translated">
|
||||
<source>specification.create</source>
|
||||
@@ -2923,6 +2929,42 @@ Wenn Sie dies fehlerhafterweise gemacht haben oder ein Computer nicht mehr vertr
|
||||
<target>Dateianhänge</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="f3Dggp6" name="part.table.eda_status">
|
||||
<segment state="translated">
|
||||
<source>part.table.eda_status</source>
|
||||
<target>EDA</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="Q_myBuD" name="eda.status.symbol_set">
|
||||
<segment state="translated">
|
||||
<source>eda.status.symbol_set</source>
|
||||
<target>KiCad Symbol gesetzt</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="QGLfvit" name="eda.status.footprint_set">
|
||||
<segment state="translated">
|
||||
<source>eda.status.footprint_set</source>
|
||||
<target>KiCad Footprint gesetzt</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="hkze9M." name="eda.status.reference_set">
|
||||
<segment state="translated">
|
||||
<source>eda.status.reference_set</source>
|
||||
<target>Referenzpräfix gesetzt</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="OTXbAfL" name="eda.status.complete">
|
||||
<segment state="translated">
|
||||
<source>eda.status.complete</source>
|
||||
<target>EDA Felder vollständig (Symbol, Footprint, Referenz)</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="z9E5RB." name="eda.status.partial">
|
||||
<segment state="translated">
|
||||
<source>eda.status.partial</source>
|
||||
<target>EDA Felder teilweise gesetzt</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bMkafCp" name="flash.login_successful">
|
||||
<segment state="translated">
|
||||
<source>flash.login_successful</source>
|
||||
@@ -3265,6 +3307,12 @@ Wenn Sie dies fehlerhafterweise gemacht haben oder ein Computer nicht mehr vertr
|
||||
<target>Nicht mehr lieferbar</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="6H0WQWq" name="orderdetails.edit.eda_visibility">
|
||||
<segment state="translated">
|
||||
<source>orderdetails.edit.eda_visibility</source>
|
||||
<target>Sichtbar in EDA</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="ZsO5AKM" name="orderdetails.edit.supplierpartnr.placeholder">
|
||||
<segment state="translated">
|
||||
<source>orderdetails.edit.supplierpartnr.placeholder</source>
|
||||
@@ -9499,6 +9547,12 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön
|
||||
<target>EIGP 114 Barcode (z. B. der Datamatrix-Code auf Digikey und Mouser Bauteilen)</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="BnqcKWx" name="scan_dialog.mode.lcsc">
|
||||
<segment state="translated">
|
||||
<source>scan_dialog.mode.lcsc</source>
|
||||
<target>LCSC.com Barcode</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="QSMS_Bd" name="scan_dialog.info_mode">
|
||||
<segment state="translated">
|
||||
<source>scan_dialog.info_mode</source>
|
||||
@@ -9511,6 +9565,24 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön
|
||||
<target>Dekodierte Informationen</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="kQnodbA" name="label_scanner.target_found">
|
||||
<segment state="translated">
|
||||
<source>label_scanner.target_found</source>
|
||||
<target>Artikel in Datenbank gefunden</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="7Arfw2q" name="label_scanner.scan_result.title">
|
||||
<segment state="translated">
|
||||
<source>label_scanner.scan_result.title</source>
|
||||
<target>Scan-Ergebnis</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="PTh4EK_" name="label_scanner.no_locations">
|
||||
<segment state="translated">
|
||||
<source>label_scanner.no_locations</source>
|
||||
<target>Bauteil ist an keinem Standort gespeichert.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="nmXQWcS" name="label_generator.edit_profiles">
|
||||
<segment state="translated">
|
||||
<source>label_generator.edit_profiles</source>
|
||||
@@ -9944,6 +10016,18 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön
|
||||
<target>Dieser Wert bestimmt die Tiefe des Kategoriebaums, der in KiCad sichtbar ist. 0 bedeutet, dass nur die Kategorien der obersten Ebene sichtbar sind. Setzen Sie den Wert auf > 0, um weitere Ebenen anzuzeigen. Setzen Sie den Wert auf -1, um alle Teile der Part-DB innerhalb einer einzigen Kategorie in KiCad anzuzeigen.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="X5.rQdO" name="settings.misc.kicad_eda.datasheet_link">
|
||||
<segment state="translated">
|
||||
<source>settings.misc.kicad_eda.datasheet_link</source>
|
||||
<target>Datasheet Feld verlinkt auf PDF</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="Fm1QTCs" name="settings.misc.kicad_eda.datasheet_link.help">
|
||||
<segment state="translated">
|
||||
<source>settings.misc.kicad_eda.datasheet_link.help</source>
|
||||
<target>Wenn aktiviert, verlinkt das Datenblatt-Feld in KiCad auf die tatsächliche PDF-Datei (sofern gefunden). Wenn deaktiviert, führt es stattdessen zur Part-DB-Seite. Der Link zur Part-DB-Seite ist immer als separates "Part-DB URL"-Feld verfügbar.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="VwvmcWE" name="settings.behavior.sidebar">
|
||||
<segment state="translated">
|
||||
<source>settings.behavior.sidebar</source>
|
||||
@@ -10286,6 +10370,24 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön
|
||||
<target>Zeigen Sie die Bildoverlay mit den Details zum Anhang an, wenn Sie mit der Maus über die Teilebildgalerie fahren.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="0iYdzdk" name="settings.behavior.keybindings">
|
||||
<segment state="translated">
|
||||
<source>settings.behavior.keybindings</source>
|
||||
<target>Tastenkürzel</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="_x13bMa" name="settings.behavior.keybindings.enable_special_characters">
|
||||
<segment state="translated">
|
||||
<source>settings.behavior.keybindings.enable_special_characters</source>
|
||||
<target>Tastenkürzel für Sonderzeichen aktivieren</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="Af8Zzqr" name="settings.behavior.keybindings.enable_special_characters.help">
|
||||
<segment state="translated">
|
||||
<source>settings.behavior.keybindings.enable_special_characters.help</source>
|
||||
<target>Aktivieren Sie Alt+Taste-Kürzel zum Einfügen von Sonderzeichen (griechische Buchstaben, mathematische Symbole etc.) in Texteingabefeldern. Deaktivieren, wenn die Kürzel mit Ihrer Tastaturbelegung oder Systemkürzeln kollidieren.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="ALfPkeR" name="perm.config.change_system_settings">
|
||||
<segment state="translated">
|
||||
<source>perm.config.change_system_settings</source>
|
||||
@@ -10910,6 +11012,84 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön
|
||||
<target>Massenimport von Datenquellen</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="VtS1yT7" name="part_list.action.group.eda">
|
||||
<segment state="translated">
|
||||
<source>part_list.action.group.eda</source>
|
||||
<target>EDA / KiCad</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="swU1Rp2" name="part_list.action.batch_edit_eda">
|
||||
<segment state="translated">
|
||||
<source>part_list.action.batch_edit_eda</source>
|
||||
<target>EDA-Felder in Stapel bearbeiten</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="ZaS_Hg5" name="batch_eda.title">
|
||||
<segment state="translated">
|
||||
<source>batch_eda.title</source>
|
||||
<target>EDA-Felder in Stapel bearbeiten</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="k2FDo7A" name="batch_eda.description">
|
||||
<segment state="translated">
|
||||
<source>batch_eda.description</source>
|
||||
<target>Bearbeiten Sie EDA/KiCad-Felder für %count% ausgewählte Bauteile. Aktivieren Sie das Kontrollkästchen "Anwenden" neben jedem Feld, das Sie ändern möchten.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="WVHbic3" name="batch_eda.show_parts">
|
||||
<segment state="translated">
|
||||
<source>batch_eda.show_parts</source>
|
||||
<target>Ausgewählte Bauteile anzeigen</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="ubQd6G4" name="batch_eda.apply_hint">
|
||||
<segment state="translated">
|
||||
<source>batch_eda.apply_hint</source>
|
||||
<target>Nur Felder mit aktiviertem Kontrollkästchen "Anwenden" werden geändert. Deaktivierte Felder bleiben unverändert.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="w.5FGYL" name="batch_eda.apply">
|
||||
<segment state="translated">
|
||||
<source>batch_eda.apply</source>
|
||||
<target>Anwenden</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="9EmHp5C" name="batch_eda.field">
|
||||
<segment state="translated">
|
||||
<source>batch_eda.field</source>
|
||||
<target>Feld</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="xHaCnEQ" name="batch_eda.value">
|
||||
<segment state="translated">
|
||||
<source>batch_eda.value</source>
|
||||
<target>Wert</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="PLqIBvC" name="batch_eda.submit">
|
||||
<segment state="translated">
|
||||
<source>batch_eda.submit</source>
|
||||
<target>Auf ausgewählte Bauteile anwenden</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="5nO7Fpq" name="batch_eda.cancel">
|
||||
<segment state="translated">
|
||||
<source>batch_eda.cancel</source>
|
||||
<target>Abbrechen</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="vhlPBNU" name="batch_eda.success">
|
||||
<segment state="translated">
|
||||
<source>batch_eda.success</source>
|
||||
<target>EDA-Felder erfolgreich aktualisiert.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="2fMo760" name="batch_eda.no_parts_selected">
|
||||
<segment state="translated">
|
||||
<source>batch_eda.no_parts_selected</source>
|
||||
<target>Für die Stapelbearbeitung wurden keine Bauteile ausgewählt.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="yzpXFkB" name="info_providers.bulk_import.step1.spn_recommendation">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.step1.spn_recommendation</source>
|
||||
@@ -12507,5 +12687,137 @@ Buerklin-API-Authentication-Server:
|
||||
<target>Letzte Inventur</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="GNWhoTW" name="part.table.eda_reference">
|
||||
<segment state="translated">
|
||||
<source>part.table.eda_reference</source>
|
||||
<target>EDA-Referenz</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="tW4yCbf" name="part.table.eda_value">
|
||||
<segment state="translated">
|
||||
<source>part.table.eda_value</source>
|
||||
<target>EDA-Wert</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="s1pgReC" name="settings.misc.kicad_eda.default_parameter_visibility">
|
||||
<segment state="translated">
|
||||
<source>settings.misc.kicad_eda.default_parameter_visibility</source>
|
||||
<target>Standard EDA-Sichtbarkeit für Parameter</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="Z78QunV" name="settings.misc.kicad_eda.default_parameter_visibility.help">
|
||||
<segment state="translated">
|
||||
<source>settings.misc.kicad_eda.default_parameter_visibility.help</source>
|
||||
<target>EDA-Sichtbarkeit für alle [Part]-Parameter, die keine explizite Sichtbarkeit gesetzt haben. Wenn aktiviert, sind alle Parameter standardmäßig in der EDA-Software sichtbar.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="J6pYnaC" name="settings.misc.kicad_eda.default_orderdetails_visibility">
|
||||
<segment state="translated">
|
||||
<source>settings.misc.kicad_eda.default_orderdetails_visibility</source>
|
||||
<target>Standard EDA-Sichtbarkeit für Einkaufsinformationen</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="Hiye4C." name="settings.misc.kicad_eda.default_orderdetails_visibility.help">
|
||||
<segment state="translated">
|
||||
<source>settings.misc.kicad_eda.default_orderdetails_visibility.help</source>
|
||||
<target>EDA-Sichtbarkeit für alle Bestellinformationen, die keine explizite Sichtbarkeit gesetzt haben. Bei Aktivierung sind alle Bestellinfos standardmäßig in der EDA-Software sichtbar.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="aEgd0if" name="label_scanner.open">
|
||||
<segment state="translated">
|
||||
<source>label_scanner.open</source>
|
||||
<target>Details anzeigen</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="vw_0Qws" name="label_scanner.db_part_found">
|
||||
<segment state="translated">
|
||||
<source>label_scanner.db_part_found</source>
|
||||
<target>Datenbank-[Part] für Barcode gefunden</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="zntajcd" name="label_scanner.part_can_be_created">
|
||||
<segment state="translated">
|
||||
<source>label_scanner.part_can_be_created</source>
|
||||
<target>[Part] kann erstellt werden</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="cLTbd9w" name="label_scanner.part_can_be_created.help">
|
||||
<segment state="translated">
|
||||
<source>label_scanner.part_can_be_created.help</source>
|
||||
<target>Kein passendes [Part] in der Datenbank gefunden, aber Sie können ein neues [Part] basierend auf diesem Barcode erstellen.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="FfHA3Yf" name="label_scanner.part_create_btn">
|
||||
<segment state="translated">
|
||||
<source>label_scanner.part_create_btn</source>
|
||||
<target>[Part] aus Barcode erstellen</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="xH258F." name="parts.create_from_scan.title">
|
||||
<segment state="translated">
|
||||
<source>parts.create_from_scan.title</source>
|
||||
<target>[Part] aus Scan erstellen</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="8WZYwRJ" name="scan_dialog.mode.amazon">
|
||||
<segment state="translated">
|
||||
<source>scan_dialog.mode.amazon</source>
|
||||
<target>Amazon Barcode</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="BQWuR_G" name="settings.ips.canopy">
|
||||
<segment state="translated">
|
||||
<source>settings.ips.canopy</source>
|
||||
<target>Canopy</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="44BfYzy" name="settings.ips.canopy.alwaysGetDetails">
|
||||
<segment state="translated">
|
||||
<source>settings.ips.canopy.alwaysGetDetails</source>
|
||||
<target>Details immer abrufen</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="so_ms3t" name="settings.ips.canopy.alwaysGetDetails.help">
|
||||
<segment state="translated">
|
||||
<source>settings.ips.canopy.alwaysGetDetails.help</source>
|
||||
<target>Wenn aktiviert, werden beim Erstellen eines Bauteils mehr Details von canopy abgerufen. Das verursacht eine zusätzliche API-Anfrage, liefert aber Produkt-Bulletpoints und Kategoriedaten.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="D055xh8" name="attachment.sandbox.warning">
|
||||
<segment state="translated">
|
||||
<source>attachment.sandbox.warning</source>
|
||||
<target>WARNUNG: Sie betrachten einen von einem Nutzer hochgeladenen Anhang. Dabei handelt es sich um nicht vertrauenswürdigen Inhalt. Bitte vorsichtig fortfahren.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bRcdnJK" name="attachment.sandbox.back_to_partdb">
|
||||
<segment state="translated">
|
||||
<source>attachment.sandbox.back_to_partdb</source>
|
||||
<target>Zurück zu Part-DB</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="MzyA7N8" name="settings.system.attachments.showHTMLAttachments">
|
||||
<segment state="translated">
|
||||
<source>settings.system.attachments.showHTMLAttachments</source>
|
||||
<target>Hochgeladene HTML-Dateianhänge anzeigen (sandboxed)</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="V_LJkRy" name="settings.system.attachments.showHTMLAttachments.help">
|
||||
<segment state="translated">
|
||||
<source>settings.system.attachments.showHTMLAttachments.help</source>
|
||||
<target>⚠️ Wenn aktiviert, können vom Nutzer hochgeladene HTML-Anhänge direkt im Browser angezeigt werden. Viele potenziell schädliche Funktionen sind eingeschränkt, dennoch besteht ein Sicherheitsrisiko und sollte nur aktiviert werden, wenn den hochladenden Nutzern vertraut wird.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="BQo2xWi" name="attachment.sandbox.title">
|
||||
<segment state="translated">
|
||||
<source>attachment.sandbox.title</source>
|
||||
<target>HTML [Attachment]</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="sJ6v9uJ" name="attachment.sandbox.as_plain_text">
|
||||
<segment state="translated">
|
||||
<source>attachment.sandbox.as_plain_text</source>
|
||||
<target>Als Klartext anzeigen</target>
|
||||
</segment>
|
||||
</unit>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -642,6 +642,12 @@ Sub elements will be moved upwards.</target>
|
||||
<target>Group</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="8rz303Z" name="specifications.eda_visibility.help">
|
||||
<segment state="translated">
|
||||
<source>specifications.eda_visibility.help</source>
|
||||
<target>Export this parameter as an EDA field</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="XclPxI9" name="specification.create">
|
||||
<segment state="translated">
|
||||
<source>specification.create</source>
|
||||
@@ -2924,6 +2930,42 @@ If you have done this incorrectly or if a computer is no longer trusted, you can
|
||||
<target>Attachments</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="f3Dggp6" name="part.table.eda_status">
|
||||
<segment state="translated">
|
||||
<source>part.table.eda_status</source>
|
||||
<target>EDA</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="Q_myBuD" name="eda.status.symbol_set">
|
||||
<segment state="translated">
|
||||
<source>eda.status.symbol_set</source>
|
||||
<target>KiCad symbol set</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="QGLfvit" name="eda.status.footprint_set">
|
||||
<segment state="translated">
|
||||
<source>eda.status.footprint_set</source>
|
||||
<target>KiCad footprint set</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="hkze9M." name="eda.status.reference_set">
|
||||
<segment state="translated">
|
||||
<source>eda.status.reference_set</source>
|
||||
<target>Reference prefix set</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="OTXbAfL" name="eda.status.complete">
|
||||
<segment state="translated">
|
||||
<source>eda.status.complete</source>
|
||||
<target>EDA fields complete (symbol, footprint, reference)</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="z9E5RB." name="eda.status.partial">
|
||||
<segment state="translated">
|
||||
<source>eda.status.partial</source>
|
||||
<target>EDA fields partially set</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bMkafCp" name="flash.login_successful">
|
||||
<segment state="translated">
|
||||
<source>flash.login_successful</source>
|
||||
@@ -3266,6 +3308,12 @@ If you have done this incorrectly or if a computer is no longer trusted, you can
|
||||
<target>No longer available</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="6H0WQWq" name="orderdetails.edit.eda_visibility">
|
||||
<segment state="translated">
|
||||
<source>orderdetails.edit.eda_visibility</source>
|
||||
<target>Visible in EDA</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="ZsO5AKM" name="orderdetails.edit.supplierpartnr.placeholder">
|
||||
<segment state="translated">
|
||||
<source>orderdetails.edit.supplierpartnr.placeholder</source>
|
||||
@@ -9501,7 +9549,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="BnqcKWx" name="scan_dialog.mode.lcsc">
|
||||
<segment>
|
||||
<segment state="translated">
|
||||
<source>scan_dialog.mode.lcsc</source>
|
||||
<target>LCSC.com barcode</target>
|
||||
</segment>
|
||||
@@ -9519,19 +9567,19 @@ Please note, that you can not impersonate a disabled user. If you try you will g
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="kQnodbA" name="label_scanner.target_found">
|
||||
<segment>
|
||||
<segment state="translated">
|
||||
<source>label_scanner.target_found</source>
|
||||
<target>Item found in database</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="7Arfw2q" name="label_scanner.scan_result.title">
|
||||
<segment>
|
||||
<segment state="translated">
|
||||
<source>label_scanner.scan_result.title</source>
|
||||
<target>Scan result</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="PTh4EK_" name="label_scanner.no_locations">
|
||||
<segment>
|
||||
<segment state="translated">
|
||||
<source>label_scanner.no_locations</source>
|
||||
<target>Part is not stored at any location.</target>
|
||||
</segment>
|
||||
@@ -9969,6 +10017,18 @@ Please note, that you can not impersonate a disabled user. If you try you will g
|
||||
<target>This value determines the depth of the category tree, that is visible inside KiCad. 0 means that only the top level categories are visible. Set to a value > 0 to show more levels. Set to -1, to show all parts of Part-DB inside a sigle cnategory in KiCad.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="X5.rQdO" name="settings.misc.kicad_eda.datasheet_link">
|
||||
<segment state="translated">
|
||||
<source>settings.misc.kicad_eda.datasheet_link</source>
|
||||
<target>Datasheet field links to PDF</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="Fm1QTCs" name="settings.misc.kicad_eda.datasheet_link.help">
|
||||
<segment state="translated">
|
||||
<source>settings.misc.kicad_eda.datasheet_link.help</source>
|
||||
<target>When enabled, the datasheet field in KiCad will link to the actual PDF file (if found). When disabled, it will link to the Part-DB page instead. The Part-DB page link is always available as a separate "Part-DB URL" field.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="VwvmcWE" name="settings.behavior.sidebar">
|
||||
<segment state="translated">
|
||||
<source>settings.behavior.sidebar</source>
|
||||
@@ -10953,6 +11013,84 @@ Please note, that you can not impersonate a disabled user. If you try you will g
|
||||
<target>Bulk Info Provider Import</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="VtS1yT7" name="part_list.action.group.eda">
|
||||
<segment state="translated">
|
||||
<source>part_list.action.group.eda</source>
|
||||
<target>EDA / KiCad</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="swU1Rp2" name="part_list.action.batch_edit_eda">
|
||||
<segment state="translated">
|
||||
<source>part_list.action.batch_edit_eda</source>
|
||||
<target>Batch Edit EDA Fields</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="ZaS_Hg5" name="batch_eda.title">
|
||||
<segment state="translated">
|
||||
<source>batch_eda.title</source>
|
||||
<target>Batch Edit EDA Fields</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="k2FDo7A" name="batch_eda.description">
|
||||
<segment state="translated">
|
||||
<source>batch_eda.description</source>
|
||||
<target>Edit EDA/KiCad fields for %count% selected parts. Check the "Apply" box next to each field you want to change.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="WVHbic3" name="batch_eda.show_parts">
|
||||
<segment state="translated">
|
||||
<source>batch_eda.show_parts</source>
|
||||
<target>Show selected parts</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="ubQd6G4" name="batch_eda.apply_hint">
|
||||
<segment state="translated">
|
||||
<source>batch_eda.apply_hint</source>
|
||||
<target>Only fields with the "Apply" checkbox checked will be changed. Unchecked fields are left unchanged.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="w.5FGYL" name="batch_eda.apply">
|
||||
<segment state="translated">
|
||||
<source>batch_eda.apply</source>
|
||||
<target>Apply</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="9EmHp5C" name="batch_eda.field">
|
||||
<segment state="translated">
|
||||
<source>batch_eda.field</source>
|
||||
<target>Field</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="xHaCnEQ" name="batch_eda.value">
|
||||
<segment state="translated">
|
||||
<source>batch_eda.value</source>
|
||||
<target>Value</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="PLqIBvC" name="batch_eda.submit">
|
||||
<segment state="translated">
|
||||
<source>batch_eda.submit</source>
|
||||
<target>Apply to Selected Parts</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="5nO7Fpq" name="batch_eda.cancel">
|
||||
<segment state="translated">
|
||||
<source>batch_eda.cancel</source>
|
||||
<target>Cancel</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="vhlPBNU" name="batch_eda.success">
|
||||
<segment state="translated">
|
||||
<source>batch_eda.success</source>
|
||||
<target>EDA fields updated successfully.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="2fMo760" name="batch_eda.no_parts_selected">
|
||||
<segment state="translated">
|
||||
<source>batch_eda.no_parts_selected</source>
|
||||
<target>No parts were selected for batch editing.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="yzpXFkB" name="info_providers.bulk_import.step1.spn_recommendation">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.step1.spn_recommendation</source>
|
||||
@@ -12551,98 +12689,134 @@ Buerklin-API Authentication server:
|
||||
<target>Last stocktake</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="GNWhoTW" name="part.table.eda_reference">
|
||||
<segment state="translated">
|
||||
<source>part.table.eda_reference</source>
|
||||
<target>EDA Reference</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="tW4yCbf" name="part.table.eda_value">
|
||||
<segment state="translated">
|
||||
<source>part.table.eda_value</source>
|
||||
<target>EDA value</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="s1pgReC" name="settings.misc.kicad_eda.default_parameter_visibility">
|
||||
<segment state="translated">
|
||||
<source>settings.misc.kicad_eda.default_parameter_visibility</source>
|
||||
<target>Default EDA visibility of parameters</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="Z78QunV" name="settings.misc.kicad_eda.default_parameter_visibility.help">
|
||||
<segment state="translated">
|
||||
<source>settings.misc.kicad_eda.default_parameter_visibility.help</source>
|
||||
<target>EDA visibility for all [part] parameters who does not have an explicit visibility set. When enabled all parameters will be visible in the EDA software by default.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="J6pYnaC" name="settings.misc.kicad_eda.default_orderdetails_visibility">
|
||||
<segment state="translated">
|
||||
<source>settings.misc.kicad_eda.default_orderdetails_visibility</source>
|
||||
<target>Default EDA visibility of purchase infos</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="Hiye4C." name="settings.misc.kicad_eda.default_orderdetails_visibility.help">
|
||||
<segment state="translated">
|
||||
<source>settings.misc.kicad_eda.default_orderdetails_visibility.help</source>
|
||||
<target>EDA visibility for all purchase infos who does not have an explicit visibility set. When enabled all purchase infos will be visible in the EDA software by default.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="aEgd0if" name="label_scanner.open">
|
||||
<segment>
|
||||
<segment state="translated">
|
||||
<source>label_scanner.open</source>
|
||||
<target>View details</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="vw_0Qws" name="label_scanner.db_part_found">
|
||||
<segment>
|
||||
<segment state="translated">
|
||||
<source>label_scanner.db_part_found</source>
|
||||
<target>Database [part] found for barcode</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="zntajcd" name="label_scanner.part_can_be_created">
|
||||
<segment>
|
||||
<segment state="translated">
|
||||
<source>label_scanner.part_can_be_created</source>
|
||||
<target>[Part] can be created</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="cLTbd9w" name="label_scanner.part_can_be_created.help">
|
||||
<segment>
|
||||
<segment state="translated">
|
||||
<source>label_scanner.part_can_be_created.help</source>
|
||||
<target>No matching [part] was found in the database, but you can create a new [part] based of this barcode.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="FfHA3Yf" name="label_scanner.part_create_btn">
|
||||
<segment>
|
||||
<segment state="translated">
|
||||
<source>label_scanner.part_create_btn</source>
|
||||
<target>Create [part] from barcode</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="xH258F." name="parts.create_from_scan.title">
|
||||
<segment>
|
||||
<segment state="translated">
|
||||
<source>parts.create_from_scan.title</source>
|
||||
<target>Create [part] from label scan</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="8WZYwRJ" name="scan_dialog.mode.amazon">
|
||||
<segment>
|
||||
<segment state="translated">
|
||||
<source>scan_dialog.mode.amazon</source>
|
||||
<target>Amazon barcode</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="BQWuR_G" name="settings.ips.canopy">
|
||||
<segment>
|
||||
<segment state="translated">
|
||||
<source>settings.ips.canopy</source>
|
||||
<target>Canopy</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="44BfYzy" name="settings.ips.canopy.alwaysGetDetails">
|
||||
<segment>
|
||||
<segment state="translated">
|
||||
<source>settings.ips.canopy.alwaysGetDetails</source>
|
||||
<target>Always fetch details</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="so_ms3t" name="settings.ips.canopy.alwaysGetDetails.help">
|
||||
<segment>
|
||||
<segment state="translated">
|
||||
<source>settings.ips.canopy.alwaysGetDetails.help</source>
|
||||
<target>When selected, more details will be fetched from canopy when creating a part. This causes an additional API request, but gives product bullet points and category info.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="D055xh8" name="attachment.sandbox.warning">
|
||||
<segment>
|
||||
<segment state="translated">
|
||||
<source>attachment.sandbox.warning</source>
|
||||
<target>WARNING: You are viewing an user uploaded attachment. This is untrusted content. Proceed with care.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bRcdnJK" name="attachment.sandbox.back_to_partdb">
|
||||
<segment>
|
||||
<segment state="translated">
|
||||
<source>attachment.sandbox.back_to_partdb</source>
|
||||
<target>Back to Part-DB</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="MzyA7N8" name="settings.system.attachments.showHTMLAttachments">
|
||||
<segment>
|
||||
<segment state="translated">
|
||||
<source>settings.system.attachments.showHTMLAttachments</source>
|
||||
<target>Show uploaded HTML file attachments (sandboxed)</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="V_LJkRy" name="settings.system.attachments.showHTMLAttachments.help">
|
||||
<segment>
|
||||
<segment state="translated">
|
||||
<source>settings.system.attachments.showHTMLAttachments.help</source>
|
||||
<target>⚠️ When enabled, user uploaded HTML attachments can be viewed directly in the browser. Many potential malicious functions are restricted, still this is a potential security risk and should only be enabled, if you trust the users who can upload files.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="BQo2xWi" name="attachment.sandbox.title">
|
||||
<segment>
|
||||
<segment state="translated">
|
||||
<source>attachment.sandbox.title</source>
|
||||
<target>HTML [Attachment]</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="sJ6v9uJ" name="attachment.sandbox.as_plain_text">
|
||||
<segment>
|
||||
<segment state="translated">
|
||||
<source>attachment.sandbox.as_plain_text</source>
|
||||
<target>View as plain text</target>
|
||||
</segment>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -223,6 +223,12 @@
|
||||
<target>A causa di limitazioni tecniche, non è possibile selezionare date successive al 19-01-2038 su sistemi a 32 bit!</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="iM9yb_p" name="validator.fileSize.invalidFormat">
|
||||
<segment state="translated">
|
||||
<source>validator.fileSize.invalidFormat</source>
|
||||
<target>Formato di dimensione file non valido. Utilizzare un numero intero più K, M, G come suffisso per Kilo, Mega or Gigabytes.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="ZFxQ0BZ" name="validator.invalid_range">
|
||||
<segment state="translated">
|
||||
<source>validator.invalid_range</source>
|
||||
@@ -235,5 +241,17 @@
|
||||
<target>Codice non valido. Controlla che la tua app di autenticazione sia impostata correttamente e che sia il server che il dispositivo di autenticazione abbiano l'ora impostata correttamente.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="I330cr5" name="settings.synonyms.type_synonyms.collection_type.duplicate">
|
||||
<segment state="translated">
|
||||
<source>settings.synonyms.type_synonyms.collection_type.duplicate</source>
|
||||
<target>Esiste già una traduzione definita per questo tipo e questa lingua!</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="zT_j_oQ" name="validator.invalid_gtin">
|
||||
<segment state="translated">
|
||||
<source>validator.invalid_gtin</source>
|
||||
<target>Questo non è un valido GTIN / EAN!</target>
|
||||
</segment>
|
||||
</unit>
|
||||
</file>
|
||||
</xliff>
|
||||
</xliff>
|
||||
|
||||
82
yarn.lock
82
yarn.lock
@@ -2160,9 +2160,9 @@
|
||||
integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==
|
||||
|
||||
"@types/node@*":
|
||||
version "25.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-25.3.0.tgz#749b1bd4058e51b72e22bd41e9eab6ebd0180470"
|
||||
integrity sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==
|
||||
version "25.3.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-25.3.3.tgz#605862544ee7ffd7a936bcbf0135a14012f1e549"
|
||||
integrity sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==
|
||||
dependencies:
|
||||
undici-types "~7.18.0"
|
||||
|
||||
@@ -2393,7 +2393,7 @@ acorn-walk@^8.0.0:
|
||||
dependencies:
|
||||
acorn "^8.11.0"
|
||||
|
||||
acorn@^8.0.4, acorn@^8.11.0, acorn@^8.15.0:
|
||||
acorn@^8.0.4, acorn@^8.11.0, acorn@^8.15.0, acorn@^8.16.0:
|
||||
version "8.16.0"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.16.0.tgz#4ce79c89be40afe7afe8f3adb902a1f1ce9ac08a"
|
||||
integrity sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==
|
||||
@@ -2606,11 +2606,11 @@ balanced-match@^4.0.2:
|
||||
integrity sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==
|
||||
|
||||
barcode-detector@^3.0.0, barcode-detector@^3.0.5:
|
||||
version "3.0.8"
|
||||
resolved "https://registry.yarnpkg.com/barcode-detector/-/barcode-detector-3.0.8.tgz#09a3363cb24699d1d6389a291383113c44420324"
|
||||
integrity sha512-Z9jzzE8ngEDyN9EU7lWdGgV07mcnEQnrX8W9WecXDqD2v+5CcVjt9+a134a5zb+kICvpsrDx6NYA6ay4LGFs8A==
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/barcode-detector/-/barcode-detector-3.1.0.tgz#ce340cead9f267951f4c53887ac24b64c21a79c4"
|
||||
integrity sha512-aQjGxrgsb/WTlw6pHZwFRO6NhFMhwHGEkd0pzV25fBn8dnRA1PA1G7bLeAzvSea646S/96nW5W3jD8wezQZ1vQ==
|
||||
dependencies:
|
||||
zxing-wasm "2.2.4"
|
||||
zxing-wasm "3.0.0"
|
||||
|
||||
base64-js@1.3.1:
|
||||
version "1.3.1"
|
||||
@@ -2671,9 +2671,9 @@ brace-expansion@^1.1.7:
|
||||
concat-map "0.0.1"
|
||||
|
||||
brace-expansion@^5.0.2:
|
||||
version "5.0.3"
|
||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.3.tgz#6a9c6c268f85b53959ec527aeafe0f7300258eef"
|
||||
integrity sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==
|
||||
version "5.0.4"
|
||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.4.tgz#614daaecd0a688f660bbbc909a8748c3d80d4336"
|
||||
integrity sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==
|
||||
dependencies:
|
||||
balanced-match "^4.0.2"
|
||||
|
||||
@@ -2793,9 +2793,9 @@ caniuse-api@^3.0.0:
|
||||
lodash.uniq "^4.5.0"
|
||||
|
||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001759:
|
||||
version "1.0.30001772"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001772.tgz#aa8a176eba0006e78c965a8215c7a1ceb030122d"
|
||||
integrity sha512-mIwLZICj+ntVTw4BT2zfp+yu/AqV6GMKfJVJMx3MwPxs+uk/uj2GLl2dH8LQbjiLDX66amCga5nKFyDgRR43kg==
|
||||
version "1.0.30001775"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001775.tgz#9572266e3f7f77efee5deac1efeb4795879d1b7f"
|
||||
integrity sha512-s3Qv7Lht9zbVKE9XoTyRG6wVDCKdtOFIjBGg3+Yhn6JaytuNKPIjBMTMIY1AnOH3seL5mvF+x33oGAyK3hVt3A==
|
||||
|
||||
ccount@^2.0.0:
|
||||
version "2.0.1"
|
||||
@@ -3694,9 +3694,9 @@ emojis-list@^3.0.0:
|
||||
integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==
|
||||
|
||||
enhanced-resolve@^5.0.0, enhanced-resolve@^5.19.0:
|
||||
version "5.19.0"
|
||||
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz#6687446a15e969eaa63c2fa2694510e17ae6d97c"
|
||||
integrity sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==
|
||||
version "5.20.0"
|
||||
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz#323c2a70d2aa7fb4bdfd6d3c24dfc705c581295d"
|
||||
integrity sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==
|
||||
dependencies:
|
||||
graceful-fs "^4.2.4"
|
||||
tapable "^2.3.0"
|
||||
@@ -4960,9 +4960,9 @@ jszip@^3.2.0:
|
||||
setimmediate "^1.0.5"
|
||||
|
||||
katex@^0.16.0:
|
||||
version "0.16.29"
|
||||
resolved "https://registry.yarnpkg.com/katex/-/katex-0.16.29.tgz#d6d2cc2e1840663c2ceb6fc764d4f0d9ca04fa4c"
|
||||
integrity sha512-ef+wYUDehNgScWoA0ZhEngsNqUv9uIj4ftd/PapQmT+E85lXI6Wx6BvJO48v80Vhj3t/IjEoZWw9/ZPe8kHwHg==
|
||||
version "0.16.33"
|
||||
resolved "https://registry.yarnpkg.com/katex/-/katex-0.16.33.tgz#5cd5af2ddc1132fe6a710ae6604ec1f19fca9e91"
|
||||
integrity sha512-q3N5u+1sY9Bu7T4nlXoiRBXWfwSefNGoKeOwekV+gw0cAXQlz2Ww6BLcmBxVDeXBMUDQv6fK5bcNaJLxob3ZQA==
|
||||
dependencies:
|
||||
commander "^8.3.0"
|
||||
|
||||
@@ -5592,9 +5592,9 @@ mini-css-extract-plugin@^2.4.2, mini-css-extract-plugin@^2.6.0:
|
||||
tapable "^2.2.1"
|
||||
|
||||
minimatch@*:
|
||||
version "10.2.2"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.2.tgz#361603ee323cfb83496fea2ae17cc44ea4e1f99f"
|
||||
integrity sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==
|
||||
version "10.2.4"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.4.tgz#465b3accbd0218b8281f5301e27cedc697f96fde"
|
||||
integrity sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==
|
||||
dependencies:
|
||||
brace-expansion "^5.0.2"
|
||||
|
||||
@@ -5606,9 +5606,9 @@ minimatch@3.0.4:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
minimatch@^3.0.4, minimatch@^3.1.1:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.3.tgz#6a5cba9b31f503887018f579c89f81f61162e624"
|
||||
integrity sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==
|
||||
version "3.1.5"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.5.tgz#580c88f8d5445f2bd6aa8f3cadefa0de79fbd69e"
|
||||
integrity sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
@@ -7458,9 +7458,9 @@ to-regex-range@^5.0.1:
|
||||
is-number "^7.0.0"
|
||||
|
||||
tom-select@^2.1.0:
|
||||
version "2.5.1"
|
||||
resolved "https://registry.yarnpkg.com/tom-select/-/tom-select-2.5.1.tgz#8c8d3f11e5c1780b5f26c9e90f4e650842ff9596"
|
||||
integrity sha512-63D5/Qf6bb6kLSgksEuas/60oawDcuUHrD90jZofeOpF6bkQFYriKrvtpJBQQ4xIA5dUGcjhBbk/yrlfOQsy3g==
|
||||
version "2.5.2"
|
||||
resolved "https://registry.yarnpkg.com/tom-select/-/tom-select-2.5.2.tgz#77dd4bc780b1ea72905337b24f04ce19dc6d2ca1"
|
||||
integrity sha512-VAlGj5MBWVLMJje2NwA3XSmxa7CUFpp1tdzFZ8wymCkcLeP0NwF4ARmSuUK4BWbmSN1fETlSazWkMIxEpP4GdQ==
|
||||
dependencies:
|
||||
"@orchidjs/sifter" "^1.1.0"
|
||||
"@orchidjs/unicode-variants" "^1.1.2"
|
||||
@@ -7501,7 +7501,7 @@ tslib@^2.8.0:
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
|
||||
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
|
||||
|
||||
type-fest@^5.2.0:
|
||||
type-fest@^5.4.4:
|
||||
version "5.4.4"
|
||||
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-5.4.4.tgz#577f165b5ecb44cfc686559cc54ca77f62aa374d"
|
||||
integrity sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw==
|
||||
@@ -7840,15 +7840,15 @@ webpack-sources@^2.0.1, webpack-sources@^2.2.0:
|
||||
source-list-map "^2.0.1"
|
||||
source-map "^0.6.1"
|
||||
|
||||
webpack-sources@^3.3.3:
|
||||
webpack-sources@^3.3.4:
|
||||
version "3.3.4"
|
||||
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.3.4.tgz#a338b95eb484ecc75fbb196cbe8a2890618b4891"
|
||||
integrity sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==
|
||||
|
||||
webpack@^5.74.0:
|
||||
version "5.105.2"
|
||||
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.105.2.tgz#f3b76f9fc36f1152e156e63ffda3bbb82e6739ea"
|
||||
integrity sha512-dRXm0a2qcHPUBEzVk8uph0xWSjV/xZxenQQbLwnwP7caQCYpqG1qddwlyEkIDkYn0K8tvmcrZ+bOrzoQ3HxCDw==
|
||||
version "5.105.3"
|
||||
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.105.3.tgz#307ad95bafffd08bc81049d6519477b16e42e7ba"
|
||||
integrity sha512-LLBBA4oLmT7sZdHiYE/PeVuifOxYyE2uL/V+9VQP7YSYdJU7bSf7H8bZRRxW8kEPMkmVjnrXmoR3oejIdX0xbg==
|
||||
dependencies:
|
||||
"@types/eslint-scope" "^3.7.7"
|
||||
"@types/estree" "^1.0.8"
|
||||
@@ -7856,7 +7856,7 @@ webpack@^5.74.0:
|
||||
"@webassemblyjs/ast" "^1.14.1"
|
||||
"@webassemblyjs/wasm-edit" "^1.14.1"
|
||||
"@webassemblyjs/wasm-parser" "^1.14.1"
|
||||
acorn "^8.15.0"
|
||||
acorn "^8.16.0"
|
||||
acorn-import-phases "^1.0.3"
|
||||
browserslist "^4.28.1"
|
||||
chrome-trace-event "^1.0.2"
|
||||
@@ -7874,7 +7874,7 @@ webpack@^5.74.0:
|
||||
tapable "^2.3.0"
|
||||
terser-webpack-plugin "^5.3.16"
|
||||
watchpack "^2.5.1"
|
||||
webpack-sources "^3.3.3"
|
||||
webpack-sources "^3.3.4"
|
||||
|
||||
which-boxed-primitive@^1.1.0, which-boxed-primitive@^1.1.1:
|
||||
version "1.1.1"
|
||||
@@ -8054,10 +8054,10 @@ zwitch@^2.0.0, zwitch@^2.0.4:
|
||||
resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7"
|
||||
integrity sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==
|
||||
|
||||
zxing-wasm@2.2.4:
|
||||
version "2.2.4"
|
||||
resolved "https://registry.yarnpkg.com/zxing-wasm/-/zxing-wasm-2.2.4.tgz#06b73db93c5a980d4441f357c0a1f8483c7af691"
|
||||
integrity sha512-1gq5zs4wuNTs5umWLypzNNeuJoluFvwmvjiiT3L9z/TMlVveeJRWy7h90xyUqCe+Qq0zL0w7o5zkdDMWDr9aZA==
|
||||
zxing-wasm@3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/zxing-wasm/-/zxing-wasm-3.0.0.tgz#184feade580ef7763cac4f1231eae1aa6fe28a39"
|
||||
integrity sha512-s7ASCPKX+QnH7Y83f4Byxmq/vDzYW7B9m6jMP5S30JGfN2A6WAUn6P3vcBmNguDhPLE6ny2fjTooQVyKBXI1qA==
|
||||
dependencies:
|
||||
"@types/emscripten" "^1.41.5"
|
||||
type-fest "^5.2.0"
|
||||
type-fest "^5.4.4"
|
||||
|
||||
Reference in New Issue
Block a user