From 50f478f7efa61272ececec33d9d0cca03ad8217b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 30 Aug 2025 21:59:33 +0200 Subject: [PATCH 001/228] New translations messages.en.xlf (German) --- translations/messages.de.xlf | 2226 ---------------------------------- 1 file changed, 2226 deletions(-) diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index a5c18cdd..3c8d3400 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -7161,2232 +7161,6 @@ Element 2 Element 3 - - - obsolete - obsolete - - - entity.mass_creation.btn - Anlegen - - - - - obsolete - obsolete - - - measurement_unit.edit.is_integer - Ganzzahlig - - - - - obsolete - obsolete - - - measurement_unit.edit.is_integer.help - Wenn diese Option aktiviert ist, werden alle Mengen in dieser Einheit auf ganze Zahlen gerundet. - - - - - obsolete - obsolete - - - measurement_unit.edit.use_si_prefix - Benutze SI Prefixe - - - - - obsolete - obsolete - - - measurement_unit.edit.use_si_prefix.help - Wenn diese Option aktiviert ist, werden bei Ausgabe der Zahlen SI Prefixe benutzt (z.B. 1,2kg anstatt 1200g) - - - - - obsolete - obsolete - - - measurement_unit.edit.unit_symbol - Einheitensymbol - - - - - obsolete - obsolete - - - measurement_unit.edit.unit_symbol.placeholder - z.B. m - - - - - obsolete - obsolete - - - storelocation.edit.is_full.label - Lagerort voll - - - - - obsolete - obsolete - - - storelocation.edit.is_full.help - Wenn diese Option aktiviert ist, ist es weder möglich neue Bauteile zu diesem Lagerort hinzuzufügen, noch die Anzahl bereits vorhandener Bauteile zu erhöhen. - - - - - obsolete - obsolete - - - storelocation.limit_to_existing.label - Nur bestehende Bauteile - - - - - obsolete - obsolete - - - storelocation.limit_to_existing.help - Wenn diese Option aktiv ist, ist es nicht möglich neue Bauteile zu diesem Lagerort hinzuzufügen, es ist aber möglich die Anzahl bereits vorhandener Bauteile zu erhöhen. - - - - - obsolete - obsolete - - - storelocation.only_single_part.label - Nur ein Bauteil - - - - - obsolete - obsolete - - - storelocation.only_single_part.help - Wenn diese Option aktiviert ist, kann dieser Lagerort nur ein einzelnes Bauteil aber in beliebiger Menge fassen. Hilfreich für kleine SMD Fächer oder Feeder. - - - - - obsolete - obsolete - - - storelocation.storage_type.label - Lagertyp - - - - - obsolete - obsolete - - - storelocation.storage_type.help - Hier kann eine Maßeinheit gewählt werden, die ein Bauteil haben muss, damit es in diesem Lagerort gelagert werden kann. - - - - - obsolete - obsolete - - - supplier.edit.default_currency - Standardwährung - - - - - obsolete - obsolete - - - supplier.shipping_costs.label - Versandkosten - - - - - obsolete - obsolete - - - user.username.placeholder - z.B. m.muster - - - - - obsolete - obsolete - - - user.firstName.placeholder - z.B. Max - - - - - obsolete - obsolete - - - user.lastName.placeholder - z.B. Muster - - - - - obsolete - obsolete - - - user.email.placeholder - z.B. m.muster@ecorp.com - - - - - obsolete - obsolete - - - user.department.placeholder - z.B. Entwicklung - - - - - obsolete - obsolete - - - user.settings.pw_new.label - Neues Passwort - - - - - obsolete - obsolete - - - user.settings.pw_confirm.label - Neues Passwort bestätigen - - - - - obsolete - obsolete - - - user.edit.needs_pw_change - Nutzer muss Passwort ändern - - - - - obsolete - obsolete - - - user.edit.user_disabled - Benutzer deaktiviert (kein Login möglich) - - - - - obsolete - obsolete - - - user.create - Benutzer anlegen - - - - - obsolete - obsolete - - - user.edit.save - Speichern - - - - - obsolete - obsolete - - - entity.edit.reset - Änderungen verwerfen - - - - - templates\Parts\show_part_info.html.twig:166 - obsolete - obsolete - - - part.withdraw.btn - Entnehmen - - - - - templates\Parts\show_part_info.html.twig:171 - obsolete - obsolete - - - part.withdraw.comment: - Kommentar/Zweck - - - - - templates\Parts\show_part_info.html.twig:189 - obsolete - obsolete - - - part.add.caption - Bauteil hinzufügen - - - - - templates\Parts\show_part_info.html.twig:194 - obsolete - obsolete - - - part.add.btn - Hinzufügen - - - - - templates\Parts\show_part_info.html.twig:199 - obsolete - obsolete - - - part.add.comment - Kommentar/Zweck - - - - - templates\AdminPages\CompanyAdminBase.html.twig:15 - obsolete - obsolete - - - admin.comment - Notizen - - - - - src\Form\PartType.php:83 - obsolete - obsolete - - - manufacturer_url.label - Herstellerlink - - - - - src\Form\PartType.php:66 - obsolete - obsolete - - - part.description.placeholder - z.B. NPN 45V 0,1A 0,5W - - - - - src\Form\PartType.php:69 - obsolete - obsolete - - - part.instock.placeholder - z.B. 12 - - - - - src\Form\PartType.php:72 - obsolete - obsolete - - - part.mininstock.placeholder - z.B. 10 - - - - - obsolete - obsolete - - - part.order.price_per - pro - - - - - obsolete - obsolete - - - part.withdraw.caption - Bauteile entnehmen - - - - - obsolete - obsolete - - - datatable.datatable.lengthMenu - _MENU_ - - - - - obsolete - obsolete - - - perm.group.parts - Bauteile - - - - - obsolete - obsolete - - - perm.group.structures - Datenstrukturen - - - - - obsolete - obsolete - - - perm.group.system - System - - - - - obsolete - obsolete - - - perm.parts - Allgemein - - - - - obsolete - obsolete - - - perm.read - Anzeigen - - - - - obsolete - obsolete - - - perm.edit - Bearbeiten - - - - - obsolete - obsolete - - - perm.create - Anlegen - - - - - obsolete - obsolete - - - perm.part.move - Kategorie verändern - - - - - obsolete - obsolete - - - perm.delete - Löschen - - - - - obsolete - obsolete - - - perm.part.search - Suchen - - - - - obsolete - obsolete - - - perm.part.all_parts - Alle Bauteile auflisten - - - - - obsolete - obsolete - - - perm.part.no_price_parts - Teile ohne Preis auflisten - - - - - obsolete - obsolete - - - perm.part.obsolete_parts - Obsolete Teile auflisten - - - - - obsolete - obsolete - - - perm.part.unknown_instock_parts - Bauteile mit unbekanntem Bestand auflisten - - - - - obsolete - obsolete - - - perm.part.change_favorite - Favoritenstatus ändern - - - - - obsolete - obsolete - - - perm.part.show_favorite - Favoriten anzeigen - - - - - obsolete - obsolete - - - perm.part.show_last_edit_parts - Zeige zuletzt bearbeitete/hinzugefügte Bauteile - - - - - obsolete - obsolete - - - perm.part.show_users - Letzten bearbeitenden Nutzer anzeigen - - - - - obsolete - obsolete - - - perm.part.show_history - Historie anzeigen - - - - - obsolete - obsolete - - - perm.part.name - Name - - - - - obsolete - obsolete - - - perm.part.description - Beschreibung - - - - - obsolete - obsolete - - - perm.part.instock - Vorhanden - - - - - obsolete - obsolete - - - perm.part.mininstock - Min. Bestand - - - - - obsolete - obsolete - - - perm.part.comment - Notizen - - - - - obsolete - obsolete - - - perm.part.storelocation - Lagerort - - - - - obsolete - obsolete - - - perm.part.manufacturer - Hersteller - - - - - obsolete - obsolete - - - perm.part.orderdetails - Bestellinformationen - - - - - obsolete - obsolete - - - perm.part.prices - Preise - - - - - obsolete - obsolete - - - perm.part.attachments - Dateianhänge - - - - - obsolete - obsolete - - - perm.part.order - Bestellungen - - - - - obsolete - obsolete - - - perm.storelocations - Lagerorte - - - - - obsolete - obsolete - - - perm.move - Verschieben - - - - - obsolete - obsolete - - - perm.list_parts - Teile auflisten - - - - - obsolete - obsolete - - - perm.part.footprints - Footprints - - - - - obsolete - obsolete - - - perm.part.categories - Kategorien - - - - - obsolete - obsolete - - - perm.part.supplier - Lieferanten - - - - - obsolete - obsolete - - - perm.part.manufacturers - Hersteller - - - - - obsolete - obsolete - - - perm.projects - Projekte - - - - - obsolete - obsolete - - - perm.part.attachment_types - Dateitypen - - - - - obsolete - obsolete - - - perm.tools.import - Import - - - - - obsolete - obsolete - - - perm.tools.labels - Labels - - - - - obsolete - obsolete - - - perm.tools.calculator - Widerstandsrechner - - - - - obsolete - obsolete - - - perm.tools.footprints - Footprints - - - - - obsolete - obsolete - - - perm.tools.ic_logos - IC-Logos - - - - - obsolete - obsolete - - - perm.tools.statistics - Statistik - - - - - obsolete - obsolete - - - perm.edit_permissions - Berechtigungen ändern - - - - - obsolete - obsolete - - - perm.users.edit_user_name - Nutzernamen ändern - - - - - obsolete - obsolete - - - perm.users.edit_change_group - Gruppe ändern - - - - - obsolete - obsolete - - - perm.users.edit_infos - Informationen ändern - - - - - obsolete - obsolete - - - perm.users.edit_permissions - Berechtigungen ändern - - - - - obsolete - obsolete - - - perm.users.set_password - Passwort ändern - - - - - obsolete - obsolete - - - perm.users.change_user_settings - Benutzereinstellungen ändern - - - - - obsolete - obsolete - - - perm.database.see_status - Status anzeigen - - - - - obsolete - obsolete - - - perm.database.update_db - Datenbank updaten - - - - - obsolete - obsolete - - - perm.database.read_db_settings - Einstellungen anzeigen - - - - - obsolete - obsolete - - - perm.database.write_db_settings - Einstellungen ändern - - - - - obsolete - obsolete - - - perm.config.read_config - Konfiguration anzeigen - - - - - obsolete - obsolete - - - perm.config.edit_config - Konfiguration ändern - - - - - obsolete - obsolete - - - perm.config.server_info - Server info - - - - - obsolete - obsolete - - - perm.config.use_debug - Debugtools benutzen - - - - - obsolete - obsolete - - - perm.show_logs - Logs anzeigen - - - - - obsolete - obsolete - - - perm.delete_logs - Logeinträge löschen - - - - - obsolete - obsolete - - - perm.self.edit_infos - Informationen ändern - - - - - obsolete - obsolete - - - perm.self.edit_username - Benutzernamen ändern - - - - - obsolete - obsolete - - - perm.self.show_permissions - Berechtigungen anzeigen - - - - - obsolete - obsolete - - - perm.self.show_logs - Logs anzeigen - - - - - obsolete - obsolete - - - perm.self.create_labels - Labels erstellen - - - - - obsolete - obsolete - - - perm.self.edit_options - Einstellungen ändern - - - - - obsolete - obsolete - - - perm.self.delete_profiles - Profile löschen - - - - - obsolete - obsolete - - - perm.self.edit_profiles - Profile bearbeiten - - - - - obsolete - obsolete - - - perm.part.tools - Tools - - - - - obsolete - obsolete - - - perm.groups - Gruppen - - - - - obsolete - obsolete - - - perm.users - Benutzer - - - - - obsolete - obsolete - - - perm.database - Datenbank - - - - - obsolete - obsolete - - - perm.config - Einstellungen - - - - - obsolete - obsolete - - - perm.system - System - - - - - obsolete - obsolete - - - perm.self - Eigenen Benutzer bearbeiten - - - - - obsolete - obsolete - - - perm.labels - Labels - - - - - obsolete - obsolete - - - perm.part.category - Kategorie - - - - - obsolete - obsolete - - - perm.part.minamount - Mindestbestand - - - - - obsolete - obsolete - - - perm.part.footprint - Footprint - - - - - obsolete - obsolete - - - perm.part.mpn - MPN - - - - - obsolete - obsolete - - - perm.part.status - Herstellungsstatus - - - - - obsolete - obsolete - - - perm.part.tags - Tags - - - - - obsolete - obsolete - - - perm.part.unit - Maßeinheit - - - - - obsolete - obsolete - - - perm.part.mass - Gewicht - - - - - obsolete - obsolete - - - perm.part.lots - Lagerorte - - - - - obsolete - obsolete - - - perm.show_users - Letzten bearbeitenden Nutzer anzeigen - - - - - obsolete - obsolete - - - perm.currencies - Währungen - - - - - obsolete - obsolete - - - perm.measurement_units - Maßeinheiten - - - - - obsolete - obsolete - - - user.settings.pw_old.label - Altes Passwort - - - - - obsolete - obsolete - - - pw_reset.submit - Passwort zurücksetzen - - - - - obsolete - obsolete - - - u2f_two_factor - Sicherheitsschlüssel (U2F) - - - - - obsolete - obsolete - - - google - Google - - - - - tfa.provider.webauthn_two_factor_provider - Sicherheitsschlüssel - - - - - obsolete - obsolete - - - tfa.provider.google - Authenticator App - - - - - obsolete - obsolete - - - Login successful - Login erfolgreich. - - - - - obsolete - obsolete - - - log.type.exception - Unbehandelte Exception (veraltet) - - - - - obsolete - obsolete - - - log.type.user_login - Nutzer eingeloggt - - - - - obsolete - obsolete - - - log.type.user_logout - Nutzer ausgeloggt - - - - - obsolete - obsolete - - - log.type.unknown - Unbekannt - - - - - obsolete - obsolete - - - log.type.element_created - Element angelegt - - - - - obsolete - obsolete - - - log.type.element_edited - Element bearbeitet - - - - - obsolete - obsolete - - - log.type.element_deleted - Element gelöscht - - - - - obsolete - obsolete - - - log.type.database_updated - Datenbank aktualisiert - - - - - obsolete - - - perm.revert_elements - Element zurücksetzen - - - - - obsolete - - - perm.show_history - Historie anzeigen - - - - - obsolete - - - perm.tools.lastActivity - Letzte Aktivität anzeigen - - - - - obsolete - - - perm.tools.timeTravel - Alte Versionsstände anzeigen (Zeitreisen) - - - - - obsolete - - - tfa_u2f.key_added_successful - Sicherheitsschlüssel erfolgreich hinzugefügt. - - - - - obsolete - - - Username - Benutzername - - - - - obsolete - - - log.type.security.google_disabled - Authenticator App deaktiviert - - - - - obsolete - - - log.type.security.u2f_removed - Sicherheitsschlüssel gelöscht - - - - - obsolete - - - log.type.security.u2f_added - Sicherheitsschlüssel hinzugefügt - - - - - obsolete - - - log.type.security.backup_keys_reset - Neue Backupkeys erzeugt - - - - - obsolete - - - log.type.security.google_enabled - Authenticator App aktiviert - - - - - obsolete - - - log.type.security.password_changed - Passwort geändert - - - - - obsolete - - - log.type.security.trusted_device_reset - Vertrauenswürdige Geräte zurückgesetzt - - - - - obsolete - - - log.type.collection_element_deleted - Kollektionselement gelöscht - - - - - obsolete - - - log.type.security.password_reset - Passwort zurückgesetzt - - - - - obsolete - - - log.type.security.2fa_admin_reset - Zwei-Faktor-Authentifizierung durch Administrator zurückgesetzt - - - - - obsolete - - - log.type.user_not_allowed - Unerlaubter Zugriffsversuch - - - - - obsolete - - - log.database_updated.success - Erfolgreich - - - - - obsolete - - - label_options.barcode_type.2D - 2D - - - - - obsolete - - - label_options.barcode_type.1D - 1D - - - - - obsolete - - - perm.part.parameters - Parameter - - - - - obsolete - - - perm.attachment_show_private - Private Anhänge zeigen - - - - - obsolete - - - perm.tools.label_scanner - Labelscanner - - - - - obsolete - - - perm.self.read_profiles - Profile anzeigen - - - - - obsolete - - - perm.self.create_profiles - Profil anlegen - - - - - obsolete - - - perm.labels.use_twig - Twig Modus benutzen - - - - - label_profile.showInDropdown - In Barcode Schnellauswahl anzeigen - - - - - group.edit.enforce_2fa - Erzwinge Zwei-Faktor-Authentifizierung (2FA) - - - - - group.edit.enforce_2fa.help - Wenn diese Option aktiv ist, muss jedes direkte Mitglied dieser Gruppe, mindestens einen zweiten Faktor zur Authentifizierung einrichten. Empfohlen z.B. für administrative Gruppen mit weitreichenden Berechtigungen. - - - - - selectpicker.empty - Nichts ausgewählt - - - - - selectpicker.nothing_selected - Nichts ausgewählt - - - - - entity.delete.must_not_contain_parts - Element "%PATH%" enthält noch Bauteile. Bearbeite die Bauteile, um dieses Element löschen zu können. - - - - - entity.delete.must_not_contain_attachments - Dateityp enthält noch Bauteile. Ändere deren Dateityp, um diesen Dateityp löschen zu können. - - - - - entity.delete.must_not_contain_prices - Währung enthält noch Bauteile. Ändere deren Währung, um diese Währung löschen zu können. - - - - - entity.delete.must_not_contain_users - Benutzer sind noch Teil dieser Gruppe. Ändere deren Gruppe, um diese Gruppe löschen zu können. - - - - - part.table.edit - Ändern - - - - - part.table.edit.title - Bauteil ändern - - - - - part_list.action.action.title - Aktion auswählen - - - - - part_list.action.action.group.favorite - Favorit - - - - - part_list.action.action.favorite - Bauteile favorisieren - - - - - part_list.action.action.unfavorite - Favorisierung aufheben - - - - - part_list.action.action.group.change_field - Change field - - - - - part_list.action.action.change_category - Kategorie ändern - - - - - part_list.action.action.change_footprint - Footprint ändern - - - - - part_list.action.action.change_manufacturer - Hersteller ändern - - - - - part_list.action.action.change_unit - Maßeinheit ändern - - - - - part_list.action.action.delete - Löschen - - - - - part_list.action.submit - Ok - - - - - part_list.action.part_count - %count% Bauteile ausgewählt! - - - - - company.edit.quick.website - Website öffnen - - - - - company.edit.quick.email - E-Mail senden - - - - - company.edit.quick.phone - Call phone - - - - - company.edit.quick.fax - Fax senden - - - - - company.fax_number.placeholder - z.B. +49 1234 567890 - - - - - part.edit.save_and_clone - Speichern und duplizieren - - - - - validator.file_ext_not_allowed - Dateierweiterung nicht erlaubt für diesen Anhangstyp. - - - - - tools.reel_calc.title - SMD Reel Rechner - - - - - tools.reel_calc.inner_dia - Innerer Durchmesser - - - - - tools.reel_calc.outer_dia - Äußerer Durchmesser - - - - - tools.reel_calc.tape_thick - Tape Dicke - - - - - tools.reel_calc.part_distance - Bauteile Abstand - - - - - tools.reel_calc.update - Update - - - - - tools.reel_calc.parts_per_meter - Bauteile pro Meter - - - - - tools.reel_calc.result_length - Tape Länge - - - - - tools.reel_calc.result_amount - Ungefähre Anzahl Bauteile - - - - - tools.reel_calc.outer_greater_inner_error - Fehler: Äußerer Durchmesser muss großer sein als der innere Durchmesser! - - - - - tools.reel_calc.missing_values.error - Bitte alle Werte angeben! - - - - - tools.reel_calc.load_preset - Preset laden - - - - - tools.reel_calc.explanation - Dieser Rechner erlaubt es Abzuschätzen wie viele Bauteile noch auf einer SMD Rolle (Reel) vorhanden sind. Messen Sie die angegeben Dimensionen auf der Rolle nach (oder nutzen Sie die Vorgaben) und drücken Sie "Update". - - - - - perm.tools.reel_calculator - SMD Reel Rechner - - - - - tree.tools.tools.reel_calculator - SMD Reel Rechner - - - - - user.pw_change_needed.flash - Passwortänderung benötigt! Bitte setze ein neues Passwort. - - - - - tree.root_node.text - Wurzel - - - - - part_list.action.select_null - Keine Elemente vorhanden! - - - - - part_list.action.delete-title - Möchten Sie diese Bauteile wirklich löschen? - - - - - part_list.action.delete-message - Diese Bauteile und alle verknüpften Informationen (Anhänge, Preisinformationen, etc.) werden gelöscht. Dies kann nicht rückgängig gemacht werden! - - - - - part.table.actions.success - Aktionen erfolgreich. - - - - - attachment.edit.delete.confirm - Möchten Sie diesen Anhang wirklich löschen? - - - - - filter.text_constraint.value.operator.EQ - Gleich - - - - - filter.text_constraint.value.operator.NEQ - Ungleich - - - - - filter.text_constraint.value.operator.STARTS - Beginnt mit - - - - - filter.text_constraint.value.operator.CONTAINS - Enthält - - - - - filter.text_constraint.value.operator.ENDS - Endet mit - - - - - filter.text_constraint.value.operator.LIKE - LIKE Ausdruck - - - - - filter.text_constraint.value.operator.REGEX - Regulärer Ausdruck - - - - - filter.number_constraint.value.operator.BETWEEN - Zwischen - - - - - filter.number_constraint.AND - und - - - - - filter.entity_constraint.operator.EQ - Gleich (ohne Kindelemente) - - - - - filter.entity_constraint.operator.NEQ - Ungleich (ohne Kindelemente) - - - - - filter.entity_constraint.operator.INCLUDING_CHILDREN - Gleich (inklusive Kindelementen) - - - - - filter.entity_constraint.operator.EXCLUDING_CHILDREN - Nicht gleich (inklusive Kindelemente) - - - - - part.filter.dbId - Datenbank ID - - - - - filter.tags_constraint.operator.ANY - Irgendeiner der Tags - - - - - filter.tags_constraint.operator.ALL - Alle der Tags - - - - - filter.tags_constraint.operator.NONE - Keine der Tags - - - - - part.filter.lot_count - Anzahl der Lagerbestände - - - - - part.filter.attachments_count - Anzahl der Anhänge - - - - - part.filter.orderdetails_count - Anzahl der Bestellinformationen - - - - - part.filter.lotExpirationDate - Bauteilebestand Ablaufdatum - - - - - part.filter.lotNeedsRefill - Lagerbestand benötigt Auffüllung - - - - - part.filter.lotUnknwonAmount - Lagerbestand mit unbekannter Anzahl - - - - - part.filter.attachmentName - Name des Anhangs - - - - - filter.choice_constraint.operator.ANY - Einer der Ausgewählten - - - - - filter.choice_constraint.operator.NONE - Keine der Ausgewählten - - - - - part.filter.amount_sum - Gesamtmenge - - - - - filter.submit - Update - - - - - filter.discard - Änderungen verwerfen - - - - - filter.clear_filters - Alle Filter zurücksetzen - - - - - filter.title - Filter - - - - - filter.parameter_value_constraint.operator.= - Typ. Wert = - - - - - filter.parameter_value_constraint.operator.!= - Typ. Wert != - - - - - filter.parameter_value_constraint.operator.< - Typ. Wert < - - filter.parameter_value_constraint.operator.> From b19cc13897b196e04f47171ec8246f7559a6dbfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 30 Aug 2025 21:59:36 +0200 Subject: [PATCH 002/228] New translations messages.en.xlf (English) --- translations/messages.en.xlf | 2292 +--------------------------------- 1 file changed, 33 insertions(+), 2259 deletions(-) diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index e0a1edcc..3e21d257 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -242,7 +242,7 @@ part.info.timetravel_hint - Please note that this feature is experimental, so the info may not be correct.]]> + This is how the part appeared before %timestamp%. <i>Please note that this feature is experimental, so the info may not be correct.</i> @@ -731,10 +731,10 @@ user.edit.tfa.disable_tfa_message - all active two-factor authentication methods of the user and delete the backup codes! -
-The user will have to set up all two-factor authentication methods again and print new backup codes!

-Only do this if you are absolutely sure about the identity of the user (seeking help), otherwise the account could be compromised by an attacker!]]>
+ This will disable <b>all active two-factor authentication methods of the user</b> and delete the <b>backup codes</b>! +<br> +The user will have to set up all two-factor authentication methods again and print new backup codes! <br><br> +<b>Only do this if you are absolutely sure about the identity of the user (seeking help), otherwise the account could be compromised by an attacker!</b>
@@ -885,9 +885,9 @@ The user will have to set up all two-factor authentication methods again and pri entity.delete.message - -Sub elements will be moved upwards.]]> + This can not be undone! +<br> +Sub elements will be moved upwards. @@ -1441,7 +1441,7 @@ Sub elements will be moved upwards.]]> homepage.github.text - GitHub project page]]> + Source, downloads, bug reports, to-do-list etc. can be found on <a href="%href%" class="link-external" target="_blank">GitHub project page</a> @@ -1463,7 +1463,7 @@ Sub elements will be moved upwards.]]> homepage.help.text - GitHub page]]> + Help and tips can be found in Wiki the <a href="%href%" class="link-external" target="_blank">GitHub page</a> @@ -1705,7 +1705,7 @@ Sub elements will be moved upwards.]]> email.pw_reset.fallback - %url% and enter the following info]]> + If this does not work for you, go to <a href="%url%">%url%</a> and enter the following info @@ -1735,7 +1735,7 @@ Sub elements will be moved upwards.]]> email.pw_reset.valid_unit %date% - %date%.]]> + The reset token will be valid until <i>%date%</i>. @@ -3578,8 +3578,8 @@ Sub elements will be moved upwards.]]> tfa_google.disable.confirm_message - -Also note that without two-factor authentication, your account is no longer as well protected against attackers!]]> + If you disable the Authenticator App, all backup codes will be deleted, so you may need to reprint them.<br> +Also note that without two-factor authentication, your account is no longer as well protected against attackers! @@ -3599,7 +3599,7 @@ Also note that without two-factor authentication, your account is no longer as w tfa_google.step.download - Google Authenticator oder FreeOTP Authenticator)]]> + Download an authenticator app (e.g. <a class="link-external" target="_blank" href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2">Google Authenticator</a> oder <a class="link-external" target="_blank" href="https://play.google.com/store/apps/details?id=org.fedorahosted.freeotp">FreeOTP Authenticator</a>) @@ -3841,8 +3841,8 @@ Also note that without two-factor authentication, your account is no longer as w tfa_trustedDevices.explanation - all computers here.]]> + When checking the second factor, the current computer can be marked as trustworthy, so no more two-factor checks on this computer are needed. +If you have done this incorrectly or if a computer is no longer trusted, you can reset the status of <i>all </i>computers here. @@ -5313,7 +5313,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can label_options.lines_mode.help - Twig documentation and Wiki for more information.]]> + If you select Twig here, the content field is interpreted as Twig template. See <a href="https://twig.symfony.com/doc/3.x/templates.html">Twig documentation</a> and <a href="https://docs.part-db.de/usage/labels.html#twig-mode">Wiki</a> for more information. @@ -7157,7 +7157,7 @@ Exampletown mass_creation.lines.placeholder - Element 1 Element 1.1 Element 1.1.1 Element 1.2 @@ -7168,2248 +7168,22 @@ Element 1 -> Element 1.1 Element 1 -> Element 1.2 - - - obsolete - obsolete - - - entity.mass_creation.btn - Create - - - - - obsolete - obsolete - - - measurement_unit.edit.is_integer - Is integer - - - - - obsolete - obsolete - - - measurement_unit.edit.is_integer.help - If this option is activated, all values with this unit will be rounded to whole numbers. - - - - - obsolete - obsolete - - - measurement_unit.edit.use_si_prefix - Use SI prefix - - - - - obsolete - obsolete - - - measurement_unit.edit.use_si_prefix.help - If this option is activated, values are outputted with SI prefixes (e.g. 1,2kg instead of 1200g) - - - - - obsolete - obsolete - - - measurement_unit.edit.unit_symbol - Unit symbol - - - - - obsolete - obsolete - - - measurement_unit.edit.unit_symbol.placeholder - e.g. m - - - - - obsolete - obsolete - - - storelocation.edit.is_full.label - Storelocation full - - - - - obsolete - obsolete - - - storelocation.edit.is_full.help - If this option is selected, it is neither possible to add new parts to this storelocation or to increase the amount of existing parts. - - - - - obsolete - obsolete - - - storelocation.limit_to_existing.label - Limit to existing parts - - - - - obsolete - obsolete - - - storelocation.limit_to_existing.help - If this option is activated, it is not possible to add new parts to this storelocation, but the amount of existing parts can be increased. - - - - - obsolete - obsolete - - - storelocation.only_single_part.label - Only single part - - - - - obsolete - obsolete - - - storelocation.only_single_part.help - If this option is activated, only a single part (with every amount) can be assigned to this storage location. Useful for small SMD boxes or feeders. - - - - - obsolete - obsolete - - - storelocation.storage_type.label - Storage type - - - - - obsolete - obsolete - - - storelocation.storage_type.help - You can select a measurement unit here, which a part must have to be able to be assigned to this storage location - - - - - obsolete - obsolete - - - supplier.edit.default_currency - Default currency - - - - - obsolete - obsolete - - - supplier.shipping_costs.label - Shipping Costs - - - - - obsolete - obsolete - - - user.username.placeholder - e.g. j.doe - - - - - obsolete - obsolete - - - user.firstName.placeholder - e.g John - - - - - obsolete - obsolete - - - user.lastName.placeholder - e.g. Doe - - - - - obsolete - obsolete - - - user.email.placeholder - j.doe@ecorp.com - - - - - obsolete - obsolete - - - user.department.placeholder - e.g. Development - - - - - obsolete - obsolete - - - user.settings.pw_new.label - New password - - - - - obsolete - obsolete - - - user.settings.pw_confirm.label - Confirm new password - - - - - obsolete - obsolete - - - user.edit.needs_pw_change - User needs to change password - - - - - obsolete - obsolete - - - user.edit.user_disabled - User disabled (no login possible) - - - - - obsolete - obsolete - - - user.create - Create user - - - - - obsolete - obsolete - - - user.edit.save - Save - - - - - obsolete - obsolete - - - entity.edit.reset - Discard changes - - - - - templates\Parts\show_part_info.html.twig:166 - obsolete - obsolete - - - part.withdraw.btn - Withdraw - - - - - templates\Parts\show_part_info.html.twig:171 - obsolete - obsolete - - - part.withdraw.comment: - Comment/Purpose - - - - - templates\Parts\show_part_info.html.twig:189 - obsolete - obsolete - - - part.add.caption - Add parts - - - - - templates\Parts\show_part_info.html.twig:194 - obsolete - obsolete - - - part.add.btn - Add - - - - - templates\Parts\show_part_info.html.twig:199 - obsolete - obsolete - - - part.add.comment - Comment/Purpose - - - - - templates\AdminPages\CompanyAdminBase.html.twig:15 - obsolete - obsolete - - - admin.comment - Notes - - - - - src\Form\PartType.php:83 - obsolete - obsolete - - - manufacturer_url.label - Manufacturer link - - - - - src\Form\PartType.php:66 - obsolete - obsolete - - - part.description.placeholder - e.g. NPN 45V 0,1A 0,5W - - - - - src\Form\PartType.php:69 - obsolete - obsolete - - - part.instock.placeholder - e.g. 10 - - - - - src\Form\PartType.php:72 - obsolete - obsolete - - - part.mininstock.placeholder - e.g. 5 - - - - - obsolete - obsolete - - - part.order.price_per - Price per - - - - - obsolete - obsolete - - - part.withdraw.caption - Withdraw parts - - - - - obsolete - obsolete - - - datatable.datatable.lengthMenu - _MENU_ - - - - - obsolete - obsolete - - - perm.group.parts - Parts - - - - - obsolete - obsolete - - - perm.group.structures - Data structures - - - - - obsolete - obsolete - - - perm.group.system - System - - - - - obsolete - obsolete - - - perm.parts - Parts - - - - - obsolete - obsolete - - - perm.read - View - - - - - obsolete - obsolete - - - perm.edit - Edit - - - - - obsolete - obsolete - - - perm.create - Create - - - - - obsolete - obsolete - - - perm.part.move - Change category - - - - - obsolete - obsolete - - - perm.delete - Delete - - - - - obsolete - obsolete - - - perm.part.search - Search - - - - - obsolete - obsolete - - - perm.part.all_parts - List all parts - - - - - obsolete - obsolete - - - perm.part.no_price_parts - List parts without price info - - - - - obsolete - obsolete - - - perm.part.obsolete_parts - List obsolete parts - - - - - obsolete - obsolete - - - perm.part.unknown_instock_parts - Show parts with unknown instock - - - - - obsolete - obsolete - - - perm.part.change_favorite - Change favorite status - - - - - obsolete - obsolete - - - perm.part.show_favorite - List favorite parts - - - - - obsolete - obsolete - - - perm.part.show_last_edit_parts - Show last edited/added parts - - - - - obsolete - obsolete - - - perm.part.show_users - Show last modifying user - - - - - obsolete - obsolete - - - perm.part.show_history - Show history - - - - - obsolete - obsolete - - - perm.part.name - Name - - - - - obsolete - obsolete - - - perm.part.description - Description - - - - - obsolete - obsolete - - - perm.part.instock - Instock - - - - - obsolete - obsolete - - - perm.part.mininstock - Minimum instock - - - - - obsolete - obsolete - - - perm.part.comment - Notes - - - - - obsolete - obsolete - - - perm.part.storelocation - Storage location - - - - - obsolete - obsolete - - - perm.part.manufacturer - Manufacturer - - - - - obsolete - obsolete - - - perm.part.orderdetails - Order information - - - - - obsolete - obsolete - - - perm.part.prices - Prices - - - - - obsolete - obsolete - - - perm.part.attachments - File attachments - - - - - obsolete - obsolete - - - perm.part.order - Orders - - - - - obsolete - obsolete - - - perm.storelocations - Storage locations - - - - - obsolete - obsolete - - - perm.move - Move - - - - - obsolete - obsolete - - - perm.list_parts - List parts - - - - - obsolete - obsolete - - - perm.part.footprints - Footprints - - - - - obsolete - obsolete - - - perm.part.categories - Categories - - - - - obsolete - obsolete - - - perm.part.supplier - Suppliers - - - - - obsolete - obsolete - - - perm.part.manufacturers - Manufacturers - - - - - obsolete - obsolete - - - perm.projects - Projects - - - - - obsolete - obsolete - - - perm.part.attachment_types - Attachment types - - - - - obsolete - obsolete - - - perm.tools.import - Import - - - - - obsolete - obsolete - - - perm.tools.labels - Labels - - - - - obsolete - obsolete - - - perm.tools.calculator - Resistor calculator - - - - - obsolete - obsolete - - - perm.tools.footprints - Footprints - - - - - obsolete - obsolete - - - perm.tools.ic_logos - IC logos - - - - - obsolete - obsolete - - - perm.tools.statistics - Statistics - - - - - obsolete - obsolete - - - perm.edit_permissions - Edit permissions - - - - - obsolete - obsolete - - - perm.users.edit_user_name - Edit user name - - - - - obsolete - obsolete - - - perm.users.edit_change_group - Change group - - - - - obsolete - obsolete - - - perm.users.edit_infos - Edit info - - - - - obsolete - obsolete - - - perm.users.edit_permissions - Edit permissions - - - - - obsolete - obsolete - - - perm.users.set_password - Set password - - - - - obsolete - obsolete - - - perm.users.change_user_settings - Change user settings - - - - - obsolete - obsolete - - - perm.database.see_status - Show status - - - - - obsolete - obsolete - - - perm.database.update_db - Update DB - - - - - obsolete - obsolete - - - perm.database.read_db_settings - Read DB settings - - - - - obsolete - obsolete - - - perm.database.write_db_settings - Write DB settings - - - - - obsolete - obsolete - - - perm.config.read_config - Read config - - - - - obsolete - obsolete - - - perm.config.edit_config - Edit config - - - - - obsolete - obsolete - - - perm.config.server_info - Server info - - - - - obsolete - obsolete - - - perm.config.use_debug - Use debug tools - - - - - obsolete - obsolete - - - perm.show_logs - Show logs - - - - - obsolete - obsolete - - - perm.delete_logs - Delete logs - - - - - obsolete - obsolete - - - perm.self.edit_infos - Edit info - - - - - obsolete - obsolete - - - perm.self.edit_username - Edit username - - - - - obsolete - obsolete - - - perm.self.show_permissions - View permissions - - - - - obsolete - obsolete - - - perm.self.show_logs - Show own log entries - - - - - obsolete - obsolete - - - perm.self.create_labels - Create labels - - - - - obsolete - obsolete - - - perm.self.edit_options - Edit options - - - - - obsolete - obsolete - - - perm.self.delete_profiles - Delete profiles - - - - - obsolete - obsolete - - - perm.self.edit_profiles - Edit profiles - - - - - obsolete - obsolete - - - perm.part.tools - Tools - - - - - obsolete - obsolete - - - perm.groups - Groups - - - - - obsolete - obsolete - - - perm.users - Users - - - - - obsolete - obsolete - - - perm.database - Database - - - - - obsolete - obsolete - - - perm.config - Configuration - - - - - obsolete - obsolete - - - perm.system - System - - - - - obsolete - obsolete - - - perm.self - Own user - - - - - obsolete - obsolete - - - perm.labels - Labels - - - - - obsolete - obsolete - - - perm.part.category - Category - - - - - obsolete - obsolete - - - perm.part.minamount - Minimum amount - - - - - obsolete - obsolete - - - perm.part.footprint - Footprint - - - - - obsolete - obsolete - - - perm.part.mpn - MPN - - - - - obsolete - obsolete - - - perm.part.status - Manufacturing status - - - - - obsolete - obsolete - - - perm.part.tags - Tags - - - - - obsolete - obsolete - - - perm.part.unit - Part unit - - - - - obsolete - obsolete - - - perm.part.mass - Mass - - - - - obsolete - obsolete - - - perm.part.lots - Part lots - - - - - obsolete - obsolete - - - perm.show_users - Show last modifying user - - - - - obsolete - obsolete - - - perm.currencies - Currencies - - - - - obsolete - obsolete - - - perm.measurement_units - Measurement unit - - - - - obsolete - obsolete - - - user.settings.pw_old.label - Old password - - - - - obsolete - obsolete - - - pw_reset.submit - Reset password - - - - - obsolete - obsolete - - - u2f_two_factor - Security key (U2F) - - - - - obsolete - obsolete - - - google - Google - - - - - tfa.provider.webauthn_two_factor_provider - Security key - - - - - obsolete - obsolete - - - tfa.provider.google - Authenticator app - - - - - obsolete - obsolete - - - Login successful - Login successful - - - - - obsolete - obsolete - - - log.type.exception - Unhandled exception (obsolete) - - - - - obsolete - obsolete - - - log.type.user_login - User login - - - - - obsolete - obsolete - - - log.type.user_logout - User logout - - - - - obsolete - obsolete - - - log.type.unknown - Unknown - - - - - obsolete - obsolete - - - log.type.element_created - Element created - - - - - obsolete - obsolete - - - log.type.element_edited - Element edited - - - - - obsolete - obsolete - - - log.type.element_deleted - Element deleted - - - - - obsolete - obsolete - - - log.type.database_updated - Database updated - - - - - obsolete - - - perm.revert_elements - Revert element - - - - - obsolete - - - perm.show_history - Show history - - - - - obsolete - - - perm.tools.lastActivity - Show last activity - - - - - obsolete - - - perm.tools.timeTravel - Show old element versions (time travel) - - - - - obsolete - - - tfa_u2f.key_added_successful - Security key added successfully. - - - - - obsolete - - - Username - Username - - - - - obsolete - - - log.type.security.google_disabled - Authenticator App disabled - - - - - obsolete - - - log.type.security.u2f_removed - Security key removed - - - - - obsolete - - - log.type.security.u2f_added - Security key added - - - - - obsolete - - - log.type.security.backup_keys_reset - Backup keys regenerated - - - - - obsolete - - - log.type.security.google_enabled - Authenticator App enabled - - - - - obsolete - - - log.type.security.password_changed - Password changed - - - - - obsolete - - - log.type.security.trusted_device_reset - Trusted devices resetted - - - - - obsolete - - - log.type.collection_element_deleted - Element of Collection deleted - - - - - obsolete - - - log.type.security.password_reset - Password reset - - - - - obsolete - - - log.type.security.2fa_admin_reset - Two Factor Reset by Administrator - - - - - obsolete - - - log.type.user_not_allowed - Unauthorized access attempt - - - - - obsolete - - - log.database_updated.success - Success - - - - - obsolete - - - label_options.barcode_type.2D - 2D - - - - - obsolete - - - label_options.barcode_type.1D - 1D - - - - - obsolete - - - perm.part.parameters - Parameters - - - - - obsolete - - - perm.attachment_show_private - View private attachments - - - - - obsolete - - - perm.tools.label_scanner - Label scanner - - - - - obsolete - - - perm.self.read_profiles - Read profiles - - - - - obsolete - - - perm.self.create_profiles - Create profiles - - - - - obsolete - - - perm.labels.use_twig - Use twig mode - - - - - label_profile.showInDropdown - Show in quick select - - - - - group.edit.enforce_2fa - Enforce Two-factor authentication (2FA) - - - - - group.edit.enforce_2fa.help - If this option is enabled, every direct member of this group, has to configure at least one second-factor for authentication. Recommended for administrative groups with much permissions. - - - - - selectpicker.empty - Nothing selected - - - - - selectpicker.nothing_selected - Nothing selected - - - - - entity.delete.must_not_contain_parts - Element "%PATH%" still contains parts! You have to move the parts, to be able to delete this element. - - - - - entity.delete.must_not_contain_attachments - Attachment type still contains attachments. Change their type, to be able to delete this attachment type. - - - - - entity.delete.must_not_contain_prices - Currency still contains price details. You have to change their currency to be able to delete this element. - - - - - entity.delete.must_not_contain_users - Users still uses this group! Change their group, to be able to delete this group. - - - - - part.table.edit - Edit - - - - - part.table.edit.title - Edit part - - - - - part_list.action.action.title - Select action - - - - - part_list.action.action.group.favorite - Favorite status - - - - - part_list.action.action.favorite - Favorite - - - - - part_list.action.action.unfavorite - Unfavorite - - - - - part_list.action.action.group.change_field - Change field - - - - - part_list.action.action.change_category - Change category - - - - - part_list.action.action.change_footprint - Change footprint - - - - - part_list.action.action.change_manufacturer - Change manufacturer - - - - - part_list.action.action.change_unit - Change part unit - - - - - part_list.action.action.delete - Delete - - - - - part_list.action.submit - Submit - - - - - part_list.action.part_count - %count% parts selected! - - - - - company.edit.quick.website - Open website - - - - - company.edit.quick.email - Send email - - - - - company.edit.quick.phone - Call phone - - - - - company.edit.quick.fax - Send fax - - - - - company.fax_number.placeholder - e.g. +49 1234 567890 - - - - - part.edit.save_and_clone - Save and clone - - - - - validator.file_ext_not_allowed - File extension not allowed for this attachment type. - - - - - tools.reel_calc.title - SMD Reel calculator - - - - - tools.reel_calc.inner_dia - Inner diameter - - - - - tools.reel_calc.outer_dia - Outer diameter - - - - - tools.reel_calc.tape_thick - Tape thickness - - - - - tools.reel_calc.part_distance - Part distance - - - - - tools.reel_calc.update - Update - - - - - tools.reel_calc.parts_per_meter - Parts per meter - - - - - tools.reel_calc.result_length - Tape length - - - - - tools.reel_calc.result_amount - Approx. parts count - - - - - tools.reel_calc.outer_greater_inner_error - Error: Outer diameter must be greater than inner diameter! - - - - - tools.reel_calc.missing_values.error - Please fill in all values! - - - - - tools.reel_calc.load_preset - Load preset - - - - - tools.reel_calc.explanation - This calculator gives you an estimation, how many parts are remaining on an SMD reel. Measure the noted the dimensions on the reel (or use some of the presets) and click "Update" to get an result. - - - - - perm.tools.reel_calculator - SMD Reel calculator - - - - - tree.tools.tools.reel_calculator - SMD Reel calculator - - - - - user.pw_change_needed.flash - Your password needs to be changed! Please set a new password. - - - - - tree.root_node.text - Root node - - - - - part_list.action.select_null - Empty element - - - - - part_list.action.delete-title - Do you really want to delete these parts? - - - - - part_list.action.delete-message - These parts and any associated information (like attachments, price information, etc.) will be deleted. This can not be undone! - - - - - part.table.actions.success - Actions finished successfully. - - - - - attachment.edit.delete.confirm - Do you really want to delete this attachment? - - - - - filter.text_constraint.value.operator.EQ - Is - - - - - filter.text_constraint.value.operator.NEQ - Is not - - - - - filter.text_constraint.value.operator.STARTS - Starts with - - - - - filter.text_constraint.value.operator.CONTAINS - Contains - - - - - filter.text_constraint.value.operator.ENDS - Ends with - - - - - filter.text_constraint.value.operator.LIKE - LIKE pattern - - - - - filter.text_constraint.value.operator.REGEX - Regular expression - - - - - filter.number_constraint.value.operator.BETWEEN - Between - - - - - filter.number_constraint.AND - and - - - - - filter.entity_constraint.operator.EQ - Is (excluding children) - - - - - filter.entity_constraint.operator.NEQ - Is not (excluding children) - - - - - filter.entity_constraint.operator.INCLUDING_CHILDREN - Is (including children) - - - - - filter.entity_constraint.operator.EXCLUDING_CHILDREN - Is not (including children) - - - - - part.filter.dbId - Database ID - - - - - filter.tags_constraint.operator.ANY - Any of the tags - - - - - filter.tags_constraint.operator.ALL - All the tags - - - - - filter.tags_constraint.operator.NONE - None of the tags - - - - - part.filter.lot_count - Number of lots - - - - - part.filter.attachments_count - Number of attachments - - - - - part.filter.orderdetails_count - Number of order details - - - - - part.filter.lotExpirationDate - Lot expiration date - - - - - part.filter.lotNeedsRefill - Any lot needs refill - - - - - part.filter.lotUnknwonAmount - Any lot has unknown amount - - - - - part.filter.attachmentName - Attachment name - - - - - filter.choice_constraint.operator.ANY - Any of - - - - - filter.choice_constraint.operator.NONE - None of - - - - - part.filter.amount_sum - Total amount - - - - - filter.submit - Update - - - - - filter.discard - Discard changes - - - - - filter.clear_filters - Clear all filters - - - - - filter.title - Filter - - - - - filter.parameter_value_constraint.operator.= - Typ. Value = - - - - - filter.parameter_value_constraint.operator.!= - Typ. Value != - - - - - filter.parameter_value_constraint.operator.< - - - filter.parameter_value_constraint.operator.> - ]]> + Typ. Value > filter.parameter_value_constraint.operator.<= - + Typ. Value <= filter.parameter_value_constraint.operator.>= - =]]> + Typ. Value >= @@ -9517,7 +7291,7 @@ Element 1 -> Element 1.2 parts_list.search.searching_for - %keyword%]]> + Searching parts with keyword <b>%keyword%</b> @@ -10177,13 +7951,13 @@ Element 1 -> Element 1.2 project.builds.number_of_builds_possible - %max_builds% builds of this project.]]> + You have enough stocked to build <b>%max_builds%</b> builds of this project. project.builds.check_project_status - "%project_status%". You should check if you really want to build the project with this status!]]> + The current project status is <b>"%project_status%"</b>. You should check if you really want to build the project with this status! @@ -10285,7 +8059,7 @@ Element 1 -> Element 1.2 entity.select.add_hint - to create nested structures, e.g. "Node 1->Node 1.1"]]> + Use -> to create nested structures, e.g. "Node 1->Node 1.1" @@ -10309,13 +8083,13 @@ Element 1 -> Element 1.2 homepage.first_steps.introduction - documentation or start to creating the following data structures:]]> + Your database is still empty. You might want to read the <a href="%url%">documentation</a> or start to creating the following data structures: homepage.first_steps.create_part - create a new part.]]> + Or you can directly <a href="%url%">create a new part</a>. @@ -10327,7 +8101,7 @@ Element 1 -> Element 1.2 homepage.forum.text - discussion forum]]> + For questions about Part-DB use the <a href="%href%" class="link-external" target="_blank">discussion forum</a> @@ -10981,7 +8755,7 @@ Element 1 -> Element 1.2 parts.import.help_documentation - documentation for more information on the file format.]]> + See the <a href="%link%">documentation</a> for more information on the file format. @@ -11161,7 +8935,7 @@ Element 1 -> Element 1.2 part.filter.lessThanDesired - + In stock less than desired (total amount < min. amount) @@ -11973,13 +9747,13 @@ Please note, that you can not impersonate a disabled user. If you try you will g part.merge.confirm.title - %other% into %target%?]]> + Do you really want to merge <b>%other%</b> into <b>%target%</b>? part.merge.confirm.message - %other% will be deleted, and the part will be saved with the shown information.]]> + <b>%other%</b> will be deleted, and the part will be saved with the shown information. From cc70e77deecdaa74b41ddcf3e26677af150d2797 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 30 Aug 2025 22:15:27 +0200 Subject: [PATCH 003/228] Revert "New translations messages.en.xlf (German)" This reverts commit 50f478f7efa61272ececec33d9d0cca03ad8217b. --- translations/messages.de.xlf | 2226 ++++++++++++++++++++++++++++++++++ 1 file changed, 2226 insertions(+) diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index 3c8d3400..a5c18cdd 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -7161,6 +7161,2232 @@ Element 2 Element 3 + + + obsolete + obsolete + + + entity.mass_creation.btn + Anlegen + + + + + obsolete + obsolete + + + measurement_unit.edit.is_integer + Ganzzahlig + + + + + obsolete + obsolete + + + measurement_unit.edit.is_integer.help + Wenn diese Option aktiviert ist, werden alle Mengen in dieser Einheit auf ganze Zahlen gerundet. + + + + + obsolete + obsolete + + + measurement_unit.edit.use_si_prefix + Benutze SI Prefixe + + + + + obsolete + obsolete + + + measurement_unit.edit.use_si_prefix.help + Wenn diese Option aktiviert ist, werden bei Ausgabe der Zahlen SI Prefixe benutzt (z.B. 1,2kg anstatt 1200g) + + + + + obsolete + obsolete + + + measurement_unit.edit.unit_symbol + Einheitensymbol + + + + + obsolete + obsolete + + + measurement_unit.edit.unit_symbol.placeholder + z.B. m + + + + + obsolete + obsolete + + + storelocation.edit.is_full.label + Lagerort voll + + + + + obsolete + obsolete + + + storelocation.edit.is_full.help + Wenn diese Option aktiviert ist, ist es weder möglich neue Bauteile zu diesem Lagerort hinzuzufügen, noch die Anzahl bereits vorhandener Bauteile zu erhöhen. + + + + + obsolete + obsolete + + + storelocation.limit_to_existing.label + Nur bestehende Bauteile + + + + + obsolete + obsolete + + + storelocation.limit_to_existing.help + Wenn diese Option aktiv ist, ist es nicht möglich neue Bauteile zu diesem Lagerort hinzuzufügen, es ist aber möglich die Anzahl bereits vorhandener Bauteile zu erhöhen. + + + + + obsolete + obsolete + + + storelocation.only_single_part.label + Nur ein Bauteil + + + + + obsolete + obsolete + + + storelocation.only_single_part.help + Wenn diese Option aktiviert ist, kann dieser Lagerort nur ein einzelnes Bauteil aber in beliebiger Menge fassen. Hilfreich für kleine SMD Fächer oder Feeder. + + + + + obsolete + obsolete + + + storelocation.storage_type.label + Lagertyp + + + + + obsolete + obsolete + + + storelocation.storage_type.help + Hier kann eine Maßeinheit gewählt werden, die ein Bauteil haben muss, damit es in diesem Lagerort gelagert werden kann. + + + + + obsolete + obsolete + + + supplier.edit.default_currency + Standardwährung + + + + + obsolete + obsolete + + + supplier.shipping_costs.label + Versandkosten + + + + + obsolete + obsolete + + + user.username.placeholder + z.B. m.muster + + + + + obsolete + obsolete + + + user.firstName.placeholder + z.B. Max + + + + + obsolete + obsolete + + + user.lastName.placeholder + z.B. Muster + + + + + obsolete + obsolete + + + user.email.placeholder + z.B. m.muster@ecorp.com + + + + + obsolete + obsolete + + + user.department.placeholder + z.B. Entwicklung + + + + + obsolete + obsolete + + + user.settings.pw_new.label + Neues Passwort + + + + + obsolete + obsolete + + + user.settings.pw_confirm.label + Neues Passwort bestätigen + + + + + obsolete + obsolete + + + user.edit.needs_pw_change + Nutzer muss Passwort ändern + + + + + obsolete + obsolete + + + user.edit.user_disabled + Benutzer deaktiviert (kein Login möglich) + + + + + obsolete + obsolete + + + user.create + Benutzer anlegen + + + + + obsolete + obsolete + + + user.edit.save + Speichern + + + + + obsolete + obsolete + + + entity.edit.reset + Änderungen verwerfen + + + + + templates\Parts\show_part_info.html.twig:166 + obsolete + obsolete + + + part.withdraw.btn + Entnehmen + + + + + templates\Parts\show_part_info.html.twig:171 + obsolete + obsolete + + + part.withdraw.comment: + Kommentar/Zweck + + + + + templates\Parts\show_part_info.html.twig:189 + obsolete + obsolete + + + part.add.caption + Bauteil hinzufügen + + + + + templates\Parts\show_part_info.html.twig:194 + obsolete + obsolete + + + part.add.btn + Hinzufügen + + + + + templates\Parts\show_part_info.html.twig:199 + obsolete + obsolete + + + part.add.comment + Kommentar/Zweck + + + + + templates\AdminPages\CompanyAdminBase.html.twig:15 + obsolete + obsolete + + + admin.comment + Notizen + + + + + src\Form\PartType.php:83 + obsolete + obsolete + + + manufacturer_url.label + Herstellerlink + + + + + src\Form\PartType.php:66 + obsolete + obsolete + + + part.description.placeholder + z.B. NPN 45V 0,1A 0,5W + + + + + src\Form\PartType.php:69 + obsolete + obsolete + + + part.instock.placeholder + z.B. 12 + + + + + src\Form\PartType.php:72 + obsolete + obsolete + + + part.mininstock.placeholder + z.B. 10 + + + + + obsolete + obsolete + + + part.order.price_per + pro + + + + + obsolete + obsolete + + + part.withdraw.caption + Bauteile entnehmen + + + + + obsolete + obsolete + + + datatable.datatable.lengthMenu + _MENU_ + + + + + obsolete + obsolete + + + perm.group.parts + Bauteile + + + + + obsolete + obsolete + + + perm.group.structures + Datenstrukturen + + + + + obsolete + obsolete + + + perm.group.system + System + + + + + obsolete + obsolete + + + perm.parts + Allgemein + + + + + obsolete + obsolete + + + perm.read + Anzeigen + + + + + obsolete + obsolete + + + perm.edit + Bearbeiten + + + + + obsolete + obsolete + + + perm.create + Anlegen + + + + + obsolete + obsolete + + + perm.part.move + Kategorie verändern + + + + + obsolete + obsolete + + + perm.delete + Löschen + + + + + obsolete + obsolete + + + perm.part.search + Suchen + + + + + obsolete + obsolete + + + perm.part.all_parts + Alle Bauteile auflisten + + + + + obsolete + obsolete + + + perm.part.no_price_parts + Teile ohne Preis auflisten + + + + + obsolete + obsolete + + + perm.part.obsolete_parts + Obsolete Teile auflisten + + + + + obsolete + obsolete + + + perm.part.unknown_instock_parts + Bauteile mit unbekanntem Bestand auflisten + + + + + obsolete + obsolete + + + perm.part.change_favorite + Favoritenstatus ändern + + + + + obsolete + obsolete + + + perm.part.show_favorite + Favoriten anzeigen + + + + + obsolete + obsolete + + + perm.part.show_last_edit_parts + Zeige zuletzt bearbeitete/hinzugefügte Bauteile + + + + + obsolete + obsolete + + + perm.part.show_users + Letzten bearbeitenden Nutzer anzeigen + + + + + obsolete + obsolete + + + perm.part.show_history + Historie anzeigen + + + + + obsolete + obsolete + + + perm.part.name + Name + + + + + obsolete + obsolete + + + perm.part.description + Beschreibung + + + + + obsolete + obsolete + + + perm.part.instock + Vorhanden + + + + + obsolete + obsolete + + + perm.part.mininstock + Min. Bestand + + + + + obsolete + obsolete + + + perm.part.comment + Notizen + + + + + obsolete + obsolete + + + perm.part.storelocation + Lagerort + + + + + obsolete + obsolete + + + perm.part.manufacturer + Hersteller + + + + + obsolete + obsolete + + + perm.part.orderdetails + Bestellinformationen + + + + + obsolete + obsolete + + + perm.part.prices + Preise + + + + + obsolete + obsolete + + + perm.part.attachments + Dateianhänge + + + + + obsolete + obsolete + + + perm.part.order + Bestellungen + + + + + obsolete + obsolete + + + perm.storelocations + Lagerorte + + + + + obsolete + obsolete + + + perm.move + Verschieben + + + + + obsolete + obsolete + + + perm.list_parts + Teile auflisten + + + + + obsolete + obsolete + + + perm.part.footprints + Footprints + + + + + obsolete + obsolete + + + perm.part.categories + Kategorien + + + + + obsolete + obsolete + + + perm.part.supplier + Lieferanten + + + + + obsolete + obsolete + + + perm.part.manufacturers + Hersteller + + + + + obsolete + obsolete + + + perm.projects + Projekte + + + + + obsolete + obsolete + + + perm.part.attachment_types + Dateitypen + + + + + obsolete + obsolete + + + perm.tools.import + Import + + + + + obsolete + obsolete + + + perm.tools.labels + Labels + + + + + obsolete + obsolete + + + perm.tools.calculator + Widerstandsrechner + + + + + obsolete + obsolete + + + perm.tools.footprints + Footprints + + + + + obsolete + obsolete + + + perm.tools.ic_logos + IC-Logos + + + + + obsolete + obsolete + + + perm.tools.statistics + Statistik + + + + + obsolete + obsolete + + + perm.edit_permissions + Berechtigungen ändern + + + + + obsolete + obsolete + + + perm.users.edit_user_name + Nutzernamen ändern + + + + + obsolete + obsolete + + + perm.users.edit_change_group + Gruppe ändern + + + + + obsolete + obsolete + + + perm.users.edit_infos + Informationen ändern + + + + + obsolete + obsolete + + + perm.users.edit_permissions + Berechtigungen ändern + + + + + obsolete + obsolete + + + perm.users.set_password + Passwort ändern + + + + + obsolete + obsolete + + + perm.users.change_user_settings + Benutzereinstellungen ändern + + + + + obsolete + obsolete + + + perm.database.see_status + Status anzeigen + + + + + obsolete + obsolete + + + perm.database.update_db + Datenbank updaten + + + + + obsolete + obsolete + + + perm.database.read_db_settings + Einstellungen anzeigen + + + + + obsolete + obsolete + + + perm.database.write_db_settings + Einstellungen ändern + + + + + obsolete + obsolete + + + perm.config.read_config + Konfiguration anzeigen + + + + + obsolete + obsolete + + + perm.config.edit_config + Konfiguration ändern + + + + + obsolete + obsolete + + + perm.config.server_info + Server info + + + + + obsolete + obsolete + + + perm.config.use_debug + Debugtools benutzen + + + + + obsolete + obsolete + + + perm.show_logs + Logs anzeigen + + + + + obsolete + obsolete + + + perm.delete_logs + Logeinträge löschen + + + + + obsolete + obsolete + + + perm.self.edit_infos + Informationen ändern + + + + + obsolete + obsolete + + + perm.self.edit_username + Benutzernamen ändern + + + + + obsolete + obsolete + + + perm.self.show_permissions + Berechtigungen anzeigen + + + + + obsolete + obsolete + + + perm.self.show_logs + Logs anzeigen + + + + + obsolete + obsolete + + + perm.self.create_labels + Labels erstellen + + + + + obsolete + obsolete + + + perm.self.edit_options + Einstellungen ändern + + + + + obsolete + obsolete + + + perm.self.delete_profiles + Profile löschen + + + + + obsolete + obsolete + + + perm.self.edit_profiles + Profile bearbeiten + + + + + obsolete + obsolete + + + perm.part.tools + Tools + + + + + obsolete + obsolete + + + perm.groups + Gruppen + + + + + obsolete + obsolete + + + perm.users + Benutzer + + + + + obsolete + obsolete + + + perm.database + Datenbank + + + + + obsolete + obsolete + + + perm.config + Einstellungen + + + + + obsolete + obsolete + + + perm.system + System + + + + + obsolete + obsolete + + + perm.self + Eigenen Benutzer bearbeiten + + + + + obsolete + obsolete + + + perm.labels + Labels + + + + + obsolete + obsolete + + + perm.part.category + Kategorie + + + + + obsolete + obsolete + + + perm.part.minamount + Mindestbestand + + + + + obsolete + obsolete + + + perm.part.footprint + Footprint + + + + + obsolete + obsolete + + + perm.part.mpn + MPN + + + + + obsolete + obsolete + + + perm.part.status + Herstellungsstatus + + + + + obsolete + obsolete + + + perm.part.tags + Tags + + + + + obsolete + obsolete + + + perm.part.unit + Maßeinheit + + + + + obsolete + obsolete + + + perm.part.mass + Gewicht + + + + + obsolete + obsolete + + + perm.part.lots + Lagerorte + + + + + obsolete + obsolete + + + perm.show_users + Letzten bearbeitenden Nutzer anzeigen + + + + + obsolete + obsolete + + + perm.currencies + Währungen + + + + + obsolete + obsolete + + + perm.measurement_units + Maßeinheiten + + + + + obsolete + obsolete + + + user.settings.pw_old.label + Altes Passwort + + + + + obsolete + obsolete + + + pw_reset.submit + Passwort zurücksetzen + + + + + obsolete + obsolete + + + u2f_two_factor + Sicherheitsschlüssel (U2F) + + + + + obsolete + obsolete + + + google + Google + + + + + tfa.provider.webauthn_two_factor_provider + Sicherheitsschlüssel + + + + + obsolete + obsolete + + + tfa.provider.google + Authenticator App + + + + + obsolete + obsolete + + + Login successful + Login erfolgreich. + + + + + obsolete + obsolete + + + log.type.exception + Unbehandelte Exception (veraltet) + + + + + obsolete + obsolete + + + log.type.user_login + Nutzer eingeloggt + + + + + obsolete + obsolete + + + log.type.user_logout + Nutzer ausgeloggt + + + + + obsolete + obsolete + + + log.type.unknown + Unbekannt + + + + + obsolete + obsolete + + + log.type.element_created + Element angelegt + + + + + obsolete + obsolete + + + log.type.element_edited + Element bearbeitet + + + + + obsolete + obsolete + + + log.type.element_deleted + Element gelöscht + + + + + obsolete + obsolete + + + log.type.database_updated + Datenbank aktualisiert + + + + + obsolete + + + perm.revert_elements + Element zurücksetzen + + + + + obsolete + + + perm.show_history + Historie anzeigen + + + + + obsolete + + + perm.tools.lastActivity + Letzte Aktivität anzeigen + + + + + obsolete + + + perm.tools.timeTravel + Alte Versionsstände anzeigen (Zeitreisen) + + + + + obsolete + + + tfa_u2f.key_added_successful + Sicherheitsschlüssel erfolgreich hinzugefügt. + + + + + obsolete + + + Username + Benutzername + + + + + obsolete + + + log.type.security.google_disabled + Authenticator App deaktiviert + + + + + obsolete + + + log.type.security.u2f_removed + Sicherheitsschlüssel gelöscht + + + + + obsolete + + + log.type.security.u2f_added + Sicherheitsschlüssel hinzugefügt + + + + + obsolete + + + log.type.security.backup_keys_reset + Neue Backupkeys erzeugt + + + + + obsolete + + + log.type.security.google_enabled + Authenticator App aktiviert + + + + + obsolete + + + log.type.security.password_changed + Passwort geändert + + + + + obsolete + + + log.type.security.trusted_device_reset + Vertrauenswürdige Geräte zurückgesetzt + + + + + obsolete + + + log.type.collection_element_deleted + Kollektionselement gelöscht + + + + + obsolete + + + log.type.security.password_reset + Passwort zurückgesetzt + + + + + obsolete + + + log.type.security.2fa_admin_reset + Zwei-Faktor-Authentifizierung durch Administrator zurückgesetzt + + + + + obsolete + + + log.type.user_not_allowed + Unerlaubter Zugriffsversuch + + + + + obsolete + + + log.database_updated.success + Erfolgreich + + + + + obsolete + + + label_options.barcode_type.2D + 2D + + + + + obsolete + + + label_options.barcode_type.1D + 1D + + + + + obsolete + + + perm.part.parameters + Parameter + + + + + obsolete + + + perm.attachment_show_private + Private Anhänge zeigen + + + + + obsolete + + + perm.tools.label_scanner + Labelscanner + + + + + obsolete + + + perm.self.read_profiles + Profile anzeigen + + + + + obsolete + + + perm.self.create_profiles + Profil anlegen + + + + + obsolete + + + perm.labels.use_twig + Twig Modus benutzen + + + + + label_profile.showInDropdown + In Barcode Schnellauswahl anzeigen + + + + + group.edit.enforce_2fa + Erzwinge Zwei-Faktor-Authentifizierung (2FA) + + + + + group.edit.enforce_2fa.help + Wenn diese Option aktiv ist, muss jedes direkte Mitglied dieser Gruppe, mindestens einen zweiten Faktor zur Authentifizierung einrichten. Empfohlen z.B. für administrative Gruppen mit weitreichenden Berechtigungen. + + + + + selectpicker.empty + Nichts ausgewählt + + + + + selectpicker.nothing_selected + Nichts ausgewählt + + + + + entity.delete.must_not_contain_parts + Element "%PATH%" enthält noch Bauteile. Bearbeite die Bauteile, um dieses Element löschen zu können. + + + + + entity.delete.must_not_contain_attachments + Dateityp enthält noch Bauteile. Ändere deren Dateityp, um diesen Dateityp löschen zu können. + + + + + entity.delete.must_not_contain_prices + Währung enthält noch Bauteile. Ändere deren Währung, um diese Währung löschen zu können. + + + + + entity.delete.must_not_contain_users + Benutzer sind noch Teil dieser Gruppe. Ändere deren Gruppe, um diese Gruppe löschen zu können. + + + + + part.table.edit + Ändern + + + + + part.table.edit.title + Bauteil ändern + + + + + part_list.action.action.title + Aktion auswählen + + + + + part_list.action.action.group.favorite + Favorit + + + + + part_list.action.action.favorite + Bauteile favorisieren + + + + + part_list.action.action.unfavorite + Favorisierung aufheben + + + + + part_list.action.action.group.change_field + Change field + + + + + part_list.action.action.change_category + Kategorie ändern + + + + + part_list.action.action.change_footprint + Footprint ändern + + + + + part_list.action.action.change_manufacturer + Hersteller ändern + + + + + part_list.action.action.change_unit + Maßeinheit ändern + + + + + part_list.action.action.delete + Löschen + + + + + part_list.action.submit + Ok + + + + + part_list.action.part_count + %count% Bauteile ausgewählt! + + + + + company.edit.quick.website + Website öffnen + + + + + company.edit.quick.email + E-Mail senden + + + + + company.edit.quick.phone + Call phone + + + + + company.edit.quick.fax + Fax senden + + + + + company.fax_number.placeholder + z.B. +49 1234 567890 + + + + + part.edit.save_and_clone + Speichern und duplizieren + + + + + validator.file_ext_not_allowed + Dateierweiterung nicht erlaubt für diesen Anhangstyp. + + + + + tools.reel_calc.title + SMD Reel Rechner + + + + + tools.reel_calc.inner_dia + Innerer Durchmesser + + + + + tools.reel_calc.outer_dia + Äußerer Durchmesser + + + + + tools.reel_calc.tape_thick + Tape Dicke + + + + + tools.reel_calc.part_distance + Bauteile Abstand + + + + + tools.reel_calc.update + Update + + + + + tools.reel_calc.parts_per_meter + Bauteile pro Meter + + + + + tools.reel_calc.result_length + Tape Länge + + + + + tools.reel_calc.result_amount + Ungefähre Anzahl Bauteile + + + + + tools.reel_calc.outer_greater_inner_error + Fehler: Äußerer Durchmesser muss großer sein als der innere Durchmesser! + + + + + tools.reel_calc.missing_values.error + Bitte alle Werte angeben! + + + + + tools.reel_calc.load_preset + Preset laden + + + + + tools.reel_calc.explanation + Dieser Rechner erlaubt es Abzuschätzen wie viele Bauteile noch auf einer SMD Rolle (Reel) vorhanden sind. Messen Sie die angegeben Dimensionen auf der Rolle nach (oder nutzen Sie die Vorgaben) und drücken Sie "Update". + + + + + perm.tools.reel_calculator + SMD Reel Rechner + + + + + tree.tools.tools.reel_calculator + SMD Reel Rechner + + + + + user.pw_change_needed.flash + Passwortänderung benötigt! Bitte setze ein neues Passwort. + + + + + tree.root_node.text + Wurzel + + + + + part_list.action.select_null + Keine Elemente vorhanden! + + + + + part_list.action.delete-title + Möchten Sie diese Bauteile wirklich löschen? + + + + + part_list.action.delete-message + Diese Bauteile und alle verknüpften Informationen (Anhänge, Preisinformationen, etc.) werden gelöscht. Dies kann nicht rückgängig gemacht werden! + + + + + part.table.actions.success + Aktionen erfolgreich. + + + + + attachment.edit.delete.confirm + Möchten Sie diesen Anhang wirklich löschen? + + + + + filter.text_constraint.value.operator.EQ + Gleich + + + + + filter.text_constraint.value.operator.NEQ + Ungleich + + + + + filter.text_constraint.value.operator.STARTS + Beginnt mit + + + + + filter.text_constraint.value.operator.CONTAINS + Enthält + + + + + filter.text_constraint.value.operator.ENDS + Endet mit + + + + + filter.text_constraint.value.operator.LIKE + LIKE Ausdruck + + + + + filter.text_constraint.value.operator.REGEX + Regulärer Ausdruck + + + + + filter.number_constraint.value.operator.BETWEEN + Zwischen + + + + + filter.number_constraint.AND + und + + + + + filter.entity_constraint.operator.EQ + Gleich (ohne Kindelemente) + + + + + filter.entity_constraint.operator.NEQ + Ungleich (ohne Kindelemente) + + + + + filter.entity_constraint.operator.INCLUDING_CHILDREN + Gleich (inklusive Kindelementen) + + + + + filter.entity_constraint.operator.EXCLUDING_CHILDREN + Nicht gleich (inklusive Kindelemente) + + + + + part.filter.dbId + Datenbank ID + + + + + filter.tags_constraint.operator.ANY + Irgendeiner der Tags + + + + + filter.tags_constraint.operator.ALL + Alle der Tags + + + + + filter.tags_constraint.operator.NONE + Keine der Tags + + + + + part.filter.lot_count + Anzahl der Lagerbestände + + + + + part.filter.attachments_count + Anzahl der Anhänge + + + + + part.filter.orderdetails_count + Anzahl der Bestellinformationen + + + + + part.filter.lotExpirationDate + Bauteilebestand Ablaufdatum + + + + + part.filter.lotNeedsRefill + Lagerbestand benötigt Auffüllung + + + + + part.filter.lotUnknwonAmount + Lagerbestand mit unbekannter Anzahl + + + + + part.filter.attachmentName + Name des Anhangs + + + + + filter.choice_constraint.operator.ANY + Einer der Ausgewählten + + + + + filter.choice_constraint.operator.NONE + Keine der Ausgewählten + + + + + part.filter.amount_sum + Gesamtmenge + + + + + filter.submit + Update + + + + + filter.discard + Änderungen verwerfen + + + + + filter.clear_filters + Alle Filter zurücksetzen + + + + + filter.title + Filter + + + + + filter.parameter_value_constraint.operator.= + Typ. Wert = + + + + + filter.parameter_value_constraint.operator.!= + Typ. Wert != + + + + + filter.parameter_value_constraint.operator.< + Typ. Wert < + + filter.parameter_value_constraint.operator.> From af4ea17faab951137e12692d5e4e3e026235d862 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 31 Aug 2025 01:22:19 +0200 Subject: [PATCH 004/228] Fixed formatting error in english translations --- translations/messages.en.xlf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index e0a1edcc..e65445ce 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -7165,7 +7165,7 @@ Element 2 Element 3 Element 1 -> Element 1.1 -Element 1 -> Element 1.2 +Element 1 -> Element 1.2]]> From e369ce6db99bbaad1a4825020c2cdac576af111f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 31 Aug 2025 01:34:31 +0200 Subject: [PATCH 005/228] Disable searching option on datatables which we do not need and which causes an CSP violation --- config/packages/datatables.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/packages/datatables.yaml b/config/packages/datatables.yaml index 1297fc9d..f1ea4715 100644 --- a/config/packages/datatables.yaml +++ b/config/packages/datatables.yaml @@ -18,7 +18,7 @@ datatables: > <'row' <'col mt-2 input-group flex-nowrap' B l > <'col-auto mt-2' < p >>>" pagingType: 'simple_numbers' - searching: true + searching: false stateSave: true From 08ce1795fcf01acbea096c6513e053dbd524250a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 31 Aug 2025 01:44:26 +0200 Subject: [PATCH 006/228] Use correct column for ordering when the columns were reordered --- assets/js/lib/datatables.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/assets/js/lib/datatables.js b/assets/js/lib/datatables.js index 8e39548b..67bab02d 100644 --- a/assets/js/lib/datatables.js +++ b/assets/js/lib/datatables.js @@ -75,11 +75,10 @@ request._dt = config.name; //Try to resolve the original column index when the column was reordered (using the ColReorder plugin) - //Only do this when _ColReorder_iOrigCol is available - if (settings.aoColumns && settings.aoColumns.length && settings.aoColumns[0]._ColReorder_iOrigCol !== undefined) { + if (dt.colReorder && dt.colReorder.transpose) { if (request.order && request.order.length) { request.order.forEach(function (order) { - order.column = settings.aoColumns[order.column]._ColReorder_iOrigCol; + order.column = dt.colReorder.transpose(order.column, "toOriginal"); }); } } From 431cf236008ab2b1b959cf419523e42290dd0290 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 31 Aug 2025 15:11:31 +0200 Subject: [PATCH 007/228] Do not pollute docker logs with deprecation notices in error case --- config/packages/monolog.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/packages/monolog.yaml b/config/packages/monolog.yaml index 44a078b8..725ebd7c 100644 --- a/config/packages/monolog.yaml +++ b/config/packages/monolog.yaml @@ -69,6 +69,7 @@ when@docker: excluded_http_codes: [404, 405] buffer_size: 50 # How many messages should be saved? Prevent memory leaks include_stacktraces: true + channels: ["!deprecation"] nested: type: stream path: "php://stderr" From e1600cdec913571a88e88ef61617fa0fd412bd4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 31 Aug 2025 15:12:30 +0200 Subject: [PATCH 008/228] New translations messages.en.xlf (Czech) --- translations/messages.cs.xlf | 3675 +++++++++++++++++++++------------- 1 file changed, 2260 insertions(+), 1415 deletions(-) diff --git a/translations/messages.cs.xlf b/translations/messages.cs.xlf index bde090df..c70ad2af 100644 --- a/translations/messages.cs.xlf +++ b/translations/messages.cs.xlf @@ -1,7 +1,7 @@ - + Part-DB1\templates\AdminPages\AttachmentTypeAdmin.html.twig:4 Part-DB1\templates\AdminPages\AttachmentTypeAdmin.html.twig:4 @@ -12,7 +12,7 @@ Typy souborů pro přílohy - + Part-DB1\templates\AdminPages\AttachmentTypeAdmin.html.twig:12 new @@ -22,7 +22,7 @@ Upravit typ souboru - + Part-DB1\templates\AdminPages\AttachmentTypeAdmin.html.twig:16 new @@ -32,7 +32,7 @@ Nový typ souboru - + Part-DB1\templates\AdminPages\CategoryAdmin.html.twig:4 Part-DB1\templates\_sidebar.html.twig:22 @@ -51,7 +51,7 @@ Kategorie - + Part-DB1\templates\AdminPages\CategoryAdmin.html.twig:8 Part-DB1\templates\AdminPages\StorelocationAdmin.html.twig:19 @@ -64,7 +64,7 @@ Možnosti - + Part-DB1\templates\AdminPages\CategoryAdmin.html.twig:9 Part-DB1\templates\AdminPages\CompanyAdminBase.html.twig:15 @@ -77,7 +77,7 @@ Pokročilé - + Part-DB1\templates\AdminPages\CategoryAdmin.html.twig:13 new @@ -87,7 +87,7 @@ Upravit kategorii - + Part-DB1\templates\AdminPages\CategoryAdmin.html.twig:17 new @@ -97,7 +97,7 @@ Nová kategorie - + Part-DB1\templates\AdminPages\CurrencyAdmin.html.twig:4 Part-DB1\templates\AdminPages\CurrencyAdmin.html.twig:4 @@ -107,7 +107,7 @@ Měna - + Part-DB1\templates\AdminPages\CurrencyAdmin.html.twig:12 Part-DB1\templates\AdminPages\CurrencyAdmin.html.twig:12 @@ -117,7 +117,7 @@ Kód ISO - + Part-DB1\templates\AdminPages\CurrencyAdmin.html.twig:15 Part-DB1\templates\AdminPages\CurrencyAdmin.html.twig:15 @@ -127,7 +127,7 @@ Symbol měny - + Part-DB1\templates\AdminPages\CurrencyAdmin.html.twig:29 new @@ -137,7 +137,7 @@ Upravit měnu - + Part-DB1\templates\AdminPages\CurrencyAdmin.html.twig:33 new @@ -147,7 +147,7 @@ Nová měna - + Part-DB1\templates\AdminPages\DeviceAdmin.html.twig:4 Part-DB1\templates\AdminPages\DeviceAdmin.html.twig:4 @@ -158,7 +158,7 @@ Projekt - + Part-DB1\templates\AdminPages\DeviceAdmin.html.twig:8 new @@ -168,7 +168,7 @@ Upravit projekt - + Part-DB1\templates\AdminPages\DeviceAdmin.html.twig:12 new @@ -178,7 +178,7 @@ Nový projekt - + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:19 Part-DB1\templates\_navbar_search.html.twig:67 @@ -201,7 +201,7 @@ Hledat - + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:23 Part-DB1\templates\_sidebar.html.twig:3 @@ -217,7 +217,7 @@ Rozbalit vše - + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:27 Part-DB1\templates\_sidebar.html.twig:4 @@ -233,7 +233,7 @@ Sbalit vše - + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:54 Part-DB1\templates\Parts\info\_sidebar.html.twig:4 @@ -245,7 +245,7 @@ Takto vypadal díl před %timestamp%. <i>Upozorňujeme, že tato funkce je experimentální, takže informace nemusí být správné.</i> - + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:60 Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:60 @@ -256,7 +256,7 @@ Vlastnosti - + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:61 Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:61 @@ -267,7 +267,7 @@ Informace - + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:63 Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:63 @@ -278,7 +278,7 @@ Historie - + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:66 Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:66 @@ -289,7 +289,7 @@ Export - + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:68 Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:68 @@ -300,7 +300,7 @@ Import / Export - + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:69 Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:69 @@ -310,7 +310,7 @@ Hromadné vytváření - + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:82 Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:82 @@ -321,7 +321,7 @@ Obecné - + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:86 Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:86 @@ -331,7 +331,7 @@ Přílohy - + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:90 @@ -340,7 +340,7 @@ Parametr - + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:179 Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:167 @@ -351,7 +351,7 @@ Exportovat všechny prvky - + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:185 Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:173 @@ -361,7 +361,7 @@ Každý řádek bude interpretován jako název prvku, který bude vytvořen. Vnořené struktury můžete vytvářet pomocí odsazení. - + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:45 Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:45 @@ -372,7 +372,7 @@ Upravit prvek "%name" - + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:50 Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:50 @@ -383,7 +383,7 @@ Nový prvek - + Part-DB1\templates\AdminPages\FootprintAdmin.html.twig:4 Part-DB1\templates\_sidebar.html.twig:9 @@ -398,7 +398,7 @@ Otisky - + Part-DB1\templates\AdminPages\FootprintAdmin.html.twig:13 new @@ -408,7 +408,7 @@ Upravit otisk - + Part-DB1\templates\AdminPages\FootprintAdmin.html.twig:17 new @@ -418,7 +418,7 @@ Nový otisk - + Part-DB1\templates\AdminPages\GroupAdmin.html.twig:4 Part-DB1\templates\AdminPages\GroupAdmin.html.twig:4 @@ -428,7 +428,7 @@ Skupiny - + Part-DB1\templates\AdminPages\GroupAdmin.html.twig:9 Part-DB1\templates\AdminPages\UserAdmin.html.twig:16 @@ -440,7 +440,7 @@ Oprávnění - + Part-DB1\templates\AdminPages\GroupAdmin.html.twig:24 new @@ -450,7 +450,7 @@ Upravit skupinu - + Part-DB1\templates\AdminPages\GroupAdmin.html.twig:28 new @@ -460,7 +460,7 @@ Nová skupina - + Part-DB1\templates\AdminPages\LabelProfileAdmin.html.twig:4 @@ -469,7 +469,7 @@ Profily štítků - + Part-DB1\templates\AdminPages\LabelProfileAdmin.html.twig:8 @@ -478,7 +478,7 @@ Pokročilé - + Part-DB1\templates\AdminPages\LabelProfileAdmin.html.twig:9 @@ -487,7 +487,7 @@ Poznámky - + Part-DB1\templates\AdminPages\LabelProfileAdmin.html.twig:55 new @@ -497,7 +497,7 @@ Upravit profil štítku - + Part-DB1\templates\AdminPages\LabelProfileAdmin.html.twig:59 new @@ -507,7 +507,7 @@ Nový profil štítku - + Part-DB1\templates\AdminPages\ManufacturerAdmin.html.twig:4 Part-DB1\templates\AdminPages\ManufacturerAdmin.html.twig:4 @@ -518,7 +518,7 @@ Výrobci - + Part-DB1\templates\AdminPages\ManufacturerAdmin.html.twig:8 new @@ -528,7 +528,7 @@ Upravit výrobce - + Part-DB1\templates\AdminPages\ManufacturerAdmin.html.twig:12 new @@ -538,7 +538,7 @@ Nový výrobce - + Part-DB1\templates\AdminPages\MeasurementUnitAdmin.html.twig:4 Part-DB1\templates\AdminPages\MeasurementUnitAdmin.html.twig:4 @@ -548,7 +548,7 @@ Měrné jednotky - + Part-DB1\templates\AdminPages\StorelocationAdmin.html.twig:5 Part-DB1\templates\_sidebar.html.twig:8 @@ -563,7 +563,7 @@ Umístění - + Part-DB1\templates\AdminPages\StorelocationAdmin.html.twig:32 new @@ -573,7 +573,7 @@ Upravit umístění - + Part-DB1\templates\AdminPages\StorelocationAdmin.html.twig:36 new @@ -583,7 +583,7 @@ Nové místo skladování - + Part-DB1\templates\AdminPages\SupplierAdmin.html.twig:4 Part-DB1\templates\AdminPages\SupplierAdmin.html.twig:4 @@ -594,7 +594,7 @@ Dodavatelé - + Part-DB1\templates\AdminPages\SupplierAdmin.html.twig:16 new @@ -604,7 +604,7 @@ Upravit dodavatele - + Part-DB1\templates\AdminPages\SupplierAdmin.html.twig:20 new @@ -614,7 +614,7 @@ Nový dodavatel - + Part-DB1\templates\AdminPages\UserAdmin.html.twig:8 Part-DB1\templates\AdminPages\UserAdmin.html.twig:8 @@ -624,7 +624,7 @@ Uživatelé - + Part-DB1\templates\AdminPages\UserAdmin.html.twig:14 Part-DB1\templates\AdminPages\UserAdmin.html.twig:14 @@ -634,7 +634,7 @@ Konfigurace - + Part-DB1\templates\AdminPages\UserAdmin.html.twig:15 Part-DB1\templates\AdminPages\UserAdmin.html.twig:15 @@ -644,7 +644,7 @@ Heslo - + Part-DB1\templates\AdminPages\UserAdmin.html.twig:45 Part-DB1\templates\AdminPages\UserAdmin.html.twig:45 @@ -654,7 +654,7 @@ Dvoufaktorové ověřování - + Part-DB1\templates\AdminPages\UserAdmin.html.twig:47 Part-DB1\templates\AdminPages\UserAdmin.html.twig:47 @@ -664,7 +664,7 @@ Aplikace Authenticator aktivní - + Part-DB1\templates\AdminPages\UserAdmin.html.twig:48 Part-DB1\templates\Users\backup_codes.html.twig:15 @@ -678,7 +678,7 @@ Počet zbývajících záložních kódů - + Part-DB1\templates\AdminPages\UserAdmin.html.twig:49 Part-DB1\templates\Users\backup_codes.html.twig:17 @@ -692,7 +692,7 @@ Datum generování záložních kódů - + Part-DB1\templates\AdminPages\UserAdmin.html.twig:53 Part-DB1\templates\AdminPages\UserAdmin.html.twig:60 @@ -704,7 +704,7 @@ Metoda není povolena - + Part-DB1\templates\AdminPages\UserAdmin.html.twig:56 Part-DB1\templates\AdminPages\UserAdmin.html.twig:56 @@ -714,7 +714,7 @@ Aktivní bezpečnostní klíče - + Part-DB1\templates\AdminPages\UserAdmin.html.twig:72 Part-DB1\templates\AdminPages\UserAdmin.html.twig:72 @@ -724,7 +724,7 @@ Opravdu chcete pokračovat? - + Part-DB1\templates\AdminPages\UserAdmin.html.twig:72 Part-DB1\templates\AdminPages\UserAdmin.html.twig:72 @@ -737,7 +737,7 @@ Uživatel bude muset znovu nastavit všechny metody dvoufaktorového ověřován <b>Toto proveďte pouze v případě, že jste si naprosto jisti identitou uživatele (hledajícího pomoc), jinak by mohl být účet kompromitován útočníkem!</b> - + Part-DB1\templates\AdminPages\UserAdmin.html.twig:73 Part-DB1\templates\AdminPages\UserAdmin.html.twig:73 @@ -747,7 +747,7 @@ Uživatel bude muset znovu nastavit všechny metody dvoufaktorového ověřován Zakázat všechny metody dvoufaktorového ověřování - + Part-DB1\templates\AdminPages\UserAdmin.html.twig:85 new @@ -757,7 +757,7 @@ Uživatel bude muset znovu nastavit všechny metody dvoufaktorového ověřován Upravit uživatele - + Part-DB1\templates\AdminPages\UserAdmin.html.twig:89 new @@ -767,7 +767,7 @@ Uživatel bude muset znovu nastavit všechny metody dvoufaktorového ověřován Nový uživatel - + Part-DB1\templates\AdminPages\_attachments.html.twig:4 Part-DB1\templates\Parts\edit\_attachments.html.twig:4 @@ -780,21 +780,13 @@ Uživatel bude muset znovu nastavit všechny metody dvoufaktorového ověřován Smazat - - - Part-DB1\templates\AdminPages\_attachments.html.twig:41 - Part-DB1\templates\Parts\edit\_attachments.html.twig:38 - Part-DB1\templates\Parts\info\_attachments_info.html.twig:35 - Part-DB1\src\DataTables\AttachmentDataTable.php:159 - Part-DB1\templates\Parts\edit\_attachments.html.twig:38 - Part-DB1\src\DataTables\AttachmentDataTable.php:159 - + - attachment.external - Externí + attachment.external_only + Pouze externí - + Part-DB1\templates\AdminPages\_attachments.html.twig:49 Part-DB1\templates\Parts\edit\_attachments.html.twig:47 @@ -806,7 +798,7 @@ Uživatel bude muset znovu nastavit všechny metody dvoufaktorového ověřován Náhled přílohy - + Part-DB1\templates\AdminPages\_attachments.html.twig:52 Part-DB1\templates\Parts\edit\_attachments.html.twig:50 @@ -816,11 +808,11 @@ Uživatel bude muset znovu nastavit všechny metody dvoufaktorového ověřován Part-DB1\templates\Parts\info\_attachments_info.html.twig:45 - attachment.view - Zobrazit + attachment.view_local + Zobrazit místní kopii - + Part-DB1\templates\AdminPages\_attachments.html.twig:58 Part-DB1\templates\Parts\edit\_attachments.html.twig:56 @@ -836,7 +828,7 @@ Uživatel bude muset znovu nastavit všechny metody dvoufaktorového ověřován Soubor nebyl nalezen - + Part-DB1\templates\AdminPages\_attachments.html.twig:66 Part-DB1\templates\Parts\edit\_attachments.html.twig:64 @@ -848,7 +840,7 @@ Uživatel bude muset znovu nastavit všechny metody dvoufaktorového ověřován Soukromá příloha - + Part-DB1\templates\AdminPages\_attachments.html.twig:79 Part-DB1\templates\Parts\edit\_attachments.html.twig:77 @@ -860,7 +852,7 @@ Uživatel bude muset znovu nastavit všechny metody dvoufaktorového ověřován Přidat přílohu - + Part-DB1\templates\AdminPages\_attachments.html.twig:84 Part-DB1\templates\Parts\edit\_attachments.html.twig:82 @@ -874,7 +866,7 @@ Uživatel bude muset znovu nastavit všechny metody dvoufaktorového ověřován Opravdu chcete tuto zásobu smazat? To nelze vzít zpět! - + Part-DB1\templates\AdminPages\_delete_form.html.twig:2 Part-DB1\templates\AdminPages\_delete_form.html.twig:2 @@ -885,7 +877,7 @@ Uživatel bude muset znovu nastavit všechny metody dvoufaktorového ověřován Opravdu chcete smazat %name%? - + Part-DB1\templates\AdminPages\_delete_form.html.twig:3 Part-DB1\templates\AdminPages\_delete_form.html.twig:3 @@ -898,7 +890,7 @@ Uživatel bude muset znovu nastavit všechny metody dvoufaktorového ověřován Související prvky budou přesunuty nahoru. - + Part-DB1\templates\AdminPages\_delete_form.html.twig:11 Part-DB1\templates\AdminPages\_delete_form.html.twig:11 @@ -909,7 +901,7 @@ Související prvky budou přesunuty nahoru. Odstranit prvek - + Part-DB1\templates\AdminPages\_delete_form.html.twig:16 Part-DB1\templates\Parts\info\_tools.html.twig:45 @@ -924,7 +916,7 @@ Související prvky budou přesunuty nahoru. Změnit komentář - + Part-DB1\templates\AdminPages\_delete_form.html.twig:24 Part-DB1\templates\AdminPages\_delete_form.html.twig:24 @@ -935,7 +927,7 @@ Související prvky budou přesunuty nahoru. Odstranit rekurzivně (všechny související prvky) - + Part-DB1\templates\AdminPages\_duplicate.html.twig:3 @@ -944,7 +936,7 @@ Související prvky budou přesunuty nahoru. Duplikovat prvek - + Part-DB1\templates\AdminPages\_export_form.html.twig:4 Part-DB1\src\Form\AdminPages\ImportType.php:76 @@ -958,7 +950,7 @@ Související prvky budou přesunuty nahoru. Formát souboru - + Part-DB1\templates\AdminPages\_export_form.html.twig:16 Part-DB1\templates\AdminPages\_export_form.html.twig:16 @@ -969,7 +961,7 @@ Související prvky budou přesunuty nahoru. Úroveň podrobností - + Part-DB1\templates\AdminPages\_export_form.html.twig:19 Part-DB1\templates\AdminPages\_export_form.html.twig:19 @@ -980,7 +972,7 @@ Související prvky budou přesunuty nahoru. Jednoduchý - + Part-DB1\templates\AdminPages\_export_form.html.twig:20 Part-DB1\templates\AdminPages\_export_form.html.twig:20 @@ -991,7 +983,7 @@ Související prvky budou přesunuty nahoru. Rozšířený - + Part-DB1\templates\AdminPages\_export_form.html.twig:21 Part-DB1\templates\AdminPages\_export_form.html.twig:21 @@ -1002,7 +994,7 @@ Související prvky budou přesunuty nahoru. Úplný - + Part-DB1\templates\AdminPages\_export_form.html.twig:31 Part-DB1\templates\AdminPages\_export_form.html.twig:31 @@ -1013,7 +1005,7 @@ Související prvky budou přesunuty nahoru. Zahrnutí podřízených prvků do exportu - + Part-DB1\templates\AdminPages\_export_form.html.twig:39 Part-DB1\templates\AdminPages\_export_form.html.twig:39 @@ -1024,7 +1016,7 @@ Související prvky budou přesunuty nahoru. Export - + Part-DB1\templates\AdminPages\_info.html.twig:4 Part-DB1\templates\Parts\edit\edit_part_info.html.twig:12 @@ -1043,7 +1035,7 @@ Související prvky budou přesunuty nahoru. ID - + Part-DB1\templates\AdminPages\_info.html.twig:11 Part-DB1\templates\Parts\info\_attachments_info.html.twig:76 @@ -1067,7 +1059,7 @@ Související prvky budou přesunuty nahoru. Vytvořeno - + Part-DB1\templates\AdminPages\_info.html.twig:25 Part-DB1\templates\Parts\info\_extended_infos.html.twig:21 @@ -1085,7 +1077,7 @@ Související prvky budou přesunuty nahoru. Naposledy upraveno - + Part-DB1\templates\AdminPages\_info.html.twig:38 Part-DB1\templates\AdminPages\_info.html.twig:38 @@ -1095,7 +1087,7 @@ Související prvky budou přesunuty nahoru. Počet dílů s tímto prvkem - + Part-DB1\templates\AdminPages\_parameters.html.twig:6 Part-DB1\templates\helper.twig:125 @@ -1106,7 +1098,7 @@ Související prvky budou přesunuty nahoru. Parametr - + Part-DB1\templates\AdminPages\_parameters.html.twig:7 Part-DB1\templates\Parts\edit\_specifications.html.twig:7 @@ -1116,7 +1108,7 @@ Související prvky budou přesunuty nahoru. Symbol - + Part-DB1\templates\AdminPages\_parameters.html.twig:8 Part-DB1\templates\Parts\edit\_specifications.html.twig:8 @@ -1126,7 +1118,7 @@ Související prvky budou přesunuty nahoru. Min. - + Part-DB1\templates\AdminPages\_parameters.html.twig:9 Part-DB1\templates\Parts\edit\_specifications.html.twig:9 @@ -1136,7 +1128,7 @@ Související prvky budou přesunuty nahoru. Typ. - + Part-DB1\templates\AdminPages\_parameters.html.twig:10 Part-DB1\templates\Parts\edit\_specifications.html.twig:10 @@ -1146,7 +1138,7 @@ Související prvky budou přesunuty nahoru. Max. - + Part-DB1\templates\AdminPages\_parameters.html.twig:11 Part-DB1\templates\Parts\edit\_specifications.html.twig:11 @@ -1156,7 +1148,7 @@ Související prvky budou přesunuty nahoru. Jednotka - + Part-DB1\templates\AdminPages\_parameters.html.twig:12 Part-DB1\templates\Parts\edit\_specifications.html.twig:12 @@ -1166,7 +1158,7 @@ Související prvky budou přesunuty nahoru. Text - + Part-DB1\templates\AdminPages\_parameters.html.twig:13 Part-DB1\templates\Parts\edit\_specifications.html.twig:13 @@ -1176,7 +1168,7 @@ Související prvky budou přesunuty nahoru. Skupina - + Part-DB1\templates\AdminPages\_parameters.html.twig:26 Part-DB1\templates\Parts\edit\_specifications.html.twig:26 @@ -1186,7 +1178,7 @@ Související prvky budou přesunuty nahoru. Nový parametr - + Part-DB1\templates\AdminPages\_parameters.html.twig:31 Part-DB1\templates\Parts\edit\_specifications.html.twig:31 @@ -1196,7 +1188,7 @@ Související prvky budou přesunuty nahoru. Opravdu chcete tento parametr odstranit? - + Part-DB1\templates\attachment_list.html.twig:3 Part-DB1\templates\attachment_list.html.twig:3 @@ -1206,7 +1198,7 @@ Související prvky budou přesunuty nahoru. Seznam příloh - + Part-DB1\templates\attachment_list.html.twig:10 Part-DB1\templates\LogSystem\_log_table.html.twig:8 @@ -1220,7 +1212,7 @@ Související prvky budou přesunuty nahoru. Načítání - + Part-DB1\templates\attachment_list.html.twig:11 Part-DB1\templates\LogSystem\_log_table.html.twig:9 @@ -1234,7 +1226,7 @@ Související prvky budou přesunuty nahoru. To může chvíli trvat. Pokud tato zpráva nezmizí, zkuste stránku načíst znovu. - + Part-DB1\templates\base.html.twig:68 Part-DB1\templates\base.html.twig:68 @@ -1245,7 +1237,7 @@ Související prvky budou přesunuty nahoru. Chcete-li používat všechny funkce, aktivujte prosím JavaScript! - + Part-DB1\templates\base.html.twig:73 Part-DB1\templates\base.html.twig:73 @@ -1255,7 +1247,7 @@ Související prvky budou přesunuty nahoru. Zobrazit/skrýt postranní panel - + Part-DB1\templates\base.html.twig:95 Part-DB1\templates\base.html.twig:95 @@ -1266,7 +1258,7 @@ Související prvky budou přesunuty nahoru. Načítání: - + Part-DB1\templates\base.html.twig:96 Part-DB1\templates\base.html.twig:96 @@ -1277,7 +1269,7 @@ Související prvky budou přesunuty nahoru. To může chvíli trvat. Pokud tato zpráva zůstává dlouho, zkuste stránku znovu načíst. - + Part-DB1\templates\base.html.twig:101 Part-DB1\templates\base.html.twig:101 @@ -1288,7 +1280,7 @@ Související prvky budou přesunuty nahoru. Načítání... - + Part-DB1\templates\base.html.twig:112 Part-DB1\templates\base.html.twig:112 @@ -1299,7 +1291,7 @@ Související prvky budou přesunuty nahoru. Zpět na začátek stránky - + Part-DB1\templates\Form\permissionLayout.html.twig:35 Part-DB1\templates\Form\permissionLayout.html.twig:35 @@ -1309,7 +1301,7 @@ Související prvky budou přesunuty nahoru. Oprávnění - + Part-DB1\templates\Form\permissionLayout.html.twig:36 Part-DB1\templates\Form\permissionLayout.html.twig:36 @@ -1319,7 +1311,7 @@ Související prvky budou přesunuty nahoru. Hodnota - + Part-DB1\templates\Form\permissionLayout.html.twig:53 Part-DB1\templates\Form\permissionLayout.html.twig:53 @@ -1329,7 +1321,7 @@ Související prvky budou přesunuty nahoru. Vysvětlení režimů - + Part-DB1\templates\Form\permissionLayout.html.twig:57 Part-DB1\templates\Form\permissionLayout.html.twig:57 @@ -1339,7 +1331,7 @@ Související prvky budou přesunuty nahoru. Zakázáno - + Part-DB1\templates\Form\permissionLayout.html.twig:61 Part-DB1\templates\Form\permissionLayout.html.twig:61 @@ -1349,7 +1341,7 @@ Související prvky budou přesunuty nahoru. Povoleno - + Part-DB1\templates\Form\permissionLayout.html.twig:65 Part-DB1\templates\Form\permissionLayout.html.twig:65 @@ -1359,7 +1351,7 @@ Související prvky budou přesunuty nahoru. Převzít z (nadřazené) skupiny - + Part-DB1\templates\helper.twig:3 Part-DB1\templates\helper.twig:3 @@ -1369,7 +1361,7 @@ Související prvky budou přesunuty nahoru. Ano - + Part-DB1\templates\helper.twig:5 Part-DB1\templates\helper.twig:5 @@ -1379,7 +1371,7 @@ Související prvky budou přesunuty nahoru. Ne - + Part-DB1\templates\helper.twig:92 Part-DB1\templates\helper.twig:87 @@ -1389,7 +1381,7 @@ Související prvky budou přesunuty nahoru. Ano - + Part-DB1\templates\helper.twig:94 Part-DB1\templates\helper.twig:89 @@ -1399,7 +1391,7 @@ Související prvky budou přesunuty nahoru. Ne - + Part-DB1\templates\helper.twig:126 @@ -1408,7 +1400,7 @@ Související prvky budou přesunuty nahoru. Hodnota - + Part-DB1\templates\homepage.html.twig:7 Part-DB1\templates\homepage.html.twig:7 @@ -1419,7 +1411,7 @@ Související prvky budou přesunuty nahoru. Verze - + Part-DB1\templates\homepage.html.twig:22 Part-DB1\templates\homepage.html.twig:22 @@ -1430,7 +1422,7 @@ Související prvky budou přesunuty nahoru. Informace o licenci - + Part-DB1\templates\homepage.html.twig:31 Part-DB1\templates\homepage.html.twig:31 @@ -1441,7 +1433,7 @@ Související prvky budou přesunuty nahoru. Stránka projektu - + Part-DB1\templates\homepage.html.twig:31 Part-DB1\templates\homepage.html.twig:31 @@ -1452,7 +1444,7 @@ Související prvky budou přesunuty nahoru. Zdrojové kódy, soubory ke stažení, hlášení chyb, seznam úkolů atd. najdete na <a href="%href%" class="link-external" target="_blank">stránce projektu GitHub</a> - + Part-DB1\templates\homepage.html.twig:32 Part-DB1\templates\homepage.html.twig:32 @@ -1463,7 +1455,7 @@ Související prvky budou přesunuty nahoru. Nápověda - + Part-DB1\templates\homepage.html.twig:32 Part-DB1\templates\homepage.html.twig:32 @@ -1474,7 +1466,7 @@ Související prvky budou přesunuty nahoru. Nápovědu a tipy najdete na Wiki na <a href="%href%" class="link-external" target="_blank">stránce GitHub</a> - + Part-DB1\templates\homepage.html.twig:33 Part-DB1\templates\homepage.html.twig:33 @@ -1485,7 +1477,7 @@ Související prvky budou přesunuty nahoru. Fórum - + Part-DB1\templates\homepage.html.twig:45 Part-DB1\templates\homepage.html.twig:45 @@ -1496,7 +1488,7 @@ Související prvky budou přesunuty nahoru. Poslední aktivita - + Part-DB1\templates\LabelSystem\dialog.html.twig:3 Part-DB1\templates\LabelSystem\dialog.html.twig:6 @@ -1506,7 +1498,7 @@ Související prvky budou přesunuty nahoru. Generátor štítků - + Part-DB1\templates\LabelSystem\dialog.html.twig:16 @@ -1515,7 +1507,7 @@ Související prvky budou přesunuty nahoru. Obecné - + Part-DB1\templates\LabelSystem\dialog.html.twig:20 @@ -1524,7 +1516,7 @@ Související prvky budou přesunuty nahoru. Pokročilé - + Part-DB1\templates\LabelSystem\dialog.html.twig:24 @@ -1533,7 +1525,7 @@ Související prvky budou přesunuty nahoru. Profily - + Part-DB1\templates\LabelSystem\dialog.html.twig:58 @@ -1542,7 +1534,7 @@ Související prvky budou přesunuty nahoru. Aktuálně vybraný profil - + Part-DB1\templates\LabelSystem\dialog.html.twig:62 @@ -1551,7 +1543,7 @@ Související prvky budou přesunuty nahoru. Upravit profil - + Part-DB1\templates\LabelSystem\dialog.html.twig:75 @@ -1560,7 +1552,7 @@ Související prvky budou přesunuty nahoru. Načíst profil - + Part-DB1\templates\LabelSystem\dialog.html.twig:102 @@ -1569,7 +1561,7 @@ Související prvky budou přesunuty nahoru. Stáhnout - + Part-DB1\templates\LabelSystem\dropdown_macro.html.twig:3 Part-DB1\templates\LabelSystem\dropdown_macro.html.twig:5 @@ -1579,7 +1571,7 @@ Související prvky budou přesunuty nahoru. Vytvořit štítek - + Part-DB1\templates\LabelSystem\dropdown_macro.html.twig:20 @@ -1588,7 +1580,7 @@ Související prvky budou přesunuty nahoru. Nový prázdný štítek - + Part-DB1\templates\LabelSystem\Scanner\dialog.html.twig:3 @@ -1597,7 +1589,7 @@ Související prvky budou přesunuty nahoru. Čtečka štítků - + Part-DB1\templates\LabelSystem\Scanner\dialog.html.twig:7 @@ -1606,7 +1598,7 @@ Související prvky budou přesunuty nahoru. Nebyla nalezena žádná webová kamera - + Part-DB1\templates\LabelSystem\Scanner\dialog.html.twig:7 @@ -1615,7 +1607,7 @@ Související prvky budou přesunuty nahoru. Potřebujete webovou kameru a povolení k použití funkce čtečky. Kód čárového kódu můžete zadat ručně níže. - + Part-DB1\templates\LabelSystem\Scanner\dialog.html.twig:27 @@ -1624,7 +1616,7 @@ Související prvky budou přesunuty nahoru. Vybrat zdroj - + Part-DB1\templates\LogSystem\log_list.html.twig:3 Part-DB1\templates\LogSystem\log_list.html.twig:3 @@ -1634,7 +1626,7 @@ Související prvky budou přesunuty nahoru. Systémový protokol - + Part-DB1\templates\LogSystem\_log_table.html.twig:1 Part-DB1\templates\LogSystem\_log_table.html.twig:1 @@ -1645,7 +1637,7 @@ Související prvky budou přesunuty nahoru. Opravdu vrátit změnu / vrátit se k časovému razítku? - + Part-DB1\templates\LogSystem\_log_table.html.twig:2 Part-DB1\templates\LogSystem\_log_table.html.twig:2 @@ -1656,7 +1648,7 @@ Související prvky budou přesunuty nahoru. Opravdu chcete vrátit danou změnu / resetovat prvek na dané časové razítko? - + Part-DB1\templates\mail\base.html.twig:24 Part-DB1\templates\mail\base.html.twig:24 @@ -1666,7 +1658,7 @@ Související prvky budou přesunuty nahoru. Tento e-mail byl automaticky odeslán - + Part-DB1\templates\mail\base.html.twig:24 Part-DB1\templates\mail\base.html.twig:24 @@ -1676,7 +1668,7 @@ Související prvky budou přesunuty nahoru. Na tento e-mail neodpovídejte. - + Part-DB1\templates\mail\pw_reset.html.twig:6 Part-DB1\templates\mail\pw_reset.html.twig:6 @@ -1686,7 +1678,7 @@ Související prvky budou přesunuty nahoru. Ahoj %name% - + Part-DB1\templates\mail\pw_reset.html.twig:7 Part-DB1\templates\mail\pw_reset.html.twig:7 @@ -1696,7 +1688,7 @@ Související prvky budou přesunuty nahoru. někdo (doufejme, že vy) požádal o obnovení vašeho hesla. Pokud jste tuto žádost nepodali vy, ignorujte tento e-mail. - + Part-DB1\templates\mail\pw_reset.html.twig:9 Part-DB1\templates\mail\pw_reset.html.twig:9 @@ -1706,7 +1698,7 @@ Související prvky budou přesunuty nahoru. Klikněte zde pro obnovení hesla - + Part-DB1\templates\mail\pw_reset.html.twig:11 Part-DB1\templates\mail\pw_reset.html.twig:11 @@ -1716,7 +1708,7 @@ Související prvky budou přesunuty nahoru. Pokud vám to nefunguje, přejděte na <a href="%url%">%url%</a> a zadejte následující informace. - + Part-DB1\templates\mail\pw_reset.html.twig:16 Part-DB1\templates\mail\pw_reset.html.twig:16 @@ -1726,7 +1718,7 @@ Související prvky budou přesunuty nahoru. Uživatelské jméno - + Part-DB1\templates\mail\pw_reset.html.twig:19 Part-DB1\templates\mail\pw_reset.html.twig:19 @@ -1736,7 +1728,7 @@ Související prvky budou přesunuty nahoru. Token - + Part-DB1\templates\mail\pw_reset.html.twig:24 Part-DB1\templates\mail\pw_reset.html.twig:24 @@ -1746,7 +1738,7 @@ Související prvky budou přesunuty nahoru. Token obnovení bude platný do <i>%date%</i>. - + Part-DB1\templates\Parts\edit\edit_form_styles.html.twig:18 Part-DB1\templates\Parts\edit\edit_form_styles.html.twig:58 @@ -1758,7 +1750,7 @@ Související prvky budou přesunuty nahoru. Odstranit - + Part-DB1\templates\Parts\edit\edit_form_styles.html.twig:39 Part-DB1\templates\Parts\edit\edit_form_styles.html.twig:39 @@ -1768,7 +1760,7 @@ Související prvky budou přesunuty nahoru. Minimální množství slevy - + Part-DB1\templates\Parts\edit\edit_form_styles.html.twig:40 Part-DB1\templates\Parts\edit\edit_form_styles.html.twig:40 @@ -1778,7 +1770,7 @@ Související prvky budou přesunuty nahoru. Cena - + Part-DB1\templates\Parts\edit\edit_form_styles.html.twig:41 Part-DB1\templates\Parts\edit\edit_form_styles.html.twig:41 @@ -1788,7 +1780,7 @@ Související prvky budou přesunuty nahoru. za množství - + Part-DB1\templates\Parts\edit\edit_form_styles.html.twig:54 Part-DB1\templates\Parts\edit\edit_form_styles.html.twig:54 @@ -1798,7 +1790,7 @@ Související prvky budou přesunuty nahoru. Přidat cenu - + Part-DB1\templates\Parts\edit\edit_part_info.html.twig:4 Part-DB1\templates\Parts\edit\edit_part_info.html.twig:4 @@ -1809,7 +1801,7 @@ Související prvky budou přesunuty nahoru. Upravit díl - + Part-DB1\templates\Parts\edit\edit_part_info.html.twig:9 Part-DB1\templates\Parts\edit\edit_part_info.html.twig:9 @@ -1820,7 +1812,7 @@ Související prvky budou přesunuty nahoru. Upravit díl - + Part-DB1\templates\Parts\edit\edit_part_info.html.twig:22 Part-DB1\templates\Parts\edit\edit_part_info.html.twig:22 @@ -1830,7 +1822,7 @@ Související prvky budou přesunuty nahoru. Obecné - + Part-DB1\templates\Parts\edit\edit_part_info.html.twig:28 Part-DB1\templates\Parts\edit\edit_part_info.html.twig:28 @@ -1840,7 +1832,7 @@ Související prvky budou přesunuty nahoru. Výrobce - + Part-DB1\templates\Parts\edit\edit_part_info.html.twig:34 Part-DB1\templates\Parts\edit\edit_part_info.html.twig:34 @@ -1850,7 +1842,7 @@ Související prvky budou přesunuty nahoru. Pokročilé - + Part-DB1\templates\Parts\edit\edit_part_info.html.twig:40 Part-DB1\templates\Parts\edit\edit_part_info.html.twig:40 @@ -1860,7 +1852,7 @@ Související prvky budou přesunuty nahoru. Zásoby - + Part-DB1\templates\Parts\edit\edit_part_info.html.twig:46 Part-DB1\templates\Parts\edit\edit_part_info.html.twig:46 @@ -1870,7 +1862,7 @@ Související prvky budou přesunuty nahoru. Přílohy - + Part-DB1\templates\Parts\edit\edit_part_info.html.twig:52 Part-DB1\templates\Parts\edit\edit_part_info.html.twig:52 @@ -1880,7 +1872,7 @@ Související prvky budou přesunuty nahoru. Informace o nákupu - + Part-DB1\templates\Parts\edit\edit_part_info.html.twig:58 @@ -1889,7 +1881,7 @@ Související prvky budou přesunuty nahoru. Parametry - + Part-DB1\templates\Parts\edit\edit_part_info.html.twig:64 Part-DB1\templates\Parts\edit\edit_part_info.html.twig:58 @@ -1899,7 +1891,7 @@ Související prvky budou přesunuty nahoru. Poznámky - + Part-DB1\templates\Parts\edit\new_part.html.twig:8 Part-DB1\templates\Parts\edit\new_part.html.twig:8 @@ -1910,7 +1902,7 @@ Související prvky budou přesunuty nahoru. Přidat nový díl - + Part-DB1\templates\Parts\edit\_lots.html.twig:5 Part-DB1\templates\Parts\edit\_lots.html.twig:5 @@ -1920,7 +1912,7 @@ Související prvky budou přesunuty nahoru. Odstranit - + Part-DB1\templates\Parts\edit\_lots.html.twig:28 Part-DB1\templates\Parts\edit\_lots.html.twig:28 @@ -1930,7 +1922,7 @@ Související prvky budou přesunuty nahoru. Přidat zásoby - + Part-DB1\templates\Parts\edit\_orderdetails.html.twig:13 Part-DB1\templates\Parts\edit\_orderdetails.html.twig:13 @@ -1940,7 +1932,7 @@ Související prvky budou přesunuty nahoru. Přidat distributora - + Part-DB1\templates\Parts\edit\_orderdetails.html.twig:18 Part-DB1\templates\Parts\edit\_orderdetails.html.twig:18 @@ -1950,7 +1942,7 @@ Související prvky budou přesunuty nahoru. Opravdu chcete tuto cenu smazat? To nelze vzít zpět. - + Part-DB1\templates\Parts\edit\_orderdetails.html.twig:62 Part-DB1\templates\Parts\edit\_orderdetails.html.twig:61 @@ -1960,7 +1952,7 @@ Související prvky budou přesunuty nahoru. Opravdu chcete smazat informace o distributorovi? To nelze vzít zpět! - + Part-DB1\templates\Parts\info\show_part_info.html.twig:4 Part-DB1\templates\Parts\info\show_part_info.html.twig:19 @@ -1974,7 +1966,7 @@ Související prvky budou přesunuty nahoru. Detailní informace o dílu - + Part-DB1\templates\Parts\info\show_part_info.html.twig:47 Part-DB1\templates\Parts\info\show_part_info.html.twig:47 @@ -1984,7 +1976,7 @@ Související prvky budou přesunuty nahoru. Zásoby - + Part-DB1\templates\Parts\info\show_part_info.html.twig:56 Part-DB1\templates\Parts\lists\_info_card.html.twig:43 @@ -1999,7 +1991,7 @@ Související prvky budou přesunuty nahoru. Poznámky - + Part-DB1\templates\Parts\info\show_part_info.html.twig:64 @@ -2008,7 +2000,7 @@ Související prvky budou přesunuty nahoru. Parametry - + Part-DB1\templates\Parts\info\show_part_info.html.twig:74 Part-DB1\templates\Parts\info\show_part_info.html.twig:64 @@ -2019,7 +2011,7 @@ Související prvky budou přesunuty nahoru. Přílohy - + Part-DB1\templates\Parts\info\show_part_info.html.twig:83 Part-DB1\templates\Parts\info\show_part_info.html.twig:71 @@ -2030,7 +2022,7 @@ Související prvky budou přesunuty nahoru. Informace o nákupu - + Part-DB1\templates\Parts\info\show_part_info.html.twig:91 Part-DB1\templates\Parts\info\show_part_info.html.twig:78 @@ -2041,7 +2033,7 @@ Související prvky budou přesunuty nahoru. Historie - + Part-DB1\templates\Parts\info\show_part_info.html.twig:97 Part-DB1\templates\_sidebar.html.twig:54 @@ -2060,7 +2052,7 @@ Související prvky budou přesunuty nahoru. Nástroje - + Part-DB1\templates\Parts\info\show_part_info.html.twig:103 Part-DB1\templates\Parts\info\show_part_info.html.twig:90 @@ -2070,7 +2062,7 @@ Související prvky budou přesunuty nahoru. Rozšířené informace - + Part-DB1\templates\Parts\info\_attachments_info.html.twig:7 Part-DB1\templates\Parts\info\_attachments_info.html.twig:7 @@ -2080,7 +2072,7 @@ Související prvky budou přesunuty nahoru. Jméno - + Part-DB1\templates\Parts\info\_attachments_info.html.twig:8 Part-DB1\templates\Parts\info\_attachments_info.html.twig:8 @@ -2090,7 +2082,7 @@ Související prvky budou přesunuty nahoru. Typ přílohy - + Part-DB1\templates\Parts\info\_attachments_info.html.twig:9 Part-DB1\templates\Parts\info\_attachments_info.html.twig:9 @@ -2100,7 +2092,7 @@ Související prvky budou přesunuty nahoru. Název souboru - + Part-DB1\templates\Parts\info\_attachments_info.html.twig:10 Part-DB1\templates\Parts\info\_attachments_info.html.twig:10 @@ -2110,7 +2102,7 @@ Související prvky budou přesunuty nahoru. Velikost souboru - + Part-DB1\templates\Parts\info\_attachments_info.html.twig:54 @@ -2119,17 +2111,17 @@ Související prvky budou přesunuty nahoru. Náhled - + Part-DB1\templates\Parts\info\_attachments_info.html.twig:67 Part-DB1\templates\Parts\info\_attachments_info.html.twig:50 - attachment.download - Stáhnout + attachment.download_local + Stáhnout místní kopii - + Part-DB1\templates\Parts\info\_extended_infos.html.twig:11 Part-DB1\templates\Parts\info\_extended_infos.html.twig:11 @@ -2140,7 +2132,7 @@ Související prvky budou přesunuty nahoru. Uživatel, který vytvořil tento díl - + Part-DB1\templates\Parts\info\_extended_infos.html.twig:13 Part-DB1\templates\Parts\info\_extended_infos.html.twig:28 @@ -2154,7 +2146,7 @@ Související prvky budou přesunuty nahoru. Neznámý - + Part-DB1\templates\Parts\info\_extended_infos.html.twig:15 Part-DB1\templates\Parts\info\_extended_infos.html.twig:30 @@ -2167,7 +2159,7 @@ Související prvky budou přesunuty nahoru. Přístup odepřen - + Part-DB1\templates\Parts\info\_extended_infos.html.twig:26 Part-DB1\templates\Parts\info\_extended_infos.html.twig:26 @@ -2178,7 +2170,7 @@ Související prvky budou přesunuty nahoru. Uživatel, který tento díl upravil jako poslední - + Part-DB1\templates\Parts\info\_extended_infos.html.twig:41 Part-DB1\templates\Parts\info\_extended_infos.html.twig:41 @@ -2188,7 +2180,7 @@ Související prvky budou přesunuty nahoru. Oblíbené - + Part-DB1\templates\Parts\info\_extended_infos.html.twig:46 Part-DB1\templates\Parts\info\_extended_infos.html.twig:46 @@ -2198,7 +2190,7 @@ Související prvky budou přesunuty nahoru. Minimální množství - + Part-DB1\templates\Parts\info\_main_infos.html.twig:8 Part-DB1\templates\_navbar_search.html.twig:46 @@ -2215,7 +2207,7 @@ Související prvky budou přesunuty nahoru. Výrobce - + Part-DB1\templates\Parts\info\_main_infos.html.twig:24 Part-DB1\templates\_navbar_search.html.twig:11 @@ -2227,7 +2219,7 @@ Související prvky budou přesunuty nahoru. Jméno - + Part-DB1\templates\Parts\info\_main_infos.html.twig:27 Part-DB1\templates\Parts\info\_main_infos.html.twig:27 @@ -2238,7 +2230,7 @@ Související prvky budou přesunuty nahoru. Zpět na aktuální verzi - + Part-DB1\templates\Parts\info\_main_infos.html.twig:32 Part-DB1\templates\_navbar_search.html.twig:19 @@ -2253,7 +2245,7 @@ Související prvky budou přesunuty nahoru. Popis - + Part-DB1\templates\Parts\info\_main_infos.html.twig:34 Part-DB1\templates\_navbar_search.html.twig:15 @@ -2270,7 +2262,7 @@ Související prvky budou přesunuty nahoru. Kategorie - + Part-DB1\templates\Parts\info\_main_infos.html.twig:39 Part-DB1\templates\Parts\info\_main_infos.html.twig:39 @@ -2282,7 +2274,7 @@ Související prvky budou přesunuty nahoru. Skladem - + Part-DB1\templates\Parts\info\_main_infos.html.twig:41 Part-DB1\templates\Parts\info\_main_infos.html.twig:41 @@ -2294,7 +2286,7 @@ Související prvky budou přesunuty nahoru. Minimální skladová zásoba - + Part-DB1\templates\Parts\info\_main_infos.html.twig:45 Part-DB1\templates\_navbar_search.html.twig:52 @@ -2310,7 +2302,7 @@ Související prvky budou přesunuty nahoru. Otisk - + Part-DB1\templates\Parts\info\_main_infos.html.twig:56 Part-DB1\templates\Parts\info\_main_infos.html.twig:59 @@ -2323,7 +2315,7 @@ Související prvky budou přesunuty nahoru. Průměrná cena - + Part-DB1\templates\Parts\info\_order_infos.html.twig:5 Part-DB1\templates\Parts\info\_order_infos.html.twig:5 @@ -2333,7 +2325,7 @@ Související prvky budou přesunuty nahoru. Jméno - + Part-DB1\templates\Parts\info\_order_infos.html.twig:6 Part-DB1\templates\Parts\info\_order_infos.html.twig:6 @@ -2343,7 +2335,7 @@ Související prvky budou přesunuty nahoru. Objednací číslo - + Part-DB1\templates\Parts\info\_order_infos.html.twig:28 Part-DB1\templates\Parts\info\_order_infos.html.twig:28 @@ -2353,7 +2345,7 @@ Související prvky budou přesunuty nahoru. Minimální množství - + Part-DB1\templates\Parts\info\_order_infos.html.twig:29 Part-DB1\templates\Parts\info\_order_infos.html.twig:29 @@ -2363,7 +2355,7 @@ Související prvky budou přesunuty nahoru. Cena - + Part-DB1\templates\Parts\info\_order_infos.html.twig:31 Part-DB1\templates\Parts\info\_order_infos.html.twig:31 @@ -2373,7 +2365,7 @@ Související prvky budou přesunuty nahoru. Jednotková cena - + Part-DB1\templates\Parts\info\_order_infos.html.twig:71 Part-DB1\templates\Parts\info\_order_infos.html.twig:71 @@ -2383,7 +2375,7 @@ Související prvky budou přesunuty nahoru. Upravit - + Part-DB1\templates\Parts\info\_order_infos.html.twig:72 Part-DB1\templates\Parts\info\_order_infos.html.twig:72 @@ -2393,7 +2385,7 @@ Související prvky budou přesunuty nahoru. Smazat - + Part-DB1\templates\Parts\info\_part_lots.html.twig:7 Part-DB1\templates\Parts\info\_part_lots.html.twig:6 @@ -2403,7 +2395,7 @@ Související prvky budou přesunuty nahoru. Popis - + Part-DB1\templates\Parts\info\_part_lots.html.twig:8 Part-DB1\templates\Parts\info\_part_lots.html.twig:7 @@ -2413,7 +2405,7 @@ Související prvky budou přesunuty nahoru. Umístění - + Part-DB1\templates\Parts\info\_part_lots.html.twig:9 Part-DB1\templates\Parts\info\_part_lots.html.twig:8 @@ -2423,7 +2415,7 @@ Související prvky budou přesunuty nahoru. Množství - + Part-DB1\templates\Parts\info\_part_lots.html.twig:24 Part-DB1\templates\Parts\info\_part_lots.html.twig:22 @@ -2433,7 +2425,7 @@ Související prvky budou přesunuty nahoru. Umístění neznámé - + Part-DB1\templates\Parts\info\_part_lots.html.twig:31 Part-DB1\templates\Parts\info\_part_lots.html.twig:29 @@ -2443,7 +2435,7 @@ Související prvky budou přesunuty nahoru. Množství neznámé - + Part-DB1\templates\Parts\info\_part_lots.html.twig:40 Part-DB1\templates\Parts\info\_part_lots.html.twig:38 @@ -2453,7 +2445,7 @@ Související prvky budou přesunuty nahoru. Datum vypršení platnosti - + Part-DB1\templates\Parts\info\_part_lots.html.twig:48 Part-DB1\templates\Parts\info\_part_lots.html.twig:46 @@ -2463,7 +2455,7 @@ Související prvky budou přesunuty nahoru. Platnost vypršela - + Part-DB1\templates\Parts\info\_part_lots.html.twig:55 Part-DB1\templates\Parts\info\_part_lots.html.twig:53 @@ -2473,7 +2465,7 @@ Související prvky budou přesunuty nahoru. Potřebuje doplnit - + Part-DB1\templates\Parts\info\_picture.html.twig:15 Part-DB1\templates\Parts\info\_picture.html.twig:15 @@ -2483,7 +2475,7 @@ Související prvky budou přesunuty nahoru. Předchozí obrázek - + Part-DB1\templates\Parts\info\_picture.html.twig:19 Part-DB1\templates\Parts\info\_picture.html.twig:19 @@ -2493,7 +2485,7 @@ Související prvky budou přesunuty nahoru. Další obrázek - + Part-DB1\templates\Parts\info\_sidebar.html.twig:21 Part-DB1\templates\Parts\info\_sidebar.html.twig:21 @@ -2503,7 +2495,7 @@ Související prvky budou přesunuty nahoru. Hromadné - + Part-DB1\templates\Parts\info\_sidebar.html.twig:30 Part-DB1\templates\Parts\info\_sidebar.html.twig:30 @@ -2513,7 +2505,7 @@ Související prvky budou přesunuty nahoru. Potřeba revize - + Part-DB1\templates\Parts\info\_sidebar.html.twig:39 Part-DB1\templates\Parts\info\_sidebar.html.twig:39 @@ -2523,7 +2515,7 @@ Související prvky budou přesunuty nahoru. Oblíbené - + Part-DB1\templates\Parts\info\_sidebar.html.twig:47 Part-DB1\templates\Parts\info\_sidebar.html.twig:47 @@ -2533,7 +2525,7 @@ Související prvky budou přesunuty nahoru. Již není k dispozici - + Part-DB1\templates\Parts\info\_specifications.html.twig:10 @@ -2542,7 +2534,7 @@ Související prvky budou přesunuty nahoru. Automaticky extrahováno z popisu - + Part-DB1\templates\Parts\info\_specifications.html.twig:15 @@ -2551,7 +2543,7 @@ Související prvky budou přesunuty nahoru. Automaticky extrahované z poznámek - + Part-DB1\templates\Parts\info\_tools.html.twig:6 Part-DB1\templates\Parts\info\_tools.html.twig:4 @@ -2562,7 +2554,7 @@ Související prvky budou přesunuty nahoru. Upravit díl - + Part-DB1\templates\Parts\info\_tools.html.twig:16 Part-DB1\templates\Parts\info\_tools.html.twig:14 @@ -2573,7 +2565,7 @@ Související prvky budou přesunuty nahoru. Duplikovat díl - + Part-DB1\templates\Parts\info\_tools.html.twig:24 Part-DB1\templates\Parts\lists\_action_bar.html.twig:4 @@ -2584,7 +2576,7 @@ Související prvky budou přesunuty nahoru. Přidat nový díl - + Part-DB1\templates\Parts\info\_tools.html.twig:31 Part-DB1\templates\Parts\info\_tools.html.twig:29 @@ -2594,7 +2586,7 @@ Související prvky budou přesunuty nahoru. Opravdu chcete tento díl odstranit? - + Part-DB1\templates\Parts\info\_tools.html.twig:32 Part-DB1\templates\Parts\info\_tools.html.twig:30 @@ -2604,7 +2596,7 @@ Související prvky budou přesunuty nahoru. Tento díl a všechny související informace (např. přílohy, informace o ceně atd.) budou odstraněny. Toto nelze vrátit zpět! - + Part-DB1\templates\Parts\info\_tools.html.twig:39 Part-DB1\templates\Parts\info\_tools.html.twig:37 @@ -2614,7 +2606,7 @@ Související prvky budou přesunuty nahoru. Odstranit díl - + Part-DB1\templates\Parts\lists\all_list.html.twig:4 Part-DB1\templates\Parts\lists\all_list.html.twig:4 @@ -2624,7 +2616,7 @@ Související prvky budou přesunuty nahoru. Všechny díly - + Part-DB1\templates\Parts\lists\category_list.html.twig:4 Part-DB1\templates\Parts\lists\category_list.html.twig:4 @@ -2634,7 +2626,7 @@ Související prvky budou přesunuty nahoru. Díly s kategorií - + Part-DB1\templates\Parts\lists\footprint_list.html.twig:4 Part-DB1\templates\Parts\lists\footprint_list.html.twig:4 @@ -2644,7 +2636,7 @@ Související prvky budou přesunuty nahoru. Díly s otiskem - + Part-DB1\templates\Parts\lists\manufacturer_list.html.twig:4 Part-DB1\templates\Parts\lists\manufacturer_list.html.twig:4 @@ -2654,7 +2646,7 @@ Související prvky budou přesunuty nahoru. Díly s výrobcem - + Part-DB1\templates\Parts\lists\search_list.html.twig:4 Part-DB1\templates\Parts\lists\search_list.html.twig:4 @@ -2664,7 +2656,7 @@ Související prvky budou přesunuty nahoru. Vyhledat díly - + Part-DB1\templates\Parts\lists\store_location_list.html.twig:4 Part-DB1\templates\Parts\lists\store_location_list.html.twig:4 @@ -2674,7 +2666,7 @@ Související prvky budou přesunuty nahoru. Díly s místy uložení - + Part-DB1\templates\Parts\lists\supplier_list.html.twig:4 Part-DB1\templates\Parts\lists\supplier_list.html.twig:4 @@ -2684,7 +2676,7 @@ Související prvky budou přesunuty nahoru. Díly s dodavatelem - + Part-DB1\templates\Parts\lists\tags_list.html.twig:4 Part-DB1\templates\Parts\lists\tags_list.html.twig:4 @@ -2694,7 +2686,7 @@ Související prvky budou přesunuty nahoru. Díly se štítkem - + Part-DB1\templates\Parts\lists\_info_card.html.twig:22 Part-DB1\templates\Parts\lists\_info_card.html.twig:17 @@ -2704,7 +2696,7 @@ Související prvky budou přesunuty nahoru. Obecné - + Part-DB1\templates\Parts\lists\_info_card.html.twig:26 Part-DB1\templates\Parts\lists\_info_card.html.twig:20 @@ -2714,7 +2706,7 @@ Související prvky budou přesunuty nahoru. Statistika - + Part-DB1\templates\Parts\lists\_info_card.html.twig:31 @@ -2723,7 +2715,7 @@ Související prvky budou přesunuty nahoru. Attachments - + Part-DB1\templates\Parts\lists\_info_card.html.twig:37 @@ -2732,7 +2724,7 @@ Související prvky budou přesunuty nahoru. Parametry - + Part-DB1\templates\Parts\lists\_info_card.html.twig:54 Part-DB1\templates\Parts\lists\_info_card.html.twig:30 @@ -2742,7 +2734,7 @@ Související prvky budou přesunuty nahoru. Jméno - + Part-DB1\templates\Parts\lists\_info_card.html.twig:58 Part-DB1\templates\Parts\lists\_info_card.html.twig:96 @@ -2754,7 +2746,7 @@ Související prvky budou přesunuty nahoru. Nadřazený - + Part-DB1\templates\Parts\lists\_info_card.html.twig:70 Part-DB1\templates\Parts\lists\_info_card.html.twig:46 @@ -2764,7 +2756,7 @@ Související prvky budou přesunuty nahoru. Upravit - + Part-DB1\templates\Parts\lists\_info_card.html.twig:92 Part-DB1\templates\Parts\lists\_info_card.html.twig:63 @@ -2774,7 +2766,7 @@ Související prvky budou přesunuty nahoru. Počet podřízených prvků - + Part-DB1\templates\security\2fa_base_form.html.twig:3 Part-DB1\templates\security\2fa_base_form.html.twig:5 @@ -2786,7 +2778,7 @@ Související prvky budou přesunuty nahoru. Potřeba dvoufaktorového ověřování - + Part-DB1\templates\security\2fa_base_form.html.twig:39 Part-DB1\templates\security\2fa_base_form.html.twig:39 @@ -2796,7 +2788,7 @@ Související prvky budou přesunuty nahoru. Jedná se o důvěryhodný počítač (pokud je tato možnost povolena, neprovádějí se na tomto počítači žádné další dvoufaktorové dotazy). - + Part-DB1\templates\security\2fa_base_form.html.twig:52 Part-DB1\templates\security\login.html.twig:58 @@ -2808,7 +2800,7 @@ Související prvky budou přesunuty nahoru. Přihlášení - + Part-DB1\templates\security\2fa_base_form.html.twig:53 Part-DB1\templates\security\U2F\u2f_login.html.twig:13 @@ -2822,7 +2814,7 @@ Související prvky budou přesunuty nahoru. Odhlásit se - + Part-DB1\templates\security\2fa_form.html.twig:6 Part-DB1\templates\security\2fa_form.html.twig:6 @@ -2832,7 +2824,7 @@ Související prvky budou přesunuty nahoru. Kód aplikace Authenticator - + Part-DB1\templates\security\2fa_form.html.twig:10 Part-DB1\templates\security\2fa_form.html.twig:10 @@ -2842,7 +2834,7 @@ Související prvky budou přesunuty nahoru. Zde zadejte šestimístný kód z ověřovací aplikace nebo jeden ze záložních kódů, pokud ověřovací aplikace není k dispozici. - + Part-DB1\templates\security\login.html.twig:3 Part-DB1\templates\security\login.html.twig:3 @@ -2853,7 +2845,7 @@ Související prvky budou přesunuty nahoru. Přihlášení - + Part-DB1\templates\security\login.html.twig:7 Part-DB1\templates\security\login.html.twig:7 @@ -2864,7 +2856,7 @@ Související prvky budou přesunuty nahoru. Přihlášení - + Part-DB1\templates\security\login.html.twig:31 Part-DB1\templates\security\login.html.twig:31 @@ -2875,7 +2867,7 @@ Související prvky budou přesunuty nahoru. Uživatelské jméno - + Part-DB1\templates\security\login.html.twig:34 Part-DB1\templates\security\login.html.twig:34 @@ -2886,7 +2878,7 @@ Související prvky budou přesunuty nahoru. Uživatelské jméno - + Part-DB1\templates\security\login.html.twig:38 Part-DB1\templates\security\login.html.twig:38 @@ -2897,7 +2889,7 @@ Související prvky budou přesunuty nahoru. Heslo - + Part-DB1\templates\security\login.html.twig:40 Part-DB1\templates\security\login.html.twig:40 @@ -2908,7 +2900,7 @@ Související prvky budou přesunuty nahoru. Heslo - + Part-DB1\templates\security\login.html.twig:50 Part-DB1\templates\security\login.html.twig:50 @@ -2919,7 +2911,7 @@ Související prvky budou přesunuty nahoru. Zapamatovat si (nemělo by se používat na sdílených počítačích) - + Part-DB1\templates\security\login.html.twig:64 Part-DB1\templates\security\login.html.twig:64 @@ -2929,7 +2921,7 @@ Související prvky budou přesunuty nahoru. Zapomněli jste uživatelské jméno/heslo? - + Part-DB1\templates\security\pw_reset_new_pw.html.twig:5 Part-DB1\templates\security\pw_reset_new_pw.html.twig:5 @@ -2939,7 +2931,7 @@ Související prvky budou přesunuty nahoru. Nastavit nové heslo - + Part-DB1\templates\security\pw_reset_request.html.twig:5 Part-DB1\templates\security\pw_reset_request.html.twig:5 @@ -2949,7 +2941,7 @@ Související prvky budou přesunuty nahoru. Požádat o nové heslo - + Part-DB1\templates\security\U2F\u2f_login.html.twig:7 Part-DB1\templates\security\U2F\u2f_register.html.twig:10 @@ -2961,7 +2953,7 @@ Související prvky budou přesunuty nahoru. Na tuto stránku přistupujete pomocí nezabezpečené metody HTTP, takže U2F pravděpodobně nebude fungovat (chybová zpráva Bad Request). Pokud chcete používat bezpečnostní klíče, požádejte správce o nastavení zabezpečené metody HTTPS. - + Part-DB1\templates\security\U2F\u2f_login.html.twig:10 Part-DB1\templates\security\U2F\u2f_register.html.twig:22 @@ -2973,7 +2965,7 @@ Související prvky budou přesunuty nahoru. Připojte bezpečnostní klíč a stiskněte jeho tlačítko! - + Part-DB1\templates\security\U2F\u2f_register.html.twig:3 Part-DB1\templates\security\U2F\u2f_register.html.twig:3 @@ -2983,7 +2975,7 @@ Související prvky budou přesunuty nahoru. Přidání bezpečnostního klíče - + Part-DB1\templates\security\U2F\u2f_register.html.twig:6 Part-DB1\templates\Users\_2fa_settings.html.twig:111 @@ -2995,7 +2987,7 @@ Související prvky budou přesunuty nahoru. Pomocí bezpečnostního klíče kompatibilního s U2F/FIDO (např. YubiKey nebo NitroKey) lze dosáhnout uživatelsky přívětivého a bezpečného dvoufaktorového ověřování. Bezpečnostní klíče lze zde zaregistrovat a pokud je vyžadováno dvoufaktorové ověření, stačí vložit klíč do USB, nebo zadat přes zařízení prostřednictvím NFC. - + Part-DB1\templates\security\U2F\u2f_register.html.twig:7 Part-DB1\templates\security\U2F\u2f_register.html.twig:7 @@ -3005,7 +2997,7 @@ Související prvky budou přesunuty nahoru. Pro zajištění přístupu i v případě ztráty klíče doporučujeme zaregistrovat druhý klíč jako zálohu a uložit jej na bezpečném místě! - + Part-DB1\templates\security\U2F\u2f_register.html.twig:16 Part-DB1\templates\security\U2F\u2f_register.html.twig:16 @@ -3015,7 +3007,7 @@ Související prvky budou přesunuty nahoru. Zobrazený název klíče (např. Záloha) - + Part-DB1\templates\security\U2F\u2f_register.html.twig:19 Part-DB1\templates\security\U2F\u2f_register.html.twig:19 @@ -3025,7 +3017,7 @@ Související prvky budou přesunuty nahoru. Přidat bezpečnostní klíč - + Part-DB1\templates\security\U2F\u2f_register.html.twig:27 Part-DB1\templates\security\U2F\u2f_register.html.twig:27 @@ -3035,7 +3027,7 @@ Související prvky budou přesunuty nahoru. Zpět do nastavení - + Part-DB1\templates\Statistics\statistics.html.twig:5 Part-DB1\templates\Statistics\statistics.html.twig:8 @@ -3048,7 +3040,7 @@ Související prvky budou přesunuty nahoru. Statistiky - + Part-DB1\templates\Statistics\statistics.html.twig:14 Part-DB1\templates\Statistics\statistics.html.twig:14 @@ -3059,7 +3051,7 @@ Související prvky budou přesunuty nahoru. Díly - + Part-DB1\templates\Statistics\statistics.html.twig:19 Part-DB1\templates\Statistics\statistics.html.twig:19 @@ -3070,7 +3062,7 @@ Související prvky budou přesunuty nahoru. Datové struktury - + Part-DB1\templates\Statistics\statistics.html.twig:24 Part-DB1\templates\Statistics\statistics.html.twig:24 @@ -3081,7 +3073,7 @@ Související prvky budou přesunuty nahoru. Přílohy - + Part-DB1\templates\Statistics\statistics.html.twig:34 Part-DB1\templates\Statistics\statistics.html.twig:59 @@ -3096,7 +3088,7 @@ Související prvky budou přesunuty nahoru. Vlastnictví - + Part-DB1\templates\Statistics\statistics.html.twig:35 Part-DB1\templates\Statistics\statistics.html.twig:60 @@ -3111,7 +3103,7 @@ Související prvky budou přesunuty nahoru. Hodnota - + Part-DB1\templates\Statistics\statistics.html.twig:40 Part-DB1\templates\Statistics\statistics.html.twig:40 @@ -3122,7 +3114,7 @@ Související prvky budou přesunuty nahoru. Počet jednotlivých dílů - + Part-DB1\templates\Statistics\statistics.html.twig:44 Part-DB1\templates\Statistics\statistics.html.twig:44 @@ -3133,7 +3125,7 @@ Související prvky budou přesunuty nahoru. Součet všech zásob dílů - + Part-DB1\templates\Statistics\statistics.html.twig:48 Part-DB1\templates\Statistics\statistics.html.twig:48 @@ -3144,7 +3136,7 @@ Související prvky budou přesunuty nahoru. Počet dílů s informacemi o ceně - + Part-DB1\templates\Statistics\statistics.html.twig:65 Part-DB1\templates\Statistics\statistics.html.twig:65 @@ -3155,7 +3147,7 @@ Související prvky budou přesunuty nahoru. Počet kategorií - + Part-DB1\templates\Statistics\statistics.html.twig:69 Part-DB1\templates\Statistics\statistics.html.twig:69 @@ -3166,7 +3158,7 @@ Související prvky budou přesunuty nahoru. Počet otisků - + Part-DB1\templates\Statistics\statistics.html.twig:73 Part-DB1\templates\Statistics\statistics.html.twig:73 @@ -3177,7 +3169,7 @@ Související prvky budou přesunuty nahoru. Počet výrobců - + Part-DB1\templates\Statistics\statistics.html.twig:77 Part-DB1\templates\Statistics\statistics.html.twig:77 @@ -3188,7 +3180,7 @@ Související prvky budou přesunuty nahoru. Počet umístění - + Part-DB1\templates\Statistics\statistics.html.twig:81 Part-DB1\templates\Statistics\statistics.html.twig:81 @@ -3199,7 +3191,7 @@ Související prvky budou přesunuty nahoru. Počet dodavatelů - + Part-DB1\templates\Statistics\statistics.html.twig:85 Part-DB1\templates\Statistics\statistics.html.twig:85 @@ -3210,7 +3202,7 @@ Související prvky budou přesunuty nahoru. Počet měn - + Part-DB1\templates\Statistics\statistics.html.twig:89 Part-DB1\templates\Statistics\statistics.html.twig:89 @@ -3221,7 +3213,7 @@ Související prvky budou přesunuty nahoru. Počet měrných jednotek - + Part-DB1\templates\Statistics\statistics.html.twig:93 Part-DB1\templates\Statistics\statistics.html.twig:93 @@ -3232,7 +3224,7 @@ Související prvky budou přesunuty nahoru. Počet projektů - + Part-DB1\templates\Statistics\statistics.html.twig:110 Part-DB1\templates\Statistics\statistics.html.twig:110 @@ -3243,7 +3235,7 @@ Související prvky budou přesunuty nahoru. Počet typů příloh - + Part-DB1\templates\Statistics\statistics.html.twig:114 Part-DB1\templates\Statistics\statistics.html.twig:114 @@ -3254,7 +3246,7 @@ Související prvky budou přesunuty nahoru. Počet všech příloh - + Part-DB1\templates\Statistics\statistics.html.twig:118 Part-DB1\templates\Statistics\statistics.html.twig:118 @@ -3265,7 +3257,7 @@ Související prvky budou přesunuty nahoru. Počet příloh nahraných uživatelem - + Part-DB1\templates\Statistics\statistics.html.twig:122 Part-DB1\templates\Statistics\statistics.html.twig:122 @@ -3276,7 +3268,7 @@ Související prvky budou přesunuty nahoru. Počet soukromých příloh - + Part-DB1\templates\Statistics\statistics.html.twig:126 Part-DB1\templates\Statistics\statistics.html.twig:126 @@ -3287,7 +3279,7 @@ Související prvky budou přesunuty nahoru. Počet externích příloh (URL) - + Part-DB1\templates\Users\backup_codes.html.twig:3 Part-DB1\templates\Users\backup_codes.html.twig:9 @@ -3299,7 +3291,7 @@ Související prvky budou přesunuty nahoru. Záložní kódy - + Part-DB1\templates\Users\backup_codes.html.twig:12 Part-DB1\templates\Users\backup_codes.html.twig:12 @@ -3309,7 +3301,7 @@ Související prvky budou přesunuty nahoru. Vytiskněte si tyto kódy a uschovejte je na bezpečném místě! - + Part-DB1\templates\Users\backup_codes.html.twig:13 Part-DB1\templates\Users\backup_codes.html.twig:13 @@ -3319,7 +3311,7 @@ Související prvky budou přesunuty nahoru. Pokud již nemáte přístup ke svému zařízení s aplikací Authenticator (ztráta smartphonu, ztráta dat atd.), můžete použít jeden z těchto kódů pro přístup ke svému účtu a případně nastavit novou aplikaci Authenticator. Každý z těchto kódů lze použít jednou, použité kódy se doporučuje odstranit. Kdokoli s přístupem k těmto kódům může potenciálně získat přístup k vašemu účtu, proto je uchovávejte na bezpečném místě. - + Part-DB1\templates\Users\backup_codes.html.twig:16 Part-DB1\templates\Users\backup_codes.html.twig:16 @@ -3329,7 +3321,7 @@ Související prvky budou přesunuty nahoru. Uživatelské jméno - + Part-DB1\templates\Users\backup_codes.html.twig:29 Part-DB1\templates\Users\backup_codes.html.twig:29 @@ -3339,7 +3331,7 @@ Související prvky budou přesunuty nahoru. Stránka vygenerovaná dne %date% - + Part-DB1\templates\Users\backup_codes.html.twig:32 Part-DB1\templates\Users\backup_codes.html.twig:32 @@ -3349,7 +3341,7 @@ Související prvky budou přesunuty nahoru. Tisk - + Part-DB1\templates\Users\backup_codes.html.twig:35 Part-DB1\templates\Users\backup_codes.html.twig:35 @@ -3359,7 +3351,7 @@ Související prvky budou přesunuty nahoru. Zkopírovat do schránky - + Part-DB1\templates\Users\user_info.html.twig:3 Part-DB1\templates\Users\user_info.html.twig:6 @@ -3376,7 +3368,7 @@ Související prvky budou přesunuty nahoru. Informace o uživateli - + Part-DB1\templates\Users\user_info.html.twig:18 Part-DB1\src\Form\UserSettingsType.php:77 @@ -3390,7 +3382,7 @@ Související prvky budou přesunuty nahoru. Jméno - + Part-DB1\templates\Users\user_info.html.twig:24 Part-DB1\src\Form\UserSettingsType.php:82 @@ -3404,7 +3396,7 @@ Související prvky budou přesunuty nahoru. Příjmení - + Part-DB1\templates\Users\user_info.html.twig:30 Part-DB1\src\Form\UserSettingsType.php:92 @@ -3418,7 +3410,7 @@ Související prvky budou přesunuty nahoru. e-mail - + Part-DB1\templates\Users\user_info.html.twig:37 Part-DB1\src\Form\UserSettingsType.php:87 @@ -3432,7 +3424,7 @@ Související prvky budou přesunuty nahoru. Oddělení - + Part-DB1\templates\Users\user_info.html.twig:47 Part-DB1\src\Form\UserSettingsType.php:73 @@ -3446,7 +3438,7 @@ Související prvky budou přesunuty nahoru. Uživatelské jméno - + Part-DB1\templates\Users\user_info.html.twig:53 Part-DB1\src\Services\ElementTypeNameGenerator.php:93 @@ -3459,7 +3451,7 @@ Související prvky budou přesunuty nahoru. Skupina - + Part-DB1\templates\Users\user_info.html.twig:67 Part-DB1\templates\Users\user_info.html.twig:67 @@ -3469,7 +3461,7 @@ Související prvky budou přesunuty nahoru. Oprávnění - + Part-DB1\templates\Users\user_settings.html.twig:3 Part-DB1\templates\Users\user_settings.html.twig:6 @@ -3486,7 +3478,7 @@ Související prvky budou přesunuty nahoru. Uživatelské nastavení - + Part-DB1\templates\Users\user_settings.html.twig:18 Part-DB1\templates\Users\user_settings.html.twig:18 @@ -3497,7 +3489,7 @@ Související prvky budou přesunuty nahoru. Osobní údaje - + Part-DB1\templates\Users\user_settings.html.twig:22 Part-DB1\templates\Users\user_settings.html.twig:22 @@ -3508,7 +3500,7 @@ Související prvky budou přesunuty nahoru. Konfigurace - + Part-DB1\templates\Users\user_settings.html.twig:55 Part-DB1\templates\Users\user_settings.html.twig:55 @@ -3519,7 +3511,7 @@ Související prvky budou přesunuty nahoru. Změnit heslo - + Part-DB1\templates\Users\_2fa_settings.html.twig:6 Part-DB1\templates\Users\_2fa_settings.html.twig:6 @@ -3529,7 +3521,7 @@ Související prvky budou přesunuty nahoru. Dvoufaktorové ověřování - + Part-DB1\templates\Users\_2fa_settings.html.twig:13 Part-DB1\templates\Users\_2fa_settings.html.twig:13 @@ -3539,7 +3531,7 @@ Související prvky budou přesunuty nahoru. Authenticator app - + Part-DB1\templates\Users\_2fa_settings.html.twig:17 Part-DB1\templates\Users\_2fa_settings.html.twig:17 @@ -3549,7 +3541,7 @@ Související prvky budou přesunuty nahoru. Záložní kódy - + Part-DB1\templates\Users\_2fa_settings.html.twig:21 Part-DB1\templates\Users\_2fa_settings.html.twig:21 @@ -3559,7 +3551,7 @@ Související prvky budou přesunuty nahoru. Bezpečnostní klíče (U2F) - + Part-DB1\templates\Users\_2fa_settings.html.twig:25 Part-DB1\templates\Users\_2fa_settings.html.twig:25 @@ -3569,7 +3561,7 @@ Související prvky budou přesunuty nahoru. Důvěryhodná zařízení - + Part-DB1\templates\Users\_2fa_settings.html.twig:33 Part-DB1\templates\Users\_2fa_settings.html.twig:33 @@ -3579,7 +3571,7 @@ Související prvky budou přesunuty nahoru. Opravdu chcete aplikaci Authenticator zakázat? - + Part-DB1\templates\Users\_2fa_settings.html.twig:33 Part-DB1\templates\Users\_2fa_settings.html.twig:33 @@ -3590,7 +3582,7 @@ Související prvky budou přesunuty nahoru. Upozorňujeme také, že bez dvoufaktorového ověřování není váš účet tak dobře chráněn před útočníky! - + Part-DB1\templates\Users\_2fa_settings.html.twig:39 Part-DB1\templates\Users\_2fa_settings.html.twig:39 @@ -3600,7 +3592,7 @@ Upozorňujeme také, že bez dvoufaktorového ověřování není váš účet t Aplikace Authenticator deaktivována! - + Part-DB1\templates\Users\_2fa_settings.html.twig:48 Part-DB1\templates\Users\_2fa_settings.html.twig:48 @@ -3610,7 +3602,7 @@ Upozorňujeme také, že bez dvoufaktorového ověřování není váš účet t Stáhněte si aplikaci Authenticator (např. <a class="link-external" target="_blank" href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2">Google Authenticator</a> nebo <a class="link-external" target="_blank" href="https://play.google.com/store/apps/details?id=org.fedorahosted.freeotp">FreeOTP Authenticator</a>). - + Part-DB1\templates\Users\_2fa_settings.html.twig:49 Part-DB1\templates\Users\_2fa_settings.html.twig:49 @@ -3620,7 +3612,7 @@ Upozorňujeme také, že bez dvoufaktorového ověřování není váš účet t Naskenujte přiložený QR kód pomocí aplikace nebo zadejte údaje ručně. - + Part-DB1\templates\Users\_2fa_settings.html.twig:50 Part-DB1\templates\Users\_2fa_settings.html.twig:50 @@ -3630,7 +3622,7 @@ Upozorňujeme také, že bez dvoufaktorového ověřování není váš účet t Vygenerovaný kód zadejte do níže uvedeného pole a potvrďte. - + Part-DB1\templates\Users\_2fa_settings.html.twig:51 Part-DB1\templates\Users\_2fa_settings.html.twig:51 @@ -3640,7 +3632,7 @@ Upozorňujeme také, že bez dvoufaktorového ověřování není váš účet t Vytiskněte si záložní kódy a uložte je na bezpečném místě. - + Part-DB1\templates\Users\_2fa_settings.html.twig:58 Part-DB1\templates\Users\_2fa_settings.html.twig:58 @@ -3650,7 +3642,7 @@ Upozorňujeme také, že bez dvoufaktorového ověřování není váš účet t Ruční nastavení - + Part-DB1\templates\Users\_2fa_settings.html.twig:62 Part-DB1\templates\Users\_2fa_settings.html.twig:62 @@ -3660,7 +3652,7 @@ Upozorňujeme také, že bez dvoufaktorového ověřování není váš účet t Typ - + Part-DB1\templates\Users\_2fa_settings.html.twig:63 Part-DB1\templates\Users\_2fa_settings.html.twig:63 @@ -3670,7 +3662,7 @@ Upozorňujeme také, že bez dvoufaktorového ověřování není váš účet t Uživatelské jméno - + Part-DB1\templates\Users\_2fa_settings.html.twig:64 Part-DB1\templates\Users\_2fa_settings.html.twig:64 @@ -3680,7 +3672,7 @@ Upozorňujeme také, že bez dvoufaktorového ověřování není váš účet t Tajné - + Part-DB1\templates\Users\_2fa_settings.html.twig:65 Part-DB1\templates\Users\_2fa_settings.html.twig:65 @@ -3690,7 +3682,7 @@ Upozorňujeme také, že bez dvoufaktorového ověřování není váš účet t Počet číslic - + Part-DB1\templates\Users\_2fa_settings.html.twig:74 Part-DB1\templates\Users\_2fa_settings.html.twig:74 @@ -3700,7 +3692,7 @@ Upozorňujeme také, že bez dvoufaktorového ověřování není váš účet t Aplikace Authenticator povolena - + Part-DB1\templates\Users\_2fa_settings.html.twig:83 Part-DB1\templates\Users\_2fa_settings.html.twig:83 @@ -3710,7 +3702,7 @@ Upozorňujeme také, že bez dvoufaktorového ověřování není váš účet t Záložní kódy jsou zakázány. Nastavte aplikaci Authenticator pro povolení záložních kódů. - + Part-DB1\templates\Users\_2fa_settings.html.twig:84 Part-DB1\templates\Users\_2fa_settings.html.twig:92 @@ -3722,7 +3714,7 @@ Upozorňujeme také, že bez dvoufaktorového ověřování není váš účet t Tyto záložní kódy můžete použít k přístupu k účtu i v případě ztráty zařízení s aplikací Authenticator. Kódy si vytiskněte a uschovejte na bezpečném místě. - + Part-DB1\templates\Users\_2fa_settings.html.twig:88 Part-DB1\templates\Users\_2fa_settings.html.twig:88 @@ -3732,7 +3724,7 @@ Upozorňujeme také, že bez dvoufaktorového ověřování není váš účet t Opravdu resetovat kódy? - + Part-DB1\templates\Users\_2fa_settings.html.twig:88 Part-DB1\templates\Users\_2fa_settings.html.twig:88 @@ -3742,7 +3734,7 @@ Upozorňujeme také, že bez dvoufaktorového ověřování není váš účet t Tím se odstraní všechny předchozí kódy a vygeneruje se sada nových kódů. Tuto akci nelze vrátit zpět. Nezapomeňte si nové kódy vytisknout a uložit na bezpečném místě! - + Part-DB1\templates\Users\_2fa_settings.html.twig:91 Part-DB1\templates\Users\_2fa_settings.html.twig:91 @@ -3752,7 +3744,7 @@ Upozorňujeme také, že bez dvoufaktorového ověřování není váš účet t Záložní kódy povoleny - + Part-DB1\templates\Users\_2fa_settings.html.twig:99 Part-DB1\templates\Users\_2fa_settings.html.twig:99 @@ -3762,7 +3754,7 @@ Upozorňujeme také, že bez dvoufaktorového ověřování není váš účet t Zobrazit záložní kódy - + Part-DB1\templates\Users\_2fa_settings.html.twig:114 Part-DB1\templates\Users\_2fa_settings.html.twig:114 @@ -3772,7 +3764,7 @@ Upozorňujeme také, že bez dvoufaktorového ověřování není váš účet t Registrované bezpečnostní klíče - + Part-DB1\templates\Users\_2fa_settings.html.twig:115 Part-DB1\templates\Users\_2fa_settings.html.twig:115 @@ -3782,7 +3774,7 @@ Upozorňujeme také, že bez dvoufaktorového ověřování není váš účet t Opravdu odstranit tento bezpečnostní klíč? - + Part-DB1\templates\Users\_2fa_settings.html.twig:116 Part-DB1\templates\Users\_2fa_settings.html.twig:116 @@ -3792,7 +3784,7 @@ Upozorňujeme také, že bez dvoufaktorového ověřování není váš účet t Pokud tento klíč odstraníte, nebude již možné se pomocí tohoto klíče přihlásit. Pokud nezůstanou žádné bezpečnostní klíče, dvoufaktorové ověřování bude zakázáno. - + Part-DB1\templates\Users\_2fa_settings.html.twig:123 Part-DB1\templates\Users\_2fa_settings.html.twig:123 @@ -3802,7 +3794,7 @@ Upozorňujeme také, že bez dvoufaktorového ověřování není váš účet t Název klíče - + Part-DB1\templates\Users\_2fa_settings.html.twig:124 Part-DB1\templates\Users\_2fa_settings.html.twig:124 @@ -3812,7 +3804,7 @@ Upozorňujeme také, že bez dvoufaktorového ověřování není váš účet t Datum registrace - + Part-DB1\templates\Users\_2fa_settings.html.twig:134 Part-DB1\templates\Users\_2fa_settings.html.twig:134 @@ -3822,7 +3814,7 @@ Upozorňujeme také, že bez dvoufaktorového ověřování není váš účet t Smazat klíč - + Part-DB1\templates\Users\_2fa_settings.html.twig:141 Part-DB1\templates\Users\_2fa_settings.html.twig:141 @@ -3832,7 +3824,7 @@ Upozorňujeme také, že bez dvoufaktorového ověřování není váš účet t Zatím nebyly zaregistrovány žádné klíče. - + Part-DB1\templates\Users\_2fa_settings.html.twig:144 Part-DB1\templates\Users\_2fa_settings.html.twig:144 @@ -3842,7 +3834,7 @@ Upozorňujeme také, že bez dvoufaktorového ověřování není váš účet t Registrace nového bezpečnostního klíče - + Part-DB1\templates\Users\_2fa_settings.html.twig:148 Part-DB1\templates\Users\_2fa_settings.html.twig:148 @@ -3853,7 +3845,7 @@ Upozorňujeme také, že bez dvoufaktorového ověřování není váš účet t Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodný, můžete zde obnovit stav <i>všech </i>počítačů. - + Part-DB1\templates\Users\_2fa_settings.html.twig:149 Part-DB1\templates\Users\_2fa_settings.html.twig:149 @@ -3863,7 +3855,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Opravdu odebrat všechny důvěryhodné počítače? - + Part-DB1\templates\Users\_2fa_settings.html.twig:150 Part-DB1\templates\Users\_2fa_settings.html.twig:150 @@ -3873,7 +3865,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Ve všech počítačích bude nutné znovu provést dvoufaktorové ověřování. Ujistěte se, že máte po ruce dvoufaktorové zařízení. - + Part-DB1\templates\Users\_2fa_settings.html.twig:154 Part-DB1\templates\Users\_2fa_settings.html.twig:154 @@ -3883,7 +3875,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Reset důvěryhodných zařízení - + Part-DB1\templates\_navbar.html.twig:4 Part-DB1\templates\_navbar.html.twig:4 @@ -3894,7 +3886,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Přepnutí postranního panelu - + Part-DB1\templates\_navbar.html.twig:22 @@ -3903,7 +3895,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Čtečka štítků - + Part-DB1\templates\_navbar.html.twig:38 Part-DB1\templates\_navbar.html.twig:36 @@ -3914,7 +3906,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Přihlášen jako - + Part-DB1\templates\_navbar.html.twig:44 Part-DB1\templates\_navbar.html.twig:42 @@ -3925,7 +3917,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Příhlásit - + Part-DB1\templates\_navbar.html.twig:50 Part-DB1\templates\_navbar.html.twig:48 @@ -3935,7 +3927,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Tmavý režim - + Part-DB1\templates\_navbar.html.twig:54 Part-DB1\src\Form\UserSettingsType.php:97 @@ -3949,7 +3941,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Jazyk - + Part-DB1\templates\_navbar_search.html.twig:4 Part-DB1\templates\_navbar_search.html.twig:4 @@ -3960,7 +3952,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Možnosti hledání - + Part-DB1\templates\_navbar_search.html.twig:23 @@ -3969,7 +3961,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Štítky - + Part-DB1\templates\_navbar_search.html.twig:27 Part-DB1\src\Form\LabelOptionsType.php:68 @@ -3984,7 +3976,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Umístění - + Part-DB1\templates\_navbar_search.html.twig:36 Part-DB1\templates\_navbar_search.html.twig:31 @@ -3995,7 +3987,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Číslo dílu dodavatele - + Part-DB1\templates\_navbar_search.html.twig:40 Part-DB1\src\Services\ElementTypeNameGenerator.php:89 @@ -4008,7 +4000,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Dodavatel - + Part-DB1\templates\_navbar_search.html.twig:57 Part-DB1\templates\_navbar_search.html.twig:52 @@ -4019,7 +4011,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Deaktivovat čárový kód - + Part-DB1\templates\_navbar_search.html.twig:61 Part-DB1\templates\_navbar_search.html.twig:56 @@ -4030,7 +4022,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn RegEx. shoda - + Part-DB1\templates\_navbar_search.html.twig:68 Part-DB1\templates\_navbar_search.html.twig:62 @@ -4040,7 +4032,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Jdi! - + Part-DB1\templates\_sidebar.html.twig:37 Part-DB1\templates\_sidebar.html.twig:12 @@ -4056,7 +4048,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Projekty - + Part-DB1\templates\_sidebar.html.twig:2 Part-DB1\templates\_sidebar.html.twig:2 @@ -4069,7 +4061,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Akce - + Part-DB1\templates\_sidebar.html.twig:6 Part-DB1\templates\_sidebar.html.twig:6 @@ -4082,7 +4074,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Zdroj dat - + Part-DB1\templates\_sidebar.html.twig:10 Part-DB1\templates\_sidebar.html.twig:10 @@ -4095,7 +4087,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Výrobce - + Part-DB1\templates\_sidebar.html.twig:11 Part-DB1\templates\_sidebar.html.twig:11 @@ -4108,7 +4100,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Dodavatelé - + Part-DB1\src\Controller\AdminPages\BaseAdminController.php:213 Part-DB1\src\Controller\AdminPages\BaseAdminController.php:293 @@ -4124,7 +4116,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Stažení externí přílohy se nezdařilo. - + Part-DB1\src\Controller\AdminPages\BaseAdminController.php:222 Part-DB1\src\Controller\AdminPages\BaseAdminController.php:190 @@ -4134,7 +4126,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Změny byly úspěšně uloženy. - + Part-DB1\src\Controller\AdminPages\BaseAdminController.php:231 Part-DB1\src\Controller\AdminPages\BaseAdminController.php:196 @@ -4144,7 +4136,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Nelze uložit změnit. Zkontrolujte prosím své zadání! - + Part-DB1\src\Controller\AdminPages\BaseAdminController.php:302 Part-DB1\src\Controller\AdminPages\BaseAdminController.php:252 @@ -4154,7 +4146,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Vytvořený prvek. - + Part-DB1\src\Controller\AdminPages\BaseAdminController.php:308 Part-DB1\src\Controller\AdminPages\BaseAdminController.php:258 @@ -4164,7 +4156,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Nepodařilo se vytvořit prvek. Zkontrolujte prosím zadání! - + Part-DB1\src\Controller\AdminPages\BaseAdminController.php:399 Part-DB1\src\Controller\AdminPages\BaseAdminController.php:352 @@ -4175,7 +4167,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Prvek smazán! - + Part-DB1\src\Controller\AdminPages\BaseAdminController.php:401 Part-DB1\src\Controller\UserController.php:109 @@ -4191,7 +4183,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Token CSFR je neplatný. Pokud tato zpráva přetrvává, načtěte prosím tuto stránku znovu nebo kontaktujte správce. - + Part-DB1\src\Controller\LabelController.php:125 @@ -4200,7 +4192,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Nebyly nalezeny žádné entity odpovídající zadání. - + Part-DB1\src\Controller\LogController.php:149 Part-DB1\src\Controller\LogController.php:154 @@ -4211,7 +4203,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Cílový prvek nebyl v DB nalezen! - + Part-DB1\src\Controller\LogController.php:156 Part-DB1\src\Controller\LogController.php:160 @@ -4222,7 +4214,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Úspěšně vráceno na časové razítko. - + Part-DB1\src\Controller\LogController.php:176 Part-DB1\src\Controller\LogController.php:180 @@ -4233,7 +4225,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Prvek byl úspěšně odstraněn. - + Part-DB1\src\Controller\LogController.php:178 Part-DB1\src\Controller\LogController.php:182 @@ -4244,7 +4236,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Prvek byl již smazán! - + Part-DB1\src\Controller\LogController.php:185 Part-DB1\src\Controller\LogController.php:189 @@ -4255,7 +4247,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Prvek byl úspěšně odstraněn. - + Part-DB1\src\Controller\LogController.php:187 Part-DB1\src\Controller\LogController.php:191 @@ -4266,7 +4258,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Prvek byl již smazán! - + Part-DB1\src\Controller\LogController.php:194 Part-DB1\src\Controller\LogController.php:198 @@ -4277,7 +4269,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Změna úspěšně vrácena! - + Part-DB1\src\Controller\LogController.php:196 Part-DB1\src\Controller\LogController.php:200 @@ -4288,7 +4280,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Před zrušením této změny musíte prvek smazat! - + Part-DB1\src\Controller\LogController.php:199 Part-DB1\src\Controller\LogController.php:203 @@ -4299,7 +4291,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Tento záznam v protokolu nelze vrátit zpět! - + Part-DB1\src\Controller\PartController.php:182 Part-DB1\src\Controller\PartController.php:182 @@ -4310,7 +4302,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Uložené změny! - + Part-DB1\src\Controller\PartController.php:186 Part-DB1\src\Controller\PartController.php:186 @@ -4320,7 +4312,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Chyba při ukládání: Zkontrolujte prosím své zadání! - + Part-DB1\src\Controller\PartController.php:216 Part-DB1\src\Controller\PartController.php:219 @@ -4330,7 +4322,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Díl úspěšně vymazán. - + Part-DB1\src\Controller\PartController.php:302 Part-DB1\src\Controller\PartController.php:277 @@ -4343,7 +4335,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Díl vytvořen! - + Part-DB1\src\Controller\PartController.php:308 Part-DB1\src\Controller\PartController.php:283 @@ -4353,7 +4345,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Chyba při vytváření: Zkontrolujte prosím své zadání! - + Part-DB1\src\Controller\ScanController.php:68 Part-DB1\src\Controller\ScanController.php:90 @@ -4363,7 +4355,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Pro daný čárový kód nebyl nalezen žádný prvek. - + Part-DB1\src\Controller\ScanController.php:71 @@ -4372,7 +4364,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Neznámý formát! - + Part-DB1\src\Controller\ScanController.php:86 @@ -4381,7 +4373,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Nalezený prvek. - + Part-DB1\src\Controller\SecurityController.php:114 Part-DB1\src\Controller\SecurityController.php:109 @@ -4391,7 +4383,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Uživatelské jméno / e-mail - + Part-DB1\src\Controller\SecurityController.php:131 Part-DB1\src\Controller\SecurityController.php:126 @@ -4401,7 +4393,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Žádost o obnovení byla úspěšná! Zkontrolujte prosím své e-maily, kde najdete další pokyny. - + Part-DB1\src\Controller\SecurityController.php:162 Part-DB1\src\Controller\SecurityController.php:160 @@ -4411,7 +4403,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Uživatelské jméno - + Part-DB1\src\Controller\SecurityController.php:165 Part-DB1\src\Controller\SecurityController.php:163 @@ -4421,7 +4413,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Token - + Part-DB1\src\Controller\SecurityController.php:194 Part-DB1\src\Controller\SecurityController.php:192 @@ -4431,7 +4423,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Uživatelské jméno nebo token je neplatný! Zkontrolujte prosím své zadání. - + Part-DB1\src\Controller\SecurityController.php:196 Part-DB1\src\Controller\SecurityController.php:194 @@ -4441,7 +4433,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Heslo bylo úspěšně obnoveno. Nyní se můžete přihlásit pomocí nového hesla. - + Part-DB1\src\Controller\UserController.php:107 Part-DB1\src\Controller\UserController.php:99 @@ -4451,7 +4443,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Všechny metody dvoufaktorového ověřování byly úspěšně zakázány. - + Part-DB1\src\Controller\UserSettingsController.php:101 Part-DB1\src\Controller\UserSettingsController.php:92 @@ -4461,7 +4453,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Nejsou povoleny žádné zálohovací kódy! - + Part-DB1\src\Controller\UserSettingsController.php:138 Part-DB1\src\Controller\UserSettingsController.php:132 @@ -4471,7 +4463,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Žádný bezpečnostní klíč s tímto ID neexistuje. - + Part-DB1\src\Controller\UserSettingsController.php:145 Part-DB1\src\Controller\UserSettingsController.php:139 @@ -4481,7 +4473,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Bezpečnostní klíče jiných uživatelů nelze odstranit! - + Part-DB1\src\Controller\UserSettingsController.php:153 Part-DB1\src\Controller\UserSettingsController.php:147 @@ -4491,7 +4483,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Bezpečnostní klíč byl úspěšně odstraněn. - + Part-DB1\src\Controller\UserSettingsController.php:188 Part-DB1\src\Controller\UserSettingsController.php:180 @@ -4501,7 +4493,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Důvěryhodná zařízení byla úspěšně resetována. - + Part-DB1\src\Controller\UserSettingsController.php:235 Part-DB1\src\Controller\UserSettingsController.php:226 @@ -4512,7 +4504,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Nastavení uloženo! - + Part-DB1\src\Controller\UserSettingsController.php:297 Part-DB1\src\Controller\UserSettingsController.php:288 @@ -4523,7 +4515,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Heslo změněno! - + Part-DB1\src\Controller\UserSettingsController.php:317 Part-DB1\src\Controller\UserSettingsController.php:306 @@ -4533,7 +4525,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Aplikace Authenticator byla úspěšně aktivována. - + Part-DB1\src\Controller\UserSettingsController.php:328 Part-DB1\src\Controller\UserSettingsController.php:315 @@ -4543,7 +4535,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Aplikace Authenticator byla úspěšně deaktivována. - + Part-DB1\src\Controller\UserSettingsController.php:346 Part-DB1\src\Controller\UserSettingsController.php:332 @@ -4553,7 +4545,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Nové záložní kódy byly úspěšně vygenerovány. - + Part-DB1\src\DataTables\AttachmentDataTable.php:148 Part-DB1\src\DataTables\AttachmentDataTable.php:148 @@ -4563,7 +4555,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Název souboru - + Part-DB1\src\DataTables\AttachmentDataTable.php:153 Part-DB1\src\DataTables\AttachmentDataTable.php:153 @@ -4573,7 +4565,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Velikost souboru - + Part-DB1\src\DataTables\AttachmentDataTable.php:183 Part-DB1\src\DataTables\AttachmentDataTable.php:191 @@ -4593,7 +4585,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn pravda - + Part-DB1\src\DataTables\AttachmentDataTable.php:184 Part-DB1\src\DataTables\AttachmentDataTable.php:192 @@ -4615,7 +4607,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn nepravda - + Part-DB1\src\DataTables\Column\LogEntryTargetColumn.php:128 Part-DB1\src\DataTables\Column\LogEntryTargetColumn.php:119 @@ -4625,7 +4617,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn smazáno - + Part-DB1\src\DataTables\Column\RevertLogColumn.php:57 Part-DB1\src\DataTables\Column\RevertLogColumn.php:60 @@ -4636,7 +4628,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Odstranit prvek - + Part-DB1\src\DataTables\Column\RevertLogColumn.php:63 Part-DB1\src\DataTables\Column\RevertLogColumn.php:66 @@ -4647,7 +4639,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Vrátit změnu - + Part-DB1\src\DataTables\Column\RevertLogColumn.php:83 Part-DB1\src\DataTables\Column\RevertLogColumn.php:86 @@ -4658,7 +4650,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Vrátit prvek na toto časové razítko - + Part-DB1\src\DataTables\LogDataTable.php:173 Part-DB1\src\DataTables\LogDataTable.php:161 @@ -4668,7 +4660,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn ID - + Part-DB1\src\DataTables\LogDataTable.php:178 Part-DB1\src\DataTables\LogDataTable.php:166 @@ -4678,7 +4670,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Časové razítko - + Part-DB1\src\DataTables\LogDataTable.php:183 Part-DB1\src\DataTables\LogDataTable.php:171 @@ -4688,7 +4680,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Událost - + Part-DB1\src\DataTables\LogDataTable.php:191 Part-DB1\src\DataTables\LogDataTable.php:179 @@ -4698,7 +4690,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Úroveň - + Part-DB1\src\DataTables\LogDataTable.php:200 Part-DB1\src\DataTables\LogDataTable.php:188 @@ -4708,7 +4700,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Uživatel - + Part-DB1\src\DataTables\LogDataTable.php:213 Part-DB1\src\DataTables\LogDataTable.php:201 @@ -4718,7 +4710,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Typ cíle - + Part-DB1\src\DataTables\LogDataTable.php:226 Part-DB1\src\DataTables\LogDataTable.php:214 @@ -4728,7 +4720,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Cíl - + Part-DB1\src\DataTables\LogDataTable.php:231 Part-DB1\src\DataTables\LogDataTable.php:218 @@ -4739,7 +4731,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Extra - + Part-DB1\src\DataTables\PartsDataTable.php:168 Part-DB1\src\DataTables\PartsDataTable.php:116 @@ -4749,7 +4741,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Název - + Part-DB1\src\DataTables\PartsDataTable.php:178 Part-DB1\src\DataTables\PartsDataTable.php:126 @@ -4759,7 +4751,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn ID - + Part-DB1\src\DataTables\PartsDataTable.php:182 Part-DB1\src\DataTables\PartsDataTable.php:130 @@ -4769,7 +4761,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Popis - + Part-DB1\src\DataTables\PartsDataTable.php:185 Part-DB1\src\DataTables\PartsDataTable.php:133 @@ -4779,7 +4771,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Kategorie - + Part-DB1\src\DataTables\PartsDataTable.php:190 Part-DB1\src\DataTables\PartsDataTable.php:138 @@ -4789,7 +4781,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Otisk - + Part-DB1\src\DataTables\PartsDataTable.php:194 Part-DB1\src\DataTables\PartsDataTable.php:142 @@ -4799,7 +4791,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Výrobce - + Part-DB1\src\DataTables\PartsDataTable.php:197 Part-DB1\src\DataTables\PartsDataTable.php:145 @@ -4809,7 +4801,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Umístění - + Part-DB1\src\DataTables\PartsDataTable.php:216 Part-DB1\src\DataTables\PartsDataTable.php:164 @@ -4819,7 +4811,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Množství - + Part-DB1\src\DataTables\PartsDataTable.php:224 Part-DB1\src\DataTables\PartsDataTable.php:172 @@ -4829,7 +4821,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Min. množství - + Part-DB1\src\DataTables\PartsDataTable.php:232 Part-DB1\src\DataTables\PartsDataTable.php:180 @@ -4839,7 +4831,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Měrné jednotky - + Part-DB1\src\DataTables\PartsDataTable.php:236 Part-DB1\src\DataTables\PartsDataTable.php:184 @@ -4849,7 +4841,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Vytvořeno - + Part-DB1\src\DataTables\PartsDataTable.php:240 Part-DB1\src\DataTables\PartsDataTable.php:188 @@ -4859,7 +4851,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Naposledy upraveno - + Part-DB1\src\DataTables\PartsDataTable.php:244 Part-DB1\src\DataTables\PartsDataTable.php:192 @@ -4869,7 +4861,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Potřeba revize - + Part-DB1\src\DataTables\PartsDataTable.php:251 Part-DB1\src\DataTables\PartsDataTable.php:199 @@ -4879,7 +4871,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Oblíbené - + Part-DB1\src\DataTables\PartsDataTable.php:258 Part-DB1\src\DataTables\PartsDataTable.php:206 @@ -4889,7 +4881,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Stav - + Part-DB1\src\DataTables\PartsDataTable.php:260 Part-DB1\src\DataTables\PartsDataTable.php:262 @@ -4903,7 +4895,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Neznámý - + Part-DB1\src\DataTables\PartsDataTable.php:263 Part-DB1\src\Form\Part\PartBaseType.php:90 @@ -4915,7 +4907,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Oznámeno - + Part-DB1\src\DataTables\PartsDataTable.php:264 Part-DB1\src\Form\Part\PartBaseType.php:90 @@ -4927,7 +4919,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Aktivní - + Part-DB1\src\DataTables\PartsDataTable.php:265 Part-DB1\src\Form\Part\PartBaseType.php:90 @@ -4939,7 +4931,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Nedoporučuje se pro nové návrhy - + Part-DB1\src\DataTables\PartsDataTable.php:266 Part-DB1\src\Form\Part\PartBaseType.php:90 @@ -4951,7 +4943,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Ukončeno - + Part-DB1\src\DataTables\PartsDataTable.php:267 Part-DB1\src\Form\Part\PartBaseType.php:90 @@ -4963,7 +4955,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Přerušeno - + Part-DB1\src\DataTables\PartsDataTable.php:271 Part-DB1\src\DataTables\PartsDataTable.php:219 @@ -4973,7 +4965,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn MPN - + Part-DB1\src\DataTables\PartsDataTable.php:275 Part-DB1\src\DataTables\PartsDataTable.php:223 @@ -4983,7 +4975,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Hmotnost - + Part-DB1\src\DataTables\PartsDataTable.php:279 Part-DB1\src\DataTables\PartsDataTable.php:227 @@ -4993,7 +4985,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Štítky - + Part-DB1\src\DataTables\PartsDataTable.php:283 Part-DB1\src\DataTables\PartsDataTable.php:231 @@ -5003,7 +4995,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Přílohy - + Part-DB1\src\EventSubscriber\UserSystem\LoginSuccessSubscriber.php:82 Part-DB1\src\EventSubscriber\LoginSuccessListener.php:82 @@ -5013,7 +5005,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Přihlášení bylo úspěšné - + Part-DB1\src\Form\AdminPages\ImportType.php:77 Part-DB1\src\Form\AdminPages\ImportType.php:77 @@ -5024,7 +5016,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn JSON - + Part-DB1\src\Form\AdminPages\ImportType.php:77 Part-DB1\src\Form\AdminPages\ImportType.php:77 @@ -5035,7 +5027,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn XML - + Part-DB1\src\Form\AdminPages\ImportType.php:77 Part-DB1\src\Form\AdminPages\ImportType.php:77 @@ -5046,7 +5038,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn CSV - + Part-DB1\src\Form\AdminPages\ImportType.php:77 Part-DB1\src\Form\AdminPages\ImportType.php:77 @@ -5057,7 +5049,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn YAML - + Part-DB1\src\Form\AdminPages\ImportType.php:124 Part-DB1\src\Form\AdminPages\ImportType.php:124 @@ -5067,7 +5059,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Pokud je tato možnost aktivována, celý proces importu se přeruší, pokud jsou zjištěna neplatná data. Není-li tato možnost vybrána, neplatná data jsou ignorována a importér se pokusí importovat ostatní prvky. - + Part-DB1\src\Form\AdminPages\ImportType.php:86 Part-DB1\src\Form\AdminPages\ImportType.php:86 @@ -5078,7 +5070,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn CSV oddělovač - + Part-DB1\src\Form\AdminPages\ImportType.php:93 Part-DB1\src\Form\AdminPages\ImportType.php:93 @@ -5089,7 +5081,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Nadřazený prvek - + Part-DB1\src\Form\AdminPages\ImportType.php:101 Part-DB1\src\Form\AdminPages\ImportType.php:101 @@ -5100,7 +5092,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Soubor - + Part-DB1\src\Form\AdminPages\ImportType.php:111 Part-DB1\src\Form\AdminPages\ImportType.php:111 @@ -5111,7 +5103,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Zachování podřízených prvků při importu - + Part-DB1\src\Form\AdminPages\ImportType.php:120 Part-DB1\src\Form\AdminPages\ImportType.php:120 @@ -5122,7 +5114,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Přerušit při neplatných datech - + Part-DB1\src\Form\AdminPages\ImportType.php:132 Part-DB1\src\Form\AdminPages\ImportType.php:132 @@ -5133,7 +5125,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Import - + Part-DB1\src\Form\AttachmentFormType.php:113 Part-DB1\src\Form\AttachmentFormType.php:109 @@ -5143,7 +5135,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn K příloze označené jako soukromá mají přístup pouze ověření uživatelé s příslušným oprávněním. Pokud je tato funkce aktivována, negenerují se náhledy a přístup k souboru je méně výkonný. - + Part-DB1\src\Form\AttachmentFormType.php:127 Part-DB1\src\Form\AttachmentFormType.php:123 @@ -5153,7 +5145,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Zde můžete zadat adresu URL externího souboru nebo zadat klíčové slovo, které se používá k hledání ve vestavěných zdrojích (např. otisky). - + Part-DB1\src\Form\AttachmentFormType.php:82 Part-DB1\src\Form\AttachmentFormType.php:79 @@ -5163,7 +5155,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Název - + Part-DB1\src\Form\AttachmentFormType.php:85 Part-DB1\src\Form\AttachmentFormType.php:82 @@ -5173,7 +5165,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Typ přílohy - + Part-DB1\src\Form\AttachmentFormType.php:94 Part-DB1\src\Form\AttachmentFormType.php:91 @@ -5183,7 +5175,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Zobrazit v tabulce - + Part-DB1\src\Form\AttachmentFormType.php:105 Part-DB1\src\Form\AttachmentFormType.php:102 @@ -5193,7 +5185,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Soukromá příloha - + Part-DB1\src\Form\AttachmentFormType.php:119 Part-DB1\src\Form\AttachmentFormType.php:115 @@ -5203,7 +5195,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn URL - + Part-DB1\src\Form\AttachmentFormType.php:133 Part-DB1\src\Form\AttachmentFormType.php:129 @@ -5213,7 +5205,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Stáhnout externí soubor - + Part-DB1\src\Form\AttachmentFormType.php:146 Part-DB1\src\Form\AttachmentFormType.php:142 @@ -5223,7 +5215,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Nahrát soubor - + Part-DB1\src\Form\LabelOptionsType.php:68 Part-DB1\src\Services\ElementTypeNameGenerator.php:86 @@ -5233,7 +5225,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Díl - + Part-DB1\src\Form\LabelOptionsType.php:68 Part-DB1\src\Services\ElementTypeNameGenerator.php:87 @@ -5243,7 +5235,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Inventář - + Part-DB1\src\Form\LabelOptionsType.php:78 @@ -5252,7 +5244,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Žádné - + Part-DB1\src\Form\LabelOptionsType.php:78 @@ -5261,7 +5253,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn QR kód (doporučeno) - + Part-DB1\src\Form\LabelOptionsType.php:78 @@ -5270,7 +5262,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Kód 128 (doporučeno) - + Part-DB1\src\Form\LabelOptionsType.php:78 @@ -5279,7 +5271,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Kód 39 (doporučeno) - + Part-DB1\src\Form\LabelOptionsType.php:78 @@ -5288,7 +5280,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Kód 39 - + Part-DB1\src\Form\LabelOptionsType.php:78 @@ -5297,7 +5289,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Datamatrix - + Part-DB1\src\Form\LabelOptionsType.php:122 @@ -5306,7 +5298,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Zástupné symboly - + Part-DB1\src\Form\LabelOptionsType.php:122 @@ -5315,7 +5307,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Twig - + Part-DB1\src\Form\LabelOptionsType.php:126 @@ -5324,7 +5316,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Pokud zde vyberete Twig, bude pole obsahu interpretováno jako Twig šablona. Viz <a href="https://twig.symfony.com/doc/3.x/templates.html">dokumentace Twig</a> a <a href="https://docs.part-db.de/usage/labels.html#twig-mode">Wiki</a>, kde najdete další informace. - + Part-DB1\src\Form\LabelOptionsType.php:47 @@ -5333,7 +5325,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Velikost štítku - + Part-DB1\src\Form\LabelOptionsType.php:66 @@ -5342,7 +5334,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Typ cíle - + Part-DB1\src\Form\LabelOptionsType.php:75 @@ -5351,7 +5343,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Čárový kód - + Part-DB1\src\Form\LabelOptionsType.php:102 @@ -5360,7 +5352,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Obsah - + Part-DB1\src\Form\LabelOptionsType.php:111 @@ -5369,7 +5361,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Další styly (CSS) - + Part-DB1\src\Form\LabelOptionsType.php:120 @@ -5378,7 +5370,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Režim parseru - + Part-DB1\src\Form\LabelOptionsType.php:51 @@ -5387,7 +5379,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Šířka - + Part-DB1\src\Form\LabelOptionsType.php:60 @@ -5396,7 +5388,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Výška - + Part-DB1\src\Form\LabelSystem\LabelDialogType.php:49 @@ -5405,7 +5397,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Zde můžete zadat více ID (např. 1,2,3) a/nebo rozsah (1-3), abyste mohli generovat štítky pro více prvků najednou. - + Part-DB1\src\Form\LabelSystem\LabelDialogType.php:46 @@ -5414,7 +5406,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Cílové ID - + Part-DB1\src\Form\LabelSystem\LabelDialogType.php:59 @@ -5423,7 +5415,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Aktualizovat - + Part-DB1\src\Form\LabelSystem\ScanDialogType.php:36 @@ -5432,7 +5424,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Zadání - + Part-DB1\src\Form\LabelSystem\ScanDialogType.php:44 @@ -5441,7 +5433,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Odeslat - + Part-DB1\src\Form\ParameterType.php:41 @@ -5450,7 +5442,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn např. Stejnosměrný proudový zisk - + Part-DB1\src\Form\ParameterType.php:50 @@ -5459,7 +5451,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn např. h_{FE} - + Part-DB1\src\Form\ParameterType.php:60 @@ -5468,7 +5460,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn např. Testovací podmínky - + Part-DB1\src\Form\ParameterType.php:71 @@ -5477,7 +5469,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn např. 350 - + Part-DB1\src\Form\ParameterType.php:82 @@ -5486,7 +5478,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn např. 100 - + Part-DB1\src\Form\ParameterType.php:93 @@ -5495,7 +5487,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn např. 200 - + Part-DB1\src\Form\ParameterType.php:103 @@ -5504,7 +5496,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn např. V - + Part-DB1\src\Form\ParameterType.php:114 @@ -5513,7 +5505,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn např. Technické specifikace - + Part-DB1\src\Form\Part\OrderdetailType.php:72 Part-DB1\src\Form\Part\OrderdetailType.php:75 @@ -5523,7 +5515,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Číslo dílu dodavatele - + Part-DB1\src\Form\Part\OrderdetailType.php:81 Part-DB1\src\Form\Part\OrderdetailType.php:84 @@ -5533,7 +5525,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Dodavatel - + Part-DB1\src\Form\Part\OrderdetailType.php:87 Part-DB1\src\Form\Part\OrderdetailType.php:90 @@ -5543,7 +5535,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Odkaz na nabídku - + Part-DB1\src\Form\Part\OrderdetailType.php:93 Part-DB1\src\Form\Part\OrderdetailType.php:96 @@ -5553,7 +5545,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Již není k dispozici - + Part-DB1\src\Form\Part\OrderdetailType.php:75 Part-DB1\src\Form\Part\OrderdetailType.php:78 @@ -5563,7 +5555,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn např. BC 547 - + Part-DB1\src\Form\Part\PartBaseType.php:101 Part-DB1\src\Form\Part\PartBaseType.php:99 @@ -5573,7 +5565,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Název - + Part-DB1\src\Form\Part\PartBaseType.php:109 Part-DB1\src\Form\Part\PartBaseType.php:107 @@ -5583,7 +5575,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Popis - + Part-DB1\src\Form\Part\PartBaseType.php:120 Part-DB1\src\Form\Part\PartBaseType.php:118 @@ -5593,7 +5585,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Minimální zásoba - + Part-DB1\src\Form\Part\PartBaseType.php:129 Part-DB1\src\Form\Part\PartBaseType.php:127 @@ -5603,7 +5595,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Kategorie - + Part-DB1\src\Form\Part\PartBaseType.php:135 Part-DB1\src\Form\Part\PartBaseType.php:133 @@ -5613,7 +5605,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Otisk - + Part-DB1\src\Form\Part\PartBaseType.php:142 Part-DB1\src\Form\Part\PartBaseType.php:140 @@ -5623,7 +5615,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Štítky - + Part-DB1\src\Form\Part\PartBaseType.php:154 Part-DB1\src\Form\Part\PartBaseType.php:152 @@ -5633,7 +5625,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Výrobce - + Part-DB1\src\Form\Part\PartBaseType.php:161 Part-DB1\src\Form\Part\PartBaseType.php:159 @@ -5643,7 +5635,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Odkaz na stránku produktu - + Part-DB1\src\Form\Part\PartBaseType.php:167 Part-DB1\src\Form\Part\PartBaseType.php:165 @@ -5653,7 +5645,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Číslo dílu výrobce - + Part-DB1\src\Form\Part\PartBaseType.php:173 Part-DB1\src\Form\Part\PartBaseType.php:171 @@ -5663,7 +5655,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Stav výroby - + Part-DB1\src\Form\Part\PartBaseType.php:181 Part-DB1\src\Form\Part\PartBaseType.php:179 @@ -5673,7 +5665,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Potřeba revize - + Part-DB1\src\Form\Part\PartBaseType.php:189 Part-DB1\src\Form\Part\PartBaseType.php:187 @@ -5683,7 +5675,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Oblíbené - + Part-DB1\src\Form\Part\PartBaseType.php:197 Part-DB1\src\Form\Part\PartBaseType.php:195 @@ -5693,7 +5685,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Hmotnost - + Part-DB1\src\Form\Part\PartBaseType.php:203 Part-DB1\src\Form\Part\PartBaseType.php:201 @@ -5703,7 +5695,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Měrná jednotka - + Part-DB1\src\Form\Part\PartBaseType.php:212 Part-DB1\src\Form\Part\PartBaseType.php:210 @@ -5713,7 +5705,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Poznámky - + Part-DB1\src\Form\Part\PartBaseType.php:250 Part-DB1\src\Form\Part\PartBaseType.php:246 @@ -5723,7 +5715,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Náhled - + Part-DB1\src\Form\Part\PartBaseType.php:295 Part-DB1\src\Form\Part\PartBaseType.php:276 @@ -5734,7 +5726,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Uložit změny - + Part-DB1\src\Form\Part\PartBaseType.php:296 Part-DB1\src\Form\Part\PartBaseType.php:277 @@ -5745,7 +5737,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Zrušit změny - + Part-DB1\src\Form\Part\PartBaseType.php:105 Part-DB1\src\Form\Part\PartBaseType.php:103 @@ -5755,7 +5747,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn např. BC547 - + Part-DB1\src\Form\Part\PartBaseType.php:115 Part-DB1\src\Form\Part\PartBaseType.php:113 @@ -5765,7 +5757,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn např. NPN 45V, 0,1A, 0,5W - + Part-DB1\src\Form\Part\PartBaseType.php:123 Part-DB1\src\Form\Part\PartBaseType.php:121 @@ -5775,7 +5767,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn např. 1 - + Part-DB1\src\Form\Part\PartLotType.php:69 Part-DB1\src\Form\Part\PartLotType.php:69 @@ -5785,7 +5777,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Popis - + Part-DB1\src\Form\Part\PartLotType.php:78 Part-DB1\src\Form\Part\PartLotType.php:78 @@ -5795,7 +5787,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Umístění - + Part-DB1\src\Form\Part\PartLotType.php:89 Part-DB1\src\Form\Part\PartLotType.php:89 @@ -5805,7 +5797,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Množství - + Part-DB1\src\Form\Part\PartLotType.php:98 Part-DB1\src\Form\Part\PartLotType.php:97 @@ -5815,7 +5807,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Množství neznámé - + Part-DB1\src\Form\Part\PartLotType.php:109 Part-DB1\src\Form\Part\PartLotType.php:108 @@ -5825,7 +5817,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Potřebuje doplnit - + Part-DB1\src\Form\Part\PartLotType.php:120 Part-DB1\src\Form\Part\PartLotType.php:119 @@ -5835,7 +5827,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Datum vypršení platnosti - + Part-DB1\src\Form\Part\PartLotType.php:128 Part-DB1\src\Form\Part\PartLotType.php:125 @@ -5845,7 +5837,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Poznámky - + Part-DB1\src\Form\Permissions\PermissionsType.php:99 Part-DB1\src\Form\Permissions\PermissionsType.php:99 @@ -5855,7 +5847,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Různé - + Part-DB1\src\Form\TFAGoogleSettingsType.php:97 Part-DB1\src\Form\TFAGoogleSettingsType.php:97 @@ -5865,7 +5857,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Povolit aplikaci Authenticator - + Part-DB1\src\Form\TFAGoogleSettingsType.php:101 Part-DB1\src\Form\TFAGoogleSettingsType.php:101 @@ -5875,7 +5867,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Deaktivovat aplikaci Authenticator - + Part-DB1\src\Form\TFAGoogleSettingsType.php:74 Part-DB1\src\Form\TFAGoogleSettingsType.php:74 @@ -5885,7 +5877,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Potvrzovací kód - + Part-DB1\src\Form\UserSettingsType.php:108 Part-DB1\src\Form\UserSettingsType.php:108 @@ -5896,7 +5888,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Časové pásmo - + Part-DB1\src\Form\UserSettingsType.php:133 Part-DB1\src\Form\UserSettingsType.php:132 @@ -5906,7 +5898,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Preferovaná měna - + Part-DB1\src\Form\UserSettingsType.php:140 Part-DB1\src\Form\UserSettingsType.php:139 @@ -5917,7 +5909,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Použít změny - + Part-DB1\src\Form\UserSettingsType.php:141 Part-DB1\src\Form\UserSettingsType.php:140 @@ -5928,7 +5920,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Zrušit změny - + Part-DB1\src\Form\UserSettingsType.php:104 Part-DB1\src\Form\UserSettingsType.php:104 @@ -5939,7 +5931,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Jazyk serveru - + Part-DB1\src\Form\UserSettingsType.php:115 Part-DB1\src\Form\UserSettingsType.php:115 @@ -5950,7 +5942,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Časové pásmo serveru - + Part-DB1\src\Services\ElementTypeNameGenerator.php:79 Part-DB1\src\Services\ElementTypeNameGenerator.php:79 @@ -5960,7 +5952,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Příloha - + Part-DB1\src\Services\ElementTypeNameGenerator.php:81 Part-DB1\src\Services\ElementTypeNameGenerator.php:81 @@ -5970,7 +5962,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Typ přílohy - + Part-DB1\src\Services\ElementTypeNameGenerator.php:82 Part-DB1\src\Services\ElementTypeNameGenerator.php:82 @@ -5980,7 +5972,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Projekt - + Part-DB1\src\Services\ElementTypeNameGenerator.php:85 Part-DB1\src\Services\ElementTypeNameGenerator.php:85 @@ -5990,7 +5982,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Měrná jednotka - + Part-DB1\src\Services\ElementTypeNameGenerator.php:90 Part-DB1\src\Services\ElementTypeNameGenerator.php:90 @@ -6000,7 +5992,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Měna - + Part-DB1\src\Services\ElementTypeNameGenerator.php:91 Part-DB1\src\Services\ElementTypeNameGenerator.php:91 @@ -6010,7 +6002,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Detail objednávky - + Part-DB1\src\Services\ElementTypeNameGenerator.php:92 Part-DB1\src\Services\ElementTypeNameGenerator.php:92 @@ -6020,7 +6012,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Detail ceny - + Part-DB1\src\Services\ElementTypeNameGenerator.php:94 Part-DB1\src\Services\ElementTypeNameGenerator.php:94 @@ -6030,7 +6022,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Uživatel - + Part-DB1\src\Services\ElementTypeNameGenerator.php:95 @@ -6039,7 +6031,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Parametr - + Part-DB1\src\Services\ElementTypeNameGenerator.php:96 @@ -6048,7 +6040,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Profil štítku - + Part-DB1\src\Services\LogSystem\LogEntryExtraFormatter.php:176 Part-DB1\src\Services\LogSystem\LogEntryExtraFormatter.php:161 @@ -6059,7 +6051,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Neznámý - + Part-DB1\src\Services\MarkdownParser.php:73 Part-DB1\src\Services\MarkdownParser.php:73 @@ -6069,7 +6061,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Načítání markdown. Pokud tato zpráva nezmizí, zkuste stránku načíst znovu. - + Part-DB1\src\Services\PasswordResetManager.php:98 Part-DB1\src\Services\PasswordResetManager.php:98 @@ -6079,7 +6071,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Obnovení hesla k účtu Part-DB - + Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:108 @@ -6088,7 +6080,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Nástroje - + Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:109 Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:107 @@ -6099,7 +6091,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Upravit - + Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:110 Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:108 @@ -6110,7 +6102,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Zobrazit - + Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:111 Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:109 @@ -6120,7 +6112,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Systém - + Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:123 @@ -6129,7 +6121,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Generátor štítků - + Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:130 @@ -6138,7 +6130,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Čtečka štítků - + Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:149 Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:126 @@ -6149,7 +6141,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Typy příloh - + Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:155 Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:132 @@ -6160,7 +6152,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Kategorie - + Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:161 Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:138 @@ -6171,7 +6163,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Projekty - + Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:167 Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:144 @@ -6182,7 +6174,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Dodavatelé - + Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:173 Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:150 @@ -6193,7 +6185,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Výrobce - + Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:179 Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:156 @@ -6203,7 +6195,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Umístění - + Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:185 Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:162 @@ -6213,7 +6205,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Otisky - + Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:191 Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:168 @@ -6223,7 +6215,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Měny - + Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:197 Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:174 @@ -6233,7 +6225,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Měrné jednotky - + Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:203 @@ -6242,7 +6234,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Profily štítků - + Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:209 Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:180 @@ -6252,7 +6244,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Nový díl - + Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:226 Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:197 @@ -6263,7 +6255,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Zobrazit všechny díly - + Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:232 Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:203 @@ -6273,7 +6265,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Přílohy - + Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:239 Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:210 @@ -6284,7 +6276,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Statistiky - + Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:258 Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:229 @@ -6294,7 +6286,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Uživatelé - + Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:264 Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:235 @@ -6304,7 +6296,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Skupiny - + Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:271 Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:242 @@ -6315,7 +6307,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Protokol událostí - + Part-DB1\src\Services\Trees\TreeViewGenerator.php:95 Part-DB1\src\Services\Trees\TreeViewGenerator.php:95 @@ -6326,7 +6318,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Nový prvek - + Part-DB1\templates\Parts\info\_attachments_info.html.twig:34 obsolete @@ -6336,7 +6328,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Externí soubor - + Part-DB1\templates\Parts\info\_attachments_info.html.twig:62 obsolete @@ -6346,7 +6338,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Upravit - + Part-DB1\templates\_navbar.html.twig:27 templates\base.html.twig:88 @@ -6357,7 +6349,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Skenovat čárový kód - + Part-DB1\src\Form\UserSettingsType.php:119 src\Form\UserSettingsType.php:49 @@ -6368,7 +6360,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Téma - + Part-DB1\src\Form\UserSettingsType.php:129 src\Form\UserSettingsType.php:50 @@ -6379,7 +6371,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Serverové téma - + Part-DB1\src\Services\LogSystem\LogEntryExtraFormatter.php:100 new @@ -6390,7 +6382,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn IP - + Part-DB1\src\Services\LogSystem\LogEntryExtraFormatter.php:128 Part-DB1\src\Services\LogSystem\LogEntryExtraFormatter.php:150 @@ -6404,7 +6396,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Změna zrušena - + Part-DB1\src\Services\LogSystem\LogEntryExtraFormatter.php:130 Part-DB1\src\Services\LogSystem\LogEntryExtraFormatter.php:152 @@ -6418,7 +6410,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Prvek vrácen - + Part-DB1\src\Services\LogSystem\LogEntryExtraFormatter.php:139 new @@ -6429,7 +6421,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Staré zásoby - + Part-DB1\src\Services\LogSystem\LogEntryExtraFormatter.php:160 new @@ -6440,7 +6432,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Starý název - + Part-DB1\src\Services\LogSystem\LogEntryExtraFormatter.php:184 new @@ -6451,7 +6443,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Změněná pole - + Part-DB1\src\Services\LogSystem\LogEntryExtraFormatter.php:198 new @@ -6462,7 +6454,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Komentář - + Part-DB1\src\Services\LogSystem\LogEntryExtraFormatter.php:214 new @@ -6473,7 +6465,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Odstraněný prvek: - + templates\base.html.twig:81 obsolete @@ -6484,7 +6476,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Jdi! - + templates\base.html.twig:109 obsolete @@ -6495,7 +6487,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn English - + templates\base.html.twig:112 obsolete @@ -6506,7 +6498,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn German - + obsolete obsolete @@ -6516,7 +6508,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Nutná změna hesla! - + obsolete obsolete @@ -6526,7 +6518,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Typ přílohy - + obsolete obsolete @@ -6536,7 +6528,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Související prvek - + obsolete obsolete @@ -6546,7 +6538,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Obrázek? - + obsolete obsolete @@ -6556,7 +6548,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn 3D model? - + obsolete obsolete @@ -6566,7 +6558,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Vestavěný? - + obsolete obsolete @@ -6576,7 +6568,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn např. užitečné pro přepínání - + obsolete obsolete @@ -6586,7 +6578,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Generování nových záložních kódů - + obsolete obsolete @@ -6596,7 +6588,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Prvek nemůže být svým vlastním rodičem. - + obsolete obsolete @@ -6606,7 +6598,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Rodič nemůže být jedním ze svých potomků. - + obsolete obsolete @@ -6616,7 +6608,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Místo je obsazeno. Množství nelze navýšit (nová hodnota musí být menší než {{ old_amount }}). - + obsolete obsolete @@ -6626,7 +6618,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Úložiště bylo označeno jako plné, takže do něj nelze přidat nový díl. - + obsolete obsolete @@ -6636,17 +6628,17 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Úložiště bylo označeno jako "pouze existující", takže do něj nelze přidat novou část. - + obsolete obsolete validator.part_lot.single_part - Úložiště bylo označeno jako "jeden díl", takže do něj nelze přidat nový díl. + Toto umístění může obsahovat pouze jeden díl, takže do něj nelze přídávat další! - + obsolete obsolete @@ -6656,7 +6648,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Tento díl je v současné době a v dohledné budoucnosti ve výrobě. - + obsolete obsolete @@ -6666,7 +6658,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Díl byl oznámen, ale zatím není k dispozici. - + obsolete obsolete @@ -6676,7 +6668,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Tento díl se přestal vyrábět a již se nevyrábí. - + obsolete obsolete @@ -6686,7 +6678,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Výrobek dosáhl konce své životnosti a jeho výroba bude brzy ukončena. - + obsolete obsolete @@ -6696,7 +6688,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Tento díl se v současné době vyrábí, ale pro nové konstrukce se nedoporučuje. - + obsolete obsolete @@ -6706,7 +6698,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Stav výroby dílu není znám. - + obsolete obsolete @@ -6716,7 +6708,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Úspěšné - + obsolete obsolete @@ -6726,7 +6718,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Chyba - + obsolete obsolete @@ -6736,7 +6728,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Upozornění - + obsolete obsolete @@ -6746,7 +6738,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Oznámení - + obsolete obsolete @@ -6756,7 +6748,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Info - + obsolete obsolete @@ -6766,7 +6758,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Oprávnění "změnit oprávnění" nemůžete sami odebrat, abyste se náhodou nezablokovali. - + obsolete obsolete @@ -6776,7 +6768,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Povolené přípony souborů - + obsolete obsolete @@ -6786,7 +6778,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Můžete zadat čárkou oddělený seznam přípon souborů nebo typů mime, které musí mít nahraný soubor přiřazený k tomuto typu přílohy. Chcete-li povolit všechny podporované obrazové soubory, můžete použít příkaz image/*. - + obsolete obsolete @@ -6796,7 +6788,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn např. .txt, application/pdf, image/* - + src\Form\PartType.php:63 obsolete @@ -6807,7 +6799,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn např. BC547 - + obsolete obsolete @@ -6817,7 +6809,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Nelze vybrat - + obsolete obsolete @@ -6827,7 +6819,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Pokud je tato možnost aktivována, nelze tento prvek přiřadit k vlastnosti dílu. Užitečné, pokud se tento prvek používá pouze pro seskupování. - + obsolete obsolete @@ -6837,7 +6829,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Zde můžete použít kód BBC (např. [b]Bold[/b]). - + obsolete obsolete @@ -6847,7 +6839,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Vytvořit prvek - + obsolete obsolete @@ -6857,7 +6849,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Uložit - + obsolete obsolete @@ -6867,7 +6859,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Zakázat otisky - + obsolete obsolete @@ -6877,7 +6869,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Pokud je tato možnost aktivována, je vlastnost otisku zakázána pro všechny díly s touto kategorií. - + obsolete obsolete @@ -6887,7 +6879,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Zakázat výrobce - + obsolete obsolete @@ -6897,7 +6889,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Pokud je tato možnost aktivována, je vlastnost výrobce zakázána pro všechny díly s touto kategorií. - + obsolete obsolete @@ -6907,7 +6899,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Zakázat automatické odkazy na katalogové listy - + obsolete obsolete @@ -6917,7 +6909,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Pokud je tato možnost aktivována, nevytvářejí se pro díly s touto kategorií žádné automatické odkazy na datové listy. - + obsolete obsolete @@ -6927,7 +6919,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Zakázat vlastnosti - + obsolete obsolete @@ -6937,7 +6929,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Pokud je tato možnost aktivována, vlastnosti dílu jsou pro díly s touto kategorií zakázány. - + obsolete obsolete @@ -6947,7 +6939,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Nápověda k názvu dílu - + obsolete obsolete @@ -6957,7 +6949,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn např. 100nF - + obsolete obsolete @@ -6967,7 +6959,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Filtr názvů - + obsolete obsolete @@ -6977,7 +6969,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Výchozí popis - + obsolete obsolete @@ -6987,7 +6979,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Např. kondenzátor, 10 mm x 10 mm, SMD - + obsolete obsolete @@ -6997,7 +6989,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Výchozí poznámky - + obsolete obsolete @@ -7007,7 +6999,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Addresa - + obsolete obsolete @@ -7018,7 +7010,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Město - + obsolete obsolete @@ -7028,7 +7020,7 @@ Město Telefonní číslo - + obsolete obsolete @@ -7038,7 +7030,7 @@ Město +420 123 456 789 - + obsolete obsolete @@ -7048,7 +7040,7 @@ Město Číslo faxu - + obsolete obsolete @@ -7058,7 +7050,7 @@ Město e-mail - + obsolete obsolete @@ -7068,7 +7060,7 @@ Město např. contact@foo.bar - + obsolete obsolete @@ -7078,7 +7070,7 @@ Město Webové stránky - + obsolete obsolete @@ -7088,7 +7080,7 @@ Město https://www.foo.bar - + obsolete obsolete @@ -7098,7 +7090,7 @@ Město Produkt URL - + obsolete obsolete @@ -7108,7 +7100,7 @@ Město Toto pole slouží k určení odkazu na díl na stránce společnosti. %PARTNUMBER% bude nahrazeno číslem objednávky. - + obsolete obsolete @@ -7118,7 +7110,7 @@ Město https://foo.bar/product/%PARTNUMBER% - + obsolete obsolete @@ -7128,7 +7120,7 @@ Město Kód ISO - + obsolete obsolete @@ -7138,7 +7130,7 @@ Město Směnný kurz - + obsolete obsolete @@ -7148,7 +7140,7 @@ Město 3D model - + obsolete obsolete @@ -7158,7 +7150,7 @@ Město Zadání - + obsolete obsolete @@ -7173,7 +7165,7 @@ Element 2 Element 3 - + obsolete obsolete @@ -7183,7 +7175,7 @@ Element 3 Vytvořit - + obsolete obsolete @@ -7193,7 +7185,7 @@ Element 3 Je celé číslo - + obsolete obsolete @@ -7203,7 +7195,7 @@ Element 3 Pokud je tato možnost aktivována, budou všechny hodnoty s touto jednotkou zaokrouhleny na celá čísla. - + obsolete obsolete @@ -7213,7 +7205,7 @@ Element 3 Použití předpony SI - + obsolete obsolete @@ -7223,7 +7215,7 @@ Element 3 Pokud je tato možnost aktivována, hodnoty se zobrazují s předponami SI (např. 1,2 kg místo 1200 g). - + obsolete obsolete @@ -7233,7 +7225,7 @@ Element 3 Symbol jednotky - + obsolete obsolete @@ -7243,7 +7235,7 @@ Element 3 např. m - + obsolete obsolete @@ -7253,7 +7245,7 @@ Element 3 Úložiště plné - + obsolete obsolete @@ -7263,7 +7255,7 @@ Element 3 Pokud je tato možnost vybrána, není možné přidávat nové díly do tohoto umístění skladu ani zvyšovat množství stávajících dílů. - + obsolete obsolete @@ -7273,7 +7265,7 @@ Element 3 Omezení na stávající díly - + obsolete obsolete @@ -7283,7 +7275,7 @@ Element 3 Pokud je tato možnost aktivována, není možné přidávat nové díly do tohoto umístění, ale množství stávajících dílů lze navýšit. - + obsolete obsolete @@ -7293,7 +7285,7 @@ Element 3 Pouze jeden díl - + obsolete obsolete @@ -7303,7 +7295,7 @@ Element 3 Pokud je tato možnost aktivována, lze k tomuto úložišti přiřadit pouze jeden díl (s každým množstvím). Užitečné pro malé krabičky SMD nebo podavače. - + obsolete obsolete @@ -7313,7 +7305,7 @@ Element 3 Typ úložiště - + obsolete obsolete @@ -7323,7 +7315,7 @@ Element 3 Zde můžete vybrat měrnou jednotku, kterou musí díl mít, aby mohl být přiřazen k tomuto úložišti. - + obsolete obsolete @@ -7333,7 +7325,7 @@ Element 3 Výchozí měna - + obsolete obsolete @@ -7343,7 +7335,7 @@ Element 3 Náklady na dopravu - + obsolete obsolete @@ -7353,7 +7345,7 @@ Element 3 např. j.doe - + obsolete obsolete @@ -7363,7 +7355,7 @@ Element 3 např. John - + obsolete obsolete @@ -7373,7 +7365,7 @@ Element 3 např. Doe - + obsolete obsolete @@ -7383,7 +7375,7 @@ Element 3 j.doe@ecorp.com - + obsolete obsolete @@ -7393,7 +7385,7 @@ Element 3 např. Vývoj - + obsolete obsolete @@ -7403,7 +7395,7 @@ Element 3 Nové heslo - + obsolete obsolete @@ -7413,7 +7405,7 @@ Element 3 Potvrdit nové heslo - + obsolete obsolete @@ -7423,7 +7415,7 @@ Element 3 Uživatel musí změnit heslo - + obsolete obsolete @@ -7433,7 +7425,7 @@ Element 3 Uživatel zakázán (přihlášení není možné) - + obsolete obsolete @@ -7443,7 +7435,7 @@ Element 3 Vytvořit uživatele - + obsolete obsolete @@ -7453,7 +7445,7 @@ Element 3 Uložit - + obsolete obsolete @@ -7463,7 +7455,7 @@ Element 3 Zrušit změny - + templates\Parts\show_part_info.html.twig:166 obsolete @@ -7474,7 +7466,7 @@ Element 3 Stáhnout - + templates\Parts\show_part_info.html.twig:171 obsolete @@ -7485,7 +7477,7 @@ Element 3 Komentář/účel - + templates\Parts\show_part_info.html.twig:189 obsolete @@ -7496,7 +7488,7 @@ Element 3 Přidání dílů - + templates\Parts\show_part_info.html.twig:194 obsolete @@ -7507,7 +7499,7 @@ Element 3 Přidat - + templates\Parts\show_part_info.html.twig:199 obsolete @@ -7518,7 +7510,7 @@ Element 3 Komentář/účel - + templates\AdminPages\CompanyAdminBase.html.twig:15 obsolete @@ -7529,7 +7521,7 @@ Element 3 Poznámky - + src\Form\PartType.php:83 obsolete @@ -7540,7 +7532,7 @@ Element 3 Odkaz na výrobce - + src\Form\PartType.php:66 obsolete @@ -7551,7 +7543,7 @@ Element 3 např. NPN 45V 0,1A 0,5W - + src\Form\PartType.php:69 obsolete @@ -7562,7 +7554,7 @@ Element 3 např. 10 - + src\Form\PartType.php:72 obsolete @@ -7573,7 +7565,7 @@ Element 3 např. 5 - + obsolete obsolete @@ -7583,7 +7575,7 @@ Element 3 Cena za - + obsolete obsolete @@ -7593,7 +7585,7 @@ Element 3 Odebrání dílů - + obsolete obsolete @@ -7603,7 +7595,7 @@ Element 3 _MENU_ - + obsolete obsolete @@ -7613,7 +7605,7 @@ Element 3 Díly - + obsolete obsolete @@ -7623,7 +7615,7 @@ Element 3 Datové struktury - + obsolete obsolete @@ -7633,7 +7625,7 @@ Element 3 Systém - + obsolete obsolete @@ -7643,7 +7635,7 @@ Element 3 Díly - + obsolete obsolete @@ -7653,7 +7645,7 @@ Element 3 Zobrazit - + obsolete obsolete @@ -7663,7 +7655,7 @@ Element 3 Upravit - + obsolete obsolete @@ -7673,7 +7665,7 @@ Element 3 Vytvořit - + obsolete obsolete @@ -7683,7 +7675,7 @@ Element 3 Změnit kategorii - + obsolete obsolete @@ -7693,7 +7685,7 @@ Element 3 Smazat - + obsolete obsolete @@ -7703,7 +7695,7 @@ Element 3 Hledat - + obsolete obsolete @@ -7713,7 +7705,7 @@ Element 3 Seznam všech dílů - + obsolete obsolete @@ -7723,7 +7715,7 @@ Element 3 Seznam dílů bez informací o ceně - + obsolete obsolete @@ -7733,7 +7725,7 @@ Element 3 Seznam zastaralých dílů - + obsolete obsolete @@ -7743,7 +7735,7 @@ Element 3 Zobrazit díly s neznámým skladem - + obsolete obsolete @@ -7753,7 +7745,7 @@ Element 3 Změna stavu oblíbených položek - + obsolete obsolete @@ -7763,7 +7755,7 @@ Element 3 Seznam oblíbených dílů - + obsolete obsolete @@ -7773,7 +7765,7 @@ Element 3 Zobrazit naposledy upravené/přidané díly - + obsolete obsolete @@ -7783,7 +7775,7 @@ Element 3 Zobrazit posledního upravujícího uživatele - + obsolete obsolete @@ -7793,7 +7785,7 @@ Element 3 Zobrazit historii - + obsolete obsolete @@ -7803,7 +7795,7 @@ Element 3 Název - + obsolete obsolete @@ -7813,7 +7805,7 @@ Element 3 Popis - + obsolete obsolete @@ -7823,7 +7815,7 @@ Element 3 Skladem - + obsolete obsolete @@ -7833,7 +7825,7 @@ Element 3 Minimální stav zásob - + obsolete obsolete @@ -7843,7 +7835,7 @@ Element 3 Poznámky - + obsolete obsolete @@ -7853,7 +7845,7 @@ Element 3 Místo skladování - + obsolete obsolete @@ -7863,7 +7855,7 @@ Element 3 Výrobce - + obsolete obsolete @@ -7873,7 +7865,7 @@ Element 3 Informace o objednávce - + obsolete obsolete @@ -7883,7 +7875,7 @@ Element 3 Ceny - + obsolete obsolete @@ -7893,7 +7885,7 @@ Element 3 Přílohy souborů - + obsolete obsolete @@ -7903,7 +7895,7 @@ Element 3 Objednávky - + obsolete obsolete @@ -7913,7 +7905,7 @@ Element 3 Umístění - + obsolete obsolete @@ -7923,7 +7915,7 @@ Element 3 Přesun - + obsolete obsolete @@ -7933,7 +7925,7 @@ Element 3 Seznam dílů - + obsolete obsolete @@ -7943,7 +7935,7 @@ Element 3 Otisky - + obsolete obsolete @@ -7953,7 +7945,7 @@ Element 3 Kategorie - + obsolete obsolete @@ -7963,7 +7955,7 @@ Element 3 Dodavatelé - + obsolete obsolete @@ -7973,7 +7965,7 @@ Element 3 Výrobce - + obsolete obsolete @@ -7983,7 +7975,7 @@ Element 3 Projekty - + obsolete obsolete @@ -7993,7 +7985,7 @@ Element 3 Typy příloh - + obsolete obsolete @@ -8003,7 +7995,7 @@ Element 3 Import - + obsolete obsolete @@ -8013,7 +8005,7 @@ Element 3 Štítky - + obsolete obsolete @@ -8023,7 +8015,7 @@ Element 3 Kalkulačka odporu - + obsolete obsolete @@ -8033,7 +8025,7 @@ Element 3 Otisky - + obsolete obsolete @@ -8043,7 +8035,7 @@ Element 3 Loga IC - + obsolete obsolete @@ -8053,7 +8045,7 @@ Element 3 Statistiky - + obsolete obsolete @@ -8063,7 +8055,7 @@ Element 3 Oprávnění k úpravám - + obsolete obsolete @@ -8073,7 +8065,7 @@ Element 3 Upravit uživatelské jméno - + obsolete obsolete @@ -8083,7 +8075,7 @@ Element 3 Změna skupiny - + obsolete obsolete @@ -8093,7 +8085,7 @@ Element 3 Upravit informace - + obsolete obsolete @@ -8103,7 +8095,7 @@ Element 3 Upravit oprávnění - + obsolete obsolete @@ -8113,7 +8105,7 @@ Element 3 Nastavit heslo - + obsolete obsolete @@ -8123,7 +8115,7 @@ Element 3 Změna uživatelských nastavení - + obsolete obsolete @@ -8133,7 +8125,7 @@ Element 3 Zobrazit stav - + obsolete obsolete @@ -8143,7 +8135,7 @@ Element 3 Aktualizace databáze - + obsolete obsolete @@ -8153,7 +8145,7 @@ Element 3 Čtení parametrů databáze - + obsolete obsolete @@ -8163,7 +8155,7 @@ Element 3 Úprava parametrů databáze - + obsolete obsolete @@ -8173,7 +8165,7 @@ Element 3 Čtení konfigurace - + obsolete obsolete @@ -8183,7 +8175,7 @@ Element 3 Úprava konfigurace - + obsolete obsolete @@ -8193,7 +8185,7 @@ Element 3 Informace o serveru - + obsolete obsolete @@ -8203,7 +8195,7 @@ Element 3 Použití ladicích nástrojů - + obsolete obsolete @@ -8213,7 +8205,7 @@ Element 3 Zobrazit protokoly - + obsolete obsolete @@ -8223,7 +8215,7 @@ Element 3 Odstranit protokoly - + obsolete obsolete @@ -8233,7 +8225,7 @@ Element 3 Upravit informace - + obsolete obsolete @@ -8243,7 +8235,7 @@ Element 3 Upravit uživatelské jméno - + obsolete obsolete @@ -8253,7 +8245,7 @@ Element 3 Zobrazit oprávnění - + obsolete obsolete @@ -8263,7 +8255,7 @@ Element 3 Zobrazit vlastní záznamy v protokolu - + obsolete obsolete @@ -8273,7 +8265,7 @@ Element 3 Vytvořit štítky - + obsolete obsolete @@ -8283,7 +8275,7 @@ Element 3 Upravit možnosti - + obsolete obsolete @@ -8293,7 +8285,7 @@ Element 3 Odstranit profily - + obsolete obsolete @@ -8303,7 +8295,7 @@ Element 3 Upravit profily - + obsolete obsolete @@ -8313,7 +8305,7 @@ Element 3 Nástroje - + obsolete obsolete @@ -8323,7 +8315,7 @@ Element 3 Skupiny - + obsolete obsolete @@ -8333,7 +8325,7 @@ Element 3 Uživatelé - + obsolete obsolete @@ -8343,7 +8335,7 @@ Element 3 Databáze - + obsolete obsolete @@ -8353,7 +8345,7 @@ Element 3 Konfigurace - + obsolete obsolete @@ -8363,7 +8355,7 @@ Element 3 Systém - + obsolete obsolete @@ -8373,7 +8365,7 @@ Element 3 Vlastní uživatel - + obsolete obsolete @@ -8383,7 +8375,7 @@ Element 3 Štítky - + obsolete obsolete @@ -8393,7 +8385,7 @@ Element 3 Kategorie - + obsolete obsolete @@ -8403,7 +8395,7 @@ Element 3 Min. množství - + obsolete obsolete @@ -8413,7 +8405,7 @@ Element 3 Otisk - + obsolete obsolete @@ -8423,7 +8415,7 @@ Element 3 MPN - + obsolete obsolete @@ -8433,7 +8425,7 @@ Element 3 Stav výroby - + obsolete obsolete @@ -8443,7 +8435,7 @@ Element 3 Štítky - + obsolete obsolete @@ -8453,7 +8445,7 @@ Element 3 Jednotka dílu - + obsolete obsolete @@ -8463,7 +8455,7 @@ Element 3 Hmotnost - + obsolete obsolete @@ -8473,7 +8465,7 @@ Element 3 Množství dílů - + obsolete obsolete @@ -8483,7 +8475,7 @@ Element 3 Zobrazit posledního upravujícího uživatele - + obsolete obsolete @@ -8493,7 +8485,7 @@ Element 3 Měny - + obsolete obsolete @@ -8503,7 +8495,7 @@ Element 3 Měrná jednotka - + obsolete obsolete @@ -8513,7 +8505,7 @@ Element 3 Původní heslo - + obsolete obsolete @@ -8523,7 +8515,7 @@ Element 3 Obnovit heslo - + obsolete obsolete @@ -8533,7 +8525,7 @@ Element 3 Bezpečnostní klíč (U2F) - + obsolete obsolete @@ -8543,13 +8535,13 @@ Element 3 Google - + tfa.provider.webauthn_two_factor_provider Bezpečnostní klíč - + obsolete obsolete @@ -8559,7 +8551,7 @@ Element 3 Authenticator app - + obsolete obsolete @@ -8569,7 +8561,7 @@ Element 3 Přihlášení bylo úspěšné - + obsolete obsolete @@ -8579,7 +8571,7 @@ Element 3 Neošetřená výjimka (zastaralé) - + obsolete obsolete @@ -8589,7 +8581,7 @@ Element 3 Přihlášení uživatele - + obsolete obsolete @@ -8599,7 +8591,7 @@ Element 3 Odhlášení uživatele - + obsolete obsolete @@ -8609,7 +8601,7 @@ Element 3 Neznámý - + obsolete obsolete @@ -8619,7 +8611,7 @@ Element 3 Prvek vytvořen - + obsolete obsolete @@ -8629,7 +8621,7 @@ Element 3 Prvek upraven - + obsolete obsolete @@ -8639,7 +8631,7 @@ Element 3 Prvek smazán - + obsolete obsolete @@ -8649,7 +8641,7 @@ Element 3 Databáze aktualizována - + obsolete @@ -8658,7 +8650,7 @@ Element 3 Vrátit prvek - + obsolete @@ -8667,7 +8659,7 @@ Element 3 Zobrazit historii - + obsolete @@ -8676,7 +8668,7 @@ Element 3 Zobrazit poslední aktivitu - + obsolete @@ -8685,7 +8677,7 @@ Element 3 Zobrazit staré verze prvků (cestování v čase) - + obsolete @@ -8694,7 +8686,7 @@ Element 3 Bezpečnostní klíč byl úspěšně přidán. - + obsolete @@ -8703,7 +8695,7 @@ Element 3 Uživatelské jméno - + obsolete @@ -8712,7 +8704,7 @@ Element 3 Aplikace Authenticator je vypnutá - + obsolete @@ -8721,7 +8713,7 @@ Element 3 Bezpečnostní klíč odstraněn - + obsolete @@ -8730,7 +8722,7 @@ Element 3 Bezpečnostní klíč přidán - + obsolete @@ -8739,7 +8731,7 @@ Element 3 Přegenerování záložních klíčů - + obsolete @@ -8748,7 +8740,7 @@ Element 3 Aplikace Authenticator je zapnutá - + obsolete @@ -8757,7 +8749,7 @@ Element 3 Heslo bylo změněno - + obsolete @@ -8766,7 +8758,7 @@ Element 3 Důvěryhodná zařízení resetována - + obsolete @@ -8775,7 +8767,7 @@ Element 3 Vymazaný prvek sbírky - + obsolete @@ -8784,7 +8776,7 @@ Element 3 Obnovení hesla - + obsolete @@ -8793,7 +8785,7 @@ Element 3 Obnovení dvoufaktorového nastavení správcem - + obsolete @@ -8802,7 +8794,7 @@ Element 3 Neoprávněný pokus o přístup - + obsolete @@ -8811,7 +8803,7 @@ Element 3 Úspěšné - + obsolete @@ -8820,7 +8812,7 @@ Element 3 2D - + obsolete @@ -8829,7 +8821,7 @@ Element 3 1D - + obsolete @@ -8838,7 +8830,7 @@ Element 3 Parametry - + obsolete @@ -8847,7 +8839,7 @@ Element 3 Zobrazit soukromé přílohy - + obsolete @@ -8856,7 +8848,7 @@ Element 3 Čtečka štítků - + obsolete @@ -8865,7 +8857,7 @@ Element 3 Přečíst profily - + obsolete @@ -8874,7 +8866,7 @@ Element 3 Vytvořit profily - + obsolete @@ -8883,2545 +8875,2545 @@ Element 3 Použití režimu Twig - + label_profile.showInDropdown Zobrazit v rychlém výběru - + group.edit.enforce_2fa Vynucení dvoufaktorového ověřování (2FA) - + group.edit.enforce_2fa.help Pokud je tato možnost povolena, musí každý přímý člen této skupiny nakonfigurovat alespoň jeden druhý faktor pro ověření. Doporučeno pro skupiny správců s velkým počtem oprávnění. - + selectpicker.empty Nic není vybráno - + selectpicker.nothing_selected Nic není vybráno - + entity.delete.must_not_contain_parts Element "%PATH%" stále obsahuje díly! Abyste mohli tento prvek odstranit, musíte díly přesunout. - + entity.delete.must_not_contain_attachments Typ přílohy stále obsahuje přílohy. Změňte jejich typ, abyste mohli tento typ přílohy odstranit. - + entity.delete.must_not_contain_prices Měna stále obsahuje údaje o ceně. Abyste mohli tento prvek odstranit, musíte změnit jejich měnu. - + entity.delete.must_not_contain_users Uživatelé stále používají tuto skupinu! Změňte jejich skupinu, abyste ji mohli smazat. - + part.table.edit Upravit - + part.table.edit.title Upravit díl - + part_list.action.action.title Zvolte akci - + part_list.action.action.group.favorite Oblíbený stav - + part_list.action.action.favorite Oblíbený - + part_list.action.action.unfavorite Neoblíbený - + part_list.action.action.group.change_field Změnit pole - + part_list.action.action.change_category Změnit kategorii - + part_list.action.action.change_footprint Změnit otisk - + part_list.action.action.change_manufacturer Změnit výrobce - + part_list.action.action.change_unit Změna jednotky dílu - + part_list.action.action.delete Smazat - + part_list.action.submit Odeslat - + part_list.action.part_count %count% dílů vybráno! - + company.edit.quick.website Otevřít webové stránky - + company.edit.quick.email Odeslat e-mail - + company.edit.quick.phone Telefonní hovor - + company.edit.quick.fax Odeslat fax - + company.fax_number.placeholder např. +420 123 456 789 - + part.edit.save_and_clone Uložit a duplikovat - + validator.file_ext_not_allowed Přípona souboru není pro tento typ přílohy povolena. - + tools.reel_calc.title Kalkulačka SMD kotoučů - + tools.reel_calc.inner_dia Vnitřní průměr - + tools.reel_calc.outer_dia Vnější průměr - + tools.reel_calc.tape_thick Tloušťka pásky - + tools.reel_calc.part_distance Vzdálenost mezi díly - + tools.reel_calc.update Aktualizace - + tools.reel_calc.parts_per_meter Díly na metr - + tools.reel_calc.result_length Délka pásky - + tools.reel_calc.result_amount Přibližný počet dílů - + tools.reel_calc.outer_greater_inner_error Chyba: Vnější průměr musí být větší než vnitřní průměr! - + tools.reel_calc.missing_values.error Vyplňte všechny hodnoty! - + tools.reel_calc.load_preset Načíst předvolbu - + tools.reel_calc.explanation Tato kalkulačka vám umožní odhadnout, kolik dílů zbývá na kotouči SMD. Změřte zaznamenané rozměry na cívce (nebo použijte některou z přednastavených hodnot) a kliknutím na tlačítko "Aktualizovat" získáte výsledek. - + perm.tools.reel_calculator Kalkulačka SMD kotoučů - + tree.tools.tools.reel_calculator Kalkulačka SMD kotoučů - + user.pw_change_needed.flash Vaše heslo je třeba změnit! Nastavte prosím nové heslo. - + tree.root_node.text Kořenový uzel - + part_list.action.select_null Prázdný prvek - + part_list.action.delete-title Opravdu chcete tyto díly odstranit? - + part_list.action.delete-message Tyto díly a všechny související informace (např. přílohy, informace o ceně atd.) budou odstraněny. Toto nelze vrátit zpět! - + part.table.actions.success Akce byly úspěšně dokončeny. - + attachment.edit.delete.confirm Opravdu chcete tuto přílohu smazat? - + filter.text_constraint.value.operator.EQ Is - + filter.text_constraint.value.operator.NEQ Není - + filter.text_constraint.value.operator.STARTS Začíná na - + filter.text_constraint.value.operator.CONTAINS Obsahuje - + filter.text_constraint.value.operator.ENDS Končí - + filter.text_constraint.value.operator.LIKE LIKE vzor - + filter.text_constraint.value.operator.REGEX Regulární výraz - + filter.number_constraint.value.operator.BETWEEN Mezi - + filter.number_constraint.AND a - + filter.entity_constraint.operator.EQ Je (kromě podřízených) - + filter.entity_constraint.operator.NEQ Není (s výjimkou podřízených) - + filter.entity_constraint.operator.INCLUDING_CHILDREN Je (včetně podřízených) - + filter.entity_constraint.operator.EXCLUDING_CHILDREN Není (s výjimkou podřízených) - + part.filter.dbId Databáze ID - + filter.tags_constraint.operator.ANY Kterákoli ze značek - + filter.tags_constraint.operator.ALL Všechny značky - + filter.tags_constraint.operator.NONE Žádná ze značek - + part.filter.lot_count Počet inventářů - + part.filter.attachments_count Počet příloh - + part.filter.orderdetails_count Počet údajů k objednávce - + part.filter.lotExpirationDate Datum ukončení platnosti inventáře - + part.filter.lotNeedsRefill Jakýkoliv inventář potřebuje doplnit - + part.filter.lotUnknwonAmount Jakýkoliv inventář má neznámé množství - + part.filter.attachmentName Název přílohy - + filter.choice_constraint.operator.ANY Kterýkoli z - + filter.choice_constraint.operator.NONE Žádný z - + part.filter.amount_sum Celkové množství - + filter.submit Aktualizovat - + filter.discard Zrušit změny - + filter.clear_filters Vymazat všechny filtry - + filter.title Filtr - + filter.parameter_value_constraint.operator.= Typ. Hodnota = - + filter.parameter_value_constraint.operator.!= Typ. Hodnota != - + filter.parameter_value_constraint.operator.< Typ. Hodnota < - + filter.parameter_value_constraint.operator.> Typ. Hodnota > - + filter.parameter_value_constraint.operator.<= Typ. Hodnota <= - + filter.parameter_value_constraint.operator.>= Typ. Hodnota >= - + filter.parameter_value_constraint.operator.BETWEEN Typ. Hodnota je mezi - + filter.parameter_value_constraint.operator.IN_RANGE V rozsahu hodnot - + filter.parameter_value_constraint.operator.NOT_IN_RANGE Není v rozsahu hodnot - + filter.parameter_value_constraint.operator.GREATER_THAN_RANGE Větší než rozsah hodnot - + filter.parameter_value_constraint.operator.GREATER_EQUAL_RANGE Větší rovná se než rozsah hodnot - + filter.parameter_value_constraint.operator.LESS_THAN_RANGE Méně než rozsah hodnot - + filter.parameter_value_constraint.operator.LESS_EQUAL_RANGE Méně rovné než rozsah hodnot - + filter.parameter_value_constraint.operator.RANGE_IN_RANGE Rozsah je zcela v rozsahu hodnot - + filter.parameter_value_constraint.operator.RANGE_INTERSECT_RANGE Rozsah protíná rozsah hodnot - + filter.text_constraint.value Žádná nastavená hodnota - + filter.number_constraint.value1 Žádná nastavená hodnota - + filter.number_constraint.value2 Maximální hodnota - + filter.datetime_constraint.value1 Není nastaven žádný datum - + filter.datetime_constraint.value2 Maximální datum - + filter.constraint.add Přidat filtr - + part.filter.parameters_count Počet parametrů - + part.filter.lotDescription Popis inventáře - + parts_list.search.searching_for Hledání dílů pomocí klíčového slova <b>%keyword%</b> - + parts_list.search_options.caption Povolené možnosti hledání - + attachment.table.element_type Přidružený typ prvku - + log.level.debug Ladění - + log.level.info Info - + log.level.notice Oznámení - + log.level.warning Varování - + log.level.error Chyba - + log.level.critical Kritické - + log.level.alert Upozornění - + log.level.emergency Nouzové - + log.type.security Událost související s bezpečností - + log.type.instock_changed [LEGACY] Instock changed - + log.target_id ID cílového prvku - + entity.info.parts_count_recursive Počet dílů s tímto prvkem nebo jeho souvisejícími prvky - + tools.server_infos.title Informace o serveru - + permission.preset.read_only Pouze pro čtení - + permission.preset.read_only.desc Povolit pouze operace čtení dat - + permission.preset.all_inherit Převzít vše - + permission.preset.all_inherit.desc Nastavení všech oprávnění na Převzít - + permission.preset.all_forbid Zakázat všechny - + permission.preset.all_forbid.desc Nastavit všechna oprávnění na hodnotu Zakázat - + permission.preset.all_allow Povolit všechny - + permission.preset.all_allow.desc Nastavit všechny oprávnění na možnost povolit - + perm.server_infos Informace o serveru - + permission.preset.editor Editor - + permission.preset.editor.desc Umožňuje měnit díly a datové struktury - + permission.preset.admin Admin - + permission.preset.admin.desc Povolení administrativních úkonů - + permission.preset.button Použít předvolbu - + perm.attachments.show_private Zobrazit soukromé přílohy - + perm.attachments.list_attachments Zobrazit seznam všech příloh - + user.edit.permission_success Předvolba oprávnění byla úspěšně použita. Zkontrolujte, zda nová oprávnění vyhovují vašim potřebám. - + perm.group.data Data - + part_list.action.action.group.needs_review Potřeba revize - + part_list.action.action.set_needs_review Nastavit stav "Potřebuje kontrolu" - + part_list.action.action.unset_needs_review Zrušit stav "Potřebuje kontrolu" - + part.edit.ipn Interní číslo dílu (IPN) - + part.ipn.not_defined Není definováno - + part.table.ipn IPN - + currency.edit.update_rate Získat směnný kurz - + currency.edit.exchange_rate_update.unsupported_currency Měna není podporována zdrojem směnných kurzů. Zkontrolujte konfiguraci zdroje směnných kurzů. - + currency.edit.exchange_rate_update.generic_error Nelze načíst směnný kurz. Zkontrolujte konfiguraci zdroje směnných kurzů. - + currency.edit.exchange_rate_updated.success Úspěšně načtený směnný kurz. - + project.bom.quantity Množství BOM. - + project.bom.mountnames Názvy sestav - + project.bom.name Název - + project.bom.comment Poznámky - + project.bom.part Díl - + project.bom.add_entry Přidat položku - + part_list.action.group.projects Projekty - + part_list.action.projects.add_to_project Přidat díl do projektu - + project.bom.delete.confirm Opravdu chcete tuto položku BOM odstranit? - + project.add_parts_to_project Přidání dílů do BOM projektu - + part.info.add_part_to_project Přidat tento díl do projektu - + project_bom_entry.label Položka BOM - + project.edit.status Stav projektu - + project.status.draft Návrh - + project.status.planning Plánování - + project.status.in_production Ve výrobě - + project.status.finished Dokončeno - + project.status.archived Archivováno - + part.new_build_part.error.build_part_already_exists Projekt již má stavební díl! - + project.edit.associated_build_part Přidružené sestavy dílu - + project.edit.associated_build_part.add Přidat díl sestavy - + project.edit.associated_build.hint Tento díl představuje sestavu tohoto projektu, která jsou někde uložena. - + part.info.projectBuildPart.hint Tento díl představuje sestavy následujícího projektu a je s ním spojena. - + part.is_build_part Je součástí projektu - + project.info.title Informace o projektu - + project.info.bom_entries_count Položky BOM - + project.info.sub_projects_count Podprojekty - + project.info.bom_add_parts Přidat položku BOM - + project.info.info.label Info - + project.info.sub_projects.label Podprojekty - + project.bom.price Cena - + part.info.withdraw_modal.title.withdraw Odebrání dílů z inventáře - + part.info.withdraw_modal.title.add Přidání dílů do inventáře - + part.info.withdraw_modal.title.move Přesun dílů z inventáře do jiného - + part.info.withdraw_modal.amount Množství - + part.info.withdraw_modal.move_to Přesun do - + part.info.withdraw_modal.comment Komentář - + part.info.withdraw_modal.comment.hint Zde můžete zadat komentář, ve kterém popíšete, proč tuto operaci provádíte (např. k čemu díly potřebujete). Tato informace se uloží do protokolu. - + modal.close Zavřít - + modal.submit Odeslat - + part.withdraw.success Úspěšně přidané/přesunuté/odebrané díly. - + perm.parts_stock Zásoba dílů - + perm.parts_stock.withdraw Vyskladnění dílů ze skladu - + perm.parts_stock.add Přidání dílů na sklad - + perm.parts_stock.move Přesun dílů mezi inventáři - + user.permissions_schema_updated Schéma oprávnění vašeho uživatele bylo aktualizováno na nejnovější verzi. - + log.type.part_stock_changed Změna skladové zásoby dílu - + log.part_stock_changed.withdraw Odebrání zásob - + log.part_stock_changed.add Přidání zásob - + log.part_stock_changed.move Přesun zásob - + log.part_stock_changed.comment Komentář - + log.part_stock_changed.change Změna - + log.part_stock_changed.move_target Přesun cíle - + tools.builtin_footprints_viewer.title Vestavěná galerie obrázků - + tools.builtin_footprints_viewer.hint V této galerii jsou uvedeny všechny dostupné vestavěné obrázky otisků. Pokud je chcete použít v příloze, zadejte název (nebo klíčové slovo) do pole cesta k příloze a vyberte obrázek z rozbalovacího seznamu. - + tools.ic_logos.title Loga IC - + part_list.action.group.labels Štítky - + part_list.action.projects.generate_label Generování štítků (pro díly) - + part_list.action.projects.generate_label_lot Generování štítků (pro inventáře dílů) - + part_list.action.generate_label.empty Prázdný štítek - + project.info.builds.label Sestavit - + project.builds.build_not_possible Sestavení není možné: Díly nejsou skladem - + project.builds.following_bom_entries_miss_instock Následující díly nejsou dostatečně skladem, aby bylo možné tento projekt alespoň jednou sestavit: - + project.builds.stocked k dispozici - + project.builds.needed potřebné - + project.builds.build_possible Sestavení možné - + project.builds.number_of_builds_possible Máte dostatek zásob na sestavení <b>%max_builds%</b> sestavení tohoto projektu. - + project.builds.check_project_status Aktuální stav projektu je <b>"%project_status%"</b>. Měli byste zkontrolovat, zda chcete projekt s tímto stavem skutečně sestavit! - + project.builds.following_bom_entries_miss_instock_n Nemáte na skladě dostatek dílů pro sestavení tohoto projektu %number_of_builds% times. Následující díly chybí na skladě: - + project.build.flash.invalid_input Nelze sestavit projekt. Zkontrolujte zadání! - + project.build.required_qty Požadované množství - + project.build.btn_build Sestavit - + project.build.help Zvolte, z jakých zásob (a v jakém množství) mají být odebrány komponenty potřebné pro sestavení. Zaškrtněte políčko u každé položky BOM, když jste komponenty odebrali, nebo pomocí horního zaškrtávacího políčka zaškrtněte všechna políčka najednou. - + project.build.buildsPartLot.new_lot Vytvořit nový inventář - + project.build.add_builds_to_builds_part Přidání sestavení do části sestavení projektu - + project.build.builds_part_lot Cílový inventář - + project.builds.number_of_builds Množství sestavy - + project.builds.no_stocked_builds Počet skladovaných sestav - + user.change_avatar.label Změna profilového obrázku - + user_settings.change_avatar.label Změna profilového obrázku - + user_settings.remove_avatar.label Odstranit profilový obrázek - + part.edit.name.category_hint Nápověda z kategorie - + category.edit.partname_regex.placeholder např. "/Kondenzátor \d+ nF/i" - + category.edit.partname_regex.help Regulární výraz kompatibilní s PCRE, kterému musí název dílu odpovídat. - + entity.select.add_hint Použijte -> pro vytvoření vnořených struktur, např. "Node 1->Node 1.1". - + entity.select.group.new_not_added_to_DB Nový (zatím nebyl přidán do DB) - + part.edit.save_and_new Uložit a přidat nový prázdný díl - + homepage.first_steps.title První kroky - + homepage.first_steps.introduction Vaše databáze je stále prázdná. Možná byste si měli přečíst <a href="%url%">dokumentaci</a> nebo začít vytvářet následující datové struktury: - + homepage.first_steps.create_part Nebo můžete přímo <a href="%url%">přidat nový díl</a>. - + homepage.first_steps.hide_hint Toto pole se skryje, jakmile vytvoříte první díl. - + homepage.forum.text Pro dotazy týkající se Part-DB použijte <a href="%href%" class="link-external" target="_blank">diskusní fórum</a> - + log.element_edited.changed_fields.category Kategorie - + log.element_edited.changed_fields.footprint Otisk - + log.element_edited.changed_fields.manufacturer Výrobce - + log.element_edited.changed_fields.value_typical typ. hodnota - + log.element_edited.changed_fields.pw_reset_expires Obnovení hesla - + log.element_edited.changed_fields.comment Poznámky - + log.element_edited.changed_fields.supplierpartnr Číslo dílu dodavatele - + log.element_edited.changed_fields.supplier_product_url Odkaz na nabídku - + log.element_edited.changed_fields.price Cena - + log.element_edited.changed_fields.min_discount_quantity Minimální výše slevy - + log.element_edited.changed_fields.original_filename Původní název souboru - + log.element_edited.changed_fields.path Cesta k souboru - + log.element_edited.changed_fields.description Popis - + log.element_edited.changed_fields.manufacturing_status Stav výroby - + log.element_edited.changed_fields.options.barcode_type Typ čárového kódu - + log.element_edited.changed_fields.status Stav - + log.element_edited.changed_fields.quantity Množství BOM - + log.element_edited.changed_fields.mountnames Názvy sestav - + log.element_edited.changed_fields.name Název - + log.element_edited.changed_fields.part Díl - + log.element_edited.changed_fields.price_currency Měna ceny - + log.element_edited.changed_fields.partname_hint Nápověda k názvu dílu - + log.element_edited.changed_fields.partname_regex Filtr názvu - + log.element_edited.changed_fields.disable_footprints Zakázat otisky - + log.element_edited.changed_fields.disable_manufacturers Zakázat výrobce - + log.element_edited.changed_fields.disable_autodatasheets Zakázat automatické odkazy na katalogové listy - + log.element_edited.changed_fields.disable_properties Zakázat vlastnosti - + log.element_edited.changed_fields.default_description Výchozí popis - + log.element_edited.changed_fields.default_comment Výchozí poznámky - + log.element_edited.changed_fields.filetype_filter Povolené přípony souborů - + log.element_edited.changed_fields.not_selectable Nevybráno - + log.element_edited.changed_fields.parent Nadřazený prvek - + log.element_edited.changed_fields.shipping_costs Náklady na dopravu - + log.element_edited.changed_fields.default_currency Výchozí měna - + log.element_edited.changed_fields.address Adresa - + log.element_edited.changed_fields.phone_number Telefonní číslo - + log.element_edited.changed_fields.fax_number Číslo faxu - + log.element_edited.changed_fields.email_address e-mail - + log.element_edited.changed_fields.website Webové stránky - + log.element_edited.changed_fields.auto_product_url Produkt URL - + log.element_edited.changed_fields.is_full Umístění plné - + log.element_edited.changed_fields.limit_to_existing_parts Omezení na stávající díly - + log.element_edited.changed_fields.only_single_part Pouze jeden díl - + log.element_edited.changed_fields.storage_type Typ úložiště - + log.element_edited.changed_fields.footprint_3d 3D model - + log.element_edited.changed_fields.master_picture_attachment Náhled - + log.element_edited.changed_fields.exchange_rate Směnný kurz - + log.element_edited.changed_fields.iso_code Směnný kurz - + log.element_edited.changed_fields.unit Symbol jednotky - + log.element_edited.changed_fields.is_integer Je celé číslo - + log.element_edited.changed_fields.use_si_prefix Použít předponu SI - + log.element_edited.changed_fields.options.width Šířka - + log.element_edited.changed_fields.options.height Výška - + log.element_edited.changed_fields.options.supported_element Typ cíle - + log.element_edited.changed_fields.options.additional_css Další styly (CSS) - + log.element_edited.changed_fields.options.lines Obsah - + log.element_edited.changed_fields.permissions.data Oprávnění - + log.element_edited.changed_fields.disabled Zakázano - + log.element_edited.changed_fields.theme Téma - + log.element_edited.changed_fields.timezone Časové pásmo - + log.element_edited.changed_fields.language Jazyk - + log.element_edited.changed_fields.email e-mail - + log.element_edited.changed_fields.department Oddělení - + log.element_edited.changed_fields.last_name Příjmení - + log.element_edited.changed_fields.first_name Jméno - + log.element_edited.changed_fields.group Skupina - + log.element_edited.changed_fields.currency Preferovaná měna - + log.element_edited.changed_fields.enforce2FA Vynucení 2FA - + log.element_edited.changed_fields.symbol Symbol - + log.element_edited.changed_fields.value_min Min. hodnota - + log.element_edited.changed_fields.value_max Max. hodnota - + log.element_edited.changed_fields.value_text Hodnota textu - + log.element_edited.changed_fields.show_in_table Zobrazit v tabulce - + log.element_edited.changed_fields.attachment_type Zobrazit v tabulce - + log.element_edited.changed_fields.needs_review Potřeba revize - + log.element_edited.changed_fields.tags Štítky - + log.element_edited.changed_fields.mass Hmotnost - + log.element_edited.changed_fields.ipn IPN - + log.element_edited.changed_fields.favorite Oblíbené - + log.element_edited.changed_fields.minamount Minimální zásoba - + log.element_edited.changed_fields.manufacturer_product_url Odkaz na stránku produktu - + log.element_edited.changed_fields.manufacturer_product_number MPN - + log.element_edited.changed_fields.partUnit Měrná jednotka - + log.element_edited.changed_fields.expiration_date Datum vypršení platnosti - + log.element_edited.changed_fields.amount Množství - + log.element_edited.changed_fields.storage_location Umístění - + attachment.max_file_size Maximální velikost souboru - + user.saml_user SSO / SAML uživatel - + user.saml_user.pw_change_hint Uživatel používá jednotné přihlášení (SSO). Heslo a nastavení 2FA zde nelze změnit. Nakonfigurujte je raději u svého centrálního zdroje SSO! - + login.sso_saml_login Jednotné přihlášení (SSO) - + login.local_login_hint Níže uvedený formulář je určen pouze pro přihlášení pomocí místního uživatele. Pokud se chcete přihlásit prostřednictvím jednotného přihlášení, stiskněte tlačítko výše. - + part_list.action.action.export Export dílů - + part_list.action.export_json Export jako JSON - + part_list.action.export_csv Export jako CSV - + part_list.action.export_yaml Export jako YAML - + part_list.action.export_xml Export jako XML - + parts.import.title Import dílů - + parts.import.errors.title Porušení při dovozu - + parts.import.flash.error Chyby při importu. Příčinou jsou pravděpodobně některá neplatná data. - + parts.import.format.auto Automaticky (na základě přípony souboru) - + parts.import.flash.error.unknown_format Z daného souboru se nepodařilo určit formát! - + parts.import.flash.error.invalid_file Soubor je neplatný. Zkontrolujte, zda jste vybrali správný formát! - + parts.import.part_category.label Přepsání kategorie - + parts.import.part_category.help Pokud zde vyberete hodnotu, budou do této kategorie přiřazeny všechny importované díly. Bez ohledu na to, co bylo nastaveno v datech. - + import.create_unknown_datastructures Vytvořit neznámé datové struktury - + import.create_unknown_datastructures.help Pokud je tato možnost vybrána, budou automaticky vytvořeny datové struktury (jako jsou kategorie, otisky atd.), které v databázi ještě neexistují. Není-li tato možnost vybrána, budou použity pouze existující datové struktury, a pokud nebude nalezena žádná odpovídající datová struktura, nebude dílu přiřazeno nic. - + import.path_delimiter Oddělovač cesty - + import.path_delimiter.help Oddělovač používaný k označení různých úrovní v datových strukturách, jako je kategorie, otisk atd. - + parts.import.help_documentation Další informace o formátu souboru najdete v <a href="%link%">dokumentaci</a>. - + parts.import.help Pomocí tohoto nástroje můžete importovat díly z existujících souborů. Díly budou zapsány přímo do databáze, proto před nahráním souboru sem zkontrolujte, zda je jeho obsah správný. - + parts.import.flash.success Import dílu úspěšný! - + parts.import.errors.imported_entities Dovážené díly - + perm.import Import dat - + parts.import.part_needs_review.label Označit všechny importované díly jako "Potřeba zkontrolovat". - + parts.import.part_needs_review.help Pokud je tato možnost vybrána, budou všechny díly označeny jako "Potřeba revize" bez ohledu na to, co bylo nastaveno v údajích. - + project.bom_import.flash.success Import %count% položek BOM proběhl úspěšně. - + project.bom_import.type Typ - + project.bom_import.type.kicad_pcbnew KiCAD Pcbnew BOM (CSV soubor) - + project.bom_import.clear_existing_bom Vymazání stávajících položek BOM před importem - + project.bom_import.clear_existing_bom.help Výběrem této možnosti odstraníte všechny existující položky BOM v projektu a přepíšete je importovaným souborem BOM! - + project.bom_import.flash.invalid_file Soubor se nepodařilo importovat. Zkontrolujte, zda jste vybrali správný typ souboru. Chybové hlášení: Zprávy: %zprávy% - + project.bom_import.flash.invalid_entries Chyba ověření! Zkontrolujte prosím svá data! - + project.import_bom Import BOM do projektu - + project.edit.bom.import_bom Import BOM - + measurement_unit.new Nová měrná jednotka - + measurement_unit.edit Upravit měrnou jednotku - + user.aboutMe.label O mně - + storelocation.owner.label Majitel - + storelocation.part_owner_must_match.label Vlastník se musí shodovat s vlastníkem umístění - + part_lot.owner Majitel - + part_lot.owner.help Pouze vlastník může z tohoto skladu odebírat nebo přidávat díly. - + log.element_edited.changed_fields.owner Majitel - + log.element_edited.changed_fields.instock_unknown Množství neznámé - + log.element_edited.changed_fields.needs_refill Potřebné doplnění - + part.withdraw.access_denied Není povoleno provést požadovanou akci. Zkontrolujte prosím svá oprávnění a vlastníka souvisejících prvků. - + part.info.amount.less_than_desired Méně než je požadováno - + log.cli_user Uživatel CLI - + log.element_edited.changed_fields.part_owner_must_match Vlastník dílu se musí shodovat s vlastníkem umístění - + part.filter.lessThanDesired Méně než požadované množství (celkové množství < min. množství) - + part.filter.lotOwner Vlastník - + user.show_email_on_profile.label Zobrazit e-mail na veřejné stránce profilu - + log.details.title Podrobnosti záznamu - + log.user_login.login_from_ip Přihlášení z IP adresy - + log.user_login.ip_anonymize_hint - Pokud poslední číslice IP adresy chybí, je povolen režim GDPR, ve kterém jsou IP adresy anynomizovány. + Pokud poslední číslice IP adresy chybí, je povolen režim GPDR, ve kterém jsou IP adresy anynomizovány. - + log.user_not_allowed.unauthorized_access_attempt_to Neoprávněný pokus o přístup na stránku - + log.user_not_allowed.hint Žádost byla zablokována. Neměla by být vyžadována žádná akce. - + log.no_comment Bez komentáře - + log.element_changed.field Pole - + log.element_changed.data_before Údaje před změnou - + error_table.error Během vašeho požadavku došlo k chybě. - + part.table.invalid_regex Nesprávný regulární výraz (regex) - + log.element_changed.data_after Údaje po změně - + log.element_changed.diff Rozdíl - + log.undo.undo.short Zrušit - + log.undo.revert.short Návrat k tomuto časovému razítku - + log.view_version Zobrazit verzi - + log.undo.undelete.short Odstranit - + log.element_edited.changed_fields.id ID - + log.element_edited.changed_fields.id_owner Majitel - + log.element_edited.changed_fields.parent_id Nadřazený - + log.details.delete_entry Odstranit záznam protokolu - + log.delete.message.title Opravdu chcete odstranit záznam protokolu? - + log.delete.message Pokud se jedná o položku historie prvků, dojde k přerušení historie prvků! To může vést k neočekávaným výsledkům při použití funkce cestování v čase. - + log.collection_deleted.on_collection o sbírce - + log.element_edited.changed_fields.attachments Přílohy - + tfa_u2f.add_key.registration_error Při registraci bezpečnostního klíče došlo k chybě. Zkuste to znovu nebo použijte jiný bezpečnostní klíč! - + log.target_type.none Žádné - + ui.darkmode.light Světlý - + ui.darkmode.dark Tmavý - + ui.darkmode.auto Automaticky (podle nastavení systému) - + label_generator.no_lines_given Není uveden žádný textový obsah! Popisky zůstanou prázdné. - + user.password_strength.very_weak Velmi slabé - + user.password_strength.weak Slabé - + user.password_strength.medium Střední - + user.password_strength.strong Silné - + user.password_strength.very_strong Velmi silné - + perm.users.impersonate Vydávat se za jiné uživatele - + user.impersonated_by.label Vydává se za - + user.stop_impersonation Zastavit vydávání se za někoho jiného - + user.impersonate.btn Vydávat se za - + user.impersonate.confirm.title Opravdu se chcete vydávat za tohoto uživatele? - + user.impersonate.confirm.message Tato skutečnost bude zaznamenána. Měli byste to dělat pouze z dobrého důvodu. @@ -11429,785 +11421,1638 @@ Element 3 Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakázaným přístupem. Pokud se o to pokusíte, zobrazí se zpráva "Přístup odepřen". - + log.type.security.user_impersonated Vydávaný uživatel - + info_providers.providers_list.title Poskytovatelé informací - + info_providers.providers_list.active Aktivní - + info_providers.providers_list.disabled Zakázané - + info_providers.capabilities.basic Základní - + info_providers.capabilities.footprint Otisk - + info_providers.capabilities.picture Obrázek - + info_providers.capabilities.datasheet Datové listy - + info_providers.capabilities.price Ceny - + part.info_provider_reference.badge Zdroj informací použitý k vytvoření tohoto dílu. - + part.info_provider_reference Vytvořeno zdrojem informací - + oauth_client.connect.btn Připojení OAuth - + info_providers.table.provider.label Poskytovatel - + info_providers.search.keyword Klíčové slovo - + info_providers.search.submit Hledat - + info_providers.search.providers.help Vyberte zdroje, ve kterých se má vyhledávat. - + info_providers.search.providers Zdroje - + info_providers.search.info_providers_list Zobrazit všechny dostupné zdroje informací - + info_providers.search.title Vytvořit díly ze zdroje informací - + oauth_client.flash.connection_successful Úspěšné připojení k aplikaci OAuth! - + perm.part.info_providers Poskytovatelé informací - + perm.part.info_providers.create_parts Vytvořit díly ze zdroje informací - + entity.edit.alternative_names.label Alternativní názvy - + entity.edit.alternative_names.help Zde uvedené alternativní názvy se používají k vyhledání tohoto prvku na základě výsledků poskytovatelů informací. - + info_providers.form.help_prefix Zdroj - + update_manager.new_version_available.title K dispozici je nová verze - + update_manager.new_version_available.text K dispozici je nová verze Part-DB. Podívejte se na ni zde - + update_manager.new_version_available.only_administrators_can_see Tuto zprávu mohou vidět pouze správci. - + perm.system.show_available_updates Zobrazit dostupné aktualizace Part-DB - + user.settings.api_tokens API tokeny - + user.settings.api_tokens.description Pomocí tokenu API mohou jiné aplikace přistupovat k Part-DB s vašimi uživatelskými právy a provádět různé akce pomocí rozhraní Part-DB REST API. Pokud zde token API odstraníte, aplikace, která token používá, již nebude moci vaším jménem přistupovat k Part-DB. - + api_tokens.name Název - + api_tokens.access_level Úroveň přístupu - + api_tokens.expiration_date Datum vypršení platnosti - + api_tokens.added_date Přidáno v - + api_tokens.last_time_used Naposledy použité - + datetime.never Nikdy - + api_token.valid Platný - + api_token.expired Vypršela platnost - + user.settings.show_api_documentation Zobrazit dokumentaci API - + api_token.create_new Vytvořit nový token API - + api_token.level.read_only Pouze pro čtení - + api_token.level.edit Upravit - + api_token.level.admin Admin - + api_token.level.full Úplný - + api_tokens.access_level.help Můžete omezit, k čemu má token API přístup. Přístup je vždy omezen oprávněním uživatele. - + api_tokens.expiration_date.help Po tomto datu již není token použitelný. Pokud token nemá nikdy vypršet, ponechte prázdné pole. - + api_tokens.your_token_is Token API je - + api_tokens.please_save_it Prosím, uložte si ji. Nebudete ji moci znovu vidět! - + api_tokens.create_new.back_to_user_settings Zpět na uživatelská nastavení - + project.build.dont_check_quantity Nekontrolovat množství - + project.build.dont_check_quantity.help Pokud je tato možnost vybrána, použijí se zadaná stažená množství bez ohledu na to, zda je k sestavení projektu skutečně zapotřebí více nebo méně dílů. - + part_list.action.invert_selection Inverzní výběr - + perm.api API - + perm.api.access_api Přístup k API - + perm.api.manage_tokens Správa tokenů API - + user.settings.api_tokens.delete.title Opravdu chcete tento token API odstranit? - + user.settings.api_tokens.delete Smazat - + user.settings.api_tokens.delete.message Aplikace, která používá tento token API, již nebude mít přístup k Part-DB. Tuto akci nelze vzít zpět! - + api_tokens.deleted Token API byl úspěšně smazán! - + user.settings.api_tokens.no_api_tokens_yet Zatím nejsou nakonfigurovány žádné tokeny API. - + api_token.ends_with Končí - + entity.select.creating_new_entities_not_allowed Není dovoleno vytvářet nové entity tohoto typu! Vyberte si prosím již existující subjekt. - + scan_dialog.mode Typ čárového kódu - + scan_dialog.mode.auto Automatická detekce - + scan_dialog.mode.ipn Čárový kód IPN - + scan_dialog.mode.internal Čárový kód Part-DB - + part_association.label Spojení dílu - + part.edit.tab.associations Související díly - + part_association.edit.other_part Související díl - + part_association.edit.type Typ vztahu - + part_association.edit.comment Poznámky - + part_association.edit.type.help Zde můžete vybrat, jak vybraný díl souvisí s tímto dílem. - + part_association.table.from_this_part Přidružení tohoto dílu k ostatním - + part_association.table.from Z - + part_association.table.type Vztah - + part_association.table.to Do - + part_association.type.compatible Je kompatibilní s - + part_association.table.to_this_part Přidružení k tomuto dílu od ostatních - + part_association.type.other Ostatní (vlastní hodnota) - + part_association.type.supersedes Nahrazuje - + part_association.edit.other_type Vlastní typ - + part_association.edit.delete.confirm Opravdu chcete toto přidružení smazat? To nelze vrátit zpět. - + part_lot.edit.advanced Rozbalit pokročilé možnosti - + part_lot.edit.vendor_barcode Čárový kód dodavatele - + part_lot.edit.vendor_barcode.help Pokud již tento inventář má čárový kód (např. vložený prodejcem), můžete zde zadat jeho obsah, abyste jej mohli snadno naskenovat. - + scan_dialog.mode.vendor Čárový kód prodejce (nakonfigurovaný v inventáři) - + project.bom.instockAmount Množství zásob - + collection_type.new_element.tooltip Tento prvek byl nově vytvořen a dosud není uložen v databázi. - + part.merge.title Sloučit díl - + part.merge.title.into na - + part.merge.confirm.title Opravdu chcete sloučit <b>%other%</b> do <b>%target%</b>? - + part.merge.confirm.message <b>%other%</b> bude odstraněn a díl bude uložen se zobrazenými informacemi. - + part.info.merge_modal.title Sloučení dílů - + part.info.merge_modal.other_part Jiné díly - + part.info.merge_modal.other_into_this Sloučení jiný díl do tohoto (smazání jiného dílu, ponechání tohoto) - + part.info.merge_modal.this_into_other Sloučit tento díl do jiného (tento díl smazat, jiný ponechat) - + part.info.merge_btn Sloučit díl - + part.update_part_from_info_provider.btn Aktualizovat díl ze zdroje informací - + info_providers.update_part.title Aktualizace stávajícího dílu ze zdroje informací - + part.merge.flash.please_review Data zatím nebyla uložena. Zkontrolujte změny a kliknutím na tlačítko uložit nová data zachovejte. - + user.edit.flash.permissions_fixed Chyběla oprávnění vyžadovaná jinými oprávněními. To bylo opraveno. Zkontrolujte prosím, zda jsou oprávnění v souladu s vaším záměrem. - + permission.legend.dependency_note Vezměte prosím na vědomí, že některé operace povolení jsou na sobě závislé. Pokud se setkáte s varováním, že chybějící oprávnění byla opravena a oprávnění bylo znovu nastaveno na povolit, musíte nastavit i závislou operaci na zakázat. Závislé operace lze obvykle nalézt napravo od operace. - + log.part_stock_changed.timestamp Časové razítko - + part.info.withdraw_modal.timestamp Časové razítko akce - + part.info.withdraw_modal.timestamp.hint Toto pole umožňuje zadat skutečné datum, kdy byla skladová operace skutečně provedena, a ne pouze kdy byla zaznamenána. Tato hodnota je uložena v extra poli záznamu protokolu. - + part.info.withdraw_modal.delete_lot_if_empty Vymazat tento inventář, až se vyprázdní - + info_providers.search.error.client_exception Při komunikaci se zdrojem informací došlo k chybě. Zkontrolujte konfiguraci tohoto zdroje a pokud možno obnovte tokeny OAuth. - + eda_info.reference_prefix.placeholder např. R - + eda_info.reference_prefix Referenční předpona - + eda_info.kicad_section.title KiCad specifické nastavení - + eda_info.value Hodnota - + eda_info.value.placeholder např. 100n - + eda_info.exclude_from_bom Vyloučit díl z BOM - + eda_info.exclude_from_board Vyloučit díl z desky plošných spojů - + eda_info.exclude_from_sim Vyloučit díl ze simulace - + eda_info.kicad_symbol Symbol schématu KiCad - + eda_info.kicad_symbol.placeholder např. Transistor_BJT:BC547 - + eda_info.kicad_footprint KiCad otisk - + eda_info.kicad_footprint.placeholder např. Package_TO_SOT_THT:TO-92 - + part.edit.tab.eda Informace EDA - + api.api_endpoints.title Koncové body API - + api.api_endpoints.partdb Part-DB API - + api.api_endpoints.kicad_root_url KiCad API root URL - + eda_info.visibility Viditelné - + eda_info.visibility.help Ve výchozím nastavení je viditelnost pro software EDA určena automaticky. Pomocí tohoto zaškrtávacího políčka můžete vynutit, aby byl díl viditelný nebo neviditelný. - + part.withdraw.zero_amount Pokusili jste se vybrat/přidat nulové množství! Nebyla provedena žádná akce. - + login.flash.access_denied_please_login Přístup odepřen! Pro pokračování se prosím přihlaste. - + attachment.upload_multiple_files Nahrát soubory - + entity.mass_creation_flash Úspěšně vytvořeno %COUNT% entit. + + + info_providers.search.number_of_results + %number% výsledků + + + + + info_providers.search.no_results + U vybraných poskytovatelů nebyly nalezeny žádné výsledky! Zkontrolujte hledaný výraz nebo zkuste vybrat další poskytovatele. + + + + + tfa.check.code.confirmation + Vygenerovaný kód + + + + + info_providers.search.show_existing_part + Zobrazit existující díl + + + + + info_providers.search.edit_existing_part + Upravit existující díl + + + + + info_providers.search.existing_part_found.short + Díl už existuje + + + + + info_providers.search.existing_part_found + Tento díl (nebo velmi podobná) již byl nalezen v databázi. Zkontrolujte, zda se jedná o stejný díl, a zda ho chcete vytvořit znovu! + + + + + info_providers.search.update_existing_part + Aktualizovat existující díl od poskytovatele informací + + + + + part.create_from_info_provider.no_category_yet + Kategorie nemohla být automaticky určena poskytovatelem informací. Zkontrolujte data a vyberte kategorii ručně. + + + + + part_lot.edit.user_barcode + Čárový kód uživatele + + + + + scan_dialog.mode.user + Uživatelem definovaný čárový kód (nakonfigurovaný v dávce dílů) + + + + + scan_dialog.mode.eigp + Čárový kód EIGP 114 (např. kódy datamatrix na objednávkách digikey a mouser) + + + + + scan_dialog.info_mode + Režim Info (dekódovat čárový kód a zobrazit jeho obsah, ale nepřesměrovat na díl) + + + + + label_scanner.decoded_info.title + Dekódované informace + + + + + label_generator.edit_profiles + Upravit profily + + + + + label_generator.profile_name_empty + Jméno profilu nesmí být prázdné! + + + + + label_generator.save_profile_name + Jméno profilu + + + + + label_generator.save_profile + Uložit jako nový profil + + + + + label_generator.profile_saved + Profil uložen! + + + + + settings.ips.element14 + Element 14 / Farnell + + + + + settings.ips.element14.apiKey + API klíč + + + + + settings.ips.element14.apiKey.help + API klíč si můžete zaregistrovat na adrese <a href="https://partner.element14.com/">https://partner.element14.com/</a>. + + + + + settings.ips.element14.storeId + Doména obchodu + + + + + settings.ips.element14.storeId.help + Doména obchodu, ze které se mají načíst data. Určuje jazyk a měnu výsledků. Seznam platných domén najdete <a href="https://partner.element14.com/docs/Product_Search_API_REST__Description">zde</a>. + + + + + settings.ips.tme + TME + + + + + settings.ips.tme.token + API token + + + + + settings.ips.tme.token.help + API token a tajný klíč můžete získat na adrese <a href="https://developers.tme.eu/en/">https://developers.tme.eu/en/</a>. + + + + + settings.ips.tme.secret + API tajný klíč + + + + + settings.ips.tme.currency + Měna + + + + + settings.ips.tme.language + Jazyk + + + + + settings.ips.tme.country + Země + + + + + settings.ips.tme.grossPrices + Zobrazit hrubé ceny (včetně daně) + + + + + settings.ips.mouser + Mouser + + + + + settings.ips.mouser.apiKey + API klíč + + + + + settings.ips.mouser.apiKey.help + API klíč si můžete zaregistrovat na adrese <a href="https://eu.mouser.com/api-hub/">https://eu.mouser.com/api-hub/</a>. + + + + + settings.ips.mouser.searchLimit + Limit vyhledávání + + + + + settings.ips.mouser.searchLimit.help + Maximální počet výsledků pro jedno vyhledávání. Nesmí být vyšší než 50. + + + + + settings.ips.mouser.searchOptions + Filtry vyhledávání + + + + + settings.ips.mouser.searchOptions.help + To vám umožňuje zobrazit pouze díly s určitou dostupností a/nebo shodou. + + + + + settings.ips.mouser.searchOptions.none + Žádný filtr + + + + + settings.ips.mouser.searchOptions.rohs + Pouze součásti splňující požadavky směrnice RoHS + + + + + settings.ips.mouser.searchOptions.inStock + Pouze skladem + + + + + settings.ips.mouser.searchOptions.rohsAndInStock + Pouze skladem, díly splňující požadavky směrnice RoHS + + + + + settings.ips.lcsc + LCSC + + + + + settings.ips.lcsc.help + Upozornění: LCSC neposkytuje oficiální API. Tento poskytovatel využívá API internetového obchodu. LCSC nezamýšlelo použití tohoto API a může kdykoli přestat fungovat, proto jej používejte na vlastní riziko. + + + + + settings.ips.lcsc.enabled + Povolit + + + + + settings.ips.lcsc.currency + Měna + + + + + settings.system.attachments + Přílohy a soubory + + + + + settings.system.attachments.maxFileSize + Maximální velikost souboru + + + + + settings.system.attachments.maxFileSize.help + Maximální velikost souborů, které lze nahrát. Upozorňujeme, že tato velikost je také omezena konfigurací PHP. + + + + + settings.system.attachments.allowDownloads + Povolit stahování externích souborů + + + + + settings.system.attachments.allowDownloads.help + Pomocí této možnosti mohou uživatelé stahovat externí soubory do Part-DB zadáním URL adresy. <b>Upozornění: Může se jednat o bezpečnostní riziko, protože by to mohlo uživatelům umožnit přístup k intranetovým zdrojům prostřednictvím Part-DB!</b> + + + + + settings.system.attachments.downloadByDefault + Ve výchozím nastavení stahovat nové adresy URL příloh + + + + + settings.system.customization + Přizpůsobení + + + + + settings.system.customization.instanceName + Instance name + + + + + settings.system.customization.instanceName.help + Název této instalace Part-DB. Hodnota se zobrazuje v navigační liště a názvech. + + + + + settings.system.customization.banner + Banner na domovské stránce + + + + + settings.system.history + Historie záznamů + + + + + settings.system.history.saveChangedFields + Uložit, která pole prvku byla změněna v položkách protokolu + + + + + settings.system.history.saveOldData + Uložit stará data v záznamech protokolu při změnách prvků + + + + + settings.system.history.saveNewData + Uložit nová data do záznamů protokolu při změně/vytvoření prvku + + + + + settings.system.history.saveRemovedData + Uložit odstraněná data v položkách protokolu při odstranění prvku + + + + + settings.system.customization.theme + Globální téma + + + + + settings.system.history.enforceComments + Vynutit komentáře pro typy akcí + + + + + settings.system.history.enforceComments.description + Pomocí této možnosti můžete určit, u kterých akcí jsou uživatelé povinni uvést důvod, který bude zaznamenán v historii. + + + + + settings.system.history.enforceComments.type.part_edit + Úprava dílu + + + + + settings.system.history.enforceComments.type.part_create + Vytvoření dílu + + + + + settings.system.history.enforceComments.type.part_delete + Smazání dílu + + + + + settings.system.history.enforceComments.type.part_stock_operation + Skladová operace dílu + + + + + settings.system.history.enforceComments.type.datastructure_edit + Úprava datové struktury + + + + + settings.system.history.enforceComments.type.datastructure_create + Vytvoření datové struktury + + + + + settings.system.history.enforceComments.type.datastructure_delete + Smazání datové struktury + + + + + settings.system.privacy.useGravatar + Použít avatary Gravatar + + + + + settings.system.privacy.useGravatar.description + Pokud uživatel nemá nastavený avatar, použijte avatar z Gravataru na základě e-mailové adresy uživatele. To způsobí, že prohlížeč načte obrázky od třetí strany! + + + + + settings.system.privacy.checkForUpdates + Kontrolovat aktualizace Part-DB + + + + + settings.system.privacy.checkForUpdates.description + Part-DB pravidelně kontroluje, zda je na GitHubu k dispozici nová verze. Pokud tuto funkci nechcete používat nebo pokud se váš server nemůže připojit k internetu, deaktivujte ji zde. + + + + + settings.system.localization.locale + Výchozí jazyk / místní nastavení + + + + + settings.system.localization + Lokalizace + + + + + settings.system.localization.timezone + Výchozí časové pásmo + + + + + settings.system.localization.base_currency + Základní měna + + + + + settings.system.localization.base_currency_description + Měna, která se používá k ukládání informací o cenách a směnných kurzech. Tato měna se předpokládá, pokud není pro informace o cenách nastavena žádná měna. +<b>Upozorňujeme, že při změně této hodnoty nedochází k převodu měn. Změna výchozí měny po přidání informací o cenách tedy povede k nesprávným cenám!</b> + + + + + settings.system.privacy + Ochrana osobních údajů + + + + + settings.title + Nastavení serveru + + + + + settings.misc.kicad_eda + KiCAD integrace + + + + + settings.misc.kicad_eda.category_depth + Hloubka kategorie + + + + + settings.misc.kicad_eda.category_depth.help + Tato hodnota určuje hloubku stromu kategorií, který je viditelný v KiCad. Hodnota 0 znamená, že jsou viditelné pouze kategorie nejvyšší úrovně. Nastavte hodnotu > 0, chcete-li zobrazit více úrovní. Nastavte hodnotu -1, chcete-li zobrazit všechny části Part-DB v jedné kategorii v KiCad. + + + + + settings.behavior.sidebar + Postranní panel + + + + + settings.behavior.sidebar.items + Položky postranního panelu + + + + + settings.behavior.sidebar.items.help + Nabídky, které se standardně zobrazují na postranním panelu. Pořadí položek lze změnit pomocí funkce drag & drop. + + + + + settings.behavior.sidebar.rootNodeEnabled + Použít kořenový uzel + + + + + settings.behavior.sidebar.rootNodeEnabled.help + Pokud je tato možnost povolena, všechny kategorie nejvyšší úrovně, stopy atd. budou umístěny pod jediný kořenový uzel. Pokud je tato možnost zakázána, kategorie nejvyšší úrovně budou umístěny přímo do nabídky. + + + + + settings.behavior.sidebar.rootNodeExpanded + Rozbalit kořenový uzel ve výchozím nastavení + + + + + settings.behavior.table + Tabulky + + + + + settings.behavior.table.default_page_size + Výchozí velikost stránky + + + + + settings.behavior.table.default_page_size.help + Výchozí velikost stránky u tabulek na celou stránku. Nastavte na -1, aby se všechny položky zobrazovaly ve výchozím nastavení bez stránkování. + + + + + settings.behavior.table.parts_default_columns + Výchozí sloupce pro tabulky dílů + + + + + settings.behavior.table.parts_default_columns.help + Sloupce, které se mají ve výchozím nastavení zobrazovat v částečných tabulkách. Pořadí položek lze změnit pomocí funkce drag & drop. + + + + + settings.ips.oemsecrets + OEMSecrets + + + + + settings.ips.oemsecrets.keepZeroPrices + Zachovat distributory s nulovými cenami + + + + + settings.ips.oemsecrets.keepZeroPrices.help + Pokud toto není nastaveno, distributoři, u kterých jsou ceny 0, budou vyřazeni jako neplatní. + + + + + settings.ips.oemsecrets.parseParams + Extrahovat parametry z popisu + + + + + settings.ips.oemsecrets.parseParams.help + Pokud je tato možnost povolena, poskytovatel se pokusí převést nestrukturované popisy OEMSecrets na strukturované parametry. Každý parametr v popisu by měl mít tvar „...;name1:value1;name2:value2“. + + + + + settings.ips.oemsecrets.sortMode + Režim řazení výsledků + + + + + settings.ips.oemsecrets.sortMode.N + Žádné + + + + + settings.ips.oemsecrets.sortMode.C + Úplnost (upřednostňujte položky s podrobnými informacemi) + + + + + settings.ips.oemsecrets.sortMode.M + Úplnost a název výrobce + + + + + entity.export.flash.error.no_entities + Neexistují žádné entity k exportu! + + + + + attachment.table.internal_file + Interní soubor + + + + + attachment.table.external_link + Externí odkaz + + + + + attachment.view_external.view_at + Zobrazit na %host% + + + + + attachment.view_external + Zobrazit externí verzi + + + + + part.table.actions.error + Při provádění akce došlo k %count% chybám: + + + + + part.table.actions.error_detail + %part_name% (ID: %part_id%): %message% + + + + + part_list.action.action.change_location + Změnit umístění (pouze pro díly s jedinou šarží) + + + + + parts.table.action_handler.error.part_lots_multiple + Tento dlobsahuje více než jeden skladem. Změňte umístění ručně a vyberte, který sklad chcete použít. + + + + + settings.ips.reichelt + Reichelt + + + + + settings.ips.reichelt.help + Reichelt.com nenabízí žádné oficiální API, proto tento poskytovatel informací extrahuje informace z webových stránek pomocí webového scrapingu. Tato služba může kdykoli přestat fungovat, proto ji používejte na vlastní riziko. + + + + + settings.ips.reichelt.include_vat + Zahrnout DPH do cen + + + + + settings.ips.pollin + Pollin + + + + + settings.ips.pollin.help + Pollin.de nenabízí žádné oficiální API, proto tento poskytovatel informací získává informace pomocí webového scrapingu. Tato služba může kdykoli přestat fungovat, proto ji používejte na vlastní riziko. + + + + + settings.behavior.sidebar.rootNodeRedirectsToNewEntity + Kořenové uzly přesměrovat na nové stránky entit + + + + + settings.ips.digikey + Digikey + + + + + settings.ips.digikey.client_id + ID klienta + + + + + settings.ips.digikey.secret + Tajný klíč + + + + + settings.ips.octopart + Octopart / Nexar + + + + + settings.ips.octopart.searchLimit + Počet výsledků + + + + + settings.ips.octopart.searchLimit.help + Počet výsledků, které lze získat z Octopart při vyhledávání (uveďte prosím, že se to započítává do vašich limitů API) + + + + + settings.ips.octopart.onlyAuthorizedSellers + Pouze autorizovaní prodejci + + + + + settings.ips.octopart.onlyAuthorizedSellers.help + Nastavte na hodnotu false, chcete-li do výsledků zahrnout neautorizované nabídky. + + + + + settings.misc.exchange_rate + Směnné kurzy + + + + + settings.misc.exchange_rate.fixer_api_key + Fixer.io API klíč + + + + + settings.misc.exchange_rate.fixer_api_key.help + Pokud potřebujete směnné kurzy mezi měnami mimo eurozónu, můžete zde zadat API klíč z fixer.io. + + + + + settings.behavior.part_info + Stránka s informacemi o dílu + + + + + settings.behavior.part_info.show_part_image_overlay + Zobrazit překryv obrázku + + + + + settings.behavior.part_info.show_part_image_overlay.help + Při najetí kurzorem myši na galerii obrázků dílů se zobrazí překryvný obrázek s podrobnostmi o příloze. + + + + + perm.config.change_system_settings + Změnit nastavení systému + + + + + tree.tools.system.settings + Nastavení systému + + + + + settings.tooltip.overrideable_by_env + Hodnotu tohoto parametru lze přepsat nastavením proměnné prostředí „%env%“. + + + + + settings.flash.saved + Nastavení bylo úspěšně uloženo. + + + + + settings.flash.invalid + Nastavení je neplatné. Zkontrolujte prosím své zadání! + + + + + info_providers.settings.title + Nastavení poskytovatele informací + + + + + form.apikey.redacted + Z bezpečnostních důvodů redigováno + + From c3cc7cb0d61989023f39954c36ebc6ef4585f7c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 31 Aug 2025 15:12:31 +0200 Subject: [PATCH 009/228] New translations validators.en.xlf (Czech) --- translations/validators.cs.xlf | 98 ++++++++++++++++++++-------------- 1 file changed, 58 insertions(+), 40 deletions(-) diff --git a/translations/validators.cs.xlf b/translations/validators.cs.xlf index 4a6886fd..c298266a 100644 --- a/translations/validators.cs.xlf +++ b/translations/validators.cs.xlf @@ -1,7 +1,7 @@ - + Part-DB1\src\Entity\Attachments\AttachmentContainingDBElement.php:0 Part-DB1\src\Entity\Attachments\AttachmentType.php:0 @@ -42,7 +42,7 @@ Příloha náhledu musí být platný obrázek! - + Part-DB1\src\Entity\Attachments\AttachmentType.php:0 Part-DB1\src\Entity\Base\AbstractCompany.php:0 @@ -87,7 +87,7 @@ Prvek s tímto názvem již na této úrovni existuje! - + Part-DB1\src\Entity\Parameters\AbstractParameter.php:0 Part-DB1\src\Entity\Parameters\AttachmentTypeParameter.php:0 @@ -107,7 +107,7 @@ Hodnota musí být menší nebo rovna typické hodnotě ({{ compared_value }}). - + Part-DB1\src\Entity\Parameters\AbstractParameter.php:0 Part-DB1\src\Entity\Parameters\AttachmentTypeParameter.php:0 @@ -127,7 +127,7 @@ Hodnota musí být menší než maximální hodnota ({{ compared_value }}). - + Part-DB1\src\Entity\Parameters\AbstractParameter.php:0 Part-DB1\src\Entity\Parameters\AttachmentTypeParameter.php:0 @@ -147,7 +147,7 @@ Hodnota musí být větší nebo rovna typické hodnotě ({{ compared_value }}). - + Part-DB1\src\Entity\UserSystem\User.php:0 Part-DB1\src\Entity\UserSystem\User.php:0 @@ -157,7 +157,7 @@ Uživatel s tímto jménem již existuje - + Part-DB1\src\Entity\UserSystem\User.php:0 Part-DB1\src\Entity\UserSystem\User.php:0 @@ -167,185 +167,203 @@ Uživatelské jméno musí obsahovat pouze písmena, číslice, podtržítka, tečky, plusy nebo mínusy! - + obsolete validator.noneofitschild.self - Prvek nemůže být svým vlastním rodičem! + Prvek nemůže být svým vlastním rodičem. - + obsolete validator.noneofitschild.children - Podřízený prvek nemůže být nadřazeným prvkem! + Rodič nemůže být jedním ze svých potomků. - + validator.select_valid_category Vyberte prosím platnou kategorii! - + validator.part_lot.only_existing - Do tohoto umístění nelze přidávat nové díly, protože je označeno jako "Pouze existující". + Úložiště bylo označeno jako "pouze existující", takže do něj nelze přidat novou část. - + validator.part_lot.location_full.no_increase Místo je obsazeno. Množství nelze navýšit (nová hodnota musí být menší než {{ old_amount }}). - + validator.part_lot.location_full - Místo je obsazeno. Nelze do něj přidávat nové díly. + Úložiště bylo označeno jako plné, takže do něj nelze přidat nový díl. - + validator.part_lot.single_part Toto umístění může obsahovat pouze jeden díl, takže do něj nelze přídávat další! - + validator.attachment.must_not_be_null Musíte vybrat typ přílohy! - + validator.orderdetail.supplier_must_not_be_null Musíte si vybrat dodavatele! - + validator.measurement_unit.use_si_prefix_needs_unit Chcete-li povolit předpony SI, musíte nastavit symbol jednotky! - + part.ipn.must_be_unique Interní číslo dílu musí být jedinečné. {{ value }} se již používá! - + validator.project.bom_entry.name_or_part_needed Musíte vybrat díl pro položku BOM dílu nebo nastavit název pro položku BOM bez dílu. - + project.bom_entry.name_already_in_bom Již existuje položka BOM s tímto názvem! - + project.bom_entry.part_already_in_bom Tento díl již existuje v tomto BOM! - + project.bom_entry.mountnames_quantity_mismatch Počet názvů sestav musí odpovídat počtu komponent v BOM! - + project.bom_entry.can_not_add_own_builds_part Seznam BOM projektu nelze přidat do BOM. - + project.bom_has_to_include_all_subelement_parts BOM projektu musí obsahovat všechny výrobní díly dílčích projektů. Díl %part_name% projektu %project_name% chybí! - + project.bom_entry.price_not_allowed_on_parts U položek komponent BOM nelze nastavit cenu. Zadejte cenu samotného dílu. - + validator.project_build.lot_bigger_than_needed Zvolili jste větší množství pro vychystávání, než je nutné. Odstraňte přebytečné množství - + validator.project_build.lot_smaller_than_needed Zvolili jste menší množství k odebrání, než je potřeba pro sestavení! Přidejte další množství. - + part.name.must_match_category_regex Název komponenty neodpovídá regulárnímu výrazu zadanému pro kategorii: %regex% - + validator.attachment.name_not_blank Vyberte hodnotu nebo nahrajte soubor, aby se jeho název automaticky použil jako název této přílohy. - + validator.part_lot.owner_must_match_storage_location_owner Vlastník inventáře této komponenty a vybrané umístění se musí shodovat (%owner_name%)! - + validator.part_lot.owner_must_not_be_anonymous Vlastníkem nemůže být anonymní uživatel! - + validator.part_association.must_set_an_value_if_type_is_other Pokud nastavíte typ na "jiný", musíte pro něj nastavit popisnou hodnotu! - + validator.part_association.part_cannot_be_associated_with_itself Díl nemůže být spojen sám se sebou! - + validator.part_association.already_exists Asociace s tímto dílem již existuje! - + validator.part_lot.vendor_barcode_must_be_unique Tato hodnota čárového kódu dodavatele již byla použita v jiném inventáře. Čárový kód musí být jedinečný! - + validator.year_2038_bug_on_32bit Kvůli technickým omezením není možné na 32bitových systémech vybrat datumpo 19.1.2038! + + + validator.fileSize.invalidFormat + Neplatný formát velikosti souboru. Použijte celé číslo a jako příponu K, M, G pro kilobajty, megabajty nebo gigabajty. + + + + + validator.invalid_range + Zadaný rozsah není platný! + + + + + validator.google_code.wrong_code + Neplatný kód. Zkontrolujte, zda je vaše ověřovací aplikace správně nastavena a zda je čas správně nastaven jak na serveru, tak na ověřovacím zařízení. + + From 87cf75f67d56c2927beb9511843d690b11fdf72b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 31 Aug 2025 15:12:32 +0200 Subject: [PATCH 010/228] New translations security.en.xlf (Czech) --- translations/security.cs.xlf | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/translations/security.cs.xlf b/translations/security.cs.xlf index b4a58697..4e9570c2 100644 --- a/translations/security.cs.xlf +++ b/translations/security.cs.xlf @@ -1,17 +1,23 @@ - + user.login_error.user_disabled Váš účet je deaktivován! Pokud si myslíte, že je to špatně, kontaktujte správce. - + saml.error.cannot_login_local_user_per_saml Přes SSO se nelze přihlásit jako místní uživatel! Místo toho použijte heslo místního uživatele. + + + saml.error.cannot_login_saml_user_locally + Pro přihlášení jako uživatel SAML nelze použít místní ověření! Místo toho použijte přihlášení SSO. + + From bdd88700d4ca6e8c659c83565afea47586d89816 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 31 Aug 2025 15:13:45 +0200 Subject: [PATCH 011/228] Start php-fpm directly in our docker entrypoint This way it gets all environment variables and we do not need to hassle ourselves with the generation of php-fpm config files and we can use the normal clear_env=no option This fixes issue #1006 --- .docker/partdb-entrypoint.sh | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/.docker/partdb-entrypoint.sh b/.docker/partdb-entrypoint.sh index 3b0326ac..f5071e22 100644 --- a/.docker/partdb-entrypoint.sh +++ b/.docker/partdb-entrypoint.sh @@ -20,25 +20,6 @@ set -e -# Pass all environment variables to PHP-FPM -# Path where PHP-FPM pool configs live -PHP_FPM_ENV_CONF="/etc/php/PHP_VERSION/fpm/pool.d/99-env.conf" - -# start fresh -echo "; auto-generated env config" > "$PHP_FPM_ENV_CONF" -echo "[www]" >> "$PHP_FPM_ENV_CONF" -echo "clear_env = no" >> "$PHP_FPM_ENV_CONF" - -# add all container envs -printenv | while IFS='=' read -r name value; do - case "$name" in - HOSTNAME|PWD|SHLVL|PATH|_*) continue ;; - esac - # write literal value in quotes - echo "env[$name] = \"$value\"" >> "$PHP_FPM_ENV_CONF" -done - - # recursive chowns can take a while, so we'll just do it if the owner is wrong # Chown uploads/ folder if it does not belong to www-data @@ -59,7 +40,7 @@ if [ -d /var/www/html/var/db ]; then fi # Start PHP-FPM (the PHP_VERSION is replaced by the configured version in the Dockerfile) -service phpPHP_VERSION-fpm start +php-fpmPHP_VERSION -F & # Run migrations if automigration is enabled via env variable DB_AUTOMIGRATE From 578a030175615e405c28d0429c438c24cc598fd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 31 Aug 2025 15:19:05 +0200 Subject: [PATCH 012/228] Reverted english translations which were broken by the PR --- translations/messages.en.xlf | 2292 +++++++++++++++++++++++++++++++++- 1 file changed, 2259 insertions(+), 33 deletions(-) diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 3b71fe83..e65445ce 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -242,7 +242,7 @@ part.info.timetravel_hint - This is how the part appeared before %timestamp%. <i>Please note that this feature is experimental, so the info may not be correct.</i> + Please note that this feature is experimental, so the info may not be correct.]]> @@ -731,10 +731,10 @@ user.edit.tfa.disable_tfa_message - This will disable <b>all active two-factor authentication methods of the user</b> and delete the <b>backup codes</b>! -<br> -The user will have to set up all two-factor authentication methods again and print new backup codes! <br><br> -<b>Only do this if you are absolutely sure about the identity of the user (seeking help), otherwise the account could be compromised by an attacker!</b> + all active two-factor authentication methods of the user and delete the backup codes! +
+The user will have to set up all two-factor authentication methods again and print new backup codes!

+Only do this if you are absolutely sure about the identity of the user (seeking help), otherwise the account could be compromised by an attacker!]]>
@@ -885,9 +885,9 @@ The user will have to set up all two-factor authentication methods again and pri entity.delete.message - This can not be undone! -<br> -Sub elements will be moved upwards. + +Sub elements will be moved upwards.]]> @@ -1441,7 +1441,7 @@ Sub elements will be moved upwards. homepage.github.text - Source, downloads, bug reports, to-do-list etc. can be found on <a href="%href%" class="link-external" target="_blank">GitHub project page</a> + GitHub project page]]> @@ -1463,7 +1463,7 @@ Sub elements will be moved upwards. homepage.help.text - Help and tips can be found in Wiki the <a href="%href%" class="link-external" target="_blank">GitHub page</a> + GitHub page]]> @@ -1705,7 +1705,7 @@ Sub elements will be moved upwards. email.pw_reset.fallback - If this does not work for you, go to <a href="%url%">%url%</a> and enter the following info + %url% and enter the following info]]> @@ -1735,7 +1735,7 @@ Sub elements will be moved upwards. email.pw_reset.valid_unit %date% - The reset token will be valid until <i>%date%</i>. + %date%.]]> @@ -3578,8 +3578,8 @@ Sub elements will be moved upwards. tfa_google.disable.confirm_message - If you disable the Authenticator App, all backup codes will be deleted, so you may need to reprint them.<br> -Also note that without two-factor authentication, your account is no longer as well protected against attackers! + +Also note that without two-factor authentication, your account is no longer as well protected against attackers!]]> @@ -3599,7 +3599,7 @@ Also note that without two-factor authentication, your account is no longer as w tfa_google.step.download - Download an authenticator app (e.g. <a class="link-external" target="_blank" href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2">Google Authenticator</a> oder <a class="link-external" target="_blank" href="https://play.google.com/store/apps/details?id=org.fedorahosted.freeotp">FreeOTP Authenticator</a>) + Google Authenticator oder FreeOTP Authenticator)]]> @@ -3841,8 +3841,8 @@ Also note that without two-factor authentication, your account is no longer as w tfa_trustedDevices.explanation - When checking the second factor, the current computer can be marked as trustworthy, so no more two-factor checks on this computer are needed. -If you have done this incorrectly or if a computer is no longer trusted, you can reset the status of <i>all </i>computers here. + all computers here.]]> @@ -5313,7 +5313,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can label_options.lines_mode.help - If you select Twig here, the content field is interpreted as Twig template. See <a href="https://twig.symfony.com/doc/3.x/templates.html">Twig documentation</a> and <a href="https://docs.part-db.de/usage/labels.html#twig-mode">Wiki</a> for more information. + Twig documentation and Wiki for more information.]]> @@ -7157,7 +7157,7 @@ Exampletown mass_creation.lines.placeholder - Element 1 + + + + obsolete + obsolete + + + entity.mass_creation.btn + Create + + + + + obsolete + obsolete + + + measurement_unit.edit.is_integer + Is integer + + + + + obsolete + obsolete + + + measurement_unit.edit.is_integer.help + If this option is activated, all values with this unit will be rounded to whole numbers. + + + + + obsolete + obsolete + + + measurement_unit.edit.use_si_prefix + Use SI prefix + + + + + obsolete + obsolete + + + measurement_unit.edit.use_si_prefix.help + If this option is activated, values are outputted with SI prefixes (e.g. 1,2kg instead of 1200g) + + + + + obsolete + obsolete + + + measurement_unit.edit.unit_symbol + Unit symbol + + + + + obsolete + obsolete + + + measurement_unit.edit.unit_symbol.placeholder + e.g. m + + + + + obsolete + obsolete + + + storelocation.edit.is_full.label + Storelocation full + + + + + obsolete + obsolete + + + storelocation.edit.is_full.help + If this option is selected, it is neither possible to add new parts to this storelocation or to increase the amount of existing parts. + + + + + obsolete + obsolete + + + storelocation.limit_to_existing.label + Limit to existing parts + + + + + obsolete + obsolete + + + storelocation.limit_to_existing.help + If this option is activated, it is not possible to add new parts to this storelocation, but the amount of existing parts can be increased. + + + + + obsolete + obsolete + + + storelocation.only_single_part.label + Only single part + + + + + obsolete + obsolete + + + storelocation.only_single_part.help + If this option is activated, only a single part (with every amount) can be assigned to this storage location. Useful for small SMD boxes or feeders. + + + + + obsolete + obsolete + + + storelocation.storage_type.label + Storage type + + + + + obsolete + obsolete + + + storelocation.storage_type.help + You can select a measurement unit here, which a part must have to be able to be assigned to this storage location + + + + + obsolete + obsolete + + + supplier.edit.default_currency + Default currency + + + + + obsolete + obsolete + + + supplier.shipping_costs.label + Shipping Costs + + + + + obsolete + obsolete + + + user.username.placeholder + e.g. j.doe + + + + + obsolete + obsolete + + + user.firstName.placeholder + e.g John + + + + + obsolete + obsolete + + + user.lastName.placeholder + e.g. Doe + + + + + obsolete + obsolete + + + user.email.placeholder + j.doe@ecorp.com + + + + + obsolete + obsolete + + + user.department.placeholder + e.g. Development + + + + + obsolete + obsolete + + + user.settings.pw_new.label + New password + + + + + obsolete + obsolete + + + user.settings.pw_confirm.label + Confirm new password + + + + + obsolete + obsolete + + + user.edit.needs_pw_change + User needs to change password + + + + + obsolete + obsolete + + + user.edit.user_disabled + User disabled (no login possible) + + + + + obsolete + obsolete + + + user.create + Create user + + + + + obsolete + obsolete + + + user.edit.save + Save + + + + + obsolete + obsolete + + + entity.edit.reset + Discard changes + + + + + templates\Parts\show_part_info.html.twig:166 + obsolete + obsolete + + + part.withdraw.btn + Withdraw + + + + + templates\Parts\show_part_info.html.twig:171 + obsolete + obsolete + + + part.withdraw.comment: + Comment/Purpose + + + + + templates\Parts\show_part_info.html.twig:189 + obsolete + obsolete + + + part.add.caption + Add parts + + + + + templates\Parts\show_part_info.html.twig:194 + obsolete + obsolete + + + part.add.btn + Add + + + + + templates\Parts\show_part_info.html.twig:199 + obsolete + obsolete + + + part.add.comment + Comment/Purpose + + + + + templates\AdminPages\CompanyAdminBase.html.twig:15 + obsolete + obsolete + + + admin.comment + Notes + + + + + src\Form\PartType.php:83 + obsolete + obsolete + + + manufacturer_url.label + Manufacturer link + + + + + src\Form\PartType.php:66 + obsolete + obsolete + + + part.description.placeholder + e.g. NPN 45V 0,1A 0,5W + + + + + src\Form\PartType.php:69 + obsolete + obsolete + + + part.instock.placeholder + e.g. 10 + + + + + src\Form\PartType.php:72 + obsolete + obsolete + + + part.mininstock.placeholder + e.g. 5 + + + + + obsolete + obsolete + + + part.order.price_per + Price per + + + + + obsolete + obsolete + + + part.withdraw.caption + Withdraw parts + + + + + obsolete + obsolete + + + datatable.datatable.lengthMenu + _MENU_ + + + + + obsolete + obsolete + + + perm.group.parts + Parts + + + + + obsolete + obsolete + + + perm.group.structures + Data structures + + + + + obsolete + obsolete + + + perm.group.system + System + + + + + obsolete + obsolete + + + perm.parts + Parts + + + + + obsolete + obsolete + + + perm.read + View + + + + + obsolete + obsolete + + + perm.edit + Edit + + + + + obsolete + obsolete + + + perm.create + Create + + + + + obsolete + obsolete + + + perm.part.move + Change category + + + + + obsolete + obsolete + + + perm.delete + Delete + + + + + obsolete + obsolete + + + perm.part.search + Search + + + + + obsolete + obsolete + + + perm.part.all_parts + List all parts + + + + + obsolete + obsolete + + + perm.part.no_price_parts + List parts without price info + + + + + obsolete + obsolete + + + perm.part.obsolete_parts + List obsolete parts + + + + + obsolete + obsolete + + + perm.part.unknown_instock_parts + Show parts with unknown instock + + + + + obsolete + obsolete + + + perm.part.change_favorite + Change favorite status + + + + + obsolete + obsolete + + + perm.part.show_favorite + List favorite parts + + + + + obsolete + obsolete + + + perm.part.show_last_edit_parts + Show last edited/added parts + + + + + obsolete + obsolete + + + perm.part.show_users + Show last modifying user + + + + + obsolete + obsolete + + + perm.part.show_history + Show history + + + + + obsolete + obsolete + + + perm.part.name + Name + + + + + obsolete + obsolete + + + perm.part.description + Description + + + + + obsolete + obsolete + + + perm.part.instock + Instock + + + + + obsolete + obsolete + + + perm.part.mininstock + Minimum instock + + + + + obsolete + obsolete + + + perm.part.comment + Notes + + + + + obsolete + obsolete + + + perm.part.storelocation + Storage location + + + + + obsolete + obsolete + + + perm.part.manufacturer + Manufacturer + + + + + obsolete + obsolete + + + perm.part.orderdetails + Order information + + + + + obsolete + obsolete + + + perm.part.prices + Prices + + + + + obsolete + obsolete + + + perm.part.attachments + File attachments + + + + + obsolete + obsolete + + + perm.part.order + Orders + + + + + obsolete + obsolete + + + perm.storelocations + Storage locations + + + + + obsolete + obsolete + + + perm.move + Move + + + + + obsolete + obsolete + + + perm.list_parts + List parts + + + + + obsolete + obsolete + + + perm.part.footprints + Footprints + + + + + obsolete + obsolete + + + perm.part.categories + Categories + + + + + obsolete + obsolete + + + perm.part.supplier + Suppliers + + + + + obsolete + obsolete + + + perm.part.manufacturers + Manufacturers + + + + + obsolete + obsolete + + + perm.projects + Projects + + + + + obsolete + obsolete + + + perm.part.attachment_types + Attachment types + + + + + obsolete + obsolete + + + perm.tools.import + Import + + + + + obsolete + obsolete + + + perm.tools.labels + Labels + + + + + obsolete + obsolete + + + perm.tools.calculator + Resistor calculator + + + + + obsolete + obsolete + + + perm.tools.footprints + Footprints + + + + + obsolete + obsolete + + + perm.tools.ic_logos + IC logos + + + + + obsolete + obsolete + + + perm.tools.statistics + Statistics + + + + + obsolete + obsolete + + + perm.edit_permissions + Edit permissions + + + + + obsolete + obsolete + + + perm.users.edit_user_name + Edit user name + + + + + obsolete + obsolete + + + perm.users.edit_change_group + Change group + + + + + obsolete + obsolete + + + perm.users.edit_infos + Edit info + + + + + obsolete + obsolete + + + perm.users.edit_permissions + Edit permissions + + + + + obsolete + obsolete + + + perm.users.set_password + Set password + + + + + obsolete + obsolete + + + perm.users.change_user_settings + Change user settings + + + + + obsolete + obsolete + + + perm.database.see_status + Show status + + + + + obsolete + obsolete + + + perm.database.update_db + Update DB + + + + + obsolete + obsolete + + + perm.database.read_db_settings + Read DB settings + + + + + obsolete + obsolete + + + perm.database.write_db_settings + Write DB settings + + + + + obsolete + obsolete + + + perm.config.read_config + Read config + + + + + obsolete + obsolete + + + perm.config.edit_config + Edit config + + + + + obsolete + obsolete + + + perm.config.server_info + Server info + + + + + obsolete + obsolete + + + perm.config.use_debug + Use debug tools + + + + + obsolete + obsolete + + + perm.show_logs + Show logs + + + + + obsolete + obsolete + + + perm.delete_logs + Delete logs + + + + + obsolete + obsolete + + + perm.self.edit_infos + Edit info + + + + + obsolete + obsolete + + + perm.self.edit_username + Edit username + + + + + obsolete + obsolete + + + perm.self.show_permissions + View permissions + + + + + obsolete + obsolete + + + perm.self.show_logs + Show own log entries + + + + + obsolete + obsolete + + + perm.self.create_labels + Create labels + + + + + obsolete + obsolete + + + perm.self.edit_options + Edit options + + + + + obsolete + obsolete + + + perm.self.delete_profiles + Delete profiles + + + + + obsolete + obsolete + + + perm.self.edit_profiles + Edit profiles + + + + + obsolete + obsolete + + + perm.part.tools + Tools + + + + + obsolete + obsolete + + + perm.groups + Groups + + + + + obsolete + obsolete + + + perm.users + Users + + + + + obsolete + obsolete + + + perm.database + Database + + + + + obsolete + obsolete + + + perm.config + Configuration + + + + + obsolete + obsolete + + + perm.system + System + + + + + obsolete + obsolete + + + perm.self + Own user + + + + + obsolete + obsolete + + + perm.labels + Labels + + + + + obsolete + obsolete + + + perm.part.category + Category + + + + + obsolete + obsolete + + + perm.part.minamount + Minimum amount + + + + + obsolete + obsolete + + + perm.part.footprint + Footprint + + + + + obsolete + obsolete + + + perm.part.mpn + MPN + + + + + obsolete + obsolete + + + perm.part.status + Manufacturing status + + + + + obsolete + obsolete + + + perm.part.tags + Tags + + + + + obsolete + obsolete + + + perm.part.unit + Part unit + + + + + obsolete + obsolete + + + perm.part.mass + Mass + + + + + obsolete + obsolete + + + perm.part.lots + Part lots + + + + + obsolete + obsolete + + + perm.show_users + Show last modifying user + + + + + obsolete + obsolete + + + perm.currencies + Currencies + + + + + obsolete + obsolete + + + perm.measurement_units + Measurement unit + + + + + obsolete + obsolete + + + user.settings.pw_old.label + Old password + + + + + obsolete + obsolete + + + pw_reset.submit + Reset password + + + + + obsolete + obsolete + + + u2f_two_factor + Security key (U2F) + + + + + obsolete + obsolete + + + google + Google + + + + + tfa.provider.webauthn_two_factor_provider + Security key + + + + + obsolete + obsolete + + + tfa.provider.google + Authenticator app + + + + + obsolete + obsolete + + + Login successful + Login successful + + + + + obsolete + obsolete + + + log.type.exception + Unhandled exception (obsolete) + + + + + obsolete + obsolete + + + log.type.user_login + User login + + + + + obsolete + obsolete + + + log.type.user_logout + User logout + + + + + obsolete + obsolete + + + log.type.unknown + Unknown + + + + + obsolete + obsolete + + + log.type.element_created + Element created + + + + + obsolete + obsolete + + + log.type.element_edited + Element edited + + + + + obsolete + obsolete + + + log.type.element_deleted + Element deleted + + + + + obsolete + obsolete + + + log.type.database_updated + Database updated + + + + + obsolete + + + perm.revert_elements + Revert element + + + + + obsolete + + + perm.show_history + Show history + + + + + obsolete + + + perm.tools.lastActivity + Show last activity + + + + + obsolete + + + perm.tools.timeTravel + Show old element versions (time travel) + + + + + obsolete + + + tfa_u2f.key_added_successful + Security key added successfully. + + + + + obsolete + + + Username + Username + + + + + obsolete + + + log.type.security.google_disabled + Authenticator App disabled + + + + + obsolete + + + log.type.security.u2f_removed + Security key removed + + + + + obsolete + + + log.type.security.u2f_added + Security key added + + + + + obsolete + + + log.type.security.backup_keys_reset + Backup keys regenerated + + + + + obsolete + + + log.type.security.google_enabled + Authenticator App enabled + + + + + obsolete + + + log.type.security.password_changed + Password changed + + + + + obsolete + + + log.type.security.trusted_device_reset + Trusted devices resetted + + + + + obsolete + + + log.type.collection_element_deleted + Element of Collection deleted + + + + + obsolete + + + log.type.security.password_reset + Password reset + + + + + obsolete + + + log.type.security.2fa_admin_reset + Two Factor Reset by Administrator + + + + + obsolete + + + log.type.user_not_allowed + Unauthorized access attempt + + + + + obsolete + + + log.database_updated.success + Success + + + + + obsolete + + + label_options.barcode_type.2D + 2D + + + + + obsolete + + + label_options.barcode_type.1D + 1D + + + + + obsolete + + + perm.part.parameters + Parameters + + + + + obsolete + + + perm.attachment_show_private + View private attachments + + + + + obsolete + + + perm.tools.label_scanner + Label scanner + + + + + obsolete + + + perm.self.read_profiles + Read profiles + + + + + obsolete + + + perm.self.create_profiles + Create profiles + + + + + obsolete + + + perm.labels.use_twig + Use twig mode + + + + + label_profile.showInDropdown + Show in quick select + + + + + group.edit.enforce_2fa + Enforce Two-factor authentication (2FA) + + + + + group.edit.enforce_2fa.help + If this option is enabled, every direct member of this group, has to configure at least one second-factor for authentication. Recommended for administrative groups with much permissions. + + + + + selectpicker.empty + Nothing selected + + + + + selectpicker.nothing_selected + Nothing selected + + + + + entity.delete.must_not_contain_parts + Element "%PATH%" still contains parts! You have to move the parts, to be able to delete this element. + + + + + entity.delete.must_not_contain_attachments + Attachment type still contains attachments. Change their type, to be able to delete this attachment type. + + + + + entity.delete.must_not_contain_prices + Currency still contains price details. You have to change their currency to be able to delete this element. + + + + + entity.delete.must_not_contain_users + Users still uses this group! Change their group, to be able to delete this group. + + + + + part.table.edit + Edit + + + + + part.table.edit.title + Edit part + + + + + part_list.action.action.title + Select action + + + + + part_list.action.action.group.favorite + Favorite status + + + + + part_list.action.action.favorite + Favorite + + + + + part_list.action.action.unfavorite + Unfavorite + + + + + part_list.action.action.group.change_field + Change field + + + + + part_list.action.action.change_category + Change category + + + + + part_list.action.action.change_footprint + Change footprint + + + + + part_list.action.action.change_manufacturer + Change manufacturer + + + + + part_list.action.action.change_unit + Change part unit + + + + + part_list.action.action.delete + Delete + + + + + part_list.action.submit + Submit + + + + + part_list.action.part_count + %count% parts selected! + + + + + company.edit.quick.website + Open website + + + + + company.edit.quick.email + Send email + + + + + company.edit.quick.phone + Call phone + + + + + company.edit.quick.fax + Send fax + + + + + company.fax_number.placeholder + e.g. +49 1234 567890 + + + + + part.edit.save_and_clone + Save and clone + + + + + validator.file_ext_not_allowed + File extension not allowed for this attachment type. + + + + + tools.reel_calc.title + SMD Reel calculator + + + + + tools.reel_calc.inner_dia + Inner diameter + + + + + tools.reel_calc.outer_dia + Outer diameter + + + + + tools.reel_calc.tape_thick + Tape thickness + + + + + tools.reel_calc.part_distance + Part distance + + + + + tools.reel_calc.update + Update + + + + + tools.reel_calc.parts_per_meter + Parts per meter + + + + + tools.reel_calc.result_length + Tape length + + + + + tools.reel_calc.result_amount + Approx. parts count + + + + + tools.reel_calc.outer_greater_inner_error + Error: Outer diameter must be greater than inner diameter! + + + + + tools.reel_calc.missing_values.error + Please fill in all values! + + + + + tools.reel_calc.load_preset + Load preset + + + + + tools.reel_calc.explanation + This calculator gives you an estimation, how many parts are remaining on an SMD reel. Measure the noted the dimensions on the reel (or use some of the presets) and click "Update" to get an result. + + + + + perm.tools.reel_calculator + SMD Reel calculator + + + + + tree.tools.tools.reel_calculator + SMD Reel calculator + + + + + user.pw_change_needed.flash + Your password needs to be changed! Please set a new password. + + + + + tree.root_node.text + Root node + + + + + part_list.action.select_null + Empty element + + + + + part_list.action.delete-title + Do you really want to delete these parts? + + + + + part_list.action.delete-message + These parts and any associated information (like attachments, price information, etc.) will be deleted. This can not be undone! + + + + + part.table.actions.success + Actions finished successfully. + + + + + attachment.edit.delete.confirm + Do you really want to delete this attachment? + + + + + filter.text_constraint.value.operator.EQ + Is + + + + + filter.text_constraint.value.operator.NEQ + Is not + + + + + filter.text_constraint.value.operator.STARTS + Starts with + + + + + filter.text_constraint.value.operator.CONTAINS + Contains + + + + + filter.text_constraint.value.operator.ENDS + Ends with + + + + + filter.text_constraint.value.operator.LIKE + LIKE pattern + + + + + filter.text_constraint.value.operator.REGEX + Regular expression + + + + + filter.number_constraint.value.operator.BETWEEN + Between + + + + + filter.number_constraint.AND + and + + + + + filter.entity_constraint.operator.EQ + Is (excluding children) + + + + + filter.entity_constraint.operator.NEQ + Is not (excluding children) + + + + + filter.entity_constraint.operator.INCLUDING_CHILDREN + Is (including children) + + + + + filter.entity_constraint.operator.EXCLUDING_CHILDREN + Is not (including children) + + + + + part.filter.dbId + Database ID + + + + + filter.tags_constraint.operator.ANY + Any of the tags + + + + + filter.tags_constraint.operator.ALL + All the tags + + + + + filter.tags_constraint.operator.NONE + None of the tags + + + + + part.filter.lot_count + Number of lots + + + + + part.filter.attachments_count + Number of attachments + + + + + part.filter.orderdetails_count + Number of order details + + + + + part.filter.lotExpirationDate + Lot expiration date + + + + + part.filter.lotNeedsRefill + Any lot needs refill + + + + + part.filter.lotUnknwonAmount + Any lot has unknown amount + + + + + part.filter.attachmentName + Attachment name + + + + + filter.choice_constraint.operator.ANY + Any of + + + + + filter.choice_constraint.operator.NONE + None of + + + + + part.filter.amount_sum + Total amount + + + + + filter.submit + Update + + + + + filter.discard + Discard changes + + + + + filter.clear_filters + Clear all filters + + + + + filter.title + Filter + + + + + filter.parameter_value_constraint.operator.= + Typ. Value = + + + + + filter.parameter_value_constraint.operator.!= + Typ. Value != + + + + + filter.parameter_value_constraint.operator.< + + + filter.parameter_value_constraint.operator.> - Typ. Value > + ]]> filter.parameter_value_constraint.operator.<= - Typ. Value <= + filter.parameter_value_constraint.operator.>= - Typ. Value >= + =]]> @@ -7291,7 +9517,7 @@ Element 1 -> Element 1.2]]> parts_list.search.searching_for - Searching parts with keyword <b>%keyword%</b> + %keyword%]]> @@ -7951,13 +10177,13 @@ Element 1 -> Element 1.2]]> project.builds.number_of_builds_possible - You have enough stocked to build <b>%max_builds%</b> builds of this project. + %max_builds% builds of this project.]]> project.builds.check_project_status - The current project status is <b>"%project_status%"</b>. You should check if you really want to build the project with this status! + "%project_status%". You should check if you really want to build the project with this status!]]> @@ -8059,7 +10285,7 @@ Element 1 -> Element 1.2]]> entity.select.add_hint - Use -> to create nested structures, e.g. "Node 1->Node 1.1" + to create nested structures, e.g. "Node 1->Node 1.1"]]> @@ -8083,13 +10309,13 @@ Element 1 -> Element 1.2]]> homepage.first_steps.introduction - Your database is still empty. You might want to read the <a href="%url%">documentation</a> or start to creating the following data structures: + documentation or start to creating the following data structures:]]> homepage.first_steps.create_part - Or you can directly <a href="%url%">create a new part</a>. + create a new part.]]> @@ -8101,7 +10327,7 @@ Element 1 -> Element 1.2]]> homepage.forum.text - For questions about Part-DB use the <a href="%href%" class="link-external" target="_blank">discussion forum</a> + discussion forum]]> @@ -8755,7 +10981,7 @@ Element 1 -> Element 1.2]]> parts.import.help_documentation - See the <a href="%link%">documentation</a> for more information on the file format. + documentation for more information on the file format.]]> @@ -8935,7 +11161,7 @@ Element 1 -> Element 1.2]]> part.filter.lessThanDesired - In stock less than desired (total amount < min. amount) + @@ -9747,13 +11973,13 @@ Please note, that you can not impersonate a disabled user. If you try you will g part.merge.confirm.title - Do you really want to merge <b>%other%</b> into <b>%target%</b>? + %other% into %target%?]]> part.merge.confirm.message - <b>%other%</b> will be deleted, and the part will be saved with the shown information. + %other% will be deleted, and the part will be saved with the shown information.]]> From a6be786d5dd12dcb58d8c5e65e0804da454142ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 31 Aug 2025 15:20:22 +0200 Subject: [PATCH 013/228] Bump to version 2.0.2 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 227cea21..e9307ca5 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.0.0 +2.0.2 From c27f2246a3021f55e081c349be54f57d88796090 Mon Sep 17 00:00:00 2001 From: barisgit Date: Fri, 1 Aug 2025 18:50:19 +0200 Subject: [PATCH 014/228] Update part merger to consider rows with same supplier and spn duplicates --- .../EntityMergers/Mergers/PartMerger.php | 64 +++++++++---------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/src/Services/EntityMergers/Mergers/PartMerger.php b/src/Services/EntityMergers/Mergers/PartMerger.php index 4ce779e8..01b53e25 100644 --- a/src/Services/EntityMergers/Mergers/PartMerger.php +++ b/src/Services/EntityMergers/Mergers/PartMerger.php @@ -100,7 +100,8 @@ class PartMerger implements EntityMergerInterface return $target; } - private function comparePartAssociations(PartAssociation $t, PartAssociation $o): bool { + private function comparePartAssociations(PartAssociation $t, PartAssociation $o): bool + { //We compare the translation keys, as it contains info about the type and other type info return $t->getOther() === $o->getOther() && $t->getTypeTranslationKey() === $o->getTypeTranslationKey(); @@ -141,40 +142,39 @@ class PartMerger implements EntityMergerInterface $owner->addAssociatedPartsAsOwner($clone); } + // Merge orderdetails, considering same supplier+part number as duplicates $this->mergeCollections($target, $other, 'orderdetails', function (Orderdetail $t, Orderdetail $o) { - //First check that the orderdetails infos are equal - $tmp = $t->getSupplier() === $o->getSupplier() - && $t->getSupplierPartNr() === $o->getSupplierPartNr() - && $t->getSupplierProductUrl(false) === $o->getSupplierProductUrl(false); - - if (!$tmp) { - return false; - } - - //Check if the pricedetails are equal - $t_pricedetails = $t->getPricedetails(); - $o_pricedetails = $o->getPricedetails(); - //Ensure that both pricedetails have the same length - if (count($t_pricedetails) !== count($o_pricedetails)) { - return false; - } - - //Check if all pricedetails are equal - for ($n=0, $nMax = count($t_pricedetails); $n< $nMax; $n++) { - $t_price = $t_pricedetails->get($n); - $o_price = $o_pricedetails->get($n); - - if (!$t_price->getPrice()->isEqualTo($o_price->getPrice()) - || $t_price->getCurrency() !== $o_price->getCurrency() - || $t_price->getPriceRelatedQuantity() !== $o_price->getPriceRelatedQuantity() - || $t_price->getMinDiscountQuantity() !== $o_price->getMinDiscountQuantity() - ) { - return false; + // If supplier and part number match, merge the orderdetails + if ($t->getSupplier() === $o->getSupplier() && $t->getSupplierPartNr() === $o->getSupplierPartNr()) { + // Update URL if target doesn't have one + if (empty($t->getSupplierProductUrl(false)) && !empty($o->getSupplierProductUrl(false))) { + $t->setSupplierProductUrl($o->getSupplierProductUrl(false)); } + // Merge price details: add new ones, update empty ones, keep existing non-empty ones + foreach ($o->getPricedetails() as $otherPrice) { + $found = false; + foreach ($t->getPricedetails() as $targetPrice) { + if ($targetPrice->getMinDiscountQuantity() === $otherPrice->getMinDiscountQuantity() + && $targetPrice->getCurrency() === $otherPrice->getCurrency()) { + // Only update price if the existing one is zero/empty (most logical) + if ($targetPrice->getPrice()->isZero()) { + $targetPrice->setPrice($otherPrice->getPrice()); + $targetPrice->setPriceRelatedQuantity($otherPrice->getPriceRelatedQuantity()); + } + $found = true; + break; + } + } + // Add completely new price tiers + if (!$found) { + $clonedPrice = clone $otherPrice; + $clonedPrice->setOrderdetail($t); + $t->addPricedetail($clonedPrice); + } + } + return true; // Consider them equal so the other one gets skipped } - - //If all pricedetails are equal, the orderdetails are equal - return true; + return false; // Different supplier/part number, add as new }); //The pricedetails are not correctly assigned to the new orderdetails, so fix that foreach ($target->getOrderdetails() as $orderdetail) { From aa4299041b098137e4d098d462fb98650e8b69ac Mon Sep 17 00:00:00 2001 From: barisgit Date: Fri, 1 Aug 2025 18:50:45 +0200 Subject: [PATCH 015/228] Update example import csv to schow real capatibilities --- .../usage/import_export/part_import_example.csv | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/assets/usage/import_export/part_import_example.csv b/docs/assets/usage/import_export/part_import_example.csv index 08701426..14d4500f 100644 --- a/docs/assets/usage/import_export/part_import_example.csv +++ b/docs/assets/usage/import_export/part_import_example.csv @@ -1,4 +1,7 @@ -name;description;category;notes;footprint;tags;quantity;storage_location;mass;ipn;mpn;manufacturing_status;manufacturer;supplier;spn;price;favorite;needs_review;minamount;partUnit;manufacturing_status -BC547;NPN transistor;Transistors -> NPN;very important notes;TO -> TO-92;NPN,Transistor;5;Room 1 -> Shelf 1 -> Box 2;10;;;Manufacturer;;You need to fill this line, to use spn and price;BC547C;2,3;0;;;; -BC557;PNP transistor;HTML;;TO -> TO-92;PNP,Transistor;10;Room 2-> Box 3;;Internal1234;;;;;;;;1;;;active -Copper Wire;;Wire;;;;;;;;;;;;;;;;;Meter; \ No newline at end of file +name;description;category;notes;footprint;tags;quantity;storage_location;mass;ipn;mpn;manufacturing_status;manufacturer;supplier;spn;price;favorite;needs_review;minamount;partUnit;eda_info.reference_prefix;eda_info.value;eda_info.visibility;eda_info.exclude_from_bom;eda_info.exclude_from_board;eda_info.exclude_from_sim;eda_info.kicad_symbol;eda_info.kicad_footprint +"MLCC; 0603; 0.22uF";Multilayer ceramic capacitor;Electrical Components->Passive Components->Capacitors_SMD;High quality MLCC;0603;Capacitor,SMD,MLCC,0603;500;Room 1->Shelf 1->Box 2;0.1;CL10B224KO8NNNC;CL10B224KO8NNNC;active;Samsung;LCSC;C160828;0.0023;0;0;1;pcs;C;0.22uF;1;0;0;0;Device:C;Capacitor_SMD:C_0603_1608Metric +"MLCC; 0402; 10pF";Small MLCC for high frequency;Electrical Components->Passive Components->Capacitors_SMD;;0402;Capacitor,SMD,MLCC,0402;500;Room 1->Shelf 1->Box 3;0.05;FCC0402N100J500AT;FCC0402N100J500AT;active;Fenghua;LCSC;C5137557;0.0015;0;0;1;pcs;C;10pF;1;0;0;0;Device:C;Capacitor_SMD:C_0402_1005Metric +"Diode; 1N4148W";Fast switching diode;Electrical Components->Semiconductors->Diodes;Fast recovery time;Diode_SMD:D_SOD-123;Diode,SMD,Schottky;100;Room 2->Box 1;0.2;1N4148W;1N4148W;active;Vishay;LCSC;C917030;0.008;0;0;1;pcs;D;1N4148W;1;0;0;0;Device:D;Diode_SMD:D_SOD-123 +BC547;NPN transistor;Transistors->NPN;very important notes;TO->TO-92;NPN,Transistor;5;Room 1->Shelf 1->Box 2;10;BC547;BC547;active;Generic;LCSC;BC547C;2.3;0;0;1;pcs;Q;BC547;1;0;0;0;Device:Q_NPN_EBC;TO_SOT_Packages_SMD:TO-92_HandSolder +BC557;PNP transistor;Transistors->PNP;PNP complement to BC547;TO->TO-92;PNP,Transistor;10;Room 2->Box 3;10;BC557;BC557;active;Generic;LCSC;BC557C;2.1;0;0;1;pcs;Q;BC557;1;0;0;0;Device:Q_PNP_EBC;TO_SOT_Packages_SMD:TO-92_HandSolder +Copper Wire;Bare copper wire;Wire->Copper;For prototyping;Wire;Wire,Copper;50;Room 3->Spool Rack;0.5;CW-22AWG;CW-22AWG;active;Generic;Local Supplier;LS-CW-22;0.15;0;0;1;Meter;W;22AWG;1;0;0;0;Device:Wire;Connector_PinHeader_2.54mm:PinHeader_1x01_P2.54mm_Vertical From c5751b2aa65b65f94065e7bec50ef4f50c8d4d14 Mon Sep 17 00:00:00 2001 From: barisgit Date: Fri, 1 Aug 2025 19:14:17 +0200 Subject: [PATCH 016/228] Fix timestamp test --- .../TimestampableElementProviderTest.php | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/tests/Services/LabelSystem/PlaceholderProviders/TimestampableElementProviderTest.php b/tests/Services/LabelSystem/PlaceholderProviders/TimestampableElementProviderTest.php index a72f06df..af30b734 100644 --- a/tests/Services/LabelSystem/PlaceholderProviders/TimestampableElementProviderTest.php +++ b/tests/Services/LabelSystem/PlaceholderProviders/TimestampableElementProviderTest.php @@ -60,26 +60,29 @@ class TimestampableElementProviderTest extends WebTestCase protected function setUp(): void { self::bootKernel(); - \Locale::setDefault('en'); + \Locale::setDefault('en_US'); $this->service = self::getContainer()->get(TimestampableElementProvider::class); - $this->target = new class() implements TimeStampableInterface { + $this->target = new class () implements TimeStampableInterface { public function getLastModified(): ?DateTime { - return new \DateTime('2000-01-01'); + return new DateTime('2000-01-01'); } public function getAddedDate(): ?DateTime { - return new \DateTime('2000-01-01'); + return new DateTime('2000-01-01'); } }; } public static function dataProvider(): \Iterator { - \Locale::setDefault('en'); - yield ['1/1/00, 12:00 AM', '[[LAST_MODIFIED]]']; - yield ['1/1/00, 12:00 AM', '[[CREATION_DATE]]']; + \Locale::setDefault('en_US'); + // Use IntlDateFormatter like the actual service does + $formatter = new \IntlDateFormatter(\Locale::getDefault(), \IntlDateFormatter::SHORT, \IntlDateFormatter::SHORT); + $expectedFormat = $formatter->format(new DateTime('2000-01-01')); + yield [$expectedFormat, '[[LAST_MODIFIED]]']; + yield [$expectedFormat, '[[CREATION_DATE]]']; } #[DataProvider('dataProvider')] From facfb37383b6524f2283f5f4babd539a49820e3e Mon Sep 17 00:00:00 2001 From: barisgit Date: Fri, 1 Aug 2025 19:32:49 +0200 Subject: [PATCH 017/228] Implement excel based import/export --- composer.json | 27 +- composer.lock | 373 +++++++++++++++++- src/Form/AdminPages/ImportType.php | 2 + .../ImportExportSystem/EntityExporter.php | 83 +++- .../ImportExportSystem/EntityImporter.php | 139 ++++++- .../ImportExportSystem/EntityExporterTest.php | 34 ++ .../ImportExportSystem/EntityImporterTest.php | 44 +++ 7 files changed, 690 insertions(+), 12 deletions(-) diff --git a/composer.json b/composer.json index 8e3d1194..4f5891bc 100644 --- a/composer.json +++ b/composer.json @@ -117,9 +117,29 @@ "symfony/stopwatch": "7.3.*", "symfony/web-profiler-bundle": "7.3.*" }, - "suggest": { - "ext-bcmath": "Used to improve price calculation performance", - "ext-gmp": "Used to improve price calculation performanice" + "sort-packages": true, + "allow-plugins": { + "composer/package-versions-deprecated": true, + "symfony/flex": true, + "phpstan/extension-installer": true, + "symfony/runtime": true, + "php-http/discovery": true + } + }, + "autoload": { + "psr-4": { + "App\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "App\\Tests\\": "tests/" + } + }, + "scripts": { + "auto-scripts": { + "cache:clear": "symfony-cmd", + "assets:install %PUBLIC_DIR%": "symfony-cmd" }, "config": { "preferred-install": { @@ -170,4 +190,5 @@ "docker": true } } + } } diff --git a/composer.lock b/composer.lock index 6b9888d7..7acebb97 100644 --- a/composer.lock +++ b/composer.lock @@ -2500,6 +2500,85 @@ ], "time": "2022-01-17T14:14:24+00:00" }, + { + "name": "composer/pcre", + "version": "3.3.2", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<1.11.10" + }, + "require-dev": { + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-strict-rules": "^1 || ^2", + "phpunit/phpunit": "^8 || ^9" + }, + "type": "library", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.3.2" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-11-12T16:29:46+00:00" + }, { "name": "daverandom/libdns", "version": "v2.1.0", @@ -6514,6 +6593,190 @@ }, "time": "2023-07-31T13:36:50+00:00" }, + { + "name": "maennchen/zipstream-php", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/maennchen/ZipStream-PHP.git", + "reference": "6187e9cc4493da94b9b63eb2315821552015fca9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/6187e9cc4493da94b9b63eb2315821552015fca9", + "reference": "6187e9cc4493da94b9b63eb2315821552015fca9", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "ext-zlib": "*", + "php-64bit": "^8.1" + }, + "require-dev": { + "ext-zip": "*", + "friendsofphp/php-cs-fixer": "^3.16", + "guzzlehttp/guzzle": "^7.5", + "mikey179/vfsstream": "^1.6", + "php-coveralls/php-coveralls": "^2.5", + "phpunit/phpunit": "^10.0", + "vimeo/psalm": "^5.0" + }, + "suggest": { + "guzzlehttp/psr7": "^2.4", + "psr/http-message": "^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "ZipStream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paul Duncan", + "email": "pabs@pablotron.org" + }, + { + "name": "Jonatan Männchen", + "email": "jonatan@maennchen.ch" + }, + { + "name": "Jesse Donat", + "email": "donatj@gmail.com" + }, + { + "name": "András Kolesár", + "email": "kolesar@kolesar.hu" + } + ], + "description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.", + "keywords": [ + "stream", + "zip" + ], + "support": { + "issues": "https://github.com/maennchen/ZipStream-PHP/issues", + "source": "https://github.com/maennchen/ZipStream-PHP/tree/3.1.1" + }, + "funding": [ + { + "url": "https://github.com/maennchen", + "type": "github" + } + ], + "time": "2024-10-10T12:33:01+00:00" + }, + { + "name": "markbaker/complex", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/MarkBaker/PHPComplex.git", + "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/95c56caa1cf5c766ad6d65b6344b807c1e8405b9", + "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-master", + "phpcompatibility/php-compatibility": "^9.3", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", + "squizlabs/php_codesniffer": "^3.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Complex\\": "classes/src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Baker", + "email": "mark@lange.demon.co.uk" + } + ], + "description": "PHP Class for working with complex numbers", + "homepage": "https://github.com/MarkBaker/PHPComplex", + "keywords": [ + "complex", + "mathematics" + ], + "support": { + "issues": "https://github.com/MarkBaker/PHPComplex/issues", + "source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.2" + }, + "time": "2022-12-06T16:21:08+00:00" + }, + { + "name": "markbaker/matrix", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/MarkBaker/PHPMatrix.git", + "reference": "728434227fe21be27ff6d86621a1b13107a2562c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/728434227fe21be27ff6d86621a1b13107a2562c", + "reference": "728434227fe21be27ff6d86621a1b13107a2562c", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-master", + "phpcompatibility/php-compatibility": "^9.3", + "phpdocumentor/phpdocumentor": "2.*", + "phploc/phploc": "^4.0", + "phpmd/phpmd": "2.*", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", + "sebastian/phpcpd": "^4.0", + "squizlabs/php_codesniffer": "^3.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Matrix\\": "classes/src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Baker", + "email": "mark@demon-angel.eu" + } + ], + "description": "PHP Class for working with matrices", + "homepage": "https://github.com/MarkBaker/PHPMatrix", + "keywords": [ + "mathematics", + "matrix", + "vector" + ], + "support": { + "issues": "https://github.com/MarkBaker/PHPMatrix/issues", + "source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.1" + }, + "time": "2022-12-02T22:17:43+00:00" + }, { "name": "masterminds/html5", "version": "2.10.0", @@ -8034,6 +8297,112 @@ }, "time": "2024-11-09T15:12:26+00:00" }, + { + "name": "phpoffice/phpspreadsheet", + "version": "4.5.0", + "source": { + "type": "git", + "url": "https://github.com/PHPOffice/PhpSpreadsheet.git", + "reference": "2ea9786632e6fac1aee601b6e426bcc723d8ce13" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/2ea9786632e6fac1aee601b6e426bcc723d8ce13", + "reference": "2ea9786632e6fac1aee601b6e426bcc723d8ce13", + "shasum": "" + }, + "require": { + "composer/pcre": "^1||^2||^3", + "ext-ctype": "*", + "ext-dom": "*", + "ext-fileinfo": "*", + "ext-gd": "*", + "ext-iconv": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-simplexml": "*", + "ext-xml": "*", + "ext-xmlreader": "*", + "ext-xmlwriter": "*", + "ext-zip": "*", + "ext-zlib": "*", + "maennchen/zipstream-php": "^2.1 || ^3.0", + "markbaker/complex": "^3.0", + "markbaker/matrix": "^3.0", + "php": "^8.1", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/simple-cache": "^1.0 || ^2.0 || ^3.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-main", + "dompdf/dompdf": "^2.0 || ^3.0", + "friendsofphp/php-cs-fixer": "^3.2", + "mitoteam/jpgraph": "^10.3", + "mpdf/mpdf": "^8.1.1", + "phpcompatibility/php-compatibility": "^9.3", + "phpstan/phpstan": "^1.1 || ^2.0", + "phpstan/phpstan-deprecation-rules": "^1.0 || ^2.0", + "phpstan/phpstan-phpunit": "^1.0 || ^2.0", + "phpunit/phpunit": "^10.5", + "squizlabs/php_codesniffer": "^3.7", + "tecnickcom/tcpdf": "^6.5" + }, + "suggest": { + "dompdf/dompdf": "Option for rendering PDF with PDF Writer", + "ext-intl": "PHP Internationalization Functions", + "mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers", + "mpdf/mpdf": "Option for rendering PDF with PDF Writer", + "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer" + }, + "type": "library", + "autoload": { + "psr-4": { + "PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Maarten Balliauw", + "homepage": "https://blog.maartenballiauw.be" + }, + { + "name": "Mark Baker", + "homepage": "https://markbakeruk.net" + }, + { + "name": "Franck Lefevre", + "homepage": "https://rootslabs.net" + }, + { + "name": "Erik Tilt" + }, + { + "name": "Adrien Crivelli" + } + ], + "description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine", + "homepage": "https://github.com/PHPOffice/PhpSpreadsheet", + "keywords": [ + "OpenXML", + "excel", + "gnumeric", + "ods", + "php", + "spreadsheet", + "xls", + "xlsx" + ], + "support": { + "issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues", + "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/4.5.0" + }, + "time": "2025-07-24T05:15:59+00:00" + }, { "name": "phpstan/phpdoc-parser", "version": "2.3.0", @@ -20974,9 +21343,9 @@ "ext-json": "*", "ext-mbstring": "*" }, - "platform-dev": [], + "platform-dev": {}, "platform-overrides": { "php": "8.2.0" }, - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/src/Form/AdminPages/ImportType.php b/src/Form/AdminPages/ImportType.php index 3e87812c..0bd3cea1 100644 --- a/src/Form/AdminPages/ImportType.php +++ b/src/Form/AdminPages/ImportType.php @@ -59,6 +59,8 @@ class ImportType extends AbstractType 'XML' => 'xml', 'CSV' => 'csv', 'YAML' => 'yaml', + 'XLSX' => 'xlsx', + 'XLS' => 'xls', ], 'label' => 'export.format', 'disabled' => $disabled, diff --git a/src/Services/ImportExportSystem/EntityExporter.php b/src/Services/ImportExportSystem/EntityExporter.php index 271642da..6c0cdd04 100644 --- a/src/Services/ImportExportSystem/EntityExporter.php +++ b/src/Services/ImportExportSystem/EntityExporter.php @@ -38,6 +38,9 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\ResponseHeaderBag; use Symfony\Component\Serializer\SerializerInterface; use function Symfony\Component\String\u; +use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Writer\Xlsx; +use PhpOffice\PhpSpreadsheet\Writer\Xls; /** * Use this class to export an entity to multiple file formats. @@ -52,7 +55,7 @@ class EntityExporter protected function configureOptions(OptionsResolver $resolver): void { $resolver->setDefault('format', 'csv'); - $resolver->setAllowedValues('format', ['csv', 'json', 'xml', 'yaml']); + $resolver->setAllowedValues('format', ['csv', 'json', 'xml', 'yaml', 'xlsx', 'xls']); $resolver->setDefault('csv_delimiter', ';'); $resolver->setAllowedTypes('csv_delimiter', 'string'); @@ -88,6 +91,11 @@ class EntityExporter $options = $resolver->resolve($options); + //Handle Excel formats by converting from CSV + if (in_array($options['format'], ['xlsx', 'xls'])) { + return $this->exportToExcel($entities, $options); + } + //If include children is set, then we need to add the include_children group $groups = [$options['level']]; if ($options['include_children']) { @@ -122,6 +130,73 @@ class EntityExporter throw new CircularReferenceException('Circular reference detected for object of type '.get_class($object)); } + /** + * Exports entities to Excel format (xlsx or xls). + * + * @param AbstractNamedDBElement[] $entities The entities to export + * @param array $options The export options + * + * @return string The Excel file content as binary string + */ + protected function exportToExcel(array $entities, array $options): string + { + //First get CSV data using existing serializer + $csvOptions = $options; + $csvOptions['format'] = 'csv'; + $groups = [$options['level']]; + if ($options['include_children']) { + $groups[] = 'include_children'; + } + + $csvData = $this->serializer->serialize($entities, 'csv', + [ + 'groups' => $groups, + 'as_collection' => true, + 'csv_delimiter' => $options['csv_delimiter'], + 'partdb_export' => true, + SkippableItemNormalizer::DISABLE_ITEM_NORMALIZER => true, + AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER => $this->handleCircularReference(...), + ] + ); + + //Convert CSV to Excel + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + + $rows = explode("\n", $csvData); + $rowIndex = 1; + + foreach ($rows as $row) { + if (trim($row) === '') { + continue; + } + + $columns = str_getcsv($row, $options['csv_delimiter']); + $colIndex = 1; + + foreach ($columns as $column) { + $cellCoordinate = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($colIndex) . $rowIndex; + $worksheet->setCellValue($cellCoordinate, $column); + $colIndex++; + } + $rowIndex++; + } + + //Save to memory stream + if ($options['format'] === 'xlsx') { + $writer = new Xlsx($spreadsheet); + } else { + $writer = new Xls($spreadsheet); + } + + ob_start(); + $writer->save('php://output'); + $content = ob_get_contents(); + ob_end_clean(); + + return $content; + } + /** * Exports an Entity or an array of entities to multiple file formats. * @@ -168,6 +243,12 @@ class EntityExporter case 'json': $content_type = 'application/json'; break; + case 'xlsx': + $content_type = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; + break; + case 'xls': + $content_type = 'application/vnd.ms-excel'; + break; } $response->headers->set('Content-Type', $content_type); diff --git a/src/Services/ImportExportSystem/EntityImporter.php b/src/Services/ImportExportSystem/EntityImporter.php index 11915cfb..a36dc2be 100644 --- a/src/Services/ImportExportSystem/EntityImporter.php +++ b/src/Services/ImportExportSystem/EntityImporter.php @@ -38,6 +38,9 @@ use Symfony\Component\HttpFoundation\File\File; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Serializer\SerializerInterface; use Symfony\Component\Validator\Validator\ValidatorInterface; +use PhpOffice\PhpSpreadsheet\IOFactory; +use PhpOffice\PhpSpreadsheet\Spreadsheet; +use Psr\Log\LoggerInterface; /** * @see \App\Tests\Services\ImportExportSystem\EntityImporterTest @@ -50,7 +53,7 @@ class EntityImporter */ private const ENCODINGS = ["ASCII", "UTF-8", "ISO-8859-1", "ISO-8859-15", "Windows-1252", "UTF-16", "UTF-32"]; - public function __construct(protected SerializerInterface $serializer, protected EntityManagerInterface $em, protected ValidatorInterface $validator) + public function __construct(protected SerializerInterface $serializer, protected EntityManagerInterface $em, protected ValidatorInterface $validator, protected LoggerInterface $logger) { } @@ -102,7 +105,7 @@ class EntityImporter foreach ($names as $name) { //Count indentation level (whitespace characters at the beginning of the line) - $identSize = strlen($name)-strlen(ltrim($name)); + $identSize = strlen($name) - strlen(ltrim($name)); //If the line is intended more than the last line, we have a new parent element if ($identSize > end($indentations)) { @@ -195,16 +198,20 @@ class EntityImporter } //The [] behind class_name denotes that we expect an array. - $entities = $this->serializer->deserialize($data, $options['class'].'[]', $options['format'], + $entities = $this->serializer->deserialize( + $data, + $options['class'] . '[]', + $options['format'], [ 'groups' => $groups, 'csv_delimiter' => $options['csv_delimiter'], 'create_unknown_datastructures' => $options['create_unknown_datastructures'], 'path_delimiter' => $options['path_delimiter'], 'partdb_import' => true, - //Disable API Platform normalizer, as we don't want to use it here + //Disable API Platform normalizer, as we don't want to use it here SkippableItemNormalizer::DISABLE_ITEM_NORMALIZER => true, - ]); + ] + ); //Ensure we have an array of entity elements. if (!is_array($entities)) { @@ -279,7 +286,7 @@ class EntityImporter 'path_delimiter' => '->', //The delimiter used to separate the path elements in the name of a structural element ]); - $resolver->setAllowedValues('format', ['csv', 'json', 'xml', 'yaml']); + $resolver->setAllowedValues('format', ['csv', 'json', 'xml', 'yaml', 'xlsx', 'xls']); $resolver->setAllowedTypes('csv_delimiter', 'string'); $resolver->setAllowedTypes('preserve_children', 'bool'); $resolver->setAllowedTypes('class', 'string'); @@ -335,6 +342,33 @@ class EntityImporter */ public function importFile(File $file, array $options = [], array &$errors = []): array { + $resolver = new OptionsResolver(); + $this->configureOptions($resolver); + $options = $resolver->resolve($options); + + if (in_array($options['format'], ['xlsx', 'xls'])) { + $this->logger->info('Converting Excel file to CSV', [ + 'filename' => $file->getFilename(), + 'format' => $options['format'], + 'delimiter' => $options['csv_delimiter'] + ]); + + $csvData = $this->convertExcelToCsv($file, $options['csv_delimiter']); + $options['format'] = 'csv'; + + $this->logger->debug('Excel to CSV conversion completed', [ + 'csv_length' => strlen($csvData), + 'csv_lines' => substr_count($csvData, "\n") + 1 + ]); + + // Log the converted CSV for debugging (first 1000 characters) + $this->logger->debug('Converted CSV preview', [ + 'csv_preview' => substr($csvData, 0, 1000) . (strlen($csvData) > 1000 ? '...' : '') + ]); + + return $this->importString($csvData, $options, $errors); + } + return $this->importString($file->getContent(), $options, $errors); } @@ -354,10 +388,103 @@ class EntityImporter 'xml' => 'xml', 'csv', 'tsv' => 'csv', 'yaml', 'yml' => 'yaml', + 'xlsx' => 'xlsx', + 'xls' => 'xls', default => null, }; } + /** + * Converts Excel file to CSV format using PhpSpreadsheet. + * + * @param File $file The Excel file to convert + * @param string $delimiter The CSV delimiter to use + * + * @return string The CSV data as string + */ + protected function convertExcelToCsv(File $file, string $delimiter = ';'): string + { + try { + $this->logger->debug('Loading Excel file', ['path' => $file->getPathname()]); + $spreadsheet = IOFactory::load($file->getPathname()); + $worksheet = $spreadsheet->getActiveSheet(); + + $csvData = []; + $highestRow = $worksheet->getHighestRow(); + $highestColumn = $worksheet->getHighestColumn(); + + $this->logger->debug('Excel file dimensions', [ + 'rows' => $highestRow, + 'columns_detected' => $highestColumn, + 'worksheet_title' => $worksheet->getTitle() + ]); + + $highestColumnIndex = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($highestColumn); + + for ($row = 1; $row <= $highestRow; $row++) { + $rowData = []; + + // Read all columns using numeric index + for ($colIndex = 1; $colIndex <= $highestColumnIndex; $colIndex++) { + $col = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($colIndex); + try { + $cellValue = $worksheet->getCell("{$col}{$row}")->getCalculatedValue(); + $rowData[] = $cellValue ?? ''; + + } catch (\Exception $e) { + $this->logger->warning('Error reading cell value', [ + 'cell' => "{$col}{$row}", + 'error' => $e->getMessage() + ]); + $rowData[] = ''; + } + } + + $csvRow = implode($delimiter, array_map(function ($value) use ($delimiter) { + $value = (string) $value; + if (strpos($value, $delimiter) !== false || strpos($value, '"') !== false || strpos($value, "\n") !== false) { + return '"' . str_replace('"', '""', $value) . '"'; + } + return $value; + }, $rowData)); + + $csvData[] = $csvRow; + + // Log first few rows for debugging + if ($row <= 3) { + $this->logger->debug("Row {$row} converted", [ + 'original_data' => $rowData, + 'csv_row' => $csvRow, + 'first_cell_raw' => $worksheet->getCell("A{$row}")->getValue(), + 'first_cell_calculated' => $worksheet->getCell("A{$row}")->getCalculatedValue() + ]); + } + } + + $result = implode("\n", $csvData); + + $this->logger->info('Excel to CSV conversion successful', [ + 'total_rows' => count($csvData), + 'total_characters' => strlen($result) + ]); + + $this->logger->debug('Full CSV data', [ + 'csv_data' => $result + ]); + + return $result; + + } catch (\Exception $e) { + $this->logger->error('Failed to convert Excel to CSV', [ + 'file' => $file->getFilename(), + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + throw $e; + } + } + + /** * This functions corrects the parent setting based on the children value of the parent. * diff --git a/tests/Services/ImportExportSystem/EntityExporterTest.php b/tests/Services/ImportExportSystem/EntityExporterTest.php index 004971ab..e9b924b1 100644 --- a/tests/Services/ImportExportSystem/EntityExporterTest.php +++ b/tests/Services/ImportExportSystem/EntityExporterTest.php @@ -26,6 +26,7 @@ use App\Entity\Parts\Category; use App\Services\ImportExportSystem\EntityExporter; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; use Symfony\Component\HttpFoundation\Request; +use PhpOffice\PhpSpreadsheet\IOFactory; class EntityExporterTest extends WebTestCase { @@ -76,7 +77,40 @@ class EntityExporterTest extends WebTestCase $this->assertSame('application/json', $response->headers->get('Content-Type')); $this->assertNotEmpty($response->headers->get('Content-Disposition')); + } + public function testExportToExcel(): void + { + $entities = $this->getEntities(); + $xlsxData = $this->service->exportEntities($entities, ['format' => 'xlsx', 'level' => 'simple']); + $this->assertNotEmpty($xlsxData); + + $tempFile = tempnam(sys_get_temp_dir(), 'test_export') . '.xlsx'; + file_put_contents($tempFile, $xlsxData); + + $spreadsheet = IOFactory::load($tempFile); + $worksheet = $spreadsheet->getActiveSheet(); + + $this->assertSame('name', $worksheet->getCell('A1')->getValue()); + $this->assertSame('full_name', $worksheet->getCell('B1')->getValue()); + + $this->assertSame('Enitity 1', $worksheet->getCell('A2')->getValue()); + $this->assertSame('Enitity 1', $worksheet->getCell('B2')->getValue()); + + unlink($tempFile); + } + + public function testExportExcelFromRequest(): void + { + $entities = $this->getEntities(); + + $request = new Request(); + $request->request->set('format', 'xlsx'); + $request->request->set('level', 'simple'); + $response = $this->service->exportEntityFromRequest($entities, $request); + + $this->assertSame('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', $response->headers->get('Content-Type')); + $this->assertStringContainsString('export_Category_simple.xlsx', $response->headers->get('Content-Disposition')); } } diff --git a/tests/Services/ImportExportSystem/EntityImporterTest.php b/tests/Services/ImportExportSystem/EntityImporterTest.php index fd5e8b9e..83367f80 100644 --- a/tests/Services/ImportExportSystem/EntityImporterTest.php +++ b/tests/Services/ImportExportSystem/EntityImporterTest.php @@ -36,6 +36,9 @@ use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; use Symfony\Component\Validator\ConstraintViolation; use Symfony\Component\Validator\ConstraintViolationListInterface; +use Symfony\Component\HttpFoundation\File\File; +use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Writer\Xlsx; #[Group('DB')] class EntityImporterTest extends WebTestCase @@ -207,6 +210,10 @@ EOT; yield ['json', 'json']; yield ['yaml', 'yml']; yield ['yaml', 'YAML']; + yield ['xlsx', 'xlsx']; + yield ['xlsx', 'XLSX']; + yield ['xls', 'xls']; + yield ['xls', 'XLS']; } #[DataProvider('formatDataProvider')] @@ -342,4 +349,41 @@ EOT; $this->assertSame($category, $results[0]->getCategory()); $this->assertSame('test,test2', $results[0]->getTags()); } + + public function testImportExcelFileProjects(): void + { + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + + $worksheet->setCellValue('A1', 'name'); + $worksheet->setCellValue('B1', 'comment'); + $worksheet->setCellValue('A2', 'Test Excel 1'); + $worksheet->setCellValue('B2', 'Test Excel 1 notes'); + $worksheet->setCellValue('A3', 'Test Excel 2'); + $worksheet->setCellValue('B3', 'Test Excel 2 notes'); + + $tempFile = tempnam(sys_get_temp_dir(), 'test_excel') . '.xlsx'; + $writer = new Xlsx($spreadsheet); + $writer->save($tempFile); + + $file = new File($tempFile); + + $errors = []; + $results = $this->service->importFile($file, [ + 'class' => Project::class, + 'format' => 'xlsx', + 'csv_delimiter' => ';', + ], $errors); + + $this->assertCount(2, $results); + $this->assertEmpty($errors); + $this->assertContainsOnlyInstancesOf(Project::class, $results); + + $this->assertSame('Test Excel 1', $results[0]->getName()); + $this->assertSame('Test Excel 1 notes', $results[0]->getComment()); + $this->assertSame('Test Excel 2', $results[1]->getName()); + $this->assertSame('Test Excel 2 notes', $results[1]->getComment()); + + unlink($tempFile); + } } From 1fb137e89ff0d8514d81b0c1a16e743f34c00fc1 Mon Sep 17 00:00:00 2001 From: barisgit Date: Fri, 1 Aug 2025 23:12:08 +0200 Subject: [PATCH 018/228] Add export functionality to batch select and fix errors --- src/Services/ImportExportSystem/EntityExporter.php | 2 +- src/Services/ImportExportSystem/EntityImporter.php | 2 +- src/Services/Parts/PartsTableActionHandler.php | 4 +--- templates/components/datatables.macro.html.twig | 1 + translations/messages.en.xlf | 6 ++++++ 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Services/ImportExportSystem/EntityExporter.php b/src/Services/ImportExportSystem/EntityExporter.php index 6c0cdd04..6786e8a1 100644 --- a/src/Services/ImportExportSystem/EntityExporter.php +++ b/src/Services/ImportExportSystem/EntityExporter.php @@ -92,7 +92,7 @@ class EntityExporter $options = $resolver->resolve($options); //Handle Excel formats by converting from CSV - if (in_array($options['format'], ['xlsx', 'xls'])) { + if (in_array($options['format'], ['xlsx', 'xls'], true)) { return $this->exportToExcel($entities, $options); } diff --git a/src/Services/ImportExportSystem/EntityImporter.php b/src/Services/ImportExportSystem/EntityImporter.php index a36dc2be..459866ba 100644 --- a/src/Services/ImportExportSystem/EntityImporter.php +++ b/src/Services/ImportExportSystem/EntityImporter.php @@ -346,7 +346,7 @@ class EntityImporter $this->configureOptions($resolver); $options = $resolver->resolve($options); - if (in_array($options['format'], ['xlsx', 'xls'])) { + if (in_array($options['format'], ['xlsx', 'xls'], true)) { $this->logger->info('Converting Excel file to CSV', [ 'filename' => $file->getFilename(), 'format' => $options['format'], diff --git a/src/Services/Parts/PartsTableActionHandler.php b/src/Services/Parts/PartsTableActionHandler.php index 616df229..bb8ab45f 100644 --- a/src/Services/Parts/PartsTableActionHandler.php +++ b/src/Services/Parts/PartsTableActionHandler.php @@ -30,13 +30,11 @@ use App\Entity\Parts\Manufacturer; use App\Entity\Parts\MeasurementUnit; use App\Entity\Parts\Part; use App\Entity\Parts\PartLot; -use App\Repository\PartRepository; use Doctrine\ORM\EntityManagerInterface; use InvalidArgumentException; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Security\Core\Exception\AccessDeniedException; -use Symfony\Contracts\Translation\TranslatableInterface; use function Symfony\Component\Translation\t; @@ -100,7 +98,7 @@ implode(',', array_map(static fn (PartLot $lot) => $lot->getID(), $part->getPart //When action starts with "export_" we have to redirect to the export controller $matches = []; - if (preg_match('/^export_(json|yaml|xml|csv)$/', $action, $matches)) { + if (preg_match('/^export_(json|yaml|xml|csv|xlsx)$/', $action, $matches)) { $ids = implode(',', array_map(static fn (Part $part) => $part->getID(), $selected_parts)); $level = match ($target_id) { 2 => 'extended', diff --git a/templates/components/datatables.macro.html.twig b/templates/components/datatables.macro.html.twig index 5ce0f23f..5e1747e3 100644 --- a/templates/components/datatables.macro.html.twig +++ b/templates/components/datatables.macro.html.twig @@ -72,6 +72,7 @@ + diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index e65445ce..1d3d4595 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -10906,6 +10906,12 @@ Element 1 -> Element 1.2]]> Export to XML + + + part_list.action.export_xlsx + Export to Excel + + parts.import.title From 78885ec3c5270278d13cd082ecdcabdc9b764005 Mon Sep 17 00:00:00 2001 From: barisgit Date: Sat, 2 Aug 2025 10:00:04 +0200 Subject: [PATCH 019/228] Add more tests and fix failing ones --- .../ImportExportSystem/EntityExporter.php | 62 +++++++--------- .../Parts/PartsTableActionHandlerTest.php | 70 +++++++++++++++++++ 2 files changed, 96 insertions(+), 36 deletions(-) create mode 100644 tests/Services/Parts/PartsTableActionHandlerTest.php diff --git a/src/Services/ImportExportSystem/EntityExporter.php b/src/Services/ImportExportSystem/EntityExporter.php index 6786e8a1..5a3d66bd 100644 --- a/src/Services/ImportExportSystem/EntityExporter.php +++ b/src/Services/ImportExportSystem/EntityExporter.php @@ -102,22 +102,24 @@ class EntityExporter $groups[] = 'include_children'; } - return $this->serializer->serialize($entities, $options['format'], + return $this->serializer->serialize( + $entities, + $options['format'], [ 'groups' => $groups, 'as_collection' => true, 'csv_delimiter' => $options['csv_delimiter'], 'xml_root_node_name' => 'PartDBExport', 'partdb_export' => true, - //Skip the item normalizer, so that we dont get IRIs in the output + //Skip the item normalizer, so that we dont get IRIs in the output SkippableItemNormalizer::DISABLE_ITEM_NORMALIZER => true, - //Handle circular references + //Handle circular references AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER => $this->handleCircularReference(...), ] ); } - private function handleCircularReference(object $object, string $format, array $context): string + private function handleCircularReference(object $object): string { if ($object instanceof AbstractStructuralDBElement) { return $object->getFullPath("->"); @@ -127,7 +129,7 @@ class EntityExporter return $object->__toString(); } - throw new CircularReferenceException('Circular reference detected for object of type '.get_class($object)); + throw new CircularReferenceException('Circular reference detected for object of type ' . get_class($object)); } /** @@ -148,7 +150,9 @@ class EntityExporter $groups[] = 'include_children'; } - $csvData = $this->serializer->serialize($entities, 'csv', + $csvData = $this->serializer->serialize( + $entities, + 'csv', [ 'groups' => $groups, 'as_collection' => true, @@ -162,18 +166,18 @@ class EntityExporter //Convert CSV to Excel $spreadsheet = new Spreadsheet(); $worksheet = $spreadsheet->getActiveSheet(); - + $rows = explode("\n", $csvData); $rowIndex = 1; - + foreach ($rows as $row) { if (trim($row) === '') { continue; } - - $columns = str_getcsv($row, $options['csv_delimiter']); + + $columns = str_getcsv($row, $options['csv_delimiter'], '"', '\\'); $colIndex = 1; - + foreach ($columns as $column) { $cellCoordinate = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($colIndex) . $rowIndex; $worksheet->setCellValue($cellCoordinate, $column); @@ -183,17 +187,13 @@ class EntityExporter } //Save to memory stream - if ($options['format'] === 'xlsx') { - $writer = new Xlsx($spreadsheet); - } else { - $writer = new Xls($spreadsheet); - } - + $writer = $options['format'] === 'xlsx' ? new Xlsx($spreadsheet) : new Xls($spreadsheet); + ob_start(); $writer->save('php://output'); $content = ob_get_contents(); ob_end_clean(); - + return $content; } @@ -231,25 +231,15 @@ class EntityExporter //Determine the content type for the response - //Plain text should work for all types - $content_type = 'text/plain'; - //Try to use better content types based on the format $format = $options['format']; - switch ($format) { - case 'xml': - $content_type = 'application/xml'; - break; - case 'json': - $content_type = 'application/json'; - break; - case 'xlsx': - $content_type = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; - break; - case 'xls': - $content_type = 'application/vnd.ms-excel'; - break; - } + $content_type = match ($format) { + 'xml' => 'application/xml', + 'json' => 'application/json', + 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'xls' => 'application/vnd.ms-excel', + default => 'text/plain', + }; $response->headers->set('Content-Type', $content_type); //If view option is not specified, then download the file. @@ -267,7 +257,7 @@ class EntityExporter $level = $options['level']; - $filename = 'export_'.$entity_name.'_'.$level.'.'.$format; + $filename = "export_{$entity_name}_{$level}.{$format}"; //Sanitize the filename $filename = FilenameSanatizer::sanitizeFilename($filename); diff --git a/tests/Services/Parts/PartsTableActionHandlerTest.php b/tests/Services/Parts/PartsTableActionHandlerTest.php new file mode 100644 index 00000000..f157420c --- /dev/null +++ b/tests/Services/Parts/PartsTableActionHandlerTest.php @@ -0,0 +1,70 @@ +. + */ +namespace App\Tests\Services\Parts; + +use App\Entity\Parts\Part; +use App\Services\Parts\PartsTableActionHandler; +use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; +use Symfony\Component\HttpFoundation\RedirectResponse; + +class PartsTableActionHandlerTest extends WebTestCase +{ + private PartsTableActionHandler $service; + + protected function setUp(): void + { + self::bootKernel(); + $this->service = self::getContainer()->get(PartsTableActionHandler::class); + } + + public function testExportActionsRedirectToExportController(): void + { + // Mock a Part entity with required properties + $part = $this->createMock(Part::class); + $part->method('getId')->willReturn(1); + $part->method('getName')->willReturn('Test Part'); + + $selected_parts = [$part]; + + // Test each export format, focusing on our new xlsx format + $formats = ['json', 'csv', 'xml', 'yaml', 'xlsx']; + + foreach ($formats as $format) { + $action = "export_{$format}"; + $result = $this->service->handleAction($action, $selected_parts, 1, '/test'); + + $this->assertInstanceOf(RedirectResponse::class, $result); + $this->assertStringContainsString('parts/export', $result->getTargetUrl()); + $this->assertStringContainsString("format={$format}", $result->getTargetUrl()); + } + } + + public function testIdStringToArray(): void + { + // This test would require actual Part entities in the database + // For now, we just test the method exists and handles empty strings + $result = $this->service->idStringToArray(''); + $this->assertIsArray($result); + $this->assertEmpty($result); + } +} \ No newline at end of file From aa29f10d5139e8bcdcb1cf7f5e1c8518a4817b68 Mon Sep 17 00:00:00 2001 From: barisgit Date: Sat, 2 Aug 2025 10:05:55 +0200 Subject: [PATCH 020/228] Remove problematic tests --- src/Services/ImportExportSystem/EntityExporter.php | 2 -- tests/Services/Parts/PartsTableActionHandlerTest.php | 8 -------- 2 files changed, 10 deletions(-) diff --git a/src/Services/ImportExportSystem/EntityExporter.php b/src/Services/ImportExportSystem/EntityExporter.php index 5a3d66bd..5b6765f6 100644 --- a/src/Services/ImportExportSystem/EntityExporter.php +++ b/src/Services/ImportExportSystem/EntityExporter.php @@ -143,8 +143,6 @@ class EntityExporter protected function exportToExcel(array $entities, array $options): string { //First get CSV data using existing serializer - $csvOptions = $options; - $csvOptions['format'] = 'csv'; $groups = [$options['level']]; if ($options['include_children']) { $groups[] = 'include_children'; diff --git a/tests/Services/Parts/PartsTableActionHandlerTest.php b/tests/Services/Parts/PartsTableActionHandlerTest.php index f157420c..c5105cd7 100644 --- a/tests/Services/Parts/PartsTableActionHandlerTest.php +++ b/tests/Services/Parts/PartsTableActionHandlerTest.php @@ -59,12 +59,4 @@ class PartsTableActionHandlerTest extends WebTestCase } } - public function testIdStringToArray(): void - { - // This test would require actual Part entities in the database - // For now, we just test the method exists and handles empty strings - $result = $this->service->idStringToArray(''); - $this->assertIsArray($result); - $this->assertEmpty($result); - } } \ No newline at end of file From 4c8940f9c31e08be7181b9764b85483f3c157371 Mon Sep 17 00:00:00 2001 From: barisgit Date: Sat, 2 Aug 2025 17:56:46 +0200 Subject: [PATCH 021/228] Simple batch processing --- .../BulkInfoProviderImportController.php | 210 +++++++++++ .../BulkProviderSearchType.php | 68 ++++ .../FieldToProviderMappingType.php | 58 +++ .../GlobalFieldMappingType.php | 60 ++++ .../PartProviderConfigurationType.php | 55 +++ .../Parts/PartsTableActionHandler.php | 10 + .../components/datatables.macro.html.twig | 5 +- .../bulk_import/step1.html.twig | 339 ++++++++++++++++++ translations/messages.en.xlf | 146 +++++++- 9 files changed, 949 insertions(+), 2 deletions(-) create mode 100644 src/Controller/BulkInfoProviderImportController.php create mode 100644 src/Form/InfoProviderSystem/BulkProviderSearchType.php create mode 100644 src/Form/InfoProviderSystem/FieldToProviderMappingType.php create mode 100644 src/Form/InfoProviderSystem/GlobalFieldMappingType.php create mode 100644 src/Form/InfoProviderSystem/PartProviderConfigurationType.php create mode 100644 templates/info_providers/bulk_import/step1.html.twig diff --git a/src/Controller/BulkInfoProviderImportController.php b/src/Controller/BulkInfoProviderImportController.php new file mode 100644 index 00000000..6893de93 --- /dev/null +++ b/src/Controller/BulkInfoProviderImportController.php @@ -0,0 +1,210 @@ +. + */ + +declare(strict_types=1); + +namespace App\Controller; + +use App\Entity\Parts\Part; +use App\Entity\Parts\Supplier; +use App\Form\InfoProviderSystem\GlobalFieldMappingType; +use App\Services\InfoProviderSystem\PartInfoRetriever; +use App\Services\InfoProviderSystem\ProviderRegistry; +use App\Services\InfoProviderSystem\ExistingPartFinder; +use Doctrine\ORM\EntityManagerInterface; +use Psr\Log\LoggerInterface; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpClient\Exception\ClientException; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Attribute\Route; + +use function Symfony\Component\Translation\t; + +#[Route('/tools/bulk-info-provider-import')] +class BulkInfoProviderImportController extends AbstractController +{ + public function __construct( + private readonly ProviderRegistry $providerRegistry, + private readonly PartInfoRetriever $infoRetriever, + private readonly ExistingPartFinder $existingPartFinder, + private readonly EntityManagerInterface $entityManager + ) { + } + + #[Route('/step1', name: 'bulk_info_provider_step1')] + public function step1(Request $request, LoggerInterface $exceptionLogger): Response + { + $this->denyAccessUnlessGranted('@info_providers.create_parts'); + + $ids = $request->query->get('ids'); + if (!$ids) { + $this->addFlash('error', 'No parts selected for bulk import'); + return $this->redirectToRoute('homepage'); + } + + // Get the selected parts + $partIds = explode(',', $ids); + $partRepository = $this->entityManager->getRepository(Part::class); + $parts = $partRepository->getElementsFromIDArray($partIds); + + if (empty($parts)) { + $this->addFlash('error', 'No valid parts found for bulk import'); + return $this->redirectToRoute('homepage'); + } + + // Generate field choices + $fieldChoices = [ + 'info_providers.bulk_search.field.mpn' => 'mpn', + 'info_providers.bulk_search.field.name' => 'name', + ]; + + // Add dynamic supplier fields + $suppliers = $this->entityManager->getRepository(Supplier::class)->findAll(); + foreach ($suppliers as $supplier) { + $supplierKey = strtolower(str_replace([' ', '-', '_'], '_', $supplier->getName())); + $fieldChoices["Supplier: " . $supplier->getName() . " (SPN)"] = $supplierKey . '_spn'; + } + + // Initialize form with useful default mappings + $initialData = [ + 'field_mappings' => [ + ['field' => 'mpn', 'providers' => []] + ] + ]; + + $form = $this->createForm(GlobalFieldMappingType::class, $initialData, [ + 'field_choices' => $fieldChoices + ]); + $form->handleRequest($request); + + $searchResults = null; + + if ($form->isSubmitted() && $form->isValid()) { + $fieldMappings = $form->getData()['field_mappings']; + $searchResults = []; + + foreach ($parts as $part) { + $partResult = [ + 'part' => $part, + 'search_results' => [], + 'errors' => [] + ]; + + // Collect all DTOs from all applicable field mappings + $allDtos = []; + + foreach ($fieldMappings as $mapping) { + $field = $mapping['field']; + $providers = $mapping['providers'] ?? []; + + if (empty($providers)) { + continue; + } + + $keyword = $this->getKeywordFromField($part, $field); + + if ($keyword) { + try { + $dtos = $this->infoRetriever->searchByKeyword( + keyword: $keyword, + providers: $providers + ); + + // Add field info to each DTO for tracking + foreach ($dtos as $dto) { + $dto->_source_field = $field; + $dto->_source_keyword = $keyword; + } + + $allDtos = array_merge($allDtos, $dtos); + } catch (ClientException $e) { + $partResult['errors'][] = "Error searching with {$field}: " . $e->getMessage(); + $exceptionLogger->error('Error during bulk info provider search for part ' . $part->getId() . " field {$field}: " . $e->getMessage(), ['exception' => $e]); + } + } + } + + // Remove duplicates based on provider_key + provider_id + $uniqueDtos = []; + $seenKeys = []; + foreach ($allDtos as $dto) { + $key = $dto->provider_key . '|' . $dto->provider_id; + if (!in_array($key, $seenKeys)) { + $seenKeys[] = $key; + $uniqueDtos[] = $dto; + } + } + + // Convert DTOs to result format + $partResult['search_results'] = array_map( + fn($dto) => ['dto' => $dto, 'localPart' => $this->existingPartFinder->findFirstExisting($dto)], + $uniqueDtos + ); + + $searchResults[] = $partResult; + } + } + + return $this->render('info_providers/bulk_import/step1.html.twig', [ + 'form' => $form, + 'parts' => $parts, + 'search_results' => $searchResults, + 'fieldChoices' => $fieldChoices + ]); + } + + private function getKeywordFromField(Part $part, string $field): ?string + { + return match ($field) { + 'mpn' => $part->getManufacturerProductNumber(), + 'name' => $part->getName(), + default => $this->getSupplierPartNumber($part, $field) + }; + } + + private function getSupplierPartNumber(Part $part, string $field): ?string + { + // Check if this is a supplier SPN field + if (!str_ends_with($field, '_spn')) { + return null; + } + + // Extract supplier key (remove _spn suffix) + $supplierKey = substr($field, 0, -4); + + // Get all suppliers to find matching one + $suppliers = $this->entityManager->getRepository(Supplier::class)->findAll(); + + foreach ($suppliers as $supplier) { + $normalizedName = strtolower(str_replace([' ', '-', '_'], '_', $supplier->getName())); + if ($normalizedName === $supplierKey) { + // Find order detail for this supplier + $orderDetail = $part->getOrderdetails()->filter( + fn($od) => $od->getSupplier()?->getId() === $supplier->getId() + )->first(); + + return $orderDetail ? $orderDetail->getSupplierpartnr() : null; + } + } + + return null; + } +} \ No newline at end of file diff --git a/src/Form/InfoProviderSystem/BulkProviderSearchType.php b/src/Form/InfoProviderSystem/BulkProviderSearchType.php new file mode 100644 index 00000000..5da8f53f --- /dev/null +++ b/src/Form/InfoProviderSystem/BulkProviderSearchType.php @@ -0,0 +1,68 @@ +. + */ + +declare(strict_types=1); + +namespace App\Form\InfoProviderSystem; + +use App\Entity\Parts\Part; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\Extension\Core\Type\CollectionType; +use Symfony\Component\Form\Extension\Core\Type\HiddenType; +use Symfony\Component\Form\Extension\Core\Type\SubmitType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class BulkProviderSearchType extends AbstractType +{ + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $parts = $options['parts']; + + $builder->add('part_configurations', CollectionType::class, [ + 'entry_type' => PartProviderConfigurationType::class, + 'entry_options' => [ + 'label' => false, + ], + 'allow_add' => false, + 'allow_delete' => false, + 'label' => false, + ]); + + $builder->add('submit', SubmitType::class, [ + 'label' => 'info_providers.bulk_search.submit' + ]); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'parts' => [], + ]); + $resolver->setRequired('parts'); + } + + private function getDefaultSearchField(Part $part): string + { + // Default to MPN if available, otherwise name + return $part->getManufacturerProductNumber() ? 'mpn' : 'name'; + } +} \ No newline at end of file diff --git a/src/Form/InfoProviderSystem/FieldToProviderMappingType.php b/src/Form/InfoProviderSystem/FieldToProviderMappingType.php new file mode 100644 index 00000000..20506fc8 --- /dev/null +++ b/src/Form/InfoProviderSystem/FieldToProviderMappingType.php @@ -0,0 +1,58 @@ +. + */ + +declare(strict_types=1); + +namespace App\Form\InfoProviderSystem; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class FieldToProviderMappingType extends AbstractType +{ + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $fieldChoices = $options['field_choices'] ?? []; + + $builder->add('field', ChoiceType::class, [ + 'label' => 'info_providers.bulk_search.search_field', + 'choices' => $fieldChoices, + 'expanded' => false, + 'multiple' => false, + 'required' => false, + 'placeholder' => 'info_providers.bulk_search.field.select', + ]); + + $builder->add('providers', ProviderSelectType::class, [ + 'label' => 'info_providers.bulk_search.providers', + 'help' => 'info_providers.bulk_search.providers.help', + 'required' => false, + ]); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'field_choices' => [], + ]); + } +} \ No newline at end of file diff --git a/src/Form/InfoProviderSystem/GlobalFieldMappingType.php b/src/Form/InfoProviderSystem/GlobalFieldMappingType.php new file mode 100644 index 00000000..ecc3dbc9 --- /dev/null +++ b/src/Form/InfoProviderSystem/GlobalFieldMappingType.php @@ -0,0 +1,60 @@ +. + */ + +declare(strict_types=1); + +namespace App\Form\InfoProviderSystem; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\CollectionType; +use Symfony\Component\Form\Extension\Core\Type\SubmitType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class GlobalFieldMappingType extends AbstractType +{ + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $fieldChoices = $options['field_choices'] ?? []; + + $builder->add('field_mappings', CollectionType::class, [ + 'entry_type' => FieldToProviderMappingType::class, + 'entry_options' => [ + 'label' => false, + 'field_choices' => $fieldChoices, + ], + 'allow_add' => true, + 'allow_delete' => true, + 'prototype' => true, + 'label' => false, + ]); + + $builder->add('submit', SubmitType::class, [ + 'label' => 'info_providers.bulk_search.submit' + ]); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'field_choices' => [], + ]); + } +} \ No newline at end of file diff --git a/src/Form/InfoProviderSystem/PartProviderConfigurationType.php b/src/Form/InfoProviderSystem/PartProviderConfigurationType.php new file mode 100644 index 00000000..cecf62a3 --- /dev/null +++ b/src/Form/InfoProviderSystem/PartProviderConfigurationType.php @@ -0,0 +1,55 @@ +. + */ + +declare(strict_types=1); + +namespace App\Form\InfoProviderSystem; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\Extension\Core\Type\HiddenType; +use Symfony\Component\Form\FormBuilderInterface; + +class PartProviderConfigurationType extends AbstractType +{ + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder->add('part_id', HiddenType::class); + + $builder->add('search_field', ChoiceType::class, [ + 'label' => 'info_providers.bulk_search.search_field', + 'choices' => [ + 'info_providers.bulk_search.field.mpn' => 'mpn', + 'info_providers.bulk_search.field.name' => 'name', + 'info_providers.bulk_search.field.digikey_spn' => 'digikey_spn', + 'info_providers.bulk_search.field.mouser_spn' => 'mouser_spn', + 'info_providers.bulk_search.field.lcsc_spn' => 'lcsc_spn', + 'info_providers.bulk_search.field.farnell_spn' => 'farnell_spn', + ], + 'expanded' => false, + 'multiple' => false, + ]); + + $builder->add('providers', ProviderSelectType::class, [ + 'label' => 'info_providers.bulk_search.providers', + 'help' => 'info_providers.bulk_search.providers.help', + ]); + } +} \ No newline at end of file diff --git a/src/Services/Parts/PartsTableActionHandler.php b/src/Services/Parts/PartsTableActionHandler.php index bb8ab45f..945cff7b 100644 --- a/src/Services/Parts/PartsTableActionHandler.php +++ b/src/Services/Parts/PartsTableActionHandler.php @@ -117,6 +117,16 @@ implode(',', array_map(static fn (PartLot $lot) => $lot->getID(), $part->getPart ); } + if ($action === 'bulk_info_provider_import') { + $ids = implode(',', array_map(static fn (Part $part) => $part->getID(), $selected_parts)); + return new RedirectResponse( + $this->urlGenerator->generate('bulk_info_provider_step1', [ + 'ids' => $ids, + '_redirect' => $redirect_url + ]) + ); + } + //Iterate over the parts and apply the action to it: foreach ($selected_parts as $part) { diff --git a/templates/components/datatables.macro.html.twig b/templates/components/datatables.macro.html.twig index 5e1747e3..8d7e10f7 100644 --- a/templates/components/datatables.macro.html.twig +++ b/templates/components/datatables.macro.html.twig @@ -30,7 +30,7 @@
- {# #} + {% trans %}part_list.action.scrollable_hint{% endtrans %}
+ +
+ + {% trans %}info_providers.bulk_import.editing_part{% endtrans %} +
+
+
+ + {% endif %} +{% endblock %} + {% block card_title %} {% trans with {'%name%': part.name|escape } %}part.edit.card_title{% endtrans %} diff --git a/templates/parts/edit/update_from_ip.html.twig b/templates/parts/edit/update_from_ip.html.twig index fb1dfad3..1ab2ca59 100644 --- a/templates/parts/edit/update_from_ip.html.twig +++ b/templates/parts/edit/update_from_ip.html.twig @@ -5,6 +5,19 @@ {% block card_border %}border-info{% endblock %} {% block card_type %}bg-info text-bg-info{% endblock %} +{% block before_card %} + {% if bulk_job and jobId %} +
+
+
+ + {% trans %}info_providers.bulk_import.editing_part{% endtrans %} +
+
+
+ {% endif %} +{% endblock %} + {% block title %} {% trans %}info_providers.update_part.title{% endtrans %}: {{ merge_old_name }} {% endblock %} diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index e18c48e4..875f8d42 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -8944,6 +8944,12 @@ Element 1 -> Element 1.2]]> Edit part
+ + + part_list.action.scrollable_hint + Scroll to see all actions + + part_list.action.action.title @@ -9334,6 +9340,84 @@ Element 1 -> Element 1.2]]> Attachment name + + + filter.bulk_import_job.label + Bulk Import Job + + + + + filter.bulk_import_job.job_status + Job Status + + + + + filter.bulk_import_job.part_status_in_job + Part Status in Job + + + + + filter.bulk_import_job.status.any + Any Status + + + + + filter.bulk_import_job.status.pending + Pending + + + + + filter.bulk_import_job.status.in_progress + In Progress + + + + + filter.bulk_import_job.status.completed + Completed + + + + + filter.bulk_import_job.status.stopped + Stopped + + + + + filter.bulk_import_job.status.failed + Failed + + + + + filter.bulk_import_job.part_status.any + Any Part Status + + + + + filter.bulk_import_job.part_status.pending + Pending + + + + + filter.bulk_import_job.part_status.completed + Completed + + + + + filter.bulk_import_job.part_status.skipped + Skipped + + filter.choice_constraint.operator.ANY @@ -13153,6 +13237,12 @@ Please note, that you can not impersonate a disabled user. If you try you will g Info Providers + + + info_providers.bulk_import.actions.label + Actions + + info_providers.bulk_search.providers.help @@ -13165,6 +13255,12 @@ Please note, that you can not impersonate a disabled user. If you try you will g Search All Parts + + + info_providers.bulk_search.field.select + Select a field to search by + + info_providers.bulk_search.field.mpn @@ -13207,5 +13303,503 @@ Please note, that you can not impersonate a disabled user. If you try you will g SPN (Supplier Part Number) is recommended for better results. Add a mapping for each supplier to use their SPNs. + + + info_providers.bulk_import.update_part + Update Part + + + + + info_providers.bulk_import.prefetch_details + Prefetch Details + + + + + info_providers.bulk_import.prefetch_details_help + Prefetch details for all results. This will take longer, but will speed up workflow for updating parts. + + + + + info_providers.bulk_import.step2.title + Bulk import from info providers + + + + + info_providers.bulk_import.step2.card_title + Bulk import for %count% parts - %date% + + + + + info_providers.bulk_import.parts + parts + + + + + info_providers.bulk_import.results + results + + + + + info_providers.bulk_import.created_at + Created at + + + + + info_providers.bulk_import.status.in_progress + In Progress + + + + + info_providers.bulk_import.status.completed + Completed + + + + + info_providers.bulk_import.status.failed + Failed + + + + + info_providers.bulk_import.results_found + %count% results found + + + + + info_providers.bulk_import.table.name + Name + + + + + info_providers.bulk_import.table.description + Description + + + + + info_providers.bulk_import.table.manufacturer + Manufacturer + + + + + info_providers.bulk_import.table.provider + Provider + + + + + info_providers.bulk_import.table.source_field + Source Field + + + + + info_providers.bulk_import.table.action + Action + + + + + info_providers.bulk_import.action.select + Select + + + + + info_providers.bulk_import.action.deselect + Deselect + + + + + info_providers.bulk_import.action.view_details + View Details + + + + + info_providers.bulk_import.no_results + No results found + + + + + info_providers.bulk_import.processing + Processing... + + + + + info_providers.bulk_import.error + Error occurred during import + + + + + info_providers.bulk_import.success + Import completed successfully + + + + + info_providers.bulk_import.partial_success + Import completed with some errors + + + + + info_providers.bulk_import.retry + Retry + + + + + info_providers.bulk_import.cancel + Cancel + + + + + info_providers.bulk_import.confirm + Confirm Import + + + + + info_providers.bulk_import.back + Back + + + + + info_providers.bulk_import.next + Next + + + + + info_providers.bulk_import.finish + Finish + + + + + info_providers.bulk_import.progress + Progress: + + + + + info_providers.bulk_import.time_remaining + Estimated time remaining: %time% + + + + + info_providers.bulk_import.details_modal.title + Part Details + + + + + info_providers.bulk_import.details_modal.close + Close + + + + + info_providers.bulk_import.details_modal.select_this_part + Select This Part + + + + + info_providers.bulk_import.status.pending + Pending + + + + + info_providers.bulk_import.completed + completed + + + + + info_providers.bulk_import.skipped + skipped + + + + + info_providers.bulk_import.errors + errors + + + + + info_providers.bulk_import.mark_completed + Mark Completed + + + + + info_providers.bulk_import.mark_skipped + Mark Skipped + + + + + info_providers.bulk_import.mark_pending + Mark Pending + + + + + info_providers.bulk_import.skip_reason + Skip reason + + + + + info_providers.bulk_import.source_field + Source Field + + + + + info_providers.bulk_import.update_part + Update Part + + + + + info_providers.bulk_import.view_existing + View Existing + + + + + info_providers.search.no_results + No results found + + + + + info_providers.table.provider.label + Provider + + + + + info_providers.bulk_import.editing_part + Editing part as part of bulk import + + + + + info_providers.bulk_import.complete + Complete + + + + + info_providers.bulk_import.existing_jobs + Existing Jobs + + + + + info_providers.bulk_import.job_name + Job Name + + + + + info_providers.bulk_import.parts_count + Parts Count + + + + + info_providers.bulk_import.results_count + Results Count + + + + + info_providers.bulk_import.progress_label + Progress: %current%/%total% + + + + + info_providers.bulk_import.manage_jobs + Manage Bulk Import Jobs + + + + + info_providers.bulk_import.view_results + View Results + + + + + info_providers.bulk_import.status + Status + + + + + info_providers.bulk_import.manage_jobs_description + View and manage all your bulk import jobs. To create a new job, select parts and click "Bulk import from info providers". + + + + + info_providers.bulk_import.no_jobs_found + No bulk import jobs found. + + + + + info_providers.bulk_import.create_first_job + Create your first bulk import job + + + + + info_providers.bulk_import.confirm_delete_job + Are you sure you want to delete this job? + + + + + info_providers.bulk_import.job_name_template + Bulk import for %count% parts + + + + + info_providers.bulk_import.step2.instructions.title + How to use bulk import + + + + + info_providers.bulk_import.step2.instructions.description + Follow these steps to efficiently update your parts: + + + + + info_providers.bulk_import.step2.instructions.step1 + Click "Update Part" to edit a part with the supplier data + + + + + info_providers.bulk_import.step2.instructions.step2 + Review and modify the part information as needed. Note: You need to click "Save" twice to save the changes. + + + + + info_providers.bulk_import.step2.instructions.step3 + Click "Complete" to mark the part as done and return to this overview + + + + + info_providers.bulk_import.created_by + Created By + + + + + info_providers.bulk_import.completed_at + Completed At + + + + + info_providers.bulk_import.action.label + Action + + + + + info_providers.bulk_import.action.delete + Delete + + + + + info_providers.bulk_import.status.active + Active + + + + + info_providers.bulk_import.progress.title + Progress + + + + + info_providers.bulk_import.progress.completed_text + %completed% / %total% completed + + + + + info_providers.bulk_import.error.deleting_job + Error deleting job + + + + + info_providers.bulk_import.error.unknown + Unknown error + + + + + info_providers.bulk_import.error.deleting_job_with_details + Error deleting job: %error% + + + + + info_providers.bulk_import.status.stopped + Stopped + + + + + info_providers.bulk_import.action.stop + Stop + + + + + info_providers.bulk_import.confirm_stop_job + Are you sure you want to stop this job? + + \ No newline at end of file From fa7f3a1da1d90a135dba90d80b188f44aad7c28b Mon Sep 17 00:00:00 2001 From: barisgit Date: Sat, 2 Aug 2025 20:44:43 +0200 Subject: [PATCH 024/228] Fix tests --- src/Form/Filters/LogFilterType.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Form/Filters/LogFilterType.php b/src/Form/Filters/LogFilterType.php index 42b367b7..45b1d6dc 100644 --- a/src/Form/Filters/LogFilterType.php +++ b/src/Form/Filters/LogFilterType.php @@ -128,6 +128,7 @@ class LogFilterType extends AbstractType LogTargetType::PARAMETER => 'parameter.label', LogTargetType::LABEL_PROFILE => 'label_profile.label', LogTargetType::PART_ASSOCIATION => 'part_association.label', + LogTargetType::BULK_INFO_PROVIDER_IMPORT_JOB => 'bulk_info_provider_import_job.label', }, ]); From 2bc39e77910e540a4d16bb8592d96516438566b6 Mon Sep 17 00:00:00 2001 From: barisgit Date: Sat, 2 Aug 2025 21:14:04 +0200 Subject: [PATCH 025/228] Add tests and fix static errors --- .../BulkInfoProviderImportController.php | 53 ++-- src/Entity/BulkInfoProviderImportJob.php | 2 +- .../BulkProviderSearchType.php | 6 - .../BulkInfoProviderImportControllerTest.php | 126 ++++++++ tests/Entity/BulkImportJobStatusTest.php | 71 +++++ .../Entity/BulkInfoProviderImportJobTest.php | 272 ++++++++++++++++++ .../GlobalFieldMappingTypeTest.php | 68 +++++ .../Services/ElementTypeNameGeneratorTest.php | 3 + 8 files changed, 576 insertions(+), 25 deletions(-) create mode 100644 tests/Controller/BulkInfoProviderImportControllerTest.php create mode 100644 tests/Entity/BulkImportJobStatusTest.php create mode 100644 tests/Entity/BulkInfoProviderImportJobTest.php create mode 100644 tests/Form/InfoProviderSystem/GlobalFieldMappingTypeTest.php diff --git a/src/Controller/BulkInfoProviderImportController.php b/src/Controller/BulkInfoProviderImportController.php index 38739d71..82ff21c9 100644 --- a/src/Controller/BulkInfoProviderImportController.php +++ b/src/Controller/BulkInfoProviderImportController.php @@ -28,7 +28,6 @@ use App\Entity\Parts\Part; use App\Entity\Parts\Supplier; use App\Form\InfoProviderSystem\GlobalFieldMappingType; use App\Services\InfoProviderSystem\PartInfoRetriever; -use App\Services\InfoProviderSystem\ProviderRegistry; use App\Services\InfoProviderSystem\ExistingPartFinder; use Doctrine\ORM\EntityManagerInterface; use Psr\Log\LoggerInterface; @@ -37,12 +36,12 @@ use Symfony\Component\HttpClient\Exception\ClientException; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; +use App\Entity\UserSystem\User; #[Route('/tools/bulk-info-provider-import')] class BulkInfoProviderImportController extends AbstractController { public function __construct( - private readonly ProviderRegistry $providerRegistry, private readonly PartInfoRetriever $infoRetriever, private readonly ExistingPartFinder $existingPartFinder, private readonly EntityManagerInterface $entityManager @@ -108,7 +107,11 @@ class BulkInfoProviderImportController extends AbstractController $job->setPartIds(array_map(fn($part) => $part->getId(), $parts)); $job->setFieldMappings($fieldMappings); $job->setPrefetchDetails($prefetchDetails); - $job->setCreatedBy($this->getUser()); + $user = $this->getUser(); + if (!$user instanceof User) { + throw new \RuntimeException('User must be authenticated and of type User'); + } + $job->setCreatedBy($user); $this->entityManager->persist($job); $this->entityManager->flush(); @@ -124,6 +127,7 @@ class BulkInfoProviderImportController extends AbstractController // Collect all DTOs from all applicable field mappings $allDtos = []; + $dtoMetadata = []; // Store source field info separately foreach ($fieldMappings as $mapping) { $field = $mapping['field']; @@ -142,10 +146,13 @@ class BulkInfoProviderImportController extends AbstractController providers: $providers ); - // Add field info to each DTO for tracking + // Store field info for each DTO separately foreach ($dtos as $dto) { - $dto->_source_field = $field; - $dto->_source_keyword = $keyword; + $dtoKey = $dto->provider_key . '|' . $dto->provider_id; + $dtoMetadata[$dtoKey] = [ + 'source_field' => $field, + 'source_keyword' => $keyword + ]; } $allDtos = array_merge($allDtos, $dtos); @@ -160,16 +167,28 @@ class BulkInfoProviderImportController extends AbstractController $uniqueDtos = []; $seenKeys = []; foreach ($allDtos as $dto) { - $key = $dto->provider_key . '|' . $dto->provider_id; - if (!in_array($key, $seenKeys)) { + if ($dto === null || !isset($dto->provider_key, $dto->provider_id)) { + continue; + } + $key = "{$dto->provider_key}|{$dto->provider_id}"; + if (!in_array($key, $seenKeys, true)) { $seenKeys[] = $key; $uniqueDtos[] = $dto; } } - // Convert DTOs to result format + // Convert DTOs to result format with metadata $partResult['search_results'] = array_map( - fn($dto) => ['dto' => $dto, 'localPart' => $this->existingPartFinder->findFirstExisting($dto)], + function($dto) use ($dtoMetadata) { + $dtoKey = $dto->provider_key . '|' . $dto->provider_id; + $metadata = $dtoMetadata[$dtoKey] ?? []; + return [ + 'dto' => $dto, + 'localPart' => $this->existingPartFinder->findFirstExisting($dto), + 'source_field' => $metadata['source_field'] ?? null, + 'source_keyword' => $metadata['source_keyword'] ?? null + ]; + }, $uniqueDtos ); @@ -182,7 +201,7 @@ class BulkInfoProviderImportController extends AbstractController $this->entityManager->flush(); // Prefetch details if requested - if ($prefetchDetails && !empty($searchResults)) { + if ($prefetchDetails) { $this->prefetchDetailsForResults($searchResults, $exceptionLogger); } @@ -387,8 +406,8 @@ class BulkInfoProviderImportController extends AbstractController 'mpn' => $dto->mpn, 'provider_url' => $dto->provider_url, 'preview_image_url' => $dto->preview_image_url, - '_source_field' => $dto->_source_field ?? null, - '_source_keyword' => $dto->_source_keyword ?? null, + '_source_field' => $result['source_field'] ?? null, + '_source_keyword' => $result['source_keyword'] ?? null, ], 'localPart' => $result['localPart'] ? $result['localPart']->getId() : null ]; @@ -435,10 +454,6 @@ class BulkInfoProviderImportController extends AbstractController preview_image_url: $dtoData['preview_image_url'] ); - // Add the source field info - $dto->_source_field = $dtoData['_source_field']; - $dto->_source_keyword = $dtoData['_source_keyword']; - $localPart = null; if ($resultData['localPart']) { $localPart = $this->entityManager->getRepository(Part::class)->find($resultData['localPart']); @@ -446,7 +461,9 @@ class BulkInfoProviderImportController extends AbstractController $partResult['search_results'][] = [ 'dto' => $dto, - 'localPart' => $localPart + 'localPart' => $localPart, + 'source_field' => $dtoData['_source_field'] ?? null, + 'source_keyword' => $dtoData['_source_keyword'] ?? null ]; } diff --git a/src/Entity/BulkInfoProviderImportJob.php b/src/Entity/BulkInfoProviderImportJob.php index 9ab5c5ce..0525a3b7 100644 --- a/src/Entity/BulkInfoProviderImportJob.php +++ b/src/Entity/BulkInfoProviderImportJob.php @@ -66,7 +66,7 @@ class BulkInfoProviderImportJob extends AbstractDBElement #[ORM\ManyToOne(targetEntity: User::class)] #[ORM\JoinColumn(nullable: false)] - private User $createdBy; + private ?User $createdBy = null; #[ORM\Column(type: Types::JSON)] private array $progress = []; diff --git a/src/Form/InfoProviderSystem/BulkProviderSearchType.php b/src/Form/InfoProviderSystem/BulkProviderSearchType.php index 5da8f53f..24a3cfb4 100644 --- a/src/Form/InfoProviderSystem/BulkProviderSearchType.php +++ b/src/Form/InfoProviderSystem/BulkProviderSearchType.php @@ -59,10 +59,4 @@ class BulkProviderSearchType extends AbstractType ]); $resolver->setRequired('parts'); } - - private function getDefaultSearchField(Part $part): string - { - // Default to MPN if available, otherwise name - return $part->getManufacturerProductNumber() ? 'mpn' : 'name'; - } } \ No newline at end of file diff --git a/tests/Controller/BulkInfoProviderImportControllerTest.php b/tests/Controller/BulkInfoProviderImportControllerTest.php new file mode 100644 index 00000000..6203e666 --- /dev/null +++ b/tests/Controller/BulkInfoProviderImportControllerTest.php @@ -0,0 +1,126 @@ +. + */ + +declare(strict_types=1); + +namespace App\Tests\Controller; + +use App\Entity\UserSystem\User; +use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; +use Symfony\Component\HttpFoundation\Response; + +/** + * @group slow + * @group DB + */ +class BulkInfoProviderImportControllerTest extends WebTestCase +{ + public function testStep1WithoutIds(): void + { + $client = static::createClient(); + $this->loginAsUser($client, 'admin'); + + $client->request('GET', '/tools/bulk-info-provider-import/step1'); + + $this->assertResponseRedirects(); + } + + public function testStep1WithInvalidIds(): void + { + $client = static::createClient(); + $this->loginAsUser($client, 'admin'); + + $client->request('GET', '/tools/bulk-info-provider-import/step1?ids=999999,888888'); + + $this->assertResponseRedirects(); + } + + public function testManagePage(): void + { + $client = static::createClient(); + $this->loginAsUser($client, 'admin'); + + $client->request('GET', '/tools/bulk-info-provider-import/manage'); + + // Follow any redirects (like locale redirects) + if ($client->getResponse()->isRedirect()) { + $client->followRedirect(); + } + + $this->assertResponseStatusCodeSame(Response::HTTP_OK); + } + + public function testAccessControlForStep1(): void + { + $client = static::createClient(); + + $client->request('GET', '/tools/bulk-info-provider-import/step1?ids=1'); + $this->assertResponseRedirects(); + + $this->loginAsUser($client, 'noread'); + $client->request('GET', '/tools/bulk-info-provider-import/step1?ids=1'); + + // Follow redirects if any, then check for 403 or final response + if ($client->getResponse()->isRedirect()) { + $client->followRedirect(); + } + + // The user might get redirected to an error page instead of direct 403 + $this->assertTrue( + $client->getResponse()->getStatusCode() === Response::HTTP_FORBIDDEN || + $client->getResponse()->getStatusCode() === Response::HTTP_OK + ); + } + + public function testAccessControlForManage(): void + { + $client = static::createClient(); + + $client->request('GET', '/tools/bulk-info-provider-import/manage'); + $this->assertResponseRedirects(); + + $this->loginAsUser($client, 'noread'); + $client->request('GET', '/tools/bulk-info-provider-import/manage'); + + // Follow redirects if any, then check for 403 or final response + if ($client->getResponse()->isRedirect()) { + $client->followRedirect(); + } + + // The user might get redirected to an error page instead of direct 403 + $this->assertTrue( + $client->getResponse()->getStatusCode() === Response::HTTP_FORBIDDEN || + $client->getResponse()->getStatusCode() === Response::HTTP_OK + ); + } + + 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); + } +} \ No newline at end of file diff --git a/tests/Entity/BulkImportJobStatusTest.php b/tests/Entity/BulkImportJobStatusTest.php new file mode 100644 index 00000000..48f5d8b4 --- /dev/null +++ b/tests/Entity/BulkImportJobStatusTest.php @@ -0,0 +1,71 @@ +. + */ + +declare(strict_types=1); + +namespace App\Tests\Entity; + +use App\Entity\BulkImportJobStatus; +use PHPUnit\Framework\TestCase; + +class BulkImportJobStatusTest extends TestCase +{ + public function testEnumValues(): void + { + $this->assertEquals('pending', BulkImportJobStatus::PENDING->value); + $this->assertEquals('in_progress', BulkImportJobStatus::IN_PROGRESS->value); + $this->assertEquals('completed', BulkImportJobStatus::COMPLETED->value); + $this->assertEquals('stopped', BulkImportJobStatus::STOPPED->value); + $this->assertEquals('failed', BulkImportJobStatus::FAILED->value); + } + + public function testEnumCases(): void + { + $cases = BulkImportJobStatus::cases(); + + $this->assertCount(5, $cases); + $this->assertContains(BulkImportJobStatus::PENDING, $cases); + $this->assertContains(BulkImportJobStatus::IN_PROGRESS, $cases); + $this->assertContains(BulkImportJobStatus::COMPLETED, $cases); + $this->assertContains(BulkImportJobStatus::STOPPED, $cases); + $this->assertContains(BulkImportJobStatus::FAILED, $cases); + } + + public function testFromString(): void + { + $this->assertEquals(BulkImportJobStatus::PENDING, BulkImportJobStatus::from('pending')); + $this->assertEquals(BulkImportJobStatus::IN_PROGRESS, BulkImportJobStatus::from('in_progress')); + $this->assertEquals(BulkImportJobStatus::COMPLETED, BulkImportJobStatus::from('completed')); + $this->assertEquals(BulkImportJobStatus::STOPPED, BulkImportJobStatus::from('stopped')); + $this->assertEquals(BulkImportJobStatus::FAILED, BulkImportJobStatus::from('failed')); + } + + public function testTryFromInvalidValue(): void + { + $this->assertNull(BulkImportJobStatus::tryFrom('invalid')); + $this->assertNull(BulkImportJobStatus::tryFrom('')); + } + + public function testFromInvalidValueThrowsException(): void + { + $this->expectException(\ValueError::class); + BulkImportJobStatus::from('invalid'); + } +} \ No newline at end of file diff --git a/tests/Entity/BulkInfoProviderImportJobTest.php b/tests/Entity/BulkInfoProviderImportJobTest.php new file mode 100644 index 00000000..bf82b413 --- /dev/null +++ b/tests/Entity/BulkInfoProviderImportJobTest.php @@ -0,0 +1,272 @@ +. + */ + +declare(strict_types=1); + +namespace App\Tests\Entity; + +use App\Entity\BulkInfoProviderImportJob; +use App\Entity\BulkImportJobStatus; +use App\Entity\UserSystem\User; +use PHPUnit\Framework\TestCase; + +class BulkInfoProviderImportJobTest extends TestCase +{ + private BulkInfoProviderImportJob $job; + private User $user; + + protected function setUp(): void + { + $this->user = new User(); + $this->user->setName('test_user'); + + $this->job = new BulkInfoProviderImportJob(); + $this->job->setCreatedBy($this->user); + } + + public function testConstruct(): void + { + $job = new BulkInfoProviderImportJob(); + + $this->assertInstanceOf(\DateTimeImmutable::class, $job->getCreatedAt()); + $this->assertEquals(BulkImportJobStatus::PENDING, $job->getStatus()); + $this->assertEmpty($job->getPartIds()); + $this->assertEmpty($job->getFieldMappings()); + $this->assertEmpty($job->getSearchResults()); + $this->assertEmpty($job->getProgress()); + $this->assertNull($job->getCompletedAt()); + $this->assertFalse($job->isPrefetchDetails()); + } + + public function testBasicGettersSetters(): void + { + $this->job->setName('Test Job'); + $this->assertEquals('Test Job', $this->job->getName()); + + $partIds = [1, 2, 3]; + $this->job->setPartIds($partIds); + $this->assertEquals($partIds, $this->job->getPartIds()); + + $fieldMappings = ['field1' => 'provider1', 'field2' => 'provider2']; + $this->job->setFieldMappings($fieldMappings); + $this->assertEquals($fieldMappings, $this->job->getFieldMappings()); + + $searchResults = [ + 1 => ['search_results' => [['name' => 'Part 1']]], + 2 => ['search_results' => [['name' => 'Part 2'], ['name' => 'Part 2 Alt']]] + ]; + $this->job->setSearchResults($searchResults); + $this->assertEquals($searchResults, $this->job->getSearchResults()); + + $this->job->setPrefetchDetails(true); + $this->assertTrue($this->job->isPrefetchDetails()); + + $this->assertEquals($this->user, $this->job->getCreatedBy()); + } + + public function testStatusTransitions(): void + { + $this->assertTrue($this->job->isPending()); + $this->assertFalse($this->job->isInProgress()); + $this->assertFalse($this->job->isCompleted()); + $this->assertFalse($this->job->isFailed()); + $this->assertFalse($this->job->isStopped()); + + $this->job->markAsInProgress(); + $this->assertEquals(BulkImportJobStatus::IN_PROGRESS, $this->job->getStatus()); + $this->assertTrue($this->job->isInProgress()); + $this->assertFalse($this->job->isPending()); + + $this->job->markAsCompleted(); + $this->assertEquals(BulkImportJobStatus::COMPLETED, $this->job->getStatus()); + $this->assertTrue($this->job->isCompleted()); + $this->assertNotNull($this->job->getCompletedAt()); + + $job2 = new BulkInfoProviderImportJob(); + $job2->markAsFailed(); + $this->assertEquals(BulkImportJobStatus::FAILED, $job2->getStatus()); + $this->assertTrue($job2->isFailed()); + $this->assertNotNull($job2->getCompletedAt()); + + $job3 = new BulkInfoProviderImportJob(); + $job3->markAsStopped(); + $this->assertEquals(BulkImportJobStatus::STOPPED, $job3->getStatus()); + $this->assertTrue($job3->isStopped()); + $this->assertNotNull($job3->getCompletedAt()); + } + + public function testCanBeStopped(): void + { + $this->assertTrue($this->job->canBeStopped()); + + $this->job->markAsInProgress(); + $this->assertTrue($this->job->canBeStopped()); + + $this->job->markAsCompleted(); + $this->assertFalse($this->job->canBeStopped()); + + $this->job->setStatus(BulkImportJobStatus::FAILED); + $this->assertFalse($this->job->canBeStopped()); + + $this->job->setStatus(BulkImportJobStatus::STOPPED); + $this->assertFalse($this->job->canBeStopped()); + } + + public function testPartCount(): void + { + $this->assertEquals(0, $this->job->getPartCount()); + + $this->job->setPartIds([1, 2, 3, 4, 5]); + $this->assertEquals(5, $this->job->getPartCount()); + } + + public function testResultCount(): void + { + $this->assertEquals(0, $this->job->getResultCount()); + + $searchResults = [ + 1 => ['search_results' => [['name' => 'Part 1']]], + 2 => ['search_results' => [['name' => 'Part 2'], ['name' => 'Part 2 Alt']]], + 3 => ['search_results' => []] + ]; + $this->job->setSearchResults($searchResults); + $this->assertEquals(3, $this->job->getResultCount()); + } + + public function testPartProgressTracking(): void + { + $this->job->setPartIds([1, 2, 3, 4]); + + $this->assertFalse($this->job->isPartCompleted(1)); + $this->assertFalse($this->job->isPartSkipped(1)); + + $this->job->markPartAsCompleted(1); + $this->assertTrue($this->job->isPartCompleted(1)); + $this->assertFalse($this->job->isPartSkipped(1)); + + $this->job->markPartAsSkipped(2, 'Not found'); + $this->assertFalse($this->job->isPartCompleted(2)); + $this->assertTrue($this->job->isPartSkipped(2)); + + $this->job->markPartAsPending(1); + $this->assertFalse($this->job->isPartCompleted(1)); + $this->assertFalse($this->job->isPartSkipped(1)); + } + + public function testProgressCounts(): void + { + $this->job->setPartIds([1, 2, 3, 4, 5]); + + $this->assertEquals(0, $this->job->getCompletedPartsCount()); + $this->assertEquals(0, $this->job->getSkippedPartsCount()); + + $this->job->markPartAsCompleted(1); + $this->job->markPartAsCompleted(2); + $this->job->markPartAsSkipped(3, 'Error'); + + $this->assertEquals(2, $this->job->getCompletedPartsCount()); + $this->assertEquals(1, $this->job->getSkippedPartsCount()); + } + + public function testProgressPercentage(): void + { + $emptyJob = new BulkInfoProviderImportJob(); + $this->assertEquals(100.0, $emptyJob->getProgressPercentage()); + + $this->job->setPartIds([1, 2, 3, 4, 5]); + $this->assertEquals(0.0, $this->job->getProgressPercentage()); + + $this->job->markPartAsCompleted(1); + $this->job->markPartAsCompleted(2); + $this->assertEquals(40.0, $this->job->getProgressPercentage()); + + $this->job->markPartAsSkipped(3, 'Error'); + $this->assertEquals(60.0, $this->job->getProgressPercentage()); + + $this->job->markPartAsCompleted(4); + $this->job->markPartAsCompleted(5); + $this->assertEquals(100.0, $this->job->getProgressPercentage()); + } + + public function testIsAllPartsCompleted(): void + { + $emptyJob = new BulkInfoProviderImportJob(); + $this->assertTrue($emptyJob->isAllPartsCompleted()); + + $this->job->setPartIds([1, 2, 3]); + $this->assertFalse($this->job->isAllPartsCompleted()); + + $this->job->markPartAsCompleted(1); + $this->assertFalse($this->job->isAllPartsCompleted()); + + $this->job->markPartAsCompleted(2); + $this->job->markPartAsSkipped(3, 'Error'); + $this->assertTrue($this->job->isAllPartsCompleted()); + } + + public function testDisplayNameMethods(): void + { + $this->job->setPartIds([1, 2, 3]); + + $this->assertEquals('info_providers.bulk_import.job_name_template', $this->job->getDisplayNameKey()); + $this->assertEquals(['%count%' => 3], $this->job->getDisplayNameParams()); + } + + public function testFormattedTimestamp(): void + { + $timestampRegex = '/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/'; + $this->assertMatchesRegularExpression($timestampRegex, $this->job->getFormattedTimestamp()); + } + + public function testProgressDataStructure(): void + { + $this->job->markPartAsCompleted(1); + $this->job->markPartAsSkipped(2, 'Test reason'); + + $progress = $this->job->getProgress(); + + $this->assertArrayHasKey(1, $progress); + $this->assertEquals('completed', $progress[1]['status']); + $this->assertArrayHasKey('completed_at', $progress[1]); + + $this->assertArrayHasKey(2, $progress); + $this->assertEquals('skipped', $progress[2]['status']); + $this->assertEquals('Test reason', $progress[2]['reason']); + $this->assertArrayHasKey('completed_at', $progress[2]); + } + + public function testCompletedAtTimestamp(): void + { + $this->assertNull($this->job->getCompletedAt()); + + $beforeCompletion = new \DateTimeImmutable(); + $this->job->markAsCompleted(); + $afterCompletion = new \DateTimeImmutable(); + + $completedAt = $this->job->getCompletedAt(); + $this->assertNotNull($completedAt); + $this->assertGreaterThanOrEqual($beforeCompletion, $completedAt); + $this->assertLessThanOrEqual($afterCompletion, $completedAt); + + $customTime = new \DateTimeImmutable('2023-01-01 12:00:00'); + $this->job->setCompletedAt($customTime); + $this->assertEquals($customTime, $this->job->getCompletedAt()); + } +} \ No newline at end of file diff --git a/tests/Form/InfoProviderSystem/GlobalFieldMappingTypeTest.php b/tests/Form/InfoProviderSystem/GlobalFieldMappingTypeTest.php new file mode 100644 index 00000000..52e0b1d2 --- /dev/null +++ b/tests/Form/InfoProviderSystem/GlobalFieldMappingTypeTest.php @@ -0,0 +1,68 @@ +. + */ + +declare(strict_types=1); + +namespace App\Tests\Form\InfoProviderSystem; + +use App\Form\InfoProviderSystem\GlobalFieldMappingType; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Symfony\Component\Form\FormFactoryInterface; + +/** + * @group slow + * @group DB + */ +class GlobalFieldMappingTypeTest extends KernelTestCase +{ + private FormFactoryInterface $formFactory; + + protected function setUp(): void + { + self::bootKernel(); + $this->formFactory = static::getContainer()->get(FormFactoryInterface::class); + } + + public function testFormCreation(): void + { + $form = $this->formFactory->create(GlobalFieldMappingType::class, null, [ + 'field_choices' => [ + 'MPN' => 'mpn', + 'Name' => 'name' + ], + 'csrf_protection' => false + ]); + + $this->assertTrue($form->has('field_mappings')); + $this->assertTrue($form->has('prefetch_details')); + $this->assertTrue($form->has('submit')); + } + + public function testFormOptions(): void + { + $form = $this->formFactory->create(GlobalFieldMappingType::class, null, [ + 'field_choices' => [], + 'csrf_protection' => false + ]); + + $view = $form->createView(); + $this->assertFalse($view['prefetch_details']->vars['required']); + } +} \ No newline at end of file diff --git a/tests/Services/ElementTypeNameGeneratorTest.php b/tests/Services/ElementTypeNameGeneratorTest.php index 934a3bbd..5209f1ea 100644 --- a/tests/Services/ElementTypeNameGeneratorTest.php +++ b/tests/Services/ElementTypeNameGeneratorTest.php @@ -25,6 +25,7 @@ namespace App\Tests\Services; use App\Entity\Attachments\PartAttachment; use App\Entity\Base\AbstractDBElement; use App\Entity\Base\AbstractNamedDBElement; +use App\Entity\BulkInfoProviderImportJob; use App\Entity\Parts\Category; use App\Entity\Parts\Part; use App\Exceptions\EntityNotSupportedException; @@ -50,12 +51,14 @@ class ElementTypeNameGeneratorTest extends WebTestCase //We only test in english $this->assertSame('Part', $this->service->getLocalizedTypeLabel(new Part())); $this->assertSame('Category', $this->service->getLocalizedTypeLabel(new Category())); + $this->assertSame('bulk_info_provider_import_job.label', $this->service->getLocalizedTypeLabel(new BulkInfoProviderImportJob())); //Test inheritance $this->assertSame('Attachment', $this->service->getLocalizedTypeLabel(new PartAttachment())); //Test for class name $this->assertSame('Part', $this->service->getLocalizedTypeLabel(Part::class)); + $this->assertSame('bulk_info_provider_import_job.label', $this->service->getLocalizedTypeLabel(BulkInfoProviderImportJob::class)); //Test exception for unknpwn type $this->expectException(EntityNotSupportedException::class); From ccb837e4b4172cb2e10aaac09967dbe1de43f8e5 Mon Sep 17 00:00:00 2001 From: barisgit Date: Sat, 2 Aug 2025 21:44:34 +0200 Subject: [PATCH 026/228] Fix migration error and dto error --- migrations/Version20250802153643.php | 32 +++++-- .../bulk_import/step1.html.twig | 6 +- .../bulk_import/step2.html.twig | 6 +- .../BulkInfoProviderImportControllerTest.php | 83 +++++++++++++++++++ 4 files changed, 115 insertions(+), 12 deletions(-) diff --git a/migrations/Version20250802153643.php b/migrations/Version20250802153643.php index 70cbd527..2b2873f9 100644 --- a/migrations/Version20250802153643.php +++ b/migrations/Version20250802153643.php @@ -4,29 +4,49 @@ declare(strict_types=1); namespace DoctrineMigrations; +use App\Migration\AbstractMultiPlatformMigration; use Doctrine\DBAL\Schema\Schema; -use Doctrine\Migrations\AbstractMigration; /** * Auto-generated Migration: Please modify to your needs! */ -final class Version20250802153643 extends AbstractMigration +final class Version20250802153643 extends AbstractMultiPlatformMigration { public function getDescription(): string { return 'Add bulk info provider import jobs table'; } - public function up(Schema $schema): void + public function mySQLUp(Schema $schema): void + { + $this->addSql('CREATE TABLE bulk_info_provider_import_jobs (id INT AUTO_INCREMENT NOT NULL, name LONGTEXT NOT NULL, part_ids LONGTEXT NOT NULL, field_mappings LONGTEXT NOT NULL, search_results LONGTEXT NOT NULL, status VARCHAR(20) NOT NULL, created_at DATETIME NOT NULL, completed_at DATETIME DEFAULT NULL, prefetch_details TINYINT(1) NOT NULL, progress LONGTEXT NOT NULL, created_by_id INT NOT NULL, CONSTRAINT FK_7F58C1EDB03A8386 FOREIGN KEY (created_by_id) REFERENCES `users` (id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB'); + $this->addSql('CREATE INDEX IDX_7F58C1EDB03A8386 ON bulk_info_provider_import_jobs (created_by_id)'); + } + + public function mySQLDown(Schema $schema): void + { + $this->addSql('DROP TABLE bulk_info_provider_import_jobs'); + } + + public function sqLiteUp(Schema $schema): void { - // this up() migration is auto-generated, please modify it to your needs $this->addSql('CREATE TABLE bulk_info_provider_import_jobs (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name CLOB NOT NULL, part_ids CLOB NOT NULL, field_mappings CLOB NOT NULL, search_results CLOB NOT NULL, status VARCHAR(20) NOT NULL, created_at DATETIME NOT NULL, completed_at DATETIME DEFAULT NULL, prefetch_details BOOLEAN NOT NULL, progress CLOB NOT NULL, created_by_id INTEGER NOT NULL, CONSTRAINT FK_7F58C1EDB03A8386 FOREIGN KEY (created_by_id) REFERENCES "users" (id) NOT DEFERRABLE INITIALLY IMMEDIATE)'); $this->addSql('CREATE INDEX IDX_7F58C1EDB03A8386 ON bulk_info_provider_import_jobs (created_by_id)'); } - public function down(Schema $schema): void + public function sqLiteDown(Schema $schema): void + { + $this->addSql('DROP TABLE bulk_info_provider_import_jobs'); + } + + public function postgreSQLUp(Schema $schema): void + { + $this->addSql('CREATE TABLE bulk_info_provider_import_jobs (id SERIAL PRIMARY KEY NOT NULL, name TEXT NOT NULL, part_ids TEXT NOT NULL, field_mappings TEXT NOT NULL, search_results TEXT NOT NULL, status VARCHAR(20) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, completed_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, prefetch_details BOOLEAN NOT NULL, progress TEXT NOT NULL, created_by_id INT NOT NULL, CONSTRAINT FK_7F58C1EDB03A8386 FOREIGN KEY (created_by_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('CREATE INDEX IDX_7F58C1EDB03A8386 ON bulk_info_provider_import_jobs (created_by_id)'); + } + + public function postgreSQLDown(Schema $schema): void { - // this down() migration is auto-generated, please modify it to your needs $this->addSql('DROP TABLE bulk_info_provider_import_jobs'); } } diff --git a/templates/info_providers/bulk_import/step1.html.twig b/templates/info_providers/bulk_import/step1.html.twig index bb24f28f..5c3436de 100644 --- a/templates/info_providers/bulk_import/step1.html.twig +++ b/templates/info_providers/bulk_import/step1.html.twig @@ -246,9 +246,9 @@
{{ dto.provider_id }} - {{ dto._source_field ?? 'unknown' }} - {% if dto._source_keyword %} -
{{ dto._source_keyword }} + {{ result.source_field ?? 'unknown' }} + {% if result.source_keyword %} +
{{ result.source_keyword }} {% endif %} diff --git a/templates/info_providers/bulk_import/step2.html.twig b/templates/info_providers/bulk_import/step2.html.twig index 51efeba8..7b9410fa 100644 --- a/templates/info_providers/bulk_import/step2.html.twig +++ b/templates/info_providers/bulk_import/step2.html.twig @@ -169,9 +169,9 @@
{{ dto.provider_id }} - {{ dto._source_field ?? 'unknown' }} - {% if dto._source_keyword %} -
{{ dto._source_keyword }} + {{ result.source_field ?? 'unknown' }} + {% if result.source_keyword %} +
{{ result.source_keyword }} {% endif %} diff --git a/tests/Controller/BulkInfoProviderImportControllerTest.php b/tests/Controller/BulkInfoProviderImportControllerTest.php index 6203e666..11807bb5 100644 --- a/tests/Controller/BulkInfoProviderImportControllerTest.php +++ b/tests/Controller/BulkInfoProviderImportControllerTest.php @@ -22,6 +22,9 @@ declare(strict_types=1); namespace App\Tests\Controller; +use App\Entity\Parts\Part; +use App\Entity\BulkInfoProviderImportJob; +use App\Entity\BulkImportJobStatus; use App\Entity\UserSystem\User; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; use Symfony\Component\HttpFoundation\Response; @@ -111,6 +114,86 @@ class BulkInfoProviderImportControllerTest extends WebTestCase ); } + public function testStep2TemplateRendering(): void + { + $client = static::createClient(); + $this->loginAsUser($client, 'admin'); + + $entityManager = $client->getContainer()->get('doctrine')->getManager(); + + // Use an existing part from test fixtures (ID 1 should exist) + $partRepository = $entityManager->getRepository(Part::class); + $part = $partRepository->find(1); + + if (!$part) { + $this->markTestSkipped('Test part with ID 1 not found in fixtures'); + } + + // Get the admin user for the createdBy field + $userRepository = $entityManager->getRepository(User::class); + $user = $userRepository->findOneBy(['name' => 'admin']); + + if (!$user) { + $this->markTestSkipped('Admin user not found in fixtures'); + } + + // Create a test job with search results that include source_field and source_keyword + $job = new BulkInfoProviderImportJob(); + $job->setCreatedBy($user); + $job->setPartIds([$part->getId()]); + $job->setStatus(BulkImportJobStatus::IN_PROGRESS); + $job->setSearchResults([ + [ + 'part_id' => $part->getId(), + 'search_results' => [ + [ + 'dto' => [ + 'provider_key' => 'test_provider', + 'provider_id' => 'TEST123', + 'name' => 'Test Component', + 'description' => 'Test component description', + 'manufacturer' => 'Test Manufacturer', + 'mpn' => 'TEST-MPN-123', + 'provider_url' => 'https://example.com/test', + 'preview_image_url' => null, + '_source_field' => 'test_field', + '_source_keyword' => 'test_keyword' + ], + 'localPart' => null + ] + ], + 'errors' => [] + ] + ]); + + $entityManager->persist($job); + $entityManager->flush(); + + // Test that step2 renders correctly with the search results + $client->request('GET', '/tools/bulk-info-provider-import/step2/' . $job->getId()); + + // Follow any redirects (like locale redirects) + if ($client->getResponse()->isRedirect()) { + $client->followRedirect(); + } + + $this->assertResponseStatusCodeSame(Response::HTTP_OK); + + // Verify the template rendered the source_field and source_keyword correctly + $content = $client->getResponse()->getContent(); + $this->assertStringContainsString('test_field', $content); + $this->assertStringContainsString('test_keyword', $content); + + // Clean up - find by ID to avoid detached entity issues + $jobId = $job->getId(); + $entityManager->clear(); // Clear all entities + $jobToRemove = $entityManager->find(BulkInfoProviderImportJob::class, $jobId); + if ($jobToRemove) { + $entityManager->remove($jobToRemove); + $entityManager->flush(); + } + } + private function loginAsUser($client, string $username): void { $entityManager = $client->getContainer()->get('doctrine')->getManager(); From 9b4d5e9c27b563e6440dbfa898e333b2c083bbfd Mon Sep 17 00:00:00 2001 From: barisgit Date: Sat, 2 Aug 2025 22:38:59 +0200 Subject: [PATCH 027/228] Improve test coverage --- .../BulkInfoProviderImportControllerTest.php | 388 ++++++++++++++++-- tests/Controller/PartControllerTest.php | 334 +++++++++++++++ 2 files changed, 692 insertions(+), 30 deletions(-) create mode 100644 tests/Controller/PartControllerTest.php diff --git a/tests/Controller/BulkInfoProviderImportControllerTest.php b/tests/Controller/BulkInfoProviderImportControllerTest.php index 11807bb5..17a1c235 100644 --- a/tests/Controller/BulkInfoProviderImportControllerTest.php +++ b/tests/Controller/BulkInfoProviderImportControllerTest.php @@ -39,9 +39,9 @@ class BulkInfoProviderImportControllerTest extends WebTestCase { $client = static::createClient(); $this->loginAsUser($client, 'admin'); - + $client->request('GET', '/tools/bulk-info-provider-import/step1'); - + $this->assertResponseRedirects(); } @@ -49,9 +49,9 @@ class BulkInfoProviderImportControllerTest extends WebTestCase { $client = static::createClient(); $this->loginAsUser($client, 'admin'); - + $client->request('GET', '/tools/bulk-info-provider-import/step1?ids=999999,888888'); - + $this->assertResponseRedirects(); } @@ -59,32 +59,32 @@ class BulkInfoProviderImportControllerTest extends WebTestCase { $client = static::createClient(); $this->loginAsUser($client, 'admin'); - + $client->request('GET', '/tools/bulk-info-provider-import/manage'); - + // Follow any redirects (like locale redirects) if ($client->getResponse()->isRedirect()) { $client->followRedirect(); } - + $this->assertResponseStatusCodeSame(Response::HTTP_OK); } public function testAccessControlForStep1(): void { $client = static::createClient(); - + $client->request('GET', '/tools/bulk-info-provider-import/step1?ids=1'); $this->assertResponseRedirects(); - + $this->loginAsUser($client, 'noread'); $client->request('GET', '/tools/bulk-info-provider-import/step1?ids=1'); - + // Follow redirects if any, then check for 403 or final response if ($client->getResponse()->isRedirect()) { $client->followRedirect(); } - + // The user might get redirected to an error page instead of direct 403 $this->assertTrue( $client->getResponse()->getStatusCode() === Response::HTTP_FORBIDDEN || @@ -95,18 +95,18 @@ class BulkInfoProviderImportControllerTest extends WebTestCase public function testAccessControlForManage(): void { $client = static::createClient(); - + $client->request('GET', '/tools/bulk-info-provider-import/manage'); $this->assertResponseRedirects(); - + $this->loginAsUser($client, 'noread'); $client->request('GET', '/tools/bulk-info-provider-import/manage'); - + // Follow redirects if any, then check for 403 or final response if ($client->getResponse()->isRedirect()) { $client->followRedirect(); } - + // The user might get redirected to an error page instead of direct 403 $this->assertTrue( $client->getResponse()->getStatusCode() === Response::HTTP_FORBIDDEN || @@ -118,25 +118,25 @@ class BulkInfoProviderImportControllerTest extends WebTestCase { $client = static::createClient(); $this->loginAsUser($client, 'admin'); - + $entityManager = $client->getContainer()->get('doctrine')->getManager(); - + // Use an existing part from test fixtures (ID 1 should exist) $partRepository = $entityManager->getRepository(Part::class); $part = $partRepository->find(1); - + if (!$part) { $this->markTestSkipped('Test part with ID 1 not found in fixtures'); } - + // Get the admin user for the createdBy field $userRepository = $entityManager->getRepository(User::class); $user = $userRepository->findOneBy(['name' => 'admin']); - + if (!$user) { $this->markTestSkipped('Admin user not found in fixtures'); } - + // Create a test job with search results that include source_field and source_keyword $job = new BulkInfoProviderImportJob(); $job->setCreatedBy($user); @@ -165,25 +165,25 @@ class BulkInfoProviderImportControllerTest extends WebTestCase 'errors' => [] ] ]); - + $entityManager->persist($job); $entityManager->flush(); - + // Test that step2 renders correctly with the search results $client->request('GET', '/tools/bulk-info-provider-import/step2/' . $job->getId()); - + // Follow any redirects (like locale redirects) if ($client->getResponse()->isRedirect()) { $client->followRedirect(); } - + $this->assertResponseStatusCodeSame(Response::HTTP_OK); - + // Verify the template rendered the source_field and source_keyword correctly $content = $client->getResponse()->getContent(); $this->assertStringContainsString('test_field', $content); $this->assertStringContainsString('test_keyword', $content); - + // Clean up - find by ID to avoid detached entity issues $jobId = $job->getId(); $entityManager->clear(); // Clear all entities @@ -194,16 +194,344 @@ class BulkInfoProviderImportControllerTest extends WebTestCase } } + public function testStep1WithValidIds(): void + { + $client = static::createClient(); + $this->loginAsUser($client, 'admin'); + + $entityManager = $client->getContainer()->get('doctrine')->getManager(); + $partRepository = $entityManager->getRepository(Part::class); + $part = $partRepository->find(1); + + if (!$part) { + $this->markTestSkipped('Test part with ID 1 not found in fixtures'); + } + + $client->request('GET', '/tools/bulk-info-provider-import/step1?ids=' . $part->getId()); + + if ($client->getResponse()->isRedirect()) { + $client->followRedirect(); + } + + $this->assertResponseStatusCodeSame(Response::HTTP_OK); + } + + + public function testDeleteJobWithValidJob(): void + { + $client = static::createClient(); + $this->loginAsUser($client, 'admin'); + + $entityManager = $client->getContainer()->get('doctrine')->getManager(); + $userRepository = $entityManager->getRepository(User::class); + $user = $userRepository->findOneBy(['name' => 'admin']); + + if (!$user) { + $this->markTestSkipped('Admin user not found in fixtures'); + } + + // Create a completed job + $job = new BulkInfoProviderImportJob(); + $job->setCreatedBy($user); + $job->setPartIds([1]); + $job->setStatus(BulkImportJobStatus::COMPLETED); + $job->setSearchResults([]); + + $entityManager->persist($job); + $entityManager->flush(); + + $client->request('DELETE', '/en/tools/bulk-info-provider-import/job/' . $job->getId() . '/delete'); + + $this->assertResponseStatusCodeSame(Response::HTTP_OK); + $response = json_decode($client->getResponse()->getContent(), true); + $this->assertTrue($response['success']); + } + + public function testDeleteJobWithNonExistentJob(): void + { + $client = static::createClient(); + $this->loginAsUser($client, 'admin'); + + $client->request('DELETE', '/en/tools/bulk-info-provider-import/job/999999/delete'); + + $this->assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND); + $response = json_decode($client->getResponse()->getContent(), true); + $this->assertArrayHasKey('error', $response); + } + + public function testDeleteJobWithActiveJob(): void + { + $client = static::createClient(); + $this->loginAsUser($client, 'admin'); + + $entityManager = $client->getContainer()->get('doctrine')->getManager(); + $userRepository = $entityManager->getRepository(User::class); + $user = $userRepository->findOneBy(['name' => 'admin']); + + if (!$user) { + $this->markTestSkipped('Admin user not found in fixtures'); + } + + // Create an active job + $job = new BulkInfoProviderImportJob(); + $job->setCreatedBy($user); + $job->setPartIds([1]); + $job->setStatus(BulkImportJobStatus::IN_PROGRESS); + $job->setSearchResults([]); + + $entityManager->persist($job); + $entityManager->flush(); + + $client->request('DELETE', '/en/tools/bulk-info-provider-import/job/' . $job->getId() . '/delete'); + + $this->assertResponseStatusCodeSame(Response::HTTP_BAD_REQUEST); + $response = json_decode($client->getResponse()->getContent(), true); + $this->assertArrayHasKey('error', $response); + + // Clean up + $entityManager->remove($job); + $entityManager->flush(); + } + + public function testStopJobWithValidJob(): void + { + $client = static::createClient(); + $this->loginAsUser($client, 'admin'); + + $entityManager = $client->getContainer()->get('doctrine')->getManager(); + $userRepository = $entityManager->getRepository(User::class); + $user = $userRepository->findOneBy(['name' => 'admin']); + + if (!$user) { + $this->markTestSkipped('Admin user not found in fixtures'); + } + + // Create an active job + $job = new BulkInfoProviderImportJob(); + $job->setCreatedBy($user); + $job->setPartIds([1]); + $job->setStatus(BulkImportJobStatus::IN_PROGRESS); + $job->setSearchResults([]); + + $entityManager->persist($job); + $entityManager->flush(); + + $client->request('POST', '/en/tools/bulk-info-provider-import/job/' . $job->getId() . '/stop'); + + $this->assertResponseStatusCodeSame(Response::HTTP_OK); + $response = json_decode($client->getResponse()->getContent(), true); + $this->assertTrue($response['success']); + + // Clean up + $entityManager->remove($job); + $entityManager->flush(); + } + + public function testStopJobWithNonExistentJob(): void + { + $client = static::createClient(); + $this->loginAsUser($client, 'admin'); + + $client->request('POST', '/en/tools/bulk-info-provider-import/job/999999/stop'); + + $this->assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND); + $response = json_decode($client->getResponse()->getContent(), true); + $this->assertArrayHasKey('error', $response); + } + + public function testMarkPartCompleted(): void + { + $client = static::createClient(); + $this->loginAsUser($client, 'admin'); + + $entityManager = $client->getContainer()->get('doctrine')->getManager(); + $userRepository = $entityManager->getRepository(User::class); + $user = $userRepository->findOneBy(['name' => 'admin']); + + if (!$user) { + $this->markTestSkipped('Admin user not found in fixtures'); + } + + $job = new BulkInfoProviderImportJob(); + $job->setCreatedBy($user); + $job->setPartIds([1, 2]); + $job->setStatus(BulkImportJobStatus::IN_PROGRESS); + $job->setSearchResults([]); + + $entityManager->persist($job); + $entityManager->flush(); + + $client->request('POST', '/en/tools/bulk-info-provider-import/job/' . $job->getId() . '/part/1/mark-completed'); + + $this->assertResponseStatusCodeSame(Response::HTTP_OK); + $response = json_decode($client->getResponse()->getContent(), true); + $this->assertTrue($response['success']); + $this->assertArrayHasKey('progress', $response); + $this->assertArrayHasKey('completed_count', $response); + + // Clean up + $entityManager->remove($job); + $entityManager->flush(); + } + + public function testMarkPartSkipped(): void + { + $client = static::createClient(); + $this->loginAsUser($client, 'admin'); + + $entityManager = $client->getContainer()->get('doctrine')->getManager(); + $userRepository = $entityManager->getRepository(User::class); + $user = $userRepository->findOneBy(['name' => 'admin']); + + if (!$user) { + $this->markTestSkipped('Admin user not found in fixtures'); + } + + $job = new BulkInfoProviderImportJob(); + $job->setCreatedBy($user); + $job->setPartIds([1, 2]); + $job->setStatus(BulkImportJobStatus::IN_PROGRESS); + $job->setSearchResults([]); + + $entityManager->persist($job); + $entityManager->flush(); + + $client->request('POST', '/en/tools/bulk-info-provider-import/job/' . $job->getId() . '/part/1/mark-skipped', [ + 'reason' => 'Test skip reason' + ]); + + $this->assertResponseStatusCodeSame(Response::HTTP_OK); + $response = json_decode($client->getResponse()->getContent(), true); + $this->assertTrue($response['success']); + $this->assertArrayHasKey('skipped_count', $response); + + // Clean up + $entityManager->remove($job); + $entityManager->flush(); + } + + public function testMarkPartPending(): void + { + $client = static::createClient(); + $this->loginAsUser($client, 'admin'); + + $entityManager = $client->getContainer()->get('doctrine')->getManager(); + $userRepository = $entityManager->getRepository(User::class); + $user = $userRepository->findOneBy(['name' => 'admin']); + + if (!$user) { + $this->markTestSkipped('Admin user not found in fixtures'); + } + + $job = new BulkInfoProviderImportJob(); + $job->setCreatedBy($user); + $job->setPartIds([1]); + $job->setStatus(BulkImportJobStatus::IN_PROGRESS); + $job->setSearchResults([]); + + $entityManager->persist($job); + $entityManager->flush(); + + $client->request('POST', '/en/tools/bulk-info-provider-import/job/' . $job->getId() . '/part/1/mark-pending'); + + $this->assertResponseStatusCodeSame(Response::HTTP_OK); + $response = json_decode($client->getResponse()->getContent(), true); + $this->assertTrue($response['success']); + + // Clean up + $entityManager->remove($job); + $entityManager->flush(); + } + + public function testStep2WithNonExistentJob(): void + { + $client = static::createClient(); + $this->loginAsUser($client, 'admin'); + + $client->request('GET', '/tools/bulk-info-provider-import/step2/999999'); + + $this->assertResponseRedirects(); + } + + public function testStep2WithUnauthorizedAccess(): void + { + $client = static::createClient(); + $this->loginAsUser($client, 'admin'); + + $entityManager = $client->getContainer()->get('doctrine')->getManager(); + $userRepository = $entityManager->getRepository(User::class); + $admin = $userRepository->findOneBy(['name' => 'admin']); + $readonly = $userRepository->findOneBy(['name' => 'noread']); + + if (!$admin || !$readonly) { + $this->markTestSkipped('Required test users not found in fixtures'); + } + + // Create job as admin + $job = new BulkInfoProviderImportJob(); + $job->setCreatedBy($admin); + $job->setPartIds([1]); + $job->setStatus(BulkImportJobStatus::IN_PROGRESS); + $job->setSearchResults([]); + + $entityManager->persist($job); + $entityManager->flush(); + + // Try to access as readonly user + $this->loginAsUser($client, 'noread'); + $client->request('GET', '/tools/bulk-info-provider-import/step2/' . $job->getId()); + + $this->assertResponseRedirects(); + + // Clean up + $entityManager->remove($job); + $entityManager->flush(); + } + + public function testJobAccessControlForDelete(): void + { + $client = static::createClient(); + $this->loginAsUser($client, 'admin'); + + $entityManager = $client->getContainer()->get('doctrine')->getManager(); + $userRepository = $entityManager->getRepository(User::class); + $admin = $userRepository->findOneBy(['name' => 'admin']); + $readonly = $userRepository->findOneBy(['name' => 'noread']); + + if (!$admin || !$readonly) { + $this->markTestSkipped('Required test users not found in fixtures'); + } + + // Create job as readonly user + $job = new BulkInfoProviderImportJob(); + $job->setCreatedBy($readonly); + $job->setPartIds([1]); + $job->setStatus(BulkImportJobStatus::COMPLETED); + $job->setSearchResults([]); + + $entityManager->persist($job); + $entityManager->flush(); + + // Try to delete as admin (should fail due to ownership) + $client->request('DELETE', '/en/tools/bulk-info-provider-import/job/' . $job->getId() . '/delete'); + + $this->assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND); + + // Clean up + $entityManager->remove($job); + $entityManager->flush(); + } + 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'); + $this->markTestSkipped("User {$username} not found"); } - + $client->loginUser($user); } } \ No newline at end of file diff --git a/tests/Controller/PartControllerTest.php b/tests/Controller/PartControllerTest.php new file mode 100644 index 00000000..b6a1ec19 --- /dev/null +++ b/tests/Controller/PartControllerTest.php @@ -0,0 +1,334 @@ +. + */ + +declare(strict_types=1); + +namespace App\Tests\Controller; + +use App\Entity\Parts\Part; +use App\Entity\Parts\PartLot; +use App\Entity\Parts\Category; +use App\Entity\Parts\Footprint; +use App\Entity\Parts\Manufacturer; +use App\Entity\Parts\StorageLocation; +use App\Entity\Parts\Supplier; +use App\Entity\UserSystem\User; +use App\Entity\BulkInfoProviderImportJob; +use App\Entity\BulkImportJobStatus; +use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; +use Symfony\Component\HttpFoundation\Response; + +/** + * @group slow + * @group DB + */ +class PartControllerTest extends WebTestCase +{ + public function testShowPart(): void + { + $client = static::createClient(); + $this->loginAsUser($client, 'admin'); + + $entityManager = $client->getContainer()->get('doctrine')->getManager(); + $partRepository = $entityManager->getRepository(Part::class); + $part = $partRepository->find(1); + + if (!$part) { + $this->markTestSkipped('Test part with ID 1 not found in fixtures'); + } + + $client->request('GET', '/en/part/' . $part->getId()); + + $this->assertResponseStatusCodeSame(Response::HTTP_OK); + $this->assertResponseStatusCodeSame(Response::HTTP_OK); + } + + public function testShowPartWithTimestamp(): void + { + $client = static::createClient(); + $this->loginAsUser($client, 'admin'); + + $entityManager = $client->getContainer()->get('doctrine')->getManager(); + $partRepository = $entityManager->getRepository(Part::class); + $part = $partRepository->find(1); + + if (!$part) { + $this->markTestSkipped('Test part with ID 1 not found in fixtures'); + } + + $timestamp = time(); + $client->request('GET', "/en/part/{$part->getId()}/info/{$timestamp}"); + + $this->assertResponseStatusCodeSame(Response::HTTP_OK); + } + + public function testEditPart(): void + { + $client = static::createClient(); + $this->loginAsUser($client, 'admin'); + + $entityManager = $client->getContainer()->get('doctrine')->getManager(); + $partRepository = $entityManager->getRepository(Part::class); + $part = $partRepository->find(1); + + if (!$part) { + $this->markTestSkipped('Test part with ID 1 not found in fixtures'); + } + + $client->request('GET', '/en/part/' . $part->getId() . '/edit'); + + $this->assertResponseStatusCodeSame(Response::HTTP_OK); + $this->assertSelectorExists('form[name="part_base"]'); + } + + public function testEditPartWithBulkJob(): void + { + $client = static::createClient(); + $this->loginAsUser($client, 'admin'); + + $entityManager = $client->getContainer()->get('doctrine')->getManager(); + $partRepository = $entityManager->getRepository(Part::class); + $part = $partRepository->find(1); + + $userRepository = $entityManager->getRepository(User::class); + $user = $userRepository->findOneBy(['name' => 'admin']); + + if (!$part || !$user) { + $this->markTestSkipped('Required test data not found in fixtures'); + } + + // Create a bulk job + $job = new BulkInfoProviderImportJob(); + $job->setCreatedBy($user); + $job->setPartIds([$part->getId()]); + $job->setStatus(BulkImportJobStatus::IN_PROGRESS); + $job->setSearchResults([]); + + $entityManager->persist($job); + $entityManager->flush(); + + $client->request('GET', '/en/part/' . $part->getId() . '/edit?jobId=' . $job->getId()); + + $this->assertResponseStatusCodeSame(Response::HTTP_OK); + + // Clean up + $entityManager->remove($job); + $entityManager->flush(); + } + + + + public function testNewPart(): void + { + $client = static::createClient(); + $this->loginAsUser($client, 'admin'); + + $client->request('GET', '/en/part/new'); + + $this->assertResponseStatusCodeSame(Response::HTTP_OK); + $this->assertSelectorExists('form[name="part_base"]'); + } + + public function testNewPartWithCategory(): void + { + $client = static::createClient(); + $this->loginAsUser($client, 'admin'); + + $entityManager = $client->getContainer()->get('doctrine')->getManager(); + $categoryRepository = $entityManager->getRepository(Category::class); + $category = $categoryRepository->find(1); + + if (!$category) { + $this->markTestSkipped('Test category with ID 1 not found in fixtures'); + } + + $client->request('GET', '/en/part/new?category=' . $category->getId()); + + $this->assertResponseStatusCodeSame(Response::HTTP_OK); + } + + public function testNewPartWithFootprint(): void + { + $client = static::createClient(); + $this->loginAsUser($client, 'admin'); + + $entityManager = $client->getContainer()->get('doctrine')->getManager(); + $footprintRepository = $entityManager->getRepository(Footprint::class); + $footprint = $footprintRepository->find(1); + + if (!$footprint) { + $this->markTestSkipped('Test footprint with ID 1 not found in fixtures'); + } + + $client->request('GET', '/en/part/new?footprint=' . $footprint->getId()); + + $this->assertResponseStatusCodeSame(Response::HTTP_OK); + } + + public function testNewPartWithManufacturer(): void + { + $client = static::createClient(); + $this->loginAsUser($client, 'admin'); + + $entityManager = $client->getContainer()->get('doctrine')->getManager(); + $manufacturerRepository = $entityManager->getRepository(Manufacturer::class); + $manufacturer = $manufacturerRepository->find(1); + + if (!$manufacturer) { + $this->markTestSkipped('Test manufacturer with ID 1 not found in fixtures'); + } + + $client->request('GET', '/en/part/new?manufacturer=' . $manufacturer->getId()); + + $this->assertResponseStatusCodeSame(Response::HTTP_OK); + } + + public function testNewPartWithStorageLocation(): void + { + $client = static::createClient(); + $this->loginAsUser($client, 'admin'); + + $entityManager = $client->getContainer()->get('doctrine')->getManager(); + $storageLocationRepository = $entityManager->getRepository(StorageLocation::class); + $storageLocation = $storageLocationRepository->find(1); + + if (!$storageLocation) { + $this->markTestSkipped('Test storage location with ID 1 not found in fixtures'); + } + + $client->request('GET', '/en/part/new?storelocation=' . $storageLocation->getId()); + + $this->assertResponseStatusCodeSame(Response::HTTP_OK); + } + + public function testNewPartWithSupplier(): void + { + $client = static::createClient(); + $this->loginAsUser($client, 'admin'); + + $entityManager = $client->getContainer()->get('doctrine')->getManager(); + $supplierRepository = $entityManager->getRepository(Supplier::class); + $supplier = $supplierRepository->find(1); + + if (!$supplier) { + $this->markTestSkipped('Test supplier with ID 1 not found in fixtures'); + } + + $client->request('GET', '/en/part/new?supplier=' . $supplier->getId()); + + $this->assertResponseStatusCodeSame(Response::HTTP_OK); + } + + public function testClonePart(): void + { + $client = static::createClient(); + $this->loginAsUser($client, 'admin'); + + $entityManager = $client->getContainer()->get('doctrine')->getManager(); + $partRepository = $entityManager->getRepository(Part::class); + $part = $partRepository->find(1); + + if (!$part) { + $this->markTestSkipped('Test part with ID 1 not found in fixtures'); + } + + $client->request('GET', '/en/part/' . $part->getId() . '/clone'); + + $this->assertResponseStatusCodeSame(Response::HTTP_OK); + $this->assertSelectorExists('form[name="part_base"]'); + } + + public function testMergeParts(): void + { + $client = static::createClient(); + $this->loginAsUser($client, 'admin'); + + $entityManager = $client->getContainer()->get('doctrine')->getManager(); + $categoryRepository = $entityManager->getRepository(Category::class); + $category = $categoryRepository->find(1); + + if (!$category) { + $this->markTestSkipped('Test category with ID 1 not found in fixtures'); + } + + // Create two test parts + $targetPart = new Part(); + $targetPart->setName('Target Part'); + $targetPart->setCategory($category); + + $otherPart = new Part(); + $otherPart->setName('Other Part'); + $otherPart->setCategory($category); + + $entityManager->persist($targetPart); + $entityManager->persist($otherPart); + $entityManager->flush(); + + $client->request('GET', "/en/part/{$targetPart->getId()}/merge/{$otherPart->getId()}"); + + $this->assertResponseStatusCodeSame(Response::HTTP_OK); + $this->assertSelectorExists('form[name="part_base"]'); + + // Clean up + $entityManager->remove($targetPart); + $entityManager->remove($otherPart); + $entityManager->flush(); + } + + + + + + public function testAccessControlForUnauthorizedUser(): void + { + $client = static::createClient(); + $this->loginAsUser($client, 'noread'); + + $entityManager = $client->getContainer()->get('doctrine')->getManager(); + $partRepository = $entityManager->getRepository(Part::class); + $part = $partRepository->find(1); + + if (!$part) { + $this->markTestSkipped('Test part with ID 1 not found in fixtures'); + } + + $client->request('GET', '/en/part/' . $part->getId()); + + // Should either be forbidden or redirected to error page + $this->assertTrue( + $client->getResponse()->getStatusCode() === Response::HTTP_FORBIDDEN || + $client->getResponse()->isRedirect() + ); + } + + 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); + } + +} \ No newline at end of file From cc9d50a8fe72d9d5e27339919231ca3c454646a7 Mon Sep 17 00:00:00 2001 From: barisgit Date: Sat, 2 Aug 2025 23:35:30 +0200 Subject: [PATCH 028/228] Add makefile to help with development setup, change part_ids in bulk import jobs to junction table and implement filtering based on bulk import jobs status and its associated parts' statuses. --- Makefile | 99 ++++++++++ migrations/Version20250802153643.php | 52 ------ migrations/Version20250802205143.php | 70 +++++++ .../BulkInfoProviderImportController.php | 13 +- .../Part/BulkImportJobExistsConstraint.php | 82 +++++++++ .../Part/BulkImportJobStatusConstraint.php | 105 +++++++++++ .../Part/BulkImportPartStatusConstraint.php | 104 +++++++++++ src/DataTables/Filters/PartFilter.php | 20 +- src/DataTables/PartsDataTable.php | 26 ++- src/Entity/BulkInfoProviderImportJob.php | 120 +++++++++--- src/Entity/BulkInfoProviderImportJobPart.php | 172 ++++++++++++++++++ src/Entity/LogSystem/LogTargetType.php | 3 + src/Entity/Parts/Part.php | 60 +++++- .../BulkImportJobExistsConstraintType.php | 63 +++++++ .../BulkImportJobStatusConstraintType.php | 80 ++++++++ .../BulkImportPartStatusConstraintType.php | 79 ++++++++ src/Form/Filters/LogFilterType.php | 5 +- src/Form/Filters/PartFilterType.php | 21 +++ templates/parts/lists/_filter.html.twig | 12 ++ .../BulkInfoProviderImportControllerTest.php | 77 +++++++- .../Entity/BulkInfoProviderImportJobTest.php | 118 ++++++++++-- translations/messages.en.xlf | 96 ++++++++++ 22 files changed, 1357 insertions(+), 120 deletions(-) create mode 100644 Makefile delete mode 100644 migrations/Version20250802153643.php create mode 100644 migrations/Version20250802205143.php create mode 100644 src/DataTables/Filters/Constraints/Part/BulkImportJobExistsConstraint.php create mode 100644 src/DataTables/Filters/Constraints/Part/BulkImportJobStatusConstraint.php create mode 100644 src/DataTables/Filters/Constraints/Part/BulkImportPartStatusConstraint.php create mode 100644 src/Entity/BulkInfoProviderImportJobPart.php create mode 100644 src/Form/Filters/Constraints/BulkImportJobExistsConstraintType.php create mode 100644 src/Form/Filters/Constraints/BulkImportJobStatusConstraintType.php create mode 100644 src/Form/Filters/Constraints/BulkImportPartStatusConstraintType.php diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..68c63d3f --- /dev/null +++ b/Makefile @@ -0,0 +1,99 @@ +# PartDB Makefile for Test Environment Management + +.PHONY: help test-setup test-clean test-db-create test-db-migrate test-cache-clear test-fixtures test-run dev-setup dev-clean dev-db-create dev-db-migrate dev-cache-clear dev-warmup dev-reset + +# Default target +help: + @echo "PartDB Test Environment Management" + @echo "==================================" + @echo "" + @echo "Available targets:" + @echo " test-setup - Complete test environment setup (clean, create DB, migrate, load fixtures)" + @echo " test-clean - Clean test cache and database files" + @echo " test-db-create - Create test database (if not exists)" + @echo " test-db-migrate - Run database migrations for test environment" + @echo " test-cache-clear- Clear test cache" + @echo " test-fixtures - Load test fixtures" + @echo " test-run - Run PHPUnit tests" + @echo "" + @echo "Development Environment:" + @echo " dev-setup - Complete development environment setup (clean, create DB, migrate, warmup)" + @echo " dev-clean - Clean development cache and database files" + @echo " dev-db-create - Create development database (if not exists)" + @echo " dev-db-migrate - Run database migrations for development environment" + @echo " dev-cache-clear - Clear development cache" + @echo " dev-warmup - Warm up development cache" + @echo " dev-reset - Quick development reset (clean + migrate)" + @echo "" + @echo " help - Show this help message" + +# Complete test environment setup +test-setup: test-clean test-db-create test-db-migrate test-fixtures + @echo "✅ Test environment setup complete!" + +# Clean test environment +test-clean: + @echo "🧹 Cleaning test environment..." + rm -rf var/cache/test + rm -f var/app_test.db + @echo "✅ Test environment cleaned" + +# Create test database +test-db-create: + @echo "🗄️ Creating test database..." + -php bin/console doctrine:database:create --if-not-exists --env test || echo "⚠️ Database creation failed (expected for SQLite) - continuing..." + +# Run database migrations for test environment +test-db-migrate: + @echo "🔄 Running database migrations..." + COMPOSER_MEMORY_LIMIT=-1 php bin/console doctrine:migrations:migrate -n --env test + +# Clear test cache +test-cache-clear: + @echo "🗑️ Clearing test cache..." + rm -rf var/cache/test + @echo "✅ Test cache cleared" + +# Load test fixtures +test-fixtures: + @echo "📦 Loading test fixtures..." + php bin/console partdb:fixtures:load -n --env test + +# Run PHPUnit tests +test-run: + @echo "🧪 Running tests..." + php bin/phpunit + +# Quick test reset (clean + migrate + fixtures, skip DB creation) +test-reset: test-cache-clear test-db-migrate test-fixtures + @echo "✅ Test environment reset complete!" + +# Development helpers +dev-setup: dev-clean dev-db-create dev-db-migrate dev-warmup + @echo "✅ Development environment setup complete!" + +dev-clean: + @echo "🧹 Cleaning development environment..." + rm -rf var/cache/dev + rm -f var/app_dev.db + @echo "✅ Development environment cleaned" + +dev-db-create: + @echo "🗄️ Creating development database..." + -php bin/console doctrine:database:create --if-not-exists --env dev || echo "⚠️ Database creation failed (expected for SQLite) - continuing..." + +dev-db-migrate: + @echo "🔄 Running database migrations..." + COMPOSER_MEMORY_LIMIT=-1 php bin/console doctrine:migrations:migrate -n --env dev + +dev-cache-clear: + @echo "🗑️ Clearing development cache..." + rm -rf var/cache/dev + @echo "✅ Development cache cleared" + +dev-warmup: + @echo "🔥 Warming up development cache..." + COMPOSER_MEMORY_LIMIT=-1 php bin/console cache:warmup --env dev -n --memory-limit=1G + +dev-reset: dev-cache-clear dev-db-migrate + @echo "✅ Development environment reset complete!" \ No newline at end of file diff --git a/migrations/Version20250802153643.php b/migrations/Version20250802153643.php deleted file mode 100644 index 2b2873f9..00000000 --- a/migrations/Version20250802153643.php +++ /dev/null @@ -1,52 +0,0 @@ -addSql('CREATE TABLE bulk_info_provider_import_jobs (id INT AUTO_INCREMENT NOT NULL, name LONGTEXT NOT NULL, part_ids LONGTEXT NOT NULL, field_mappings LONGTEXT NOT NULL, search_results LONGTEXT NOT NULL, status VARCHAR(20) NOT NULL, created_at DATETIME NOT NULL, completed_at DATETIME DEFAULT NULL, prefetch_details TINYINT(1) NOT NULL, progress LONGTEXT NOT NULL, created_by_id INT NOT NULL, CONSTRAINT FK_7F58C1EDB03A8386 FOREIGN KEY (created_by_id) REFERENCES `users` (id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB'); - $this->addSql('CREATE INDEX IDX_7F58C1EDB03A8386 ON bulk_info_provider_import_jobs (created_by_id)'); - } - - public function mySQLDown(Schema $schema): void - { - $this->addSql('DROP TABLE bulk_info_provider_import_jobs'); - } - - public function sqLiteUp(Schema $schema): void - { - $this->addSql('CREATE TABLE bulk_info_provider_import_jobs (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name CLOB NOT NULL, part_ids CLOB NOT NULL, field_mappings CLOB NOT NULL, search_results CLOB NOT NULL, status VARCHAR(20) NOT NULL, created_at DATETIME NOT NULL, completed_at DATETIME DEFAULT NULL, prefetch_details BOOLEAN NOT NULL, progress CLOB NOT NULL, created_by_id INTEGER NOT NULL, CONSTRAINT FK_7F58C1EDB03A8386 FOREIGN KEY (created_by_id) REFERENCES "users" (id) NOT DEFERRABLE INITIALLY IMMEDIATE)'); - $this->addSql('CREATE INDEX IDX_7F58C1EDB03A8386 ON bulk_info_provider_import_jobs (created_by_id)'); - } - - public function sqLiteDown(Schema $schema): void - { - $this->addSql('DROP TABLE bulk_info_provider_import_jobs'); - } - - public function postgreSQLUp(Schema $schema): void - { - $this->addSql('CREATE TABLE bulk_info_provider_import_jobs (id SERIAL PRIMARY KEY NOT NULL, name TEXT NOT NULL, part_ids TEXT NOT NULL, field_mappings TEXT NOT NULL, search_results TEXT NOT NULL, status VARCHAR(20) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, completed_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, prefetch_details BOOLEAN NOT NULL, progress TEXT NOT NULL, created_by_id INT NOT NULL, CONSTRAINT FK_7F58C1EDB03A8386 FOREIGN KEY (created_by_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE)'); - $this->addSql('CREATE INDEX IDX_7F58C1EDB03A8386 ON bulk_info_provider_import_jobs (created_by_id)'); - } - - public function postgreSQLDown(Schema $schema): void - { - $this->addSql('DROP TABLE bulk_info_provider_import_jobs'); - } -} diff --git a/migrations/Version20250802205143.php b/migrations/Version20250802205143.php new file mode 100644 index 00000000..5eb09a77 --- /dev/null +++ b/migrations/Version20250802205143.php @@ -0,0 +1,70 @@ +addSql('CREATE TABLE bulk_info_provider_import_jobs (id INT AUTO_INCREMENT NOT NULL, name LONGTEXT NOT NULL, field_mappings LONGTEXT NOT NULL, search_results LONGTEXT NOT NULL, status VARCHAR(20) NOT NULL, created_at DATETIME NOT NULL, completed_at DATETIME DEFAULT NULL, prefetch_details TINYINT(1) NOT NULL, created_by_id INT NOT NULL, CONSTRAINT FK_7F58C1EDB03A8386 FOREIGN KEY (created_by_id) REFERENCES `users` (id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB'); + $this->addSql('CREATE INDEX IDX_7F58C1EDB03A8386 ON bulk_info_provider_import_jobs (created_by_id)'); + + $this->addSql('CREATE TABLE bulk_info_provider_import_job_parts (id INT AUTO_INCREMENT NOT NULL, status VARCHAR(20) NOT NULL, reason LONGTEXT DEFAULT NULL, completed_at DATETIME DEFAULT NULL, job_id INT NOT NULL, part_id INT NOT NULL, CONSTRAINT FK_CD93F28FBE04EA9 FOREIGN KEY (job_id) REFERENCES bulk_info_provider_import_jobs (id), CONSTRAINT FK_CD93F28F4CE34BEC FOREIGN KEY (part_id) REFERENCES `parts` (id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB'); + $this->addSql('CREATE INDEX IDX_CD93F28FBE04EA9 ON bulk_info_provider_import_job_parts (job_id)'); + $this->addSql('CREATE INDEX IDX_CD93F28F4CE34BEC ON bulk_info_provider_import_job_parts (part_id)'); + $this->addSql('CREATE UNIQUE INDEX unique_job_part ON bulk_info_provider_import_job_parts (job_id, part_id)'); + } + + public function mySQLDown(Schema $schema): void + { + $this->addSql('DROP TABLE bulk_info_provider_import_job_parts'); + $this->addSql('DROP TABLE bulk_info_provider_import_jobs'); + } + + public function sqLiteUp(Schema $schema): void + { + $this->addSql('CREATE TABLE bulk_info_provider_import_jobs (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name CLOB NOT NULL, field_mappings CLOB NOT NULL, search_results CLOB NOT NULL, status VARCHAR(20) NOT NULL, created_at DATETIME NOT NULL, completed_at DATETIME DEFAULT NULL, prefetch_details BOOLEAN NOT NULL, created_by_id INTEGER NOT NULL, CONSTRAINT FK_7F58C1EDB03A8386 FOREIGN KEY (created_by_id) REFERENCES "users" (id) NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('CREATE INDEX IDX_7F58C1EDB03A8386 ON bulk_info_provider_import_jobs (created_by_id)'); + + $this->addSql('CREATE TABLE bulk_info_provider_import_job_parts (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, status VARCHAR(20) NOT NULL, reason CLOB DEFAULT NULL, completed_at DATETIME DEFAULT NULL, job_id INTEGER NOT NULL, part_id INTEGER NOT NULL, CONSTRAINT FK_CD93F28FBE04EA9 FOREIGN KEY (job_id) REFERENCES bulk_info_provider_import_jobs (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_CD93F28F4CE34BEC FOREIGN KEY (part_id) REFERENCES "parts" (id) NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('CREATE INDEX IDX_CD93F28FBE04EA9 ON bulk_info_provider_import_job_parts (job_id)'); + $this->addSql('CREATE INDEX IDX_CD93F28F4CE34BEC ON bulk_info_provider_import_job_parts (part_id)'); + $this->addSql('CREATE UNIQUE INDEX unique_job_part ON bulk_info_provider_import_job_parts (job_id, part_id)'); + } + + public function sqLiteDown(Schema $schema): void + { + $this->addSql('DROP TABLE bulk_info_provider_import_job_parts'); + $this->addSql('DROP TABLE bulk_info_provider_import_jobs'); + } + + public function postgreSQLUp(Schema $schema): void + { + $this->addSql('CREATE TABLE bulk_info_provider_import_jobs (id SERIAL PRIMARY KEY NOT NULL, name TEXT NOT NULL, field_mappings TEXT NOT NULL, search_results TEXT NOT NULL, status VARCHAR(20) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, completed_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, prefetch_details BOOLEAN NOT NULL, created_by_id INT NOT NULL, CONSTRAINT FK_7F58C1EDB03A8386 FOREIGN KEY (created_by_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('CREATE INDEX IDX_7F58C1EDB03A8386 ON bulk_info_provider_import_jobs (created_by_id)'); + + $this->addSql('CREATE TABLE bulk_info_provider_import_job_parts (id SERIAL PRIMARY KEY NOT NULL, status VARCHAR(20) NOT NULL, reason TEXT DEFAULT NULL, completed_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, job_id INT NOT NULL, part_id INT NOT NULL, CONSTRAINT FK_CD93F28FBE04EA9 FOREIGN KEY (job_id) REFERENCES bulk_info_provider_import_jobs (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_CD93F28F4CE34BEC FOREIGN KEY (part_id) REFERENCES parts (id) NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('CREATE INDEX IDX_CD93F28FBE04EA9 ON bulk_info_provider_import_job_parts (job_id)'); + $this->addSql('CREATE INDEX IDX_CD93F28F4CE34BEC ON bulk_info_provider_import_job_parts (part_id)'); + $this->addSql('CREATE UNIQUE INDEX unique_job_part ON bulk_info_provider_import_job_parts (job_id, part_id)'); + } + + public function postgreSQLDown(Schema $schema): void + { + $this->addSql('DROP TABLE bulk_info_provider_import_job_parts'); + $this->addSql('DROP TABLE bulk_info_provider_import_jobs'); + } +} diff --git a/src/Controller/BulkInfoProviderImportController.php b/src/Controller/BulkInfoProviderImportController.php index 82ff21c9..6c434191 100644 --- a/src/Controller/BulkInfoProviderImportController.php +++ b/src/Controller/BulkInfoProviderImportController.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace App\Controller; use App\Entity\BulkInfoProviderImportJob; +use App\Entity\BulkInfoProviderImportJobPart; use App\Entity\BulkImportJobStatus; use App\Entity\Parts\Part; use App\Entity\Parts\Supplier; @@ -104,7 +105,6 @@ class BulkInfoProviderImportController extends AbstractController // Create and save the job $job = new BulkInfoProviderImportJob(); - $job->setPartIds(array_map(fn($part) => $part->getId(), $parts)); $job->setFieldMappings($fieldMappings); $job->setPrefetchDetails($prefetchDetails); $user = $this->getUser(); @@ -113,6 +113,12 @@ class BulkInfoProviderImportController extends AbstractController } $job->setCreatedBy($user); + // Create job parts for each part + foreach ($parts as $part) { + $jobPart = new BulkInfoProviderImportJobPart($job, $part); + $job->addJobPart($jobPart); + } + $this->entityManager->persist($job); $this->entityManager->flush(); @@ -179,7 +185,7 @@ class BulkInfoProviderImportController extends AbstractController // Convert DTOs to result format with metadata $partResult['search_results'] = array_map( - function($dto) use ($dtoMetadata) { + function ($dto) use ($dtoMetadata) { $dtoKey = $dto->provider_key . '|' . $dto->provider_id; $metadata = $dtoMetadata[$dtoKey] ?? []; return [ @@ -372,8 +378,7 @@ class BulkInfoProviderImportController extends AbstractController } // Get the parts and deserialize search results - $partRepository = $this->entityManager->getRepository(Part::class); - $parts = $partRepository->getElementsFromIDArray($job->getPartIds()); + $parts = $job->getJobParts()->map(fn($jobPart) => $jobPart->getPart())->toArray(); $searchResults = $this->deserializeSearchResults($job->getSearchResults(), $parts); return $this->render('info_providers/bulk_import/step2.html.twig', [ diff --git a/src/DataTables/Filters/Constraints/Part/BulkImportJobExistsConstraint.php b/src/DataTables/Filters/Constraints/Part/BulkImportJobExistsConstraint.php new file mode 100644 index 00000000..0e5a3696 --- /dev/null +++ b/src/DataTables/Filters/Constraints/Part/BulkImportJobExistsConstraint.php @@ -0,0 +1,82 @@ +. + */ + +namespace App\DataTables\Filters\Constraints\Part; + +use App\DataTables\Filters\Constraints\AbstractConstraint; +use App\Entity\BulkInfoProviderImportJobPart; +use Doctrine\ORM\QueryBuilder; + +class BulkImportJobExistsConstraint extends AbstractConstraint +{ + /** @var bool|null The value of our constraint */ + protected ?bool $value = null; + + public function __construct() + { + parent::__construct('bulk_import_job_exists'); + } + + /** + * Gets the value of this constraint. Null means "don't filter", true means "filter for parts in bulk import jobs", false means "filter for parts not in bulk import jobs". + */ + public function getValue(): ?bool + { + return $this->value; + } + + /** + * Sets the value of this constraint. Null means "don't filter", true means "filter for parts in bulk import jobs", false means "filter for parts not in bulk import jobs". + */ + public function setValue(?bool $value): void + { + $this->value = $value; + } + + public function isEnabled(): bool + { + return $this->value !== null; + } + + public function apply(QueryBuilder $queryBuilder): void + { + // Do not apply a filter if value is null (filter is set to ignore) + if (!$this->isEnabled()) { + return; + } + + // Use EXISTS subquery to avoid join conflicts + $existsSubquery = $queryBuilder->getEntityManager()->createQueryBuilder(); + $existsSubquery->select('1') + ->from(BulkInfoProviderImportJobPart::class, 'bip_exists') + ->where('bip_exists.part = part.id'); + + if ($this->value === true) { + // Filter for parts that ARE in bulk import jobs + $queryBuilder->andWhere('EXISTS (' . $existsSubquery->getDQL() . ')'); + } else { + // Filter for parts that are NOT in bulk import jobs + $queryBuilder->andWhere('NOT EXISTS (' . $existsSubquery->getDQL() . ')'); + } + } +} \ No newline at end of file diff --git a/src/DataTables/Filters/Constraints/Part/BulkImportJobStatusConstraint.php b/src/DataTables/Filters/Constraints/Part/BulkImportJobStatusConstraint.php new file mode 100644 index 00000000..cc5c8ce0 --- /dev/null +++ b/src/DataTables/Filters/Constraints/Part/BulkImportJobStatusConstraint.php @@ -0,0 +1,105 @@ +. + */ + +namespace App\DataTables\Filters\Constraints\Part; + +use App\DataTables\Filters\Constraints\AbstractConstraint; +use App\Entity\BulkInfoProviderImportJobPart; +use Doctrine\ORM\QueryBuilder; + +class BulkImportJobStatusConstraint extends AbstractConstraint +{ + /** @var array The status values to filter by */ + protected array $values = []; + + /** @var string|null The operator to use ('any_of', 'none_of', 'all_of') */ + protected ?string $operator = null; + + public function __construct() + { + parent::__construct('bulk_import_job_status'); + } + + /** + * Gets the status values to filter by. + */ + public function getValues(): array + { + return $this->values; + } + + /** + * Sets the status values to filter by. + */ + public function setValues(array $values): void + { + $this->values = $values; + } + + /** + * Gets the operator to use. + */ + public function getOperator(): ?string + { + return $this->operator; + } + + /** + * Sets the operator to use. + */ + public function setOperator(?string $operator): void + { + $this->operator = $operator; + } + + public function isEnabled(): bool + { + return !empty($this->values) && $this->operator !== null; + } + + public function apply(QueryBuilder $queryBuilder): void + { + // Do not apply a filter if values are empty or operator is null + if (!$this->isEnabled()) { + return; + } + + // Use EXISTS subquery to check if part has a job with the specified status(es) + $existsSubquery = $queryBuilder->getEntityManager()->createQueryBuilder(); + $existsSubquery->select('1') + ->from(BulkInfoProviderImportJobPart::class, 'bip_status') + ->join('bip_status.job', 'job_status') + ->where('bip_status.part = part.id'); + + // Add status conditions based on operator + if ($this->operator === 'ANY') { + $existsSubquery->andWhere('job_status.status IN (:job_status_values)'); + $queryBuilder->andWhere('EXISTS (' . $existsSubquery->getDQL() . ')'); + $queryBuilder->setParameter('job_status_values', $this->values); + } elseif ($this->operator === 'NONE') { + $existsSubquery->andWhere('job_status.status IN (:job_status_values)'); + $queryBuilder->andWhere('NOT EXISTS (' . $existsSubquery->getDQL() . ')'); + $queryBuilder->setParameter('job_status_values', $this->values); + } + } +} \ No newline at end of file diff --git a/src/DataTables/Filters/Constraints/Part/BulkImportPartStatusConstraint.php b/src/DataTables/Filters/Constraints/Part/BulkImportPartStatusConstraint.php new file mode 100644 index 00000000..168934d6 --- /dev/null +++ b/src/DataTables/Filters/Constraints/Part/BulkImportPartStatusConstraint.php @@ -0,0 +1,104 @@ +. + */ + +namespace App\DataTables\Filters\Constraints\Part; + +use App\DataTables\Filters\Constraints\AbstractConstraint; +use App\Entity\BulkInfoProviderImportJobPart; +use Doctrine\ORM\QueryBuilder; + +class BulkImportPartStatusConstraint extends AbstractConstraint +{ + /** @var array The status values to filter by */ + protected array $values = []; + + /** @var string|null The operator to use ('any_of', 'none_of', 'all_of') */ + protected ?string $operator = null; + + public function __construct() + { + parent::__construct('bulk_import_part_status'); + } + + /** + * Gets the status values to filter by. + */ + public function getValues(): array + { + return $this->values; + } + + /** + * Sets the status values to filter by. + */ + public function setValues(array $values): void + { + $this->values = $values; + } + + /** + * Gets the operator to use. + */ + public function getOperator(): ?string + { + return $this->operator; + } + + /** + * Sets the operator to use. + */ + public function setOperator(?string $operator): void + { + $this->operator = $operator; + } + + public function isEnabled(): bool + { + return !empty($this->values) && $this->operator !== null; + } + + public function apply(QueryBuilder $queryBuilder): void + { + // Do not apply a filter if values are empty or operator is null + if (!$this->isEnabled()) { + return; + } + + // Use EXISTS subquery to check if part has the specified status(es) + $existsSubquery = $queryBuilder->getEntityManager()->createQueryBuilder(); + $existsSubquery->select('1') + ->from(BulkInfoProviderImportJobPart::class, 'bip_part_status') + ->where('bip_part_status.part = part.id'); + + // Add status conditions based on operator + if ($this->operator === 'ANY') { + $existsSubquery->andWhere('bip_part_status.status IN (:part_status_values)'); + $queryBuilder->andWhere('EXISTS (' . $existsSubquery->getDQL() . ')'); + $queryBuilder->setParameter('part_status_values', $this->values); + } elseif ($this->operator === 'NONE') { + $existsSubquery->andWhere('bip_part_status.status IN (:part_status_values)'); + $queryBuilder->andWhere('NOT EXISTS (' . $existsSubquery->getDQL() . ')'); + $queryBuilder->setParameter('part_status_values', $this->values); + } + } +} \ No newline at end of file diff --git a/src/DataTables/Filters/PartFilter.php b/src/DataTables/Filters/PartFilter.php index ff98c76f..a13bb929 100644 --- a/src/DataTables/Filters/PartFilter.php +++ b/src/DataTables/Filters/PartFilter.php @@ -31,6 +31,9 @@ use App\DataTables\Filters\Constraints\NumberConstraint; use App\DataTables\Filters\Constraints\Part\LessThanDesiredConstraint; use App\DataTables\Filters\Constraints\Part\ParameterConstraint; use App\DataTables\Filters\Constraints\Part\TagsConstraint; +use App\DataTables\Filters\Constraints\Part\BulkImportJobExistsConstraint; +use App\DataTables\Filters\Constraints\Part\BulkImportJobStatusConstraint; +use App\DataTables\Filters\Constraints\Part\BulkImportPartStatusConstraint; use App\DataTables\Filters\Constraints\TextConstraint; use App\Entity\Attachments\AttachmentType; use App\Entity\Parts\Category; @@ -42,6 +45,8 @@ use App\Entity\Parts\StorageLocation; use App\Entity\Parts\Supplier; use App\Entity\ProjectSystem\Project; use App\Entity\UserSystem\User; +use App\Entity\BulkInfoProviderImportJob; +use App\Entity\BulkInfoProviderImportJobPart; use App\Services\Trees\NodesListBuilder; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ORM\QueryBuilder; @@ -101,6 +106,14 @@ class PartFilter implements FilterInterface public readonly TextConstraint $bomName; public readonly TextConstraint $bomComment; + /************************************************* + * Bulk Import Job tab + *************************************************/ + + public readonly BulkImportJobExistsConstraint $inBulkImportJob; + public readonly BulkImportJobStatusConstraint $bulkImportJobStatus; + public readonly BulkImportPartStatusConstraint $bulkImportPartStatus; + public function __construct(NodesListBuilder $nodesListBuilder) { $this->name = new TextConstraint('part.name'); @@ -126,7 +139,7 @@ class PartFilter implements FilterInterface */ $this->amountSum = (new IntConstraint('( SELECT COALESCE(SUM(__partLot.amount), 0.0) - FROM '.PartLot::class.' __partLot + FROM ' . PartLot::class . ' __partLot WHERE __partLot.part = part.id AND __partLot.instock_unknown = false AND (__partLot.expiration_date IS NULL OR __partLot.expiration_date > CURRENT_DATE()) @@ -162,6 +175,11 @@ class PartFilter implements FilterInterface $this->bomName = new TextConstraint('_projectBomEntries.name'); $this->bomComment = new TextConstraint('_projectBomEntries.comment'); + // Bulk Import Job filters + $this->inBulkImportJob = new BulkImportJobExistsConstraint(); + $this->bulkImportJobStatus = new BulkImportJobStatusConstraint(); + $this->bulkImportPartStatus = new BulkImportPartStatusConstraint(); + } public function apply(QueryBuilder $queryBuilder): void diff --git a/src/DataTables/PartsDataTable.php b/src/DataTables/PartsDataTable.php index f0decf27..f63cb9a4 100644 --- a/src/DataTables/PartsDataTable.php +++ b/src/DataTables/PartsDataTable.php @@ -43,6 +43,7 @@ use App\Entity\Parts\ManufacturingStatus; use App\Entity\Parts\Part; use App\Entity\Parts\PartLot; use App\Entity\ProjectSystem\Project; +use App\Entity\BulkInfoProviderImportJobPart; use App\Services\EntityURLGenerator; use App\Services\Formatters\AmountFormatter; use App\Settings\BehaviorSettings\TableSettings; @@ -142,23 +143,25 @@ final class PartsDataTable implements DataTableTypeInterface 'label' => $this->translator->trans('part.table.storeLocations'), //We need to use a aggregate function to get the first store location, as we have a one-to-many relation 'orderField' => 'NATSORT(MIN(_storelocations.name))', - 'render' => fn ($value, Part $context) => $this->partDataTableHelper->renderStorageLocations($context), + 'render' => fn($value, Part $context) => $this->partDataTableHelper->renderStorageLocations($context), ], alias: 'storage_location') ->add('amount', TextColumn::class, [ 'label' => $this->translator->trans('part.table.amount'), - 'render' => fn ($value, Part $context) => $this->partDataTableHelper->renderAmount($context), + 'render' => fn($value, Part $context) => $this->partDataTableHelper->renderAmount($context), 'orderField' => 'amountSum' ]) ->add('minamount', TextColumn::class, [ 'label' => $this->translator->trans('part.table.minamount'), - 'render' => fn($value, Part $context): string => htmlspecialchars($this->amountFormatter->format($value, - $context->getPartUnit())), + 'render' => fn($value, Part $context): string => htmlspecialchars($this->amountFormatter->format( + $value, + $context->getPartUnit() + )), ]) ->add('partUnit', TextColumn::class, [ 'label' => $this->translator->trans('part.table.partUnit'), 'orderField' => 'NATSORT(_partUnit.name)', - 'render' => function($value, Part $context): string { + 'render' => function ($value, Part $context): string { $partUnit = $context->getPartUnit(); if ($partUnit === null) { return ''; @@ -167,7 +170,7 @@ final class PartsDataTable implements DataTableTypeInterface $tmp = htmlspecialchars($partUnit->getName()); if ($partUnit->getUnit()) { - $tmp .= ' ('.htmlspecialchars($partUnit->getUnit()).')'; + $tmp .= ' (' . htmlspecialchars($partUnit->getUnit()) . ')'; } return $tmp; } @@ -230,7 +233,7 @@ final class PartsDataTable implements DataTableTypeInterface } if (count($projects) > $max) { - $tmp .= ", + ".(count($projects) - $max); + $tmp .= ", + " . (count($projects) - $max); } return $tmp; @@ -366,7 +369,7 @@ final class PartsDataTable implements DataTableTypeInterface $builder->addSelect( '( SELECT COALESCE(SUM(partLot.amount), 0.0) - FROM '.PartLot::class.' partLot + FROM ' . PartLot::class . ' partLot WHERE partLot.part = part.id AND partLot.instock_unknown = false AND (partLot.expiration_date IS NULL OR partLot.expiration_date > CURRENT_DATE()) @@ -423,6 +426,13 @@ final class PartsDataTable implements DataTableTypeInterface //Do not group by many-to-* relations, as it would restrict the COUNT having clauses to be maximum 1 //$builder->addGroupBy('_projectBomEntries'); } + if (str_contains($dql, '_jobPart')) { + $builder->leftJoin('part.bulkImportJobParts', '_jobPart'); + $builder->leftJoin('_jobPart.job', '_bulkImportJob'); + //Do not group by many-to-* relations, as it would restrict the COUNT having clauses to be maximum 1 + //$builder->addGroupBy('_jobPart'); + //$builder->addGroupBy('_bulkImportJob'); + } return $builder; } diff --git a/src/Entity/BulkInfoProviderImportJob.php b/src/Entity/BulkInfoProviderImportJob.php index 0525a3b7..2a602030 100644 --- a/src/Entity/BulkInfoProviderImportJob.php +++ b/src/Entity/BulkInfoProviderImportJob.php @@ -23,7 +23,10 @@ declare(strict_types=1); namespace App\Entity; use App\Entity\Base\AbstractDBElement; +use App\Entity\Parts\Part; use App\Entity\UserSystem\User; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; @@ -43,9 +46,6 @@ class BulkInfoProviderImportJob extends AbstractDBElement #[ORM\Column(type: Types::TEXT)] private string $name = ''; - #[ORM\Column(type: Types::JSON)] - private array $partIds = []; - #[ORM\Column(type: Types::JSON)] private array $fieldMappings = []; @@ -68,12 +68,14 @@ class BulkInfoProviderImportJob extends AbstractDBElement #[ORM\JoinColumn(nullable: false)] private ?User $createdBy = null; - #[ORM\Column(type: Types::JSON)] - private array $progress = []; + /** @var Collection */ + #[ORM\OneToMany(targetEntity: BulkInfoProviderImportJobPart::class, mappedBy: 'job', cascade: ['persist', 'remove'], orphanRemoval: true)] + private Collection $jobParts; public function __construct() { $this->createdAt = new \DateTimeImmutable(); + $this->jobParts = new ArrayCollection(); } public function getName(): string @@ -102,14 +104,50 @@ class BulkInfoProviderImportJob extends AbstractDBElement return $this; } + public function getJobParts(): Collection + { + return $this->jobParts; + } + + public function addJobPart(BulkInfoProviderImportJobPart $jobPart): self + { + if (!$this->jobParts->contains($jobPart)) { + $this->jobParts->add($jobPart); + $jobPart->setJob($this); + } + return $this; + } + + public function removeJobPart(BulkInfoProviderImportJobPart $jobPart): self + { + if ($this->jobParts->removeElement($jobPart)) { + if ($jobPart->getJob() === $this) { + $jobPart->setJob(null); + } + } + return $this; + } + public function getPartIds(): array { - return $this->partIds; + return $this->jobParts->map(fn($jobPart) => $jobPart->getPart()->getId())->toArray(); } public function setPartIds(array $partIds): self { - $this->partIds = $partIds; + // This method is kept for backward compatibility but should be replaced with addJobPart + // Clear existing job parts + $this->jobParts->clear(); + + // Add new job parts (this would need the actual Part entities, not just IDs) + // This is a simplified implementation - in practice, you'd want to pass Part entities + return $this; + } + + public function addPart(Part $part): self + { + $jobPart = new BulkInfoProviderImportJobPart($this, $part); + $this->addJobPart($jobPart); return $this; } @@ -186,12 +224,31 @@ class BulkInfoProviderImportJob extends AbstractDBElement public function getProgress(): array { - return $this->progress; + $progress = []; + foreach ($this->jobParts as $jobPart) { + $progressData = [ + 'status' => $jobPart->getStatus()->value + ]; + + // Only include completed_at if it's not null + if ($jobPart->getCompletedAt() !== null) { + $progressData['completed_at'] = $jobPart->getCompletedAt()->format('c'); + } + + // Only include reason if it's not null + if ($jobPart->getReason() !== null) { + $progressData['reason'] = $jobPart->getReason(); + } + + $progress[$jobPart->getPart()->getId()] = $progressData; + } + return $progress; } public function setProgress(array $progress): self { - $this->progress = $progress; + // This method is kept for backward compatibility + // The progress is now managed through the jobParts relationship return $this; } @@ -254,7 +311,7 @@ class BulkInfoProviderImportJob extends AbstractDBElement public function getPartCount(): int { - return count($this->partIds); + return $this->jobParts->count(); } public function getResultCount(): int @@ -268,48 +325,61 @@ class BulkInfoProviderImportJob extends AbstractDBElement public function markPartAsCompleted(int $partId): self { - $this->progress[$partId] = [ - 'status' => 'completed', - 'completed_at' => (new \DateTimeImmutable())->format('c') - ]; + $jobPart = $this->findJobPartByPartId($partId); + if ($jobPart) { + $jobPart->markAsCompleted(); + } return $this; } public function markPartAsSkipped(int $partId, string $reason = ''): self { - $this->progress[$partId] = [ - 'status' => 'skipped', - 'reason' => $reason, - 'completed_at' => (new \DateTimeImmutable())->format('c') - ]; + $jobPart = $this->findJobPartByPartId($partId); + if ($jobPart) { + $jobPart->markAsSkipped($reason); + } return $this; } public function markPartAsPending(int $partId): self { - // Remove from progress array to mark as pending - unset($this->progress[$partId]); + $jobPart = $this->findJobPartByPartId($partId); + if ($jobPart) { + $jobPart->markAsPending(); + } return $this; } public function isPartCompleted(int $partId): bool { - return isset($this->progress[$partId]) && $this->progress[$partId]['status'] === 'completed'; + $jobPart = $this->findJobPartByPartId($partId); + return $jobPart ? $jobPart->isCompleted() : false; } public function isPartSkipped(int $partId): bool { - return isset($this->progress[$partId]) && $this->progress[$partId]['status'] === 'skipped'; + $jobPart = $this->findJobPartByPartId($partId); + return $jobPart ? $jobPart->isSkipped() : false; } public function getCompletedPartsCount(): int { - return count(array_filter($this->progress, fn($p) => $p['status'] === 'completed')); + return $this->jobParts->filter(fn($jobPart) => $jobPart->isCompleted())->count(); } public function getSkippedPartsCount(): int { - return count(array_filter($this->progress, fn($p) => $p['status'] === 'skipped')); + return $this->jobParts->filter(fn($jobPart) => $jobPart->isSkipped())->count(); + } + + private function findJobPartByPartId(int $partId): ?BulkInfoProviderImportJobPart + { + foreach ($this->jobParts as $jobPart) { + if ($jobPart->getPart()->getId() === $partId) { + return $jobPart; + } + } + return null; } public function getProgressPercentage(): float diff --git a/src/Entity/BulkInfoProviderImportJobPart.php b/src/Entity/BulkInfoProviderImportJobPart.php new file mode 100644 index 00000000..df99aa19 --- /dev/null +++ b/src/Entity/BulkInfoProviderImportJobPart.php @@ -0,0 +1,172 @@ +. + */ + +namespace App\Entity; + +use App\Entity\Base\AbstractDBElement; +use App\Entity\Parts\Part; +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; + +enum BulkImportPartStatus: string +{ + case PENDING = 'pending'; + case COMPLETED = 'completed'; + case SKIPPED = 'skipped'; + case FAILED = 'failed'; +} + +#[ORM\Entity] +#[ORM\Table(name: 'bulk_info_provider_import_job_parts')] +#[ORM\UniqueConstraint(name: 'unique_job_part', columns: ['job_id', 'part_id'])] +class BulkInfoProviderImportJobPart extends AbstractDBElement +{ + #[ORM\ManyToOne(targetEntity: BulkInfoProviderImportJob::class, inversedBy: 'jobParts')] + #[ORM\JoinColumn(nullable: false)] + private BulkInfoProviderImportJob $job; + + #[ORM\ManyToOne(targetEntity: Part::class)] + #[ORM\JoinColumn(nullable: false)] + private Part $part; + + #[ORM\Column(type: Types::STRING, length: 20, enumType: BulkImportPartStatus::class)] + private BulkImportPartStatus $status = BulkImportPartStatus::PENDING; + + #[ORM\Column(type: Types::TEXT, nullable: true)] + private ?string $reason = null; + + #[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)] + private ?\DateTimeImmutable $completedAt = null; + + public function __construct(BulkInfoProviderImportJob $job, Part $part) + { + $this->job = $job; + $this->part = $part; + } + + public function getJob(): BulkInfoProviderImportJob + { + return $this->job; + } + + public function setJob(?BulkInfoProviderImportJob $job): self + { + $this->job = $job; + return $this; + } + + public function getPart(): Part + { + return $this->part; + } + + public function setPart(?Part $part): self + { + $this->part = $part; + return $this; + } + + public function getStatus(): BulkImportPartStatus + { + return $this->status; + } + + public function setStatus(BulkImportPartStatus $status): self + { + $this->status = $status; + return $this; + } + + public function getReason(): ?string + { + return $this->reason; + } + + public function setReason(?string $reason): self + { + $this->reason = $reason; + return $this; + } + + public function getCompletedAt(): ?\DateTimeImmutable + { + return $this->completedAt; + } + + public function setCompletedAt(?\DateTimeImmutable $completedAt): self + { + $this->completedAt = $completedAt; + return $this; + } + + public function markAsCompleted(): self + { + $this->status = BulkImportPartStatus::COMPLETED; + $this->completedAt = new \DateTimeImmutable(); + return $this; + } + + public function markAsSkipped(string $reason = ''): self + { + $this->status = BulkImportPartStatus::SKIPPED; + $this->reason = $reason; + $this->completedAt = new \DateTimeImmutable(); + return $this; + } + + public function markAsFailed(string $reason = ''): self + { + $this->status = BulkImportPartStatus::FAILED; + $this->reason = $reason; + $this->completedAt = new \DateTimeImmutable(); + return $this; + } + + public function markAsPending(): self + { + $this->status = BulkImportPartStatus::PENDING; + $this->reason = null; + $this->completedAt = null; + return $this; + } + + public function isPending(): bool + { + return $this->status === BulkImportPartStatus::PENDING; + } + + public function isCompleted(): bool + { + return $this->status === BulkImportPartStatus::COMPLETED; + } + + public function isSkipped(): bool + { + return $this->status === BulkImportPartStatus::SKIPPED; + } + + public function isFailed(): bool + { + return $this->status === BulkImportPartStatus::FAILED; + } +} \ No newline at end of file diff --git a/src/Entity/LogSystem/LogTargetType.php b/src/Entity/LogSystem/LogTargetType.php index 55c18c1b..1e07ddc5 100644 --- a/src/Entity/LogSystem/LogTargetType.php +++ b/src/Entity/LogSystem/LogTargetType.php @@ -25,6 +25,7 @@ namespace App\Entity\LogSystem; use App\Entity\Attachments\Attachment; use App\Entity\Attachments\AttachmentType; use App\Entity\BulkInfoProviderImportJob; +use App\Entity\BulkInfoProviderImportJobPart; use App\Entity\LabelSystem\LabelProfile; use App\Entity\Parameters\AbstractParameter; use App\Entity\Parts\Category; @@ -69,6 +70,7 @@ enum LogTargetType: int case PART_ASSOCIATION = 20; case BULK_INFO_PROVIDER_IMPORT_JOB = 21; + case BULK_INFO_PROVIDER_IMPORT_JOB_PART = 22; /** * Returns the class name of the target type or null if the target type is NONE. @@ -99,6 +101,7 @@ enum LogTargetType: int self::LABEL_PROFILE => LabelProfile::class, self::PART_ASSOCIATION => PartAssociation::class, self::BULK_INFO_PROVIDER_IMPORT_JOB => BulkInfoProviderImportJob::class, + self::BULK_INFO_PROVIDER_IMPORT_JOB_PART => BulkInfoProviderImportJobPart::class, }; } diff --git a/src/Entity/Parts/Part.php b/src/Entity/Parts/Part.php index 14a7903f..98c1b884 100644 --- a/src/Entity/Parts/Part.php +++ b/src/Entity/Parts/Part.php @@ -55,6 +55,7 @@ use App\Entity\Parts\PartTraits\ManufacturerTrait; use App\Entity\Parts\PartTraits\OrderTrait; use App\Entity\Parts\PartTraits\ProjectTrait; use App\EntityListeners\TreeCacheInvalidationListener; +use App\Entity\BulkInfoProviderImportJobPart; use App\Repository\PartRepository; use App\Validator\Constraints\UniqueObjectCollection; use Doctrine\Common\Collections\ArrayCollection; @@ -83,8 +84,18 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface; #[ORM\Index(columns: ['ipn'], name: 'parts_idx_ipn')] #[ApiResource( operations: [ - new Get(normalizationContext: ['groups' => ['part:read', 'provider_reference:read', 'api:basic:read', 'part_lot:read', - 'orderdetail:read', 'pricedetail:read', 'parameter:read', 'attachment:read', 'eda_info:read'], + new Get(normalizationContext: [ + 'groups' => [ + 'part:read', + 'provider_reference:read', + 'api:basic:read', + 'part_lot:read', + 'orderdetail:read', + 'pricedetail:read', + 'parameter:read', + 'attachment:read', + 'eda_info:read' + ], 'openapi_definition_name' => 'Read', ], security: 'is_granted("read", object)'), new GetCollection(security: 'is_granted("@parts.read")'), @@ -92,7 +103,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface; new Patch(security: 'is_granted("edit", object)'), new Delete(security: 'is_granted("delete", object)'), ], - normalizationContext: ['groups' => ['part:read', 'provider_reference:read', 'api:basic:read', 'part_lot:read'], 'openapi_definition_name' => 'Read'], + normalizationContext: ['groups' => ['part:read', 'provider_reference:read', 'api:basic:read', 'part_lot:read'], 'openapi_definition_name' => 'Read'], denormalizationContext: ['groups' => ['part:write', 'api:basic:write', 'eda_info:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'], )] #[ApiFilter(PropertyFilter::class)] @@ -100,7 +111,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface; #[ApiFilter(PartStoragelocationFilter::class, properties: ["storage_location"])] #[ApiFilter(LikeFilter::class, properties: ["name", "comment", "description", "ipn", "manufacturer_product_number"])] #[ApiFilter(TagFilter::class, properties: ["tags"])] -#[ApiFilter(BooleanFilter::class, properties: ["favorite" , "needs_review"])] +#[ApiFilter(BooleanFilter::class, properties: ["favorite", "needs_review"])] #[ApiFilter(RangeFilter::class, properties: ["mass", "minamount"])] #[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)] #[ApiFilter(OrderFilter::class, properties: ['name', 'id', 'addedDate', 'lastModified'])] @@ -160,6 +171,12 @@ class Part extends AttachmentContainingDBElement #[Groups(['part:read'])] protected ?\DateTimeImmutable $lastModified = null; + /** + * @var Collection + */ + #[ORM\OneToMany(mappedBy: 'part', targetEntity: BulkInfoProviderImportJobPart::class, cascade: ['remove'], orphanRemoval: true)] + protected Collection $bulkImportJobParts; + public function __construct() { @@ -172,6 +189,7 @@ class Part extends AttachmentContainingDBElement $this->associated_parts_as_owner = new ArrayCollection(); $this->associated_parts_as_other = new ArrayCollection(); + $this->bulkImportJobParts = new ArrayCollection(); //By default, the part has no provider $this->providerReference = InfoProviderReference::noProvider(); @@ -230,4 +248,38 @@ class Part extends AttachmentContainingDBElement } } } + + /** + * Get all bulk import job parts for this part + * @return Collection + */ + public function getBulkImportJobParts(): Collection + { + return $this->bulkImportJobParts; + } + + /** + * Add a bulk import job part to this part + */ + public function addBulkImportJobPart(BulkInfoProviderImportJobPart $jobPart): self + { + if (!$this->bulkImportJobParts->contains($jobPart)) { + $this->bulkImportJobParts->add($jobPart); + $jobPart->setPart($this); + } + return $this; + } + + /** + * Remove a bulk import job part from this part + */ + public function removeBulkImportJobPart(BulkInfoProviderImportJobPart $jobPart): self + { + if ($this->bulkImportJobParts->removeElement($jobPart)) { + if ($jobPart->getPart() === $this) { + $jobPart->setPart(null); + } + } + return $this; + } } diff --git a/src/Form/Filters/Constraints/BulkImportJobExistsConstraintType.php b/src/Form/Filters/Constraints/BulkImportJobExistsConstraintType.php new file mode 100644 index 00000000..e26b5f5a --- /dev/null +++ b/src/Form/Filters/Constraints/BulkImportJobExistsConstraintType.php @@ -0,0 +1,63 @@ +. + */ + +namespace App\Form\Filters\Constraints; + +use App\DataTables\Filters\Constraints\Part\BulkImportJobExistsConstraint; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class BulkImportJobExistsConstraintType extends AbstractType +{ + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'compound' => true, + 'data_class' => BulkImportJobExistsConstraint::class, + ]); + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $choices = [ + '' => '', + 'part.filter.in_bulk_import_job.yes' => true, + 'part.filter.in_bulk_import_job.no' => false, + ]; + + $builder->add('value', ChoiceType::class, [ + 'label' => 'part.filter.in_bulk_import_job', + 'choices' => $choices, + 'required' => false, + ]); + } + + public function buildView(FormView $view, FormInterface $form, array $options): void + { + parent::buildView($view, $form, $options); + } +} \ No newline at end of file diff --git a/src/Form/Filters/Constraints/BulkImportJobStatusConstraintType.php b/src/Form/Filters/Constraints/BulkImportJobStatusConstraintType.php new file mode 100644 index 00000000..6809f98b --- /dev/null +++ b/src/Form/Filters/Constraints/BulkImportJobStatusConstraintType.php @@ -0,0 +1,80 @@ +. + */ + +namespace App\Form\Filters\Constraints; + +use App\DataTables\Filters\Constraints\Part\BulkImportJobStatusConstraint; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class BulkImportJobStatusConstraintType extends AbstractType +{ + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'compound' => true, + 'data_class' => BulkImportJobStatusConstraint::class, + ]); + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $statusChoices = [ + 'bulk_import.status.pending' => 'pending', + 'bulk_import.status.in_progress' => 'in_progress', + 'bulk_import.status.completed' => 'completed', + 'bulk_import.status.stopped' => 'stopped', + 'bulk_import.status.failed' => 'failed', + ]; + + $operatorChoices = [ + 'filter.choice_constraint.operator.ANY' => 'ANY', + 'filter.choice_constraint.operator.NONE' => 'NONE', + ]; + + $builder->add('operator', ChoiceType::class, [ + 'label' => 'filter.operator', + 'choices' => $operatorChoices, + 'required' => false, + ]); + + $builder->add('values', ChoiceType::class, [ + 'label' => 'part.filter.bulk_import_job_status', + 'choices' => $statusChoices, + 'required' => false, + 'multiple' => true, + 'attr' => [ + 'data-controller' => 'elements--select-multiple', + ] + ]); + } + + public function buildView(FormView $view, FormInterface $form, array $options): void + { + parent::buildView($view, $form, $options); + } +} \ No newline at end of file diff --git a/src/Form/Filters/Constraints/BulkImportPartStatusConstraintType.php b/src/Form/Filters/Constraints/BulkImportPartStatusConstraintType.php new file mode 100644 index 00000000..e02a3197 --- /dev/null +++ b/src/Form/Filters/Constraints/BulkImportPartStatusConstraintType.php @@ -0,0 +1,79 @@ +. + */ + +namespace App\Form\Filters\Constraints; + +use App\DataTables\Filters\Constraints\Part\BulkImportPartStatusConstraint; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class BulkImportPartStatusConstraintType extends AbstractType +{ + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'compound' => true, + 'data_class' => BulkImportPartStatusConstraint::class, + ]); + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $statusChoices = [ + 'bulk_import.part_status.pending' => 'pending', + 'bulk_import.part_status.completed' => 'completed', + 'bulk_import.part_status.skipped' => 'skipped', + 'bulk_import.part_status.failed' => 'failed', + ]; + + $operatorChoices = [ + 'filter.choice_constraint.operator.ANY' => 'ANY', + 'filter.choice_constraint.operator.NONE' => 'NONE', + ]; + + $builder->add('operator', ChoiceType::class, [ + 'label' => 'filter.operator', + 'choices' => $operatorChoices, + 'required' => false, + ]); + + $builder->add('values', ChoiceType::class, [ + 'label' => 'part.filter.bulk_import_part_status', + 'choices' => $statusChoices, + 'required' => false, + 'multiple' => true, + 'attr' => [ + 'data-controller' => 'elements--select-multiple', + ] + ]); + } + + public function buildView(FormView $view, FormInterface $form, array $options): void + { + parent::buildView($view, $form, $options); + } +} \ No newline at end of file diff --git a/src/Form/Filters/LogFilterType.php b/src/Form/Filters/LogFilterType.php index 45b1d6dc..c973ad0f 100644 --- a/src/Form/Filters/LogFilterType.php +++ b/src/Form/Filters/LogFilterType.php @@ -100,7 +100,7 @@ class LogFilterType extends AbstractType ]); $builder->add('user', UserEntityConstraintType::class, [ - 'label' => 'log.user', + 'label' => 'log.user', ]); $builder->add('targetType', EnumConstraintType::class, [ @@ -129,11 +129,12 @@ class LogFilterType extends AbstractType LogTargetType::LABEL_PROFILE => 'label_profile.label', LogTargetType::PART_ASSOCIATION => 'part_association.label', LogTargetType::BULK_INFO_PROVIDER_IMPORT_JOB => 'bulk_info_provider_import_job.label', + LogTargetType::BULK_INFO_PROVIDER_IMPORT_JOB_PART => 'bulk_info_provider_import_job_part.label', }, ]); $builder->add('targetId', NumberConstraintType::class, [ - 'label' => 'log.target_id', + 'label' => 'log.target_id', 'min' => 1, 'step' => 1, ]); diff --git a/src/Form/Filters/PartFilterType.php b/src/Form/Filters/PartFilterType.php index dfe449d1..1515c61b 100644 --- a/src/Form/Filters/PartFilterType.php +++ b/src/Form/Filters/PartFilterType.php @@ -32,7 +32,11 @@ use App\Entity\Parts\MeasurementUnit; use App\Entity\Parts\StorageLocation; use App\Entity\Parts\Supplier; use App\Entity\ProjectSystem\Project; +use App\Entity\BulkInfoProviderImportJob; use App\Form\Filters\Constraints\BooleanConstraintType; +use App\Form\Filters\Constraints\BulkImportJobExistsConstraintType; +use App\Form\Filters\Constraints\BulkImportJobStatusConstraintType; +use App\Form\Filters\Constraints\BulkImportPartStatusConstraintType; use App\Form\Filters\Constraints\ChoiceConstraintType; use App\Form\Filters\Constraints\DateTimeConstraintType; use App\Form\Filters\Constraints\NumberConstraintType; @@ -298,6 +302,23 @@ class PartFilterType extends AbstractType } + /************************************************************************** + * Bulk Import Job tab + **************************************************************************/ + if ($this->security->isGranted('@info_providers.create_parts')) { + $builder + ->add('inBulkImportJob', BulkImportJobExistsConstraintType::class, [ + 'label' => 'part.filter.in_bulk_import_job', + ]) + ->add('bulkImportJobStatus', BulkImportJobStatusConstraintType::class, [ + 'label' => 'part.filter.bulk_import_job_status', + ]) + ->add('bulkImportPartStatus', BulkImportPartStatusConstraintType::class, [ + 'label' => 'part.filter.bulk_import_part_status', + ]) + ; + } + $builder->add('submit', SubmitType::class, [ 'label' => 'filter.submit', diff --git a/templates/parts/lists/_filter.html.twig b/templates/parts/lists/_filter.html.twig index c29e8ecd..ba9168d1 100644 --- a/templates/parts/lists/_filter.html.twig +++ b/templates/parts/lists/_filter.html.twig @@ -31,6 +31,11 @@ {% endif %} + {% if filterForm.inBulkImportJob is defined %} + + {% endif %} {{ form_start(filterForm, {"attr": {"data-controller": "helpers--form-cleanup", "data-action": "helpers--form-cleanup#submit"}}) }} @@ -126,6 +131,13 @@ {{ form_row(filterForm.bomComment) }} {% endif %} + {% if filterForm.inBulkImportJob is defined %} +
+ {{ form_row(filterForm.inBulkImportJob) }} + {{ form_row(filterForm.bulkImportJobStatus) }} + {{ form_row(filterForm.bulkImportPartStatus) }} +
+ {% endif %} diff --git a/tests/Controller/BulkInfoProviderImportControllerTest.php b/tests/Controller/BulkInfoProviderImportControllerTest.php index 17a1c235..0cf57696 100644 --- a/tests/Controller/BulkInfoProviderImportControllerTest.php +++ b/tests/Controller/BulkInfoProviderImportControllerTest.php @@ -140,7 +140,7 @@ class BulkInfoProviderImportControllerTest extends WebTestCase // Create a test job with search results that include source_field and source_keyword $job = new BulkInfoProviderImportJob(); $job->setCreatedBy($user); - $job->setPartIds([$part->getId()]); + $job->addPart($part); $job->setStatus(BulkImportJobStatus::IN_PROGRESS); $job->setSearchResults([ [ @@ -230,10 +230,18 @@ class BulkInfoProviderImportControllerTest extends WebTestCase $this->markTestSkipped('Admin user not found in fixtures'); } + // Get a test part + $partRepository = $entityManager->getRepository(Part::class); + $part = $partRepository->find(1); + + if (!$part) { + $this->markTestSkipped('Test part with ID 1 not found in fixtures'); + } + // Create a completed job $job = new BulkInfoProviderImportJob(); $job->setCreatedBy($user); - $job->setPartIds([1]); + $job->addPart($part); $job->setStatus(BulkImportJobStatus::COMPLETED); $job->setSearchResults([]); @@ -272,10 +280,15 @@ class BulkInfoProviderImportControllerTest extends WebTestCase $this->markTestSkipped('Admin user not found in fixtures'); } + // Get test parts + $parts = $this->getTestParts($entityManager, [1]); + // Create an active job $job = new BulkInfoProviderImportJob(); $job->setCreatedBy($user); - $job->setPartIds([1]); + foreach ($parts as $part) { + $job->addPart($part); + } $job->setStatus(BulkImportJobStatus::IN_PROGRESS); $job->setSearchResults([]); @@ -306,10 +319,15 @@ class BulkInfoProviderImportControllerTest extends WebTestCase $this->markTestSkipped('Admin user not found in fixtures'); } + // Get test parts + $parts = $this->getTestParts($entityManager, [1]); + // Create an active job $job = new BulkInfoProviderImportJob(); $job->setCreatedBy($user); - $job->setPartIds([1]); + foreach ($parts as $part) { + $job->addPart($part); + } $job->setStatus(BulkImportJobStatus::IN_PROGRESS); $job->setSearchResults([]); @@ -352,9 +370,14 @@ class BulkInfoProviderImportControllerTest extends WebTestCase $this->markTestSkipped('Admin user not found in fixtures'); } + // Get test parts + $parts = $this->getTestParts($entityManager, [1, 2]); + $job = new BulkInfoProviderImportJob(); $job->setCreatedBy($user); - $job->setPartIds([1, 2]); + foreach ($parts as $part) { + $job->addPart($part); + } $job->setStatus(BulkImportJobStatus::IN_PROGRESS); $job->setSearchResults([]); @@ -387,9 +410,14 @@ class BulkInfoProviderImportControllerTest extends WebTestCase $this->markTestSkipped('Admin user not found in fixtures'); } + // Get test parts + $parts = $this->getTestParts($entityManager, [1, 2]); + $job = new BulkInfoProviderImportJob(); $job->setCreatedBy($user); - $job->setPartIds([1, 2]); + foreach ($parts as $part) { + $job->addPart($part); + } $job->setStatus(BulkImportJobStatus::IN_PROGRESS); $job->setSearchResults([]); @@ -423,9 +451,14 @@ class BulkInfoProviderImportControllerTest extends WebTestCase $this->markTestSkipped('Admin user not found in fixtures'); } + // Get test parts + $parts = $this->getTestParts($entityManager, [1]); + $job = new BulkInfoProviderImportJob(); $job->setCreatedBy($user); - $job->setPartIds([1]); + foreach ($parts as $part) { + $job->addPart($part); + } $job->setStatus(BulkImportJobStatus::IN_PROGRESS); $job->setSearchResults([]); @@ -467,10 +500,15 @@ class BulkInfoProviderImportControllerTest extends WebTestCase $this->markTestSkipped('Required test users not found in fixtures'); } + // Get test parts + $parts = $this->getTestParts($entityManager, [1]); + // Create job as admin $job = new BulkInfoProviderImportJob(); $job->setCreatedBy($admin); - $job->setPartIds([1]); + foreach ($parts as $part) { + $job->addPart($part); + } $job->setStatus(BulkImportJobStatus::IN_PROGRESS); $job->setSearchResults([]); @@ -502,10 +540,15 @@ class BulkInfoProviderImportControllerTest extends WebTestCase $this->markTestSkipped('Required test users not found in fixtures'); } + // Get test parts + $parts = $this->getTestParts($entityManager, [1]); + // Create job as readonly user $job = new BulkInfoProviderImportJob(); $job->setCreatedBy($readonly); - $job->setPartIds([1]); + foreach ($parts as $part) { + $job->addPart($part); + } $job->setStatus(BulkImportJobStatus::COMPLETED); $job->setSearchResults([]); @@ -534,4 +577,20 @@ class BulkInfoProviderImportControllerTest extends WebTestCase $client->loginUser($user); } + + private function getTestParts($entityManager, array $ids): array + { + $partRepository = $entityManager->getRepository(Part::class); + $parts = []; + + foreach ($ids as $id) { + $part = $partRepository->find($id); + if (!$part) { + $this->markTestSkipped("Test part with ID {$id} not found in fixtures"); + } + $parts[] = $part; + } + + return $parts; + } } \ No newline at end of file diff --git a/tests/Entity/BulkInfoProviderImportJobTest.php b/tests/Entity/BulkInfoProviderImportJobTest.php index bf82b413..48678bf7 100644 --- a/tests/Entity/BulkInfoProviderImportJobTest.php +++ b/tests/Entity/BulkInfoProviderImportJobTest.php @@ -36,15 +36,23 @@ class BulkInfoProviderImportJobTest extends TestCase { $this->user = new User(); $this->user->setName('test_user'); - + $this->job = new BulkInfoProviderImportJob(); $this->job->setCreatedBy($this->user); } + private function createMockPart(int $id): \App\Entity\Parts\Part + { + $part = $this->createMock(\App\Entity\Parts\Part::class); + $part->method('getId')->willReturn($id); + $part->method('getName')->willReturn("Test Part {$id}"); + return $part; + } + public function testConstruct(): void { $job = new BulkInfoProviderImportJob(); - + $this->assertInstanceOf(\DateTimeImmutable::class, $job->getCreatedAt()); $this->assertEquals(BulkImportJobStatus::PENDING, $job->getStatus()); $this->assertEmpty($job->getPartIds()); @@ -60,9 +68,12 @@ class BulkInfoProviderImportJobTest extends TestCase $this->job->setName('Test Job'); $this->assertEquals('Test Job', $this->job->getName()); - $partIds = [1, 2, 3]; - $this->job->setPartIds($partIds); - $this->assertEquals($partIds, $this->job->getPartIds()); + // Test with actual parts - this is what actually works + $parts = [$this->createMockPart(1), $this->createMockPart(2), $this->createMockPart(3)]; + foreach ($parts as $part) { + $this->job->addPart($part); + } + $this->assertEquals([1, 2, 3], $this->job->getPartIds()); $fieldMappings = ['field1' => 'provider1', 'field2' => 'provider2']; $this->job->setFieldMappings($fieldMappings); @@ -133,7 +144,17 @@ class BulkInfoProviderImportJobTest extends TestCase { $this->assertEquals(0, $this->job->getPartCount()); - $this->job->setPartIds([1, 2, 3, 4, 5]); + // Test with actual parts - setPartIds doesn't actually add parts + $parts = [ + $this->createMockPart(1), + $this->createMockPart(2), + $this->createMockPart(3), + $this->createMockPart(4), + $this->createMockPart(5) + ]; + foreach ($parts as $part) { + $this->job->addPart($part); + } $this->assertEquals(5, $this->job->getPartCount()); } @@ -152,7 +173,16 @@ class BulkInfoProviderImportJobTest extends TestCase public function testPartProgressTracking(): void { - $this->job->setPartIds([1, 2, 3, 4]); + // Test with actual parts - setPartIds doesn't actually add parts + $parts = [ + $this->createMockPart(1), + $this->createMockPart(2), + $this->createMockPart(3), + $this->createMockPart(4) + ]; + foreach ($parts as $part) { + $this->job->addPart($part); + } $this->assertFalse($this->job->isPartCompleted(1)); $this->assertFalse($this->job->isPartSkipped(1)); @@ -172,7 +202,17 @@ class BulkInfoProviderImportJobTest extends TestCase public function testProgressCounts(): void { - $this->job->setPartIds([1, 2, 3, 4, 5]); + // Test with actual parts - setPartIds doesn't actually add parts + $parts = [ + $this->createMockPart(1), + $this->createMockPart(2), + $this->createMockPart(3), + $this->createMockPart(4), + $this->createMockPart(5) + ]; + foreach ($parts as $part) { + $this->job->addPart($part); + } $this->assertEquals(0, $this->job->getCompletedPartsCount()); $this->assertEquals(0, $this->job->getSkippedPartsCount()); @@ -190,7 +230,18 @@ class BulkInfoProviderImportJobTest extends TestCase $emptyJob = new BulkInfoProviderImportJob(); $this->assertEquals(100.0, $emptyJob->getProgressPercentage()); - $this->job->setPartIds([1, 2, 3, 4, 5]); + // Test with actual parts - setPartIds doesn't actually add parts + $parts = [ + $this->createMockPart(1), + $this->createMockPart(2), + $this->createMockPart(3), + $this->createMockPart(4), + $this->createMockPart(5) + ]; + foreach ($parts as $part) { + $this->job->addPart($part); + } + $this->assertEquals(0.0, $this->job->getProgressPercentage()); $this->job->markPartAsCompleted(1); @@ -210,7 +261,16 @@ class BulkInfoProviderImportJobTest extends TestCase $emptyJob = new BulkInfoProviderImportJob(); $this->assertTrue($emptyJob->isAllPartsCompleted()); - $this->job->setPartIds([1, 2, 3]); + // Test with actual parts - setPartIds doesn't actually add parts + $parts = [ + $this->createMockPart(1), + $this->createMockPart(2), + $this->createMockPart(3) + ]; + foreach ($parts as $part) { + $this->job->addPart($part); + } + $this->assertFalse($this->job->isAllPartsCompleted()); $this->job->markPartAsCompleted(1); @@ -223,8 +283,16 @@ class BulkInfoProviderImportJobTest extends TestCase public function testDisplayNameMethods(): void { - $this->job->setPartIds([1, 2, 3]); - + // Test with actual parts - setPartIds doesn't actually add parts + $parts = [ + $this->createMockPart(1), + $this->createMockPart(2), + $this->createMockPart(3) + ]; + foreach ($parts as $part) { + $this->job->addPart($part); + } + $this->assertEquals('info_providers.bulk_import.job_name_template', $this->job->getDisplayNameKey()); $this->assertEquals(['%count%' => 3], $this->job->getDisplayNameParams()); } @@ -237,19 +305,39 @@ class BulkInfoProviderImportJobTest extends TestCase public function testProgressDataStructure(): void { + $parts = [ + $this->createMockPart(1), + $this->createMockPart(2), + $this->createMockPart(3) + ]; + foreach ($parts as $part) { + $this->job->addPart($part); + } + $this->job->markPartAsCompleted(1); $this->job->markPartAsSkipped(2, 'Test reason'); $progress = $this->job->getProgress(); - - $this->assertArrayHasKey(1, $progress); + + // The progress array should have keys for all part IDs, even if not completed/skipped + $this->assertArrayHasKey(1, $progress, 'Progress should contain key for part 1'); + $this->assertArrayHasKey(2, $progress, 'Progress should contain key for part 2'); + $this->assertArrayHasKey(3, $progress, 'Progress should contain key for part 3'); + + // Part 1: completed $this->assertEquals('completed', $progress[1]['status']); $this->assertArrayHasKey('completed_at', $progress[1]); + $this->assertArrayNotHasKey('reason', $progress[1]); - $this->assertArrayHasKey(2, $progress); + // Part 2: skipped $this->assertEquals('skipped', $progress[2]['status']); $this->assertEquals('Test reason', $progress[2]['reason']); $this->assertArrayHasKey('completed_at', $progress[2]); + + // Part 3: should be present but not completed/skipped + $this->assertEquals('pending', $progress[3]['status']); + $this->assertArrayNotHasKey('completed_at', $progress[3]); + $this->assertArrayNotHasKey('reason', $progress[3]); } public function testCompletedAtTimestamp(): void diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 875f8d42..3d304fb5 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -13801,5 +13801,101 @@ Please note, that you can not impersonate a disabled user. If you try you will g Are you sure you want to stop this job?
+ + + part.filter.in_bulk_import_job + In Bulk Import Job + + + + + part.filter.in_bulk_import_job.yes + Yes + + + + + part.filter.in_bulk_import_job.no + No + + + + + part.filter.bulk_import_job_status + Bulk Import Job Status + + + + + part.filter.bulk_import_part_status + Bulk Import Part Status + + + + + part.edit.tab.bulk_import + Bulk Import Job + + + + + bulk_import.status.pending + Pending + + + + + bulk_import.status.in_progress + In Progress + + + + + bulk_import.status.completed + Completed + + + + + bulk_import.status.stopped + Stopped + + + + + bulk_import.status.failed + Failed + + + + + bulk_import.part_status.pending + Pending + + + + + bulk_import.part_status.completed + Completed + + + + + bulk_import.part_status.skipped + Skipped + + + + + bulk_import.part_status.failed + Failed + + + + + filter.operator + Operator + + \ No newline at end of file From ed396765c8999625e57a79cb6cfe9dc29a6116d0 Mon Sep 17 00:00:00 2001 From: barisgit Date: Sat, 2 Aug 2025 23:40:09 +0200 Subject: [PATCH 029/228] Let symfony manage translations --- src/Entity/BulkInfoProviderImportJobPart.php | 2 +- translations/messages.en.xlf | 868 +++++++++---------- 2 files changed, 417 insertions(+), 453 deletions(-) diff --git a/src/Entity/BulkInfoProviderImportJobPart.php b/src/Entity/BulkInfoProviderImportJobPart.php index df99aa19..3625f377 100644 --- a/src/Entity/BulkInfoProviderImportJobPart.php +++ b/src/Entity/BulkInfoProviderImportJobPart.php @@ -45,7 +45,7 @@ class BulkInfoProviderImportJobPart extends AbstractDBElement #[ORM\JoinColumn(nullable: false)] private BulkInfoProviderImportJob $job; - #[ORM\ManyToOne(targetEntity: Part::class)] + #[ORM\ManyToOne(targetEntity: Part::class, inversedBy: 'bulkImportJobParts')] #[ORM\JoinColumn(nullable: false)] private Part $part; diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 3d304fb5..6f66dab1 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -8944,7 +8944,7 @@ Element 1 -> Element 1.2]]> Edit part
- + part_list.action.scrollable_hint Scroll to see all actions @@ -9340,79 +9340,79 @@ Element 1 -> Element 1.2]]> Attachment name - + filter.bulk_import_job.label Bulk Import Job - + filter.bulk_import_job.job_status Job Status - + filter.bulk_import_job.part_status_in_job Part Status in Job - + filter.bulk_import_job.status.any Any Status - + filter.bulk_import_job.status.pending Pending - + filter.bulk_import_job.status.in_progress In Progress - + filter.bulk_import_job.status.completed Completed - + filter.bulk_import_job.status.stopped Stopped - + filter.bulk_import_job.status.failed Failed - + filter.bulk_import_job.part_status.any Any Part Status - + filter.bulk_import_job.part_status.pending Pending - + filter.bulk_import_job.part_status.completed Completed - + filter.bulk_import_job.part_status.skipped Skipped @@ -10990,7 +10990,7 @@ Element 1 -> Element 1.2]]> Export to XML - + part_list.action.export_xlsx Export to Excel @@ -12303,7 +12303,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g info_providers.search.no_results - No results found at the selected providers! Check your search term or try to choose additional providers. + No results found @@ -13147,755 +13147,719 @@ Please note, that you can not impersonate a disabled user. If you try you will g Redacted for security reasons - + info_providers.bulk_import.step1.title Bulk Info Provider Import - Step 1 - + info_providers.bulk_import.parts_selected parts selected - + info_providers.bulk_import.step1.global_mapping_description Configure field mappings that will be applied to all selected parts. For example: "MPN → LCSC + Mouser" means search LCSC and Mouser providers using each part's MPN field. - + info_providers.bulk_import.selected_parts Selected Parts - + info_providers.bulk_import.field_mappings Field Mappings - + info_providers.bulk_import.field_mappings_help Define which part fields to search with which info providers. Multiple mappings will be combined. - + info_providers.bulk_import.add_mapping Add Mapping - + info_providers.bulk_import.search_results.title Search Results - + info_providers.bulk_import.errors errors - - - info_providers.bulk_import.results_found - results found - - - - - info_providers.bulk_import.source_field - Source Field - - - - - info_providers.bulk_import.create_part - Create Part - - - - - info_providers.bulk_import.view_existing - View Existing - - - - - info_providers.bulk_search.search_field - Search Field - - - - - info_providers.bulk_search.providers - Info Providers - - - - - info_providers.bulk_import.actions.label - Actions - - - - - info_providers.bulk_search.providers.help - Select which info providers to search when parts have this field. - - - - - info_providers.bulk_search.submit - Search All Parts - - - - - info_providers.bulk_search.field.select - Select a field to search by - - - - - info_providers.bulk_search.field.mpn - Manufacturer Part Number (MPN) - - - - - info_providers.bulk_search.field.name - Part Name - - - - - part_list.action.action.info_provider - Info Provider - - - - - part_list.action.bulk_info_provider_import - Bulk Info Provider Import - - - - - info_providers.bulk_import.clear_selections - Clear All Selections - - - - - info_providers.bulk_import.clear_row - Clear this row's selections - - - - - info_providers.bulk_import.step1.spn_recommendation - SPN (Supplier Part Number) is recommended for better results. Add a mapping for each supplier to use their SPNs. - - - - - info_providers.bulk_import.update_part - Update Part - - - - - info_providers.bulk_import.prefetch_details - Prefetch Details - - - - - info_providers.bulk_import.prefetch_details_help - Prefetch details for all results. This will take longer, but will speed up workflow for updating parts. - - - - - info_providers.bulk_import.step2.title - Bulk import from info providers - - - - - info_providers.bulk_import.step2.card_title - Bulk import for %count% parts - %date% - - - - - info_providers.bulk_import.parts - parts - - - - - info_providers.bulk_import.results - results - - - - - info_providers.bulk_import.created_at - Created at - - - - - info_providers.bulk_import.status.in_progress - In Progress - - - - - info_providers.bulk_import.status.completed - Completed - - - - - info_providers.bulk_import.status.failed - Failed - - - + info_providers.bulk_import.results_found %count% results found - - - info_providers.bulk_import.table.name - Name - - - - - info_providers.bulk_import.table.description - Description - - - - - info_providers.bulk_import.table.manufacturer - Manufacturer - - - - - info_providers.bulk_import.table.provider - Provider - - - - - info_providers.bulk_import.table.source_field - Source Field - - - - - info_providers.bulk_import.table.action - Action - - - - - info_providers.bulk_import.action.select - Select - - - - - info_providers.bulk_import.action.deselect - Deselect - - - - - info_providers.bulk_import.action.view_details - View Details - - - - - info_providers.bulk_import.no_results - No results found - - - - - info_providers.bulk_import.processing - Processing... - - - - - info_providers.bulk_import.error - Error occurred during import - - - - - info_providers.bulk_import.success - Import completed successfully - - - - - info_providers.bulk_import.partial_success - Import completed with some errors - - - - - info_providers.bulk_import.retry - Retry - - - - - info_providers.bulk_import.cancel - Cancel - - - - - info_providers.bulk_import.confirm - Confirm Import - - - - - info_providers.bulk_import.back - Back - - - - - info_providers.bulk_import.next - Next - - - - - info_providers.bulk_import.finish - Finish - - - - - info_providers.bulk_import.progress - Progress: - - - - - info_providers.bulk_import.time_remaining - Estimated time remaining: %time% - - - - - info_providers.bulk_import.details_modal.title - Part Details - - - - - info_providers.bulk_import.details_modal.close - Close - - - - - info_providers.bulk_import.details_modal.select_this_part - Select This Part - - - - - info_providers.bulk_import.status.pending - Pending - - - - - info_providers.bulk_import.completed - completed - - - - - info_providers.bulk_import.skipped - skipped - - - - - info_providers.bulk_import.errors - errors - - - - - info_providers.bulk_import.mark_completed - Mark Completed - - - - - info_providers.bulk_import.mark_skipped - Mark Skipped - - - - - info_providers.bulk_import.mark_pending - Mark Pending - - - - - info_providers.bulk_import.skip_reason - Skip reason - - - + info_providers.bulk_import.source_field Source Field - + - info_providers.bulk_import.update_part - Update Part + info_providers.bulk_import.create_part + Create Part - + info_providers.bulk_import.view_existing View Existing - + - info_providers.search.no_results - No results found + info_providers.bulk_search.search_field + Search Field - + - info_providers.table.provider.label + info_providers.bulk_search.providers + Info Providers + + + + + info_providers.bulk_import.actions.label + Actions + + + + + info_providers.bulk_search.providers.help + Select which info providers to search when parts have this field. + + + + + info_providers.bulk_search.submit + Search All Parts + + + + + info_providers.bulk_search.field.select + Select a field to search by + + + + + info_providers.bulk_search.field.mpn + Manufacturer Part Number (MPN) + + + + + info_providers.bulk_search.field.name + Part Name + + + + + part_list.action.action.info_provider + Info Provider + + + + + part_list.action.bulk_info_provider_import + Bulk Info Provider Import + + + + + info_providers.bulk_import.clear_selections + Clear All Selections + + + + + info_providers.bulk_import.clear_row + Clear this row's selections + + + + + info_providers.bulk_import.step1.spn_recommendation + SPN (Supplier Part Number) is recommended for better results. Add a mapping for each supplier to use their SPNs. + + + + + info_providers.bulk_import.update_part + Update Part + + + + + info_providers.bulk_import.prefetch_details + Prefetch Details + + + + + info_providers.bulk_import.prefetch_details_help + Prefetch details for all results. This will take longer, but will speed up workflow for updating parts. + + + + + info_providers.bulk_import.step2.title + Bulk import from info providers + + + + + info_providers.bulk_import.step2.card_title + Bulk import for %count% parts - %date% + + + + + info_providers.bulk_import.parts + parts + + + + + info_providers.bulk_import.results + results + + + + + info_providers.bulk_import.created_at + Created at + + + + + info_providers.bulk_import.status.in_progress + In Progress + + + + + info_providers.bulk_import.status.completed + Completed + + + + + info_providers.bulk_import.status.failed + Failed + + + + + info_providers.bulk_import.table.name + Name + + + + + info_providers.bulk_import.table.description + Description + + + + + info_providers.bulk_import.table.manufacturer + Manufacturer + + + + + info_providers.bulk_import.table.provider Provider - + + + info_providers.bulk_import.table.source_field + Source Field + + + + + info_providers.bulk_import.table.action + Action + + + + + info_providers.bulk_import.action.select + Select + + + + + info_providers.bulk_import.action.deselect + Deselect + + + + + info_providers.bulk_import.action.view_details + View Details + + + + + info_providers.bulk_import.no_results + No results found + + + + + info_providers.bulk_import.processing + Processing... + + + + + info_providers.bulk_import.error + Error occurred during import + + + + + info_providers.bulk_import.success + Import completed successfully + + + + + info_providers.bulk_import.partial_success + Import completed with some errors + + + + + info_providers.bulk_import.retry + Retry + + + + + info_providers.bulk_import.cancel + Cancel + + + + + info_providers.bulk_import.confirm + Confirm Import + + + + + info_providers.bulk_import.back + Back + + + + + info_providers.bulk_import.next + Next + + + + + info_providers.bulk_import.finish + Finish + + + + + info_providers.bulk_import.progress + Progress: + + + + + info_providers.bulk_import.time_remaining + Estimated time remaining: %time% + + + + + info_providers.bulk_import.details_modal.title + Part Details + + + + + info_providers.bulk_import.details_modal.close + Close + + + + + info_providers.bulk_import.details_modal.select_this_part + Select This Part + + + + + info_providers.bulk_import.status.pending + Pending + + + + + info_providers.bulk_import.completed + completed + + + + + info_providers.bulk_import.skipped + skipped + + + + + info_providers.bulk_import.mark_completed + Mark Completed + + + + + info_providers.bulk_import.mark_skipped + Mark Skipped + + + + + info_providers.bulk_import.mark_pending + Mark Pending + + + + + info_providers.bulk_import.skip_reason + Skip reason + + + info_providers.bulk_import.editing_part Editing part as part of bulk import - + info_providers.bulk_import.complete Complete - + info_providers.bulk_import.existing_jobs Existing Jobs - + info_providers.bulk_import.job_name Job Name - + info_providers.bulk_import.parts_count Parts Count - + info_providers.bulk_import.results_count Results Count - + info_providers.bulk_import.progress_label Progress: %current%/%total% - + info_providers.bulk_import.manage_jobs Manage Bulk Import Jobs - + info_providers.bulk_import.view_results View Results - + info_providers.bulk_import.status Status - + info_providers.bulk_import.manage_jobs_description View and manage all your bulk import jobs. To create a new job, select parts and click "Bulk import from info providers". - + info_providers.bulk_import.no_jobs_found No bulk import jobs found. - + info_providers.bulk_import.create_first_job Create your first bulk import job - + info_providers.bulk_import.confirm_delete_job Are you sure you want to delete this job? - + info_providers.bulk_import.job_name_template Bulk import for %count% parts - + info_providers.bulk_import.step2.instructions.title How to use bulk import - + info_providers.bulk_import.step2.instructions.description Follow these steps to efficiently update your parts: - + info_providers.bulk_import.step2.instructions.step1 Click "Update Part" to edit a part with the supplier data - + info_providers.bulk_import.step2.instructions.step2 Review and modify the part information as needed. Note: You need to click "Save" twice to save the changes. - + info_providers.bulk_import.step2.instructions.step3 Click "Complete" to mark the part as done and return to this overview - + info_providers.bulk_import.created_by Created By - + info_providers.bulk_import.completed_at Completed At - + info_providers.bulk_import.action.label Action - + info_providers.bulk_import.action.delete Delete - + info_providers.bulk_import.status.active Active - + info_providers.bulk_import.progress.title Progress - + info_providers.bulk_import.progress.completed_text %completed% / %total% completed - + info_providers.bulk_import.error.deleting_job Error deleting job - + info_providers.bulk_import.error.unknown Unknown error - + info_providers.bulk_import.error.deleting_job_with_details Error deleting job: %error% - + info_providers.bulk_import.status.stopped Stopped - + info_providers.bulk_import.action.stop Stop - + info_providers.bulk_import.confirm_stop_job Are you sure you want to stop this job? - + part.filter.in_bulk_import_job In Bulk Import Job - + part.filter.in_bulk_import_job.yes Yes - + part.filter.in_bulk_import_job.no No - + part.filter.bulk_import_job_status Bulk Import Job Status - + part.filter.bulk_import_part_status Bulk Import Part Status - + part.edit.tab.bulk_import Bulk Import Job - + bulk_import.status.pending Pending - + bulk_import.status.in_progress In Progress - + bulk_import.status.completed Completed - + bulk_import.status.stopped Stopped - + bulk_import.status.failed Failed - + bulk_import.part_status.pending Pending - + bulk_import.part_status.completed Completed - + bulk_import.part_status.skipped Skipped - + bulk_import.part_status.failed Failed - + filter.operator Operator + + + bulk_info_provider_import_job.label + Bulk Info Provider Import + + - \ No newline at end of file + From 3896d3d9ab3d7dfa4fa8eda6a76fd9c6b66f9414 Mon Sep 17 00:00:00 2001 From: barisgit Date: Sat, 2 Aug 2025 23:46:16 +0200 Subject: [PATCH 030/228] Fix a single failing test --- tests/Services/ElementTypeNameGeneratorTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Services/ElementTypeNameGeneratorTest.php b/tests/Services/ElementTypeNameGeneratorTest.php index 5209f1ea..c893fe2a 100644 --- a/tests/Services/ElementTypeNameGeneratorTest.php +++ b/tests/Services/ElementTypeNameGeneratorTest.php @@ -51,18 +51,18 @@ class ElementTypeNameGeneratorTest extends WebTestCase //We only test in english $this->assertSame('Part', $this->service->getLocalizedTypeLabel(new Part())); $this->assertSame('Category', $this->service->getLocalizedTypeLabel(new Category())); - $this->assertSame('bulk_info_provider_import_job.label', $this->service->getLocalizedTypeLabel(new BulkInfoProviderImportJob())); + $this->assertSame('Bulk Info Provider Import', $this->service->getLocalizedTypeLabel(new BulkInfoProviderImportJob())); //Test inheritance $this->assertSame('Attachment', $this->service->getLocalizedTypeLabel(new PartAttachment())); //Test for class name $this->assertSame('Part', $this->service->getLocalizedTypeLabel(Part::class)); - $this->assertSame('bulk_info_provider_import_job.label', $this->service->getLocalizedTypeLabel(BulkInfoProviderImportJob::class)); + $this->assertSame('Bulk Info Provider Import', $this->service->getLocalizedTypeLabel(BulkInfoProviderImportJob::class)); //Test exception for unknpwn type $this->expectException(EntityNotSupportedException::class); - $this->service->getLocalizedTypeLabel(new class() extends AbstractDBElement { + $this->service->getLocalizedTypeLabel(new class () extends AbstractDBElement { }); } @@ -77,7 +77,7 @@ class ElementTypeNameGeneratorTest extends WebTestCase //Test exception $this->expectException(EntityNotSupportedException::class); - $this->service->getTypeNameCombination(new class() extends AbstractNamedDBElement { + $this->service->getTypeNameCombination(new class () extends AbstractNamedDBElement { public function getIDString(): string { return 'Stub'; From 74be016b68f3a173c3140099e381b47b3b4fc070 Mon Sep 17 00:00:00 2001 From: barisgit Date: Mon, 4 Aug 2025 23:33:19 +0200 Subject: [PATCH 031/228] Add abbility to search faster on LCSC without details --- .../Providers/LCSCProvider.php | 143 ++++++++++++++---- 1 file changed, 115 insertions(+), 28 deletions(-) diff --git a/src/Services/InfoProviderSystem/Providers/LCSCProvider.php b/src/Services/InfoProviderSystem/Providers/LCSCProvider.php index 58df3b82..9a588b32 100755 --- a/src/Services/InfoProviderSystem/Providers/LCSCProvider.php +++ b/src/Services/InfoProviderSystem/Providers/LCSCProvider.php @@ -69,9 +69,10 @@ class LCSCProvider implements InfoProviderInterface /** * @param string $id + * @param bool $lightweight If true, skip expensive operations like datasheet resolution * @return PartDetailDTO */ - private function queryDetail(string $id): PartDetailDTO + private function queryDetail(string $id, bool $lightweight = false): PartDetailDTO { $response = $this->lcscClient->request('GET', self::ENDPOINT_URL . "/product/detail", [ 'headers' => [ @@ -89,7 +90,7 @@ class LCSCProvider implements InfoProviderInterface throw new \RuntimeException('Could not find product code: ' . $id); } - return $this->getPartDetail($product); + return $this->getPartDetail($product, $lightweight); } /** @@ -99,30 +100,42 @@ class LCSCProvider implements InfoProviderInterface private function getRealDatasheetUrl(?string $url): string { if ($url !== null && trim($url) !== '' && preg_match("/^https:\/\/(datasheet\.lcsc\.com|www\.lcsc\.com\/datasheet)\/.*(C\d+)\.pdf$/", $url, $matches) > 0) { - if (preg_match("/^https:\/\/datasheet\.lcsc\.com\/lcsc\/(.*\.pdf)$/", $url, $rewriteMatches) > 0) { - $url = 'https://www.lcsc.com/datasheet/lcsc_datasheet_' . $rewriteMatches[1]; - } - $response = $this->lcscClient->request('GET', $url, [ - 'headers' => [ - 'Referer' => 'https://www.lcsc.com/product-detail/_' . $matches[2] . '.html' - ], - ]); - if (preg_match('/(previewPdfUrl): ?("[^"]+wmsc\.lcsc\.com[^"]+\.pdf")/', $response->getContent(), $matches) > 0) { - //HACKY: The URL string contains escaped characters like \u002F, etc. To decode it, the JSON decoding is reused - //See https://github.com/Part-DB/Part-DB-server/pull/582#issuecomment-2033125934 - $jsonObj = json_decode('{"' . $matches[1] . '": ' . $matches[2] . '}'); - $url = $jsonObj->previewPdfUrl; - } + if (preg_match("/^https:\/\/datasheet\.lcsc\.com\/lcsc\/(.*\.pdf)$/", $url, $rewriteMatches) > 0) { + $url = 'https://www.lcsc.com/datasheet/lcsc_datasheet_' . $rewriteMatches[1]; + } + $response = $this->lcscClient->request('GET', $url, [ + 'headers' => [ + 'Referer' => 'https://www.lcsc.com/product-detail/_' . $matches[2] . '.html' + ], + ]); + if (preg_match('/(previewPdfUrl): ?("[^"]+wmsc\.lcsc\.com[^"]+\.pdf")/', $response->getContent(), $matches) > 0) { + //HACKY: The URL string contains escaped characters like \u002F, etc. To decode it, the JSON decoding is reused + //See https://github.com/Part-DB/Part-DB-server/pull/582#issuecomment-2033125934 + $jsonObj = json_decode('{"' . $matches[1] . '": ' . $matches[2] . '}'); + $url = $jsonObj->previewPdfUrl; + } } return $url; } /** * @param string $term + * @param bool $lightweight If true, skip expensive operations like datasheet resolution * @return PartDetailDTO[] */ - private function queryByTerm(string $term): array + private function queryByTerm(string $term, bool $lightweight = false): array { + // Optimize: If term looks like an LCSC part number (starts with C followed by digits), + // use direct detail query instead of slower search + if (preg_match('/^C\d+$/i', trim($term))) { + try { + return [$this->queryDetail(trim($term), $lightweight)]; + } catch (\Exception $e) { + // If direct lookup fails, fall back to search + // This handles cases where the C-code might not exist + } + } + $response = $this->lcscClient->request('GET', self::ENDPOINT_URL . "/search/global", [ 'headers' => [ 'Cookie' => new Cookie('currencyCode', $this->settings->currency) @@ -145,11 +158,11 @@ class LCSCProvider implements InfoProviderInterface // detailed product listing. It does so utilizing a product tip field. // If product tip exists and there are no products in the product list try a detail query if (count($products) === 0 && $tipProductCode !== null) { - $result[] = $this->queryDetail($tipProductCode); + $result[] = $this->queryDetail($tipProductCode, $lightweight); } foreach ($products as $product) { - $result[] = $this->getPartDetail($product); + $result[] = $this->getPartDetail($product, $lightweight); } return $result; @@ -175,7 +188,7 @@ class LCSCProvider implements InfoProviderInterface * @param array $product * @return PartDetailDTO */ - private function getPartDetail(array $product): PartDetailDTO + private function getPartDetail(array $product, bool $lightweight = false): PartDetailDTO { // Get product images in advance $product_images = $this->getProductImages($product['productImages'] ?? null); @@ -214,10 +227,10 @@ class LCSCProvider implements InfoProviderInterface manufacturing_status: null, provider_url: $this->getProductShortURL($product['productCode']), footprint: $this->sanitizeField($footprint), - datasheets: $this->getProductDatasheets($product['pdfUrl'] ?? null), - images: $product_images, - parameters: $this->attributesToParameters($product['paramVOList'] ?? []), - vendor_infos: $this->pricesToVendorInfo($product['productCode'], $this->getProductShortURL($product['productCode']), $product['productPriceList'] ?? []), + datasheets: $lightweight ? [] : $this->getProductDatasheets($product['pdfUrl'] ?? null), + images: $product_images, // Always include images - users need to see them + parameters: $lightweight ? [] : $this->attributesToParameters($product['paramVOList'] ?? []), + vendor_infos: $lightweight ? [] : $this->pricesToVendorInfo($product['productCode'], $this->getProductShortURL($product['productCode']), $product['productPriceList'] ?? []), mass: $product['weight'] ?? null, ); } @@ -286,7 +299,7 @@ class LCSCProvider implements InfoProviderInterface */ private function getProductShortURL(string $product_code): string { - return 'https://www.lcsc.com/product-detail/' . $product_code .'.html'; + return 'https://www.lcsc.com/product-detail/' . $product_code . '.html'; } /** @@ -327,7 +340,7 @@ class LCSCProvider implements InfoProviderInterface //Skip this attribute if it's empty if (in_array(trim((string) $attribute['paramValueEn']), ['', '-'], true)) { - continue; + continue; } $result[] = ParameterDTO::parseValueIncludingUnit(name: $attribute['paramNameEn'], value: $attribute['paramValueEn'], group: null); @@ -338,12 +351,86 @@ class LCSCProvider implements InfoProviderInterface public function searchByKeyword(string $keyword): array { - return $this->queryByTerm($keyword); + return $this->queryByTerm($keyword, true); // Use lightweight mode for search + } + + /** + * Batch search multiple keywords asynchronously (like JavaScript Promise.all) + * @param array $keywords Array of keywords to search + * @return array Results indexed by keyword + */ + public function searchByKeywordsBatch(array $keywords): array + { + if (empty($keywords)) { + return []; + } + + $responses = []; + $results = []; + + // Start all requests immediately (like JavaScript promises without await) + foreach ($keywords as $keyword) { + if (preg_match('/^C\d+$/i', trim($keyword))) { + // Direct detail API call for C-codes + $responses[$keyword] = $this->lcscClient->request('GET', self::ENDPOINT_URL . "/product/detail", [ + 'headers' => [ + 'Cookie' => new Cookie('currencyCode', $this->currency) + ], + 'query' => [ + 'productCode' => trim($keyword), + ], + ]); + } else { + // Search API call for other terms + $responses[$keyword] = $this->lcscClient->request('GET', self::ENDPOINT_URL . "/search/global", [ + 'headers' => [ + 'Cookie' => new Cookie('currencyCode', $this->currency) + ], + 'query' => [ + 'keyword' => $keyword, + ], + ]); + } + } + + // Now collect all results (like .then() in JavaScript) + foreach ($responses as $keyword => $response) { + try { + $arr = $response->toArray(); // This waits for the response + $results[$keyword] = $this->processSearchResponse($arr, $keyword); + } catch (\Exception $e) { + $results[$keyword] = []; // Empty results on error + } + } + + return $results; + } + + private function processSearchResponse(array $arr, string $keyword): array + { + $result = []; + + // Check if this looks like a detail response (direct C-code lookup) + if (isset($arr['result']['productCode'])) { + $product = $arr['result']; + $result[] = $this->getPartDetail($product, true); // lightweight mode + } else { + // This is a search response + $products = $arr['result']['productSearchResultVO']['productList'] ?? []; + $tipProductCode = $arr['result']['tipProductDetailUrlVO']['productCode'] ?? null; + + // If no products but has tip, we'd need another API call - skip for batch mode + foreach ($products as $product) { + $result[] = $this->getPartDetail($product, true); // lightweight mode + } + } + + return $result; } public function getDetails(string $id): PartDetailDTO { - $tmp = $this->queryByTerm($id); + $tmp = $this->queryByTerm($id, false); if (count($tmp) === 0) { throw new \RuntimeException('No part found with ID ' . $id); } From 4da403569c214fc49609e01573499ab133594185 Mon Sep 17 00:00:00 2001 From: barisgit Date: Mon, 4 Aug 2025 23:34:20 +0200 Subject: [PATCH 032/228] Increase time limit on batch search and add option to priorities which fields to choose --- .../BulkInfoProviderImportController.php | 299 +++++++++++++----- src/Controller/PartController.php | 80 +++-- .../FieldToProviderMappingType.php | 14 + .../bulk_import/step1.html.twig | 18 +- .../bulk_import/step2.html.twig | 4 +- translations/messages.en.xlf | 30 ++ 6 files changed, 338 insertions(+), 107 deletions(-) diff --git a/src/Controller/BulkInfoProviderImportController.php b/src/Controller/BulkInfoProviderImportController.php index 6c434191..d09e8d04 100644 --- a/src/Controller/BulkInfoProviderImportController.php +++ b/src/Controller/BulkInfoProviderImportController.php @@ -53,6 +53,9 @@ class BulkInfoProviderImportController extends AbstractController public function step1(Request $request, LoggerInterface $exceptionLogger): Response { $this->denyAccessUnlessGranted('@info_providers.create_parts'); + + // Increase execution time for bulk operations + set_time_limit(600); // 10 minutes for large batches $ids = $request->query->get('ids'); if (!$ids) { @@ -69,6 +72,11 @@ class BulkInfoProviderImportController extends AbstractController $this->addFlash('error', 'No valid parts found for bulk import'); return $this->redirectToRoute('homepage'); } + + // Warn about large batches + if (count($parts) > 50) { + $this->addFlash('warning', 'Processing ' . count($parts) . ' parts may take several minutes and could timeout. Consider processing smaller batches.'); + } // Generate field choices $fieldChoices = [ @@ -86,7 +94,7 @@ class BulkInfoProviderImportController extends AbstractController // Initialize form with useful default mappings $initialData = [ 'field_mappings' => [ - ['field' => 'mpn', 'providers' => []] + ['field' => 'mpn', 'providers' => [], 'priority' => 1] ], 'prefetch_details' => false ]; @@ -102,6 +110,12 @@ class BulkInfoProviderImportController extends AbstractController $formData = $form->getData(); $fieldMappings = $formData['field_mappings']; $prefetchDetails = $formData['prefetch_details'] ?? false; + + // Debug logging + $exceptionLogger->info('Form data received', [ + 'prefetch_details' => $prefetchDetails, + 'prefetch_details_type' => gettype($prefetchDetails) + ]); // Create and save the job $job = new BulkInfoProviderImportJob(); @@ -123,92 +137,195 @@ class BulkInfoProviderImportController extends AbstractController $this->entityManager->flush(); $searchResults = []; + $hasAnyResults = false; - foreach ($parts as $part) { - $partResult = [ - 'part' => $part, - 'search_results' => [], - 'errors' => [] - ]; - - // Collect all DTOs from all applicable field mappings - $allDtos = []; - $dtoMetadata = []; // Store source field info separately - - foreach ($fieldMappings as $mapping) { - $field = $mapping['field']; - $providers = $mapping['providers'] ?? []; - - if (empty($providers)) { - continue; - } - - $keyword = $this->getKeywordFromField($part, $field); - - if ($keyword) { - try { - $dtos = $this->infoRetriever->searchByKeyword( - keyword: $keyword, - providers: $providers - ); - - // Store field info for each DTO separately - foreach ($dtos as $dto) { - $dtoKey = $dto->provider_key . '|' . $dto->provider_id; - $dtoMetadata[$dtoKey] = [ - 'source_field' => $field, - 'source_keyword' => $keyword + try { + // Optimize: Use batch async requests for LCSC provider + $lcscKeywords = []; + $keywordToPartField = []; + + // First, collect all LCSC keywords for batch processing + foreach ($parts as $part) { + foreach ($fieldMappings as $mapping) { + $field = $mapping['field']; + $providers = $mapping['providers'] ?? []; + + if (in_array('lcsc', $providers, true)) { + $keyword = $this->getKeywordFromField($part, $field); + if ($keyword) { + $lcscKeywords[] = $keyword; + $keywordToPartField[$keyword] = [ + 'part' => $part, + 'field' => $field ]; } - - $allDtos = array_merge($allDtos, $dtos); - } catch (ClientException $e) { - $partResult['errors'][] = "Error searching with {$field}: " . $e->getMessage(); - $exceptionLogger->error('Error during bulk info provider search for part ' . $part->getId() . " field {$field}: " . $e->getMessage(), ['exception' => $e]); } } } - // Remove duplicates based on provider_key + provider_id - $uniqueDtos = []; - $seenKeys = []; - foreach ($allDtos as $dto) { - if ($dto === null || !isset($dto->provider_key, $dto->provider_id)) { - continue; - } - $key = "{$dto->provider_key}|{$dto->provider_id}"; - if (!in_array($key, $seenKeys, true)) { - $seenKeys[] = $key; - $uniqueDtos[] = $dto; + // Batch search LCSC keywords asynchronously + $lcscBatchResults = []; + if (!empty($lcscKeywords)) { + try { + // Try to get LCSC provider and use batch method if available + $lcscBatchResults = $this->searchLcscBatch($lcscKeywords); + } catch (\Exception $e) { + $exceptionLogger->warning('LCSC batch search failed, falling back to individual requests', [ + 'error' => $e->getMessage() + ]); } } - // Convert DTOs to result format with metadata - $partResult['search_results'] = array_map( - function ($dto) use ($dtoMetadata) { - $dtoKey = $dto->provider_key . '|' . $dto->provider_id; - $metadata = $dtoMetadata[$dtoKey] ?? []; - return [ - 'dto' => $dto, - 'localPart' => $this->existingPartFinder->findFirstExisting($dto), - 'source_field' => $metadata['source_field'] ?? null, - 'source_keyword' => $metadata['source_keyword'] ?? null - ]; - }, - $uniqueDtos - ); + // Now process each part + foreach ($parts as $part) { + $partResult = [ + 'part' => $part, + 'search_results' => [], + 'errors' => [] + ]; - $searchResults[] = $partResult; + // Collect all DTOs using priority-based search + $allDtos = []; + $dtoMetadata = []; // Store source field info separately + + // Group mappings by priority (lower number = higher priority) + $mappingsByPriority = []; + foreach ($fieldMappings as $mapping) { + $priority = $mapping['priority'] ?? 1; + $mappingsByPriority[$priority][] = $mapping; + } + ksort($mappingsByPriority); // Sort by priority (1, 2, 3...) + + // Try each priority level until we find results + foreach ($mappingsByPriority as $priority => $mappings) { + $priorityResults = []; + + // For same priority, search all and combine results + foreach ($mappings as $mapping) { + $field = $mapping['field']; + $providers = $mapping['providers'] ?? []; + + if (empty($providers)) { + continue; + } + + $keyword = $this->getKeywordFromField($part, $field); + + if ($keyword) { + try { + // Use batch results for LCSC if available + if (in_array('lcsc', $providers, true) && isset($lcscBatchResults[$keyword])) { + $dtos = $lcscBatchResults[$keyword]; + } else { + // Fall back to regular search for non-LCSC providers + $dtos = $this->infoRetriever->searchByKeyword( + keyword: $keyword, + providers: $providers + ); + } + + // Store field info for each DTO separately + foreach ($dtos as $dto) { + $dtoKey = $dto->provider_key . '|' . $dto->provider_id; + $dtoMetadata[$dtoKey] = [ + 'source_field' => $field, + 'source_keyword' => $keyword, + 'priority' => $priority + ]; + } + + $priorityResults = array_merge($priorityResults, $dtos); + } catch (ClientException $e) { + $partResult['errors'][] = "Error searching with {$field} (priority {$priority}): " . $e->getMessage(); + $exceptionLogger->error('Error during bulk info provider search for part ' . $part->getId() . " field {$field}: " . $e->getMessage(), ['exception' => $e]); + } + } + } + + // If we found results at this priority level, use them and stop + if (!empty($priorityResults)) { + $allDtos = $priorityResults; + break; + } + } + + // Remove duplicates based on provider_key + provider_id + $uniqueDtos = []; + $seenKeys = []; + foreach ($allDtos as $dto) { + if ($dto === null || !isset($dto->provider_key, $dto->provider_id)) { + continue; + } + $key = "{$dto->provider_key}|{$dto->provider_id}"; + if (!in_array($key, $seenKeys, true)) { + $seenKeys[] = $key; + $uniqueDtos[] = $dto; + } + } + + // Convert DTOs to result format with metadata + $partResult['search_results'] = array_map( + function ($dto) use ($dtoMetadata) { + $dtoKey = $dto->provider_key . '|' . $dto->provider_id; + $metadata = $dtoMetadata[$dtoKey] ?? []; + return [ + 'dto' => $dto, + 'localPart' => $this->existingPartFinder->findFirstExisting($dto), + 'source_field' => $metadata['source_field'] ?? null, + 'source_keyword' => $metadata['source_keyword'] ?? null + ]; + }, + $uniqueDtos + ); + + if (!empty($partResult['search_results'])) { + $hasAnyResults = true; + } + + $searchResults[] = $partResult; + } + + // Check if search was successful + if (!$hasAnyResults) { + $exceptionLogger->warning('Bulk import search returned no results for any parts', [ + 'job_id' => $job->getId(), + 'parts_count' => count($parts) + ]); + + // Delete the job since it has no useful results + $this->entityManager->remove($job); + $this->entityManager->flush(); + + $this->addFlash('error', 'No search results found for any of the selected parts. Please check your field mappings and provider selections.'); + return $this->redirectToRoute('bulk_info_provider_step1', ['ids' => implode(',', $partIds)]); + } + + // Save search results to job + $job->setSearchResults($this->serializeSearchResults($searchResults)); + $job->markAsInProgress(); + $this->entityManager->flush(); + + } catch (\Exception $e) { + $exceptionLogger->error('Critical error during bulk import search', [ + 'job_id' => $job->getId(), + 'error' => $e->getMessage(), + 'exception' => $e + ]); + + // Delete the job on critical failure + $this->entityManager->remove($job); + $this->entityManager->flush(); + + $this->addFlash('error', 'Search failed due to an error: ' . $e->getMessage()); + return $this->redirectToRoute('bulk_info_provider_step1', ['ids' => implode(',', $partIds)]); } - // Save search results to job - $job->setSearchResults($this->serializeSearchResults($searchResults)); - $job->markAsInProgress(); - $this->entityManager->flush(); - // Prefetch details if requested if ($prefetchDetails) { + $exceptionLogger->info('Prefetch details requested, starting prefetch for ' . count($searchResults) . ' parts'); $this->prefetchDetailsForResults($searchResults, $exceptionLogger); + } else { + $exceptionLogger->info('Prefetch details not requested, skipping prefetch'); } // Redirect to step 2 with the job @@ -236,21 +353,40 @@ class BulkInfoProviderImportController extends AbstractController ->findBy([], ['createdAt' => 'DESC']); // Check and auto-complete jobs that should be completed + // Also clean up jobs with no results (failed searches) $updatedJobs = false; + $jobsToDelete = []; + foreach ($allJobs as $job) { if ($job->isAllPartsCompleted() && !$job->isCompleted()) { $job->markAsCompleted(); $updatedJobs = true; } + + // Mark jobs with no results for deletion (failed searches) + if ($job->getResultCount() === 0 && $job->isInProgress()) { + $jobsToDelete[] = $job; + } + } + + // Delete failed jobs + foreach ($jobsToDelete as $job) { + $this->entityManager->remove($job); + $updatedJobs = true; } // Flush changes if any jobs were updated if ($updatedJobs) { $this->entityManager->flush(); + + if (!empty($jobsToDelete)) { + $this->addFlash('info', 'Cleaned up ' . count($jobsToDelete) . ' failed job(s) with no results.'); + } } return $this->render('info_providers/bulk_import/manage.html.twig', [ - 'jobs' => $allJobs + 'jobs' => $this->entityManager->getRepository(BulkInfoProviderImportJob::class) + ->findBy([], ['createdAt' => 'DESC']) // Refetch after cleanup ]); } @@ -478,6 +614,25 @@ class BulkInfoProviderImportController extends AbstractController return $searchResults; } + /** + * Perform batch LCSC search using async HTTP requests + */ + private function searchLcscBatch(array $keywords): array + { + // Get LCSC provider through reflection since PartInfoRetriever doesn't expose it + $reflection = new \ReflectionClass($this->infoRetriever); + $registryProp = $reflection->getProperty('provider_registry'); + $registryProp->setAccessible(true); + $registry = $registryProp->getValue($this->infoRetriever); + + $lcscProvider = $registry->getProviderByKey('lcsc'); + if ($lcscProvider && method_exists($lcscProvider, 'searchByKeywordsBatch')) { + return $lcscProvider->searchByKeywordsBatch($keywords); + } + + return []; + } + #[Route('/job/{jobId}/part/{partId}/mark-completed', name: 'bulk_info_provider_mark_completed', methods: ['POST'])] public function markPartCompleted(int $jobId, int $partId): Response { diff --git a/src/Controller/PartController.php b/src/Controller/PartController.php index e9c577f0..d1087254 100644 --- a/src/Controller/PartController.php +++ b/src/Controller/PartController.php @@ -65,12 +65,14 @@ use function Symfony\Component\Translation\t; #[Route(path: '/part')] class PartController extends AbstractController { - public function __construct(protected PricedetailHelper $pricedetailHelper, + public function __construct( + protected PricedetailHelper $pricedetailHelper, protected PartPreviewGenerator $partPreviewGenerator, private readonly TranslatorInterface $translator, - private readonly AttachmentSubmitHandler $attachmentSubmitHandler, private readonly EntityManagerInterface $em, - protected EventCommentHelper $commentHelper) - { + private readonly AttachmentSubmitHandler $attachmentSubmitHandler, + private readonly EntityManagerInterface $em, + protected EventCommentHelper $commentHelper + ) { } /** @@ -79,9 +81,16 @@ class PartController extends AbstractController */ #[Route(path: '/{id}/info/{timestamp}', name: 'part_info')] #[Route(path: '/{id}', requirements: ['id' => '\d+'])] - public function show(Part $part, Request $request, TimeTravel $timeTravel, HistoryHelper $historyHelper, - DataTableFactory $dataTable, ParameterExtractor $parameterExtractor, PartLotWithdrawAddHelper $withdrawAddHelper, ?string $timestamp = null): Response - { + public function show( + Part $part, + Request $request, + TimeTravel $timeTravel, + HistoryHelper $historyHelper, + DataTableFactory $dataTable, + ParameterExtractor $parameterExtractor, + PartLotWithdrawAddHelper $withdrawAddHelper, + ?string $timestamp = null + ): Response { $this->denyAccessUnlessGranted('read', $part); $timeTravel_timestamp = null; @@ -151,22 +160,22 @@ class PartController extends AbstractController public function markBulkImportComplete(Part $part, int $jobId, Request $request): Response { $this->denyAccessUnlessGranted('edit', $part); - + if (!$this->isCsrfTokenValid('bulk_complete_' . $part->getId(), $request->request->get('_token'))) { throw $this->createAccessDeniedException('Invalid CSRF token'); } - + $bulkJob = $this->em->getRepository(\App\Entity\BulkInfoProviderImportJob::class)->find($jobId); if (!$bulkJob || $bulkJob->getCreatedBy() !== $this->getUser()) { throw $this->createNotFoundException('Bulk import job not found'); } - + $bulkJob->markPartAsCompleted($part->getId()); $this->em->persist($bulkJob); $this->em->flush(); - + $this->addFlash('success', 'Part marked as completed in bulk import'); - + return $this->redirectToRoute('bulk_info_provider_step2', ['jobId' => $jobId]); } @@ -175,7 +184,7 @@ class PartController extends AbstractController { $this->denyAccessUnlessGranted('delete', $part); - if ($this->isCsrfTokenValid('delete'.$part->getID(), $request->request->get('_token'))) { + if ($this->isCsrfTokenValid('delete' . $part->getID(), $request->request->get('_token'))) { $this->commentHelper->setMessage($request->request->get('log_comment', null)); @@ -194,11 +203,15 @@ class PartController extends AbstractController #[Route(path: '/new', name: 'part_new')] #[Route(path: '/{id}/clone', name: 'part_clone')] #[Route(path: '/new_build_part/{project_id}', name: 'part_new_build_part')] - public function new(Request $request, EntityManagerInterface $em, TranslatorInterface $translator, - AttachmentSubmitHandler $attachmentSubmitHandler, ProjectBuildPartHelper $projectBuildPartHelper, + public function new( + Request $request, + EntityManagerInterface $em, + TranslatorInterface $translator, + AttachmentSubmitHandler $attachmentSubmitHandler, + ProjectBuildPartHelper $projectBuildPartHelper, #[MapEntity(mapping: ['id' => 'id'])] ?Part $part = null, - #[MapEntity(mapping: ['project_id' => 'id'])] ?Project $project = null): Response - { + #[MapEntity(mapping: ['project_id' => 'id'])] ?Project $project = null + ): Response { if ($part instanceof Part) { //Clone part @@ -293,9 +306,14 @@ class PartController extends AbstractController } #[Route(path: '/{id}/from_info_provider/{providerKey}/{providerId}/update', name: 'info_providers_update_part', requirements: ['providerId' => '.+'])] - public function updateFromInfoProvider(Part $part, Request $request, string $providerKey, string $providerId, - PartInfoRetriever $infoRetriever, PartMerger $partMerger): Response - { + public function updateFromInfoProvider( + Part $part, + Request $request, + string $providerKey, + string $providerId, + PartInfoRetriever $infoRetriever, + PartMerger $partMerger + ): Response { $this->denyAccessUnlessGranted('edit', $part); $this->denyAccessUnlessGranted('@info_providers.create_parts'); @@ -359,7 +377,7 @@ class PartController extends AbstractController } catch (AttachmentDownloadException $attachmentDownloadException) { $this->addFlash( 'error', - $this->translator->trans('attachment.download_failed').' '.$attachmentDownloadException->getMessage() + $this->translator->trans('attachment.download_failed') . ' ' . $attachmentDownloadException->getMessage() ); } } @@ -405,7 +423,7 @@ class PartController extends AbstractController if ($jobId && isset($merge_infos['bulk_job'])) { return $this->redirectToRoute('part_edit', ['id' => $new_part->getID(), 'jobId' => $jobId]); } - + return $this->redirectToRoute('part_edit', ['id' => $new_part->getID()]); } @@ -424,7 +442,8 @@ class PartController extends AbstractController $template = 'parts/edit/update_from_ip.html.twig'; } - return $this->render($template, + return $this->render( + $template, [ 'part' => $new_part, 'form' => $form, @@ -432,7 +451,8 @@ class PartController extends AbstractController 'merge_other' => $merge_infos['other_part'] ?? null, 'bulk_job' => $merge_infos['bulk_job'] ?? null, 'jobId' => $request->query->get('jobId') - ]); + ] + ); } @@ -442,17 +462,17 @@ class PartController extends AbstractController if ($this->isCsrfTokenValid('part_withraw' . $part->getID(), $request->request->get('_csfr'))) { //Retrieve partlot from the request $partLot = $em->find(PartLot::class, $request->request->get('lot_id')); - if(!$partLot instanceof PartLot) { + if (!$partLot instanceof PartLot) { throw new \RuntimeException('Part lot not found!'); } //Ensure that the partlot belongs to the part - if($partLot->getPart() !== $part) { + if ($partLot->getPart() !== $part) { throw new \RuntimeException("The origin partlot does not belong to the part!"); } //Try to determine the target lot (used for move actions), if the parameter is existing $targetId = $request->request->get('target_id', null); - $targetLot = $targetId ? $em->find(PartLot::class, $targetId) : null; + $targetLot = $targetId ? $em->find(PartLot::class, $targetId) : null; if ($targetLot && $targetLot->getPart() !== $part) { throw new \RuntimeException("The target partlot does not belong to the part!"); } @@ -466,12 +486,12 @@ class PartController extends AbstractController $timestamp = null; $timestamp_str = $request->request->getString('timestamp', ''); //Try to parse the timestamp - if($timestamp_str !== '') { + if ($timestamp_str !== '') { $timestamp = new DateTime($timestamp_str); } //Ensure that the timestamp is not in the future - if($timestamp !== null && $timestamp > new DateTime("+20min")) { + if ($timestamp !== null && $timestamp > new DateTime("+20min")) { throw new \LogicException("The timestamp must not be in the future!"); } @@ -515,7 +535,7 @@ class PartController extends AbstractController err: //If a redirect was passed, then redirect there - if($request->request->get('_redirect')) { + if ($request->request->get('_redirect')) { return $this->redirect($request->request->get('_redirect')); } //Otherwise just redirect to the part page diff --git a/src/Form/InfoProviderSystem/FieldToProviderMappingType.php b/src/Form/InfoProviderSystem/FieldToProviderMappingType.php index 20506fc8..fa7ee28b 100644 --- a/src/Form/InfoProviderSystem/FieldToProviderMappingType.php +++ b/src/Form/InfoProviderSystem/FieldToProviderMappingType.php @@ -24,6 +24,7 @@ namespace App\Form\InfoProviderSystem; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\Extension\Core\Type\IntegerType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -47,6 +48,19 @@ class FieldToProviderMappingType extends AbstractType 'help' => 'info_providers.bulk_search.providers.help', 'required' => false, ]); + + $builder->add('priority', IntegerType::class, [ + 'label' => 'info_providers.bulk_search.priority', + 'help' => 'info_providers.bulk_search.priority.help', + 'required' => false, + 'data' => 1, // Default priority + 'attr' => [ + 'min' => 1, + 'max' => 10, + 'class' => 'form-control-sm', + 'style' => 'width: 80px;' + ] + ]); } public function configureOptions(OptionsResolver $resolver): void diff --git a/templates/info_providers/bulk_import/step1.html.twig b/templates/info_providers/bulk_import/step1.html.twig index 5c3436de..af6a2fcb 100644 --- a/templates/info_providers/bulk_import/step1.html.twig +++ b/templates/info_providers/bulk_import/step1.html.twig @@ -31,7 +31,7 @@ {% trans %}info_providers.bulk_import.progress{% endtrans %} {% trans %}info_providers.bulk_import.status{% endtrans %} {% trans %}info_providers.bulk_import.created_at{% endtrans %} - {% trans %}action.label{% endtrans %} + {% trans %}info_providers.bulk_import.action.label{% endtrans %} @@ -87,6 +87,14 @@ {% trans %}info_providers.bulk_import.step1.global_mapping_description{% endtrans %} + +
{% if provider.providerInfo.settings_class is defined %} - {% endif %} From 117ff4484d01ffe1122d5c9a32b58bd71293b87a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 6 Sep 2025 00:10:50 +0200 Subject: [PATCH 049/228] Allow to show what permissions a user is lacking in case of access denied message Should help with errors like 1026 --- src/Security/Voter/PermissionVoter.php | 10 ++++++-- src/Services/UserSystem/VoterHelper.php | 24 +++++++++++++++++-- .../TwigBundle/Exception/error403.html.twig | 7 ++++-- 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/Security/Voter/PermissionVoter.php b/src/Security/Voter/PermissionVoter.php index c6ec1b3d..8c304d86 100644 --- a/src/Security/Voter/PermissionVoter.php +++ b/src/Security/Voter/PermissionVoter.php @@ -24,6 +24,7 @@ namespace App\Security\Voter; use App\Services\UserSystem\VoterHelper; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authorization\Voter\Vote; use Symfony\Component\Security\Core\Authorization\Voter\Voter; /** @@ -39,12 +40,17 @@ final class PermissionVoter extends Voter } - protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool + protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token, ?Vote $vote = null): bool { $attribute = ltrim($attribute, '@'); [$perm, $op] = explode('.', $attribute); - return $this->helper->isGranted($token, $perm, $op); + $result = $this->helper->isGranted($token, $perm, $op); + if ($result === false) { + $this->helper->addReason($vote, $perm, $op); + } + + return $result; } public function supportsAttribute(string $attribute): bool diff --git a/src/Services/UserSystem/VoterHelper.php b/src/Services/UserSystem/VoterHelper.php index 644351f4..dda00de7 100644 --- a/src/Services/UserSystem/VoterHelper.php +++ b/src/Services/UserSystem/VoterHelper.php @@ -28,6 +28,9 @@ use App\Repository\UserRepository; use App\Security\ApiTokenAuthenticatedToken; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authorization\Voter\Vote; +use Symfony\Component\Security\Core\Authorization\Voter\Voter; +use Symfony\Contracts\Translation\TranslatorInterface; /** * @see \App\Tests\Services\UserSystem\VoterHelperTest @@ -35,10 +38,14 @@ use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; final class VoterHelper { private readonly UserRepository $userRepository; + private readonly array $permissionStructure; - public function __construct(private readonly PermissionManager $permissionManager, private readonly EntityManagerInterface $entityManager) + public function __construct(private readonly PermissionManager $permissionManager, + private readonly TranslatorInterface $translator, + private readonly EntityManagerInterface $entityManager) { $this->userRepository = $this->entityManager->getRepository(User::class); + $this->permissionStructure = $this->permissionManager->getPermissionStructure(); } /** @@ -124,4 +131,17 @@ final class VoterHelper { return $this->permissionManager->isValidOperation($permission, $operation); } -} \ No newline at end of file + + public function addReason(?Vote $voter, string $permission, $operation): void + { + if ($voter !== null) { + $voter->addReason(sprintf("User does not have permission %s -> %s -> %s (%s.%s).", + $this->translator->trans('perm.group.'.$this->permissionStructure['perms'][$permission]['group'] ?? 'default' ), + $this->translator->trans($this->permissionStructure['perms'][$permission]['label'] ?? $permission), + $this->translator->trans($this->permissionStructure['perms'][$permission]['operations'][$operation]['label'] ?? $operation), + $permission, + $operation + )); + } + } +} diff --git a/templates/bundles/TwigBundle/Exception/error403.html.twig b/templates/bundles/TwigBundle/Exception/error403.html.twig index f5987179..334670fc 100644 --- a/templates/bundles/TwigBundle/Exception/error403.html.twig +++ b/templates/bundles/TwigBundle/Exception/error403.html.twig @@ -1,6 +1,9 @@ {% extends "bundles/TwigBundle/Exception/error.html.twig" %} {% block status_comment %} - Nice try! But you are not allowed to do this! + Nice try! But you are not allowed to do this!
+ {{ exception.message }}
If you think you should have access to this ressource, contact the adminstrator. -{% endblock %} \ No newline at end of file + + +{% endblock %} From eb4258053e338d4816cf036f244f7cdef5f83900 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 6 Sep 2025 00:24:55 +0200 Subject: [PATCH 050/228] Added voter reason explaination to the other voters --- src/Security/Voter/AttachmentVoter.php | 9 ++++++--- src/Security/Voter/GroupVoter.php | 5 +++-- src/Security/Voter/ImpersonateUserVoter.php | 14 +++++++++++--- src/Security/Voter/LabelProfileVoter.php | 5 +++-- src/Security/Voter/LogEntryVoter.php | 9 +++++---- src/Security/Voter/OrderdetailVoter.php | 5 +++-- src/Security/Voter/ParameterVoter.php | 5 +++-- src/Security/Voter/PartAssociationVoter.php | 5 +++-- src/Security/Voter/PartLotVoter.php | 11 ++++++++--- src/Security/Voter/PartVoter.php | 6 +++--- src/Security/Voter/PricedetailVoter.php | 5 +++-- src/Security/Voter/StructureVoter.php | 5 +++-- src/Security/Voter/UserVoter.php | 7 ++++--- src/Services/UserSystem/VoterHelper.php | 9 +++++++-- 14 files changed, 65 insertions(+), 35 deletions(-) diff --git a/src/Security/Voter/AttachmentVoter.php b/src/Security/Voter/AttachmentVoter.php index c2b17053..bd7ae4df 100644 --- a/src/Security/Voter/AttachmentVoter.php +++ b/src/Security/Voter/AttachmentVoter.php @@ -41,6 +41,7 @@ use App\Entity\Attachments\UserAttachment; use RuntimeException; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authorization\Voter\Vote; use Symfony\Component\Security\Core\Authorization\Voter\Voter; use function in_array; @@ -56,7 +57,7 @@ final class AttachmentVoter extends Voter { } - protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool + protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token, ?Vote $vote = null): bool { //This voter only works for attachments @@ -65,7 +66,8 @@ final class AttachmentVoter extends Voter } if ($attribute === 'show_private') { - return $this->helper->isGranted($token, 'attachments', 'show_private'); + $vote?->addReason('User is not allowed to view private attachments.'); + return $this->helper->isGranted($token, 'attachments', 'show_private', $vote); } @@ -111,7 +113,8 @@ final class AttachmentVoter extends Voter throw new RuntimeException('Encountered unknown Parameter type: ' . $subject); } - return $this->helper->isGranted($token, $param, $this->mapOperation($attribute)); + $vote?->addReason('User is not allowed to '.$this->mapOperation($attribute).' attachments of type '.$param.'.'); + return $this->helper->isGranted($token, $param, $this->mapOperation($attribute), $vote); } return false; diff --git a/src/Security/Voter/GroupVoter.php b/src/Security/Voter/GroupVoter.php index 34839d38..f2ce6953 100644 --- a/src/Security/Voter/GroupVoter.php +++ b/src/Security/Voter/GroupVoter.php @@ -25,6 +25,7 @@ namespace App\Security\Voter; use App\Entity\UserSystem\Group; use App\Services\UserSystem\VoterHelper; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authorization\Voter\Vote; use Symfony\Component\Security\Core\Authorization\Voter\Voter; /** @@ -43,9 +44,9 @@ final class GroupVoter extends Voter * * @param string $attribute */ - protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool + protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token, ?Vote $vote = null): bool { - return $this->helper->isGranted($token, 'groups', $attribute); + return $this->helper->isGranted($token, 'groups', $attribute, $vote); } /** diff --git a/src/Security/Voter/ImpersonateUserVoter.php b/src/Security/Voter/ImpersonateUserVoter.php index edf55c62..1f8a70c6 100644 --- a/src/Security/Voter/ImpersonateUserVoter.php +++ b/src/Security/Voter/ImpersonateUserVoter.php @@ -26,6 +26,7 @@ namespace App\Security\Voter; use App\Entity\UserSystem\User; use App\Services\UserSystem\VoterHelper; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authorization\Voter\Vote; use Symfony\Component\Security\Core\Authorization\Voter\Voter; use Symfony\Component\Security\Core\User\UserInterface; @@ -47,9 +48,16 @@ final class ImpersonateUserVoter extends Voter && $subject instanceof UserInterface; } - protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool + protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token, ?Vote $vote = null): bool { - return $this->helper->isGranted($token, 'users', 'impersonate'); + $result = $this->helper->isGranted($token, 'users', 'impersonate'); + + if ($result === false) { + $vote?->addReason('User is not allowed to impersonate other users.'); + $this->helper->addReason($vote, 'users', 'impersonate'); + } + + return $result; } public function supportsAttribute(string $attribute): bool @@ -61,4 +69,4 @@ final class ImpersonateUserVoter extends Voter { return is_a($subjectType, User::class, true); } -} \ No newline at end of file +} diff --git a/src/Security/Voter/LabelProfileVoter.php b/src/Security/Voter/LabelProfileVoter.php index 47505bf9..cd349ddb 100644 --- a/src/Security/Voter/LabelProfileVoter.php +++ b/src/Security/Voter/LabelProfileVoter.php @@ -44,6 +44,7 @@ namespace App\Security\Voter; use App\Entity\LabelSystem\LabelProfile; use App\Services\UserSystem\VoterHelper; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authorization\Voter\Vote; use Symfony\Component\Security\Core\Authorization\Voter\Voter; /** @@ -63,9 +64,9 @@ final class LabelProfileVoter extends Voter public function __construct(private readonly VoterHelper $helper) {} - protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool + protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token, ?Vote $vote = null): bool { - return $this->helper->isGranted($token, 'labels', self::MAPPING[$attribute]); + return $this->helper->isGranted($token, 'labels', self::MAPPING[$attribute], $vote); } protected function supports($attribute, $subject): bool diff --git a/src/Security/Voter/LogEntryVoter.php b/src/Security/Voter/LogEntryVoter.php index 08bc3b70..dcb75a7a 100644 --- a/src/Security/Voter/LogEntryVoter.php +++ b/src/Security/Voter/LogEntryVoter.php @@ -26,6 +26,7 @@ use App\Services\UserSystem\VoterHelper; use Symfony\Bundle\SecurityBundle\Security; use App\Entity\LogSystem\AbstractLogEntry; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authorization\Voter\Vote; use Symfony\Component\Security\Core\Authorization\Voter\Voter; /** @@ -39,7 +40,7 @@ final class LogEntryVoter extends Voter { } - protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool + protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token, ?Vote $vote = null): bool { $user = $this->helper->resolveUser($token); @@ -48,19 +49,19 @@ final class LogEntryVoter extends Voter } if ('delete' === $attribute) { - return $this->helper->isGranted($token, 'system', 'delete_logs'); + return $this->helper->isGranted($token, 'system', 'delete_logs', $vote); } if ('read' === $attribute) { //Allow read of the users own log entries if ( $subject->getUser() === $user - && $this->helper->isGranted($token, 'self', 'show_logs') + && $this->helper->isGranted($token, 'self', 'show_logs', $vote) ) { return true; } - return $this->helper->isGranted($token, 'system', 'show_logs'); + return $this->helper->isGranted($token, 'system', 'show_logs', $vote); } if ('show_details' === $attribute) { diff --git a/src/Security/Voter/OrderdetailVoter.php b/src/Security/Voter/OrderdetailVoter.php index 20843b9a..3bb2a3a3 100644 --- a/src/Security/Voter/OrderdetailVoter.php +++ b/src/Security/Voter/OrderdetailVoter.php @@ -46,6 +46,7 @@ use Symfony\Bundle\SecurityBundle\Security; use App\Entity\Parts\Part; use App\Entity\PriceInformations\Orderdetail; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authorization\Voter\Vote; use Symfony\Component\Security\Core\Authorization\Voter\Voter; /** @@ -59,7 +60,7 @@ final class OrderdetailVoter extends Voter protected const ALLOWED_PERMS = ['read', 'edit', 'create', 'delete', 'show_history', 'revert_element']; - protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool + protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token, ?Vote $vote = null): bool { if (! is_a($subject, Orderdetail::class, true)) { throw new \RuntimeException('This voter can only handle Orderdetail objects!'); @@ -75,7 +76,7 @@ final class OrderdetailVoter extends Voter //If we have no part associated use the generic part permission if (is_string($subject) || !$subject->getPart() instanceof Part) { - return $this->helper->isGranted($token, 'parts', $operation); + return $this->helper->isGranted($token, 'parts', $operation, $vote); } //Otherwise vote on the part diff --git a/src/Security/Voter/ParameterVoter.php b/src/Security/Voter/ParameterVoter.php index 8ee2b9f5..f59bdeaf 100644 --- a/src/Security/Voter/ParameterVoter.php +++ b/src/Security/Voter/ParameterVoter.php @@ -39,6 +39,7 @@ use App\Entity\Parameters\StorageLocationParameter; use App\Entity\Parameters\SupplierParameter; use RuntimeException; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authorization\Voter\Vote; use Symfony\Component\Security\Core\Authorization\Voter\Voter; /** @@ -53,7 +54,7 @@ final class ParameterVoter extends Voter { } - protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool + protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token, ?Vote $vote = null): bool { //return $this->resolver->inherit($user, 'attachments', $attribute) ?? false; @@ -108,7 +109,7 @@ final class ParameterVoter extends Voter throw new RuntimeException('Encountered unknown Parameter type: ' . (is_object($subject) ? $subject::class : $subject)); } - return $this->helper->isGranted($token, $param, $attribute); + return $this->helper->isGranted($token, $param, $attribute, $vote); } protected function supports(string $attribute, $subject): bool diff --git a/src/Security/Voter/PartAssociationVoter.php b/src/Security/Voter/PartAssociationVoter.php index 7678b67a..f1eb83c7 100644 --- a/src/Security/Voter/PartAssociationVoter.php +++ b/src/Security/Voter/PartAssociationVoter.php @@ -46,6 +46,7 @@ use App\Services\UserSystem\VoterHelper; use Symfony\Bundle\SecurityBundle\Security; use App\Entity\Parts\Part; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authorization\Voter\Vote; use Symfony\Component\Security\Core\Authorization\Voter\Voter; /** @@ -61,7 +62,7 @@ final class PartAssociationVoter extends Voter protected const ALLOWED_PERMS = ['read', 'edit', 'create', 'delete', 'show_history', 'revert_element']; - protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool + protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token, ?Vote $vote = null): bool { if (!is_string($subject) && !$subject instanceof PartAssociation) { throw new \RuntimeException('Invalid subject type!'); @@ -77,7 +78,7 @@ final class PartAssociationVoter extends Voter //If we have no part associated use the generic part permission if (is_string($subject) || !$subject->getOwner() instanceof Part) { - return $this->helper->isGranted($token, 'parts', $operation); + return $this->helper->isGranted($token, 'parts', $operation, $vote); } //Otherwise vote on the part diff --git a/src/Security/Voter/PartLotVoter.php b/src/Security/Voter/PartLotVoter.php index a64473c8..87c3d135 100644 --- a/src/Security/Voter/PartLotVoter.php +++ b/src/Security/Voter/PartLotVoter.php @@ -46,6 +46,7 @@ use Symfony\Bundle\SecurityBundle\Security; use App\Entity\Parts\Part; use App\Entity\Parts\PartLot; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authorization\Voter\Vote; use Symfony\Component\Security\Core\Authorization\Voter\Voter; /** @@ -59,13 +60,13 @@ final class PartLotVoter extends Voter protected const ALLOWED_PERMS = ['read', 'edit', 'create', 'delete', 'show_history', 'revert_element', 'withdraw', 'add', 'move']; - protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool + protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token, ?Vote $vote = null): bool { $user = $this->helper->resolveUser($token); if (in_array($attribute, ['withdraw', 'add', 'move'], true)) { - $base_permission = $this->helper->isGranted($token, 'parts_stock', $attribute); + $base_permission = $this->helper->isGranted($token, 'parts_stock', $attribute, $vote); $lot_permission = true; //If the lot has an owner, we need to check if the user is the owner of the lot to be allowed to withdraw it. @@ -73,6 +74,10 @@ final class PartLotVoter extends Voter $lot_permission = $subject->getOwner() === $user || $subject->getOwner()->getID() === $user->getID(); } + if (!$lot_permission) { + $vote->addReason('User is not the owner of the lot.'); + } + return $base_permission && $lot_permission; } @@ -86,7 +91,7 @@ final class PartLotVoter extends Voter //If we have no part associated use the generic part permission if (is_string($subject) || !$subject->getPart() instanceof Part) { - return $this->helper->isGranted($token, 'parts', $operation); + return $this->helper->isGranted($token, 'parts', $operation, $vote); } //Otherwise vote on the part diff --git a/src/Security/Voter/PartVoter.php b/src/Security/Voter/PartVoter.php index ef70b6ce..159e6893 100644 --- a/src/Security/Voter/PartVoter.php +++ b/src/Security/Voter/PartVoter.php @@ -25,6 +25,7 @@ namespace App\Security\Voter; use App\Entity\Parts\Part; use App\Services\UserSystem\VoterHelper; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authorization\Voter\Vote; use Symfony\Component\Security\Core\Authorization\Voter\Voter; /** @@ -52,10 +53,9 @@ final class PartVoter extends Voter return false; } - protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool + protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token, ?Vote $vote = null): bool { - //Null concealing operator means, that no - return $this->helper->isGranted($token, 'parts', $attribute); + return $this->helper->isGranted($token, 'parts', $attribute, $vote); } public function supportsAttribute(string $attribute): bool diff --git a/src/Security/Voter/PricedetailVoter.php b/src/Security/Voter/PricedetailVoter.php index 681b73b7..ca86f1ce 100644 --- a/src/Security/Voter/PricedetailVoter.php +++ b/src/Security/Voter/PricedetailVoter.php @@ -47,6 +47,7 @@ use App\Entity\PriceInformations\Orderdetail; use App\Entity\Parts\Part; use App\Entity\PriceInformations\Pricedetail; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authorization\Voter\Vote; use Symfony\Component\Security\Core\Authorization\Voter\Voter; /** @@ -60,7 +61,7 @@ final class PricedetailVoter extends Voter protected const ALLOWED_PERMS = ['read', 'edit', 'create', 'delete', 'show_history', 'revert_element']; - protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool + protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token, ?Vote $vote = null): bool { $operation = match ($attribute) { 'read' => 'read', @@ -72,7 +73,7 @@ final class PricedetailVoter extends Voter //If we have no part associated use the generic part permission if (is_string($subject) || !$subject->getOrderdetail() instanceof Orderdetail || !$subject->getOrderdetail()->getPart() instanceof Part) { - return $this->helper->isGranted($token, 'parts', $operation); + return $this->helper->isGranted($token, 'parts', $operation, $vote); } //Otherwise vote on the part diff --git a/src/Security/Voter/StructureVoter.php b/src/Security/Voter/StructureVoter.php index 2417b796..ad0299a7 100644 --- a/src/Security/Voter/StructureVoter.php +++ b/src/Security/Voter/StructureVoter.php @@ -33,6 +33,7 @@ use App\Entity\Parts\Supplier; use App\Entity\PriceInformations\Currency; use App\Services\UserSystem\VoterHelper; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authorization\Voter\Vote; use Symfony\Component\Security\Core\Authorization\Voter\Voter; use function is_object; @@ -113,10 +114,10 @@ final class StructureVoter extends Voter * * @param string $attribute */ - protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool + protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token, ?Vote $vote = null): bool { $permission_name = $this->instanceToPermissionName($subject); //Just resolve the permission - return $this->helper->isGranted($token, $permission_name, $attribute); + return $this->helper->isGranted($token, $permission_name, $attribute, $vote); } } diff --git a/src/Security/Voter/UserVoter.php b/src/Security/Voter/UserVoter.php index b41c1a40..97f8e4fb 100644 --- a/src/Security/Voter/UserVoter.php +++ b/src/Security/Voter/UserVoter.php @@ -26,6 +26,7 @@ use App\Entity\UserSystem\User; use App\Services\UserSystem\PermissionManager; use App\Services\UserSystem\VoterHelper; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authorization\Voter\Vote; use Symfony\Component\Security\Core\Authorization\Voter\Voter; use function in_array; @@ -79,7 +80,7 @@ final class UserVoter extends Voter * * @param string $attribute */ - protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool + protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token, ?Vote $vote = null): bool { $user = $this->helper->resolveUser($token); @@ -97,7 +98,7 @@ final class UserVoter extends Voter if (($subject instanceof User) && $subject->getID() === $user->getID() && $this->helper->isValidOperation('self', $attribute)) { //Then we also need to check the self permission - $tmp = $this->helper->isGranted($token, 'self', $attribute); + $tmp = $this->helper->isGranted($token, 'self', $attribute, $vote); //But if the self value is not allowed then use just the user value: if ($tmp) { return $tmp; @@ -106,7 +107,7 @@ final class UserVoter extends Voter //Else just check user permission: if ($this->helper->isValidOperation('users', $attribute)) { - return $this->helper->isGranted($token, 'users', $attribute); + return $this->helper->isGranted($token, 'users', $attribute, $vote); } return false; diff --git a/src/Services/UserSystem/VoterHelper.php b/src/Services/UserSystem/VoterHelper.php index dda00de7..bf65c58c 100644 --- a/src/Services/UserSystem/VoterHelper.php +++ b/src/Services/UserSystem/VoterHelper.php @@ -54,11 +54,16 @@ final class VoterHelper * @param TokenInterface $token The token to check * @param string $permission The permission to check * @param string $operation The operation to check + * @param Vote|null $vote The vote object to add reasons to (optional). If null, no reasons are added. * @return bool */ - public function isGranted(TokenInterface $token, string $permission, string $operation): bool + public function isGranted(TokenInterface $token, string $permission, string $operation, ?Vote $vote = null): bool { - return $this->isGrantedTrinary($token, $permission, $operation) ?? false; + $tmp = $this->isGrantedTrinary($token, $permission, $operation) ?? false; + if ($tmp === false) { + $this->addReason($vote, $permission, $operation); + } + return $tmp; } /** From fe7910a2f288ff7f081dae9e193de2d89bff2f49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 6 Sep 2025 00:39:16 +0200 Subject: [PATCH 051/228] Fixed invalid name for currency in data fixture --- src/DataFixtures/CurrencyFixtures.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DataFixtures/CurrencyFixtures.php b/src/DataFixtures/CurrencyFixtures.php index 0c05d578..2de5b277 100644 --- a/src/DataFixtures/CurrencyFixtures.php +++ b/src/DataFixtures/CurrencyFixtures.php @@ -51,7 +51,7 @@ class CurrencyFixtures extends Fixture $currency7 = new Currency(); $currency7->setName('Test Currency with long name'); - $currency7->setIsoCode('CHY'); + $currency7->setIsoCode('CNY'); $manager->persist($currency7); $manager->flush(); From 9b17efc12c33624f5c074c9ada3571495ed2601b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 6 Sep 2025 00:39:23 +0200 Subject: [PATCH 052/228] Fixed phpstan issue --- src/Services/UserSystem/VoterHelper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Services/UserSystem/VoterHelper.php b/src/Services/UserSystem/VoterHelper.php index bf65c58c..d3c5368c 100644 --- a/src/Services/UserSystem/VoterHelper.php +++ b/src/Services/UserSystem/VoterHelper.php @@ -141,7 +141,7 @@ final class VoterHelper { if ($voter !== null) { $voter->addReason(sprintf("User does not have permission %s -> %s -> %s (%s.%s).", - $this->translator->trans('perm.group.'.$this->permissionStructure['perms'][$permission]['group'] ?? 'default' ), + $this->translator->trans('perm.group.'.($this->permissionStructure['perms'][$permission]['group'] ?? 'unknown') ), $this->translator->trans($this->permissionStructure['perms'][$permission]['label'] ?? $permission), $this->translator->trans($this->permissionStructure['perms'][$permission]['operations'][$operation]['label'] ?? $operation), $permission, From 065ef9f8ae75f040f74f574dfd7ff7376dee3dec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 6 Sep 2025 19:22:59 +0200 Subject: [PATCH 053/228] Fixed LCSC provider LCSC has changed its search API, so it was broken. Fixes issue #1018 --- src/Services/InfoProviderSystem/Providers/LCSCProvider.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Services/InfoProviderSystem/Providers/LCSCProvider.php b/src/Services/InfoProviderSystem/Providers/LCSCProvider.php index 58df3b82..75d38c14 100755 --- a/src/Services/InfoProviderSystem/Providers/LCSCProvider.php +++ b/src/Services/InfoProviderSystem/Providers/LCSCProvider.php @@ -123,11 +123,11 @@ class LCSCProvider implements InfoProviderInterface */ private function queryByTerm(string $term): array { - $response = $this->lcscClient->request('GET', self::ENDPOINT_URL . "/search/global", [ + $response = $this->lcscClient->request('POST', self::ENDPOINT_URL . "/search/v2/global", [ 'headers' => [ 'Cookie' => new Cookie('currencyCode', $this->settings->currency) ], - 'query' => [ + 'json' => [ 'keyword' => $term, ], ]); From b093866d157eaf4b4eb1130b3c47905c4164fd97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 6 Sep 2025 19:27:10 +0200 Subject: [PATCH 054/228] Do not replace LCSC category slashes with arrows, as these are actually their names, not level separators --- src/Services/InfoProviderSystem/Providers/LCSCProvider.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Services/InfoProviderSystem/Providers/LCSCProvider.php b/src/Services/InfoProviderSystem/Providers/LCSCProvider.php index 75d38c14..8db53f76 100755 --- a/src/Services/InfoProviderSystem/Providers/LCSCProvider.php +++ b/src/Services/InfoProviderSystem/Providers/LCSCProvider.php @@ -197,9 +197,6 @@ class LCSCProvider implements InfoProviderInterface $category = $product['parentCatalogName'] ?? null; if (isset($product['catalogName'])) { $category = ($category ?? '') . ' -> ' . $product['catalogName']; - - // Replace the / with a -> for better readability - $category = str_replace('/', ' -> ', $category); } return new PartDetailDTO( From c1b7272ab1c55a81209b7fe76f68d9b5bd76fc6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 6 Sep 2025 19:30:17 +0200 Subject: [PATCH 055/228] Updated frontend dependencies --- yarn.lock | 1518 ++++++++++++++++++++++++++--------------------------- 1 file changed, 759 insertions(+), 759 deletions(-) diff --git a/yarn.lock b/yarn.lock index 307692f2..3289c949 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,66 +2,58 @@ # yarn lockfile v1 -"@algolia/autocomplete-core@1.19.2": - version "1.19.2" - resolved "https://registry.yarnpkg.com/@algolia/autocomplete-core/-/autocomplete-core-1.19.2.tgz#702df67a08cb3cfe8c33ee1111ef136ec1a9e232" - integrity sha512-mKv7RyuAzXvwmq+0XRK8HqZXt9iZ5Kkm2huLjgn5JoCPtDy+oh9yxUMfDDaVCw0oyzZ1isdJBc7l9nuCyyR7Nw== +"@algolia/autocomplete-core@1.19.3": + version "1.19.3" + resolved "https://registry.yarnpkg.com/@algolia/autocomplete-core/-/autocomplete-core-1.19.3.tgz#f480d638d2b4218f8161f313186db7a5aac99c90" + integrity sha512-45CVTxtd3PwVux5G3WLUA3So5tRKRXu+amupW0dg3KTaTeydt+KzvH1mrZhs3hUne7VQ+g8+ZRGWHbuL/Rb5mw== dependencies: - "@algolia/autocomplete-plugin-algolia-insights" "1.19.2" - "@algolia/autocomplete-shared" "1.19.2" + "@algolia/autocomplete-plugin-algolia-insights" "1.19.3" + "@algolia/autocomplete-shared" "1.19.3" -"@algolia/autocomplete-js@1.19.2", "@algolia/autocomplete-js@^1.17.0": - version "1.19.2" - resolved "https://registry.yarnpkg.com/@algolia/autocomplete-js/-/autocomplete-js-1.19.2.tgz#3768a501671b43923aee8c111680d3738c432215" - integrity sha512-pUElPLQypSGwewihADgV/g57EWepn/jHoArnbtyJNvn4onJCDwmJGelCm5+dN/3dAYZq7QO2ExFEjGsoiG/nUg== +"@algolia/autocomplete-js@1.19.3", "@algolia/autocomplete-js@^1.17.0": + version "1.19.3" + resolved "https://registry.yarnpkg.com/@algolia/autocomplete-js/-/autocomplete-js-1.19.3.tgz#a3f733ac654201beb18c29e83b61653e5037c04c" + integrity sha512-uJPElcGy1jqi8WAzTBgX4xufu+cRYSaDfAZW3ed4AVTOu8oDwUkMgrKgpKxp5u8d6BhugSm47vGkYoj87jZQ/Q== dependencies: - "@algolia/autocomplete-core" "1.19.2" - "@algolia/autocomplete-preset-algolia" "1.19.2" - "@algolia/autocomplete-shared" "1.19.2" + "@algolia/autocomplete-core" "1.19.3" + "@algolia/autocomplete-preset-algolia" "1.19.3" + "@algolia/autocomplete-shared" "1.19.3" htm "^3.1.1" preact "^10.13.2" -"@algolia/autocomplete-plugin-algolia-insights@1.19.2": - version "1.19.2" - resolved "https://registry.yarnpkg.com/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.19.2.tgz#3584b625b9317e333d1ae43664d02358e175c52d" - integrity sha512-TjxbcC/r4vwmnZaPwrHtkXNeqvlpdyR+oR9Wi2XyfORkiGkLTVhX2j+O9SaCCINbKoDfc+c2PB8NjfOnz7+oKg== +"@algolia/autocomplete-plugin-algolia-insights@1.19.3": + version "1.19.3" + resolved "https://registry.yarnpkg.com/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.19.3.tgz#04e6e8150cd0964f7521acbb1eb1a3d650e9f60d" + integrity sha512-Oy6t0Ws99xWKCzrp7pFWncLqFA3MoBAv1DDbDrn2XN9NBE9GviXw2hZsBi6CFReR/9wK72xq4vT96LBshOxhaQ== dependencies: - "@algolia/autocomplete-shared" "1.19.2" + "@algolia/autocomplete-shared" "1.19.3" "@algolia/autocomplete-plugin-recent-searches@^1.17.0": - version "1.19.2" - resolved "https://registry.yarnpkg.com/@algolia/autocomplete-plugin-recent-searches/-/autocomplete-plugin-recent-searches-1.19.2.tgz#59341b2b6e121fedd1ab3e1652d86630f4c37fc4" - integrity sha512-V4VYzv0wvsBYsGxDcicpY17YRvayiFnMl24/kNAEBdIsxtF555Yfg0CHAmR55JdZRs9er/op1SOBpcc5+3V76g== + version "1.19.3" + resolved "https://registry.yarnpkg.com/@algolia/autocomplete-plugin-recent-searches/-/autocomplete-plugin-recent-searches-1.19.3.tgz#f6a98362dc7d7fcf080b17202dd8b6207fe447d2" + integrity sha512-RfY6TyolCa2gV655EKsz5sMp7E19C59ENJ3LBe5lRyq3o6sO5jNAMMyEBAp7y8M7uGRdepa6Y7Tch1zSLlCEEw== dependencies: - "@algolia/autocomplete-core" "1.19.2" - "@algolia/autocomplete-js" "1.19.2" - "@algolia/autocomplete-preset-algolia" "1.19.2" - "@algolia/autocomplete-shared" "1.19.2" + "@algolia/autocomplete-core" "1.19.3" + "@algolia/autocomplete-js" "1.19.3" + "@algolia/autocomplete-preset-algolia" "1.19.3" + "@algolia/autocomplete-shared" "1.19.3" -"@algolia/autocomplete-preset-algolia@1.19.2": - version "1.19.2" - resolved "https://registry.yarnpkg.com/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.19.2.tgz#c6c1e1ff7b011090a70e66b02e6db4ebade4535e" - integrity sha512-/Z9tDn84fnyUyjajvWRskOX7p/BDKK5PidEA4Y/aAl0c6VfHu5dMkTDG090CIiskLUgpkHacLbz+A10gMBP++Q== +"@algolia/autocomplete-preset-algolia@1.19.3": + version "1.19.3" + resolved "https://registry.yarnpkg.com/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.19.3.tgz#027fd0937bc22b72c3eecf56053ab55f79e4b423" + integrity sha512-NIvRLWFnX5MclQVyRKPwNDxjNg214qXCTZ/jLLVXw17VmPsEYfgeSYEMWEGFapA8KKKMz+Kwb+nBOc4je6DXfg== dependencies: - "@algolia/autocomplete-shared" "1.19.2" + "@algolia/autocomplete-shared" "1.19.3" -"@algolia/autocomplete-shared@1.19.2": - version "1.19.2" - resolved "https://registry.yarnpkg.com/@algolia/autocomplete-shared/-/autocomplete-shared-1.19.2.tgz#c0b7b8dc30a5c65b70501640e62b009535e4578f" - integrity sha512-jEazxZTVD2nLrC+wYlVHQgpBoBB5KPStrJxLzsIFl6Kqd1AlG9sIAGl39V5tECLpIQzB3Qa2T6ZPJ1ChkwMK/w== +"@algolia/autocomplete-shared@1.19.3": + version "1.19.3" + resolved "https://registry.yarnpkg.com/@algolia/autocomplete-shared/-/autocomplete-shared-1.19.3.tgz#9bd9dfd80fa3e320461682e917f0f94404f60eba" + integrity sha512-zzpqoVm/I4eRFT5Mcempwa5SVKox83eVIsZyLAYQdV+7tmtEYayx225Kl7nwhGrJ7NCozE9YWMwuFFN2g5dSBg== "@algolia/autocomplete-theme-classic@^1.17.0": - version "1.19.2" - resolved "https://registry.yarnpkg.com/@algolia/autocomplete-theme-classic/-/autocomplete-theme-classic-1.19.2.tgz#7c2a7d8f74988536f0a2f6ae806b18f915523266" - integrity sha512-UapO6bGuT5NkRK8VWxSg8AOLRhIcxBZ/OYg7ao//WHBo/yyiDybxy+K/xeY1RcHQVgimqlWfXj8IWAyQxxZP6A== - -"@ampproject/remapping@^2.2.0": - version "2.3.0" - resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" - integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw== - dependencies: - "@jridgewell/gen-mapping" "^0.3.5" - "@jridgewell/trace-mapping" "^0.3.24" + version "1.19.3" + resolved "https://registry.yarnpkg.com/@algolia/autocomplete-theme-classic/-/autocomplete-theme-classic-1.19.3.tgz#b4442911e3dc38bfb40c25f56b71f099a321f9c7" + integrity sha512-f0s9AxiqWTrv+etLcVXqzBTX5QbnR6JXJPmWu5mgkch7VY4AIqIuNB8ToDkSl1Hp9prkKir7/J9xEf7BDePHww== "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.27.1": version "7.27.1" @@ -73,25 +65,25 @@ picocolors "^1.1.1" "@babel/compat-data@^7.27.2", "@babel/compat-data@^7.27.7", "@babel/compat-data@^7.28.0": - version "7.28.0" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.28.0.tgz#9fc6fd58c2a6a15243cd13983224968392070790" - integrity sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw== + version "7.28.4" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.28.4.tgz#96fdf1af1b8859c8474ab39c295312bfb7c24b04" + integrity sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw== "@babel/core@^7.19.6": - version "7.28.3" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.28.3.tgz#aceddde69c5d1def69b839d09efa3e3ff59c97cb" - integrity sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ== + version "7.28.4" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.28.4.tgz#12a550b8794452df4c8b084f95003bce1742d496" + integrity sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA== dependencies: - "@ampproject/remapping" "^2.2.0" "@babel/code-frame" "^7.27.1" "@babel/generator" "^7.28.3" "@babel/helper-compilation-targets" "^7.27.2" "@babel/helper-module-transforms" "^7.28.3" - "@babel/helpers" "^7.28.3" - "@babel/parser" "^7.28.3" + "@babel/helpers" "^7.28.4" + "@babel/parser" "^7.28.4" "@babel/template" "^7.27.2" - "@babel/traverse" "^7.28.3" - "@babel/types" "^7.28.2" + "@babel/traverse" "^7.28.4" + "@babel/types" "^7.28.4" + "@jridgewell/remapping" "^2.3.5" convert-source-map "^2.0.0" debug "^4.1.0" gensync "^1.0.0-beta.2" @@ -252,20 +244,20 @@ "@babel/traverse" "^7.28.3" "@babel/types" "^7.28.2" -"@babel/helpers@^7.28.3": - version "7.28.3" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.28.3.tgz#b83156c0a2232c133d1b535dd5d3452119c7e441" - integrity sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw== +"@babel/helpers@^7.28.4": + version "7.28.4" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.28.4.tgz#fe07274742e95bdf7cf1443593eeb8926ab63827" + integrity sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w== dependencies: "@babel/template" "^7.27.2" - "@babel/types" "^7.28.2" + "@babel/types" "^7.28.4" -"@babel/parser@^7.18.9", "@babel/parser@^7.27.2", "@babel/parser@^7.28.3": - version "7.28.3" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.3.tgz#d2d25b814621bca5fe9d172bc93792547e7a2a71" - integrity sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA== +"@babel/parser@^7.18.9", "@babel/parser@^7.27.2", "@babel/parser@^7.28.3", "@babel/parser@^7.28.4": + version "7.28.4" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.4.tgz#da25d4643532890932cc03f7705fe19637e03fa8" + integrity sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg== dependencies: - "@babel/types" "^7.28.2" + "@babel/types" "^7.28.4" "@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.27.1": version "7.27.1" @@ -366,9 +358,9 @@ "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-block-scoping@^7.28.0": - version "7.28.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.0.tgz#e7c50cbacc18034f210b93defa89638666099451" - integrity sha512-gKKnwjpdx5sER/wl0WN0efUBFzF/56YZO0RJrSYP4CljXnP31ByY7fol89AzomdlLNzI36AvOTmYHsnZTCkq8Q== + version "7.28.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.4.tgz#e19ac4ddb8b7858bac1fd5c1be98a994d9726410" + integrity sha512-1yxmvN0MJHOhPVmAsmoW5liWwoILobu/d/ShymZmj867bAdxGbehIrew1DuLpw2Ukv+qDSSPQdYW1dLNE7t11A== dependencies: "@babel/helper-plugin-utils" "^7.27.1" @@ -389,16 +381,16 @@ "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-classes@^7.28.3": - version "7.28.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.3.tgz#598297260343d0edbd51cb5f5075e07dee91963a" - integrity sha512-DoEWC5SuxuARF2KdKmGUq3ghfPMO6ZzR12Dnp5gubwbeWJo4dbNWXJPVlwvh4Zlq6Z7YVvL8VFxeSOJgjsx4Sg== + version "7.28.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz#75d66175486788c56728a73424d67cbc7473495c" + integrity sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA== dependencies: "@babel/helper-annotate-as-pure" "^7.27.3" "@babel/helper-compilation-targets" "^7.27.2" "@babel/helper-globals" "^7.28.0" "@babel/helper-plugin-utils" "^7.27.1" "@babel/helper-replace-supers" "^7.27.1" - "@babel/traverse" "^7.28.3" + "@babel/traverse" "^7.28.4" "@babel/plugin-transform-computed-properties@^7.27.1": version "7.27.1" @@ -577,15 +569,15 @@ "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-object-rest-spread@^7.28.0": - version "7.28.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.0.tgz#d23021857ffd7cd809f54d624299b8086402ed8d" - integrity sha512-9VNGikXxzu5eCiQjdE4IZn8sb9q7Xsk5EXLDBKUYg1e/Tve8/05+KJEtcxGxAgCY5t/BpKQM+JEL/yT4tvgiUA== + version "7.28.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.4.tgz#9ee1ceca80b3e6c4bac9247b2149e36958f7f98d" + integrity sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew== dependencies: "@babel/helper-compilation-targets" "^7.27.2" "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-destructuring" "^7.28.0" "@babel/plugin-transform-parameters" "^7.27.7" - "@babel/traverse" "^7.28.0" + "@babel/traverse" "^7.28.4" "@babel/plugin-transform-object-super@^7.27.1": version "7.27.1" @@ -642,9 +634,9 @@ "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-regenerator@^7.28.3": - version "7.28.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.3.tgz#b8eee0f8aed37704bbcc932fd0b1a0a34d0b7344" - integrity sha512-K3/M/a4+ESb5LEldjQb+XSrpY0nF+ZBFlTCbSnKaYAMfD8v33O6PMs4uYnOk19HlcsI8WMu3McdFPTiQHF/1/A== + version "7.28.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.4.tgz#9d3fa3bebb48ddd0091ce5729139cd99c67cea51" + integrity sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA== dependencies: "@babel/helper-plugin-utils" "^7.27.1" @@ -824,180 +816,180 @@ "@babel/parser" "^7.27.2" "@babel/types" "^7.27.1" -"@babel/traverse@^7.18.9", "@babel/traverse@^7.27.1", "@babel/traverse@^7.28.0", "@babel/traverse@^7.28.3": - version "7.28.3" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.28.3.tgz#6911a10795d2cce43ec6a28cffc440cca2593434" - integrity sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ== +"@babel/traverse@^7.18.9", "@babel/traverse@^7.27.1", "@babel/traverse@^7.28.0", "@babel/traverse@^7.28.3", "@babel/traverse@^7.28.4": + version "7.28.4" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.28.4.tgz#8d456101b96ab175d487249f60680221692b958b" + integrity sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ== dependencies: "@babel/code-frame" "^7.27.1" "@babel/generator" "^7.28.3" "@babel/helper-globals" "^7.28.0" - "@babel/parser" "^7.28.3" + "@babel/parser" "^7.28.4" "@babel/template" "^7.27.2" - "@babel/types" "^7.28.2" + "@babel/types" "^7.28.4" debug "^4.3.1" -"@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.28.2", "@babel/types@^7.4.4": - version "7.28.2" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.2.tgz#da9db0856a9a88e0a13b019881d7513588cf712b" - integrity sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ== +"@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.28.2", "@babel/types@^7.28.4", "@babel/types@^7.4.4": + version "7.28.4" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.4.tgz#0a4e618f4c60a7cd6c11cb2d48060e4dbe38ac3a" + integrity sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q== dependencies: "@babel/helper-string-parser" "^7.27.1" "@babel/helper-validator-identifier" "^7.27.1" -"@ckeditor/ckeditor5-adapter-ckfinder@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-adapter-ckfinder/-/ckeditor5-adapter-ckfinder-46.0.2.tgz#a165fc259e91189d4f13cc83fc11f7f7e0c6a1b7" - integrity sha512-S4VO8l+WS8yVGpu9vB00rWNdFIR4NTAkuCP7iLlodB45KFgMobP1GTqF8EqNFIJEU2PHJz24R0kcsOyvfU6V/A== +"@ckeditor/ckeditor5-adapter-ckfinder@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-adapter-ckfinder/-/ckeditor5-adapter-ckfinder-46.0.3.tgz#f19f9fa1a0a33aa2fa502f0f7c779c027f4f78bd" + integrity sha512-xebONgXYuF8Fuhr6C+lpwRSfpChSrJKTy5S0i7vuBY+EeuXLRED7AuCOvPwV9oed1/CqbzDWWH1IefgkLwZwvQ== dependencies: - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-upload" "46.0.2" - ckeditor5 "46.0.2" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-upload" "46.0.3" + ckeditor5 "46.0.3" -"@ckeditor/ckeditor5-alignment@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-alignment/-/ckeditor5-alignment-46.0.2.tgz#68ce011f01e4bed205e8ee3cd2599a54b89af19a" - integrity sha512-iCVJIkmJ+DT2Podmc0gH8Ntj7rYr9kziYLup1VHo/k8mKPfqC3a6o6ngT8ZtPdr1nZ4h4kozVjF+ge2BqnxzmQ== +"@ckeditor/ckeditor5-alignment@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-alignment/-/ckeditor5-alignment-46.0.3.tgz#34cb75002fefc79dbffc94b08c0a0a34e722adb5" + integrity sha512-P0qegTFO9u5gbR7Ig/JI0vGdWFtxzM08KPCbeYTpQtdI9+DrKdvWFo0LVB7LJjR6OKuUPCtnulGgCyhuzNT7lw== dependencies: - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-icons" "46.0.2" - "@ckeditor/ckeditor5-ui" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" - ckeditor5 "46.0.2" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-icons" "46.0.3" + "@ckeditor/ckeditor5-ui" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" + ckeditor5 "46.0.3" -"@ckeditor/ckeditor5-autoformat@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-autoformat/-/ckeditor5-autoformat-46.0.2.tgz#9153d5186a4e5ddcb27b04a181fd21c18625e830" - integrity sha512-IMEWvgRCYw4PkUsshIb7V54fqJvLLohFLH+CQ0RtjzGE8ZYDkuusu7cHDz8hgQwlDWH5X7VOvTdEdPzb0uRhjA== +"@ckeditor/ckeditor5-autoformat@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-autoformat/-/ckeditor5-autoformat-46.0.3.tgz#ac2390550211aa71b7065559d4d9c135e3296ad0" + integrity sha512-E3bjlf8HbTD9FiGHPQyrbRXniA7W06CecmlKXwHDisGC8lLLF8ZpuRX4oGAH5QLpSVFyGuj0C1GJtVY0+PEjOw== dependencies: - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-engine" "46.0.2" - "@ckeditor/ckeditor5-heading" "46.0.2" - "@ckeditor/ckeditor5-typing" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" - ckeditor5 "46.0.2" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-engine" "46.0.3" + "@ckeditor/ckeditor5-heading" "46.0.3" + "@ckeditor/ckeditor5-typing" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" + ckeditor5 "46.0.3" -"@ckeditor/ckeditor5-autosave@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-autosave/-/ckeditor5-autosave-46.0.2.tgz#bcf3f2c44a5341c196343ace454992a3f36468d8" - integrity sha512-DKUCaGzbpwJC4FdWLVQivjJAkOkNqAaCv4+xNESPQvq8pGzBqHPFTZl0ZBvGUxEUj7S1dypIHkVWqRywSNsKJg== +"@ckeditor/ckeditor5-autosave@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-autosave/-/ckeditor5-autosave-46.0.3.tgz#d26d0157ebf4005fac8f802de16ce65188e64c92" + integrity sha512-SStt6opEniy0i5N5QMsAttpxhPvlmQ5UgmfvVmkyBnvOGwFwSmIFjxAXdTsAhvKdDaKrsjeCpv/j6L6llYk7dw== dependencies: - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" - ckeditor5 "46.0.2" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" + ckeditor5 "46.0.3" es-toolkit "1.39.5" -"@ckeditor/ckeditor5-basic-styles@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-basic-styles/-/ckeditor5-basic-styles-46.0.2.tgz#cc38af0cfb968911ee3ce302f1ae6a2e3dd5c57e" - integrity sha512-KFMNihlxg7LG7wKhG9OgAOqY621qkdz9clzLPmaoZzFydDfoVlnumFlC3cLnhIK1HOJvDnUec3u9te49pbqllQ== +"@ckeditor/ckeditor5-basic-styles@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-basic-styles/-/ckeditor5-basic-styles-46.0.3.tgz#563cb4ef19ecfd763745cb0bd79940fd03b7a81c" + integrity sha512-THmEPEbYopSfq8NTAugPLk+QW8/vuRkJfg/NpESzeugqCkBG2to3thOHdetbpye4IJBokLFhLsGFfKVYfVF81A== dependencies: - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-icons" "46.0.2" - "@ckeditor/ckeditor5-typing" "46.0.2" - "@ckeditor/ckeditor5-ui" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" - ckeditor5 "46.0.2" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-icons" "46.0.3" + "@ckeditor/ckeditor5-typing" "46.0.3" + "@ckeditor/ckeditor5-ui" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" + ckeditor5 "46.0.3" -"@ckeditor/ckeditor5-block-quote@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-block-quote/-/ckeditor5-block-quote-46.0.2.tgz#c7498ac1b588160703a14ccdfc2fe46aba187060" - integrity sha512-QWfqWPFQ4xFSzVgX8L3XqYYnUZE8/p3K23a2S35jwUJRrJl7PzyDNtzqbqohVWn5mGRXlO66qHdbyayrHTx0Lw== +"@ckeditor/ckeditor5-block-quote@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-block-quote/-/ckeditor5-block-quote-46.0.3.tgz#79a783d36ad4f9163cc31fb608ac6213c040a145" + integrity sha512-8bI7GoxOPrIExt/32gxLDQJB5VdSp3Oi6fqA+GH0Lqj+ri8HKfl3S147GymTUfBh01IOymQNL7xX04Dq1Nbl6A== dependencies: - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-enter" "46.0.2" - "@ckeditor/ckeditor5-icons" "46.0.2" - "@ckeditor/ckeditor5-typing" "46.0.2" - "@ckeditor/ckeditor5-ui" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" - ckeditor5 "46.0.2" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-enter" "46.0.3" + "@ckeditor/ckeditor5-icons" "46.0.3" + "@ckeditor/ckeditor5-typing" "46.0.3" + "@ckeditor/ckeditor5-ui" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" + ckeditor5 "46.0.3" -"@ckeditor/ckeditor5-bookmark@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-bookmark/-/ckeditor5-bookmark-46.0.2.tgz#21fc328e4da97b8a72cd9e9bf13b1cc78e381273" - integrity sha512-qtWBf55fyogvgwR/ftHPT6paMtqWKs1nKMxFkJI2ZAYkd7R1E8YYDmZGNjzbYTCRf8NLxJn6bBc9FCwZUfSxeA== +"@ckeditor/ckeditor5-bookmark@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-bookmark/-/ckeditor5-bookmark-46.0.3.tgz#f597408d87746105ba5d7a80ce8a7f4fa32a7cb6" + integrity sha512-f1usHplw2Ndhm1AiyjWfOWoaSQehMqBaXTa94OXlvO6ci1RIijdFm+DKn4Lgh/vSjv4vo25eQReTmEM0KaysvA== dependencies: - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-icons" "46.0.2" - "@ckeditor/ckeditor5-link" "46.0.2" - "@ckeditor/ckeditor5-ui" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" - "@ckeditor/ckeditor5-widget" "46.0.2" - ckeditor5 "46.0.2" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-icons" "46.0.3" + "@ckeditor/ckeditor5-link" "46.0.3" + "@ckeditor/ckeditor5-ui" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" + "@ckeditor/ckeditor5-widget" "46.0.3" + ckeditor5 "46.0.3" -"@ckeditor/ckeditor5-ckbox@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-ckbox/-/ckeditor5-ckbox-46.0.2.tgz#9edef4293edc19dc7317ce9d378fa7fab633daf4" - integrity sha512-Q2oqIktjDFi8X2fCE9oELZH02USd4QDcPUShUPRnr/FWcUllx3nXDhz/O+i4bvSh6ckSQKyneRlDtIx11bDbuQ== +"@ckeditor/ckeditor5-ckbox@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-ckbox/-/ckeditor5-ckbox-46.0.3.tgz#e0999969662c56bc768ac0ee7a4b09a3f6fefb82" + integrity sha512-UnmCqOU/iyYDef/OVsWbixeXwo+0pb3YGNWgmd2YsCFUUerbpOkDwwGuvCZPE7Hs34lNz8ybbhjR9KmGu8WcAw== dependencies: - "@ckeditor/ckeditor5-cloud-services" "46.0.2" - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-engine" "46.0.2" - "@ckeditor/ckeditor5-icons" "46.0.2" - "@ckeditor/ckeditor5-image" "46.0.2" - "@ckeditor/ckeditor5-ui" "46.0.2" - "@ckeditor/ckeditor5-upload" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" + "@ckeditor/ckeditor5-cloud-services" "46.0.3" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-engine" "46.0.3" + "@ckeditor/ckeditor5-icons" "46.0.3" + "@ckeditor/ckeditor5-image" "46.0.3" + "@ckeditor/ckeditor5-ui" "46.0.3" + "@ckeditor/ckeditor5-upload" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" blurhash "2.0.5" - ckeditor5 "46.0.2" + ckeditor5 "46.0.3" es-toolkit "1.39.5" -"@ckeditor/ckeditor5-ckfinder@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-ckfinder/-/ckeditor5-ckfinder-46.0.2.tgz#658d361a64460927681e18360f8b10bd6b3df7ae" - integrity sha512-TC2ZIm1klZ6ZGP1aSbgqiQ6E4fx74pCGqtX5zj+Uk3E3yD48Yr7Wg4dO3eeKcVanIM2MRzg2kr2pGJVlTPcjUw== +"@ckeditor/ckeditor5-ckfinder@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-ckfinder/-/ckeditor5-ckfinder-46.0.3.tgz#f7a0c234be03f71229461668dc8a659f608ecdca" + integrity sha512-VXggqo2w0TgFPyu6z+uH3aTWQMhbq2F2iPUi8SreYCL0JclczbU4HDKqzQU+RKhrzp+yhK1n7ztX5aN1H9EVAw== dependencies: - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-icons" "46.0.2" - "@ckeditor/ckeditor5-image" "46.0.2" - "@ckeditor/ckeditor5-ui" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" - ckeditor5 "46.0.2" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-icons" "46.0.3" + "@ckeditor/ckeditor5-image" "46.0.3" + "@ckeditor/ckeditor5-ui" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" + ckeditor5 "46.0.3" -"@ckeditor/ckeditor5-clipboard@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-clipboard/-/ckeditor5-clipboard-46.0.2.tgz#04b87859a599afe2204dd208d4e43d0cc2205e7a" - integrity sha512-FL1Dy3CWRmdMrk31oCpYi9FZew3okXlfgkfLyjbXIgAdUiJ+b/9Tu2ZzR6fNjpAN6BYTiOjx5cDq8h8yMLUgwg== +"@ckeditor/ckeditor5-clipboard@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-clipboard/-/ckeditor5-clipboard-46.0.3.tgz#5a42799228875a8112c98fc61ad1ca050f42fca0" + integrity sha512-ECz2goSbYZSlhRT2HszIPCMWFfThA0uIuXpI5PjYj7rDJUoip/Y3/UZjyMo47IUFf66Y4VdvJoq0fv/Z86HYIg== dependencies: - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-engine" "46.0.2" - "@ckeditor/ckeditor5-ui" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" - "@ckeditor/ckeditor5-widget" "46.0.2" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-engine" "46.0.3" + "@ckeditor/ckeditor5-ui" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" + "@ckeditor/ckeditor5-widget" "46.0.3" es-toolkit "1.39.5" -"@ckeditor/ckeditor5-cloud-services@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-cloud-services/-/ckeditor5-cloud-services-46.0.2.tgz#edfca0c1c1661d3c0e6421a4aafcbbcb86a6c3f8" - integrity sha512-auY6i4FCrdUiRCOGPUnIEcISKQad7rUm2fkjWHtS89v9sWabDq6BWLyuAFH8HNGjb81csrwb6b2bzMAL7M1rng== +"@ckeditor/ckeditor5-cloud-services@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-cloud-services/-/ckeditor5-cloud-services-46.0.3.tgz#7c02822ed77a1b4d3e80c0f70b4b250c5e946945" + integrity sha512-eKmtcygKoAoba6LGKdsFQyU50yZeeFgD9k05HYnN4BZCqZjrmlTbo3mQrTREgM/w2yxQ4AkDVj162S9NOyibWA== dependencies: - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" - ckeditor5 "46.0.2" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" + ckeditor5 "46.0.3" -"@ckeditor/ckeditor5-code-block@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-code-block/-/ckeditor5-code-block-46.0.2.tgz#c5018d9041228197d3796b558bf3e61827f506fe" - integrity sha512-ADNMDWSmlvrle0j9vNR5WMNyWjVn8t1TVILmLOab2T0/LTZcTzFXdz5i6I/oKhoxKty7soB8lmCUfJqrXNIhTw== +"@ckeditor/ckeditor5-code-block@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-code-block/-/ckeditor5-code-block-46.0.3.tgz#a8595063ce34da2a2095e89cf79be8b0532de056" + integrity sha512-5Bny1t2jb+Fruy4Tf0Es6YGPe24eWUiCskTv7QZkebEUtectUhZXjrbAPXkn9GQH9E+jU/ywhYkkCKwDgg+Vnw== dependencies: - "@ckeditor/ckeditor5-clipboard" "46.0.2" - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-engine" "46.0.2" - "@ckeditor/ckeditor5-enter" "46.0.2" - "@ckeditor/ckeditor5-icons" "46.0.2" - "@ckeditor/ckeditor5-ui" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" - ckeditor5 "46.0.2" + "@ckeditor/ckeditor5-clipboard" "46.0.3" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-engine" "46.0.3" + "@ckeditor/ckeditor5-enter" "46.0.3" + "@ckeditor/ckeditor5-icons" "46.0.3" + "@ckeditor/ckeditor5-ui" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" + ckeditor5 "46.0.3" -"@ckeditor/ckeditor5-core@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-core/-/ckeditor5-core-46.0.2.tgz#73b20ff36d4900605f4855fcd4cd0a5769027894" - integrity sha512-nXFO2hlmz6gkGzt2/C1yqxwxNqmHxvHy3npIiIuVHWE+e+Zx1BzJjjNEUoZ/K9+6IW0uybhidzGdpdwS6apfpg== +"@ckeditor/ckeditor5-core@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-core/-/ckeditor5-core-46.0.3.tgz#e9d294b517f646d6efdccecc8b3dc030feac7641" + integrity sha512-J03+XnTDL+Ex43ttT4fBxfJGRQxDor0zJc3TxlX44g0q7xD1l7T2CIkorry+817e3By3Qe3DfiMSleHKuDnmvQ== dependencies: - "@ckeditor/ckeditor5-engine" "46.0.2" - "@ckeditor/ckeditor5-ui" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" - "@ckeditor/ckeditor5-watchdog" "46.0.2" + "@ckeditor/ckeditor5-engine" "46.0.3" + "@ckeditor/ckeditor5-ui" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" + "@ckeditor/ckeditor5-watchdog" "46.0.3" es-toolkit "1.39.5" "@ckeditor/ckeditor5-dev-translations@^43.0.1", "@ckeditor/ckeditor5-dev-translations@^43.1.0": @@ -1041,316 +1033,316 @@ terser-webpack-plugin "^4.2.3" through2 "^3.0.1" -"@ckeditor/ckeditor5-easy-image@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-easy-image/-/ckeditor5-easy-image-46.0.2.tgz#2900e18d8a193fda3a6f7698e3db5d9438c1fc46" - integrity sha512-TjSbCEd8x31k4IlZZmEXA76LW9l1IGzq/bIBX4lLjSF+X30XYVqn9jYzJnPzZ73dNZ1mbzL4gzWO20TaCNyTuA== +"@ckeditor/ckeditor5-easy-image@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-easy-image/-/ckeditor5-easy-image-46.0.3.tgz#fbf72ea4524ded6b5aceacc41fa6f5e08672f7f3" + integrity sha512-UZs1G2wZaUr4lJSUsECBpM5ntr0UIXhGYG6lhE4Lf1TBaOypzxusR0H3txNtWIX1rq6hCeFH1P7meijfvJRgbw== dependencies: - "@ckeditor/ckeditor5-cloud-services" "46.0.2" - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-upload" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" - ckeditor5 "46.0.2" + "@ckeditor/ckeditor5-cloud-services" "46.0.3" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-upload" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" + ckeditor5 "46.0.3" -"@ckeditor/ckeditor5-editor-balloon@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-editor-balloon/-/ckeditor5-editor-balloon-46.0.2.tgz#dc0b0785aeb6e9266a205d255668aa0269a28207" - integrity sha512-ZZMFkZ1xP+O3JDFP03fsWZXrPbbzzV0ut2cyHvmTbvxsL8nWkByArbAyc4qs7ceF6wQ68PqLk1o+sPkEWHdVnw== +"@ckeditor/ckeditor5-editor-balloon@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-editor-balloon/-/ckeditor5-editor-balloon-46.0.3.tgz#35382c0393babc1a5f3ec8acd9a0f68ebb56a291" + integrity sha512-NXqmQK45DybJmgWFUln2uTvWqg77BuTp/R/4F33K6fgA4QGmnlWZ+l96Z5Rpmq6Rxc7suBNIKKWRFihquHw1hw== dependencies: - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-engine" "46.0.2" - "@ckeditor/ckeditor5-ui" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" - ckeditor5 "46.0.2" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-engine" "46.0.3" + "@ckeditor/ckeditor5-ui" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" + ckeditor5 "46.0.3" es-toolkit "1.39.5" -"@ckeditor/ckeditor5-editor-classic@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-editor-classic/-/ckeditor5-editor-classic-46.0.2.tgz#5bdb980fd5b1cf995c467279e1779307e5c1f52a" - integrity sha512-LTgCEyKapUURBZHZ2y5Z5nmPrl1zl8+kTiTgtpUOgZMQURq/G5BLxx5fdSyF2P0pZAoDYbrDR4uc2ngMH+6lgg== +"@ckeditor/ckeditor5-editor-classic@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-editor-classic/-/ckeditor5-editor-classic-46.0.3.tgz#f872b541014dc24b3a3ff62331a785348ea3ae40" + integrity sha512-fw4pdBqT1UpVYkBBpACQn9w5iR2Y62AvGW7ANt6b1nv55+FIN0uEAHsuChvZdFra8iJQR1qyilT24LVOTtk5mg== dependencies: - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-engine" "46.0.2" - "@ckeditor/ckeditor5-ui" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" - ckeditor5 "46.0.2" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-engine" "46.0.3" + "@ckeditor/ckeditor5-ui" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" + ckeditor5 "46.0.3" es-toolkit "1.39.5" -"@ckeditor/ckeditor5-editor-decoupled@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-editor-decoupled/-/ckeditor5-editor-decoupled-46.0.2.tgz#2d3a3a0b0a831ac03a7a1969a9cbdc2a80597439" - integrity sha512-eunAH7bAC7Y0FkxK9ukecG2a7Jxm0NAXlaDIWBRBYmNOycUDnMjeD54Ax4udJ7SxJXiTFYYF6fUIZ/mQy/DHbQ== +"@ckeditor/ckeditor5-editor-decoupled@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-editor-decoupled/-/ckeditor5-editor-decoupled-46.0.3.tgz#dae17ccb2d3fc3461fbe174b45590f9cde8748be" + integrity sha512-svrTpgGCi9YLhzit97i+A+lVStnQ4fNbGj6O1HlRG676BA20zqUkUWbNDPlBQT5sbq4N2oLKPwBmAqtUsF9ivQ== dependencies: - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-engine" "46.0.2" - "@ckeditor/ckeditor5-ui" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" - ckeditor5 "46.0.2" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-engine" "46.0.3" + "@ckeditor/ckeditor5-ui" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" + ckeditor5 "46.0.3" es-toolkit "1.39.5" -"@ckeditor/ckeditor5-editor-inline@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-editor-inline/-/ckeditor5-editor-inline-46.0.2.tgz#7fb1c9b1b5aad15612c56b179b91ad4564600e89" - integrity sha512-XYERPRnt/KNSje/AXpT0aCr6BLpSDAXaGil7edmuPL09oC+gGfjEzvCJDyDHbPCEwOTu684AHVvjiJNKJiJOTQ== +"@ckeditor/ckeditor5-editor-inline@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-editor-inline/-/ckeditor5-editor-inline-46.0.3.tgz#31342902ec3ad3185cfaf8097d55f1086f8f63a6" + integrity sha512-VfsD95gALQrUMHRJ5f2KKIPgtRb5flAqug85GSWy+wJZXOv7dC953tc1v8PYtUOHV6R3k2SWOUAGUClRu2ijOQ== dependencies: - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-engine" "46.0.2" - "@ckeditor/ckeditor5-ui" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" - ckeditor5 "46.0.2" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-engine" "46.0.3" + "@ckeditor/ckeditor5-ui" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" + ckeditor5 "46.0.3" es-toolkit "1.39.5" -"@ckeditor/ckeditor5-editor-multi-root@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-editor-multi-root/-/ckeditor5-editor-multi-root-46.0.2.tgz#03f8d2bf50037c66cddb0ac52b18f4fe3be59c38" - integrity sha512-QUHS10vQ+9XqRfe/djzD6P4Q8rFav3ewXldW2D5trMpQ+d9HzpyyGnYOOHzM5P8VSpgXm1ma8lTuXtqeLnIhnw== +"@ckeditor/ckeditor5-editor-multi-root@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-editor-multi-root/-/ckeditor5-editor-multi-root-46.0.3.tgz#b9d9b4f62d5396e3597c24f6183ab92ea0512d52" + integrity sha512-mS9gd8zTCclstU5DROT5L3sVq6HSDk0jw/7d7bgKEvWbGvQ6iPiqcgZ+bzpyrtvXMQKnmgfytZpU9qfODLpwFA== dependencies: - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-engine" "46.0.2" - "@ckeditor/ckeditor5-ui" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" - ckeditor5 "46.0.2" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-engine" "46.0.3" + "@ckeditor/ckeditor5-ui" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" + ckeditor5 "46.0.3" es-toolkit "1.39.5" -"@ckeditor/ckeditor5-emoji@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-emoji/-/ckeditor5-emoji-46.0.2.tgz#e71825e85411b1de6d88503e12b6084d41adbdaa" - integrity sha512-ZxjWu2JxnvX8ZyMQpmJ5VpaoXXtWWJxiO6MNeWjL/tcZ2DhD6/lQye7CLuAOvW4P5WBwrGKDdnk+vx7GLO6NIA== +"@ckeditor/ckeditor5-emoji@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-emoji/-/ckeditor5-emoji-46.0.3.tgz#e129445b3a078b19268482b55dd769449922d636" + integrity sha512-XiQsDeIZdSRDuFz/eoH16L21+Ucxykt+qHvqHSXB6bnVE8A3+65fxXYXicXnlb8st6UYhVBGwd53cpRz1ljMww== dependencies: - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-icons" "46.0.2" - "@ckeditor/ckeditor5-mention" "46.0.2" - "@ckeditor/ckeditor5-typing" "46.0.2" - "@ckeditor/ckeditor5-ui" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" - ckeditor5 "46.0.2" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-icons" "46.0.3" + "@ckeditor/ckeditor5-mention" "46.0.3" + "@ckeditor/ckeditor5-typing" "46.0.3" + "@ckeditor/ckeditor5-ui" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" + ckeditor5 "46.0.3" es-toolkit "1.39.5" fuzzysort "3.1.0" -"@ckeditor/ckeditor5-engine@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-engine/-/ckeditor5-engine-46.0.2.tgz#4f215a5f729f6c43b7dca0c8034ae7e9e30036a3" - integrity sha512-KrOmMtfLON/5EFS7x8GgCTRfVE4rFniPCRfBPzNL6rA/eWOclLYvwUGHpI6+JAymZ5XzyPLb8ftn6KjG8vvC+w== +"@ckeditor/ckeditor5-engine@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-engine/-/ckeditor5-engine-46.0.3.tgz#a4d740ad4cd87aa5c2dedbf45bc60f8cad8f4823" + integrity sha512-U5BMV3pZTViU2ArsmmvfzqG1dt03laxgWtX8y2TtoEhaL+cNnT4N2cxj0StioeTbGAP3imkNKvVfRpRBhJIp/Q== dependencies: - "@ckeditor/ckeditor5-utils" "46.0.2" + "@ckeditor/ckeditor5-utils" "46.0.3" es-toolkit "1.39.5" -"@ckeditor/ckeditor5-enter@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-enter/-/ckeditor5-enter-46.0.2.tgz#db3383f5310b8f2a22689abb96e882f2bd8b24a6" - integrity sha512-AZ+WhDEWDH4Ss6i7zd/YcuszlF5QKfkbGPQVsymsUziDvD/IuIQ1WtTDvLfdXbxGKI7amp9e1HCoilOJfv5uDw== +"@ckeditor/ckeditor5-enter@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-enter/-/ckeditor5-enter-46.0.3.tgz#d511f822b98644c8c3d614930184c7df845083c3" + integrity sha512-Z/IVe2Bn/PXamXxTlG9Pf/4K1OoGsNpwBfdywiqSYxdlF5E/4e5xArCKuFVkLGPO2YPSXShPhucBorqHlGQI2Q== dependencies: - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-engine" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-engine" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" -"@ckeditor/ckeditor5-essentials@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-essentials/-/ckeditor5-essentials-46.0.2.tgz#f21b2b2033e71ddd28519c69d89b983bb5c02701" - integrity sha512-ckcjNJiT1KDfllMr6eiBO9t1GlQUELXotjvUW1H93+g87qvl2yFJa/WB7PCpFOc5Derq45/OQWGL5hjySAqGUA== +"@ckeditor/ckeditor5-essentials@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-essentials/-/ckeditor5-essentials-46.0.3.tgz#56a0b982fe52c8ba605773cfb2c3f0f901849bb3" + integrity sha512-lUk+AkDVXb0YXEbyw+14sA5vFtXoWA4i6026tyN8I9uShMIyyjzkVUtTX9a0AWp5j//sJ5Ke+wMS0QUFRDtj+Q== dependencies: - "@ckeditor/ckeditor5-clipboard" "46.0.2" - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-enter" "46.0.2" - "@ckeditor/ckeditor5-select-all" "46.0.2" - "@ckeditor/ckeditor5-typing" "46.0.2" - "@ckeditor/ckeditor5-ui" "46.0.2" - "@ckeditor/ckeditor5-undo" "46.0.2" - ckeditor5 "46.0.2" + "@ckeditor/ckeditor5-clipboard" "46.0.3" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-enter" "46.0.3" + "@ckeditor/ckeditor5-select-all" "46.0.3" + "@ckeditor/ckeditor5-typing" "46.0.3" + "@ckeditor/ckeditor5-ui" "46.0.3" + "@ckeditor/ckeditor5-undo" "46.0.3" + ckeditor5 "46.0.3" -"@ckeditor/ckeditor5-find-and-replace@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-find-and-replace/-/ckeditor5-find-and-replace-46.0.2.tgz#dbd32fd4f65e085f000631569911f83eb2d9502a" - integrity sha512-k/gAR69CxdjeBf7mrGKWswdsVrdXoHRjCR7RbnTJH+tgzPpbn1sZydD2UacqqC5hON088whTokDY3KFd6zdbXA== +"@ckeditor/ckeditor5-find-and-replace@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-find-and-replace/-/ckeditor5-find-and-replace-46.0.3.tgz#c2b4b617ea0c5009d5bbf5366865c52ed7721eab" + integrity sha512-WKJ32slfJKPE2xnOWtk8/kqaDlUE3AKXChmRw6fPXM9pRpBRItLrbMO4Lhic9F1V8UzzY88/6VMuTMUlVg7/pQ== dependencies: - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-icons" "46.0.2" - "@ckeditor/ckeditor5-ui" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" - ckeditor5 "46.0.2" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-icons" "46.0.3" + "@ckeditor/ckeditor5-ui" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" + ckeditor5 "46.0.3" es-toolkit "1.39.5" -"@ckeditor/ckeditor5-font@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-font/-/ckeditor5-font-46.0.2.tgz#874dd5102cc0c6e9152e9d27d4895806bfea644c" - integrity sha512-dKkjRE8+GU6+LtQP45nQSEJkvnW1xltdpHZQrZCKXlf/51b2gBg408JtSBhqc1NOT5t1ZxaJCKHnf91dd6g4Hg== +"@ckeditor/ckeditor5-font@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-font/-/ckeditor5-font-46.0.3.tgz#2d7e6d27f6cc0841029fca64224ebeebd46963f7" + integrity sha512-4A0F3ShSn5QE0aQVus45EiIpFntJdXQnlf/kCLbQstYBUof915vReCa/c0cRu8q+1GOB9DmTarSPfb2jxDKhaA== dependencies: - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-engine" "46.0.2" - "@ckeditor/ckeditor5-icons" "46.0.2" - "@ckeditor/ckeditor5-ui" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" - ckeditor5 "46.0.2" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-engine" "46.0.3" + "@ckeditor/ckeditor5-icons" "46.0.3" + "@ckeditor/ckeditor5-ui" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" + ckeditor5 "46.0.3" -"@ckeditor/ckeditor5-fullscreen@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-fullscreen/-/ckeditor5-fullscreen-46.0.2.tgz#86813dfebb92a2ed6fbc8e266adeb3d3aa247f22" - integrity sha512-G+w2c5PpKRa9e5mZKR333FKkS1BH5bwKnkc0Xw4p2fowdIaytyv73fmUk2oQMTWEEe8sMMNfXCe69sfRSm4FmA== +"@ckeditor/ckeditor5-fullscreen@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-fullscreen/-/ckeditor5-fullscreen-46.0.3.tgz#aaca7671cd65864924a23ac25a41990d1a0d5f31" + integrity sha512-+AjKdmknSeihgVytx2CZPvqJ8Iv0sQd8kP1AvTMsp7JWr9kP3eMZEWJ3IwUP7GaH9O+cSDqeW2pFY4rW1ajYlQ== dependencies: - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-editor-classic" "46.0.2" - "@ckeditor/ckeditor5-editor-decoupled" "46.0.2" - "@ckeditor/ckeditor5-icons" "46.0.2" - "@ckeditor/ckeditor5-ui" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" - ckeditor5 "46.0.2" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-editor-classic" "46.0.3" + "@ckeditor/ckeditor5-editor-decoupled" "46.0.3" + "@ckeditor/ckeditor5-icons" "46.0.3" + "@ckeditor/ckeditor5-ui" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" + ckeditor5 "46.0.3" -"@ckeditor/ckeditor5-heading@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-heading/-/ckeditor5-heading-46.0.2.tgz#fdaf00bfc56f792a66060c7df9c0455486e4a5dd" - integrity sha512-AdvE53zuBGyuiBitaLPztWL/OyT3hG9F2kcdf1yG+RYovLXS6lG2Ut1tEL3jzmTNOoObWLQQ9Jpthj7gawXlQw== +"@ckeditor/ckeditor5-heading@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-heading/-/ckeditor5-heading-46.0.3.tgz#5d90467e9e4f082d8c8ec1dc3b31474b74e0c320" + integrity sha512-FKTgc1I9nDvnoDJ6RzkmPX7knhU3k6iH8IGUngH78TIOmhcWPVzv7Sftszos/LdX+kTc1ZoWWaHo5vrk90waZg== dependencies: - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-engine" "46.0.2" - "@ckeditor/ckeditor5-icons" "46.0.2" - "@ckeditor/ckeditor5-paragraph" "46.0.2" - "@ckeditor/ckeditor5-ui" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" - ckeditor5 "46.0.2" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-engine" "46.0.3" + "@ckeditor/ckeditor5-icons" "46.0.3" + "@ckeditor/ckeditor5-paragraph" "46.0.3" + "@ckeditor/ckeditor5-ui" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" + ckeditor5 "46.0.3" -"@ckeditor/ckeditor5-highlight@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-highlight/-/ckeditor5-highlight-46.0.2.tgz#71a95009de63164babe51df704a3bbaa8cd15130" - integrity sha512-wOLa7exXWaIObdFmXIWchgfDEUyk4+j2/B25NLXyYFhk+EVDOIA0le48Tq+nAM7cusA6PP4skwkUZCBOP31UIA== +"@ckeditor/ckeditor5-highlight@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-highlight/-/ckeditor5-highlight-46.0.3.tgz#c75991f017a039a500bec66e17e8a07ed8a44533" + integrity sha512-woO40tvOomrE7PHV/LAIOuNDb6sm2xiRQpT3r6TU1bvHZWSdt+hBCVRbnPxMNY2b/+0FGeV6cIOP8jlZ6JXF2g== dependencies: - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-icons" "46.0.2" - "@ckeditor/ckeditor5-ui" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" - ckeditor5 "46.0.2" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-icons" "46.0.3" + "@ckeditor/ckeditor5-ui" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" + ckeditor5 "46.0.3" -"@ckeditor/ckeditor5-horizontal-line@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-horizontal-line/-/ckeditor5-horizontal-line-46.0.2.tgz#65a6ed55eeee429c4f80377c38bc02a15c2b2ac3" - integrity sha512-TWpcU7xDQnqyKvvv30cYHy+57FTLEuNgUbKRs+ziP1Ywogd6X3jFVnmJk/WMCNc315v1IfDFiuaPbZn04zrmjA== +"@ckeditor/ckeditor5-horizontal-line@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-horizontal-line/-/ckeditor5-horizontal-line-46.0.3.tgz#c57556048fbb22221a347993e2ead695f05f730a" + integrity sha512-mct0XA6XxSk9BXorR5HA6jiDmf40Wm2HbwSEL8RcCQ4s/ak+3c85loUQZtV5Enaro8ejUkQ30nbqUnrO21Z8ZA== dependencies: - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-icons" "46.0.2" - "@ckeditor/ckeditor5-ui" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" - "@ckeditor/ckeditor5-widget" "46.0.2" - ckeditor5 "46.0.2" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-icons" "46.0.3" + "@ckeditor/ckeditor5-ui" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" + "@ckeditor/ckeditor5-widget" "46.0.3" + ckeditor5 "46.0.3" -"@ckeditor/ckeditor5-html-embed@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-html-embed/-/ckeditor5-html-embed-46.0.2.tgz#cafad91a0f935ce83262c189f7b425decc6b8a3c" - integrity sha512-GJouBoKYKEP1NYrMSeu+vadP5vHsJgUBb/9yvx+kup/50u+HOylenBfVc+IdMMzZyU8ZoNw3wND5mgOpyQPLdQ== +"@ckeditor/ckeditor5-html-embed@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-html-embed/-/ckeditor5-html-embed-46.0.3.tgz#8153337107ea4ebd6cf98e8a67f57bcf5814272a" + integrity sha512-8Cf0L1REllrVffu4BrnNiga0mQgFcQ0V/L4ARMGR3vmafTvS2cOvMyrGJy/69oCGM0NigyU1eSzkGv04o+599w== dependencies: - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-icons" "46.0.2" - "@ckeditor/ckeditor5-ui" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" - "@ckeditor/ckeditor5-widget" "46.0.2" - ckeditor5 "46.0.2" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-icons" "46.0.3" + "@ckeditor/ckeditor5-ui" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" + "@ckeditor/ckeditor5-widget" "46.0.3" + ckeditor5 "46.0.3" -"@ckeditor/ckeditor5-html-support@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-html-support/-/ckeditor5-html-support-46.0.2.tgz#c07a5de1f2307e716606a6b2e89e72c986f880bb" - integrity sha512-DZAMx55Qxz7YQMy4qOCiNKf9oUp/FkAxqJRAG+102nweLQePq86w//oE6pc/mRo3q6U3/za8NLz6JP4L2duztw== +"@ckeditor/ckeditor5-html-support@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-html-support/-/ckeditor5-html-support-46.0.3.tgz#65164419632b679de09dd8040bf1d8ba837e7a51" + integrity sha512-zBRJ1aBIi/UKKRhCUvK0mTDu9c43GOINKscGJ4ZRAD8WmKdlpxO+xUfCfZouDMGwd67lD9e37LI3xZc+hGCXGA== dependencies: - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-engine" "46.0.2" - "@ckeditor/ckeditor5-enter" "46.0.2" - "@ckeditor/ckeditor5-heading" "46.0.2" - "@ckeditor/ckeditor5-image" "46.0.2" - "@ckeditor/ckeditor5-list" "46.0.2" - "@ckeditor/ckeditor5-remove-format" "46.0.2" - "@ckeditor/ckeditor5-table" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" - "@ckeditor/ckeditor5-widget" "46.0.2" - ckeditor5 "46.0.2" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-engine" "46.0.3" + "@ckeditor/ckeditor5-enter" "46.0.3" + "@ckeditor/ckeditor5-heading" "46.0.3" + "@ckeditor/ckeditor5-image" "46.0.3" + "@ckeditor/ckeditor5-list" "46.0.3" + "@ckeditor/ckeditor5-remove-format" "46.0.3" + "@ckeditor/ckeditor5-table" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" + "@ckeditor/ckeditor5-widget" "46.0.3" + ckeditor5 "46.0.3" es-toolkit "1.39.5" -"@ckeditor/ckeditor5-icons@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-icons/-/ckeditor5-icons-46.0.2.tgz#ef8994441f13c2d9bf33d8760e7093049d8ab0cc" - integrity sha512-QNLncoTeHgv4fU7Q/jv/qWH1nQMQ1JreWVQLysu1nEDlm4KiVLzP+8ng51BquY+wxw4rIVJTwZv1FYdyc6xlQw== +"@ckeditor/ckeditor5-icons@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-icons/-/ckeditor5-icons-46.0.3.tgz#fae5dec3826f5f4a6649fa01152d1aaa234a1d30" + integrity sha512-ztmFx8ujcdIMTWeIQ8Hxixlexfhx8vcclV/+maDzjVHhqRNi9eZ1b/nQ7gnS4/X5Fnh6cPQuCM+3lTUR4jQscA== -"@ckeditor/ckeditor5-image@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-image/-/ckeditor5-image-46.0.2.tgz#57d2d7e6118cf7eae5a535da0ffe9816d27fc2a3" - integrity sha512-1b72bijZ4lhysL6K9ZZBQZPldMUZwoAar4DFHmCnM/WN6psf/MEyFce+hr5Qq/LFOvCiOeevuNz6DTDKO7eXSg== +"@ckeditor/ckeditor5-image@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-image/-/ckeditor5-image-46.0.3.tgz#51814618fdb9ffe29217746cb28730dbf83911ab" + integrity sha512-9XcJVJxG+fqzwTupf7EATKeVZ+tXqeWiHLip4w/vMejjX026CPjiB3rKA2K5/H25TKDrvsMBBm22RqpK25dzCw== dependencies: - "@ckeditor/ckeditor5-clipboard" "46.0.2" - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-engine" "46.0.2" - "@ckeditor/ckeditor5-icons" "46.0.2" - "@ckeditor/ckeditor5-typing" "46.0.2" - "@ckeditor/ckeditor5-ui" "46.0.2" - "@ckeditor/ckeditor5-undo" "46.0.2" - "@ckeditor/ckeditor5-upload" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" - "@ckeditor/ckeditor5-widget" "46.0.2" - ckeditor5 "46.0.2" + "@ckeditor/ckeditor5-clipboard" "46.0.3" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-engine" "46.0.3" + "@ckeditor/ckeditor5-icons" "46.0.3" + "@ckeditor/ckeditor5-typing" "46.0.3" + "@ckeditor/ckeditor5-ui" "46.0.3" + "@ckeditor/ckeditor5-undo" "46.0.3" + "@ckeditor/ckeditor5-upload" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" + "@ckeditor/ckeditor5-widget" "46.0.3" + ckeditor5 "46.0.3" es-toolkit "1.39.5" -"@ckeditor/ckeditor5-indent@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-indent/-/ckeditor5-indent-46.0.2.tgz#b36f7ecaeec9be0ebdc2f2fef68fd4dc468a6034" - integrity sha512-EKA4kM3uZexI6j7GzQyDuYNwY0ULRet0+AZTYbr4rEaB+Mo2zaJCJxuJw1RPTNBwE/9fVJyqYsPzb0UmSRqsGQ== +"@ckeditor/ckeditor5-indent@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-indent/-/ckeditor5-indent-46.0.3.tgz#ee6a0279c9a09d2a8be0b43d3fb3aa48ec074417" + integrity sha512-XLdlp94Bitkki027adnOqL642kCSJphMoZZDYYpTNHQkKhJq6TDp8u66EFlo2/q1quVDgb1qlezDuShouYd1tQ== dependencies: - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-engine" "46.0.2" - "@ckeditor/ckeditor5-heading" "46.0.2" - "@ckeditor/ckeditor5-icons" "46.0.2" - "@ckeditor/ckeditor5-list" "46.0.2" - "@ckeditor/ckeditor5-ui" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" - ckeditor5 "46.0.2" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-engine" "46.0.3" + "@ckeditor/ckeditor5-heading" "46.0.3" + "@ckeditor/ckeditor5-icons" "46.0.3" + "@ckeditor/ckeditor5-list" "46.0.3" + "@ckeditor/ckeditor5-ui" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" + ckeditor5 "46.0.3" -"@ckeditor/ckeditor5-language@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-language/-/ckeditor5-language-46.0.2.tgz#518488ca4195a62809ac8945540ac46291892743" - integrity sha512-eYwRnEkoWGabEZ4PVtSobORa+vnUQFuRetInuhDrkBwyMv9IjVUukS46AWHEjkPBO/rlI++O9SK1oOFyzOARCg== +"@ckeditor/ckeditor5-language@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-language/-/ckeditor5-language-46.0.3.tgz#dad8aa2fa391c247001f2812a603234992c74dfa" + integrity sha512-JLkDnhZxP9J/Dw7uxJtBHYrdR1q2xpkIsi+Y0fhG0cejo6Lhfnv2F/1L76EO6JxhfhrkHWrDgLwr860PYvRztA== dependencies: - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-ui" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" - ckeditor5 "46.0.2" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-ui" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" + ckeditor5 "46.0.3" -"@ckeditor/ckeditor5-link@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-link/-/ckeditor5-link-46.0.2.tgz#8b4a6f5fe3cd3028534116b1fb8fb2f00ea42bd5" - integrity sha512-5uliK3QCIOcEsq2bgZF5Qz88cmN0E1YXUrYc5uoqC8LF0lzOimE+EA+7/dJhBZCya8/+Y/rvvpJ8SHsjhd++kg== +"@ckeditor/ckeditor5-link@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-link/-/ckeditor5-link-46.0.3.tgz#383c13c5bfa08c36f7305abc17e6129174806cc2" + integrity sha512-s2wBD0QQ2Pz8wzTbh3YN83QbYRVbGp3qLwgN+8x7Y/bOuFE4AxR+JhDo14ekdXelXYxIeGJAqG2Z4SQj8v2rXQ== dependencies: - "@ckeditor/ckeditor5-clipboard" "46.0.2" - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-engine" "46.0.2" - "@ckeditor/ckeditor5-icons" "46.0.2" - "@ckeditor/ckeditor5-image" "46.0.2" - "@ckeditor/ckeditor5-typing" "46.0.2" - "@ckeditor/ckeditor5-ui" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" - "@ckeditor/ckeditor5-widget" "46.0.2" - ckeditor5 "46.0.2" + "@ckeditor/ckeditor5-clipboard" "46.0.3" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-engine" "46.0.3" + "@ckeditor/ckeditor5-icons" "46.0.3" + "@ckeditor/ckeditor5-image" "46.0.3" + "@ckeditor/ckeditor5-typing" "46.0.3" + "@ckeditor/ckeditor5-ui" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" + "@ckeditor/ckeditor5-widget" "46.0.3" + ckeditor5 "46.0.3" es-toolkit "1.39.5" -"@ckeditor/ckeditor5-list@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-list/-/ckeditor5-list-46.0.2.tgz#5eae9c0376e50eeb8b6bdb9c16614d71922a13c9" - integrity sha512-0Pq5UU4SP9UOlcRhxpjCoGXfDxHeqdumn8qtNbL5X5yRGqRE4GsVgJ4CkOmtZNTy1JVv1clZ37NPKh5miqTP4A== +"@ckeditor/ckeditor5-list@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-list/-/ckeditor5-list-46.0.3.tgz#342a50f272b7079a3c0bc863d70855721d4c44bc" + integrity sha512-KEAnyhUO6hWWa3GO6NGS7Entn2OXutCQ2+od8l5MrqeGxmpnqj0OpPX6qn+RZTVWf1RnqwErCYQhhPoQM/mlZg== dependencies: - "@ckeditor/ckeditor5-clipboard" "46.0.2" - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-engine" "46.0.2" - "@ckeditor/ckeditor5-enter" "46.0.2" - "@ckeditor/ckeditor5-font" "46.0.2" - "@ckeditor/ckeditor5-icons" "46.0.2" - "@ckeditor/ckeditor5-typing" "46.0.2" - "@ckeditor/ckeditor5-ui" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" - ckeditor5 "46.0.2" + "@ckeditor/ckeditor5-clipboard" "46.0.3" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-engine" "46.0.3" + "@ckeditor/ckeditor5-enter" "46.0.3" + "@ckeditor/ckeditor5-font" "46.0.3" + "@ckeditor/ckeditor5-icons" "46.0.3" + "@ckeditor/ckeditor5-typing" "46.0.3" + "@ckeditor/ckeditor5-ui" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" + ckeditor5 "46.0.3" -"@ckeditor/ckeditor5-markdown-gfm@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-markdown-gfm/-/ckeditor5-markdown-gfm-46.0.2.tgz#a6e3312bbd7066f1d96e8f8a8f6eb8946c0f1542" - integrity sha512-+PaA5D10LnxqrsdW+UI45vqjR7C0l6vWAHFR+M99v7bxHEW+hQiLS6af8FhL/yv9Sno9AL4Oqdsee1HUU7hjHA== +"@ckeditor/ckeditor5-markdown-gfm@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-markdown-gfm/-/ckeditor5-markdown-gfm-46.0.3.tgz#f3c17385d7e6489e525632bff3d59166c5e6cf94" + integrity sha512-ROOQsKcb03UdzyWZOD4p6vPWUpjgBRf4VXgbxKds2z19dm3fOdUwFbolpVrmYuYzdHrI/0xWM/+waD7TEOatuQ== dependencies: - "@ckeditor/ckeditor5-clipboard" "46.0.2" - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-engine" "46.0.2" + "@ckeditor/ckeditor5-clipboard" "46.0.3" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-engine" "46.0.3" "@types/hast" "3.0.4" - ckeditor5 "46.0.2" + ckeditor5 "46.0.3" hast-util-from-dom "5.0.1" hast-util-to-html "9.0.5" hast-util-to-mdast "10.1.2" @@ -1366,271 +1358,271 @@ unified "11.0.5" unist-util-visit "5.0.0" -"@ckeditor/ckeditor5-media-embed@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-media-embed/-/ckeditor5-media-embed-46.0.2.tgz#4b700713621a02ff6abaa9e84fa1b36438be57ec" - integrity sha512-HQqtmuZPGvMKvshVIkz9GQvnSxuvsuw1o99zHvkr73H2OpL2uRRgCwVLufKZpIsn6CMtNbWq9PlZxk6ZME6Nyg== +"@ckeditor/ckeditor5-media-embed@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-media-embed/-/ckeditor5-media-embed-46.0.3.tgz#5efb29e50888bae4b38a1fdb79572bad2bed930a" + integrity sha512-aozP4L8WQuPOHBA5qXTQnH3kQrhFJd6/J5KjKl5EicR6MUqeDkvzSLxYnltUBPByoDvkNxHD/GIL8nevgeWCrQ== dependencies: - "@ckeditor/ckeditor5-clipboard" "46.0.2" - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-engine" "46.0.2" - "@ckeditor/ckeditor5-icons" "46.0.2" - "@ckeditor/ckeditor5-typing" "46.0.2" - "@ckeditor/ckeditor5-ui" "46.0.2" - "@ckeditor/ckeditor5-undo" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" - "@ckeditor/ckeditor5-widget" "46.0.2" - ckeditor5 "46.0.2" + "@ckeditor/ckeditor5-clipboard" "46.0.3" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-engine" "46.0.3" + "@ckeditor/ckeditor5-icons" "46.0.3" + "@ckeditor/ckeditor5-typing" "46.0.3" + "@ckeditor/ckeditor5-ui" "46.0.3" + "@ckeditor/ckeditor5-undo" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" + "@ckeditor/ckeditor5-widget" "46.0.3" + ckeditor5 "46.0.3" -"@ckeditor/ckeditor5-mention@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-mention/-/ckeditor5-mention-46.0.2.tgz#1ced5689dd81b3b3d9d5f64da895cd540c636aa6" - integrity sha512-/2FT0TmXyxgO5CWg841Yy5PF0uGT4mmp8NQYPpamfgP6E236L/aOTJP4kHtZV5uOSEnt6P48N59MTXswXA3Glg== +"@ckeditor/ckeditor5-mention@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-mention/-/ckeditor5-mention-46.0.3.tgz#42f2e38b6404650f2d8a09392d8069832269ccf4" + integrity sha512-a7sHtN8M5Glh20SbsB0KWlFxoothUwkq6cqNJKKAI6MrOYsOJX1WaMG2mUfhGr4VTrUieuJYxVtqMFuagbhBgQ== dependencies: - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-typing" "46.0.2" - "@ckeditor/ckeditor5-ui" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" - ckeditor5 "46.0.2" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-typing" "46.0.3" + "@ckeditor/ckeditor5-ui" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" + ckeditor5 "46.0.3" es-toolkit "1.39.5" -"@ckeditor/ckeditor5-minimap@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-minimap/-/ckeditor5-minimap-46.0.2.tgz#0eee763bcf39b475db97abf09fcf66378e2a0342" - integrity sha512-Hi0qLjWLgGSwT1u3BlDc5tXMA5eHsDm6L9Sv+LiyxPFPBgX/HQhWT6L6x4jIexHQLlDhBO5o/Hp3tnlW57K5Kg== +"@ckeditor/ckeditor5-minimap@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-minimap/-/ckeditor5-minimap-46.0.3.tgz#ba170968a44a87557319ea6efcf97eb3d8923e3a" + integrity sha512-gsac1z96MaJMFzapfzqLtEqETpI3JVXMfdQV3N0+kRbFSlUeJmrR/aHLC/+GDQAttkfOuL9i4FlWQKiDeSN15w== dependencies: - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-engine" "46.0.2" - "@ckeditor/ckeditor5-ui" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" - ckeditor5 "46.0.2" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-engine" "46.0.3" + "@ckeditor/ckeditor5-ui" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" + ckeditor5 "46.0.3" -"@ckeditor/ckeditor5-page-break@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-page-break/-/ckeditor5-page-break-46.0.2.tgz#8a7303490fe884a69a3026852671b37a046d5f84" - integrity sha512-8wSzQU0lwoqzMPFyZHYVJJRTc1GA5gwgtz7XVKKHtKRF9FsKmHYASHsEsjjX3TkU0dPTGnaqsttZ7mBGU9K9Ww== +"@ckeditor/ckeditor5-page-break@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-page-break/-/ckeditor5-page-break-46.0.3.tgz#c0fece6af88c11cddfc600e849ec04b11390c872" + integrity sha512-6V0O0sqgZMh47knEhhj0htWK3Oxm6jfHLWA4vi9vColwJMv9imuP72vYgrClmKHfN/QtyZ+DGmaufmhaXS2ffw== dependencies: - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-icons" "46.0.2" - "@ckeditor/ckeditor5-ui" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" - "@ckeditor/ckeditor5-widget" "46.0.2" - ckeditor5 "46.0.2" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-icons" "46.0.3" + "@ckeditor/ckeditor5-ui" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" + "@ckeditor/ckeditor5-widget" "46.0.3" + ckeditor5 "46.0.3" -"@ckeditor/ckeditor5-paragraph@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-paragraph/-/ckeditor5-paragraph-46.0.2.tgz#760280a3596a08466186021a60632c931821c2ba" - integrity sha512-Mg4BxYvIzonlLe9zzFZTyiiMbW40NLue9G26lWaCUz+O2z8ms5CShNc065t4alJiihJis5Dtuho8tvPDiRgCNg== +"@ckeditor/ckeditor5-paragraph@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-paragraph/-/ckeditor5-paragraph-46.0.3.tgz#c6ee4808048c0c2a23450ab7438bc9dc5d140f4a" + integrity sha512-3OlCeyykkhcueXmo+p/LppeCvC2TtEpljLpC042EbIOCJEbSMlYEGx/AJQGetn2JV8q9L3UKfgnltpOriXAeyg== dependencies: - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-engine" "46.0.2" - "@ckeditor/ckeditor5-icons" "46.0.2" - "@ckeditor/ckeditor5-ui" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-engine" "46.0.3" + "@ckeditor/ckeditor5-icons" "46.0.3" + "@ckeditor/ckeditor5-ui" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" -"@ckeditor/ckeditor5-paste-from-office@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-paste-from-office/-/ckeditor5-paste-from-office-46.0.2.tgz#26534e6b080f00c1305e293411ea674e60dfd07d" - integrity sha512-eI08nXazXzdIBxKjiU7tANFAdqz1cb5+xRdzn6dmZj0QBLHdEMWZVLLng5XC2gPqB7V3gSA0XbuYeSLF6fTfQg== +"@ckeditor/ckeditor5-paste-from-office@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-paste-from-office/-/ckeditor5-paste-from-office-46.0.3.tgz#79de54d4cdec9531f254256d8e4d251aa02f6d38" + integrity sha512-pgqBTqP3oIFbmHvk1ddICDmyvBvFE9d+jO0busPXl5oWIqTLaaumwWaredEEUJpYmu02POSrK+WPGS0Qis6mdg== dependencies: - "@ckeditor/ckeditor5-clipboard" "46.0.2" - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-engine" "46.0.2" - ckeditor5 "46.0.2" + "@ckeditor/ckeditor5-clipboard" "46.0.3" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-engine" "46.0.3" + ckeditor5 "46.0.3" -"@ckeditor/ckeditor5-remove-format@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-remove-format/-/ckeditor5-remove-format-46.0.2.tgz#804850404632c21ef63b3c75007309e5959e8c5c" - integrity sha512-/Ez72jjpnvDqFtP4afNimyrqbt3xJn/ab7p4DoByqyuBJ/Wy7mkaRcw9dDO0oJB+GVWdcGeRWeYoFUYj3Yw0NQ== +"@ckeditor/ckeditor5-remove-format@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-remove-format/-/ckeditor5-remove-format-46.0.3.tgz#9f73003093a2958f32baffda6024f07b557f28ef" + integrity sha512-rrGeK1NGE5o04/wuyMq10BD7bJ7qkVZq74dDXb7G6l1IkFWU/lY5SLt1K4FgVunY+oBcsena+hktwqgEsmEqdg== dependencies: - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-icons" "46.0.2" - "@ckeditor/ckeditor5-ui" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" - ckeditor5 "46.0.2" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-icons" "46.0.3" + "@ckeditor/ckeditor5-ui" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" + ckeditor5 "46.0.3" -"@ckeditor/ckeditor5-restricted-editing@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-restricted-editing/-/ckeditor5-restricted-editing-46.0.2.tgz#0e23d6aa6978ff9c554ab109975608b26518b0db" - integrity sha512-WR8HciP0DcD1TB+i8zRVwroPMiCy9Z7m0kfirCSLmwWP8bn792XwU+kId9DrOWalNzfNh4BXoviaPpi0vtRcmA== +"@ckeditor/ckeditor5-restricted-editing@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-restricted-editing/-/ckeditor5-restricted-editing-46.0.3.tgz#52b32ac9c9ecfcfa12266e313b62936dcb75a1bc" + integrity sha512-b1NUb7nEKdb0R5UOukXRXOeweOIE3Dsa64uwV/H6ZnRfdOmH37TVSKFJ2lWVvPUUljsT3SVdSZbl1aP4aA1SBA== dependencies: - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-engine" "46.0.2" - "@ckeditor/ckeditor5-icons" "46.0.2" - "@ckeditor/ckeditor5-ui" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" - ckeditor5 "46.0.2" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-engine" "46.0.3" + "@ckeditor/ckeditor5-icons" "46.0.3" + "@ckeditor/ckeditor5-ui" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" + ckeditor5 "46.0.3" -"@ckeditor/ckeditor5-select-all@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-select-all/-/ckeditor5-select-all-46.0.2.tgz#d7c3ebcfe0a3e4f8e181f26a26cc21b48cfe1167" - integrity sha512-qC+HAZ0BWO4daXkZ84dAu7ynMRJfhtcnUP8pR/o2D6VxJO7Cu+5MwtwfoLmSiJAUGYwcxVd/iFq3RP7ZxS4Rew== +"@ckeditor/ckeditor5-select-all@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-select-all/-/ckeditor5-select-all-46.0.3.tgz#a785a8cf89ddefb07e9cc9adc02844667bc02bd6" + integrity sha512-Uxr3/+TRLUIOGubXo/86yzqLGgoEdPV2rGqz40ulrVhG1Q7hOYerJPDs67ULPq6DLukoFFARRTah+UN9EOYRRw== dependencies: - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-engine" "46.0.2" - "@ckeditor/ckeditor5-icons" "46.0.2" - "@ckeditor/ckeditor5-ui" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-engine" "46.0.3" + "@ckeditor/ckeditor5-icons" "46.0.3" + "@ckeditor/ckeditor5-ui" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" -"@ckeditor/ckeditor5-show-blocks@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-show-blocks/-/ckeditor5-show-blocks-46.0.2.tgz#d3e0ffd6a9184519b711d0b469a53f3f552b5fdb" - integrity sha512-J+C59BMbnAH4gPrkUlu/dccKR2NBUqrRIFa01hnDHk+ECYeJsBNlsENNPImxeay4hiF+p4cujhQnI8Xq1NkzQQ== +"@ckeditor/ckeditor5-show-blocks@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-show-blocks/-/ckeditor5-show-blocks-46.0.3.tgz#a912926c7102797426040a1bc36b73dcd380fbe5" + integrity sha512-YSa+Q49hQe4oRxIFsnUjzIFRG1M5+2vWjzYwS84hQAR0xDMZDD0SqIS6poC3QewuIS/525bcnmASBwXZUrRdIA== dependencies: - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-icons" "46.0.2" - "@ckeditor/ckeditor5-ui" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" - ckeditor5 "46.0.2" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-icons" "46.0.3" + "@ckeditor/ckeditor5-ui" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" + ckeditor5 "46.0.3" -"@ckeditor/ckeditor5-source-editing@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-source-editing/-/ckeditor5-source-editing-46.0.2.tgz#7164812ce5b24c92cde99e0ffb1bec171fd66c44" - integrity sha512-UdQELANPxAMhbbKTBCOfm/dMtqgQpMcU0D58LKjvvOT35ZGyjlrvZCKmXweFtfLPK5SmQhlS9z5/yy9JIH3pVQ== +"@ckeditor/ckeditor5-source-editing@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-source-editing/-/ckeditor5-source-editing-46.0.3.tgz#ea664512ecd36ec5a32f5ee7f7bbd48e69e279c1" + integrity sha512-zJMa7ekyaeQAqAysFZDRwPRyJ7+ejaP2twYvRJQARf/BgZ6YZdSDvSoW1gGIKN/c/f0XWOSTDBdRCciPZu9vCg== dependencies: - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-icons" "46.0.2" - "@ckeditor/ckeditor5-theme-lark" "46.0.2" - "@ckeditor/ckeditor5-ui" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" - ckeditor5 "46.0.2" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-icons" "46.0.3" + "@ckeditor/ckeditor5-theme-lark" "46.0.3" + "@ckeditor/ckeditor5-ui" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" + ckeditor5 "46.0.3" -"@ckeditor/ckeditor5-special-characters@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-special-characters/-/ckeditor5-special-characters-46.0.2.tgz#0b490d77bda1f8e1c8f3da87699c154cd4de1c40" - integrity sha512-X3XuIAchgFxmKcWcc513vzzsMcN6eOPOzQlQtVr9NKgUd/Zvw7YTyxCP1Wj2w9usgLn57p2ame/7GlBt/P1quw== +"@ckeditor/ckeditor5-special-characters@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-special-characters/-/ckeditor5-special-characters-46.0.3.tgz#94b62a510608b47247c243fe12762fdfcdb1d4b4" + integrity sha512-PihS9/nmrGXaycsI3TSqVK0qGlc2ZSE3XzL7dEKTCyUta7vvI7hCC/jDaTtfch2d0fZhnIXovlgqlj35u2PjDw== dependencies: - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-icons" "46.0.2" - "@ckeditor/ckeditor5-typing" "46.0.2" - "@ckeditor/ckeditor5-ui" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" - ckeditor5 "46.0.2" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-icons" "46.0.3" + "@ckeditor/ckeditor5-typing" "46.0.3" + "@ckeditor/ckeditor5-ui" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" + ckeditor5 "46.0.3" -"@ckeditor/ckeditor5-style@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-style/-/ckeditor5-style-46.0.2.tgz#23c964ede8de1715b942bc4399173100958f416c" - integrity sha512-LeP6kV0AeY1mrv6hbuQ2s10AEoJ64Vgv7XMAieg/fYE2/CIH0GAXE9/4Xt1+X8zCEddZ0HcbKCyCJG2l20xzyQ== +"@ckeditor/ckeditor5-style@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-style/-/ckeditor5-style-46.0.3.tgz#d1c75502c27cfd717a93f238b702432e48a5b02b" + integrity sha512-/4kOCM0/s4O65AA6tHdTK9joPFaTs/Uk14RHlyGP6+QJQ5FcNx9g2yJ1HxhRAdkMLy3AsVol9lqqFXC00+W7BA== dependencies: - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-html-support" "46.0.2" - "@ckeditor/ckeditor5-list" "46.0.2" - "@ckeditor/ckeditor5-table" "46.0.2" - "@ckeditor/ckeditor5-typing" "46.0.2" - "@ckeditor/ckeditor5-ui" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" - ckeditor5 "46.0.2" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-html-support" "46.0.3" + "@ckeditor/ckeditor5-list" "46.0.3" + "@ckeditor/ckeditor5-table" "46.0.3" + "@ckeditor/ckeditor5-typing" "46.0.3" + "@ckeditor/ckeditor5-ui" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" + ckeditor5 "46.0.3" es-toolkit "1.39.5" -"@ckeditor/ckeditor5-table@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-table/-/ckeditor5-table-46.0.2.tgz#a941a45394b3b8d5472861f98ce37dc2e67528b2" - integrity sha512-dGkTe1vEk7iDEmoRCTQszyerXvO5hrJH702kwHV5md2dlXyyJBteAJ9qHiSxf1euC2mOMMUhq7n5DlqpFAFb8A== +"@ckeditor/ckeditor5-table@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-table/-/ckeditor5-table-46.0.3.tgz#39bf048644c3fcc6a9747233b54803bcd86925fe" + integrity sha512-Bt7d02s96cv28Xc+LxNRYBNrqlG7gI5xB8gjQWCuoIYHVikxtDUSBowu7q1UOkBmX/TEHuUpnYjUdBKD5M2n5w== dependencies: - "@ckeditor/ckeditor5-clipboard" "46.0.2" - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-engine" "46.0.2" - "@ckeditor/ckeditor5-icons" "46.0.2" - "@ckeditor/ckeditor5-ui" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" - "@ckeditor/ckeditor5-widget" "46.0.2" - ckeditor5 "46.0.2" + "@ckeditor/ckeditor5-clipboard" "46.0.3" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-engine" "46.0.3" + "@ckeditor/ckeditor5-icons" "46.0.3" + "@ckeditor/ckeditor5-ui" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" + "@ckeditor/ckeditor5-widget" "46.0.3" + ckeditor5 "46.0.3" es-toolkit "1.39.5" -"@ckeditor/ckeditor5-theme-lark@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-theme-lark/-/ckeditor5-theme-lark-46.0.2.tgz#b87718cc592dbaf4063319ef7fd2b080df42dbe0" - integrity sha512-sHhwOZVg0e3SHm6caeHP67VlKojtoqxiu6oCwFduC+hK4s3OhQ3J/v+FIs7wGeFPz4ReBMAp63LNJVVcllRw+g== +"@ckeditor/ckeditor5-theme-lark@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-theme-lark/-/ckeditor5-theme-lark-46.0.3.tgz#3707200acb4da4a8ba2a2afadb20a8b1bc0a9edb" + integrity sha512-0w4fwXFExlcsDsPXgNrQz86WJWCUwIYJkcRbjL+K3fMRYBPGVoBO25OHL7tPy2rYvrnZindCJXW9w8FzKSsKhA== dependencies: - "@ckeditor/ckeditor5-ui" "46.0.2" + "@ckeditor/ckeditor5-ui" "46.0.3" -"@ckeditor/ckeditor5-typing@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-typing/-/ckeditor5-typing-46.0.2.tgz#4fb72c79f084ab96c297898857289823f3c7707b" - integrity sha512-jYrsRmE1rZ6c8jtOWVm6Q3FpIT9HWdJg6fK453w4upkjWM7lH3kXxtPgSLmEATUyO/ON91VNXEGA+LGml2MHnw== +"@ckeditor/ckeditor5-typing@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-typing/-/ckeditor5-typing-46.0.3.tgz#449eb12d2916b8d6ffe026ee19a823cbeda1b460" + integrity sha512-iyxTTWIJ1/DpjCk+Uca9bE8P+Q7nvMssustEoMd6b3n39McCxnnonW7hrLUjFsRf/lPuvcAhpvFApoy2cbBRZA== dependencies: - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-engine" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-engine" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" es-toolkit "1.39.5" -"@ckeditor/ckeditor5-ui@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-ui/-/ckeditor5-ui-46.0.2.tgz#d2bc6a2cbf8f557749ced031239db1f35a5f0867" - integrity sha512-c0Emy60YDY0EZl8nLPNaFoEA60cxQvfz8cD9uK7MYw9L5s4xSi+m0Nd0P2BR8gK/dfRnwiBnUyLDcu4yPMN1hw== +"@ckeditor/ckeditor5-ui@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-ui/-/ckeditor5-ui-46.0.3.tgz#58d03f07402245ee92a9e6caad84f8a36e7770d4" + integrity sha512-5sRd7/IxWI+jL8N8CO5n35AwM5ofMieFLjvhtdzmkZsHl2hNHMHyfjERlOynp6tkX3TlelJBokqpAO7Yu+DrHA== dependencies: - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-editor-multi-root" "46.0.2" - "@ckeditor/ckeditor5-engine" "46.0.2" - "@ckeditor/ckeditor5-icons" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-editor-multi-root" "46.0.3" + "@ckeditor/ckeditor5-engine" "46.0.3" + "@ckeditor/ckeditor5-icons" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" "@types/color-convert" "2.0.4" color-convert "3.1.0" color-parse "2.0.2" es-toolkit "1.39.5" vanilla-colorful "0.7.2" -"@ckeditor/ckeditor5-undo@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-undo/-/ckeditor5-undo-46.0.2.tgz#94dddf6887e2d0b931aa739a52a015446b73a8d2" - integrity sha512-IOFL9rrYvk2KcNyFK9YPOENM3H7RRqtBNNmj9A9zntpqsoq+8QKqcY5BpcDeODrkOtmbrhwDwcwcek7uqI3S5g== +"@ckeditor/ckeditor5-undo@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-undo/-/ckeditor5-undo-46.0.3.tgz#0aa086fb2df862451e525dd9f24bfd34410a2bfc" + integrity sha512-DnSBUIVOpARMDOtMrwvAOYAMZK263ubGLp48N4Yb4bpbE9VwH9KUaTNP1aRRE36wQ46KaPYiROqhnnq+RaemLQ== dependencies: - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-engine" "46.0.2" - "@ckeditor/ckeditor5-icons" "46.0.2" - "@ckeditor/ckeditor5-ui" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-engine" "46.0.3" + "@ckeditor/ckeditor5-icons" "46.0.3" + "@ckeditor/ckeditor5-ui" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" -"@ckeditor/ckeditor5-upload@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-upload/-/ckeditor5-upload-46.0.2.tgz#3b1ab782e15b960df563daa4d5eb77a0ebb03df5" - integrity sha512-34lQ7Cx+/hiHAsY3yL+mwbD2Y1QPsqdr9VdgQU8McfwQNSh/PHBa5WplIMsdMRym8pEicj50nsli/hVl58FsZg== +"@ckeditor/ckeditor5-upload@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-upload/-/ckeditor5-upload-46.0.3.tgz#8437e5d17db98a2c2e646f0b06e5c8941dba2b57" + integrity sha512-VfC3KG1fIaXQkzQRjIlt3b+G44DPj39jD9I5cepLN/xXsHU/EAUcJWXScsd/GlViSDR0DUDCygWyhIIbF/Vobw== dependencies: - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" -"@ckeditor/ckeditor5-utils@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-utils/-/ckeditor5-utils-46.0.2.tgz#de4dff172d999b5cff5764092839f15e813ea03a" - integrity sha512-7t9PAZurES75Nz7ICadfRoGT5SbXnbxu6L5PoAxmyIGFPKICdZ6I4mVILVraPSNwgFDm/Zg2RxmiCOMWFTlxMg== +"@ckeditor/ckeditor5-utils@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-utils/-/ckeditor5-utils-46.0.3.tgz#58831c99d3834b17146ea2a3d06d93fab932a1e2" + integrity sha512-z+4EI8IOSJpDzKdRSw0KHmLK3LMwYeZ9R207oQzswqlbvhYcUib3HhfMlwhE6pyAGYTofpZQ2btHEOaLPRCTDQ== dependencies: - "@ckeditor/ckeditor5-ui" "46.0.2" + "@ckeditor/ckeditor5-ui" "46.0.3" es-toolkit "1.39.5" -"@ckeditor/ckeditor5-watchdog@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-watchdog/-/ckeditor5-watchdog-46.0.2.tgz#ad14a7a2afe04d2e80f8ff294ade8951d1fd1815" - integrity sha512-QaXczfT5WgyteNVzbYWhZ0SBLQj/qXXRefMq0v1mpI9Iro44iMV7XmvOWhTVsskwTuNq32a1C5zMzfW0Ax69rQ== +"@ckeditor/ckeditor5-watchdog@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-watchdog/-/ckeditor5-watchdog-46.0.3.tgz#78a6430ce6b9db0c8c0fcf5d5a2a539869ed7b29" + integrity sha512-TcSM3n9bsJ+Rpzc7NFN2BdobxXAnRJ52n0XY8CeVYZ0VA61GtG/zINH+OdEUORcpqKylH4F1ftyNEwf6cdUbPA== dependencies: - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-editor-multi-root" "46.0.2" - "@ckeditor/ckeditor5-engine" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-editor-multi-root" "46.0.3" + "@ckeditor/ckeditor5-engine" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" es-toolkit "1.39.5" -"@ckeditor/ckeditor5-widget@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-widget/-/ckeditor5-widget-46.0.2.tgz#e84edc5f2bc477d31b6eb639949511656fba8647" - integrity sha512-uBcYwT7vTKCyuMXZIi0Qbs3neBQQp1sFFb/ClsX0elbh3UZEoVyr13uZIgl1+TrnVZa0scICJfWLbaiRHjVTXg== +"@ckeditor/ckeditor5-widget@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-widget/-/ckeditor5-widget-46.0.3.tgz#4fda5f828f7a35e6d8b80b186d053b140cd1b5da" + integrity sha512-h5+KbQslzDVWntJQYCkSIj0huJSvE/lkjWTVCsbo2wmbKg6jusP+1oQ5ENtd7Nz4bpJlT83UkKDslSrF23xKlA== dependencies: - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-engine" "46.0.2" - "@ckeditor/ckeditor5-enter" "46.0.2" - "@ckeditor/ckeditor5-icons" "46.0.2" - "@ckeditor/ckeditor5-typing" "46.0.2" - "@ckeditor/ckeditor5-ui" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-engine" "46.0.3" + "@ckeditor/ckeditor5-enter" "46.0.3" + "@ckeditor/ckeditor5-icons" "46.0.3" + "@ckeditor/ckeditor5-typing" "46.0.3" + "@ckeditor/ckeditor5-ui" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" es-toolkit "1.39.5" -"@ckeditor/ckeditor5-word-count@46.0.2": - version "46.0.2" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-word-count/-/ckeditor5-word-count-46.0.2.tgz#5ad88e49ea96294dbd3dcc3ed9e19023c433dd49" - integrity sha512-U2b1DTchEE75ndHmDMmV3y/NXFFx9yIoSYzupsPJywKVTdBFdDZvSnulEocuP/YCgWTA1VWTiAirRTmccII/Qw== +"@ckeditor/ckeditor5-word-count@46.0.3": + version "46.0.3" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-word-count/-/ckeditor5-word-count-46.0.3.tgz#d0ffdb77e907f2eb913dade822a3b32b06194065" + integrity sha512-Qobva/b/79t4hD6ZgWsBT3PgGIFXU2dZW62kFDJNVkGpq1pkKboIdq7Iu57OffLDJaV+xkAmEvV6cIDWc4KADA== dependencies: - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-ui" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" - ckeditor5 "46.0.2" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-ui" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" + ckeditor5 "46.0.3" es-toolkit "1.39.5" "@csstools/selector-resolve-nested@^3.1.0": @@ -1838,9 +1830,9 @@ tslib "^2.8.0" "@fortawesome/fontawesome-free@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-7.0.0.tgz#e4a6788a2d395ea97e7812a096e29bf9c95b944c" - integrity sha512-X48nISrSOa89zu2VMljC4XaRf8NmgTwQBVHfS2Nu5G00ZwM31oOVrAtGxZF3b6wDYf9lJsf/Eq4cCSFKIkOWPQ== + version "7.0.1" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-7.0.1.tgz#c1ac7f07ba2df47d1de7b7236fad25c4e6ca5076" + integrity sha512-RLmb9U6H2rJDnGxEqXxzy7ANPrQz7WK2/eTjdZqyU9uRU5W+FkAec9uU5gTYzFBH7aoXIw2WTJSCJR4KPlReQw== "@gar/promisify@^1.0.1": version "1.1.3" @@ -1906,6 +1898,14 @@ "@jridgewell/sourcemap-codec" "^1.5.0" "@jridgewell/trace-mapping" "^0.3.24" +"@jridgewell/remapping@^2.3.5": + version "2.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/remapping/-/remapping-2.3.5.tgz#375c476d1972947851ba1e15ae8f123047445aa1" + integrity sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.24" + "@jridgewell/resolve-uri@^3.1.0": version "3.1.2" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" @@ -2076,9 +2076,9 @@ "@types/ms" "*" "@types/emscripten@^1.40.1": - version "1.40.1" - resolved "https://registry.yarnpkg.com/@types/emscripten/-/emscripten-1.40.1.tgz#4c34102d7cd1503979d4e6652082c23fd805805e" - integrity sha512-sr53lnYkQNhjHNN0oJDdUm5564biioI5DuOpycufDVK7D3y+GR3oUswe2rlwY1nPNyusHbrJ9WoTyIHl4/Bpwg== + version "1.41.1" + resolved "https://registry.yarnpkg.com/@types/emscripten/-/emscripten-1.41.1.tgz#318cc5f22c0108f62fe0ede8ef8c7aee38d6b43a" + integrity sha512-vW2aEgBUU1c2CB+qVMislA98amRVPszdALjqNCuUIJaEFZsNaFaM4g5IMXIs+6oHbmmb7q6zeXYubhtObJ9ZLg== "@types/eslint-scope@^3.7.7": version "3.7.7" @@ -2160,9 +2160,9 @@ integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA== "@types/node@*": - version "24.3.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-24.3.0.tgz#89b09f45cb9a8ee69466f18ee5864e4c3eb84dec" - integrity sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow== + version "24.3.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-24.3.1.tgz#b0a3fb2afed0ef98e8d7f06d46ef6349047709f3" + integrity sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g== dependencies: undici-types "~7.10.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.14.0, acorn@^8.15.0: +acorn@^8.0.4, acorn@^8.11.0, acorn@^8.15.0: version "8.15.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816" integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== @@ -2770,9 +2770,9 @@ caniuse-api@^3.0.0: lodash.uniq "^4.5.0" caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001737: - version "1.0.30001737" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001737.tgz#8292bb7591932ff09e9a765f12fdf5629a241ccc" - integrity sha512-BiloLiXtQNrY5UyF0+1nSJLXUENuhka2pzy2Fx5pGxqavdrxSCW4U6Pn/PoG3Efspi2frRbHpBV2XsrPE6EDlw== + version "1.0.30001741" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001741.tgz#67fb92953edc536442f3c9da74320774aa523143" + integrity sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw== ccount@^2.0.0: version "2.0.1" @@ -2849,72 +2849,72 @@ ci-info@^3.2.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== -ckeditor5@46.0.2, ckeditor5@^46.0.0: - version "46.0.2" - resolved "https://registry.yarnpkg.com/ckeditor5/-/ckeditor5-46.0.2.tgz#da85d11dc56a3cbac8599ae334f05854cedfbe8f" - integrity sha512-Ly+pG/OkF+9P7DaaaCp+VYJOm0+flxLR3Ue1thm10JnMvOW52XXYaRyoasAXoiGz6CC4lh0ZN7AtQSWu85oj3g== +ckeditor5@46.0.3, ckeditor5@^46.0.0: + version "46.0.3" + resolved "https://registry.yarnpkg.com/ckeditor5/-/ckeditor5-46.0.3.tgz#aa1f52ad6542e90aa4b720e592012c979e8b8194" + integrity sha512-BGadZ1td6emWnNVbX40nygpxZMAYQvtC/wRhdhedJpjqmwXQmwLte9Y9RZg+lnomrEiLiaxzFsz1j4I6u2fBnA== dependencies: - "@ckeditor/ckeditor5-adapter-ckfinder" "46.0.2" - "@ckeditor/ckeditor5-alignment" "46.0.2" - "@ckeditor/ckeditor5-autoformat" "46.0.2" - "@ckeditor/ckeditor5-autosave" "46.0.2" - "@ckeditor/ckeditor5-basic-styles" "46.0.2" - "@ckeditor/ckeditor5-block-quote" "46.0.2" - "@ckeditor/ckeditor5-bookmark" "46.0.2" - "@ckeditor/ckeditor5-ckbox" "46.0.2" - "@ckeditor/ckeditor5-ckfinder" "46.0.2" - "@ckeditor/ckeditor5-clipboard" "46.0.2" - "@ckeditor/ckeditor5-cloud-services" "46.0.2" - "@ckeditor/ckeditor5-code-block" "46.0.2" - "@ckeditor/ckeditor5-core" "46.0.2" - "@ckeditor/ckeditor5-easy-image" "46.0.2" - "@ckeditor/ckeditor5-editor-balloon" "46.0.2" - "@ckeditor/ckeditor5-editor-classic" "46.0.2" - "@ckeditor/ckeditor5-editor-decoupled" "46.0.2" - "@ckeditor/ckeditor5-editor-inline" "46.0.2" - "@ckeditor/ckeditor5-editor-multi-root" "46.0.2" - "@ckeditor/ckeditor5-emoji" "46.0.2" - "@ckeditor/ckeditor5-engine" "46.0.2" - "@ckeditor/ckeditor5-enter" "46.0.2" - "@ckeditor/ckeditor5-essentials" "46.0.2" - "@ckeditor/ckeditor5-find-and-replace" "46.0.2" - "@ckeditor/ckeditor5-font" "46.0.2" - "@ckeditor/ckeditor5-fullscreen" "46.0.2" - "@ckeditor/ckeditor5-heading" "46.0.2" - "@ckeditor/ckeditor5-highlight" "46.0.2" - "@ckeditor/ckeditor5-horizontal-line" "46.0.2" - "@ckeditor/ckeditor5-html-embed" "46.0.2" - "@ckeditor/ckeditor5-html-support" "46.0.2" - "@ckeditor/ckeditor5-icons" "46.0.2" - "@ckeditor/ckeditor5-image" "46.0.2" - "@ckeditor/ckeditor5-indent" "46.0.2" - "@ckeditor/ckeditor5-language" "46.0.2" - "@ckeditor/ckeditor5-link" "46.0.2" - "@ckeditor/ckeditor5-list" "46.0.2" - "@ckeditor/ckeditor5-markdown-gfm" "46.0.2" - "@ckeditor/ckeditor5-media-embed" "46.0.2" - "@ckeditor/ckeditor5-mention" "46.0.2" - "@ckeditor/ckeditor5-minimap" "46.0.2" - "@ckeditor/ckeditor5-page-break" "46.0.2" - "@ckeditor/ckeditor5-paragraph" "46.0.2" - "@ckeditor/ckeditor5-paste-from-office" "46.0.2" - "@ckeditor/ckeditor5-remove-format" "46.0.2" - "@ckeditor/ckeditor5-restricted-editing" "46.0.2" - "@ckeditor/ckeditor5-select-all" "46.0.2" - "@ckeditor/ckeditor5-show-blocks" "46.0.2" - "@ckeditor/ckeditor5-source-editing" "46.0.2" - "@ckeditor/ckeditor5-special-characters" "46.0.2" - "@ckeditor/ckeditor5-style" "46.0.2" - "@ckeditor/ckeditor5-table" "46.0.2" - "@ckeditor/ckeditor5-theme-lark" "46.0.2" - "@ckeditor/ckeditor5-typing" "46.0.2" - "@ckeditor/ckeditor5-ui" "46.0.2" - "@ckeditor/ckeditor5-undo" "46.0.2" - "@ckeditor/ckeditor5-upload" "46.0.2" - "@ckeditor/ckeditor5-utils" "46.0.2" - "@ckeditor/ckeditor5-watchdog" "46.0.2" - "@ckeditor/ckeditor5-widget" "46.0.2" - "@ckeditor/ckeditor5-word-count" "46.0.2" + "@ckeditor/ckeditor5-adapter-ckfinder" "46.0.3" + "@ckeditor/ckeditor5-alignment" "46.0.3" + "@ckeditor/ckeditor5-autoformat" "46.0.3" + "@ckeditor/ckeditor5-autosave" "46.0.3" + "@ckeditor/ckeditor5-basic-styles" "46.0.3" + "@ckeditor/ckeditor5-block-quote" "46.0.3" + "@ckeditor/ckeditor5-bookmark" "46.0.3" + "@ckeditor/ckeditor5-ckbox" "46.0.3" + "@ckeditor/ckeditor5-ckfinder" "46.0.3" + "@ckeditor/ckeditor5-clipboard" "46.0.3" + "@ckeditor/ckeditor5-cloud-services" "46.0.3" + "@ckeditor/ckeditor5-code-block" "46.0.3" + "@ckeditor/ckeditor5-core" "46.0.3" + "@ckeditor/ckeditor5-easy-image" "46.0.3" + "@ckeditor/ckeditor5-editor-balloon" "46.0.3" + "@ckeditor/ckeditor5-editor-classic" "46.0.3" + "@ckeditor/ckeditor5-editor-decoupled" "46.0.3" + "@ckeditor/ckeditor5-editor-inline" "46.0.3" + "@ckeditor/ckeditor5-editor-multi-root" "46.0.3" + "@ckeditor/ckeditor5-emoji" "46.0.3" + "@ckeditor/ckeditor5-engine" "46.0.3" + "@ckeditor/ckeditor5-enter" "46.0.3" + "@ckeditor/ckeditor5-essentials" "46.0.3" + "@ckeditor/ckeditor5-find-and-replace" "46.0.3" + "@ckeditor/ckeditor5-font" "46.0.3" + "@ckeditor/ckeditor5-fullscreen" "46.0.3" + "@ckeditor/ckeditor5-heading" "46.0.3" + "@ckeditor/ckeditor5-highlight" "46.0.3" + "@ckeditor/ckeditor5-horizontal-line" "46.0.3" + "@ckeditor/ckeditor5-html-embed" "46.0.3" + "@ckeditor/ckeditor5-html-support" "46.0.3" + "@ckeditor/ckeditor5-icons" "46.0.3" + "@ckeditor/ckeditor5-image" "46.0.3" + "@ckeditor/ckeditor5-indent" "46.0.3" + "@ckeditor/ckeditor5-language" "46.0.3" + "@ckeditor/ckeditor5-link" "46.0.3" + "@ckeditor/ckeditor5-list" "46.0.3" + "@ckeditor/ckeditor5-markdown-gfm" "46.0.3" + "@ckeditor/ckeditor5-media-embed" "46.0.3" + "@ckeditor/ckeditor5-mention" "46.0.3" + "@ckeditor/ckeditor5-minimap" "46.0.3" + "@ckeditor/ckeditor5-page-break" "46.0.3" + "@ckeditor/ckeditor5-paragraph" "46.0.3" + "@ckeditor/ckeditor5-paste-from-office" "46.0.3" + "@ckeditor/ckeditor5-remove-format" "46.0.3" + "@ckeditor/ckeditor5-restricted-editing" "46.0.3" + "@ckeditor/ckeditor5-select-all" "46.0.3" + "@ckeditor/ckeditor5-show-blocks" "46.0.3" + "@ckeditor/ckeditor5-source-editing" "46.0.3" + "@ckeditor/ckeditor5-special-characters" "46.0.3" + "@ckeditor/ckeditor5-style" "46.0.3" + "@ckeditor/ckeditor5-table" "46.0.3" + "@ckeditor/ckeditor5-theme-lark" "46.0.3" + "@ckeditor/ckeditor5-typing" "46.0.3" + "@ckeditor/ckeditor5-ui" "46.0.3" + "@ckeditor/ckeditor5-undo" "46.0.3" + "@ckeditor/ckeditor5-upload" "46.0.3" + "@ckeditor/ckeditor5-utils" "46.0.3" + "@ckeditor/ckeditor5-watchdog" "46.0.3" + "@ckeditor/ckeditor5-widget" "46.0.3" + "@ckeditor/ckeditor5-word-count" "46.0.3" clean-stack@^2.0.0: version "2.2.0" @@ -3656,9 +3656,9 @@ duplexer@^0.1.2: integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== electron-to-chromium@^1.5.211: - version "1.5.211" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.211.tgz#749317bf9cf894c06f67980940cf8074e5eb08ca" - integrity sha512-IGBvimJkotaLzFnwIVgW9/UD/AOJ2tByUmeOrtqBfACSbAw5b1G0XpvdaieKyc7ULmbwXVx+4e4Be8pOPBrYkw== + version "1.5.214" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.214.tgz#f7bbdc0796124292d4b8a34a49e968c5e6430763" + integrity sha512-TpvUNdha+X3ybfU78NoQatKvQEm1oq3lf2QbnmCEdw+Bd9RuIAY+hJTvq1avzHM0f7EJfnH3vbCnbzKzisc/9Q== emoji-regex@^7.0.1: version "7.0.3" @@ -5728,9 +5728,9 @@ node-notifier@^9.0.0: which "^2.0.2" node-releases@^2.0.19: - version "2.0.19" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314" - integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw== + version "2.0.20" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.20.tgz#e26bb79dbdd1e64a146df389c699014c611cbc27" + integrity sha512-7gK6zSXEH6neM212JgfYFXe+GmZQM+fia5SsusuBIUgnPheLFBmIPhtFoAQRj8/7wASYQnbDlHPVwY0BefoFgA== normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" @@ -7390,12 +7390,12 @@ terser-webpack-plugin@^5.3.0, terser-webpack-plugin@^5.3.11: terser "^5.31.1" terser@^5.3.4, terser@^5.31.1: - version "5.43.1" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.43.1.tgz#88387f4f9794ff1a29e7ad61fb2932e25b4fdb6d" - integrity sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg== + version "5.44.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.44.0.tgz#ebefb8e5b8579d93111bfdfc39d2cf63879f4a82" + integrity sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w== dependencies: "@jridgewell/source-map" "^0.3.3" - acorn "^8.14.0" + acorn "^8.15.0" commander "^2.20.0" source-map-support "~0.5.20" From 3e8ca0617700f7bd3a4bd87ecbc4389086a96e01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 6 Sep 2025 19:34:31 +0200 Subject: [PATCH 056/228] Fixed text color in ckeditor editors when in dark mode Fixes issue #1016 --- assets/css/components/ckeditor.css | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/assets/css/components/ckeditor.css b/assets/css/components/ckeditor.css index d6b3def4..5f093bf2 100644 --- a/assets/css/components/ckeditor.css +++ b/assets/css/components/ckeditor.css @@ -71,6 +71,8 @@ --ck-color-button-on-hover-background: var(--bs-secondary-bg); --ck-color-button-on-active-background: var(--bs-secondary-bg); --ck-color-button-on-disabled-background: var(--bs-secondary-bg); - --ck-color-button-on-color: var(--bs-primary) + --ck-color-button-on-color: var(--bs-primary); -} \ No newline at end of file + --ck-content-font-color: var(--ck-color-base-text); + +} From b1443a817ba22e3e3a9a54557cbd44ca26493cf9 Mon Sep 17 00:00:00 2001 From: d-buchmann Date: Sat, 6 Sep 2025 19:42:07 +0200 Subject: [PATCH 057/228] Add import permission for label profiles (#1021) --- config/permissions.yaml | 4 ++++ src/Security/Voter/LabelProfileVoter.php | 1 + 2 files changed, 5 insertions(+) diff --git a/config/permissions.yaml b/config/permissions.yaml index e5a1d65b..8cbd60c3 100644 --- a/config/permissions.yaml +++ b/config/permissions.yaml @@ -359,6 +359,10 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co label: "perm.revert_elements" alsoSet: ['read_profiles', 'edit_profiles', 'create_profiles', 'delete_profiles'] apiTokenRole: ROLE_API_EDIT + import: + label: "perm.import" + alsoSet: ['read_profiles', 'edit_profiles', 'create_profiles' ] + apiTokenRole: ROLE_API_EDIT api: label: "perm.api" diff --git a/src/Security/Voter/LabelProfileVoter.php b/src/Security/Voter/LabelProfileVoter.php index cd349ddb..1687bf45 100644 --- a/src/Security/Voter/LabelProfileVoter.php +++ b/src/Security/Voter/LabelProfileVoter.php @@ -59,6 +59,7 @@ final class LabelProfileVoter extends Voter 'delete' => 'delete_profiles', 'show_history' => 'show_history', 'revert_element' => 'revert_element', + 'import' => 'import', ]; public function __construct(private readonly VoterHelper $helper) From 411ac500baa34d2077355620fa10a3e1ebe85fd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 6 Sep 2025 19:43:05 +0200 Subject: [PATCH 058/228] New Crowdin updates (#1008) * New translations messages.en.xlf (Czech) * New translations messages.en.xlf (Czech) * New translations messages.en.xlf (Czech) * New translations messages.en.xlf (Czech) * New translations messages.en.xlf (German) * New translations messages.en.xlf (German) * New translations messages.en.xlf (German) --- translations/messages.cs.xlf | 36 ++++++++++++++++++------------------ translations/messages.de.xlf | 14 ++++++++++---- 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/translations/messages.cs.xlf b/translations/messages.cs.xlf index c70ad2af..7e896170 100644 --- a/translations/messages.cs.xlf +++ b/translations/messages.cs.xlf @@ -580,7 +580,7 @@ storelocation.new - Nové místo skladování + Nové umístění @@ -913,7 +913,7 @@ Související prvky budou přesunuty nahoru. edit.log_comment - Změnit komentář + Komentář ke změně @@ -2502,7 +2502,7 @@ Související prvky budou přesunuty nahoru. part.needs_review.badge - Potřeba revize + Vyžaduje kontrolu @@ -4019,7 +4019,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn search.regexmatching - RegEx. shoda + Reg.Ex. shoda @@ -4858,7 +4858,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn part.table.needsReview - Potřeba revize + Vyžaduje kontrolu @@ -5662,7 +5662,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn part.edit.needs_review - Potřeba revize + Vyžaduje kontrolu @@ -6357,7 +6357,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn user.theme.label - Téma + Vzhled @@ -6368,7 +6368,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn user_settings.theme.placeholder - Serverové téma + Vzhled pro celý server @@ -9718,7 +9718,7 @@ Element 3 part_list.action.action.group.needs_review - Potřeba revize + Vyžaduje kontrolu @@ -10678,7 +10678,7 @@ Element 3 log.element_edited.changed_fields.theme - Téma + Vzhled @@ -10774,7 +10774,7 @@ Element 3 log.element_edited.changed_fields.needs_review - Potřeba revize + Vyžaduje kontrolu @@ -10984,7 +10984,7 @@ Element 3 parts.import.help - Pomocí tohoto nástroje můžete importovat díly z existujících souborů. Díly budou zapsány přímo do databáze, proto před nahráním souboru sem zkontrolujte, zda je jeho obsah správný. + Pomocí tohoto nástroje můžete importovat součásti z existujících souborů. Součásti budou přímo zapsány do databáze, proto před nahráním souboru zkontrolujte jeho správný obsah. @@ -11014,7 +11014,7 @@ Element 3 parts.import.part_needs_review.help - Pokud je tato možnost vybrána, budou všechny díly označeny jako "Potřeba revize" bez ohledu na to, co bylo nastaveno v údajích. + Pokud je tato možnost vybrána, budou všechny díly označeny jako "Vyžaduje kontrolu" bez ohledu na to, co bylo nastaveno v údajích. @@ -12060,7 +12060,7 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz part.info.withdraw_modal.delete_lot_if_empty - Vymazat tento inventář, až se vyprázdní + Smazat tuto položku, pokud se vyprázdní @@ -12528,7 +12528,7 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz settings.system.customization.instanceName - Instance name + Název instance @@ -12576,7 +12576,7 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz settings.system.customization.theme - Globální téma + Globální vzhed @@ -12642,7 +12642,7 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz settings.system.privacy.useGravatar.description - Pokud uživatel nemá nastavený avatar, použijte avatar z Gravataru na základě e-mailové adresy uživatele. To způsobí, že prohlížeč načte obrázky od třetí strany! + Pokud uživatel nemá zadaný obrázek avatara, použije se avatar z Gravataru na základě e-mailu uživatele. To způsobí, že prohlížeč načte obrázky ze třetí strany! @@ -12691,7 +12691,7 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz settings.system.privacy - Ochrana osobních údajů + Soukromí diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index a5c18cdd..b579d908 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -6504,7 +6504,7 @@ Wenn Sie dies fehlerhafterweise gemacht haben oder ein Computer nicht mehr vertr flash.password_change_needed - Ihr Password muss geändert werden! + Ihr Passwort muss geändert werden! @@ -7157,8 +7157,14 @@ Wenn Sie dies fehlerhafterweise gemacht haben oder ein Computer nicht mehr vertr mass_creation.lines.placeholder Element 1 + Element 1.1 + Element 1.1.1 + Element 1.2 Element 2 -Element 3 +Element 3 + +Element 1 -> Element 1.1 +Element 1 -> Element 1.2 @@ -9006,7 +9012,7 @@ Element 3 part_list.action.part_count - %count% Bauteile ausgewählt! + %count% Bauteile ausgewählt @@ -12921,7 +12927,7 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön settings.behavior.sidebar.rootNodeRedirectsToNewEntity - Wurzelknoten leitet zur Erstellung eines neuen Elements weiter + Stammknoten leitet zur Erstellung eines neuen Elements weiter From 4e9e82d9f1832486221cf3f8b79d9925701079ba Mon Sep 17 00:00:00 2001 From: d-buchmann Date: Sat, 6 Sep 2025 19:43:50 +0200 Subject: [PATCH 059/228] Replace "range" indicators with mathematical tilde in LCSC provider (#989) * Replace "range" indicators with mathematical tilde symbols in LCSC provider * Improve comment --- src/Services/InfoProviderSystem/Providers/LCSCProvider.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Services/InfoProviderSystem/Providers/LCSCProvider.php b/src/Services/InfoProviderSystem/Providers/LCSCProvider.php index 8db53f76..2d83fc7c 100755 --- a/src/Services/InfoProviderSystem/Providers/LCSCProvider.php +++ b/src/Services/InfoProviderSystem/Providers/LCSCProvider.php @@ -165,6 +165,9 @@ class LCSCProvider implements InfoProviderInterface if ($field === null) { return null; } + // Replace "range" indicators with mathematical tilde symbols + // so they don't get rendered as strikethrough by Markdown + $field = preg_replace("/~/", "\u{223c}", $field); return strip_tags($field); } From 0e9558e331de4170548762760a48f368e8664491 Mon Sep 17 00:00:00 2001 From: d-buchmann Date: Sat, 6 Sep 2025 19:49:38 +0200 Subject: [PATCH 060/228] Do not mark internal (relative) links as external and open in new tab in markdown blocks Don't handle links as external by default. Instead distiguish internal (relative) and external (absolute) links. --- .../controllers/common/markdown_controller.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/assets/controllers/common/markdown_controller.js b/assets/controllers/common/markdown_controller.js index b6ef0034..c6cb97df 100644 --- a/assets/controllers/common/markdown_controller.js +++ b/assets/controllers/common/markdown_controller.js @@ -56,12 +56,16 @@ export default class MarkdownController extends Controller { this.element.innerHTML = DOMPurify.sanitize(MarkdownController._marked.parse(this.unescapeHTML(raw))); for(let a of this.element.querySelectorAll('a')) { - //Mark all links as external - a.classList.add('link-external'); - //Open links in new tag - a.setAttribute('target', '_blank'); - //Dont track - a.setAttribute('rel', 'noopener'); + // test if link is absolute + var r = new RegExp('^(?:[a-z+]+:)?//', 'i'); + if (r.test(a.getAttribute('href'))) { + //Mark all links as external + a.classList.add('link-external'); + //Open links in new tag + a.setAttribute('target', '_blank'); + //Dont track + a.setAttribute('rel', 'noopener'); + } } //Apply bootstrap styles to tables @@ -108,4 +112,4 @@ export default class MarkdownController extends Controller { gfm: true, }); }*/ -} \ No newline at end of file +} From 4277f4228530879f4dd3848b863afba470840194 Mon Sep 17 00:00:00 2001 From: barisgit Date: Sun, 3 Aug 2025 16:14:27 +0200 Subject: [PATCH 061/228] Fix same error as in other branch and add makefile --- makefile | 108 ++++++++++++++++++ .../TimestampableElementProviderTest.php | 19 +-- 2 files changed, 119 insertions(+), 8 deletions(-) create mode 100644 makefile diff --git a/makefile b/makefile new file mode 100644 index 00000000..3f389638 --- /dev/null +++ b/makefile @@ -0,0 +1,108 @@ +# PartDB Makefile for Test Environment Management + +.PHONY: help test-setup test-clean test-db-create test-db-migrate test-cache-clear test-fixtures test-run dev-setup dev-clean dev-db-create dev-db-migrate dev-cache-clear dev-warmup dev-reset deps-install + +# Default target +help: + @echo "PartDB Test Environment Management" + @echo "==================================" + @echo "" + @echo "Available targets:" + @echo " deps-install - Install PHP dependencies with unlimited memory" + @echo "" + @echo "Development Environment:" + @echo " dev-setup - Complete development environment setup (clean, create DB, migrate, warmup)" + @echo " dev-clean - Clean development cache and database files" + @echo " dev-db-create - Create development database (if not exists)" + @echo " dev-db-migrate - Run database migrations for development environment" + @echo " dev-cache-clear - Clear development cache" + @echo " dev-warmup - Warm up development cache" + @echo " dev-reset - Quick development reset (clean + migrate)" + @echo "" + @echo "Test Environment:" + @echo " test-setup - Complete test environment setup (clean, create DB, migrate, load fixtures)" + @echo " test-clean - Clean test cache and database files" + @echo " test-db-create - Create test database (if not exists)" + @echo " test-db-migrate - Run database migrations for test environment" + @echo " test-cache-clear- Clear test cache" + @echo " test-fixtures - Load test fixtures" + @echo " test-run - Run PHPUnit tests" + @echo "" + @echo " help - Show this help message" + +# Install PHP dependencies with unlimited memory +deps-install: + @echo "📦 Installing PHP dependencies..." + COMPOSER_MEMORY_LIMIT=-1 composer install + @echo "✅ Dependencies installed" + +# Complete test environment setup +test-setup: deps-install test-clean test-db-create test-db-migrate test-fixtures + @echo "✅ Test environment setup complete!" + +# Clean test environment +test-clean: + @echo "🧹 Cleaning test environment..." + rm -rf var/cache/test + rm -f var/app_test.db + @echo "✅ Test environment cleaned" + +# Create test database +test-db-create: + @echo "🗄️ Creating test database..." + -php bin/console doctrine:database:create --if-not-exists --env test || echo "⚠️ Database creation failed (expected for SQLite) - continuing..." + +# Run database migrations for test environment +test-db-migrate: + @echo "🔄 Running database migrations..." + php -d memory_limit=1G bin/console doctrine:migrations:migrate -n --env test + +# Clear test cache +test-cache-clear: + @echo "🗑️ Clearing test cache..." + rm -rf var/cache/test + @echo "✅ Test cache cleared" + +# Load test fixtures +test-fixtures: + @echo "📦 Loading test fixtures..." + php bin/console partdb:fixtures:load -n --env test + +# Run PHPUnit tests +test-run: + @echo "🧪 Running tests..." + php bin/phpunit + +# Quick test reset (clean + migrate + fixtures, skip DB creation) +test-reset: test-cache-clear test-db-migrate test-fixtures + @echo "✅ Test environment reset complete!" + +# Development helpers +dev-setup: deps-install dev-clean dev-db-create dev-db-migrate dev-warmup + @echo "✅ Development environment setup complete!" + +dev-clean: + @echo "🧹 Cleaning development environment..." + rm -rf var/cache/dev + rm -f var/app_dev.db + @echo "✅ Development environment cleaned" + +dev-db-create: + @echo "🗄️ Creating development database..." + -php bin/console doctrine:database:create --if-not-exists --env dev || echo "⚠️ Database creation failed (expected for SQLite) - continuing..." + +dev-db-migrate: + @echo "🔄 Running database migrations..." + php -d memory_limit=1G bin/console doctrine:migrations:migrate -n --env dev + +dev-cache-clear: + @echo "🗑️ Clearing development cache..." + rm -rf var/cache/dev + @echo "✅ Development cache cleared" + +dev-warmup: + @echo "🔥 Warming up development cache..." + php -d memory_limit=1G bin/console cache:warmup --env dev -n + +dev-reset: dev-cache-clear dev-db-migrate + @echo "✅ Development environment reset complete!" \ No newline at end of file diff --git a/tests/Services/LabelSystem/PlaceholderProviders/TimestampableElementProviderTest.php b/tests/Services/LabelSystem/PlaceholderProviders/TimestampableElementProviderTest.php index a72f06df..6aa152b9 100644 --- a/tests/Services/LabelSystem/PlaceholderProviders/TimestampableElementProviderTest.php +++ b/tests/Services/LabelSystem/PlaceholderProviders/TimestampableElementProviderTest.php @@ -60,26 +60,29 @@ class TimestampableElementProviderTest extends WebTestCase protected function setUp(): void { self::bootKernel(); - \Locale::setDefault('en'); + \Locale::setDefault('en_US'); $this->service = self::getContainer()->get(TimestampableElementProvider::class); - $this->target = new class() implements TimeStampableInterface { + $this->target = new class () implements TimeStampableInterface { public function getLastModified(): ?DateTime { - return new \DateTime('2000-01-01'); + return new DateTime('2000-01-01'); } public function getAddedDate(): ?DateTime { - return new \DateTime('2000-01-01'); + return new DateTime('2000-01-01'); } }; } public static function dataProvider(): \Iterator { - \Locale::setDefault('en'); - yield ['1/1/00, 12:00 AM', '[[LAST_MODIFIED]]']; - yield ['1/1/00, 12:00 AM', '[[CREATION_DATE]]']; + \Locale::setDefault('en_US'); + // Use IntlDateFormatter like the actual service does + $formatter = new \IntlDateFormatter(\Locale::getDefault(), \IntlDateFormatter::SHORT, \IntlDateFormatter::SHORT); + $expectedFormat = $formatter->format(new DateTime('2000-01-01')); + yield [$expectedFormat, '[[LAST_MODIFIED]]']; + yield [$expectedFormat, '[[CREATION_DATE]]']; } #[DataProvider('dataProvider')] @@ -87,4 +90,4 @@ class TimestampableElementProviderTest extends WebTestCase { $this->assertSame($expected, $this->service->replace($placeholder, $this->target)); } -} +} \ No newline at end of file From d0f2422e0dee9e9c36e14ab98207732cb4c58283 Mon Sep 17 00:00:00 2001 From: barisgit Date: Sun, 3 Aug 2025 18:46:46 +0200 Subject: [PATCH 062/228] Implement functionality to import schematic csv (or any other csv for that matter), with ability to map input columns to output columns with input validation and error handling --- makefile | 2 +- src/Controller/ProjectController.php | 300 ++++++++- .../ImportExportSystem/BOMImporter.php | 593 +++++++++++++++++- .../BOMValidationService.php | 476 ++++++++++++++ .../_bom_validation_results.html.twig | 186 ++++++ .../projects/import_bom_map_fields.html.twig | 204 ++++++ 6 files changed, 1733 insertions(+), 28 deletions(-) create mode 100644 src/Services/ImportExportSystem/BOMValidationService.php create mode 100644 templates/projects/_bom_validation_results.html.twig create mode 100644 templates/projects/import_bom_map_fields.html.twig diff --git a/makefile b/makefile index 3f389638..6b5ac61f 100644 --- a/makefile +++ b/makefile @@ -97,7 +97,7 @@ dev-db-migrate: dev-cache-clear: @echo "🗑️ Clearing development cache..." - rm -rf var/cache/dev + php -d memory_limit=1G bin/console cache:clear --env dev -n @echo "✅ Development cache cleared" dev-warmup: diff --git a/src/Controller/ProjectController.php b/src/Controller/ProjectController.php index a64c1851..444ff5b3 100644 --- a/src/Controller/ProjectController.php +++ b/src/Controller/ProjectController.php @@ -36,6 +36,7 @@ use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ORM\EntityManagerInterface; use League\Csv\SyntaxError; use Omines\DataTablesBundle\DataTableFactory; +use Psr\Log\LoggerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; @@ -102,9 +103,14 @@ class ProjectController extends AbstractController $this->addFlash('success', 'project.build.flash.success'); return $this->redirect( - $request->get('_redirect', - $this->generateUrl('project_info', ['id' => $project->getID()] - ))); + $request->get( + '_redirect', + $this->generateUrl( + 'project_info', + ['id' => $project->getID()] + ) + ) + ); } $this->addFlash('error', 'project.build.flash.invalid_input'); @@ -120,9 +126,13 @@ class ProjectController extends AbstractController } #[Route(path: '/{id}/import_bom', name: 'project_import_bom', requirements: ['id' => '\d+'])] - public function importBOM(Request $request, EntityManagerInterface $entityManager, Project $project, - BOMImporter $BOMImporter, ValidatorInterface $validator): Response - { + public function importBOM( + Request $request, + EntityManagerInterface $entityManager, + Project $project, + BOMImporter $BOMImporter, + ValidatorInterface $validator + ): Response { $this->denyAccessUnlessGranted('edit', $project); $builder = $this->createFormBuilder(); @@ -138,6 +148,7 @@ class ProjectController extends AbstractController 'required' => true, 'choices' => [ 'project.bom_import.type.kicad_pcbnew' => 'kicad_pcbnew', + 'project.bom_import.type.kicad_schematic' => 'kicad_schematic', ] ]); $builder->add('clear_existing_bom', CheckboxType::class, [ @@ -161,25 +172,40 @@ class ProjectController extends AbstractController $entityManager->flush(); } + $import_type = $form->get('type')->getData(); + try { + // For schematic imports, redirect to field mapping step + if ($import_type === 'kicad_schematic') { + // Store file content and options in session for field mapping step + $file_content = $form->get('file')->getData()->getContent(); + $clear_existing = $form->get('clear_existing_bom')->getData(); + + $request->getSession()->set('bom_import_data', $file_content); + $request->getSession()->set('bom_import_clear', $clear_existing); + + return $this->redirectToRoute('project_import_bom_map_fields', ['id' => $project->getID()]); + } + + // For PCB imports, proceed directly $entries = $BOMImporter->importFileIntoProject($form->get('file')->getData(), $project, [ - 'type' => $form->get('type')->getData(), + 'type' => $import_type, ]); - //Validate the project entries + // Validate the project entries $errors = $validator->validateProperty($project, 'bom_entries'); - //If no validation errors occured, save the changes and redirect to edit page - if (count ($errors) === 0) { + // If no validation errors occurred, save the changes and redirect to edit page + if (count($errors) === 0) { $this->addFlash('success', t('project.bom_import.flash.success', ['%count%' => count($entries)])); $entityManager->flush(); return $this->redirectToRoute('project_edit', ['id' => $project->getID()]); } - //When we get here, there were validation errors + // When we get here, there were validation errors $this->addFlash('error', t('project.bom_import.flash.invalid_entries')); - } catch (\UnexpectedValueException|SyntaxError $e) { + } catch (\UnexpectedValueException | SyntaxError $e) { $this->addFlash('error', t('project.bom_import.flash.invalid_file', ['%message%' => $e->getMessage()])); } } @@ -191,11 +217,257 @@ class ProjectController extends AbstractController ]); } + #[Route(path: '/{id}/import_bom/map_fields', name: 'project_import_bom_map_fields', requirements: ['id' => '\d+'])] + public function importBOMMapFields( + Request $request, + EntityManagerInterface $entityManager, + Project $project, + BOMImporter $BOMImporter, + ValidatorInterface $validator, + LoggerInterface $logger + ): Response { + $this->denyAccessUnlessGranted('edit', $project); + + // Get stored data from session + $file_content = $request->getSession()->get('bom_import_data'); + $clear_existing = $request->getSession()->get('bom_import_clear', false); + + + if (!$file_content) { + $this->addFlash('error', 'project.bom_import.flash.session_expired'); + return $this->redirectToRoute('project_import_bom', ['id' => $project->getID()]); + } + + // Detect fields and get suggestions + $detected_fields = $BOMImporter->detectFields($file_content); + $suggested_mapping = $BOMImporter->getSuggestedFieldMapping($detected_fields); + + // Create mapping of original field names to sanitized field names for template + $field_name_mapping = []; + foreach ($detected_fields as $field) { + $sanitized_field = preg_replace('/[^a-zA-Z0-9_-]/', '_', $field); + $field_name_mapping[$field] = $sanitized_field; + } + + // Create form for field mapping + $builder = $this->createFormBuilder(); + + // Add delimiter selection + $builder->add('delimiter', ChoiceType::class, [ + 'label' => 'project.bom_import.delimiter', + 'required' => true, + 'data' => ',', + 'choices' => [ + 'project.bom_import.delimiter.comma' => ',', + 'project.bom_import.delimiter.semicolon' => ';', + 'project.bom_import.delimiter.tab' => "\t", + ] + ]); + + // Get dynamic field mapping targets from BOMImporter + $available_targets = $BOMImporter->getAvailableFieldTargets(); + $target_fields = ['project.bom_import.field_mapping.ignore' => '']; + + foreach ($available_targets as $target_key => $target_info) { + $target_fields[$target_info['label']] = $target_key; + } + + foreach ($detected_fields as $field) { + // Sanitize field name for form use - replace invalid characters with underscores + $sanitized_field = preg_replace('/[^a-zA-Z0-9_-]/', '_', $field); + $builder->add('mapping_' . $sanitized_field, ChoiceType::class, [ + 'label' => $field, + 'required' => false, + 'choices' => $target_fields, + 'data' => $suggested_mapping[$field] ?? '', + ]); + } + + $builder->add('submit', SubmitType::class, [ + 'label' => 'project.bom_import.preview', + ]); + + $form = $builder->getForm(); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + // Build field mapping array with priority support + $field_mapping = []; + $field_priorities = []; + $delimiter = $form->get('delimiter')->getData(); + + foreach ($detected_fields as $field) { + $sanitized_field = preg_replace('/[^a-zA-Z0-9_-]/', '_', $field); + $target = $form->get('mapping_' . $sanitized_field)->getData(); + if (!empty($target)) { + $field_mapping[$field] = $target; + + // Get priority from request (default to 10) + $priority = $request->request->get('priority_' . $sanitized_field, 10); + $field_priorities[$field] = (int) $priority; + } + } + + // Validate field mapping + $validation = $BOMImporter->validateFieldMapping($field_mapping, $detected_fields); + + if (!$validation['is_valid']) { + foreach ($validation['errors'] as $error) { + $this->addFlash('error', $error); + } + foreach ($validation['warnings'] as $warning) { + $this->addFlash('warning', $warning); + } + + return $this->render('projects/import_bom_map_fields.html.twig', [ + 'project' => $project, + 'form' => $form->createView(), + 'detected_fields' => $detected_fields, + 'suggested_mapping' => $suggested_mapping, + 'field_name_mapping' => $field_name_mapping, + ]); + } + + // Show warnings but continue + foreach ($validation['warnings'] as $warning) { + $this->addFlash('warning', $warning); + } + + try { + // Re-detect fields with chosen delimiter + $detected_fields = $BOMImporter->detectFields($file_content, $delimiter); + + // Clear existing BOM entries if requested + if ($clear_existing) { + $existing_count = $project->getBomEntries()->count(); + $logger->info('Clearing existing BOM entries', [ + 'existing_count' => $existing_count, + 'project_id' => $project->getID(), + ]); + $project->getBomEntries()->clear(); + $entityManager->flush(); + $logger->info('Existing BOM entries cleared'); + } else { + $existing_count = $project->getBomEntries()->count(); + $logger->info('Keeping existing BOM entries', [ + 'existing_count' => $existing_count, + 'project_id' => $project->getID(), + ]); + } + + // Validate data before importing + $validation_result = $BOMImporter->validateBOMData($file_content, [ + 'type' => 'kicad_schematic', + 'field_mapping' => $field_mapping, + 'field_priorities' => $field_priorities, + 'delimiter' => $delimiter, + ]); + + // Log validation results + $logger->info('BOM import validation completed', [ + 'total_entries' => $validation_result['total_entries'], + 'valid_entries' => $validation_result['valid_entries'], + 'invalid_entries' => $validation_result['invalid_entries'], + 'error_count' => count($validation_result['errors']), + 'warning_count' => count($validation_result['warnings']), + ]); + + // Show validation warnings to user + foreach ($validation_result['warnings'] as $warning) { + $this->addFlash('warning', $warning); + } + + // If there are validation errors, show them and stop + if (!empty($validation_result['errors'])) { + foreach ($validation_result['errors'] as $error) { + $this->addFlash('error', $error); + } + + return $this->render('projects/import_bom_map_fields.html.twig', [ + 'project' => $project, + 'form' => $form->createView(), + 'detected_fields' => $detected_fields, + 'suggested_mapping' => $suggested_mapping, + 'field_name_mapping' => $field_name_mapping, + 'validation_result' => $validation_result, + ]); + } + + // Import with field mapping and priorities (validation already passed) + $entries = $BOMImporter->stringToBOMEntries($file_content, [ + 'type' => 'kicad_schematic', + 'field_mapping' => $field_mapping, + 'field_priorities' => $field_priorities, + 'delimiter' => $delimiter, + ]); + + // Log entry details for debugging + $logger->info('BOM entries created', [ + 'total_entries' => count($entries), + ]); + + foreach ($entries as $index => $entry) { + $logger->debug("BOM entry {$index}", [ + 'name' => $entry->getName(), + 'mountnames' => $entry->getMountnames(), + 'quantity' => $entry->getQuantity(), + 'comment' => $entry->getComment(), + 'part_id' => $entry->getPart()?->getID(), + ]); + } + + // Assign entries to project + $logger->info('Adding BOM entries to project', [ + 'entries_count' => count($entries), + 'project_id' => $project->getID(), + ]); + + foreach ($entries as $index => $entry) { + $logger->debug("Adding BOM entry {$index} to project", [ + 'name' => $entry->getName(), + 'part_id' => $entry->getPart()?->getID(), + 'quantity' => $entry->getQuantity(), + ]); + $project->addBomEntry($entry); + } + + // Validate the project entries (includes collection constraints) + $errors = $validator->validateProperty($project, 'bom_entries'); + + // If no validation errors occurred, save and redirect + if (count($errors) === 0) { + $this->addFlash('success', t('project.bom_import.flash.success', ['%count%' => count($entries)])); + $entityManager->flush(); + + // Clear session data + $request->getSession()->remove('bom_import_data'); + $request->getSession()->remove('bom_import_clear'); + + return $this->redirectToRoute('project_edit', ['id' => $project->getID()]); + } + + // When we get here, there were validation errors + $this->addFlash('error', t('project.bom_import.flash.invalid_entries')); + + } catch (\UnexpectedValueException | SyntaxError $e) { + $this->addFlash('error', t('project.bom_import.flash.invalid_file', ['%message%' => $e->getMessage()])); + } + } + + return $this->render('projects/import_bom_map_fields.html.twig', [ + 'project' => $project, + 'form' => $form, + 'detected_fields' => $detected_fields, + 'suggested_mapping' => $suggested_mapping, + 'field_name_mapping' => $field_name_mapping, + ]); + } + #[Route(path: '/add_parts', name: 'project_add_parts_no_id')] #[Route(path: '/{id}/add_parts', name: 'project_add_parts', requirements: ['id' => '\d+'])] public function addPart(Request $request, EntityManagerInterface $entityManager, ?Project $project): Response { - if($project instanceof Project) { + if ($project instanceof Project) { $this->denyAccessUnlessGranted('edit', $project); } else { $this->denyAccessUnlessGranted('@projects.edit'); @@ -242,7 +514,7 @@ class ProjectController extends AbstractController $data = $form->getData(); $bom_entries = $data['bom_entries']; - foreach ($bom_entries as $bom_entry){ + foreach ($bom_entries as $bom_entry) { $target_project->addBOMEntry($bom_entry); } diff --git a/src/Services/ImportExportSystem/BOMImporter.php b/src/Services/ImportExportSystem/BOMImporter.php index d4876445..862fa463 100644 --- a/src/Services/ImportExportSystem/BOMImporter.php +++ b/src/Services/ImportExportSystem/BOMImporter.php @@ -22,10 +22,13 @@ declare(strict_types=1); */ namespace App\Services\ImportExportSystem; +use App\Entity\Parts\Part; use App\Entity\ProjectSystem\Project; use App\Entity\ProjectSystem\ProjectBOMEntry; +use Doctrine\ORM\EntityManagerInterface; use InvalidArgumentException; use League\Csv\Reader; +use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\File\File; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -44,14 +47,25 @@ class BOMImporter 5 => 'Supplier and ref', ]; - public function __construct() - { + public function __construct( + private readonly EntityManagerInterface $entityManager, + private readonly LoggerInterface $logger, + private readonly BOMValidationService $validationService + ) { } protected function configureOptions(OptionsResolver $resolver): OptionsResolver { $resolver->setRequired('type'); - $resolver->setAllowedValues('type', ['kicad_pcbnew']); + $resolver->setAllowedValues('type', ['kicad_pcbnew', 'kicad_schematic']); + + // For flexible schematic import with field mapping + $resolver->setDefined(['field_mapping', 'field_priorities', 'delimiter']); + $resolver->setDefault('delimiter', ','); + $resolver->setDefault('field_priorities', []); + $resolver->setAllowedTypes('field_mapping', 'array'); + $resolver->setAllowedTypes('field_priorities', 'array'); + $resolver->setAllowedTypes('delimiter', 'string'); return $resolver; } @@ -82,6 +96,23 @@ class BOMImporter return $this->stringToBOMEntries($file->getContent(), $options); } + /** + * Validate BOM data before importing + * @return array Validation result with errors, warnings, and info + */ + public function validateBOMData(string $data, array $options): array + { + $resolver = new OptionsResolver(); + $resolver = $this->configureOptions($resolver); + $options = $resolver->resolve($options); + + return match ($options['type']) { + 'kicad_pcbnew' => $this->validateKiCADPCB($data), + 'kicad_schematic' => $this->validateKiCADSchematicData($data, $options), + default => throw new InvalidArgumentException('Invalid import type!'), + }; + } + /** * Import string data into an array of BOM entries, which are not yet assigned to a project. * @param string $data The data to import @@ -95,12 +126,13 @@ class BOMImporter $options = $resolver->resolve($options); return match ($options['type']) { - 'kicad_pcbnew' => $this->parseKiCADPCB($data, $options), + 'kicad_pcbnew' => $this->parseKiCADPCB($data), + 'kicad_schematic' => $this->parseKiCADSchematic($data, $options), default => throw new InvalidArgumentException('Invalid import type!'), }; } - private function parseKiCADPCB(string $data, array $options = []): array + private function parseKiCADPCB(string $data): array { $csv = Reader::createFromString($data); $csv->setDelimiter(';'); @@ -113,17 +145,17 @@ class BOMImporter $entry = $this->normalizeColumnNames($entry); //Ensure that the entry has all required fields - if (!isset ($entry['Designator'])) { - throw new \UnexpectedValueException('Designator missing at line '.($offset + 1).'!'); + if (!isset($entry['Designator'])) { + throw new \UnexpectedValueException('Designator missing at line ' . ($offset + 1) . '!'); } - if (!isset ($entry['Package'])) { - throw new \UnexpectedValueException('Package missing at line '.($offset + 1).'!'); + if (!isset($entry['Package'])) { + throw new \UnexpectedValueException('Package missing at line ' . ($offset + 1) . '!'); } - if (!isset ($entry['Designation'])) { - throw new \UnexpectedValueException('Designation missing at line '.($offset + 1).'!'); + if (!isset($entry['Designation'])) { + throw new \UnexpectedValueException('Designation missing at line ' . ($offset + 1) . '!'); } - if (!isset ($entry['Quantity'])) { - throw new \UnexpectedValueException('Quantity missing at line '.($offset + 1).'!'); + if (!isset($entry['Quantity'])) { + throw new \UnexpectedValueException('Quantity missing at line ' . ($offset + 1) . '!'); } $bom_entry = new ProjectBOMEntry(); @@ -138,6 +170,63 @@ class BOMImporter return $bom_entries; } + /** + * Validate KiCad PCB data + */ + private function validateKiCADPCB(string $data): array + { + $csv = Reader::createFromString($data); + $csv->setDelimiter(';'); + $csv->setHeaderOffset(0); + + $mapped_entries = []; + + foreach ($csv->getRecords() as $offset => $entry) { + // Translate the german field names to english + $entry = $this->normalizeColumnNames($entry); + $mapped_entries[] = $entry; + } + + return $this->validationService->validateBOMEntries($mapped_entries); + } + + /** + * Validate KiCad schematic data + */ + private function validateKiCADSchematicData(string $data, array $options): array + { + $delimiter = $options['delimiter'] ?? ','; + $field_mapping = $options['field_mapping'] ?? []; + $field_priorities = $options['field_priorities'] ?? []; + + // Handle potential BOM (Byte Order Mark) at the beginning + $data = preg_replace('/^\xEF\xBB\xBF/', '', $data); + + $csv = Reader::createFromString($data); + $csv->setDelimiter($delimiter); + $csv->setHeaderOffset(0); + + // Handle quoted fields properly + $csv->setEscape('\\'); + $csv->setEnclosure('"'); + + $mapped_entries = []; + + foreach ($csv->getRecords() as $offset => $entry) { + // Apply field mapping to translate column names + $mapped_entry = $this->applyFieldMapping($entry, $field_mapping, $field_priorities); + + // Extract footprint package name if it contains library prefix + if (isset($mapped_entry['Package']) && str_contains($mapped_entry['Package'], ':')) { + $mapped_entry['Package'] = explode(':', $mapped_entry['Package'], 2)[1]; + } + + $mapped_entries[] = $mapped_entry; + } + + return $this->validationService->validateBOMEntries($mapped_entries, $options); + } + /** * This function uses the order of the fields in the CSV files to make them locale independent. * @param array $entry @@ -160,4 +249,482 @@ class BOMImporter return $out; } + + /** + * Parse KiCad schematic BOM with flexible field mapping + */ + private function parseKiCADSchematic(string $data, array $options = []): array + { + $delimiter = $options['delimiter'] ?? ','; + $field_mapping = $options['field_mapping'] ?? []; + $field_priorities = $options['field_priorities'] ?? []; + + // Handle potential BOM (Byte Order Mark) at the beginning + $data = preg_replace('/^\xEF\xBB\xBF/', '', $data); + + $csv = Reader::createFromString($data); + $csv->setDelimiter($delimiter); + $csv->setHeaderOffset(0); + + // Handle quoted fields properly + $csv->setEscape('\\'); + $csv->setEnclosure('"'); + + $bom_entries = []; + $entries_by_key = []; // Track entries by name+part combination + $mapped_entries = []; // Collect all mapped entries for validation + + foreach ($csv->getRecords() as $offset => $entry) { + // Apply field mapping to translate column names + $mapped_entry = $this->applyFieldMapping($entry, $field_mapping, $field_priorities); + + // Extract footprint package name if it contains library prefix + if (isset($mapped_entry['Package']) && str_contains($mapped_entry['Package'], ':')) { + $mapped_entry['Package'] = explode(':', $mapped_entry['Package'], 2)[1]; + } + + $mapped_entries[] = $mapped_entry; + } + + // Validate all entries before processing + $validation_result = $this->validationService->validateBOMEntries($mapped_entries, $options); + + // Log validation results + $this->logger->info('BOM import validation completed', [ + 'total_entries' => $validation_result['total_entries'], + 'valid_entries' => $validation_result['valid_entries'], + 'invalid_entries' => $validation_result['invalid_entries'], + 'error_count' => count($validation_result['errors']), + 'warning_count' => count($validation_result['warnings']), + ]); + + // If there are validation errors, throw an exception with detailed messages + if (!empty($validation_result['errors'])) { + $error_message = $this->validationService->getErrorMessage($validation_result); + throw new \UnexpectedValueException("BOM import validation failed:\n" . $error_message); + } + + // Process validated entries + foreach ($mapped_entries as $offset => $mapped_entry) { + + // Set name - prefer MPN, fall back to Value, then default format + $mpn = trim($mapped_entry['MPN'] ?? ''); + $designation = trim($mapped_entry['Designation'] ?? ''); + $value = trim($mapped_entry['Value'] ?? ''); + + // Use the first non-empty value, or 'Unknown Component' if all are empty + $name = ''; + if (!empty($mpn)) { + $name = $mpn; + } elseif (!empty($designation)) { + $name = $designation; + } elseif (!empty($value)) { + $name = $value; + } else { + $name = 'Unknown Component'; + } + + if (isset($mapped_entry['Package']) && !empty(trim($mapped_entry['Package']))) { + $name .= ' (' . trim($mapped_entry['Package']) . ')'; + } + + // Set mountnames and quantity + // The Designator field contains comma-separated mount names for all instances + $designator = trim($mapped_entry['Designator']); + $quantity = (float) $mapped_entry['Quantity']; + + // Get mountnames array (validation already ensured they match quantity) + $mountnames_array = array_map('trim', explode(',', $designator)); + + // Try to link existing Part-DB part if ID is provided + $part = null; + if (isset($mapped_entry['Part-DB ID']) && !empty($mapped_entry['Part-DB ID'])) { + $partDbId = (int) $mapped_entry['Part-DB ID']; + $existingPart = $this->entityManager->getRepository(Part::class)->find($partDbId); + + if ($existingPart) { + $part = $existingPart; + // Update name with actual part name + $name = $existingPart->getName(); + } + } + + // Create unique key for this entry (name + part ID) + $entry_key = $name . '|' . ($part ? $part->getID() : 'null'); + + // Check if we already have an entry with the same name and part + if (isset($entries_by_key[$entry_key])) { + // Merge with existing entry + $existing_entry = $entries_by_key[$entry_key]; + + // Combine mountnames + $existing_mountnames = $existing_entry->getMountnames(); + $combined_mountnames = $existing_mountnames . ',' . $designator; + $existing_entry->setMountnames($combined_mountnames); + + // Add quantities + $existing_quantity = $existing_entry->getQuantity(); + $existing_entry->setQuantity($existing_quantity + $quantity); + + $this->logger->info('Merged duplicate BOM entry', [ + 'name' => $name, + 'part_id' => $part ? $part->getID() : null, + 'original_quantity' => $existing_quantity, + 'added_quantity' => $quantity, + 'new_quantity' => $existing_quantity + $quantity, + 'original_mountnames' => $existing_mountnames, + 'added_mountnames' => $designator, + ]); + + continue; // Skip creating new entry + } + + // Create new BOM entry + $bom_entry = new ProjectBOMEntry(); + $bom_entry->setName($name); + $bom_entry->setMountnames($designator); + $bom_entry->setQuantity($quantity); + + if ($part) { + $bom_entry->setPart($part); + } + + // Set comment with additional info + $comment_parts = []; + if (isset($mapped_entry['Value']) && $mapped_entry['Value'] !== ($mapped_entry['MPN'] ?? '')) { + $comment_parts[] = 'Value: ' . $mapped_entry['Value']; + } + if (isset($mapped_entry['MPN'])) { + $comment_parts[] = 'MPN: ' . $mapped_entry['MPN']; + } + if (isset($mapped_entry['Manufacturer'])) { + $comment_parts[] = 'Manf: ' . $mapped_entry['Manufacturer']; + } + if (isset($mapped_entry['LCSC'])) { + $comment_parts[] = 'LCSC: ' . $mapped_entry['LCSC']; + } + if (isset($mapped_entry['Supplier and ref'])) { + $comment_parts[] = $mapped_entry['Supplier and ref']; + } + + if ($part) { + $comment_parts[] = "Part-DB ID: " . $part->getID(); + } elseif (isset($mapped_entry['Part-DB ID']) && !empty($mapped_entry['Part-DB ID'])) { + $comment_parts[] = "Part-DB ID: " . $mapped_entry['Part-DB ID'] . " (NOT FOUND)"; + } + + $bom_entry->setComment(implode(', ', $comment_parts)); + + $bom_entries[] = $bom_entry; + $entries_by_key[$entry_key] = $bom_entry; + } + + return $bom_entries; + } + + /** + * Get all available field mapping targets with descriptions + */ + public function getAvailableFieldTargets(): array + { + $targets = [ + 'Designator' => [ + 'label' => 'Designator', + 'description' => 'Component reference designators (e.g., R1, C2, U3)', + 'required' => true, + 'multiple' => false, + ], + 'Quantity' => [ + 'label' => 'Quantity', + 'description' => 'Number of components', + 'required' => true, + 'multiple' => false, + ], + 'Designation' => [ + 'label' => 'Designation', + 'description' => 'Component designation/part number', + 'required' => false, + 'multiple' => true, + ], + 'Value' => [ + 'label' => 'Value', + 'description' => 'Component value (e.g., 10k, 100nF)', + 'required' => false, + 'multiple' => true, + ], + 'Package' => [ + 'label' => 'Package', + 'description' => 'Component package/footprint', + 'required' => false, + 'multiple' => true, + ], + 'MPN' => [ + 'label' => 'MPN', + 'description' => 'Manufacturer Part Number', + 'required' => false, + 'multiple' => true, + ], + 'Manufacturer' => [ + 'label' => 'Manufacturer', + 'description' => 'Component manufacturer name', + 'required' => false, + 'multiple' => true, + ], + 'Part-DB ID' => [ + 'label' => 'Part-DB ID', + 'description' => 'Existing Part-DB part ID for linking', + 'required' => false, + 'multiple' => false, + ], + 'Comment' => [ + 'label' => 'Comment', + 'description' => 'Additional component information', + 'required' => false, + 'multiple' => true, + ], + ]; + + // Add dynamic supplier fields based on available suppliers in the database + $suppliers = $this->entityManager->getRepository(\App\Entity\Parts\Supplier::class)->findAll(); + foreach ($suppliers as $supplier) { + $supplierName = $supplier->getName(); + $targets[$supplierName . ' SPN'] = [ + 'label' => $supplierName . ' SPN', + 'description' => "Supplier part number for {$supplierName}", + 'required' => false, + 'multiple' => true, + 'supplier_id' => $supplier->getID(), + ]; + } + + return $targets; + } + + /** + * Get suggested field mappings based on common field names + */ + public function getSuggestedFieldMapping(array $detected_fields): array + { + $suggestions = []; + + $field_patterns = [ + 'Part-DB ID' => ['part-db id', 'partdb_id', 'part_db_id', 'db_id', 'partdb'], + 'Designator' => ['reference', 'ref', 'designator', 'component', 'comp'], + 'Quantity' => ['qty', 'quantity', 'count', 'number', 'amount'], + 'Value' => ['value', 'val', 'component_value'], + 'Designation' => ['designation', 'part_number', 'partnumber', 'part'], + 'Package' => ['footprint', 'package', 'housing', 'fp'], + 'MPN' => ['mpn', 'part_number', 'partnumber', 'manf#', 'mfr_part_number', 'manufacturer_part'], + 'Manufacturer' => ['manufacturer', 'manf', 'mfr', 'brand', 'vendor'], + 'Comment' => ['comment', 'comments', 'note', 'notes', 'description'], + ]; + + // Add supplier-specific patterns + $suppliers = $this->entityManager->getRepository(\App\Entity\Parts\Supplier::class)->findAll(); + foreach ($suppliers as $supplier) { + $supplierName = $supplier->getName(); + $supplierLower = strtolower($supplierName); + + // Create patterns for each supplier + $field_patterns[$supplierName . ' SPN'] = [ + $supplierLower, + $supplierLower . '#', + $supplierLower . '_part', + $supplierLower . '_number', + $supplierLower . 'pn', + $supplierLower . '_spn', + $supplierLower . ' spn', + // Common abbreviations + $supplierLower === 'mouser' ? 'mouser' : null, + $supplierLower === 'digikey' ? 'dk' : null, + $supplierLower === 'farnell' ? 'farnell' : null, + $supplierLower === 'rs' ? 'rs' : null, + $supplierLower === 'lcsc' ? 'lcsc' : null, + ]; + + // Remove null values + $field_patterns[$supplierName . ' SPN'] = array_filter($field_patterns[$supplierName . ' SPN'], fn($value) => $value !== null); + } + + foreach ($detected_fields as $field) { + $field_lower = strtolower(trim($field)); + + foreach ($field_patterns as $target => $patterns) { + foreach ($patterns as $pattern) { + if (str_contains($field_lower, $pattern)) { + $suggestions[$field] = $target; + break 2; // Break both loops + } + } + } + } + + return $suggestions; + } + + /** + * Validate field mapping configuration + */ + public function validateFieldMapping(array $field_mapping, array $detected_fields): array + { + $errors = []; + $warnings = []; + $available_targets = $this->getAvailableFieldTargets(); + + // Check for required fields + $mapped_targets = array_values($field_mapping); + $required_fields = ['Designator', 'Quantity']; + + foreach ($required_fields as $required) { + if (!in_array($required, $mapped_targets, true)) { + $errors[] = "Required field '{$required}' is not mapped from any CSV column."; + } + } + + // Check for invalid target fields + foreach ($field_mapping as $csv_field => $target) { + if (!empty($target) && !isset($available_targets[$target])) { + $errors[] = "Invalid target field '{$target}' for CSV field '{$csv_field}'."; + } + } + + // Check for unmapped fields (warnings) + $unmapped_fields = array_diff($detected_fields, array_keys($field_mapping)); + if (!empty($unmapped_fields)) { + $warnings[] = "The following CSV fields are not mapped: " . implode(', ', $unmapped_fields); + } + + return [ + 'errors' => $errors, + 'warnings' => $warnings, + 'is_valid' => empty($errors), + ]; + } + + /** + * Apply field mapping with support for multiple fields and priority + */ + private function applyFieldMapping(array $entry, array $field_mapping, array $field_priorities = []): array + { + $mapped = []; + $field_groups = []; + + // Group fields by target with priority information + foreach ($field_mapping as $csv_field => $target) { + if (!empty($target)) { + if (!isset($field_groups[$target])) { + $field_groups[$target] = []; + } + $priority = $field_priorities[$csv_field] ?? 10; + $field_groups[$target][] = [ + 'field' => $csv_field, + 'priority' => $priority, + 'value' => $entry[$csv_field] ?? '' + ]; + } + } + + // Process each target field + foreach ($field_groups as $target => $field_data) { + // Sort by priority (lower number = higher priority) + usort($field_data, function ($a, $b) { + return $a['priority'] <=> $b['priority']; + }); + + $values = []; + $non_empty_values = []; + + // Collect all non-empty values for this target + foreach ($field_data as $data) { + $value = trim($data['value']); + if (!empty($value)) { + $non_empty_values[] = $value; + } + $values[] = $value; + } + + // Use the first non-empty value (highest priority) + if (!empty($non_empty_values)) { + $mapped[$target] = $non_empty_values[0]; + + // If multiple non-empty values exist, add alternatives to comment + if (count($non_empty_values) > 1) { + $mapped[$target . '_alternatives'] = array_slice($non_empty_values, 1); + } + } + } + + return $mapped; + } + + /** + * Detect available fields in CSV data for field mapping UI + */ + public function detectFields(string $data, ?string $delimiter = null): array + { + if ($delimiter === null) { + // Detect delimiter by counting occurrences in the first row (header) + $delimiters = [',', ';', "\t"]; + $lines = explode("\n", $data, 2); + $header_line = $lines[0] ?? ''; + $delimiter_counts = []; + foreach ($delimiters as $delim) { + $delimiter_counts[$delim] = substr_count($header_line, $delim); + } + // Choose the delimiter with the highest count, default to comma if all are zero + $max_count = max($delimiter_counts); + $delimiter = array_search($max_count, $delimiter_counts, true); + if ($max_count === 0 || $delimiter === false) { + $delimiter = ','; + } + } + // Handle potential BOM (Byte Order Mark) at the beginning + $data = preg_replace('/^\xEF\xBB\xBF/', '', $data); + + // Get first line only for header detection + $lines = explode("\n", $data); + $header_line = trim($lines[0] ?? ''); + + + // Simple manual parsing for header detection + // This handles quoted CSV fields better than the library for detection + $fields = []; + $current_field = ''; + $in_quotes = false; + $quote_char = '"'; + + for ($i = 0; $i < strlen($header_line); $i++) { + $char = $header_line[$i]; + + if ($char === $quote_char && !$in_quotes) { + $in_quotes = true; + } elseif ($char === $quote_char && $in_quotes) { + // Check for escaped quote (double quote) + if ($i + 1 < strlen($header_line) && $header_line[$i + 1] === $quote_char) { + $current_field .= $quote_char; + $i++; // Skip next quote + } else { + $in_quotes = false; + } + } elseif ($char === $delimiter && !$in_quotes) { + $fields[] = trim($current_field); + $current_field = ''; + } else { + $current_field .= $char; + } + } + + // Add the last field + if ($current_field !== '') { + $fields[] = trim($current_field); + } + + // Clean up headers - remove quotes and trim whitespace + $headers = array_map(function ($header) { + return trim($header, '"\''); + }, $fields); + + + return array_values($headers); + } } diff --git a/src/Services/ImportExportSystem/BOMValidationService.php b/src/Services/ImportExportSystem/BOMValidationService.php new file mode 100644 index 00000000..74f81fe3 --- /dev/null +++ b/src/Services/ImportExportSystem/BOMValidationService.php @@ -0,0 +1,476 @@ +. + */ +namespace App\Services\ImportExportSystem; + +use App\Entity\Parts\Part; +use App\Entity\ProjectSystem\ProjectBOMEntry; +use Doctrine\ORM\EntityManagerInterface; +use Symfony\Contracts\Translation\TranslatorInterface; + +/** + * Service for validating BOM import data with comprehensive validation rules + * and user-friendly error messages. + */ +class BOMValidationService +{ + public function __construct( + private readonly EntityManagerInterface $entityManager, + private readonly TranslatorInterface $translator + ) { + } + + /** + * Validation result structure + */ + public static function createValidationResult(): array + { + return [ + 'errors' => [], + 'warnings' => [], + 'info' => [], + 'is_valid' => true, + 'total_entries' => 0, + 'valid_entries' => 0, + 'invalid_entries' => 0, + ]; + } + + /** + * Validate a single BOM entry with comprehensive checks + */ + public function validateBOMEntry(array $mapped_entry, int $line_number, array $options = []): array + { + $result = [ + 'line_number' => $line_number, + 'errors' => [], + 'warnings' => [], + 'info' => [], + 'is_valid' => true, + ]; + + // Run all validation rules + $this->validateRequiredFields($mapped_entry, $result); + $this->validateDesignatorFormat($mapped_entry, $result); + $this->validateQuantityFormat($mapped_entry, $result); + $this->validateDesignatorQuantityMatch($mapped_entry, $result); + $this->validatePartDBLink($mapped_entry, $result); + $this->validateComponentName($mapped_entry, $result); + $this->validatePackageFormat($mapped_entry, $result); + $this->validateNumericFields($mapped_entry, $result); + + $result['is_valid'] = empty($result['errors']); + + return $result; + } + + /** + * Validate multiple BOM entries and provide summary + */ + public function validateBOMEntries(array $mapped_entries, array $options = []): array + { + $result = self::createValidationResult(); + $result['total_entries'] = count($mapped_entries); + + $line_results = []; + $all_errors = []; + $all_warnings = []; + $all_info = []; + + foreach ($mapped_entries as $index => $entry) { + $line_number = $index + 1; + $line_result = $this->validateBOMEntry($entry, $line_number, $options); + + $line_results[] = $line_result; + + if ($line_result['is_valid']) { + $result['valid_entries']++; + } else { + $result['invalid_entries']++; + } + + // Collect all messages + $all_errors = array_merge($all_errors, $line_result['errors']); + $all_warnings = array_merge($all_warnings, $line_result['warnings']); + $all_info = array_merge($all_info, $line_result['info']); + } + + // Add summary messages + $this->addSummaryMessages($result, $all_errors, $all_warnings, $all_info); + + $result['errors'] = $all_errors; + $result['warnings'] = $all_warnings; + $result['info'] = $all_info; + $result['line_results'] = $line_results; + $result['is_valid'] = empty($all_errors); + + return $result; + } + + /** + * Validate required fields are present + */ + private function validateRequiredFields(array $entry, array &$result): void + { + $required_fields = ['Designator', 'Quantity']; + + foreach ($required_fields as $field) { + if (!isset($entry[$field]) || trim($entry[$field]) === '') { + $result['errors'][] = $this->translator->trans('project.bom_import.validation.errors.required_field_missing', [ + '%line%' => $result['line_number'], + '%field%' => $field + ]); + } + } + } + + /** + * Validate designator format and content + */ + private function validateDesignatorFormat(array $entry, array &$result): void + { + if (!isset($entry['Designator']) || trim($entry['Designator']) === '') { + return; // Already handled by required fields validation + } + + $designator = trim($entry['Designator']); + $mountnames = array_map('trim', explode(',', $designator)); + + // Remove empty entries + $mountnames = array_filter($mountnames, fn($name) => !empty($name)); + + if (empty($mountnames)) { + $result['errors'][] = $this->translator->trans('project.bom_import.validation.errors.no_valid_designators', [ + '%line%' => $result['line_number'] + ]); + return; + } + + // Validate each mountname format (allow 1-2 uppercase letters, followed by 1+ digits) + $invalid_mountnames = []; + foreach ($mountnames as $mountname) { + if (!preg_match('/^[A-Z]{1,2}[0-9]+$/', $mountname)) { + $invalid_mountnames[] = $mountname; + } + } + + if (!empty($invalid_mountnames)) { + $result['warnings'][] = $this->translator->trans('project.bom_import.validation.warnings.unusual_designator_format', [ + '%line%' => $result['line_number'], + '%designators%' => implode(', ', $invalid_mountnames) + ]); + } + + // Check for duplicate mountnames within the same line + $duplicates = array_diff_assoc($mountnames, array_unique($mountnames)); + if (!empty($duplicates)) { + $result['errors'][] = $this->translator->trans('project.bom_import.validation.errors.duplicate_designators', [ + '%line%' => $result['line_number'], + '%designators%' => implode(', ', array_unique($duplicates)) + ]); + } + } + + /** + * Validate quantity format and value + */ + private function validateQuantityFormat(array $entry, array &$result): void + { + if (!isset($entry['Quantity']) || trim($entry['Quantity']) === '') { + return; // Already handled by required fields validation + } + + $quantity_str = trim($entry['Quantity']); + + // Check if it's a valid number + if (!is_numeric($quantity_str)) { + $result['errors'][] = $this->translator->trans('project.bom_import.validation.errors.invalid_quantity', [ + '%line%' => $result['line_number'], + '%quantity%' => $quantity_str + ]); + return; + } + + $quantity = (float) $quantity_str; + + // Check for reasonable quantity values + if ($quantity <= 0) { + $result['errors'][] = $this->translator->trans('project.bom_import.validation.errors.quantity_zero_or_negative', [ + '%line%' => $result['line_number'], + '%quantity%' => $quantity_str + ]); + } elseif ($quantity > 10000) { + $result['warnings'][] = $this->translator->trans('project.bom_import.validation.warnings.quantity_unusually_high', [ + '%line%' => $result['line_number'], + '%quantity%' => $quantity_str + ]); + } + + // Check if quantity is a whole number when it should be + if (isset($entry['Designator'])) { + $designator = trim($entry['Designator']); + $mountnames = array_map('trim', explode(',', $designator)); + $mountnames = array_filter($mountnames, fn($name) => !empty($name)); + + if (count($mountnames) > 0 && $quantity != (int) $quantity) { + $result['warnings'][] = $this->translator->trans('project.bom_import.validation.warnings.quantity_not_whole_number', [ + '%line%' => $result['line_number'], + '%quantity%' => $quantity_str, + '%count%' => count($mountnames) + ]); + } + } + } + + /** + * Validate that designator count matches quantity + */ + private function validateDesignatorQuantityMatch(array $entry, array &$result): void + { + if (!isset($entry['Designator']) || !isset($entry['Quantity'])) { + return; // Already handled by required fields validation + } + + $designator = trim($entry['Designator']); + $quantity_str = trim($entry['Quantity']); + + if (!is_numeric($quantity_str)) { + return; // Already handled by quantity validation + } + + $mountnames = array_map('trim', explode(',', $designator)); + $mountnames = array_filter($mountnames, fn($name) => !empty($name)); + $mountnames_count = count($mountnames); + $quantity = (float) $quantity_str; + + if ($mountnames_count !== (int) $quantity) { + $result['errors'][] = $this->translator->trans('project.bom_import.validation.errors.quantity_designator_mismatch', [ + '%line%' => $result['line_number'], + '%quantity%' => $quantity_str, + '%count%' => $mountnames_count, + '%designators%' => $designator + ]); + } + } + + /** + * Validate Part-DB ID link + */ + private function validatePartDBLink(array $entry, array &$result): void + { + if (!isset($entry['Part-DB ID']) || trim($entry['Part-DB ID']) === '') { + return; + } + + $part_db_id = trim($entry['Part-DB ID']); + + if (!is_numeric($part_db_id)) { + $result['errors'][] = $this->translator->trans('project.bom_import.validation.errors.invalid_partdb_id', [ + '%line%' => $result['line_number'], + '%id%' => $part_db_id + ]); + return; + } + + $part_id = (int) $part_db_id; + + if ($part_id <= 0) { + $result['errors'][] = $this->translator->trans('project.bom_import.validation.errors.partdb_id_zero_or_negative', [ + '%line%' => $result['line_number'], + '%id%' => $part_id + ]); + return; + } + + // Check if part exists in database + $existing_part = $this->entityManager->getRepository(Part::class)->find($part_id); + if (!$existing_part) { + $result['warnings'][] = $this->translator->trans('project.bom_import.validation.warnings.partdb_id_not_found', [ + '%line%' => $result['line_number'], + '%id%' => $part_id + ]); + } else { + $result['info'][] = $this->translator->trans('project.bom_import.validation.info.partdb_link_success', [ + '%line%' => $result['line_number'], + '%name%' => $existing_part->getName(), + '%id%' => $part_id + ]); + } + } + + /** + * Validate component name/designation + */ + private function validateComponentName(array $entry, array &$result): void + { + $name_fields = ['MPN', 'Designation', 'Value']; + $has_name = false; + + foreach ($name_fields as $field) { + if (isset($entry[$field]) && trim($entry[$field]) !== '') { + $has_name = true; + break; + } + } + + if (!$has_name) { + $result['warnings'][] = $this->translator->trans('project.bom_import.validation.warnings.no_component_name', [ + '%line%' => $result['line_number'] + ]); + } + } + + /** + * Validate package format + */ + private function validatePackageFormat(array $entry, array &$result): void + { + if (!isset($entry['Package']) || trim($entry['Package']) === '') { + return; + } + + $package = trim($entry['Package']); + + // Check for common package format issues + if (strlen($package) > 100) { + $result['warnings'][] = $this->translator->trans('project.bom_import.validation.warnings.package_name_too_long', [ + '%line%' => $result['line_number'], + '%package%' => $package + ]); + } + + // Check for library prefixes (KiCad format) + if (str_contains($package, ':')) { + $result['info'][] = $this->translator->trans('project.bom_import.validation.info.library_prefix_detected', [ + '%line%' => $result['line_number'], + '%package%' => $package + ]); + } + } + + /** + * Validate numeric fields + */ + private function validateNumericFields(array $entry, array &$result): void + { + $numeric_fields = ['Quantity', 'Part-DB ID']; + + foreach ($numeric_fields as $field) { + if (isset($entry[$field]) && trim($entry[$field]) !== '') { + $value = trim($entry[$field]); + if (!is_numeric($value)) { + $result['errors'][] = $this->translator->trans('project.bom_import.validation.errors.non_numeric_field', [ + '%line%' => $result['line_number'], + '%field%' => $field, + '%value%' => $value + ]); + } + } + } + } + + /** + * Add summary messages to validation result + */ + private function addSummaryMessages(array &$result, array $errors, array $warnings, array $info): void + { + $total_entries = $result['total_entries']; + $valid_entries = $result['valid_entries']; + $invalid_entries = $result['invalid_entries']; + + // Add summary info + if ($total_entries > 0) { + $result['info'][] = $this->translator->trans('project.bom_import.validation.info.import_summary', [ + '%total%' => $total_entries, + '%valid%' => $valid_entries, + '%invalid%' => $invalid_entries + ]); + } + + // Add error summary + if (!empty($errors)) { + $error_count = count($errors); + $result['errors'][] = $this->translator->trans('project.bom_import.validation.errors.summary', [ + '%count%' => $error_count + ]); + } + + // Add warning summary + if (!empty($warnings)) { + $warning_count = count($warnings); + $result['warnings'][] = $this->translator->trans('project.bom_import.validation.warnings.summary', [ + '%count%' => $warning_count + ]); + } + + // Add success message if all entries are valid + if ($total_entries > 0 && $invalid_entries === 0) { + $result['info'][] = $this->translator->trans('project.bom_import.validation.info.all_valid'); + } + } + + /** + * Get user-friendly error message for a validation result + */ + public function getErrorMessage(array $validation_result): string + { + if ($validation_result['is_valid']) { + return ''; + } + + $messages = []; + + if (!empty($validation_result['errors'])) { + $messages[] = 'Errors:'; + foreach ($validation_result['errors'] as $error) { + $messages[] = '• ' . $error; + } + } + + if (!empty($validation_result['warnings'])) { + $messages[] = 'Warnings:'; + foreach ($validation_result['warnings'] as $warning) { + $messages[] = '• ' . $warning; + } + } + + return implode("\n", $messages); + } + + /** + * Get validation statistics + */ + public function getValidationStats(array $validation_result): array + { + return [ + 'total_entries' => $validation_result['total_entries'] ?? 0, + 'valid_entries' => $validation_result['valid_entries'] ?? 0, + 'invalid_entries' => $validation_result['invalid_entries'] ?? 0, + 'error_count' => count($validation_result['errors'] ?? []), + 'warning_count' => count($validation_result['warnings'] ?? []), + 'info_count' => count($validation_result['info'] ?? []), + 'success_rate' => $validation_result['total_entries'] > 0 + ? round(($validation_result['valid_entries'] / $validation_result['total_entries']) * 100, 1) + : 0, + ]; + } +} \ No newline at end of file diff --git a/templates/projects/_bom_validation_results.html.twig b/templates/projects/_bom_validation_results.html.twig new file mode 100644 index 00000000..68f1b827 --- /dev/null +++ b/templates/projects/_bom_validation_results.html.twig @@ -0,0 +1,186 @@ +{# BOM Validation Results Component #} +{# + Usage: + {% include 'projects/_bom_validation_results.html.twig' with { + validation_result: validation_result, + show_summary: true, + show_details: true + } %} +#} + +{% if validation_result is defined and validation_result is not empty %} + {% set stats = validation_result %} + + {# Validation Summary #} + {% if show_summary is defined and show_summary %} +
+
+
+
+
+ + {% trans %}project.bom_import.validation.summary{% endtrans %} +
+
+
+
+
+
+
{{ stats.total_entries }}
+ {% trans %}project.bom_import.validation.total_entries{% endtrans %} +
+
+
+
+
{{ stats.valid_entries }}
+ {% trans %}project.bom_import.validation.valid_entries{% endtrans %} +
+
+
+
+
{{ stats.invalid_entries }}
+ {% trans %}project.bom_import.validation.invalid_entries{% endtrans %} +
+
+
+
+
+ {% if stats.total_entries > 0 %} + {{ ((stats.valid_entries / stats.total_entries) * 100) | round(1) }}% + {% else %} + 0% + {% endif %} +
+ {% trans %}project.bom_import.validation.success_rate{% endtrans %} +
+
+
+
+
+
+
+ {% endif %} + + {# Validation Messages #} + {% if validation_result.errors is defined and validation_result.errors is not empty %} +
+

{% trans %}project.bom_import.validation.errors.title{% endtrans %}

+

{% trans %}project.bom_import.validation.errors.description{% endtrans %}

+
    + {% for error in validation_result.errors %} +
  • {{ error|raw }}
  • + {% endfor %} +
+
+ {% endif %} + + {% if validation_result.warnings is defined and validation_result.warnings is not empty %} +
+

{% trans %}project.bom_import.validation.warnings.title{% endtrans %}

+

{% trans %}project.bom_import.validation.warnings.description{% endtrans %}

+
    + {% for warning in validation_result.warnings %} +
  • {{ warning|raw }}
  • + {% endfor %} +
+
+ {% endif %} + + {% if validation_result.info is defined and validation_result.info is not empty %} +
+

{% trans %}project.bom_import.validation.info.title{% endtrans %}

+
    + {% for info in validation_result.info %} +
  • {{ info|raw }}
  • + {% endfor %} +
+
+ {% endif %} + + {# Detailed Line-by-Line Results #} + {% if show_details is defined and show_details and validation_result.line_results is defined %} +
+
+
+ + {% trans %}project.bom_import.validation.details.title{% endtrans %} +
+
+
+
+ + + + + + + + + + {% for line_result in validation_result.line_results %} + + + + + + {% endfor %} + +
{% trans %}project.bom_import.validation.details.line{% endtrans %}{% trans %}project.bom_import.validation.details.status{% endtrans %}{% trans %}project.bom_import.validation.details.messages{% endtrans %}
+ {{ line_result.line_number }} + + {% if line_result.is_valid %} + + + {% trans %}project.bom_import.validation.details.valid{% endtrans %} + + {% else %} + + + {% trans %}project.bom_import.validation.details.invalid{% endtrans %} + + {% endif %} + + {% if line_result.errors is not empty %} +
+ {% for error in line_result.errors %} +
{{ error|raw }}
+ {% endfor %} +
+ {% endif %} + {% if line_result.warnings is not empty %} +
+ {% for warning in line_result.warnings %} +
{{ warning|raw }}
+ {% endfor %} +
+ {% endif %} + {% if line_result.info is not empty %} +
+ {% for info in line_result.info %} +
{{ info|raw }}
+ {% endfor %} +
+ {% endif %} +
+
+
+
+ {% endif %} + + {# Action Buttons #} + {% if validation_result.is_valid is defined %} +
+ {% if validation_result.is_valid %} +
+ + {% trans %}project.bom_import.validation.all_valid{% endtrans %} +
+ {% else %} +
+ + {% trans %}project.bom_import.validation.fix_errors{% endtrans %} +
+ {% endif %} +
+ {% endif %} +{% endif %} \ No newline at end of file diff --git a/templates/projects/import_bom_map_fields.html.twig b/templates/projects/import_bom_map_fields.html.twig new file mode 100644 index 00000000..ba10c9c5 --- /dev/null +++ b/templates/projects/import_bom_map_fields.html.twig @@ -0,0 +1,204 @@ +{% extends "main_card.html.twig" %} + +{% block title %}{% trans %}project.bom_import.map_fields{% endtrans %}{% endblock %} + +{% block card_title %} + + {% trans %}project.bom_import.map_fields{% endtrans %}{% if project %}: {{ project.name }}{% endif %} +{% endblock %} + +{% block card_content %} + {% if validation_result is defined %} + {% include 'projects/_bom_validation_results.html.twig' with { + validation_result: validation_result, + show_summary: true, + show_details: false + } %} + {% endif %} + +
+
+
+ + {% trans %}project.bom_import.map_fields.help{% endtrans %} +
+
+ + {% trans %}project.bom_import.field_mapping.priority_note{% endtrans %} +
+
+
+ + {{ form_start(form) }} + +
+
+ {{ form_row(form.delimiter) }} +
+
+ +
+
+
+ + {% trans %}project.bom_import.field_mapping.title{% endtrans %} +
+
+
+
+ + + + + + + + + + + {% for field in detected_fields %} + + + + + + + {% endfor %} + +
{% trans %}project.bom_import.field_mapping.csv_field{% endtrans %}{% trans %}project.bom_import.field_mapping.maps_to{% endtrans %}{% trans %}project.bom_import.field_mapping.suggestion{% endtrans %}{% trans %}project.bom_import.field_mapping.priority{% endtrans %}
+ {{ field }} + + {{ form_widget(form['mapping_' ~ field_name_mapping[field]], { + 'attr': { + 'class': 'form-select field-mapping-select', + 'data-field': field + } + }) }} + + {% if suggested_mapping[field] is defined %} + + + {{ suggested_mapping[field] }} + + {% else %} + + + {% trans %}project.bom_import.field_mapping.no_suggestion{% endtrans %} + + {% endif %} + + +
+
+ +
+
{% trans %}project.bom_import.field_mapping.summary{% endtrans %}:
+
+ + {% trans %}project.bom_import.field_mapping.select_to_see_summary{% endtrans %} +
+
+
+
+ +
+ {{ form_widget(form.submit, { + 'attr': { + 'class': 'btn btn-primary' + } + }) }} + + + {% trans %}common.back{% endtrans %} + +
+ + {{ form_end(form) }} + + +{% endblock %} \ No newline at end of file From 7c1ab6460d05e4d30e0707217c8b3ca67210c7bb Mon Sep 17 00:00:00 2001 From: barisgit Date: Sun, 3 Aug 2025 18:58:31 +0200 Subject: [PATCH 063/228] Add tests to cover new additions --- makefile | 4 + .../ImportExportSystem/BOMImporterTest.php | 494 ++++++++++++++++++ .../BOMValidationServiceTest.php | 349 +++++++++++++ 3 files changed, 847 insertions(+) create mode 100644 tests/Services/ImportExportSystem/BOMValidationServiceTest.php diff --git a/makefile b/makefile index 6b5ac61f..9041ba0f 100644 --- a/makefile +++ b/makefile @@ -73,6 +73,10 @@ test-run: @echo "🧪 Running tests..." php bin/phpunit +test-typecheck: + @echo "🧪 Running type checks..." + COMPOSER_MEMORY_LIMIT=-1 composer phpstan + # Quick test reset (clean + migrate + fixtures, skip DB creation) test-reset: test-cache-clear test-db-migrate test-fixtures @echo "✅ Test environment reset complete!" diff --git a/tests/Services/ImportExportSystem/BOMImporterTest.php b/tests/Services/ImportExportSystem/BOMImporterTest.php index b9aba1d4..52c633d0 100644 --- a/tests/Services/ImportExportSystem/BOMImporterTest.php +++ b/tests/Services/ImportExportSystem/BOMImporterTest.php @@ -22,9 +22,12 @@ declare(strict_types=1); */ namespace App\Tests\Services\ImportExportSystem; +use App\Entity\Parts\Part; +use App\Entity\Parts\Supplier; use App\Entity\ProjectSystem\Project; use App\Entity\ProjectSystem\ProjectBOMEntry; use App\Services\ImportExportSystem\BOMImporter; +use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; use Symfony\Component\HttpFoundation\File\File; @@ -36,11 +39,17 @@ class BOMImporterTest extends WebTestCase */ protected $service; + /** + * @var EntityManagerInterface + */ + protected $entityManager; + protected function setUp(): void { //Get a service instance. self::bootKernel(); $this->service = self::getContainer()->get(BOMImporter::class); + $this->entityManager = self::getContainer()->get(EntityManagerInterface::class); } public function testImportFileIntoProject(): void @@ -119,4 +128,489 @@ class BOMImporterTest extends WebTestCase $this->service->stringToBOMEntries($input, ['type' => 'kicad_pcbnew']); } + + public function testDetectFields(): void + { + $input = <<service->detectFields($input); + + $this->assertIsArray($fields); + $this->assertCount(8, $fields); + $this->assertContains('Reference', $fields); + $this->assertContains('Value', $fields); + $this->assertContains('Footprint', $fields); + $this->assertContains('Quantity', $fields); + $this->assertContains('MPN', $fields); + $this->assertContains('Manufacturer', $fields); + $this->assertContains('LCSC SPN', $fields); + $this->assertContains('Mouser SPN', $fields); + } + + public function testDetectFieldsWithQuotes(): void + { + $input = <<service->detectFields($input); + + $this->assertIsArray($fields); + $this->assertCount(8, $fields); + $this->assertEquals('Reference', $fields[0]); + $this->assertEquals('Value', $fields[1]); + } + + public function testDetectFieldsWithSemicolon(): void + { + $input = <<service->detectFields($input, ';'); + + $this->assertIsArray($fields); + $this->assertCount(8, $fields); + $this->assertEquals('Reference', $fields[0]); + $this->assertEquals('Value', $fields[1]); + } + + public function testGetAvailableFieldTargets(): void + { + $targets = $this->service->getAvailableFieldTargets(); + + $this->assertIsArray($targets); + $this->assertArrayHasKey('Designator', $targets); + $this->assertArrayHasKey('Quantity', $targets); + $this->assertArrayHasKey('Value', $targets); + $this->assertArrayHasKey('Package', $targets); + $this->assertArrayHasKey('MPN', $targets); + $this->assertArrayHasKey('Manufacturer', $targets); + $this->assertArrayHasKey('Part-DB ID', $targets); + $this->assertArrayHasKey('Comment', $targets); + + // Check structure of a target + $this->assertArrayHasKey('label', $targets['Designator']); + $this->assertArrayHasKey('description', $targets['Designator']); + $this->assertArrayHasKey('required', $targets['Designator']); + $this->assertArrayHasKey('multiple', $targets['Designator']); + + $this->assertTrue($targets['Designator']['required']); + $this->assertTrue($targets['Quantity']['required']); + $this->assertFalse($targets['Value']['required']); + } + + public function testGetAvailableFieldTargetsWithSuppliers(): void + { + // Create test suppliers + $supplier1 = new Supplier(); + $supplier1->setName('LCSC'); + $supplier2 = new Supplier(); + $supplier2->setName('Mouser'); + + $this->entityManager->persist($supplier1); + $this->entityManager->persist($supplier2); + $this->entityManager->flush(); + + $targets = $this->service->getAvailableFieldTargets(); + + $this->assertArrayHasKey('LCSC SPN', $targets); + $this->assertArrayHasKey('Mouser SPN', $targets); + + $this->assertEquals('LCSC SPN', $targets['LCSC SPN']['label']); + $this->assertEquals('Mouser SPN', $targets['Mouser SPN']['label']); + $this->assertFalse($targets['LCSC SPN']['required']); + $this->assertTrue($targets['LCSC SPN']['multiple']); + + // Clean up + $this->entityManager->remove($supplier1); + $this->entityManager->remove($supplier2); + $this->entityManager->flush(); + } + + public function testGetSuggestedFieldMapping(): void + { + $detected_fields = [ + 'Reference', + 'Value', + 'Footprint', + 'Quantity', + 'MPN', + 'Manufacturer', + 'LCSC', + 'Mouser', + 'Part-DB ID', + 'Comment' + ]; + + $suggestions = $this->service->getSuggestedFieldMapping($detected_fields); + + $this->assertIsArray($suggestions); + $this->assertEquals('Designator', $suggestions['Reference']); + $this->assertEquals('Value', $suggestions['Value']); + $this->assertEquals('Package', $suggestions['Footprint']); + $this->assertEquals('Quantity', $suggestions['Quantity']); + $this->assertEquals('MPN', $suggestions['MPN']); + $this->assertEquals('Manufacturer', $suggestions['Manufacturer']); + $this->assertEquals('Part-DB ID', $suggestions['Part-DB ID']); + $this->assertEquals('Comment', $suggestions['Comment']); + } + + public function testGetSuggestedFieldMappingWithSuppliers(): void + { + // Create test suppliers + $supplier1 = new Supplier(); + $supplier1->setName('LCSC'); + $supplier2 = new Supplier(); + $supplier2->setName('Mouser'); + + $this->entityManager->persist($supplier1); + $this->entityManager->persist($supplier2); + $this->entityManager->flush(); + + $detected_fields = [ + 'Reference', + 'LCSC', + 'Mouser', + 'lcsc_part', + 'mouser_spn' + ]; + + $suggestions = $this->service->getSuggestedFieldMapping($detected_fields); + + $this->assertIsArray($suggestions); + $this->assertEquals('Designator', $suggestions['Reference']); + // Note: The exact mapping depends on the pattern matching logic + // We just check that supplier fields are mapped to something + $this->assertArrayHasKey('LCSC', $suggestions); + $this->assertArrayHasKey('Mouser', $suggestions); + $this->assertArrayHasKey('lcsc_part', $suggestions); + $this->assertArrayHasKey('mouser_spn', $suggestions); + + // Clean up + $this->entityManager->remove($supplier1); + $this->entityManager->remove($supplier2); + $this->entityManager->flush(); + } + + public function testValidateFieldMappingValid(): void + { + $field_mapping = [ + 'Reference' => 'Designator', + 'Quantity' => 'Quantity', + 'Value' => 'Value' + ]; + + $detected_fields = ['Reference', 'Quantity', 'Value', 'MPN']; + + $result = $this->service->validateFieldMapping($field_mapping, $detected_fields); + + $this->assertIsArray($result); + $this->assertArrayHasKey('errors', $result); + $this->assertArrayHasKey('warnings', $result); + $this->assertArrayHasKey('is_valid', $result); + + $this->assertTrue($result['is_valid']); + $this->assertEmpty($result['errors']); + $this->assertNotEmpty($result['warnings']); // Should warn about unmapped MPN + } + + public function testValidateFieldMappingMissingRequired(): void + { + $field_mapping = [ + 'Value' => 'Value', + 'MPN' => 'MPN' + ]; + + $detected_fields = ['Value', 'MPN']; + + $result = $this->service->validateFieldMapping($field_mapping, $detected_fields); + + $this->assertFalse($result['is_valid']); + $this->assertNotEmpty($result['errors']); + $this->assertContains("Required field 'Designator' is not mapped from any CSV column.", $result['errors']); + $this->assertContains("Required field 'Quantity' is not mapped from any CSV column.", $result['errors']); + } + + public function testValidateFieldMappingInvalidTarget(): void + { + $field_mapping = [ + 'Reference' => 'Designator', + 'Quantity' => 'Quantity', + 'Value' => 'InvalidTarget' + ]; + + $detected_fields = ['Reference', 'Quantity', 'Value']; + + $result = $this->service->validateFieldMapping($field_mapping, $detected_fields); + + $this->assertFalse($result['is_valid']); + $this->assertNotEmpty($result['errors']); + $this->assertContains("Invalid target field 'InvalidTarget' for CSV field 'Value'.", $result['errors']); + } + + public function testStringToBOMEntriesKiCADSchematic(): void + { + $input = << 'Designator', + 'Value' => 'Value', + 'Footprint' => 'Package', + 'Quantity' => 'Quantity', + 'MPN' => 'MPN', + 'Manufacturer' => 'Manufacturer', + 'LCSC SPN' => 'LCSC SPN', + 'Mouser SPN' => 'Mouser SPN' + ]; + + $bom_entries = $this->service->stringToBOMEntries($input, [ + 'type' => 'kicad_schematic', + 'field_mapping' => $field_mapping, + 'delimiter' => ',' + ]); + + $this->assertContainsOnlyInstancesOf(ProjectBOMEntry::class, $bom_entries); + $this->assertCount(2, $bom_entries); + + // Check first entry + $this->assertEquals('R1,R2', $bom_entries[0]->getMountnames()); + $this->assertEquals(2.0, $bom_entries[0]->getQuantity()); + $this->assertEquals('CRCW080510K0FKEA (R_0805_2012Metric)', $bom_entries[0]->getName()); + $this->assertStringContainsString('Value: 10k', $bom_entries[0]->getComment()); + $this->assertStringContainsString('MPN: CRCW080510K0FKEA', $bom_entries[0]->getComment()); + $this->assertStringContainsString('Manf: Vishay', $bom_entries[0]->getComment()); + + // Check second entry + $this->assertEquals('C1', $bom_entries[1]->getMountnames()); + $this->assertEquals(1.0, $bom_entries[1]->getQuantity()); + } + + public function testStringToBOMEntriesKiCADSchematicWithPriority(): void + { + $input = << 'Designator', + 'Value' => 'Value', + 'MPN1' => 'MPN', + 'MPN2' => 'MPN', + 'Quantity' => 'Quantity' + ]; + + $field_priorities = [ + 'MPN1' => 1, + 'MPN2' => 2 + ]; + + $bom_entries = $this->service->stringToBOMEntries($input, [ + 'type' => 'kicad_schematic', + 'field_mapping' => $field_mapping, + 'field_priorities' => $field_priorities, + 'delimiter' => ',' + ]); + + $this->assertContainsOnlyInstancesOf(ProjectBOMEntry::class, $bom_entries); + $this->assertCount(2, $bom_entries); + + // First entry should use MPN1 (higher priority) + $this->assertEquals('CRCW080510K0FKEA', $bom_entries[0]->getName()); + + // Second entry should use MPN2 (MPN1 is empty) + $this->assertEquals('CL21A104KOCLRNC', $bom_entries[1]->getName()); + } + + public function testStringToBOMEntriesKiCADSchematicWithPartDBID(): void + { + // Create a test part with required fields + $part = new Part(); + $part->setName('Test Part'); + $part->setCategory($this->getDefaultCategory($this->entityManager)); + $this->entityManager->persist($part); + $this->entityManager->flush(); + + $input = <<getID()}","2" + CSV; + + $field_mapping = [ + 'Reference' => 'Designator', + 'Value' => 'Value', + 'Part-DB ID' => 'Part-DB ID', + 'Quantity' => 'Quantity' + ]; + + $bom_entries = $this->service->stringToBOMEntries($input, [ + 'type' => 'kicad_schematic', + 'field_mapping' => $field_mapping, + 'delimiter' => ',' + ]); + + $this->assertContainsOnlyInstancesOf(ProjectBOMEntry::class, $bom_entries); + $this->assertCount(1, $bom_entries); + + $this->assertEquals('Test Part', $bom_entries[0]->getName()); + $this->assertSame($part, $bom_entries[0]->getPart()); + $this->assertStringContainsString("Part-DB ID: {$part->getID()}", $bom_entries[0]->getComment()); + + // Clean up + $this->entityManager->remove($part); + $this->entityManager->flush(); + } + + public function testStringToBOMEntriesKiCADSchematicWithInvalidPartDBID(): void + { + $input = << 'Designator', + 'Value' => 'Value', + 'Part-DB ID' => 'Part-DB ID', + 'Quantity' => 'Quantity' + ]; + + $bom_entries = $this->service->stringToBOMEntries($input, [ + 'type' => 'kicad_schematic', + 'field_mapping' => $field_mapping, + 'delimiter' => ',' + ]); + + $this->assertContainsOnlyInstancesOf(ProjectBOMEntry::class, $bom_entries); + $this->assertCount(1, $bom_entries); + + $this->assertEquals('10k', $bom_entries[0]->getName()); // Should use Value as name + $this->assertNull($bom_entries[0]->getPart()); // Should not link to part + $this->assertStringContainsString("Part-DB ID: 99999 (NOT FOUND)", $bom_entries[0]->getComment()); + } + + public function testStringToBOMEntriesKiCADSchematicMergeDuplicates(): void + { + $input = << 'Designator', + 'Value' => 'Value', + 'MPN' => 'MPN', + 'Quantity' => 'Quantity' + ]; + + $bom_entries = $this->service->stringToBOMEntries($input, [ + 'type' => 'kicad_schematic', + 'field_mapping' => $field_mapping, + 'delimiter' => ',' + ]); + + $this->assertContainsOnlyInstancesOf(ProjectBOMEntry::class, $bom_entries); + $this->assertCount(1, $bom_entries); // Should merge into one entry + + $this->assertEquals('R1,R2', $bom_entries[0]->getMountnames()); + $this->assertEquals(2.0, $bom_entries[0]->getQuantity()); + $this->assertEquals('CRCW080510K0FKEA', $bom_entries[0]->getName()); + } + + public function testStringToBOMEntriesKiCADSchematicMissingRequired(): void + { + $input = << 'Value', + 'MPN' => 'MPN' + ]; + + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessage('Required field "Designator" is missing or empty'); + + $this->service->stringToBOMEntries($input, [ + 'type' => 'kicad_schematic', + 'field_mapping' => $field_mapping, + 'delimiter' => ',' + ]); + } + + public function testStringToBOMEntriesKiCADSchematicQuantityMismatch(): void + { + $input = << 'Designator', + 'Value' => 'Value', + 'Quantity' => 'Quantity' + ]; + + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessage('Mismatch between quantity and component references'); + + $this->service->stringToBOMEntries($input, [ + 'type' => 'kicad_schematic', + 'field_mapping' => $field_mapping, + 'delimiter' => ',' + ]); + } + + public function testStringToBOMEntriesKiCADSchematicWithBOM(): void + { + // Test with BOM (Byte Order Mark) + $input = "\xEF\xBB\xBF" . << 'Designator', + 'Value' => 'Value', + 'Quantity' => 'Quantity' + ]; + + $bom_entries = $this->service->stringToBOMEntries($input, [ + 'type' => 'kicad_schematic', + 'field_mapping' => $field_mapping, + 'delimiter' => ',' + ]); + + $this->assertContainsOnlyInstancesOf(ProjectBOMEntry::class, $bom_entries); + $this->assertCount(1, $bom_entries); + $this->assertEquals('R1,R2', $bom_entries[0]->getMountnames()); + } + + private function getDefaultCategory(EntityManagerInterface $entityManager) + { + // Get the first available category or create a default one + $categoryRepo = $entityManager->getRepository(\App\Entity\Parts\Category::class); + $categories = $categoryRepo->findAll(); + + if (empty($categories)) { + // Create a default category if none exists + $category = new \App\Entity\Parts\Category(); + $category->setName('Default Category'); + $entityManager->persist($category); + $entityManager->flush(); + return $category; + } + + return $categories[0]; + } } diff --git a/tests/Services/ImportExportSystem/BOMValidationServiceTest.php b/tests/Services/ImportExportSystem/BOMValidationServiceTest.php new file mode 100644 index 00000000..055db8b4 --- /dev/null +++ b/tests/Services/ImportExportSystem/BOMValidationServiceTest.php @@ -0,0 +1,349 @@ +. + */ +namespace App\Tests\Services\ImportExportSystem; + +use App\Entity\Parts\Part; +use App\Services\ImportExportSystem\BOMValidationService; +use Doctrine\ORM\EntityManagerInterface; +use PHPUnit\Framework\TestCase; +use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; +use Symfony\Contracts\Translation\TranslatorInterface; + +/** + * @see \App\Services\ImportExportSystem\BOMValidationService + */ +class BOMValidationServiceTest extends WebTestCase +{ + private BOMValidationService $validationService; + private EntityManagerInterface $entityManager; + private TranslatorInterface $translator; + + protected function setUp(): void + { + self::bootKernel(); + $this->entityManager = self::getContainer()->get(EntityManagerInterface::class); + $this->translator = self::getContainer()->get(TranslatorInterface::class); + $this->validationService = new BOMValidationService($this->entityManager, $this->translator); + } + + public function testValidateBOMEntryWithValidData(): void + { + $entry = [ + 'Designator' => 'R1,C2,R3', + 'Quantity' => '3', + 'MPN' => 'RES-10K', + 'Package' => '0603', + 'Value' => '10k', + ]; + + $result = $this->validationService->validateBOMEntry($entry, 1); + + $this->assertTrue($result['is_valid']); + $this->assertEmpty($result['errors']); + $this->assertEquals(1, $result['line_number']); + } + + public function testValidateBOMEntryWithMissingRequiredFields(): void + { + $entry = [ + 'MPN' => 'RES-10K', + 'Package' => '0603', + ]; + + $result = $this->validationService->validateBOMEntry($entry, 1); + + $this->assertFalse($result['is_valid']); + $this->assertCount(2, $result['errors']); + $this->assertStringContainsString('Designator', (string) $result['errors'][0]); + $this->assertStringContainsString('Quantity', (string) $result['errors'][1]); + } + + public function testValidateBOMEntryWithQuantityMismatch(): void + { + $entry = [ + 'Designator' => 'R1,C2,R3,C4', + 'Quantity' => '3', + 'MPN' => 'RES-10K', + ]; + + $result = $this->validationService->validateBOMEntry($entry, 1); + + $this->assertFalse($result['is_valid']); + $this->assertCount(1, $result['errors']); + $this->assertStringContainsString('Mismatch between quantity and component references', (string) $result['errors'][0]); + } + + public function testValidateBOMEntryWithInvalidQuantity(): void + { + $entry = [ + 'Designator' => 'R1', + 'Quantity' => 'abc', + 'MPN' => 'RES-10K', + ]; + + $result = $this->validationService->validateBOMEntry($entry, 1); + + $this->assertFalse($result['is_valid']); + $this->assertGreaterThanOrEqual(1, count($result['errors'])); + $this->assertStringContainsString('not a valid number', implode(' ', array_map('strval', $result['errors']))); + } + + public function testValidateBOMEntryWithZeroQuantity(): void + { + $entry = [ + 'Designator' => 'R1', + 'Quantity' => '0', + 'MPN' => 'RES-10K', + ]; + + $result = $this->validationService->validateBOMEntry($entry, 1); + + $this->assertFalse($result['is_valid']); + $this->assertGreaterThanOrEqual(1, count($result['errors'])); + $this->assertStringContainsString('must be greater than 0', implode(' ', array_map('strval', $result['errors']))); + } + + public function testValidateBOMEntryWithDuplicateDesignators(): void + { + $entry = [ + 'Designator' => 'R1,R1,C2', + 'Quantity' => '3', + 'MPN' => 'RES-10K', + ]; + + $result = $this->validationService->validateBOMEntry($entry, 1); + + $this->assertFalse($result['is_valid']); + $this->assertCount(1, $result['errors']); + $this->assertStringContainsString('Duplicate component references', (string) $result['errors'][0]); + } + + public function testValidateBOMEntryWithInvalidDesignatorFormat(): void + { + $entry = [ + 'Designator' => 'R1,invalid,C2', + 'Quantity' => '3', + 'MPN' => 'RES-10K', + ]; + + $result = $this->validationService->validateBOMEntry($entry, 1); + + $this->assertTrue($result['is_valid']); // Warnings don't make it invalid + $this->assertCount(1, $result['warnings']); + $this->assertStringContainsString('unusual format', (string) $result['warnings'][0]); + } + + public function testValidateBOMEntryWithEmptyDesignator(): void + { + $entry = [ + 'Designator' => '', + 'Quantity' => '1', + 'MPN' => 'RES-10K', + ]; + + $result = $this->validationService->validateBOMEntry($entry, 1); + + $this->assertFalse($result['is_valid']); + $this->assertGreaterThanOrEqual(1, count($result['errors'])); + $this->assertStringContainsString('Required field "Designator" is missing or empty', implode(' ', array_map('strval', $result['errors']))); + } + + public function testValidateBOMEntryWithInvalidPartDBID(): void + { + $entry = [ + 'Designator' => 'R1', + 'Quantity' => '1', + 'MPN' => 'RES-10K', + 'Part-DB ID' => 'abc', + ]; + + $result = $this->validationService->validateBOMEntry($entry, 1); + + $this->assertFalse($result['is_valid']); + $this->assertGreaterThanOrEqual(1, count($result['errors'])); + $this->assertStringContainsString('not a valid number', implode(' ', array_map('strval', $result['errors']))); + } + + public function testValidateBOMEntryWithNonExistentPartDBID(): void + { + $entry = [ + 'Designator' => 'R1', + 'Quantity' => '1', + 'MPN' => 'RES-10K', + 'Part-DB ID' => '999999', // Use very high ID that doesn't exist + ]; + + $result = $this->validationService->validateBOMEntry($entry, 1); + + $this->assertTrue($result['is_valid']); // Warnings don't make it invalid + $this->assertCount(1, $result['warnings']); + $this->assertStringContainsString('not found in database', (string) $result['warnings'][0]); + } + + public function testValidateBOMEntryWithNoComponentName(): void + { + $entry = [ + 'Designator' => 'R1', + 'Quantity' => '1', + 'Package' => '0603', + ]; + + $result = $this->validationService->validateBOMEntry($entry, 1); + + $this->assertTrue($result['is_valid']); // Warnings don't make it invalid + $this->assertCount(1, $result['warnings']); + $this->assertStringContainsString('No component name/designation', (string) $result['warnings'][0]); + } + + public function testValidateBOMEntryWithLongPackageName(): void + { + $entry = [ + 'Designator' => 'R1', + 'Quantity' => '1', + 'MPN' => 'RES-10K', + 'Package' => str_repeat('A', 150), // Very long package name + ]; + + $result = $this->validationService->validateBOMEntry($entry, 1); + + $this->assertTrue($result['is_valid']); // Warnings don't make it invalid + $this->assertCount(1, $result['warnings']); + $this->assertStringContainsString('unusually long', (string) $result['warnings'][0]); + } + + public function testValidateBOMEntryWithLibraryPrefix(): void + { + $entry = [ + 'Designator' => 'R1', + 'Quantity' => '1', + 'MPN' => 'RES-10K', + 'Package' => 'Resistor_SMD:R_0603_1608Metric', + ]; + + $result = $this->validationService->validateBOMEntry($entry, 1); + + $this->assertTrue($result['is_valid']); + $this->assertCount(1, $result['info']); + $this->assertStringContainsString('library prefix', $result['info'][0]); + } + + public function testValidateBOMEntriesWithMultipleEntries(): void + { + $entries = [ + [ + 'Designator' => 'R1', + 'Quantity' => '1', + 'MPN' => 'RES-10K', + ], + [ + 'Designator' => 'C1,C2', + 'Quantity' => '2', + 'MPN' => 'CAP-100nF', + ], + ]; + + $result = $this->validationService->validateBOMEntries($entries); + + $this->assertTrue($result['is_valid']); + $this->assertEquals(2, $result['total_entries']); + $this->assertEquals(2, $result['valid_entries']); + $this->assertEquals(0, $result['invalid_entries']); + $this->assertCount(2, $result['line_results']); + } + + public function testValidateBOMEntriesWithMixedResults(): void + { + $entries = [ + [ + 'Designator' => 'R1', + 'Quantity' => '1', + 'MPN' => 'RES-10K', + ], + [ + 'Designator' => 'C1,C2', + 'Quantity' => '1', // Mismatch + 'MPN' => 'CAP-100nF', + ], + ]; + + $result = $this->validationService->validateBOMEntries($entries); + + $this->assertFalse($result['is_valid']); + $this->assertEquals(2, $result['total_entries']); + $this->assertEquals(1, $result['valid_entries']); + $this->assertEquals(1, $result['invalid_entries']); + $this->assertCount(1, $result['errors']); + } + + public function testGetValidationStats(): void + { + $validation_result = [ + 'total_entries' => 10, + 'valid_entries' => 8, + 'invalid_entries' => 2, + 'errors' => ['Error 1', 'Error 2'], + 'warnings' => ['Warning 1'], + 'info' => ['Info 1', 'Info 2'], + ]; + + $stats = $this->validationService->getValidationStats($validation_result); + + $this->assertEquals(10, $stats['total_entries']); + $this->assertEquals(8, $stats['valid_entries']); + $this->assertEquals(2, $stats['invalid_entries']); + $this->assertEquals(2, $stats['error_count']); + $this->assertEquals(1, $stats['warning_count']); + $this->assertEquals(2, $stats['info_count']); + $this->assertEquals(80.0, $stats['success_rate']); + } + + public function testGetErrorMessage(): void + { + $validation_result = [ + 'is_valid' => false, + 'errors' => ['Error 1', 'Error 2'], + 'warnings' => ['Warning 1'], + ]; + + $message = $this->validationService->getErrorMessage($validation_result); + + $this->assertStringContainsString('Errors:', $message); + $this->assertStringContainsString('• Error 1', $message); + $this->assertStringContainsString('• Error 2', $message); + $this->assertStringContainsString('Warnings:', $message); + $this->assertStringContainsString('• Warning 1', $message); + } + + public function testGetErrorMessageWithValidResult(): void + { + $validation_result = [ + 'is_valid' => true, + 'errors' => [], + 'warnings' => [], + ]; + + $message = $this->validationService->getErrorMessage($validation_result); + + $this->assertEquals('', $message); + } +} \ No newline at end of file From 72e3766be534ca3621da5fc19d62133410ec7d50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 6 Sep 2025 23:10:12 +0200 Subject: [PATCH 064/228] Added missing translations that got removed during rebase --- translations/messages.en.xlf | 366 +++++++++++++++++++++++++++++++++++ 1 file changed, 366 insertions(+) diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 8d1e55c8..bbbbb075 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -13057,5 +13057,371 @@ Please note, that you can not impersonate a disabled user. If you try you will g Redacted for security reasons
+ + + project.bom_import.map_fields + Map Fields + + + + + project.bom_import.map_fields.help + Configure how CSV columns map to BOM fields + + + + + project.bom_import.delimiter + Delimiter + + + + + project.bom_import.delimiter.comma + Comma (,) + + + + + project.bom_import.delimiter.semicolon + Semicolon (;) + + + + + project.bom_import.delimiter.tab + Tab + + + + + project.bom_import.field_mapping.title + Field Mapping + + + + + project.bom_import.field_mapping.csv_field + CSV Field + + + + + project.bom_import.field_mapping.maps_to + Maps To + + + + + project.bom_import.field_mapping.suggestion + Suggestion + + + + + project.bom_import.field_mapping.priority + Priority + + + + + project.bom_import.field_mapping.priority_help + Priority (lower number = higher priority) + + + + + project.bom_import.field_mapping.priority_short + P + + + + + project.bom_import.field_mapping.priority_note + Priority Tip: Lower numbers = higher priority. Default priority is 10. Use priorities 1-9 for most important fields, 10+ for normal priority. + + + + + project.bom_import.field_mapping.summary + Field Mapping Summary + + + + + project.bom_import.field_mapping.select_to_see_summary + Select field mappings to see summary + + + + + project.bom_import.field_mapping.no_suggestion + No suggestion + + + + + project.bom_import.preview + Preview + + + + + project.bom_import.flash.session_expired + Import session has expired. Please upload your file again. + + + + + project.bom_import.field_mapping.ignore + Ignore + + + + + project.bom_import.type.kicad_schematic + KiCAD Schematic BOM (CSV file) + + + + + common.back + Back + + + + + project.bom_import.validation.errors.required_field_missing + Line %line%: Required field "%field%" is missing or empty. Please ensure this field is mapped and contains data. + + + + + project.bom_import.validation.errors.no_valid_designators + Line %line%: Designator field contains no valid component references. Expected format: "R1,C2,U3" or "R1, C2, U3". + + + + + project.bom_import.validation.warnings.unusual_designator_format + Line %line%: Some component references may have unusual format: %designators%. Expected format: "R1", "C2", "U3", etc. + + + + + project.bom_import.validation.errors.duplicate_designators + Line %line%: Duplicate component references found: %designators%. Each component should be referenced only once per line. + + + + + project.bom_import.validation.errors.invalid_quantity + Line %line%: Quantity "%quantity%" is not a valid number. Please enter a numeric value (e.g., 1, 2.5, 10). + + + + + project.bom_import.validation.errors.quantity_zero_or_negative + Line %line%: Quantity must be greater than 0, got %quantity%. + + + + + project.bom_import.validation.warnings.quantity_unusually_high + Line %line%: Quantity %quantity% seems unusually high. Please verify this is correct. + + + + + project.bom_import.validation.warnings.quantity_not_whole_number + Line %line%: Quantity %quantity% is not a whole number, but you have %count% component references. This may indicate a mismatch. + + + + + project.bom_import.validation.errors.quantity_designator_mismatch + Line %line%: Mismatch between quantity and component references. Quantity: %quantity%, References: %count% (%designators%). These should match. Either adjust the quantity or check your component references. + + + + + project.bom_import.validation.errors.invalid_partdb_id + Line %line%: Part-DB ID "%id%" is not a valid number. Please enter a numeric ID. + + + + + project.bom_import.validation.errors.partdb_id_zero_or_negative + Line %line%: Part-DB ID must be greater than 0, got %id%. + + + + + project.bom_import.validation.warnings.partdb_id_not_found + Line %line%: Part-DB ID %id% not found in database. The component will be imported without linking to an existing part. + + + + + project.bom_import.validation.info.partdb_link_success + Line %line%: Successfully linked to Part-DB part "%name%" (ID: %id%). + + + + + project.bom_import.validation.warnings.no_component_name + Line %line%: No component name/designation provided (MPN, Designation, or Value). Component will be named "Unknown Component". + + + + + project.bom_import.validation.warnings.package_name_too_long + Line %line%: Package name "%package%" is unusually long. Please verify this is correct. + + + + + project.bom_import.validation.info.library_prefix_detected + Line %line%: Package "%package%" contains library prefix. This will be automatically removed during import. + + + + + project.bom_import.validation.errors.non_numeric_field + Line %line%: Field "%field%" contains non-numeric value "%value%". Please enter a valid number. + + + + + project.bom_import.validation.info.import_summary + Import summary: %total% total entries, %valid% valid, %invalid% with issues. + + + + + project.bom_import.validation.errors.summary + Found %count% validation error(s) that must be fixed before import can proceed. + + + + + project.bom_import.validation.warnings.summary + Found %count% warning(s). Please review these issues before proceeding. + + + + + project.bom_import.validation.info.all_valid + All entries passed validation successfully! + + + + + project.bom_import.validation.summary + Validation Summary + + + + + project.bom_import.validation.total_entries + Total Entries + + + + + project.bom_import.validation.valid_entries + Valid Entries + + + + + project.bom_import.validation.invalid_entries + Invalid Entries + + + + + project.bom_import.validation.success_rate + Success Rate + + + + + project.bom_import.validation.errors.title + Validation Errors + + + + + project.bom_import.validation.errors.description + The following errors must be fixed before the import can proceed: + + + + + project.bom_import.validation.warnings.title + Validation Warnings + + + + + project.bom_import.validation.warnings.description + The following warnings should be reviewed before proceeding: + + + + + project.bom_import.validation.info.title + Information + + + + + project.bom_import.validation.details.title + Detailed Validation Results + + + + + project.bom_import.validation.details.line + Line + + + + + project.bom_import.validation.details.status + Status + + + + + project.bom_import.validation.details.messages + Messages + + + + + project.bom_import.validation.details.valid + Valid + + + + + project.bom_import.validation.details.invalid + Invalid + + + + + project.bom_import.validation.all_valid + All entries are valid and ready for import! + + + + + project.bom_import.validation.fix_errors + Please fix the validation errors before proceeding with the import. + + From 1d33d95c57831eb7ba5790918d5f9ef0c9e9a077 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 6 Sep 2025 23:10:47 +0200 Subject: [PATCH 065/228] Show validation error messages in mapping step --- src/Controller/ProjectController.php | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Controller/ProjectController.php b/src/Controller/ProjectController.php index 444ff5b3..ec9147c1 100644 --- a/src/Controller/ProjectController.php +++ b/src/Controller/ProjectController.php @@ -378,7 +378,7 @@ class ProjectController extends AbstractController } // If there are validation errors, show them and stop - if (!empty($validation_result['errors'])) { + if (!empty($validation_result['errors'])) { foreach ($validation_result['errors'] as $error) { $this->addFlash('error', $error); } @@ -449,6 +449,16 @@ class ProjectController extends AbstractController // When we get here, there were validation errors $this->addFlash('error', t('project.bom_import.flash.invalid_entries')); + //Print validation errors to log for debugging + foreach ($errors as $error) { + $logger->error('BOM entry validation error', [ + 'message' => $error->getMessage(), + 'invalid_value' => $error->getInvalidValue(), + ]); + //And show as flash message + $this->addFlash('error', $error->getMessage(),); + } + } catch (\UnexpectedValueException | SyntaxError $e) { $this->addFlash('error', t('project.bom_import.flash.invalid_file', ['%message%' => $e->getMessage()])); } From 76f3c379b508036f4bb38ce307ee5ac4f9ac77b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 6 Sep 2025 23:20:07 +0200 Subject: [PATCH 066/228] Added generic CSV type option, to highlight the universal nature of the importer --- src/Controller/ProjectController.php | 3 +- translations/messages.en.xlf | 762 ++++++++++++++------------- 2 files changed, 386 insertions(+), 379 deletions(-) diff --git a/src/Controller/ProjectController.php b/src/Controller/ProjectController.php index ec9147c1..2a6d19ee 100644 --- a/src/Controller/ProjectController.php +++ b/src/Controller/ProjectController.php @@ -149,6 +149,7 @@ class ProjectController extends AbstractController 'choices' => [ 'project.bom_import.type.kicad_pcbnew' => 'kicad_pcbnew', 'project.bom_import.type.kicad_schematic' => 'kicad_schematic', + 'project.bom_import.type.generic_csv' => 'generic_csv', ] ]); $builder->add('clear_existing_bom', CheckboxType::class, [ @@ -176,7 +177,7 @@ class ProjectController extends AbstractController try { // For schematic imports, redirect to field mapping step - if ($import_type === 'kicad_schematic') { + if (in_array($import_type, ['kicad_schematic', 'generic_csv'], true)) { // Store file content and options in session for field mapping step $file_content = $form->get('file')->getData()->getContent(); $clear_existing = $form->get('clear_existing_bom')->getData(); diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index bbbbb075..888384da 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -12333,7 +12333,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g settings.ips.element14.apiKey.help - You can register for an API key on <a href="https://partner.element14.com/">https://partner.element14.com/</a>. + https://partner.element14.com/.]]> @@ -12345,7 +12345,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g settings.ips.element14.storeId.help - The store domain to retrieve the data from. This decides the language and currency of results. See <a href="https://partner.element14.com/docs/Product_Search_API_REST__Description">here</a> for a list of valid domains. + here for a list of valid domains.]]> @@ -12363,7 +12363,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g settings.ips.tme.token.help - You can get an API token and secret on <a href="https://developers.tme.eu/en/">https://developers.tme.eu/en/</a>. + https://developers.tme.eu/en/.]]> @@ -12411,7 +12411,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g settings.ips.mouser.apiKey.help - You can register for an API key on <a href="https://eu.mouser.com/api-hub/">https://eu.mouser.com/api-hub/</a>. + https://eu.mouser.com/api-hub/.]]> @@ -12489,7 +12489,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g settings.system.attachments - Attachments & Files + @@ -12513,7 +12513,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g settings.system.attachments.allowDownloads.help - With this option users can download external files into Part-DB by providing an URL. <b>Attention: This can be a security issue, as it might allow users to access intranet ressources via Part-DB!</b> + Attention: This can be a security issue, as it might allow users to access intranet ressources via Part-DB!]]> @@ -12687,8 +12687,8 @@ Please note, that you can not impersonate a disabled user. If you try you will g settings.system.localization.base_currency_description - The currency that is used to store price information and exchange rates in. This currency is assumed, when no currency is set for a price information. -<b>Please note that the currencies are not converted, when changing this value. So changing the default currency after you already added price information, will result in wrong prices!</b> + Please note that the currencies are not converted, when changing this value. So changing the default currency after you already added price information, will result in wrong prices!]]> @@ -12718,7 +12718,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g settings.misc.kicad_eda.category_depth.help - 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. + 0 to show more levels. Set to -1, to show all parts of Part-DB inside a sigle cnategory in KiCad.]]> @@ -12736,7 +12736,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g settings.behavior.sidebar.items.help - The menus which appear at the sidebar by default. Order of items can be changed via drag & drop. + @@ -12784,7 +12784,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g settings.behavior.table.parts_default_columns.help - The columns to show by default in part tables. Order of items can be changed via drag & drop. + @@ -12838,7 +12838,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g settings.ips.oemsecrets.sortMode.M - Completeness & Manufacturer name + @@ -13057,371 +13057,377 @@ Please note, that you can not impersonate a disabled user. If you try you will g Redacted for security reasons - - - project.bom_import.map_fields - Map Fields - - - - - project.bom_import.map_fields.help - Configure how CSV columns map to BOM fields - - - - - project.bom_import.delimiter - Delimiter - - - - - project.bom_import.delimiter.comma - Comma (,) - - - - - project.bom_import.delimiter.semicolon - Semicolon (;) - - - - - project.bom_import.delimiter.tab - Tab - - - - - project.bom_import.field_mapping.title - Field Mapping - - - - - project.bom_import.field_mapping.csv_field - CSV Field - - - - - project.bom_import.field_mapping.maps_to - Maps To - - - - - project.bom_import.field_mapping.suggestion - Suggestion - - - - - project.bom_import.field_mapping.priority - Priority - - - - - project.bom_import.field_mapping.priority_help - Priority (lower number = higher priority) - - - - - project.bom_import.field_mapping.priority_short - P - - - - - project.bom_import.field_mapping.priority_note - Priority Tip: Lower numbers = higher priority. Default priority is 10. Use priorities 1-9 for most important fields, 10+ for normal priority. - - - - - project.bom_import.field_mapping.summary - Field Mapping Summary - - - - - project.bom_import.field_mapping.select_to_see_summary - Select field mappings to see summary - - - - - project.bom_import.field_mapping.no_suggestion - No suggestion - - - - - project.bom_import.preview - Preview - - - - - project.bom_import.flash.session_expired - Import session has expired. Please upload your file again. - - - - - project.bom_import.field_mapping.ignore - Ignore - - - - - project.bom_import.type.kicad_schematic - KiCAD Schematic BOM (CSV file) - - - - - common.back - Back - - - - - project.bom_import.validation.errors.required_field_missing - Line %line%: Required field "%field%" is missing or empty. Please ensure this field is mapped and contains data. - - - - - project.bom_import.validation.errors.no_valid_designators - Line %line%: Designator field contains no valid component references. Expected format: "R1,C2,U3" or "R1, C2, U3". - - - - - project.bom_import.validation.warnings.unusual_designator_format - Line %line%: Some component references may have unusual format: %designators%. Expected format: "R1", "C2", "U3", etc. - - - - - project.bom_import.validation.errors.duplicate_designators - Line %line%: Duplicate component references found: %designators%. Each component should be referenced only once per line. - - - - - project.bom_import.validation.errors.invalid_quantity - Line %line%: Quantity "%quantity%" is not a valid number. Please enter a numeric value (e.g., 1, 2.5, 10). - - - - - project.bom_import.validation.errors.quantity_zero_or_negative - Line %line%: Quantity must be greater than 0, got %quantity%. - - - - - project.bom_import.validation.warnings.quantity_unusually_high - Line %line%: Quantity %quantity% seems unusually high. Please verify this is correct. - - - - - project.bom_import.validation.warnings.quantity_not_whole_number - Line %line%: Quantity %quantity% is not a whole number, but you have %count% component references. This may indicate a mismatch. - - - - - project.bom_import.validation.errors.quantity_designator_mismatch - Line %line%: Mismatch between quantity and component references. Quantity: %quantity%, References: %count% (%designators%). These should match. Either adjust the quantity or check your component references. - - - - - project.bom_import.validation.errors.invalid_partdb_id - Line %line%: Part-DB ID "%id%" is not a valid number. Please enter a numeric ID. - - - - - project.bom_import.validation.errors.partdb_id_zero_or_negative - Line %line%: Part-DB ID must be greater than 0, got %id%. - - - - - project.bom_import.validation.warnings.partdb_id_not_found - Line %line%: Part-DB ID %id% not found in database. The component will be imported without linking to an existing part. - - - - - project.bom_import.validation.info.partdb_link_success - Line %line%: Successfully linked to Part-DB part "%name%" (ID: %id%). - - - - - project.bom_import.validation.warnings.no_component_name - Line %line%: No component name/designation provided (MPN, Designation, or Value). Component will be named "Unknown Component". - - - - - project.bom_import.validation.warnings.package_name_too_long - Line %line%: Package name "%package%" is unusually long. Please verify this is correct. - - - - - project.bom_import.validation.info.library_prefix_detected - Line %line%: Package "%package%" contains library prefix. This will be automatically removed during import. - - - - - project.bom_import.validation.errors.non_numeric_field - Line %line%: Field "%field%" contains non-numeric value "%value%". Please enter a valid number. - - - - - project.bom_import.validation.info.import_summary - Import summary: %total% total entries, %valid% valid, %invalid% with issues. - - - - - project.bom_import.validation.errors.summary - Found %count% validation error(s) that must be fixed before import can proceed. - - - - - project.bom_import.validation.warnings.summary - Found %count% warning(s). Please review these issues before proceeding. - - - - - project.bom_import.validation.info.all_valid - All entries passed validation successfully! - - - - - project.bom_import.validation.summary - Validation Summary - - - - - project.bom_import.validation.total_entries - Total Entries - - - - - project.bom_import.validation.valid_entries - Valid Entries - - - - - project.bom_import.validation.invalid_entries - Invalid Entries - - - - - project.bom_import.validation.success_rate - Success Rate - - - - - project.bom_import.validation.errors.title - Validation Errors - - - - - project.bom_import.validation.errors.description - The following errors must be fixed before the import can proceed: - - - - - project.bom_import.validation.warnings.title - Validation Warnings - - - - - project.bom_import.validation.warnings.description - The following warnings should be reviewed before proceeding: - - - - - project.bom_import.validation.info.title - Information - - - - - project.bom_import.validation.details.title - Detailed Validation Results - - - - - project.bom_import.validation.details.line - Line - - - - - project.bom_import.validation.details.status - Status - - - - - project.bom_import.validation.details.messages - Messages - - - - - project.bom_import.validation.details.valid - Valid - - - - - project.bom_import.validation.details.invalid - Invalid - - - - - project.bom_import.validation.all_valid - All entries are valid and ready for import! - - - - - project.bom_import.validation.fix_errors - Please fix the validation errors before proceeding with the import. - - + + + project.bom_import.map_fields + Map Fields + + + + + project.bom_import.map_fields.help + Configure how CSV columns map to BOM fields + + + + + project.bom_import.delimiter + Delimiter + + + + + project.bom_import.delimiter.comma + Comma (,) + + + + + project.bom_import.delimiter.semicolon + Semicolon (;) + + + + + project.bom_import.delimiter.tab + Tab + + + + + project.bom_import.field_mapping.title + Field Mapping + + + + + project.bom_import.field_mapping.csv_field + CSV Field + + + + + project.bom_import.field_mapping.maps_to + Maps To + + + + + project.bom_import.field_mapping.suggestion + Suggestion + + + + + project.bom_import.field_mapping.priority + Priority + + + + + project.bom_import.field_mapping.priority_help + Priority (lower number = higher priority) + + + + + project.bom_import.field_mapping.priority_short + P + + + + + project.bom_import.field_mapping.priority_note + Priority Tip: Lower numbers = higher priority. Default priority is 10. Use priorities 1-9 for most important fields, 10+ for normal priority. + + + + + project.bom_import.field_mapping.summary + Field Mapping Summary + + + + + project.bom_import.field_mapping.select_to_see_summary + Select field mappings to see summary + + + + + project.bom_import.field_mapping.no_suggestion + No suggestion + + + + + project.bom_import.preview + Preview + + + + + project.bom_import.flash.session_expired + Import session has expired. Please upload your file again. + + + + + project.bom_import.field_mapping.ignore + Ignore + + + + + project.bom_import.type.kicad_schematic + KiCAD Schematic BOM (CSV file) + + + + + common.back + Back + + + + + project.bom_import.validation.errors.required_field_missing + Line %line%: Required field "%field%" is missing or empty. Please ensure this field is mapped and contains data. + + + + + project.bom_import.validation.errors.no_valid_designators + Line %line%: Designator field contains no valid component references. Expected format: "R1,C2,U3" or "R1, C2, U3". + + + + + project.bom_import.validation.warnings.unusual_designator_format + Line %line%: Some component references may have unusual format: %designators%. Expected format: "R1", "C2", "U3", etc. + + + + + project.bom_import.validation.errors.duplicate_designators + Line %line%: Duplicate component references found: %designators%. Each component should be referenced only once per line. + + + + + project.bom_import.validation.errors.invalid_quantity + Line %line%: Quantity "%quantity%" is not a valid number. Please enter a numeric value (e.g., 1, 2.5, 10). + + + + + project.bom_import.validation.errors.quantity_zero_or_negative + Line %line%: Quantity must be greater than 0, got %quantity%. + + + + + project.bom_import.validation.warnings.quantity_unusually_high + Line %line%: Quantity %quantity% seems unusually high. Please verify this is correct. + + + + + project.bom_import.validation.warnings.quantity_not_whole_number + Line %line%: Quantity %quantity% is not a whole number, but you have %count% component references. This may indicate a mismatch. + + + + + project.bom_import.validation.errors.quantity_designator_mismatch + Line %line%: Mismatch between quantity and component references. Quantity: %quantity%, References: %count% (%designators%). These should match. Either adjust the quantity or check your component references. + + + + + project.bom_import.validation.errors.invalid_partdb_id + Line %line%: Part-DB ID "%id%" is not a valid number. Please enter a numeric ID. + + + + + project.bom_import.validation.errors.partdb_id_zero_or_negative + Line %line%: Part-DB ID must be greater than 0, got %id%. + + + + + project.bom_import.validation.warnings.partdb_id_not_found + Line %line%: Part-DB ID %id% not found in database. The component will be imported without linking to an existing part. + + + + + project.bom_import.validation.info.partdb_link_success + Line %line%: Successfully linked to Part-DB part "%name%" (ID: %id%). + + + + + project.bom_import.validation.warnings.no_component_name + Line %line%: No component name/designation provided (MPN, Designation, or Value). Component will be named "Unknown Component". + + + + + project.bom_import.validation.warnings.package_name_too_long + Line %line%: Package name "%package%" is unusually long. Please verify this is correct. + + + + + project.bom_import.validation.info.library_prefix_detected + Line %line%: Package "%package%" contains library prefix. This will be automatically removed during import. + + + + + project.bom_import.validation.errors.non_numeric_field + Line %line%: Field "%field%" contains non-numeric value "%value%". Please enter a valid number. + + + + + project.bom_import.validation.info.import_summary + Import summary: %total% total entries, %valid% valid, %invalid% with issues. + + + + + project.bom_import.validation.errors.summary + Found %count% validation error(s) that must be fixed before import can proceed. + + + + + project.bom_import.validation.warnings.summary + Found %count% warning(s). Please review these issues before proceeding. + + + + + project.bom_import.validation.info.all_valid + All entries passed validation successfully! + + + + + project.bom_import.validation.summary + Validation Summary + + + + + project.bom_import.validation.total_entries + Total Entries + + + + + project.bom_import.validation.valid_entries + Valid Entries + + + + + project.bom_import.validation.invalid_entries + Invalid Entries + + + + + project.bom_import.validation.success_rate + Success Rate + + + + + project.bom_import.validation.errors.title + Validation Errors + + + + + project.bom_import.validation.errors.description + The following errors must be fixed before the import can proceed: + + + + + project.bom_import.validation.warnings.title + Validation Warnings + + + + + project.bom_import.validation.warnings.description + The following warnings should be reviewed before proceeding: + + + + + project.bom_import.validation.info.title + Information + + + + + project.bom_import.validation.details.title + Detailed Validation Results + + + + + project.bom_import.validation.details.line + Line + + + + + project.bom_import.validation.details.status + Status + + + + + project.bom_import.validation.details.messages + Messages + + + + + project.bom_import.validation.details.valid + Valid + + + + + project.bom_import.validation.details.invalid + Invalid + + + + + project.bom_import.validation.all_valid + All entries are valid and ready for import! + + + + + project.bom_import.validation.fix_errors + Please fix the validation errors before proceeding with the import. + + + + + project.bom_import.type.generic_csv + Generic CSV + + From 90f83273da8a02af42b5979566b22d8e7f606198 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 6 Sep 2025 23:24:32 +0200 Subject: [PATCH 067/228] Added nonce to scripts to ensure that they are working with enabled CSP --- .../projects/import_bom_map_fields.html.twig | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/templates/projects/import_bom_map_fields.html.twig b/templates/projects/import_bom_map_fields.html.twig index ba10c9c5..4e45eb08 100644 --- a/templates/projects/import_bom_map_fields.html.twig +++ b/templates/projects/import_bom_map_fields.html.twig @@ -15,7 +15,7 @@ show_details: false } %} {% endif %} - +
@@ -30,7 +30,7 @@
{{ form_start(form) }} - +
{{ form_row(form.delimiter) }} @@ -83,10 +83,10 @@ {% endif %} - @@ -96,7 +96,7 @@
- +
{% trans %}project.bom_import.field_mapping.summary{% endtrans %}:
@@ -121,12 +121,12 @@ {{ form_end(form) }} - -{% endblock %} \ No newline at end of file +{% endblock %} From 2b28aa8ba9f15a8b6206173b45e0cc2d2b03060f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 6 Sep 2025 23:29:19 +0200 Subject: [PATCH 068/228] Enable CSP also in debug mode, as otherwise it complains about missing nonce function --- config/packages/nelmio_security.yaml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/config/packages/nelmio_security.yaml b/config/packages/nelmio_security.yaml index 1cb74da7..c283cd8e 100644 --- a/config/packages/nelmio_security.yaml +++ b/config/packages/nelmio_security.yaml @@ -69,9 +69,3 @@ nelmio_security: - 'data:' block-all-mixed-content: true # defaults to false, blocks HTTP content over HTTPS transport # upgrade-insecure-requests: true # defaults to false, upgrades HTTP requests to HTTPS transport - -when@dev: - # disables the Content-Security-Policy header - nelmio_security: - csp: - enabled: false \ No newline at end of file From fb92db8c051f08785e97f05f54fd762b12f9857e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 6 Sep 2025 23:32:08 +0200 Subject: [PATCH 069/228] Use body element as dropdownParent for tomselect elements This improves UX --- .../controllers/elements/attachment_autocomplete_controller.js | 1 + assets/controllers/elements/part_select_controller.js | 3 ++- assets/controllers/elements/select_controller.js | 3 ++- assets/controllers/elements/select_multiple_controller.js | 3 ++- .../elements/static_file_autocomplete_controller.js | 1 + .../elements/structural_entity_select_controller.js | 1 + assets/controllers/elements/tagsinput_controller.js | 3 ++- 7 files changed, 11 insertions(+), 4 deletions(-) diff --git a/assets/controllers/elements/attachment_autocomplete_controller.js b/assets/controllers/elements/attachment_autocomplete_controller.js index f8bc301e..0175b284 100644 --- a/assets/controllers/elements/attachment_autocomplete_controller.js +++ b/assets/controllers/elements/attachment_autocomplete_controller.js @@ -42,6 +42,7 @@ export default class extends Controller { selectOnTab: true, //This a an ugly solution to disable the delimiter parsing of the TomSelect plugin delimiter: 'VERY_L0NG_D€LIMITER_WHICH_WILL_NEVER_BE_ENCOUNTERED_IN_A_STRING', + dropdownParent: 'body', render: { item: (data, escape) => { return '' + escape(data.label) + ''; diff --git a/assets/controllers/elements/part_select_controller.js b/assets/controllers/elements/part_select_controller.js index 5abd5ba3..0658f4b4 100644 --- a/assets/controllers/elements/part_select_controller.js +++ b/assets/controllers/elements/part_select_controller.js @@ -16,6 +16,7 @@ export default class extends Controller { searchField: ["name", "description", "category", "footprint"], valueField: "id", labelField: "name", + dropdownParent: 'body', preload: "focus", render: { item: (data, escape) => { @@ -71,4 +72,4 @@ export default class extends Controller { //Destroy the TomSelect instance this._tomSelect.destroy(); } -} \ No newline at end of file +} diff --git a/assets/controllers/elements/select_controller.js b/assets/controllers/elements/select_controller.js index cdafe4d0..f933731a 100644 --- a/assets/controllers/elements/select_controller.js +++ b/assets/controllers/elements/select_controller.js @@ -44,6 +44,7 @@ export default class extends Controller { allowEmptyOption: true, selectOnTab: true, maxOptions: null, + dropdownParent: 'body', render: { item: this.renderItem.bind(this), @@ -108,4 +109,4 @@ export default class extends Controller { //Destroy the TomSelect instance this._tomSelect.destroy(); } -} \ No newline at end of file +} diff --git a/assets/controllers/elements/select_multiple_controller.js b/assets/controllers/elements/select_multiple_controller.js index df37871d..daa6b0a1 100644 --- a/assets/controllers/elements/select_multiple_controller.js +++ b/assets/controllers/elements/select_multiple_controller.js @@ -29,6 +29,7 @@ export default class extends Controller { this._tomSelect = new TomSelect(this.element, { maxItems: 1000, allowEmptyOption: true, + dropdownParent: 'body', plugins: ['remove_button'], }); } @@ -39,4 +40,4 @@ export default class extends Controller { this._tomSelect.destroy(); } -} \ No newline at end of file +} diff --git a/assets/controllers/elements/static_file_autocomplete_controller.js b/assets/controllers/elements/static_file_autocomplete_controller.js index 31ca0314..0421a26d 100644 --- a/assets/controllers/elements/static_file_autocomplete_controller.js +++ b/assets/controllers/elements/static_file_autocomplete_controller.js @@ -50,6 +50,7 @@ export default class extends Controller { valueField: 'text', searchField: 'text', orderField: 'text', + dropdownParent: 'body', //This a an ugly solution to disable the delimiter parsing of the TomSelect plugin delimiter: 'VERY_L0NG_D€LIMITER_WHICH_WILL_NEVER_BE_ENCOUNTERED_IN_A_STRING', diff --git a/assets/controllers/elements/structural_entity_select_controller.js b/assets/controllers/elements/structural_entity_select_controller.js index a1114a97..5c6f9490 100644 --- a/assets/controllers/elements/structural_entity_select_controller.js +++ b/assets/controllers/elements/structural_entity_select_controller.js @@ -54,6 +54,7 @@ export default class extends Controller { maxItems: 1, delimiter: "$$VERY_LONG_DELIMITER_THAT_SHOULD_NEVER_APPEAR$$", splitOn: null, + dropdownParent: 'body', searchField: [ {field: "text", weight : 2}, diff --git a/assets/controllers/elements/tagsinput_controller.js b/assets/controllers/elements/tagsinput_controller.js index 1f10c457..53bf7608 100644 --- a/assets/controllers/elements/tagsinput_controller.js +++ b/assets/controllers/elements/tagsinput_controller.js @@ -43,6 +43,7 @@ export default class extends Controller { selectOnTab: true, createOnBlur: true, create: true, + dropdownParent: 'body', }; if(this.element.dataset.autocomplete) { @@ -73,4 +74,4 @@ export default class extends Controller { //Destroy the TomSelect instance this._tomSelect.destroy(); } -} \ No newline at end of file +} From 5a5691a8c4142d8c4d849d424cc37212503a4cf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 6 Sep 2025 23:34:47 +0200 Subject: [PATCH 070/228] Added documentation about the new BOM file types --- docs/usage/bom_import.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/usage/bom_import.md b/docs/usage/bom_import.md index 94a06d55..b4bcb2be 100644 --- a/docs/usage/bom_import.md +++ b/docs/usage/bom_import.md @@ -34,3 +34,12 @@ select the BOM file you want to import and some options for the import process: has a different format and does not work with this type. You can generate this BOM file by going to "File" -> "Fabrication Outputs" -> "Bill of Materials" in Pcbnew and save the file to your desired location. +* **KiCAD Schematic BOM (CSV file)**: A CSV file of the Bill of Material (BOM) generated + by [KiCAD Eeschema](https://www.kicad.org/). + You can generate this BOM file by going to "Tools" -> "Generate Bill of Materials" in Eeschema and save the file to your + desired location. In the next step you can customize the mapping of the fields in Part-DB, if you have any special fields + in your BOM to locate your fields correctly. +* **Generic CSV file**: A generic CSV file. You can use this option if you use some different ECAD software or wanna create + your own CSV file. You will need to specify at least the designators, quantity and value fields in the CSV. In the next + step you can customize the mapping of the fields in Part-DB, if you have any special fields in your BOM to locate your + parts correctly. From ced16620ecbffece927598548c7a7a8e9e9bf645 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 6 Sep 2025 23:42:09 +0200 Subject: [PATCH 071/228] Fixed pollin info provider This fixes issue #1015 --- src/Services/InfoProviderSystem/Providers/PollinProvider.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Services/InfoProviderSystem/Providers/PollinProvider.php b/src/Services/InfoProviderSystem/Providers/PollinProvider.php index 55fa335a..b74e0365 100644 --- a/src/Services/InfoProviderSystem/Providers/PollinProvider.php +++ b/src/Services/InfoProviderSystem/Providers/PollinProvider.php @@ -158,7 +158,8 @@ class PollinProvider implements InfoProviderInterface category: $this->parseCategory($dom), manufacturer: $dom->filter('meta[property="product:brand"]')->count() > 0 ? $dom->filter('meta[property="product:brand"]')->attr('content') : null, preview_image_url: $dom->filter('meta[property="og:image"]')->attr('content'), - manufacturing_status: $this->mapAvailability($dom->filter('link[itemprop="availability"]')->attr('href')), + //TODO: Find another way to determine the manufacturing status, as the itemprop="availability" is often is not existing anymore in the page + //manufacturing_status: $this->mapAvailability($dom->filter('link[itemprop="availability"]')->attr('href')), provider_url: $productPageUrl, notes: $this->parseNotes($dom), datasheets: $this->parseDatasheets($dom), From a18ec373d2b962b4ae8a7510c6345890d952cf27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 6 Sep 2025 23:49:14 +0200 Subject: [PATCH 072/228] Validate label profiles before creating them via the label controller, so that we do not create duplicate entries This fixes issue #994 --- src/Controller/LabelController.php | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/Controller/LabelController.php b/src/Controller/LabelController.php index 4950628b..d0689330 100644 --- a/src/Controller/LabelController.php +++ b/src/Controller/LabelController.php @@ -58,12 +58,15 @@ use Symfony\Component\Form\FormError; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; +use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Contracts\Translation\TranslatorInterface; #[Route(path: '/label')] class LabelController extends AbstractController { - public function __construct(protected LabelGenerator $labelGenerator, protected EntityManagerInterface $em, protected ElementTypeNameGenerator $elementTypeNameGenerator, protected RangeParser $rangeParser, protected TranslatorInterface $translator) + public function __construct(protected LabelGenerator $labelGenerator, protected EntityManagerInterface $em, protected ElementTypeNameGenerator $elementTypeNameGenerator, protected RangeParser $rangeParser, protected TranslatorInterface $translator, + private readonly ValidatorInterface $validator + ) { } @@ -120,15 +123,25 @@ class LabelController extends AbstractController goto render; } - $profile = new LabelProfile(); - $profile->setName($form->get('save_profile_name')->getData()); - $profile->setOptions($form_options); - $this->em->persist($profile); + $new_profile = new LabelProfile(); + $new_profile->setName($form->get('save_profile_name')->getData()); + $new_profile->setOptions($form_options); + + //Validate the profile name + $errors = $this->validator->validate($new_profile); + if (count($errors) > 0) { + foreach ($errors as $error) { + $form->get('save_profile_name')->addError(new FormError($error->getMessage())); + } + goto render; + } + + $this->em->persist($new_profile); $this->em->flush(); $this->addFlash('success', 'label_generator.profile_saved'); return $this->redirectToRoute('label_dialog_profile', [ - 'profile' => $profile->getID(), + 'profile' => $new_profile->getID(), 'target_id' => (string) $form->get('target_id')->getData() ]); } From 46d1a0cb1b3274a1f6dc639480bff0c9a7b1ed7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 6 Sep 2025 23:56:51 +0200 Subject: [PATCH 073/228] Added an button to update a label profile from directly inside the label generator Related to issue #994 --- src/Controller/LabelController.php | 24 ++++++++++++++++++++++++ src/Form/LabelSystem/LabelDialogType.php | 11 +++++++++++ templates/label_system/dialog.html.twig | 4 +++- translations/messages.en.xlf | 12 ++++++++++++ 4 files changed, 50 insertions(+), 1 deletion(-) diff --git a/src/Controller/LabelController.php b/src/Controller/LabelController.php index d0689330..8c0bcca0 100644 --- a/src/Controller/LabelController.php +++ b/src/Controller/LabelController.php @@ -88,6 +88,7 @@ class LabelController extends AbstractController $form = $this->createForm(LabelDialogType::class, null, [ 'disable_options' => $disable_options, + 'profile' => $profile ]); //Try to parse given target_type and target_id @@ -146,6 +147,29 @@ class LabelController extends AbstractController ]); } + if ($form->get('update_profile')->isClicked() && $profile instanceof LabelProfile && $this->isGranted('edit', $profile)) { //@phpstan-ignore-line Phpstan does not recognize the isClicked method + //Update the profile options + $profile->setOptions($form_options); + + //Validate the profile name + $errors = $this->validator->validate($profile); + if (count($errors) > 0) { + foreach ($errors as $error) { + $this->addFlash('error', $error->getMessage()); + } + goto render; + } + + $this->em->persist($profile); + $this->em->flush(); + $this->addFlash('success', 'label_generator.profile_updated'); + + return $this->redirectToRoute('label_dialog_profile', [ + 'profile' => $profile->getID(), + 'target_id' => (string) $form->get('target_id')->getData() + ]); + } + $target_id = (string) $form->get('target_id')->getData(); $targets = $this->findObjects($form_options->getSupportedElement(), $target_id); if ($targets !== []) { diff --git a/src/Form/LabelSystem/LabelDialogType.php b/src/Form/LabelSystem/LabelDialogType.php index f2710b19..d79d01f6 100644 --- a/src/Form/LabelSystem/LabelDialogType.php +++ b/src/Form/LabelSystem/LabelDialogType.php @@ -87,6 +87,16 @@ class LabelDialogType extends AbstractType ] ]); + if ($options['profile'] !== null) { + $builder->add('update_profile', SubmitType::class, [ + 'label' => 'label_generator.update_profile', + 'disabled' => !$this->security->isGranted('edit', $options['profile']), + 'attr' => [ + 'class' => 'btn btn-outline-success' + ] + ]); + } + $builder->add('update', SubmitType::class, [ 'label' => 'label_generator.update', ]); @@ -97,5 +107,6 @@ class LabelDialogType extends AbstractType parent::configureOptions($resolver); $resolver->setDefault('mapped', false); $resolver->setDefault('disable_options', false); + $resolver->setDefault('profile', null); } } diff --git a/templates/label_system/dialog.html.twig b/templates/label_system/dialog.html.twig index 50db99e7..571eb264 100644 --- a/templates/label_system/dialog.html.twig +++ b/templates/label_system/dialog.html.twig @@ -100,6 +100,8 @@
{% endif %} + {{ form_row(form.update_profile) }} +
@@ -133,4 +135,4 @@
{% endif %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 888384da..41ad8358 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -13429,5 +13429,17 @@ Please note, that you can not impersonate a disabled user. If you try you will g Generic CSV + + + label_generator.update_profile + Update profile with current settings + + + + + label_generator.profile_updated + Label profile updated successfully. + + From c5a1df37b9c3e3a1ebb545b3ef893d921e231a25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 7 Sep 2025 00:26:24 +0200 Subject: [PATCH 074/228] Fixed tests --- src/Controller/LabelController.php | 6 +++++- templates/label_system/dialog.html.twig | 4 +++- tests/API/Endpoints/CurrencyEndpointTest.php | 6 +++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/Controller/LabelController.php b/src/Controller/LabelController.php index 8c0bcca0..90a6715b 100644 --- a/src/Controller/LabelController.php +++ b/src/Controller/LabelController.php @@ -147,7 +147,11 @@ class LabelController extends AbstractController ]); } - if ($form->get('update_profile')->isClicked() && $profile instanceof LabelProfile && $this->isGranted('edit', $profile)) { //@phpstan-ignore-line Phpstan does not recognize the isClicked method + //Check if the current profile should be updated + if ($form->has('update_profile') + && $form->get('update_profile')->isClicked() //@phpstan-ignore-line Phpstan does not recognize the isClicked method + && $profile instanceof LabelProfile + && $this->isGranted('edit', $profile)) { //Update the profile options $profile->setOptions($form_options); diff --git a/templates/label_system/dialog.html.twig b/templates/label_system/dialog.html.twig index 571eb264..037b549e 100644 --- a/templates/label_system/dialog.html.twig +++ b/templates/label_system/dialog.html.twig @@ -100,7 +100,9 @@
{% endif %} - {{ form_row(form.update_profile) }} + {% if form.update_profile is defined %} + {{ form_row(form.update_profile) }} + {% endif %}
diff --git a/tests/API/Endpoints/CurrencyEndpointTest.php b/tests/API/Endpoints/CurrencyEndpointTest.php index 78434ea3..a463daeb 100644 --- a/tests/API/Endpoints/CurrencyEndpointTest.php +++ b/tests/API/Endpoints/CurrencyEndpointTest.php @@ -36,7 +36,7 @@ class CurrencyEndpointTest extends CrudEndpointTestCase { $this->_testGetCollection(); self::assertJsonContains([ - 'hydra:totalItems' => 0, + 'hydra:totalItems' => 4, //The 4 currencies from our fixtures ]); } @@ -45,7 +45,7 @@ class CurrencyEndpointTest extends CrudEndpointTestCase { $this->_testPostItem([ 'name' => 'Test API', - 'iso_code' => 'USD', + 'iso_code' => 'CAD', ]); } @@ -61,4 +61,4 @@ class CurrencyEndpointTest extends CrudEndpointTestCase { $this->_testDeleteItem(5); }*/ -} \ No newline at end of file +} From 14cc0b9e9ad7828102ef67eb2e1e5e83967d9762 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 7 Sep 2025 17:53:12 +0200 Subject: [PATCH 075/228] New translations messages.en.xlf (German) (#1028) --- translations/messages.de.xlf | 384 +++++++++++++++++++++++++++++++++++ 1 file changed, 384 insertions(+) diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index b579d908..8515abb8 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -13056,5 +13056,389 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön Aus Sicherheitsgründen ausgeblendet + + + project.bom_import.map_fields + Spalten zuordnen + + + + + project.bom_import.map_fields.help + Wählen Sie aus, wie CSV Spalten auf BOM Felder gemappt werden + + + + + project.bom_import.delimiter + Trennzeichen + + + + + project.bom_import.delimiter.comma + Komma (,) + + + + + project.bom_import.delimiter.semicolon + Semikolon (;) + + + + + project.bom_import.delimiter.tab + Tab + + + + + project.bom_import.field_mapping.title + Spaltenzuordnung + + + + + project.bom_import.field_mapping.csv_field + CSV Spalte + + + + + project.bom_import.field_mapping.maps_to + Mappt auf + + + + + project.bom_import.field_mapping.suggestion + Vorschlag + + + + + project.bom_import.field_mapping.priority + Priorität + + + + + project.bom_import.field_mapping.priority_help + Priorität (kleinere Nummer = höhere Priorität) + + + + + project.bom_import.field_mapping.priority_short + P + + + + + project.bom_import.field_mapping.priority_note + Prioritätstipp: Niedrigere Zahlen = höhere Priorität. Die Standardpriorität ist 10. Verwenden Sie die Prioritäten 1–9 für die wichtigsten Felder und 10+ für normale Priorität. + + + + + project.bom_import.field_mapping.summary + Zusammenfassung der Zuordnung + + + + + project.bom_import.field_mapping.select_to_see_summary + Wählen Sie Zuordnungen aus, um eine Zusammenfassung anzuzeigen. + + + + + project.bom_import.field_mapping.no_suggestion + Kein Vorschlag + + + + + project.bom_import.preview + Vorschau + + + + + project.bom_import.flash.session_expired + Die Import-Sitzung ist abgelaufen. Bitte laden Sie Ihre Datei erneut hoch. + + + + + project.bom_import.field_mapping.ignore + Ignorieren + + + + + project.bom_import.type.kicad_schematic + KiCAD Schaltplaneditor BOM (CSV Datei) + + + + + common.back + Zurück + + + + + project.bom_import.validation.errors.required_field_missing + Zeile %line%: Das Pflichtfeld „%field%“ fehlt oder ist leer. Bitte stellen Sie sicher, dass dieses Feld zugeordnet ist und Daten enthält. + + + + + project.bom_import.validation.errors.no_valid_designators + Zeile %line%: Das Bezeichnungsfeld enthält keine gültigen Komponentenreferenzen. Erwartetes Format: „R1,C2,U3“ oder „R1, C2, U3“. + + + + + project.bom_import.validation.warnings.unusual_designator_format + Zeile %line%: Einige Komponentenreferenzen haben möglicherweise ein ungewöhnliches Format: %designators%. Erwartetes Format: „R1“, „C2“, „U3“ usw. + + + + + project.bom_import.validation.errors.duplicate_designators + Zeile %line%: Doppelte Komponentenreferenzen gefunden: %designators%. Jede Komponente sollte nur einmal pro Zeile referenziert werden. + + + + + project.bom_import.validation.errors.invalid_quantity + Zeile %line%: Die Menge „%quantity%“ ist keine gültige Zahl. Bitte geben Sie einen numerischen Wert ein (z. B. 1, 2,5, 10). + + + + + project.bom_import.validation.errors.quantity_zero_or_negative + Zeile %line%: Die Menge muss größer als 0 sein, erhaltene Menge %quantity%. + + + + + project.bom_import.validation.warnings.quantity_unusually_high + Zeile %line%: Die Menge %quantity% erscheint ungewöhnlich hoch. Bitte überprüfen Sie, ob dies korrekt ist. + + + + + project.bom_import.validation.warnings.quantity_not_whole_number + Zeile %line%: Die Menge %quantity% ist keine ganze Zahl, aber Sie haben %count% Komponentenreferenzen. Dies kann auf eine Nichtübereinstimmung hindeuten. + + + + + project.bom_import.validation.errors.quantity_designator_mismatch + Zeile %line%: Diskrepanz zwischen Menge und Komponentenreferenzen. Menge: %quantity%, Referenzen: %count% (%designators%). Diese sollten übereinstimmen. Passen Sie entweder die Menge an oder überprüfen Sie Ihre Komponentenreferenzen. + + + + + project.bom_import.validation.errors.invalid_partdb_id + Zeile %line%: Part-DB ID „%id%“ ist keine gültige Zahl. Bitte geben Sie eine numerische ID ein. + + + + + project.bom_import.validation.errors.partdb_id_zero_or_negative + Zeile %line%: Die Part-DB ID muss größer als 0 sein, erhaltene ID lautet %id%. + + + + + project.bom_import.validation.warnings.partdb_id_not_found + Zeile %line%: Teil-DB-ID %id% nicht in der Datenbank gefunden. Die Komponente wird ohne Verknüpfung mit einem vorhandenen Teil importiert. + + + + + project.bom_import.validation.info.partdb_link_success + Zeile %line%: Erfolgreich mit dem Bauteil „%name%“ (ID: %id%) verknüpft. + + + + + project.bom_import.validation.warnings.no_component_name + Zeile %line%: Kein Komponentenname/keine Komponentenbezeichnung angegeben (MPN, Bezeichnung oder Wert). Die Komponente wird als „Unbekanntes Bauteil” bezeichnet. + + + + + project.bom_import.validation.warnings.package_name_too_long + Zeile %line%: Der Footprintname „%package%“ ist ungewöhnlich lang. Bitte überprüfen Sie, ob er korrekt ist. + + + + + project.bom_import.validation.info.library_prefix_detected + Zeile %line%: Das Footprint „%package%“ enthält ein Bibliothekspräfix. Dieses wird beim Import automatisch entfernt. + + + + + project.bom_import.validation.errors.non_numeric_field + Zeile %line%: Das Feld „%field%“ enthält den nicht numerischen Wert „%value%“. Bitte geben Sie eine gültige Zahl ein. + + + + + project.bom_import.validation.info.import_summary + Importübersicht: %total% Einträge insgesamt, %valid% gültig, %invalid% mit Problemen. + + + + + project.bom_import.validation.errors.summary + Es wurden %count% Validierungsfehler gefunden, die behoben werden müssen, bevor der Import fortgesetzt werden kann. + + + + + project.bom_import.validation.warnings.summary + Es wurden %count% Warnungen gefunden. Bitte überprüfen Sie diese Probleme, bevor Sie fortfahren. + + + + + project.bom_import.validation.info.all_valid + Alle Einträge haben die Validierung erfolgreich bestanden! + + + + + project.bom_import.validation.summary + Validierungsübersicht + + + + + project.bom_import.validation.total_entries + Gesamtzahl der Einträge + + + + + project.bom_import.validation.valid_entries + Gültige Einträge + + + + + project.bom_import.validation.invalid_entries + Ungültige Einträge + + + + + project.bom_import.validation.success_rate + Erfolgsquote + + + + + project.bom_import.validation.errors.title + Validierungsfehler + + + + + project.bom_import.validation.errors.description + Die folgenden Fehler müssen behoben werden, bevor der Import fortgesetzt werden kann: + + + + + project.bom_import.validation.warnings.title + Validierungswarnungen + + + + + project.bom_import.validation.warnings.description + Die folgenden Warnhinweise sollten vor dem Fortfahren gelesen werden: + + + + + project.bom_import.validation.info.title + Informationen + + + + + project.bom_import.validation.details.title + Detaillierte Validierungsergebnisse + + + + + project.bom_import.validation.details.line + Zeile + + + + + project.bom_import.validation.details.status + Status + + + + + project.bom_import.validation.details.messages + Meldungen + + + + + project.bom_import.validation.details.valid + Gültig + + + + + project.bom_import.validation.details.invalid + Ungültig + + + + + project.bom_import.validation.all_valid + Alle Einträge sind gültig und bereit zum Import! + + + + + project.bom_import.validation.fix_errors + Bitte beheben Sie die Validierungsfehler, bevor Sie mit dem Import fortfahren. + + + + + project.bom_import.type.generic_csv + Generische CSV-Datei + + + + + label_generator.update_profile + Profil mit aktuellen Einstellungen aktualisieren + + + + + label_generator.profile_updated + Labelprofil aktualisiert + + From 71629a696cef2c1ca47249fd30580996561819a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 7 Sep 2025 17:55:55 +0200 Subject: [PATCH 076/228] Use updated gnu unifont --- composer.lock | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/composer.lock b/composer.lock index 6de15830..1f67b80f 100644 --- a/composer.lock +++ b/composer.lock @@ -7513,16 +7513,16 @@ }, { "name": "part-db/label-fonts", - "version": "v1.1.0", + "version": "v1.2.0", "source": { "type": "git", "url": "https://github.com/Part-DB/label-fonts.git", - "reference": "77c84b70ed3bb005df15f30ff835ddec490394b9" + "reference": "c85aeb051d6492961a2c59bc291979f15ce60e88" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Part-DB/label-fonts/zipball/77c84b70ed3bb005df15f30ff835ddec490394b9", - "reference": "77c84b70ed3bb005df15f30ff835ddec490394b9", + "url": "https://api.github.com/repos/Part-DB/label-fonts/zipball/c85aeb051d6492961a2c59bc291979f15ce60e88", + "reference": "c85aeb051d6492961a2c59bc291979f15ce60e88", "shasum": "" }, "type": "library", @@ -7545,9 +7545,9 @@ ], "support": { "issues": "https://github.com/Part-DB/label-fonts/issues", - "source": "https://github.com/Part-DB/label-fonts/tree/v1.1.0" + "source": "https://github.com/Part-DB/label-fonts/tree/v1.2.0" }, - "time": "2024-02-08T21:44:38+00:00" + "time": "2025-09-07T15:42:51+00:00" }, { "name": "part-db/swap", @@ -17883,16 +17883,16 @@ }, { "name": "phpstan/phpstan-doctrine", - "version": "2.0.4", + "version": "2.0.5", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-doctrine.git", - "reference": "6271e66ce37545bd2edcddbe6bcbdd3b665ab7b8" + "reference": "eeff19808f8ae3a6f7c4e43e388a2848eb2b0865" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/6271e66ce37545bd2edcddbe6bcbdd3b665ab7b8", - "reference": "6271e66ce37545bd2edcddbe6bcbdd3b665ab7b8", + "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/eeff19808f8ae3a6f7c4e43e388a2848eb2b0865", + "reference": "eeff19808f8ae3a6f7c4e43e388a2848eb2b0865", "shasum": "" }, "require": { @@ -17949,9 +17949,9 @@ "description": "Doctrine extensions for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-doctrine/issues", - "source": "https://github.com/phpstan/phpstan-doctrine/tree/2.0.4" + "source": "https://github.com/phpstan/phpstan-doctrine/tree/2.0.5" }, - "time": "2025-07-17T11:57:55+00:00" + "time": "2025-09-07T11:52:30+00:00" }, { "name": "phpstan/phpstan-strict-rules", @@ -18003,16 +18003,16 @@ }, { "name": "phpstan/phpstan-symfony", - "version": "2.0.7", + "version": "2.0.8", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-symfony.git", - "reference": "392f7ab8f52a0a776977be4e62535358c28e1b15" + "reference": "8820c22d785c235f69bb48da3d41e688bc8a1796" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/392f7ab8f52a0a776977be4e62535358c28e1b15", - "reference": "392f7ab8f52a0a776977be4e62535358c28e1b15", + "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/8820c22d785c235f69bb48da3d41e688bc8a1796", + "reference": "8820c22d785c235f69bb48da3d41e688bc8a1796", "shasum": "" }, "require": { @@ -18068,9 +18068,9 @@ "description": "Symfony Framework extensions and rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-symfony/issues", - "source": "https://github.com/phpstan/phpstan-symfony/tree/2.0.7" + "source": "https://github.com/phpstan/phpstan-symfony/tree/2.0.8" }, - "time": "2025-07-22T09:40:57+00:00" + "time": "2025-09-07T06:55:50+00:00" }, { "name": "phpunit/php-code-coverage", From 4b00697f02346d223de342a4e30f7cfe41c2c793 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 7 Sep 2025 19:27:02 +0200 Subject: [PATCH 077/228] Allow to customize which items get shown on the homepage and in which order This fixes issue #470 and #894 --- .../SystemSettings/CustomizationSettings.php | 27 ++++++++-- src/Settings/SystemSettings/HomepageItems.php | 51 +++++++++++++++++++ templates/homepage.html.twig | 44 +++++++++++----- translations/messages.en.xlf | 12 +++++ 4 files changed, 117 insertions(+), 17 deletions(-) create mode 100644 src/Settings/SystemSettings/HomepageItems.php diff --git a/src/Settings/SystemSettings/CustomizationSettings.php b/src/Settings/SystemSettings/CustomizationSettings.php index d7e92a51..a5f40cdf 100644 --- a/src/Settings/SystemSettings/CustomizationSettings.php +++ b/src/Settings/SystemSettings/CustomizationSettings.php @@ -28,10 +28,13 @@ use App\Form\Type\ThemeChoiceType; use App\Settings\SettingsIcon; use App\Validator\Constraints\ValidTheme; use Jbtronics\SettingsBundle\Metadata\EnvVarMode; +use Jbtronics\SettingsBundle\ParameterTypes\ArrayType; +use Jbtronics\SettingsBundle\ParameterTypes\EnumType; use Jbtronics\SettingsBundle\Settings\Settings; use Jbtronics\SettingsBundle\Settings\SettingsParameter; use Jbtronics\SettingsBundle\Settings\SettingsTrait; use Symfony\Component\Translation\TranslatableMessage as TM; +use Symfony\Component\Validator\Constraints as Assert; #[Settings(name: "customization", label: new TM("settings.system.customization"))] #[SettingsIcon("fa-paint-roller")] @@ -46,6 +49,13 @@ class CustomizationSettings )] public string $instanceName = "Part-DB"; + #[SettingsParameter( + label: new TM("settings.system.customization.theme"), + formType: ThemeChoiceType::class, formOptions: ['placeholder' => false] + )] + #[ValidTheme] + public string $theme = 'bootstrap'; + #[SettingsParameter( label: new TM("settings.system.customization.banner"), formType: RichTextEditorType::class, formOptions: ['mode' => 'markdown-full'], @@ -53,10 +63,17 @@ class CustomizationSettings )] public ?string $banner = null; - #[SettingsParameter( - label: new TM("settings.system.customization.theme"), - formType: ThemeChoiceType::class, formOptions: ['placeholder' => false] + /** + * @var HomepageItems[] The items to show in the sidebar. + */ + #[SettingsParameter(ArrayType::class, + label: new TM("settings.behavior.hompepage.items"), + description: new TM("settings.behavior.homepage.items.help"), + options: ['type' => EnumType::class, 'options' => ['class' => HomepageItems::class]], + formType: \Symfony\Component\Form\Extension\Core\Type\EnumType::class, + formOptions: ['class' => HomepageItems::class, 'multiple' => true, 'ordered' => true] )] - #[ValidTheme] - public string $theme = 'bootstrap'; + #[Assert\NotBlank()] + #[Assert\Unique()] + public array $homepageitems = [HomepageItems::SEARCH, HomepageItems::BANNER, HomepageItems::FIRST_STEPS, HomepageItems::LICENSE, HomepageItems::LAST_ACTIVITY]; } diff --git a/src/Settings/SystemSettings/HomepageItems.php b/src/Settings/SystemSettings/HomepageItems.php new file mode 100644 index 00000000..7366dfa2 --- /dev/null +++ b/src/Settings/SystemSettings/HomepageItems.php @@ -0,0 +1,51 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Settings\SystemSettings; + +use Symfony\Contracts\Translation\TranslatableInterface; +use Symfony\Contracts\Translation\TranslatorInterface; + +use function Symfony\Component\Translation\t; + +enum HomepageItems: string implements TranslatableInterface +{ + case SEARCH = 'search'; + case BANNER = 'banner'; + case LICENSE = 'license'; + case FIRST_STEPS = 'first_steps'; + case LAST_ACTIVITY = 'last_activity'; + + public function trans(TranslatorInterface $translator, ?string $locale = null): string + { + $key = match($this) { + self::SEARCH => 'search.placeholder', + self::BANNER => 'settings.system.customization.banner', + self::LICENSE => 'homepage.license', + self::FIRST_STEPS => 'homepage.first_steps.title', + self::LAST_ACTIVITY => 'homepage.last_activity', + }; + + return $translator->trans($key, locale: $locale); + } +} diff --git a/templates/homepage.html.twig b/templates/homepage.html.twig index 3f820a53..0db7cf17 100644 --- a/templates/homepage.html.twig +++ b/templates/homepage.html.twig @@ -4,18 +4,13 @@ {% import "components/search.macro.html.twig" as search %} {% import "vars.macro.twig" as vars %} -{% block content %} - - {% if is_granted('@system.show_updates') %} - {{ nv.new_version_alert(new_version_available, new_version, new_version_url) }} - {% endif %} - +{% block item_search %} {% if is_granted('@parts.read') %} {{ search.search_form("standalone") }} -
{% endif %} +{% endblock %} - +{% block item_banner %}

{{ vars.partdb_title() }}

@@ -31,9 +26,11 @@

{% endif %}
+{% endblock %} +{% block item_first_steps %} {% if show_first_steps %} -
+

{% trans %}homepage.first_steps.title{% endtrans %}

@@ -51,8 +48,10 @@
{% endif %} +{% endblock %} -
+{% block item_license %} +

{% trans %}homepage.license{% endtrans %}

@@ -68,9 +67,11 @@ {% trans %}homepage.forum.caption{% endtrans %}: {% trans with {'%href%': 'https://github.com/Part-DB/Part-DB-server/discussions'}%}homepage.forum.text{% endtrans %}
+{% endblock %} +{% block item_last_activity %} {% if datatable is not null %} -
+
{% trans %}homepage.last_activity{% endtrans %}
{% import "components/history_log_macros.html.twig" as log %} @@ -78,4 +79,23 @@
{% endif %} -{% endblock %} \ No newline at end of file +{% endblock %} + +{% block content %} + + {% if is_granted('@system.show_updates') %} + {{ nv.new_version_alert(new_version_available, new_version, new_version_url) }} + {% endif %} + + {% for item in settings_instance('customization').homepageitems %} + {% if block('item_' ~ item.value) is defined %} + {{ block('item_' ~ item.value) }} +
+ {% else %} + + {% endif %} + {% endfor %} + +{% endblock %} diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 41ad8358..7e2a816b 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -13441,5 +13441,17 @@ Please note, that you can not impersonate a disabled user. If you try you will g Label profile updated successfully. + + + settings.behavior.hompepage.items + Homepage items + + + + + settings.behavior.homepage.items.help + + + From cee6d355e8512663b1b1482720d86679230d4576 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 7 Sep 2025 19:43:23 +0200 Subject: [PATCH 078/228] Allow to hide the version number on homepage --- .../SystemSettings/CustomizationSettings.php | 5 +++++ templates/homepage.html.twig | 14 ++++++++------ translations/messages.en.xlf | 6 ++++++ 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/Settings/SystemSettings/CustomizationSettings.php b/src/Settings/SystemSettings/CustomizationSettings.php index a5f40cdf..623e6187 100644 --- a/src/Settings/SystemSettings/CustomizationSettings.php +++ b/src/Settings/SystemSettings/CustomizationSettings.php @@ -76,4 +76,9 @@ class CustomizationSettings #[Assert\NotBlank()] #[Assert\Unique()] public array $homepageitems = [HomepageItems::SEARCH, HomepageItems::BANNER, HomepageItems::FIRST_STEPS, HomepageItems::LICENSE, HomepageItems::LAST_ACTIVITY]; + + #[SettingsParameter( + label: new TM("settings.system.customization.showVersionOnHomepage") + )] + public bool $showVersionOnHomepage = true; } diff --git a/templates/homepage.html.twig b/templates/homepage.html.twig index 0db7cf17..6e7aa360 100644 --- a/templates/homepage.html.twig +++ b/templates/homepage.html.twig @@ -13,12 +13,14 @@ {% block item_banner %}

{{ vars.partdb_title() }}

-

- {% trans %}version.caption{% endtrans %}: {{ shivas_app_version }} - {% if git_branch is not empty or git_commit is not empty %} - ({{ git_branch ?? '' }}/{{ git_commit ?? '' }}) - {% endif %} -

+ {% if settings_instance('customization').showVersionOnHomepage %} +

+ {% trans %}version.caption{% endtrans %}: {{ shivas_app_version }} + {% if git_branch is not empty or git_commit is not empty %} + ({{ git_branch ?? '' }}/{{ git_commit ?? '' }}) + {% endif %} +

+ {% endif %} {% if banner is not empty %}
diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 7e2a816b..b7710f0c 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -13453,5 +13453,11 @@ Please note, that you can not impersonate a disabled user. If you try you will g + + + settings.system.customization.showVersionOnHomepage + Show Part-DB version on homepage + + From c7ec8adc31934c0d5eef7efc0e75dabb02164612 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 7 Sep 2025 19:44:17 +0200 Subject: [PATCH 079/228] Disable settings caching in debug mode Otherwise we run into errors, if a settings get changed --- config/packages/settings.yaml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/config/packages/settings.yaml b/config/packages/settings.yaml index 05e21636..c16d1804 100644 --- a/config/packages/settings.yaml +++ b/config/packages/settings.yaml @@ -5,4 +5,11 @@ jbtronics_settings: default_cacheable: true orm_storage: - default_entity_class: App\Entity\SettingsEntry \ No newline at end of file + default_entity_class: App\Entity\SettingsEntry + + +# Disable caching for development environment +when@dev: + jbtronics_settings: + cache: + default_cacheable: false From 8ff2fc5a82591a11c0c04c5ee823330538c24770 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 7 Sep 2025 19:55:47 +0200 Subject: [PATCH 080/228] Allow to disable the extraction of parameters out of part description and notes Fixes issue #747 --- src/Controller/PartController.php | 7 ++++--- src/Settings/BehaviorSettings/PartInfoSettings.php | 8 +++++++- translations/messages.en.xlf | 12 ++++++++++++ 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/Controller/PartController.php b/src/Controller/PartController.php index b11a5c90..6708ed4c 100644 --- a/src/Controller/PartController.php +++ b/src/Controller/PartController.php @@ -46,6 +46,7 @@ use App\Services\Parameters\ParameterExtractor; use App\Services\Parts\PartLotWithdrawAddHelper; use App\Services\Parts\PricedetailHelper; use App\Services\ProjectSystem\ProjectBuildPartHelper; +use App\Settings\BehaviorSettings\PartInfoSettings; use DateTime; use Doctrine\ORM\EntityManagerInterface; use Exception; @@ -69,7 +70,7 @@ class PartController extends AbstractController protected PartPreviewGenerator $partPreviewGenerator, private readonly TranslatorInterface $translator, private readonly AttachmentSubmitHandler $attachmentSubmitHandler, private readonly EntityManagerInterface $em, - protected EventCommentHelper $commentHelper) + protected EventCommentHelper $commentHelper, private readonly PartInfoSettings $partInfoSettings) { } @@ -119,8 +120,8 @@ class PartController extends AbstractController 'pricedetail_helper' => $this->pricedetailHelper, 'pictures' => $this->partPreviewGenerator->getPreviewAttachments($part), 'timeTravel' => $timeTravel_timestamp, - 'description_params' => $parameterExtractor->extractParameters($part->getDescription()), - 'comment_params' => $parameterExtractor->extractParameters($part->getComment()), + 'description_params' => $this->partInfoSettings->extractParamsFromDescription ? $parameterExtractor->extractParameters($part->getDescription()) : [], + 'comment_params' => $this->partInfoSettings->extractParamsFromNotes ? $parameterExtractor->extractParameters($part->getComment()) : [], 'withdraw_add_helper' => $withdrawAddHelper, ] ); diff --git a/src/Settings/BehaviorSettings/PartInfoSettings.php b/src/Settings/BehaviorSettings/PartInfoSettings.php index 4c44b9bb..f017c846 100644 --- a/src/Settings/BehaviorSettings/PartInfoSettings.php +++ b/src/Settings/BehaviorSettings/PartInfoSettings.php @@ -40,4 +40,10 @@ class PartInfoSettings #[SettingsParameter(label: new TM("settings.behavior.part_info.show_part_image_overlay"), description: new TM("settings.behavior.part_info.show_part_image_overlay.help"), envVar: "bool:SHOW_PART_IMAGE_OVERLAY", envVarMode: EnvVarMode::OVERWRITE)] public bool $showPartImageOverlay = true; -} \ No newline at end of file + + #[SettingsParameter(label: new TM("settings.behavior.part_info.extract_params_from_description"))] + public bool $extractParamsFromDescription = true; + + #[SettingsParameter(label: new TM("settings.behavior.part_info.extract_params_from_notes"))] + public bool $extractParamsFromNotes = true; +} diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index b7710f0c..6680521b 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -13459,5 +13459,17 @@ Please note, that you can not impersonate a disabled user. If you try you will g Show Part-DB version on homepage + + + settings.behavior.part_info.extract_params_from_description + Extract parameters from part description + + + + + settings.behavior.part_info.extract_params_from_notes + Extract parameters from part notes + + From 1f669a9c5334b1d3f8302abb3f47e6489310da01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 7 Sep 2025 20:04:48 +0200 Subject: [PATCH 081/228] Readded option to show all elements in a table --- src/Controller/AttachmentFileController.php | 3 ++- src/Controller/PartListsController.php | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Controller/AttachmentFileController.php b/src/Controller/AttachmentFileController.php index 7917e97f..81369e12 100644 --- a/src/Controller/AttachmentFileController.php +++ b/src/Controller/AttachmentFileController.php @@ -24,6 +24,7 @@ namespace App\Controller; use App\DataTables\AttachmentDataTable; use App\DataTables\Filters\AttachmentFilter; +use App\DataTables\PartsDataTable; use App\Entity\Attachments\Attachment; use App\Form\Filters\AttachmentFilterType; use App\Services\Attachments\AttachmentManager; @@ -112,7 +113,7 @@ class AttachmentFileController extends AbstractController $filterForm->handleRequest($formRequest); - $table = $dataTableFactory->createFromType(AttachmentDataTable::class, ['filter' => $filter], ['pageLength' => $tableSettings->fullDefaultPageSize]) + $table = $dataTableFactory->createFromType(AttachmentDataTable::class, ['filter' => $filter], ['pageLength' => $tableSettings->fullDefaultPageSize, 'lengthMenu' => PartsDataTable::LENGTH_MENU]) ->handleRequest($request); if ($table->isCallback()) { diff --git a/src/Controller/PartListsController.php b/src/Controller/PartListsController.php index f6836ddc..b2df18c1 100644 --- a/src/Controller/PartListsController.php +++ b/src/Controller/PartListsController.php @@ -161,7 +161,9 @@ class PartListsController extends AbstractController $filterForm->handleRequest($formRequest); - $table = $this->dataTableFactory->createFromType(PartsDataTable::class, array_merge(['filter' => $filter], $additional_table_vars), ['pageLength' => $this->tableSettings->fullDefaultPageSize]) + $table = $this->dataTableFactory->createFromType(PartsDataTable::class, array_merge( + ['filter' => $filter], $additional_table_vars), + ['pageLength' => $this->tableSettings->fullDefaultPageSize, 'lengthMenu' => PartsDataTable::LENGTH_MENU]) ->handleRequest($request); if ($table->isCallback()) { From 0d1ae030be0cc2fbb0075891dc992d5ee3f757d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 7 Sep 2025 20:42:33 +0200 Subject: [PATCH 082/228] Allow to select default info providers for search This fixes issue #556 --- src/Controller/InfoProviderController.php | 20 ++++++++- .../InfoProviderSystem/ProviderSelectType.php | 45 ++++++++++++++++--- .../InfoProviderGeneralSettings.php | 45 +++++++++++++++++++ .../InfoProviderSettings.php | 6 ++- translations/messages.en.xlf | 18 ++++++++ 5 files changed, 125 insertions(+), 9 deletions(-) create mode 100644 src/Settings/InfoProviderSystem/InfoProviderGeneralSettings.php diff --git a/src/Controller/InfoProviderController.php b/src/Controller/InfoProviderController.php index a6e886e6..dae8213e 100644 --- a/src/Controller/InfoProviderController.php +++ b/src/Controller/InfoProviderController.php @@ -30,6 +30,7 @@ use App\Services\InfoProviderSystem\ExistingPartFinder; use App\Services\InfoProviderSystem\PartInfoRetriever; use App\Services\InfoProviderSystem\ProviderRegistry; use App\Settings\AppSettings; +use App\Settings\InfoProviderSystem\InfoProviderGeneralSettings; use Doctrine\ORM\EntityManagerInterface; use Jbtronics\SettingsBundle\Form\SettingsFormFactoryInterface; use Jbtronics\SettingsBundle\Manager\SettingsManagerInterface; @@ -113,7 +114,7 @@ class InfoProviderController extends AbstractController #[Route('/search', name: 'info_providers_search')] #[Route('/update/{target}', name: 'info_providers_update_part_search')] - public function search(Request $request, #[MapEntity(id: 'target')] ?Part $update_target, LoggerInterface $exceptionLogger): Response + public function search(Request $request, #[MapEntity(id: 'target')] ?Part $update_target, LoggerInterface $exceptionLogger, InfoProviderGeneralSettings $infoProviderSettings): Response { $this->denyAccessUnlessGranted('@info_providers.create_parts'); @@ -144,6 +145,23 @@ class InfoProviderController extends AbstractController } } + //If the providers form is still empty, use our default value from the settings + if (count($form->get('providers')->getData() ?? []) === 0) { + $default_providers = $infoProviderSettings->defaultSearchProviders; + $provider_objects = []; + foreach ($default_providers as $provider_key) { + try { + $tmp = $this->providerRegistry->getProviderByKey($provider_key); + if ($tmp->isActive()) { + $provider_objects[] = $tmp; + } + } catch (\InvalidArgumentException $e) { + //If the provider is not found, just ignore it + } + } + $form->get('providers')->setData($provider_objects); + } + if ($form->isSubmitted() && $form->isValid()) { $keyword = $form->get('keyword')->getData(); $providers = $form->get('providers')->getData(); diff --git a/src/Form/InfoProviderSystem/ProviderSelectType.php b/src/Form/InfoProviderSystem/ProviderSelectType.php index a9373390..95e10791 100644 --- a/src/Form/InfoProviderSystem/ProviderSelectType.php +++ b/src/Form/InfoProviderSystem/ProviderSelectType.php @@ -28,6 +28,7 @@ use App\Services\InfoProviderSystem\Providers\InfoProviderInterface; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\ChoiceList\ChoiceList; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; class ProviderSelectType extends AbstractType @@ -44,13 +45,43 @@ class ProviderSelectType extends AbstractType public function configureOptions(OptionsResolver $resolver): void { - $resolver->setDefaults([ - 'choices' => $this->providerRegistry->getActiveProviders(), - 'choice_label' => ChoiceList::label($this, static fn (?InfoProviderInterface $choice) => $choice?->getProviderInfo()['name']), - 'choice_value' => ChoiceList::value($this, static fn(?InfoProviderInterface $choice) => $choice?->getProviderKey()), + $providers = $this->providerRegistry->getActiveProviders(); - 'multiple' => true, - ]); + $resolver->setDefault('input', 'object'); + $resolver->setAllowedTypes('input', 'string'); + //Either the form returns the provider objects or their keys + $resolver->setAllowedValues('input', ['object', 'string']); + $resolver->setDefault('multiple', true); + + $resolver->setDefault('choices', function (Options $options) use ($providers) { + if ('object' === $options['input']) { + return $this->providerRegistry->getActiveProviders(); + } + + $tmp = []; + foreach ($providers as $provider) { + $name = $provider->getProviderInfo()['name']; + $tmp[$name] = $provider->getProviderKey(); + } + + return $tmp; + }); + + //The choice_label and choice_value only needs to be set if we want the objects + $resolver->setDefault('choice_label', function (Options $options){ + if ('object' === $options['input']) { + return ChoiceList::label($this, static fn (?InfoProviderInterface $choice) => $choice?->getProviderInfo()['name']); + } + + return null; + }); + $resolver->setDefault('choice_value', function (Options $options) { + if ('object' === $options['input']) { + return ChoiceList::value($this, static fn(?InfoProviderInterface $choice) => $choice?->getProviderKey()); + } + + return null; + }); } -} \ No newline at end of file +} diff --git a/src/Settings/InfoProviderSystem/InfoProviderGeneralSettings.php b/src/Settings/InfoProviderSystem/InfoProviderGeneralSettings.php new file mode 100644 index 00000000..03fff0bf --- /dev/null +++ b/src/Settings/InfoProviderSystem/InfoProviderGeneralSettings.php @@ -0,0 +1,45 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Settings\InfoProviderSystem; + +use App\Form\InfoProviderSystem\ProviderSelectType; +use App\Settings\SettingsIcon; +use Jbtronics\SettingsBundle\ParameterTypes\ArrayType; +use Jbtronics\SettingsBundle\ParameterTypes\StringType; +use Jbtronics\SettingsBundle\Settings\Settings; +use Jbtronics\SettingsBundle\Settings\SettingsParameter; +use Symfony\Component\Translation\TranslatableMessage as TM; + +#[Settings(label: new TM("settings.ips.general"))] +#[SettingsIcon("fa-magnifying-glass")] +class InfoProviderGeneralSettings +{ + /** + * @var string[] + */ + #[SettingsParameter(type: ArrayType::class, label: new TM("settings.ips.default_providers"), + description: new TM("settings.ips.default_providers.help"), options: ['type' => StringType::class], + formType: ProviderSelectType::class, formOptions: ['input' => 'string'])] + public array $defaultSearchProviders = []; +} diff --git a/src/Settings/InfoProviderSystem/InfoProviderSettings.php b/src/Settings/InfoProviderSystem/InfoProviderSettings.php index 3c7159cb..c223bd88 100644 --- a/src/Settings/InfoProviderSystem/InfoProviderSettings.php +++ b/src/Settings/InfoProviderSystem/InfoProviderSettings.php @@ -25,6 +25,7 @@ namespace App\Settings\InfoProviderSystem; use Jbtronics\SettingsBundle\Settings\EmbeddedSettings; use Jbtronics\SettingsBundle\Settings\Settings; +use Jbtronics\SettingsBundle\Settings\SettingsParameter; use Jbtronics\SettingsBundle\Settings\SettingsTrait; #[Settings()] @@ -32,6 +33,9 @@ class InfoProviderSettings { use SettingsTrait; + #[EmbeddedSettings] + public ?InfoProviderGeneralSettings $general = null; + #[EmbeddedSettings] public ?DigikeySettings $digikey = null; @@ -58,4 +62,4 @@ class InfoProviderSettings #[EmbeddedSettings] public ?PollinSettings $pollin = null; -} \ No newline at end of file +} diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 6680521b..68bbb653 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -13471,5 +13471,23 @@ Please note, that you can not impersonate a disabled user. If you try you will g Extract parameters from part notes + + + settings.ips.default_providers + Default search providers + + + + + settings.ips.general + General settings + + + + + settings.ips.default_providers.help + These providers will be preselected for searches in part providers. + + From ecd2abe00ea20ed40a9e2816300ac71e45fb4312 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 7 Sep 2025 21:21:08 +0200 Subject: [PATCH 083/228] Made image size of preview images in tables configurable and slightly bigger by default This makes PR #984 and #623 obsolete --- assets/css/app/images.css | 6 +++--- src/Settings/BehaviorSettings/TableSettings.php | 16 +++++++++++++++- templates/base.html.twig | 8 ++++++++ translations/messages.en.xlf | 12 ++++++++++++ 4 files changed, 38 insertions(+), 4 deletions(-) diff --git a/assets/css/app/images.css b/assets/css/app/images.css index 214776e7..0212a85b 100644 --- a/assets/css/app/images.css +++ b/assets/css/app/images.css @@ -18,8 +18,8 @@ */ .hoverpic { - min-width: 10px; - max-width: 30px; + min-width: var(--table-image-preview-min-size, 20px); + max-width: var(--table-image-preview-max-size, 35px); display: block; margin-left: auto; margin-right: auto; @@ -49,7 +49,7 @@ } .part-table-image { - max-height: 40px; + max-height: calc(1.2*var(--table-image-preview-max-size, 35px)); /** Aspect ratio of maximum 1.2 */ object-fit: contain; } diff --git a/src/Settings/BehaviorSettings/TableSettings.php b/src/Settings/BehaviorSettings/TableSettings.php index 7b4e7912..b6964876 100644 --- a/src/Settings/BehaviorSettings/TableSettings.php +++ b/src/Settings/BehaviorSettings/TableSettings.php @@ -70,6 +70,20 @@ class TableSettings PartTableColumns::CATEGORY, PartTableColumns::FOOTPRINT, PartTableColumns::MANUFACTURER, PartTableColumns::LOCATION, PartTableColumns::AMOUNT]; + #[SettingsParameter(label: new TM("settings.behavior.table.preview_image_min_width"), + formOptions: ['attr' => ['min' => 1, 'max' => 100]], + envVar: "int:TABLE_IMAGE_PREVIEW_MIN_SIZE", envVarMode: EnvVarMode::OVERWRITE + )] + #[Assert\Range(min: 1, max: 100)] + public int $previewImageMinWidth = 20; + + #[SettingsParameter(label: new TM("settings.behavior.table.preview_image_max_width"), + formOptions: ['attr' => ['min' => 1, 'max' => 100]], + envVar: "int:TABLE_IMAGE_PREVIEW_MAX_SIZE", envVarMode: EnvVarMode::OVERWRITE + )] + #[Assert\Range(min: 1, max: 100)] + #[Assert\GreaterThanOrEqual(propertyPath: 'previewImageMinWidth')] + public int $previewImageMaxWidth = 35; public static function mapPartsDefaultColumnsEnv(string $columns): array { @@ -87,4 +101,4 @@ class TableSettings return $ret; } -} \ No newline at end of file +} diff --git a/templates/base.html.twig b/templates/base.html.twig index 48e45ab0..bb9844fa 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -53,6 +53,14 @@ {% endif %} {{ encore_entry_link_tags('app') }} + + {% set table_settings = settings_instance('table') %} + {% endblock %} {% block javascripts %} diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 68bbb653..88ae764a 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -13489,5 +13489,17 @@ Please note, that you can not impersonate a disabled user. If you try you will g These providers will be preselected for searches in part providers. + + + settings.behavior.table.preview_image_max_width + Preview image max width (px) + + + + + settings.behavior.table.preview_image_min_width + Preview image min width (px) + + From e81c8470beebd8f4665a6360981248239a0178b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 7 Sep 2025 21:51:58 +0200 Subject: [PATCH 084/228] Made part table action bar sticky floating Related to PR #997 --- .../elements/datatables/parts_controller.js | 2 ++ assets/css/app/tables.css | 12 +++++++++++- templates/components/datatables.macro.html.twig | 4 ++-- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/assets/controllers/elements/datatables/parts_controller.js b/assets/controllers/elements/datatables/parts_controller.js index 1fe11a20..c43fa276 100644 --- a/assets/controllers/elements/datatables/parts_controller.js +++ b/assets/controllers/elements/datatables/parts_controller.js @@ -45,8 +45,10 @@ export default class extends DatatablesController { //Hide/Unhide panel with the selection tools if (count > 0) { selectPanel.classList.remove('d-none'); + selectPanel.classList.add('sticky-select-bar'); } else { selectPanel.classList.add('d-none'); + selectPanel.classList.remove('sticky-select-bar'); } //Update selection count text diff --git a/assets/css/app/tables.css b/assets/css/app/tables.css index ae892f50..aa72fff3 100644 --- a/assets/css/app/tables.css +++ b/assets/css/app/tables.css @@ -17,6 +17,16 @@ * along with this program. If not, see . */ +/**************************************** + * Action bar + ****************************************/ + +.sticky-select-bar { + position: sticky; + top: 120px; + z-index: 3000; /* Ensure the bar is above other content */ +} + /**************************************** * Tables ****************************************/ @@ -109,4 +119,4 @@ Classes for Datatables export #export-messageTop, .export-helper{ display: none; -} \ No newline at end of file +} diff --git a/templates/components/datatables.macro.html.twig b/templates/components/datatables.macro.html.twig index 5ce0f23f..447aa69c 100644 --- a/templates/components/datatables.macro.html.twig +++ b/templates/components/datatables.macro.html.twig @@ -29,7 +29,7 @@ -
+
{# #}
@@ -95,4 +95,4 @@
-{% endmacro %} \ No newline at end of file +{% endmacro %} From c2cbbee0df692a6787bd956e4d7eca77161cb781 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 7 Sep 2025 21:59:30 +0200 Subject: [PATCH 085/228] Ensure that part table action bar dont overlap our navbar dropdowns --- assets/css/app/tables.css | 2 +- templates/components/datatables.macro.html.twig | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/css/app/tables.css b/assets/css/app/tables.css index aa72fff3..8d4b200c 100644 --- a/assets/css/app/tables.css +++ b/assets/css/app/tables.css @@ -24,7 +24,7 @@ .sticky-select-bar { position: sticky; top: 120px; - z-index: 3000; /* Ensure the bar is above other content */ + z-index: 1000; /* Ensure the bar is above other content */ } /**************************************** diff --git a/templates/components/datatables.macro.html.twig b/templates/components/datatables.macro.html.twig index 447aa69c..009f815e 100644 --- a/templates/components/datatables.macro.html.twig +++ b/templates/components/datatables.macro.html.twig @@ -29,7 +29,7 @@ -
+
{# #}
From 6ff7f64384beec9a5b6a149e42231bcc2bd4f4f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 7 Sep 2025 22:37:11 +0200 Subject: [PATCH 086/228] New translations messages.en.xlf (German) --- translations/messages.de.xlf | 79 +++++++++++++++++++++++++++--------- 1 file changed, 60 insertions(+), 19 deletions(-) diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index 8515abb8..9fb3f6ef 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -8553,16 +8553,6 @@ Element 1 -> Element 1.2 Authenticator App - - - obsolete - obsolete - - - Login successful - Login erfolgreich. - - obsolete @@ -8688,15 +8678,6 @@ Element 1 -> Element 1.2 Sicherheitsschlüssel erfolgreich hinzugefügt. - - - obsolete - - - Username - Benutzername - - obsolete @@ -13440,5 +13421,65 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön Labelprofil aktualisiert + + + settings.behavior.hompepage.items + Startseiten-Elemente + + + + + settings.behavior.homepage.items.help + Die Elemente, die auf der Startseite angezeigt werden sollen. Die Reihenfolge kann per Drag & Drop geändert werden. + + + + + settings.system.customization.showVersionOnHomepage + Part-DB-Version auf der Startseite anzeigen + + + + + settings.behavior.part_info.extract_params_from_description + Parameter aus der Bauteilebeschreibung extrahieren + + + + + settings.behavior.part_info.extract_params_from_notes + Parameter aus der Bauteilenotiz extrahieren + + + + + settings.ips.default_providers + Standard-Suchquellen + + + + + settings.ips.general + Allgemeine Einstellungen + + + + + settings.ips.default_providers.help + Diese Anbieter werden für die Suche in Informationsquellen vorausgewählt. + + + + + settings.behavior.table.preview_image_max_width + Max. Vorschaubilde-Breite (px) + + + + + settings.behavior.table.preview_image_min_width + Min. Vorschaubilde-Breite (px) + + From 8d2ff6f5d75de290ff8a53cfe0aba98fe266319f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 7 Sep 2025 22:37:16 +0200 Subject: [PATCH 087/228] New translations messages.en.xlf (English) --- translations/messages.en.xlf | 253 +++++++++++++++++++---------------- 1 file changed, 138 insertions(+), 115 deletions(-) diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index b7710f0c..af70cb50 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -242,7 +242,7 @@ part.info.timetravel_hint - Please note that this feature is experimental, so the info may not be correct.]]> + This is how the part appeared before %timestamp%. <i>Please note that this feature is experimental, so the info may not be correct.</i> @@ -731,10 +731,10 @@ user.edit.tfa.disable_tfa_message - all active two-factor authentication methods of the user and delete the backup codes! -
-The user will have to set up all two-factor authentication methods again and print new backup codes!

-Only do this if you are absolutely sure about the identity of the user (seeking help), otherwise the account could be compromised by an attacker!]]>
+ This will disable <b>all active two-factor authentication methods of the user</b> and delete the <b>backup codes</b>! +<br> +The user will have to set up all two-factor authentication methods again and print new backup codes! <br><br> +<b>Only do this if you are absolutely sure about the identity of the user (seeking help), otherwise the account could be compromised by an attacker!</b>
@@ -885,9 +885,9 @@ The user will have to set up all two-factor authentication methods again and pri entity.delete.message - -Sub elements will be moved upwards.]]> + This can not be undone! +<br> +Sub elements will be moved upwards. @@ -1441,7 +1441,7 @@ Sub elements will be moved upwards.]]> homepage.github.text - GitHub project page]]> + Source, downloads, bug reports, to-do-list etc. can be found on <a href="%href%" class="link-external" target="_blank">GitHub project page</a> @@ -1463,7 +1463,7 @@ Sub elements will be moved upwards.]]> homepage.help.text - GitHub page]]> + Help and tips can be found in Wiki the <a href="%href%" class="link-external" target="_blank">GitHub page</a> @@ -1705,7 +1705,7 @@ Sub elements will be moved upwards.]]> email.pw_reset.fallback - %url% and enter the following info]]> + If this does not work for you, go to <a href="%url%">%url%</a> and enter the following info @@ -1735,7 +1735,7 @@ Sub elements will be moved upwards.]]> email.pw_reset.valid_unit %date% - %date%.]]> + The reset token will be valid until <i>%date%</i>. @@ -3578,8 +3578,8 @@ Sub elements will be moved upwards.]]> tfa_google.disable.confirm_message - -Also note that without two-factor authentication, your account is no longer as well protected against attackers!]]> + If you disable the Authenticator App, all backup codes will be deleted, so you may need to reprint them.<br> +Also note that without two-factor authentication, your account is no longer as well protected against attackers! @@ -3599,7 +3599,7 @@ Also note that without two-factor authentication, your account is no longer as w tfa_google.step.download - Google Authenticator oder FreeOTP Authenticator)]]> + Download an authenticator app (e.g. <a class="link-external" target="_blank" href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2">Google Authenticator</a> oder <a class="link-external" target="_blank" href="https://play.google.com/store/apps/details?id=org.fedorahosted.freeotp">FreeOTP Authenticator</a>) @@ -3841,8 +3841,8 @@ Also note that without two-factor authentication, your account is no longer as w tfa_trustedDevices.explanation - all computers here.]]> + When checking the second factor, the current computer can be marked as trustworthy, so no more two-factor checks on this computer are needed. +If you have done this incorrectly or if a computer is no longer trusted, you can reset the status of <i>all </i>computers here. @@ -5313,7 +5313,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can label_options.lines_mode.help - Twig documentation and Wiki for more information.]]> + If you select Twig here, the content field is interpreted as Twig template. See <a href="https://twig.symfony.com/doc/3.x/templates.html">Twig documentation</a> and <a href="https://docs.part-db.de/usage/labels.html#twig-mode">Wiki</a> for more information. @@ -7157,15 +7157,15 @@ Exampletown mass_creation.lines.placeholder - Element 1 Element 1.1 Element 1.1.1 Element 1.2 Element 2 Element 3 -Element 1 -> Element 1.1 -Element 1 -> Element 1.2]]> +Element 1 -> Element 1.1 +Element 1 -> Element 1.2 @@ -8554,16 +8554,6 @@ Element 1 -> Element 1.2]]> Authenticator app - - - obsolete - obsolete - - - Login successful - Login successful - - obsolete @@ -8689,15 +8679,6 @@ Element 1 -> Element 1.2]]> Security key added successfully. - - - obsolete - - - Username - Username - - obsolete @@ -9391,25 +9372,25 @@ Element 1 -> Element 1.2]]> filter.parameter_value_constraint.operator.< - + Typ. Value < filter.parameter_value_constraint.operator.> - ]]> + Typ. Value > filter.parameter_value_constraint.operator.<= - + Typ. Value <= filter.parameter_value_constraint.operator.>= - =]]> + Typ. Value >= @@ -9517,7 +9498,7 @@ Element 1 -> Element 1.2]]> parts_list.search.searching_for - %keyword%]]> + Searching parts with keyword <b>%keyword%</b> @@ -10177,13 +10158,13 @@ Element 1 -> Element 1.2]]> project.builds.number_of_builds_possible - %max_builds% builds of this project.]]> + You have enough stocked to build <b>%max_builds%</b> builds of this project. project.builds.check_project_status - "%project_status%". You should check if you really want to build the project with this status!]]> + The current project status is <b>"%project_status%"</b>. You should check if you really want to build the project with this status! @@ -10285,7 +10266,7 @@ Element 1 -> Element 1.2]]> entity.select.add_hint - to create nested structures, e.g. "Node 1->Node 1.1"]]> + Use -> to create nested structures, e.g. "Node 1->Node 1.1" @@ -10309,13 +10290,13 @@ Element 1 -> Element 1.2]]> homepage.first_steps.introduction - documentation or start to creating the following data structures:]]> + Your database is still empty. You might want to read the <a href="%url%">documentation</a> or start to creating the following data structures: homepage.first_steps.create_part - create a new part.]]> + Or you can directly <a href="%url%">create a new part</a>. @@ -10327,7 +10308,7 @@ Element 1 -> Element 1.2]]> homepage.forum.text - discussion forum]]> + For questions about Part-DB use the <a href="%href%" class="link-external" target="_blank">discussion forum</a> @@ -10981,7 +10962,7 @@ Element 1 -> Element 1.2]]> parts.import.help_documentation - documentation for more information on the file format.]]> + See the <a href="%link%">documentation</a> for more information on the file format. @@ -11161,7 +11142,7 @@ Element 1 -> Element 1.2]]> part.filter.lessThanDesired - + In stock less than desired (total amount < min. amount) @@ -11973,13 +11954,13 @@ Please note, that you can not impersonate a disabled user. If you try you will g part.merge.confirm.title - %other% into %target%?]]> + Do you really want to merge <b>%other%</b> into <b>%target%</b>? part.merge.confirm.message - %other% will be deleted, and the part will be saved with the shown information.]]> + <b>%other%</b> will be deleted, and the part will be saved with the shown information. @@ -12333,7 +12314,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g settings.ips.element14.apiKey.help - https://partner.element14.com/.]]> + You can register for an API key on <a href="https://partner.element14.com/">https://partner.element14.com/</a>. @@ -12345,7 +12326,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g settings.ips.element14.storeId.help - here for a list of valid domains.]]> + The store domain to retrieve the data from. This decides the language and currency of results. See <a href="https://partner.element14.com/docs/Product_Search_API_REST__Description">here</a> for a list of valid domains. @@ -12363,7 +12344,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g settings.ips.tme.token.help - https://developers.tme.eu/en/.]]> + You can get an API token and secret on <a href="https://developers.tme.eu/en/">https://developers.tme.eu/en/</a>. @@ -12411,7 +12392,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g settings.ips.mouser.apiKey.help - https://eu.mouser.com/api-hub/.]]> + You can register for an API key on <a href="https://eu.mouser.com/api-hub/">https://eu.mouser.com/api-hub/</a>. @@ -12489,7 +12470,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g settings.system.attachments - + Attachments & Files @@ -12513,7 +12494,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g settings.system.attachments.allowDownloads.help - Attention: This can be a security issue, as it might allow users to access intranet ressources via Part-DB!]]> + With this option users can download external files into Part-DB by providing an URL. <b>Attention: This can be a security issue, as it might allow users to access intranet ressources via Part-DB!</b> @@ -12687,8 +12668,8 @@ Please note, that you can not impersonate a disabled user. If you try you will g settings.system.localization.base_currency_description - Please note that the currencies are not converted, when changing this value. So changing the default currency after you already added price information, will result in wrong prices!]]> + The currency that is used to store price information and exchange rates in. This currency is assumed, when no currency is set for a price information. +<b>Please note that the currencies are not converted, when changing this value. So changing the default currency after you already added price information, will result in wrong prices!</b> @@ -12718,7 +12699,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g settings.misc.kicad_eda.category_depth.help - 0 to show more levels. Set to -1, to show all parts of Part-DB inside a sigle cnategory in KiCad.]]> + 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. @@ -12736,7 +12717,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g settings.behavior.sidebar.items.help - + The menus which appear at the sidebar by default. Order of items can be changed via drag & drop. @@ -12784,7 +12765,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g settings.behavior.table.parts_default_columns.help - + The columns to show by default in part tables. Order of items can be changed via drag & drop. @@ -12838,7 +12819,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g settings.ips.oemsecrets.sortMode.M - + Completeness & Manufacturer name @@ -13178,286 +13159,328 @@ Please note, that you can not impersonate a disabled user. If you try you will g - + project.bom_import.type.kicad_schematic KiCAD Schematic BOM (CSV file) - + common.back Back - + project.bom_import.validation.errors.required_field_missing Line %line%: Required field "%field%" is missing or empty. Please ensure this field is mapped and contains data. - + project.bom_import.validation.errors.no_valid_designators Line %line%: Designator field contains no valid component references. Expected format: "R1,C2,U3" or "R1, C2, U3". - + project.bom_import.validation.warnings.unusual_designator_format Line %line%: Some component references may have unusual format: %designators%. Expected format: "R1", "C2", "U3", etc. - + project.bom_import.validation.errors.duplicate_designators Line %line%: Duplicate component references found: %designators%. Each component should be referenced only once per line. - + project.bom_import.validation.errors.invalid_quantity Line %line%: Quantity "%quantity%" is not a valid number. Please enter a numeric value (e.g., 1, 2.5, 10). - + project.bom_import.validation.errors.quantity_zero_or_negative Line %line%: Quantity must be greater than 0, got %quantity%. - + project.bom_import.validation.warnings.quantity_unusually_high Line %line%: Quantity %quantity% seems unusually high. Please verify this is correct. - + project.bom_import.validation.warnings.quantity_not_whole_number Line %line%: Quantity %quantity% is not a whole number, but you have %count% component references. This may indicate a mismatch. - + project.bom_import.validation.errors.quantity_designator_mismatch Line %line%: Mismatch between quantity and component references. Quantity: %quantity%, References: %count% (%designators%). These should match. Either adjust the quantity or check your component references. - + project.bom_import.validation.errors.invalid_partdb_id Line %line%: Part-DB ID "%id%" is not a valid number. Please enter a numeric ID. - + project.bom_import.validation.errors.partdb_id_zero_or_negative Line %line%: Part-DB ID must be greater than 0, got %id%. - + project.bom_import.validation.warnings.partdb_id_not_found Line %line%: Part-DB ID %id% not found in database. The component will be imported without linking to an existing part. - + project.bom_import.validation.info.partdb_link_success Line %line%: Successfully linked to Part-DB part "%name%" (ID: %id%). - + project.bom_import.validation.warnings.no_component_name Line %line%: No component name/designation provided (MPN, Designation, or Value). Component will be named "Unknown Component". - + project.bom_import.validation.warnings.package_name_too_long Line %line%: Package name "%package%" is unusually long. Please verify this is correct. - + project.bom_import.validation.info.library_prefix_detected Line %line%: Package "%package%" contains library prefix. This will be automatically removed during import. - + project.bom_import.validation.errors.non_numeric_field Line %line%: Field "%field%" contains non-numeric value "%value%". Please enter a valid number. - + project.bom_import.validation.info.import_summary Import summary: %total% total entries, %valid% valid, %invalid% with issues. - + project.bom_import.validation.errors.summary Found %count% validation error(s) that must be fixed before import can proceed. - + project.bom_import.validation.warnings.summary Found %count% warning(s). Please review these issues before proceeding. - + project.bom_import.validation.info.all_valid All entries passed validation successfully! - + project.bom_import.validation.summary Validation Summary - + project.bom_import.validation.total_entries Total Entries - + project.bom_import.validation.valid_entries Valid Entries - + project.bom_import.validation.invalid_entries Invalid Entries - + project.bom_import.validation.success_rate Success Rate - + project.bom_import.validation.errors.title Validation Errors - + project.bom_import.validation.errors.description The following errors must be fixed before the import can proceed: - + project.bom_import.validation.warnings.title Validation Warnings - + project.bom_import.validation.warnings.description The following warnings should be reviewed before proceeding: - + project.bom_import.validation.info.title Information - + project.bom_import.validation.details.title Detailed Validation Results - + project.bom_import.validation.details.line Line - + project.bom_import.validation.details.status Status - + project.bom_import.validation.details.messages Messages - + project.bom_import.validation.details.valid Valid - + project.bom_import.validation.details.invalid Invalid - + project.bom_import.validation.all_valid All entries are valid and ready for import! - + project.bom_import.validation.fix_errors Please fix the validation errors before proceeding with the import. - + project.bom_import.type.generic_csv Generic CSV - + label_generator.update_profile Update profile with current settings - + label_generator.profile_updated Label profile updated successfully. - + settings.behavior.hompepage.items Homepage items - + settings.behavior.homepage.items.help - + The items to show at the homepage. Order can be changed via drag & drop. - + settings.system.customization.showVersionOnHomepage Show Part-DB version on homepage + + + settings.behavior.part_info.extract_params_from_description + Extract parameters from part description + + + + + settings.behavior.part_info.extract_params_from_notes + Extract parameters from part notes + + + + + settings.ips.default_providers + Default search providers + + + + + settings.ips.general + General settings + + + + + settings.ips.default_providers.help + These providers will be preselected for searches in part providers. + + + + + settings.behavior.table.preview_image_max_width + Preview image max width (px) + + + + + settings.behavior.table.preview_image_min_width + Preview image min width (px) + + From 03f7ad66d2ce62572df24d8c55e42696b856b691 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 7 Sep 2025 23:16:15 +0200 Subject: [PATCH 088/228] Bumped version to 2.1.0 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index e9307ca5..7ec1d6db 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.0.2 +2.1.0 From cdc58507dbca1a0bccbf6ce1e45e89fab5b5ad45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 7 Sep 2025 23:58:21 +0200 Subject: [PATCH 089/228] Removed style nonce, as it blocks the loading of all other inline styles and kills the styling of the sidebar treeviews --- templates/base.html.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/base.html.twig b/templates/base.html.twig index bb9844fa..ee79549b 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -55,7 +55,7 @@ {{ encore_entry_link_tags('app') }} {% set table_settings = settings_instance('table') %} -