Compare commits

..

54 commits

Author SHA1 Message Date
Copilot
a356eecd74
Replace type synonym placeholders with bracket notation for better DX (#1133)
Some checks are pending
Build assets artifact / Build assets artifact (push) Waiting to run
Docker Image Build / docker (push) Waiting to run
Docker Image Build (FrankenPHP) / docker (push) Waiting to run
Static analysis / Static analysis (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, sqlite) (push) Waiting to run
* Initial plan

* Replace placeholder syntax for type synonyms to be more DX friendly

Co-authored-by: jbtronics <5410681+jbtronics@users.noreply.github.com>

* Update translation files to use new placeholder syntax

Co-authored-by: jbtronics <5410681+jbtronics@users.noreply.github.com>

* Use mb_strtoupper with mb_substr for Unicode consistency

Co-authored-by: jbtronics <5410681+jbtronics@users.noreply.github.com>

* Make placeholder generation right

* Removed obsolete transltions

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: jbtronics <5410681+jbtronics@users.noreply.github.com>
Co-authored-by: Jan Böhmer <mail@jan-boehmer.de>
2025-12-06 23:29:32 +01:00
Copilot
443cb57ce8
Add Copilot instructions for repository (#1135)
* Initial plan

* Add comprehensive Copilot instructions for Part-DB

Co-authored-by: jbtronics <5410681+jbtronics@users.noreply.github.com>

* Fix Symfony version and fixtures command in Copilot instructions

Co-authored-by: jbtronics <5410681+jbtronics@users.noreply.github.com>

* Remove PHP code style check instructions

Removed code style check instructions for PHP.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: jbtronics <5410681+jbtronics@users.noreply.github.com>
Co-authored-by: Jan Böhmer <mail@jan-boehmer.de>
2025-12-06 22:57:48 +01:00
Copilot
9dc17ad9d0
Remove 25 unused/obsolete translation keys (#1131)
* Initial plan

* Remove 25 unused/obsolete translation keys from all message files

Removed keys:
- admin.comment
- api_tokens.added_date
- attachment.external_file
- attachment.table.filename
- datatable.datatable.lengthMenu
- delete.caption
- edit.caption_short
- part.add.caption
- part.add.comment
- part.description.placeholder
- part.edited_flash.invalid
- part.instock.placeholder
- part.mininstock.placeholder
- part.order.price_per
- part.withdraw.btn
- part.withdraw.caption
- part.withdraw.comment:
- project.caption
- r_u2f_two_factor.name
- search.deactivateBarcode
- selectpicker.empty
- supplier.caption
- tfa_u2f.key_added_successful
- tree.root_node.text
- validator.part_lot.location_full.no_increasment (typo of fixed key)

These keys have zero occurrences in the codebase and are safe to remove.

Co-authored-by: jbtronics <5410681+jbtronics@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: jbtronics <5410681+jbtronics@users.noreply.github.com>
2025-12-06 22:55:02 +01:00
Copilot
02b1f7aa16
Improve documentation clarity and fix outdated information (#1129)
* Initial plan

* Improve documentation clarity, fix typos, and update outdated information

Co-authored-by: jbtronics <5410681+jbtronics@users.noreply.github.com>

* Replace LCSC with Mouser in cloud providers list per d-buchmann's review

Co-authored-by: jbtronics <5410681+jbtronics@users.noreply.github.com>

* Remove outdated Mouser API issue notice

Removed outdated information about the Mouser API's current issues with datasheets and part specifications.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: jbtronics <5410681+jbtronics@users.noreply.github.com>
Co-authored-by: Jan Böhmer <mail@jan-boehmer.de>
2025-12-06 22:40:45 +01:00
Copilot
d244400f4f
Add missing plural label translations to 12 language files (#1126)
* Initial plan

* Add missing plural label translations to 12 language files

Co-authored-by: jbtronics <5410681+jbtronics@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: jbtronics <5410681+jbtronics@users.noreply.github.com>
2025-12-06 22:24:14 +01:00
Copilot
0000cd7a02
Fix spelling and grammar mistakes in documentation (#1127)
Some checks failed
Build assets artifact / Build assets artifact (push) Has been cancelled
Docker Image Build / docker (push) Has been cancelled
Docker Image Build (FrankenPHP) / docker (push) Has been cancelled
Static analysis / Static analysis (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, sqlite) (push) Has been cancelled
* Initial plan

* Fix spelling and grammar mistakes in documentation

Co-authored-by: jbtronics <5410681+jbtronics@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: jbtronics <5410681+jbtronics@users.noreply.github.com>
2025-12-05 00:05:31 +01:00
Copilot
9a1dbe06dc
Fix spelling and grammar mistakes in German and English translations (#1125)
* Initial plan

* Fix spelling and grammar mistakes in German and English translations

Co-authored-by: jbtronics <5410681+jbtronics@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: jbtronics <5410681+jbtronics@users.noreply.github.com>
2025-12-04 23:44:03 +01:00
Jan Böhmer
fd7106af28 Allow that the DEFAULT_URI does not end with a slash
We normalize the url with an env var processor before passing it to the saml lib, to avoid an error. Fixes issue #1118
2025-12-04 23:31:42 +01:00
Jan Böhmer
cb6da36954 Merge remote-tracking branch 'origin/master' 2025-12-04 23:04:47 +01:00
Jan Böhmer
a5275f7be7 Increase DB field length for URLs to 2048 chars
This fixes issue #1122
2025-12-04 23:04:44 +01:00
dependabot[bot]
17f9755b86
Bump actions/checkout from 5 to 6 (#1116)
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-04 22:41:11 +01:00
Copilot
a3d6f77fda
Add missing German translations to messages.de.xlf (#1124)
* Initial plan

* Add 41 missing German translations to messages.de.xlf

Co-authored-by: jbtronics <5410681+jbtronics@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: jbtronics <5410681+jbtronics@users.noreply.github.com>
2025-12-04 22:33:59 +01:00
Jan Böhmer
36e1fcfbed Fixed bulk provider imports, issue described in #869
Some checks are pending
Build assets artifact / Build assets artifact (push) Waiting to run
Docker Image Build / docker (push) Waiting to run
Docker Image Build (FrankenPHP) / docker (push) Waiting to run
Static analysis / Static analysis (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, sqlite) (push) Waiting to run
This makes PR #1110 obsolete
2025-12-03 22:22:21 +01:00
Jan Böhmer
1925a71f30 Added translations for new IPN suggestion settings 2025-12-03 21:52:48 +01:00
Jan Böhmer
023d38d170 Allow to configure seperators, category fallback and a global prefix for IPN generation
Some checks failed
Build assets artifact / Build assets artifact (push) Has been cancelled
Docker Image Build / docker (push) Has been cancelled
Docker Image Build (FrankenPHP) / docker (push) Has been cancelled
Static analysis / Static analysis (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, sqlite) (push) Has been cancelled
Translations still missing
2025-12-01 23:56:36 +01:00
Jan Böhmer
9a1961bcd7 Fixed fixtures loading (for real now hopefully) 2025-12-01 22:56:47 +01:00
Jan Böhmer
f27f0ab12d Fixed fixtures loading
Some checks are pending
Build assets artifact / Build assets artifact (push) Waiting to run
Docker Image Build / docker (push) Waiting to run
Docker Image Build (FrankenPHP) / docker (push) Waiting to run
Static analysis / Static analysis (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, sqlite) (push) Waiting to run
2025-11-30 23:40:53 +01:00
Jan Böhmer
9f2989444a Fixed phpstan issues 2025-11-30 23:37:34 +01:00
Jan Böhmer
8efc1ab07d Save changes to yarn.lock 2025-11-30 23:33:11 +01:00
Jan Böhmer
1596b4d7ea Updated jbtronics/settings-bundle for fixing compatibility issues with symfony 7.4 2025-11-30 23:27:27 +01:00
Jan Böhmer
d8ec65461e fixed error messages related to datafixtures in prod 2025-11-30 21:30:17 +01:00
Jan Böhmer
d89a76bdc3 Removed unnecessary frankenphp-symfony runtime, as it is now part of symonfy 7.4 2025-11-30 21:30:00 +01:00
Jan Böhmer
3b4a099285 Use AutowireIterator instead of deprecated TaggedIterator
Some checks are pending
Build assets artifact / Build assets artifact (push) Waiting to run
Docker Image Build / docker (push) Waiting to run
Docker Image Build (FrankenPHP) / docker (push) Waiting to run
Static analysis / Static analysis (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, sqlite) (push) Waiting to run
2025-11-30 15:43:25 +01:00
Jan Böhmer
161a9e930d Made IPN format help placeholder translatable 2025-11-30 15:38:46 +01:00
Jan Böhmer
3649fffde1 Made password toggle buttons labels translatable 2025-11-30 15:36:54 +01:00
Jan Böhmer
d55d9b43f9 Removed more clutter in settings form translation 2025-11-30 15:33:52 +01:00
Jan Böhmer
8e11e06077 Fixed multi translation of env var tooltip to remove clutter in translation editor 2025-11-30 15:28:22 +01:00
Jan Böhmer
e112a8dbe0 Use static message in settings to remove translation editor clutter 2025-11-30 15:17:37 +01:00
Jan Böhmer
5d843ec8eb Merge branch 'symfony74' 2025-11-30 15:10:21 +01:00
Jan Böhmer
3518321f0e Updated webpack-encore bundle 2025-11-30 15:10:13 +01:00
Jan Böhmer
4750a50fb9 Updated stimulus recipe 2025-11-30 15:07:59 +01:00
Jan Böhmer
52a9975769 Updated security recipe 2025-11-30 15:03:23 +01:00
Jan Böhmer
bb650c2218 Updated routing recipe 2025-11-30 15:01:37 +01:00
Jan Böhmer
3dc66542b2 Updated monolog recipe 2025-11-30 14:51:56 +01:00
Jan Böhmer
070ce800d5 Updated framework bundle recipe 2025-11-30 14:50:46 +01:00
Jan Böhmer
57dc4242b2 Updated phpunit bridge recipe 2025-11-30 14:48:00 +01:00
Jan Böhmer
5fa2167755 config/references update 2025-11-30 14:45:49 +01:00
Jan Böhmer
4eac63b683 Fixed phpunit deprecations 2025-11-30 14:45:40 +01:00
Jan Böhmer
171508fcad Fixed deprecated csv reader usage 2025-11-30 14:34:45 +01:00
Jan Böhmer
e4f8252e0f Upgraded to symfony 7.4 2025-11-30 14:30:04 +01:00
Jan Böhmer
e513960401 Updated dependencies 2025-11-30 14:26:37 +01:00
Jan Böhmer
84e35603b1 Made sidebar toggle button smaller 2025-11-30 14:20:50 +01:00
Jan Böhmer
3459731ca8 Show plural translation for entity type labels in synonym settings 2025-11-30 14:16:54 +01:00
Jan Böhmer
819a8cc56d Fixed category field in part edit page not being correctly rendered 2025-11-30 14:02:42 +01:00
Jan Böhmer
95c5ab7b8b Removed unnecessary polyfills
Some checks failed
Build assets artifact / Build assets artifact (push) Has been cancelled
Docker Image Build / docker (push) Has been cancelled
Docker Image Build (FrankenPHP) / docker (push) Has been cancelled
Static analysis / Static analysis (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, sqlite) (push) Has been cancelled
2025-11-12 21:54:41 +01:00
Jan Böhmer
f184afc918 Updated dependencies 2025-11-12 21:49:44 +01:00
web-devinition.de
54f318ecac
Implemented the ability to set user-defined synonyms/labels for internal element types
* Implementiere bevorzugte Sprachauswahl und Datenquellen-Synonyme

Die Spracheinstellungen/System-Settings wurden um die Möglichkeit ergänzt, bevorzugte Sprachen für die Dropdown-Menüs festzulegen. Zudem wurde ein Datenquellen-Synonymsystem implementiert, um benutzerfreundlichere Bezeichnungen anzuzeigen und zu personalisieren.

* Anpassung aus Analyse

* Entferne alten JSON-basierten Datenquellen-Synonym-Handler

Die Verwaltung der Datenquellen-Synonyme wurde überarbeitet, um ein flexibleres und strukturiertes Konzept zu ermöglichen. Der bestehende JSON-basierte Ansatz wurde durch eine neue Service-basierte Architektur ersetzt, die eine bessere Handhabung und Erweiterbarkeit erlaubt.

* Ermögliche Rückgabe aller möglichen Sprachoptionen in Verbindung mit den vom Nutzer freigeschalteten.

* Removed unnecessary service definition

The tag is applied via autoconfiguration

* Use default translations for the NotBlank constraint

* Started refactoring ElementTypeNameGenerator

* Made ElementTypeNameGenerator class readonly

* Modified form to work properly with new datastructure

* Made the form more beautiful and space saving

* Made synonym form even more space saving

* Allow to define overrides for any element label there is

* Use defined synonyms in ElementTypeNameGenerator

* Use ElementTypeNameGenerator where possible

* Register synonyms for element types as global translation parameters

* Revert changes done to permission layout

* Use new synonym system for admin page titles

* Removed now unnecessary services

* Reworked settings name and translation

* Renamed all files to Synonyms

* Removed unnecessary translations

* Removed unnecessary translations

* Fixed duplicate check

* Renamed synoynms translations

* Use our synonyms for permission translations

* Fixed phpstan issue

* Added tests

---------

Co-authored-by: Marcel Diegelmann <marcel.diegelmann@gmail.com>
Co-authored-by: Jan Böhmer <mail@jan-boehmer.de>
2025-11-12 21:35:02 +01:00
Jan Böhmer
5e3bd26e27
New Crowdin updates (#1105)
* New translations validators.en.xlf (French)

* New translations security.en.xlf (French)

* New translations messages.en.xlf (French)

* New translations messages.en.xlf (French)
2025-11-12 21:34:05 +01:00
Jan Böhmer
e53b72a8d1 Merge remote-tracking branch 'origin/master'
Some checks failed
Build assets artifact / Build assets artifact (push) Has been cancelled
Docker Image Build / docker (push) Has been cancelled
Docker Image Build (FrankenPHP) / docker (push) Has been cancelled
Static analysis / Static analysis (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, sqlite) (push) Has been cancelled
2025-11-03 00:37:00 +01:00
Jan Böhmer
e8ff15ad0f Updated dependencies 2025-11-03 00:36:56 +01:00
dependabot[bot]
ec6b3ae414
Bump actions/setup-node from 5 to 6 (#1086)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 5 to 6.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-03 00:35:12 +01:00
dependabot[bot]
07e4521c30
Bump actions/upload-artifact from 4 to 5 (#1091)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-03 00:35:03 +01:00
web-devinition.de
771857e014
Added feature for part IPN suggest with category prefixes (#1054)
* Erweiterungstätigkeiten zur IPN-Vorschlagsliste anhand von Präfixen aus den Kategorien

* Umstellung Migrationen bzgl. Multi-Plattform-Support.
Zunächst MySQL, SQLite Statements integrieren.

* Postgre Statements integrieren

* SQL-Formatierung in Migration verbessern

* Erweitere IPN-Suggest um Bauteilbeschreibung.

Die Implementierung berücksichtigt nun zusätzlich die Bauteilbeschreibung zu maximal 150 Zeichen Länge für die Generierung von IPN-Vorschlägen und Inkrementen.

* Anpassungen aus Analyse vornehmen

* IPN-Validierung für Parts überarbeiten

* IPN-Vorschlagslogik um Konfiguration erweitert

* Anpassungen aus phpstan Analyse

* IPN-Vorschlagslogik erweitert und Bauteil-IPN vereindeutigt

Die IPN-Logik wurde um eine Konfiguration zur automatischen Suffix-Anfügung und die Berücksichtigung von doppelten Beschreibungen bei Bedarf ergänzt. Zudem wurde das Datenmodell angepasst, um eine eindeutige Speicherung der IPN zu gewährleisten.

* Regex-Konfigurationsmöglichkeit für IPN-Vorschläge einführen

Die Einstellungen für die IPN-Vorschlagslogik wurden um eine Regex-Validierung und eine Hilfetext-Konfiguration erweitert. Tests und Änderungen an den Formularoptionen wurden implementiert.

* Match range assert and form limits in suggestPartDigits

* Keep existing behavior with autoAppend suffix by default

* Show the regex hint in the browser validation notice.

* Improved translations

* Removed unnecessary service definition

* Removed german comments

---------

Co-authored-by: Marcel Diegelmann <marcel.diegelmann@gmail.com>
Co-authored-by: Jan Böhmer <mail@jan-boehmer.de>
2025-11-03 00:31:47 +01:00
web-devinition.de
14a4f1f437
Added custom part status (#1053)
Some checks failed
Build assets artifact / Build assets artifact (push) Has been cancelled
Docker Image Build / docker (push) Has been cancelled
Docker Image Build (FrankenPHP) / docker (push) Has been cancelled
Static analysis / Static analysis (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, sqlite) (push) Has been cancelled
* Benutzerdefinierten Bauteilstatus einführen

* PartCustomStateController hinzufügen

* Umstellung Migrationen bzgl. Multi-Plattform-Support.
Zunächst MySQL, SQLite Statements integrieren.

* Postgre Statements integrieren

* Semikolon in Migration entfernen

* Migration für PartCustomState aktualisieren

* Benutzerdefinierten Bauteilstatus in TableSettings aufnehmen

* PartCustomStateControllerTest: Attribute für PHPUnit-Gruppen umgestellt

* PartCustomState: Mapping für Parameter korrigieren

* PartCustomState: Darstellung und Zuordnung von Anhängen ergänzt

Die Sidebar wurde um die Anzeige des benutzerdefinierten Bauteilstatus erweitert, inklusive Vorschaubild, sofern vorhanden.

* Migrationen zusammenführen

* PartCustomState: Anpassungen bzgl. Tests

* PartCustomStateEndpoint hinzufügen

* Made custom part states plural for consistency with other entity captions

* Fixed phpunit error

* Fixed phpstan issues

---------

Co-authored-by: Marcel Diegelmann <marcel.diegelmann@gmail.com>
Co-authored-by: Jan Böhmer <mail@jan-boehmer.de>
2025-10-27 21:58:16 +01:00
183 changed files with 22892 additions and 15358 deletions

4
.env
View file

@ -31,8 +31,7 @@ DATABASE_EMULATE_NATURAL_SORT=0
# General settings
###################################################################################
# The public reachable URL of this Part-DB installation. This is used for generating links in SAML and email templates
# This must end with a slash!
# The public reachable URL of this Part-DB installation. This is used for generating links in SAML and email templates or when no request context is available.
DEFAULT_URI="https://partdb.changeme.invalid/"
###################################################################################
@ -134,4 +133,5 @@ CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$'
###> symfony/framework-bundle ###
APP_ENV=prod
APP_SECRET=a03498528f5a5fc089273ec9ae5b2849
APP_SHARE_DIR=var/share
###< symfony/framework-bundle ###

186
.github/copilot-instructions.md vendored Normal file
View file

@ -0,0 +1,186 @@
# Copilot Instructions for Part-DB
Part-DB is an Open-Source inventory management system for electronic components built with Symfony 7.4 and modern web technologies.
## Technology Stack
- **Backend**: PHP 8.2+, Symfony 7.4, Doctrine ORM
- **Frontend**: Bootstrap 5, Hotwire Stimulus/Turbo, TypeScript, Webpack Encore
- **Database**: MySQL 5.7+/MariaDB 10.4+/PostgreSQL 10+/SQLite
- **Testing**: PHPUnit with DAMA Doctrine Test Bundle
- **Code Quality**: Easy Coding Standard (ECS), PHPStan (level 5)
## Project Structure
- `src/`: PHP application code organized by purpose (Controller, Entity, Service, Form, etc.)
- `assets/`: Frontend TypeScript/JavaScript and CSS files
- `templates/`: Twig templates for views
- `tests/`: PHPUnit tests mirroring the `src/` structure
- `config/`: Symfony configuration files
- `public/`: Web-accessible files
- `translations/`: Translation files for multi-language support
## Coding Standards
### PHP Code
- Follow [PSR-12](https://www.php-fig.org/psr/psr-12/) and [Symfony coding standards](https://symfony.com/doc/current/contributing/code/standards.html)
- Use type hints for all parameters and return types
- Always declare strict types: `declare(strict_types=1);` at the top of PHP files
- Use PHPDoc blocks for complex logic or when type information is needed
### TypeScript/JavaScript
- Use TypeScript for new frontend code
- Follow existing Stimulus controller patterns in `assets/controllers/`
- Use Bootstrap 5 components and utilities
- Leverage Hotwire Turbo for dynamic page updates
### Naming Conventions
- Entities: Use descriptive names that reflect database models (e.g., `Part`, `StorageLocation`)
- Controllers: Suffix with `Controller` (e.g., `PartController`)
- Services: Descriptive names reflecting their purpose (e.g., `PartService`, `LabelGenerator`)
- Tests: Match the class being tested with `Test` suffix (e.g., `PartTest`, `PartControllerTest`)
## Development Workflow
### Dependencies
- Install PHP dependencies: `composer install`
- Install JS dependencies: `yarn install`
- Build frontend assets: `yarn build` (production) or `yarn watch` (development)
### Database
- Create database: `php bin/console doctrine:database:create --env=dev`
- Run migrations: `php bin/console doctrine:migrations:migrate --env=dev`
- Load fixtures: `php bin/console partdb:fixtures:load -n --env=dev`
Or use Makefile shortcuts:
- `make dev-setup`: Complete development environment setup
- `make dev-reset`: Reset development environment (cache clear + migrate)
### Testing
- Set up test environment: `make test-setup`
- Run all tests: `php bin/phpunit`
- Run specific test: `php bin/phpunit tests/Path/To/SpecificTest.php`
- Run tests with coverage: `php bin/phpunit --coverage-html var/coverage`
- Test environment uses SQLite by default for speed
### Static Analysis
- Run PHPStan: `composer phpstan` or `COMPOSER_MEMORY_LIMIT=-1 php -d memory_limit=1G vendor/bin/phpstan analyse src --level 5`
- PHPStan configuration is in `phpstan.dist.neon`
### Running the Application
- Development server: `symfony serve` (requires Symfony CLI)
- Or configure Apache/nginx to serve from `public/` directory
- Set `APP_ENV=dev` in `.env.local` for development mode
## Best Practices
### Security
- Always sanitize user input
- Use Symfony's security component for authentication/authorization
- Check permissions using the permission system before allowing actions
- Never expose sensitive data in logs or error messages
- Use parameterized queries (Doctrine handles this automatically)
### Performance
- Use Doctrine query builder for complex queries instead of DQL when possible
- Lazy load relationships to avoid N+1 queries
- Cache results when appropriate using Symfony's cache component
- Use pagination for large result sets (DataTables integration available)
### Database
- Always create migrations for schema changes: `php bin/console make:migration`
- Review migration files before running them
- Use Doctrine annotations or attributes for entity mapping
- Follow existing entity patterns for relationships and lifecycle callbacks
### Frontend
- Use Stimulus controllers for interactive components
- Leverage Turbo for dynamic page updates without full page reloads
- Use Bootstrap 5 classes for styling
- Keep JavaScript modular and organized in controllers
- Use the translation system for user-facing strings
### Translations
- Use translation keys, not hardcoded strings: `{{ 'part.info.title'|trans }}`
- Add new translation keys to `translations/` files
- Primary language is English (en)
- Translations are managed via Crowdin, but can be edited locally if needed
### Testing
- Write unit tests for services and helpers
- Write functional tests for controllers
- Use fixtures for test data
- Tests should be isolated and not depend on execution order
- Mock external dependencies when appropriate
- Follow existing test patterns in the repository
## Common Patterns
### Creating an Entity
1. Create entity class in `src/Entity/` with Doctrine attributes
2. Generate migration: `php bin/console make:migration`
3. Review and run migration: `php bin/console doctrine:migrations:migrate`
4. Create repository if needed in `src/Repository/`
5. Add fixtures in `src/DataFixtures/` for testing
### Adding a Form
1. Create form type in `src/Form/`
2. Extend `AbstractType` and implement `buildForm()` and `configureOptions()`
3. Use in controller and render in Twig template
4. Follow existing form patterns for consistency
### Creating a Controller Action
1. Add method to appropriate controller in `src/Controller/`
2. Use route attributes for routing
3. Check permissions using security voters
4. Return Response or render Twig template
5. Add corresponding template in `templates/`
### Adding a Service
1. Create service class in `src/Services/`
2. Use dependency injection via constructor
3. Tag service in `config/services.yaml` if needed
4. Services are autowired by default
## Important Notes
- Part-DB uses fine-grained permissions - always check user permissions before actions
- Multi-language support is critical - use translation keys everywhere
- The application supports multiple database backends - write portable code
- Responsive design is important - test on mobile/tablet viewports
- Event system is used for logging changes - emit events when appropriate
- API Platform is integrated for REST API endpoints
## Multi-tenancy Considerations
- Part-DB is designed as a single-tenant application with multiple users
- User groups have different permission levels
- Always scope queries to respect user permissions
- Use the security context to get current user information
## Resources
- [Documentation](https://docs.part-db.de/)
- [Contributing Guide](CONTRIBUTING.md)
- [Symfony Documentation](https://symfony.com/doc/current/index.html)
- [Doctrine Documentation](https://www.doctrine-project.org/projects/doctrine-orm/en/current/)
- [Bootstrap 5 Documentation](https://getbootstrap.com/docs/5.1/)
- [Hotwire Documentation](https://hotwired.dev/)

View file

@ -22,7 +22,7 @@ jobs:
APP_ENV: prod
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Setup PHP
uses: shivammathur/setup-php@v2
@ -60,7 +60,7 @@ jobs:
${{ runner.os }}-yarn-
- name: Setup node
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
node-version: '20'
@ -80,13 +80,13 @@ jobs:
run: zip -r /tmp/partdb_assets.zip public/build/ vendor/
- name: Upload assets artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: Only dependencies and built assets
path: /tmp/partdb_assets.zip
- name: Upload full artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: Full Part-DB including dependencies and built assets
path: /tmp/partdb_with_assets.zip

View file

@ -20,7 +20,7 @@ jobs:
steps:
-
name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
-
name: Docker meta
id: docker_meta

View file

@ -20,7 +20,7 @@ jobs:
steps:
-
name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
-
name: Docker meta
id: docker_meta

View file

@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Setup PHP
uses: shivammathur/setup-php@v2

View file

@ -46,7 +46,7 @@ jobs:
if: matrix.db-type == 'postgres'
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Setup PHP
uses: shivammathur/setup-php@v2
@ -104,7 +104,7 @@ jobs:
run: composer install --prefer-dist --no-progress
- name: Setup node
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
node-version: '20'

View file

@ -1,7 +1,7 @@
# How to contribute
Thank you for consider to contribute to Part-DB!
Please read the text below, so your contributed content can be contributed easily to Part-DB.
Thank you for considering contributing to Part-DB!
Please read the text below, so your contributed content can be incorporated into Part-DB easily.
You can contribute to Part-DB in various ways:
* Report bugs and request new features via [issues](https://github.com/Part-DB/Part-DB-server/issues)
@ -18,38 +18,38 @@ Part-DB uses translation keys (e.g. part.info.title) that are sorted by their us
was translated in other languages (this is possible via the "Other languages" dropdown in the translation editor).
## Project structure
Part-DB uses symfony's recommended [project structure](https://symfony.com/doc/current/best_practices.html).
Part-DB uses Symfony's recommended [project structure](https://symfony.com/doc/current/best_practices.html).
Interesting folders are:
* `public`: Everything in this directory will be publicy accessible via web. Use this folder to serve static images.
* `assets`: The frontend assets are saved here. You can find the javascript and CSS code here.
* `src`: Part-DB's PHP code is saved here. Note that the sub directories are structured by the classes purposes (so use `Controller` Controllers, `Entities` for Database models, etc.)
* `translations`: The translations used in Part-DB are saved here
* `public`: Everything in this directory will be publicly accessible via web. Use this folder to serve static images.
* `assets`: The frontend assets are saved here. You can find the JavaScript and CSS code here.
* `src`: Part-DB's PHP code is saved here. Note that the subdirectories are structured by the classes' purposes (so use `Controller` for Controllers, `Entity` for Database models, etc.)
* `translations`: The translations used in Part-DB are saved here.
* `templates`: The templates (HTML) that are used by Twig to render the different pages. Email templates are also saved here.
* `tests/`: Tests that can be run by PHPunit.
* `tests/`: Tests that can be run by PHPUnit.
## Development environment
For setting up an development you will need to install PHP, composer, a database server (MySQL or MariaDB) and yarn (which needs an nodejs environment).
* Copy `.env` to `.env.local` and change `APP_ENV` to `APP_ENV=dev`. That way you will get development tools (symfony profiler) and other features that
For setting up a development environment, you will need to install PHP, Composer, a database server (MySQL or MariaDB) and yarn (which needs a Node.js environment).
* Copy `.env` to `.env.local` and change `APP_ENV` to `APP_ENV=dev`. That way you will get development tools (Symfony profiler) and other features that
will simplify development.
* Run `composer install` (without -o) to install PHP dependencies and `yarn install` to install frontend dependencies
* Run `yarn watch`. The program will run in the background and compile the frontend files whenever you change something in the CSS or TypeScript files
* For running Part-DB it is recommended to use [Symfony CLI](https://symfony.com/download).
That way you can run a correct configured webserver with `symfony serve`
* Run `composer install` (without -o) to install PHP dependencies and `yarn install` to install frontend dependencies.
* Run `yarn watch`. The program will run in the background and compile the frontend files whenever you change something in the CSS or TypeScript files.
* For running Part-DB, it is recommended to use [Symfony CLI](https://symfony.com/download).
That way you can run a correctly configured webserver with `symfony serve`.
## Coding style
Code should follow the [PSR12-Standard](https://www.php-fig.org/psr/psr-12/) and symfony's [coding standards](https://symfony.com/doc/current/contributing/code/standards.html).
Code should follow the [PSR-12 Standard](https://www.php-fig.org/psr/psr-12/) and Symfony's [coding standards](https://symfony.com/doc/current/contributing/code/standards.html).
Part-DB uses [Easy Coding Standard](https://github.com/symplify/easy-coding-standard) to check and fix coding style violations:
* To check your code for valid code style run `vendor/bin/ecs check src/`
* To fix violations run `vendor/bin/ecs check src/` (please checks afterwards if the code is valid afterwards)
* To check your code for valid code style, run `vendor/bin/ecs check src/`
* To fix violations, run `vendor/bin/ecs check src/ --fix` (please check afterwards if the code is still valid)
## GitHub actions
Part-DB uses GitHub actions to run various tests and checks on the code:
Part-DB uses GitHub Actions to run various tests and checks on the code:
* Yarn dependencies can compile
* PHPunit tests run successful
* Config files, translations and templates has valid syntax
* Doctrine schema valid
* No known vulnerable dependecies are used
* Static analysis successful (phpstan with `--level=2`)
* PHPUnit tests run successfully
* Config files, translations, and templates have valid syntax
* Doctrine schema is valid
* No known vulnerable dependencies are used
* Static analysis is successful (phpstan with `--level=2`)
Further the code coverage of the PHPunit tests is determined and uploaded to [CodeCov](https://codecov.io/gh/Part-DB/Part-DB-server).
Further, the code coverage of the PHPUnit tests is determined and uploaded to [CodeCov](https://codecov.io/gh/Part-DB/Part-DB-server).

View file

@ -29,8 +29,8 @@ If you want to test Part-DB without installing it, you can use [this](https://de
You can log in with username: *user* and password: *user*.
Every change to the master branch gets automatically deployed, so it represents the current development progress and is
may not completely stable. Please mind, that the free Heroku instance is used, so it can take some time when loading
Every change to the master branch gets automatically deployed, so it represents the current development progress and
may not be completely stable. Please mind, that the free Heroku instance is used, so it can take some time when loading
the page
for the first time.
@ -142,7 +142,7 @@ There you will find various methods to support development on a monthly or a one
## Built with
* [Symfony 5](https://symfony.com/): The main framework used for the serverside PHP
* [Symfony 6](https://symfony.com/): The main framework used for the serverside PHP
* [Bootstrap 5](https://getbootstrap.com/) and [Bootswatch](https://bootswatch.com/): Used as website theme
* [Fontawesome](https://fontawesome.com/): Used as icon set
* [Hotwire Stimulus](https://stimulus.hotwired.dev/) and [Hotwire Turbo](https://turbo.hotwired.dev/): Frontend

View file

@ -2,6 +2,8 @@ const nameCheck = /^[-_a-zA-Z0-9]{4,22}$/;
const tokenCheck = /^[-_/+a-zA-Z0-9]{24,}$/;
// Generate and double-submit a CSRF token in a form field and a cookie, as defined by Symfony's SameOriginCsrfTokenManager
// Use `form.requestSubmit()` to ensure that the submit event is triggered. Using `form.submit()` will not trigger the event
// and thus this event-listener will not be executed.
document.addEventListener('submit', function (event) {
generateCsrfToken(event.target);
}, true);
@ -33,8 +35,8 @@ export function generateCsrfToken (formElement) {
if (!csrfCookie && nameCheck.test(csrfToken)) {
csrfField.setAttribute('data-csrf-protection-cookie-value', csrfCookie = csrfToken);
csrfField.defaultValue = csrfToken = btoa(String.fromCharCode.apply(null, (window.crypto || window.msCrypto).getRandomValues(new Uint8Array(18))));
csrfField.dispatchEvent(new Event('change', { bubbles: true }));
}
csrfField.dispatchEvent(new Event('change', { bubbles: true }));
if (csrfCookie && tokenCheck.test(csrfToken)) {
const cookie = csrfCookie + '_' + csrfToken + '=' + csrfCookie + '; path=/; samesite=strict';

View file

@ -106,6 +106,15 @@ export default class extends Controller {
editor_div.classList.add(...new_classes.split(","));
}
// Automatic synchronization of source input
editor.model.document.on("change:data", () => {
editor.updateSourceElement();
// Dispatch the input event for further treatment
const event = new Event("input");
this.element.dispatchEvent(event);
});
//This return is important! Otherwise we get mysterious errors in the console
//See: https://github.com/ckeditor/ckeditor5/issues/5897#issuecomment-628471302
return editor;

View file

@ -0,0 +1,250 @@
import { Controller } from "@hotwired/stimulus";
import "../../css/components/autocomplete_bootstrap_theme.css";
export default class extends Controller {
static targets = ["input"];
static values = {
partId: Number,
partCategoryId: Number,
partDescription: String,
suggestions: Object,
commonSectionHeader: String, // Dynamic header for common Prefixes
partIncrementHeader: String, // Dynamic header for new possible part increment
suggestUrl: String,
};
connect() {
this.configureAutocomplete();
this.watchCategoryChanges();
this.watchDescriptionChanges();
}
templates = {
commonSectionHeader({ title, html }) {
return html`
<section class="aa-Source">
<div class="aa-SourceHeader">
<span class="aa-SourceHeaderTitle">${title}</span>
<div class="aa-SourceHeaderLine"></div>
</div>
</section>
`;
},
partIncrementHeader({ title, html }) {
return html`
<section class="aa-Source">
<div class="aa-SourceHeader">
<span class="aa-SourceHeaderTitle">${title}</span>
<div class="aa-SourceHeaderLine"></div>
</div>
</section>
`;
},
list({ html }) {
return html`
<ul class="aa-List" role="listbox"></ul>
`;
},
item({ suggestion, description, html }) {
return html`
<li class="aa-Item" role="option" data-suggestion="${suggestion}" aria-selected="false">
<div class="aa-ItemWrapper">
<div class="aa-ItemContent">
<div class="aa-ItemIcon aa-ItemIcon--noBorder">
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M12 21c4.971 0 9-4.029 9-9s-4.029-9-9-9-9 4.029-9 9 4.029 9 9 9z"></path>
</svg>
</div>
<div class="aa-ItemContentBody">
<div class="aa-ItemContentTitle">${suggestion}</div>
<div class="aa-ItemContentDescription">${description}</div>
</div>
</div>
</div>
</li>
`;
},
};
configureAutocomplete() {
const inputField = this.inputTarget;
const commonPrefixes = this.suggestionsValue.commonPrefixes || [];
const prefixesPartIncrement = this.suggestionsValue.prefixesPartIncrement || [];
const commonHeader = this.commonSectionHeaderValue;
const partIncrementHeader = this.partIncrementHeaderValue;
if (!inputField || (!commonPrefixes.length && !prefixesPartIncrement.length)) return;
// Check whether the panel should be created at the update
if (this.isPanelInitialized) {
const existingPanel = inputField.parentNode.querySelector(".aa-Panel");
if (existingPanel) {
// Only remove the panel in the update phase
existingPanel.remove();
}
}
// Create panel
const panel = document.createElement("div");
panel.classList.add("aa-Panel");
panel.style.display = "none";
// Create panel layout
const panelLayout = document.createElement("div");
panelLayout.classList.add("aa-PanelLayout", "aa-Panel--scrollable");
// Section for prefixes part increment
if (prefixesPartIncrement.length) {
const partIncrementSection = document.createElement("section");
partIncrementSection.classList.add("aa-Source");
const partIncrementHeaderHtml = this.templates.partIncrementHeader({
title: partIncrementHeader,
html: String.raw,
});
partIncrementSection.innerHTML += partIncrementHeaderHtml;
const partIncrementList = document.createElement("ul");
partIncrementList.classList.add("aa-List");
partIncrementList.setAttribute("role", "listbox");
prefixesPartIncrement.forEach((prefix) => {
const itemHTML = this.templates.item({
suggestion: prefix.title,
description: prefix.description,
html: String.raw,
});
partIncrementList.innerHTML += itemHTML;
});
partIncrementSection.appendChild(partIncrementList);
panelLayout.appendChild(partIncrementSection);
}
// Section for common prefixes
if (commonPrefixes.length) {
const commonSection = document.createElement("section");
commonSection.classList.add("aa-Source");
const commonSectionHeader = this.templates.commonSectionHeader({
title: commonHeader,
html: String.raw,
});
commonSection.innerHTML += commonSectionHeader;
const commonList = document.createElement("ul");
commonList.classList.add("aa-List");
commonList.setAttribute("role", "listbox");
commonPrefixes.forEach((prefix) => {
const itemHTML = this.templates.item({
suggestion: prefix.title,
description: prefix.description,
html: String.raw,
});
commonList.innerHTML += itemHTML;
});
commonSection.appendChild(commonList);
panelLayout.appendChild(commonSection);
}
panel.appendChild(panelLayout);
inputField.parentNode.appendChild(panel);
inputField.addEventListener("focus", () => {
panel.style.display = "block";
});
inputField.addEventListener("blur", () => {
setTimeout(() => {
panel.style.display = "none";
}, 100);
});
// Selection of an item
panelLayout.addEventListener("mousedown", (event) => {
const target = event.target.closest("li");
if (target) {
inputField.value = target.dataset.suggestion;
panel.style.display = "none";
}
});
this.isPanelInitialized = true;
};
watchCategoryChanges() {
const categoryField = document.querySelector('[data-ipn-suggestion="categoryField"]');
const descriptionField = document.querySelector('[data-ipn-suggestion="descriptionField"]');
this.previousCategoryId = Number(this.partCategoryIdValue);
if (categoryField) {
categoryField.addEventListener("change", () => {
const categoryId = Number(categoryField.value);
const description = String(descriptionField?.value ?? '');
// Check whether the category has changed compared to the previous ID
if (categoryId !== this.previousCategoryId) {
this.fetchNewSuggestions(categoryId, description);
this.previousCategoryId = categoryId;
}
});
}
}
watchDescriptionChanges() {
const categoryField = document.querySelector('[data-ipn-suggestion="categoryField"]');
const descriptionField = document.querySelector('[data-ipn-suggestion="descriptionField"]');
this.previousDescription = String(this.partDescriptionValue);
if (descriptionField) {
descriptionField.addEventListener("input", () => {
const categoryId = Number(categoryField.value);
const description = String(descriptionField?.value ?? '');
// Check whether the description has changed compared to the previous one
if (description !== this.previousDescription) {
this.fetchNewSuggestions(categoryId, description);
this.previousDescription = description;
}
});
}
}
fetchNewSuggestions(categoryId, description) {
const baseUrl = this.suggestUrlValue;
const partId = this.partIdValue;
const truncatedDescription = description.length > 150 ? description.substring(0, 150) : description;
const encodedDescription = this.base64EncodeUtf8(truncatedDescription);
const url = `${baseUrl}?partId=${partId}&categoryId=${categoryId}` + (description !== '' ? `&description=${encodedDescription}` : '');
fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
},
})
.then((response) => {
if (!response.ok) {
throw new Error(`Error when calling up the IPN-suggestions: ${response.status}`);
}
return response.json();
})
.then((data) => {
this.suggestionsValue = data;
this.configureAutocomplete();
})
.catch((error) => {
console.error("Errors when loading the new IPN-suggestions:", error);
});
};
base64EncodeUtf8(text) {
const utf8Bytes = new TextEncoder().encode(text);
return btoa(String.fromCharCode(...utf8Bytes));
};
}

View file

@ -0,0 +1,68 @@
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
static targets = ['items'];
static values = {
prototype: String,
prototypeName: { type: String, default: '__name__' },
index: { type: Number, default: 0 },
};
connect() {
if (!this.hasIndexValue || Number.isNaN(this.indexValue)) {
this.indexValue = this.itemsTarget?.children.length || 0;
}
}
add(event) {
event.preventDefault();
const encodedProto = this.prototypeValue || '';
const placeholder = this.prototypeNameValue || '__name__';
if (!encodedProto || !this.itemsTarget) return;
const protoHtml = this._decodeHtmlAttribute(encodedProto);
const idx = this.indexValue;
const html = protoHtml.replace(new RegExp(placeholder, 'g'), String(idx));
const wrapper = document.createElement('div');
wrapper.innerHTML = html;
const newItem = wrapper.firstElementChild;
if (newItem) {
this.itemsTarget.appendChild(newItem);
this.indexValue = idx + 1;
}
}
remove(event) {
event.preventDefault();
const row = event.currentTarget.closest('.tc-item');
if (row) row.remove();
}
_decodeHtmlAttribute(str) {
const tmp = document.createElement('textarea');
tmp.innerHTML = str;
return tmp.value || tmp.textContent || '';
}
}

View file

@ -133,7 +133,7 @@ showing the sidebar (on devices with md or higher)
*/
#sidebar-toggle-button {
position: fixed;
left: 3px;
left: 2px;
bottom: 50%;
}

View file

@ -28,7 +28,7 @@ import '../css/app/treeview.css';
import '../css/app/images.css';
// start the Stimulus application
import '../bootstrap';
import '../stimulus_bootstrap';
// Need jQuery? Install it with "yarn add jquery", then uncomment to require it.
const $ = require('jquery');

View file

@ -1,23 +1,4 @@
#!/usr/bin/env php
<?php
if (!ini_get('date.timezone')) {
ini_set('date.timezone', 'UTC');
}
if (is_file(dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit')) {
if (PHP_VERSION_ID >= 80000) {
require dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit';
} else {
define('PHPUNIT_COMPOSER_INSTALL', dirname(__DIR__).'/vendor/autoload.php');
require PHPUNIT_COMPOSER_INSTALL;
PHPUnit\TextUI\Command::main();
}
} else {
if (!is_file(dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php')) {
echo "Unable to find the `simple-phpunit.php` script in `vendor/symfony/phpunit-bridge/bin/`.\n";
exit(1);
}
require dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php';
}
require dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit';

View file

@ -48,7 +48,6 @@
"part-db/swap-bundle": "^6.0.0",
"phpoffice/phpspreadsheet": "^5.0.0",
"rhukster/dom-sanitizer": "^1.0",
"runtime/frankenphp-symfony": "^0.2.0",
"s9e/text-formatter": "^2.1",
"scheb/2fa-backup-code": "^v7.11.0",
"scheb/2fa-bundle": "^v7.11.0",
@ -57,36 +56,35 @@
"shivas/versioning-bundle": "^4.0",
"spatie/db-dumper": "^3.3.1",
"symfony/apache-pack": "^1.0",
"symfony/asset": "7.3.*",
"symfony/console": "7.3.*",
"symfony/css-selector": "7.3.*",
"symfony/dom-crawler": "7.3.*",
"symfony/dotenv": "7.3.*",
"symfony/expression-language": "7.3.*",
"symfony/asset": "7.4.*",
"symfony/console": "7.4.*",
"symfony/css-selector": "7.4.*",
"symfony/dom-crawler": "7.4.*",
"symfony/dotenv": "7.4.*",
"symfony/expression-language": "7.4.*",
"symfony/flex": "^v2.3.1",
"symfony/form": "7.3.*",
"symfony/framework-bundle": "7.3.*",
"symfony/http-client": "7.3.*",
"symfony/http-kernel": "7.3.*",
"symfony/mailer": "7.3.*",
"symfony/form": "7.4.*",
"symfony/framework-bundle": "7.4.*",
"symfony/http-client": "7.4.*",
"symfony/http-kernel": "7.4.*",
"symfony/mailer": "7.4.*",
"symfony/monolog-bundle": "^3.1",
"symfony/polyfill-php82": "^1.28",
"symfony/process": "7.3.*",
"symfony/property-access": "7.3.*",
"symfony/property-info": "7.3.*",
"symfony/rate-limiter": "7.3.*",
"symfony/runtime": "7.3.*",
"symfony/security-bundle": "7.3.*",
"symfony/serializer": "7.3.*",
"symfony/string": "7.3.*",
"symfony/translation": "7.3.*",
"symfony/twig-bundle": "7.3.*",
"symfony/process": "7.4.*",
"symfony/property-access": "7.4.*",
"symfony/property-info": "7.4.*",
"symfony/rate-limiter": "7.4.*",
"symfony/runtime": "7.4.*",
"symfony/security-bundle": "7.4.*",
"symfony/serializer": "7.4.*",
"symfony/string": "7.4.*",
"symfony/translation": "7.4.*",
"symfony/twig-bundle": "7.4.*",
"symfony/ux-translator": "^2.10",
"symfony/ux-turbo": "^2.0",
"symfony/validator": "7.3.*",
"symfony/web-link": "7.3.*",
"symfony/validator": "7.4.*",
"symfony/web-link": "7.4.*",
"symfony/webpack-encore-bundle": "^v2.0.1",
"symfony/yaml": "7.3.*",
"symfony/yaml": "7.4.*",
"symplify/easy-coding-standard": "^12.5.20",
"tecnickcom/tc-lib-barcode": "^2.1.4",
"twig/cssinliner-extra": "^3.0",
@ -111,12 +109,19 @@
"phpunit/phpunit": "^11.5.0",
"rector/rector": "^2.0.4",
"roave/security-advisories": "dev-latest",
"symfony/browser-kit": "7.3.*",
"symfony/debug-bundle": "7.3.*",
"symfony/browser-kit": "7.4.*",
"symfony/debug-bundle": "7.4.*",
"symfony/maker-bundle": "^1.13",
"symfony/phpunit-bridge": "7.3.*",
"symfony/stopwatch": "7.3.*",
"symfony/web-profiler-bundle": "7.3.*"
"symfony/phpunit-bridge": "7.4.*",
"symfony/stopwatch": "7.4.*",
"symfony/web-profiler-bundle": "7.4.*"
},
"replace": {
"symfony/polyfill-mbstring": "*",
"symfony/polyfill-php74": "*",
"symfony/polyfill-php80": "*",
"symfony/polyfill-php81": "*",
"symfony/polyfill-php82": "*"
},
"suggest": {
"ext-bcmath": "Used to improve price calculation performance",
@ -167,7 +172,7 @@
"extra": {
"symfony": {
"allow-contrib": false,
"require": "7.3.*",
"require": "7.4.*",
"docker": true
}
}

3572
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -10,14 +10,6 @@ when@dev:
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug
channels: ["!event"]
# uncomment to get logging in your browser
# you may have to allow bigger header sizes in your Web server configuration
#firephp:
# type: firephp
# level: info
#chromephp:
# type: chromephp
# level: info
console:
type: console
process_psr_3_messages: false
@ -45,6 +37,7 @@ when@prod:
action_level: error
handler: nested
excluded_http_codes: [404, 405]
channels: ["!deprecation"]
buffer_size: 50 # How many messages should be saved? Prevent memory leaks
nested:
type: stream

View file

@ -1,7 +1,7 @@
framework:
default_locale: 'en'
# Just enable the locales we need for performance reasons.
enabled_locale: '%partdb.locale_menu%'
enabled_locale: ['en', 'de', 'it', 'fr', 'ru', 'ja', 'cs', 'da', 'zh', 'pl']
translator:
default_path: '%kernel.project_dir%/translations'
fallbacks:

View file

@ -1,6 +1,6 @@
twig:
default_path: '%kernel.project_dir%/templates'
form_themes: ['bootstrap_5_horizontal_layout.html.twig', 'form/extended_bootstrap_layout.html.twig', 'form/permission_layout.html.twig', 'form/filter_types_layout.html.twig']
form_themes: ['bootstrap_5_horizontal_layout.html.twig', 'form/extended_bootstrap_layout.html.twig', 'form/permission_layout.html.twig', 'form/filter_types_layout.html.twig', 'form/synonyms_collection.html.twig']
paths:
'%kernel.project_dir%/assets/css': css

View file

@ -10,7 +10,7 @@ parameters:
partdb.title: '%env(string:settings:customization:instanceName)%' # The title shown inside of Part-DB (e.g. in the navbar and on homepage)
partdb.locale_menu: ['en', 'de', 'it', 'fr', 'ru', 'ja', 'cs', 'da', 'zh', 'pl', 'hu'] # The languages that are shown in user drop down menu
partdb.default_uri: '%env(string:DEFAULT_URI)%' # The default URI to use for the Part-DB instance (e.g. https://part-db.example.com/). This is used for generating links in emails
partdb.default_uri: '%env(addSlash:string:DEFAULT_URI)%' # The default URI to use for the Part-DB instance (e.g. https://part-db.example.com/). This is used for generating links in emails
partdb.db.emulate_natural_sort: '%env(bool:DATABASE_EMULATE_NATURAL_SORT)%' # If this is set to true, natural sorting is emulated on platforms that do not support it natively. This can be slow on large datasets.

View file

@ -18,13 +18,13 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co
parts: # e.g. this maps to perms_parts in User/Group database
group: "data"
label: "perm.parts"
label: "[[Part]]"
operations: # Here are all possible operations are listed => the op name is mapped to bit value
read:
label: "perm.read"
# If a part can be read by a user, he can also see all the datastructures (except devices)
alsoSet: ['storelocations.read', 'footprints.read', 'categories.read', 'suppliers.read', 'manufacturers.read',
'currencies.read', 'attachment_types.read', 'measurement_units.read']
'currencies.read', 'attachment_types.read', 'measurement_units.read', 'part_custom_states.read']
apiTokenRole: ROLE_API_READ_ONLY
edit:
label: "perm.edit"
@ -71,7 +71,7 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co
storelocations: &PART_CONTAINING
label: "perm.storelocations"
label: "[[Storage_location]]"
group: "data"
operations:
read:
@ -103,35 +103,39 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co
footprints:
<<: *PART_CONTAINING
label: "perm.part.footprints"
label: "[[Footprint]]"
categories:
<<: *PART_CONTAINING
label: "perm.part.categories"
label: "[[Category]]"
suppliers:
<<: *PART_CONTAINING
label: "perm.part.supplier"
label: "[[Supplier]]"
manufacturers:
<<: *PART_CONTAINING
label: "perm.part.manufacturers"
label: "[[Manufacturer]]"
projects:
<<: *PART_CONTAINING
label: "perm.projects"
label: "[[Project]]"
attachment_types:
<<: *PART_CONTAINING
label: "perm.part.attachment_types"
label: "[[Attachment_type]]"
currencies:
<<: *PART_CONTAINING
label: "perm.currencies"
label: "[[Currency]]"
measurement_units:
<<: *PART_CONTAINING
label: "perm.measurement_units"
label: "[[Measurement_unit]]"
part_custom_states:
<<: *PART_CONTAINING
label: "[[Part_custom_state]]"
tools:
label: "perm.part.tools"

2896
config/reference.php Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,12 @@
# yaml-language-server: $schema=../vendor/symfony/routing/Loader/schema/routing.schema.json
# This file is the entry point to configure the routes of your app.
# Methods with the #[Route] attribute are automatically imported.
# See also https://symfony.com/doc/current/routing.html
# To list all registered routes, run the following command:
# bin/console debug:router
# Redirect every url without an locale to the locale of the user/the global base locale
scan_qr:

View file

@ -1,5 +1,8 @@
# yaml-language-server: $schema=../vendor/symfony/dependency-injection/Loader/schema/services.schema.json
# This file is the entry point to configure your own services.
# Files in the packages/ subdirectory configure your dependencies.
# See also https://symfony.com/doc/current/service_container/import.html
# Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
@ -29,6 +32,9 @@ services:
# this creates a service per class whose id is the fully-qualified class name
App\:
resource: '../src/'
exclude:
- '../src/DataFixtures/'
- '../src/Doctrine/Purger/'
# controllers are imported separately to make sure services can be injected
# as action arguments even if you don't extend any base controller class
@ -227,9 +233,15 @@ services:
arguments:
$enforce_index_php: '%env(bool:NO_URL_REWRITE_AVAILABLE)%'
App\Doctrine\Purger\ResetAutoIncrementPurgerFactory:
App\Repository\PartRepository:
arguments:
$translator: '@translator'
tags: ['doctrine.repository_service']
App\EventSubscriber\UserSystem\PartUniqueIpnSubscriber:
tags:
- { name: 'doctrine.fixtures.purger_factory', alias: 'reset_autoincrement_purger' }
- { name: doctrine.event_listener, event: onFlush, connection: default }
# We are needing this service inside a migration, where only the container is injected. So we need to define it as public, to access it from the container.
App\Services\UserSystem\PermissionPresetsHelper:
@ -262,8 +274,21 @@ services:
tags:
- { name: monolog.processor }
when@test:
when@test: &test
services:
App\DataFixtures\:
resource: '../src/DataFixtures/'
autoconfigure: true
autowire: true
App\Doctrine\Purger\:
resource: '../src/Doctrine/Purger/'
App\Doctrine\Purger\ResetAutoIncrementPurgerFactory:
tags:
- { name: 'doctrine.fixtures.purger_factory', alias: 'reset_autoincrement_purger' }
# Decorate the doctrine fixtures load command to use our custom purger by default
doctrine.fixtures_load_command.custom:
decorates: doctrine.fixtures_load_command
@ -272,3 +297,6 @@ when@test:
- '@doctrine.fixtures.loader'
- '@doctrine'
- { default: '@App\Doctrine\Purger\DoNotUsePurgerFactory' }
when@dev:
*test

View file

@ -17,7 +17,7 @@ This allows external applications to interact with Part-DB, extend it or integra
> Some features might be missing or not working yet.
> Also be aware, that there might be security issues in the API, which could allow attackers to access or edit data via
> the API, which
> they normally should be able to access. So currently you should only use the API with trusted users and trusted
> they normally should not be able to access. So currently you should only use the API with trusted users and trusted
> applications.
Part-DB uses [API Platform](https://api-platform.com/) to provide the API, which allows for easy creation of REST APIs
@ -46,7 +46,7 @@ See [Authentication chapter]({% link api/authentication.md %}) for more details.
The API is split into different endpoints, which are reachable under the `/api/` path of your Part-DB instance (
e.g. `https://your-part-db.local/api/`).
There are various endpoints for each entity type (like `part`, `manufacturer`, etc.), which allow you to read and write data, and some special endpoints like `search` or `statistics`.
There are various endpoints for each entity type (like `parts`, `manufacturers`, etc.), which allow you to read and write data, and some special endpoints like `search` or `statistics`.
For example, all API endpoints for managing categories are available under `/api/categories/`. Depending on the exact
path and the HTTP method used, you can read, create, update or delete categories.
@ -56,7 +56,7 @@ For most entities, there are endpoints like this:
* **POST**: `/api/categories/` - Create a new category
* **GET**: `/api/categories/{id}` - Get a specific category by its ID
* **DELETE**: `/api/categories/{id}` - Delete a specific category by its ID
* **UPDATE**: `/api/categories/{id}` - Update a specific category by its ID. Only the fields which are sent in the
* **PATCH**: `/api/categories/{id}` - Update a specific category by its ID. Only the fields which are sent in the
request are updated, all other fields are left unchanged.
Be aware that you have to set the [JSON Merge Patch](https://datatracker.ietf.org/doc/html/rfc7386) content type
header (`Content-Type: application/merge-patch+json`) for this to work.
@ -106,11 +106,11 @@ This is a great way to test the API and see how it works, without having to writ
By default, all list endpoints are paginated, which means only a certain number of results is returned per request.
To get another page of the results, you have to use the `page` query parameter, which contains the page number you want
to get (e.g. `/api/categoues/?page=2`).
to get (e.g. `/api/categories/?page=2`).
When using JSONLD, the links to the next page are also included in the `hydra:view` property of the response.
To change the size of the pages (the number of items in a single page) use the `itemsPerPage` query parameter (
e.g. `/api/categoues/?itemsPerPage=50`).
e.g. `/api/categories/?itemsPerPage=50`).
See [API Platform docs](https://api-platform.com/docs/core/pagination) for more infos.

View file

@ -23,14 +23,14 @@ each other so that it does not matter which one of your 1000 things of Part you
A part entity has many fields, which can be used to describe it better. Most of the fields are optional:
* **Name** (Required): The name of the part or how you want to call it. This could be a manufacturer-provided name, or a
name you thought of yourself. Each name needs to be unique and must exist in a single category.
name you thought of yourself. Each name needs to be unique and must exist in a single category only.
* **Description**: A short (single-line) description of what this part is/does. For longer information, you should use
the comment field or the specifications
* **Category** (Required): The category (see there) to which this part belongs to.
* **Tags**: The list of tags this part belongs to. Tags can be used to group parts logically (similar to the category),
but tags are much less strict and formal (they don't have to be defined forehands) and you can assign multiple tags to
but tags are much less strict and formal (they don't have to be defined beforehand) and you can assign multiple tags to
a part. When clicking on a tag, a list with all parts which have the same tag, is shown.
* **Min Instock**: *Not really implemented yet*. Parts where the total instock is below this value, will show up for
* **Min Instock**: *Not fully implemented yet*. Parts where the total instock is below this value will show up for
ordering.
* **Footprint**: See there. Useful especially for electronic parts, which have one of the common electronic footprints (
like DIP8, SMD0805 or similar). If a part has no explicitly defined preview picture, the preview picture of its
@ -48,9 +48,9 @@ A part entity has many fields, which can be used to describe it better. Most of
completely trustworthy.
* **Favorite**: Parts with this flag are highlighted in parts lists
* **Mass**: The mass of a single piece of this part (so of a single transistor). Given in grams.
* **Internal Part number** (IPN): Each part is automatically assigned a numerical ID that identifies a part in the
database. This ID depends on when a part was created and can not be changed. If you want to assign your own unique
identifiers, or sync parts identifiers with the identifiers of another database you can use this field.
* **Internal Part Number** (IPN): Each part is automatically assigned a numerical ID that identifies a part in the
database. This ID depends on when a part was created and cannot be changed. If you want to assign your own unique
identifiers, or sync parts identifiers with the identifiers of another database, you can use this field.
### Stock / Part lot
@ -99,12 +99,12 @@ possible category tree could look like this:
### Supplier
A Supplier is a vendor/distributor where you can buy/order parts. Price information of parts is associated with a
A supplier is a vendor/distributor where you can buy/order parts. Price information of parts is associated with a
supplier.
### Manufacturer
A manufacturer represents the company that manufacturers/builds various parts (not necessarily sell them). If the
A manufacturer represents the company that manufactures/builds various parts (not necessarily sells them). If the
manufacturer also sells the parts, you have to create a supplier for that.
### Storage location

View file

@ -6,11 +6,11 @@ nav_order: 5
# Configuration
Part-DBs behavior can be configured to your needs. There are different kinds of configuration options: Options, which are
Part-DB's behavior can be configured to your needs. There are different kinds of configuration options: Options that are
user-changeable (changeable dynamically via frontend), options that can be configured by environment variables, and
options that are only configurable via Symfony config files.
## User configruation
## User configuration
The following things can be changed for every user and a user can change it for himself (if he has the correct permission
for it). Configuration is either possible via the user's own settings page (where you can also change the password) or via
@ -40,10 +40,10 @@ The following configuration options can only be changed by the server administra
variables, changing the `.env.local` file or setting env for your docker container. Here are just the most important
options listed, see `.env` file for the full list of possible env variables.
Environment variables allow to overwrite settings in the web interface. This is useful, if you want to enforce certain
settings to be unchangable by users, or if you want to configure settings in a central place in a deployed environment.
Environment variables allow you to overwrite settings in the web interface. This is useful if you want to enforce certain
settings to be unchangeable by users, or if you want to configure settings in a central place in a deployed environment.
On the settings page, you can hover over a setting to see, which environment variable can be used to overwrite it, it
is shown as tooltip. API keys or similar sensitve data which is overwritten by env variables, are redacted on the web
is shown as tooltip. API keys or similar sensitive data which is overwritten by env variables, are redacted on the web
interface, so that even administrators cannot see them (only the last 2 characters and the length).
For technical and security reasons some settings can only be configured via environment variables and not via the web
@ -105,17 +105,27 @@ bundled with Part-DB. Set `DATABASE_MYSQL_SSL_VERIFY_CERT` if you want to accept
* `part_delete`: Delete operation of an existing part
* `part_create`: Creation of a new part
* `part_stock_operation`: Stock operation on a part (therefore withdraw, add or move stock)
* `datastructure_edit`: Edit operation of an existing datastructure (e.g. category, manufacturer, ...)
* `datastructure_delete`: Delete operation of a existing datastructure (e.g. category, manufacturer, ...)
* `datastructure_create`: Creation of a new datastructure (e.g. category, manufacturer, ...)
* `CHECK_FOR_UPDATES` (default `1`): Set this to 0, if you do not want Part-DB to connect to GitHub to check for new
versions, or if your server can not connect to the internet.
* `datastructure_edit`: Edit operation of an existing data structure (e.g. category, manufacturer, ...)
* `datastructure_delete`: Delete operation of an existing data structure (e.g. category, manufacturer, ...)
* `datastructure_create`: Creation of a new data structure (e.g. category, manufacturer, ...)
* `CHECK_FOR_UPDATES` (default `1`): Set this to 0 if you do not want Part-DB to connect to GitHub to check for new
versions, or if your server cannot connect to the internet.
* `APP_SECRET` (env only): This variable is a configuration parameter used for various security-related purposes,
particularly for securing and protecting various aspects of your application. It's a secret key that is used for
cryptographic operations and security measures (session management, CSRF protection, etc..). Therefore this
value should be handled as confidential data and not shared publicly.
* `SHOW_PART_IMAGE_OVERLAY`: Set to 0 to disable the part image overlay, which appears if you hover over an image in the
part image gallery
* `IPN_SUGGEST_REGEX`: A global regular expression, that part IPNs have to fulfill. Enforce your own format for your users.
* `IPN_SUGGEST_REGEX_HELP`: Define your own user help text for the Regex format specification.
* `IPN_AUTO_APPEND_SUFFIX`: When enabled, an incremental suffix will be added to the user input when entering an existing
* IPN again upon saving.
* `IPN_SUGGEST_PART_DIGITS`: Defines the fixed number of digits used as the increment at the end of an IPN (Internal Part Number).
IPN prefixes, maintained within part categories and their hierarchy, form the foundation for suggesting complete IPNs.
These suggestions become accessible during IPN input of a part. The constant specifies the digits used to calculate and assign
unique increments for parts within a category hierarchy, ensuring consistency and uniqueness in IPN generation.
* `IPN_USE_DUPLICATE_DESCRIPTION`: When enabled, the parts description is used to find existing parts with the same
description and to determine the next available IPN by incrementing their numeric suffix for the suggestion list.
### E-Mail settings (all env only)
@ -136,7 +146,7 @@ bundled with Part-DB. Set `DATABASE_MYSQL_SSL_VERIFY_CERT` if you want to accept
* `TABLE_PARTS_DEFAULT_COLUMNS`: The columns in parts tables, which are visible by default (when loading table for first
time).
Also specify the default order of the columns. This is a comma separated list of column names. Available columns
are: `name`, `id`, `ipn`, `description`, `category`, `footprint`, `manufacturer`, `storage_location`, `amount`, `minamount`, `partUnit`, `addedDate`, `lastModified`, `needs_review`, `favorite`, `manufacturing_status`, `manufacturer_product_number`, `mass`, `tags`, `attachments`, `edit`.
are: `name`, `id`, `ipn`, `description`, `category`, `footprint`, `manufacturer`, `storage_location`, `amount`, `minamount`, `partUnit`, `partCustomState`, `addedDate`, `lastModified`, `needs_review`, `favorite`, `manufacturing_status`, `manufacturer_product_number`, `mass`, `tags`, `attachments`, `edit`.
### History/Eventlog-related settings
@ -252,10 +262,10 @@ markdown (and even some subset of HTML) syntax to format the text.
## parameters.yaml
You can also configure some options via the `config/parameters.yaml` file. This should normally not need,
and you should know what you are doing, when you change something here. You should expect, that you will have to do some
manual merge, when you have changed something here and update to a newer version of Part-DB. It is possible that
configuration options here will change or be completely removed in future versions of Part-DB.
You can also configure some options via the `config/parameters.yaml` file. This should normally not be needed,
and you should know what you are doing when you change something here. You should expect that you will have to do some
manual merges when you have changed something here and update to a newer version of Part-DB. It is possible that
configuration options here will change or be completely removed in future versions of Part-DB.
If you change something here, you have to clear the cache, before the changes will take effect with the
command `bin/console cache:clear`.

View file

@ -18,8 +18,7 @@ It is installed on a web server and so can be accessed with any browser without
> You can log in with username: **user** and password: **user**, to change/create data.
>
> Every change to the master branch gets automatically deployed, so it represents the current development progress and
> is
> maybe not completely stable. Please mind, that the free Heroku instance is used, so it can take some time when loading
> may not be completely stable. Please mind, that the free Heroku instance is used, so it can take some time when loading
> the page
> for the first time.
@ -28,32 +27,32 @@ It is installed on a web server and so can be accessed with any browser without
* Inventory management of your electronic parts. Each part can be assigned to a category, footprint, manufacturer,
and multiple store locations and price information. Parts can be grouped using tags. You can associate various files
like datasheets or pictures with the parts.
* Multi-language support (currently German, English, Russian, Japanese and French (experimental))
* Barcodes/Labels generator for parts and storage locations, scan barcodes via webcam using the builtin barcode scanner
* User system with groups and detailed (fine granular) permissions.
* Multi-language support (currently German, English, Russian, Japanese, French, Czech, Danish, and Chinese)
* Barcodes/Labels generator for parts and storage locations, scan barcodes via webcam using the built-in barcode scanner
* User system with groups and detailed (fine-grained) permissions.
Two-factor authentication is supported (Google Authenticator and Webauthn/U2F keys) and can be enforced for groups.
Password reset via email can be setup.
Password reset via email can be set up.
* Optional support for single sign-on (SSO) via SAML (using an intermediate service
like [Keycloak](https://www.keycloak.org/) you can connect Part-DB to an existing LDAP or Active Directory server)
* Import/Export system
* Project management: Create projects and assign parts to the bill of material (BOM), to show how often you could build
this project and directly withdraw all components needed from DB
* Event log: Track what changes happens to your inventory, track which user does what. Revert your parts to older
* Event log: Track what changes happen to your inventory, track which user does what. Revert your parts to older
versions.
* Responsive design: You can use Part-DB on your PC, your tablet and your smartphone using the same interface.
* Responsive design: You can use Part-DB on your PC, your tablet, and your smartphone using the same interface.
* MySQL, SQLite and PostgreSQL are supported as database backends
* Support for rich text descriptions and comments in parts
* Support for multiple currencies and automatic update of exchange rates supported
* Powerful search and filter function, including parametric search (search for parts according to some specifications)
* Easy migration from an existing PartKeepr instance (see [here]({%link partkeepr_migration.md %}))
* Use cloud providers (like Octopart, Digikey, Farnell or TME) to automatically get part information, datasheets and
* Use cloud providers (like Octopart, Digikey, Farnell, Mouser, or TME) to automatically get part information, datasheets, and
prices for parts (see [here]({% link usage/information_provider_system.md %}))
* API to access Part-DB from other applications/scripts
* [Integration with KiCad]({%link usage/eda_integration.md %}): Use Part-DB as central datasource for your
KiCad and see available parts from Part-DB directly inside KiCad.
* [Integration with KiCad]({%link usage/eda_integration.md %}): Use Part-DB as the central datasource for your
KiCad and see available parts from Part-DB directly inside KiCad.
With these features Part-DB is useful to hobbyists, who want to keep track of their private electronic parts inventory,
or makerspaces, where many users have should have (controlled) access to the shared inventory.
With these features, Part-DB is useful to hobbyists, who want to keep track of their private electronic parts inventory,
or makerspaces, where many users should have (controlled) access to the shared inventory.
Part-DB is also used by small companies and universities for managing their inventory.
@ -68,11 +67,11 @@ See [LICENSE](https://github.com/Part-DB/Part-DB-symfony/blob/master/LICENSE) fo
## Donate for development
If you want to donate to the Part-DB developer, see the sponsor button in the top bar (next to the repo name).
There you will find various methods to support development on a monthly or a one time base.
There you will find various methods to support development on a monthly or a one-time basis.
## Built with
* [Symfony 5](https://symfony.com/): The main framework used for the serverside PHP
* [Symfony 6](https://symfony.com/): The main framework used for the serverside PHP
* [Bootstrap 5](https://getbootstrap.com/) and [Bootswatch](https://bootswatch.com/): Used as website theme
* [Fontawesome](https://fontawesome.com/): Used as icon set
* [Hotwire Stimulus](https://stimulus.hotwired.dev/) and [Hotwire Turbo](https://turbo.hotwired.dev/): Frontend

View file

@ -38,7 +38,7 @@ you have started creating data**. So you should choose the database type for you
* **Performance**: SQLite is not as fast as MySQL or PostgreSQL, especially when using complex queries or many users.
* **Emulated RegEx search**: SQLite does not support RegEx search natively. Part-DB can emulate it, however that is pretty slow.
* **Emualted natural sorting**: SQLite does not support natural sorting natively. Part-DB can emulate it, but it is pretty slow.
* **Emulated natural sorting**: SQLite does not support natural sorting natively. Part-DB can emulate it, but it is pretty slow.
* **Limitations with Unicode**: SQLite has limitations in comparisons and sorting of Unicode characters, which might lead to
unexpected behavior when using non-ASCII characters in your data. For example `µ` (micro sign) is not seen as equal to
`μ` (greek minuscule mu), therefore searching for `µ` (micro sign) will not find parts containing `μ` (mu) and vice versa.
@ -131,7 +131,7 @@ The host (here 127.0.0.1) and port should also be specified according to your My
In the `serverVersion` parameter you can specify the version of the MySQL/MariaDB server you are using, in the way the server returns it
(e.g. `8.0.37` for MySQL and `10.4.14-MariaDB`). If you do not know it, you can leave the default value.
If you want to use a unix socket for the connection instead of a TCP connnection, you can specify the socket path in the `unix_socket` parameter.
If you want to use a unix socket for the connection instead of a TCP connection, you can specify the socket path in the `unix_socket` parameter.
```shell
DATABASE_URL="mysql://user:password@localhost/database?serverVersion=8.0.37&unix_socket=/var/run/mysqld/mysqld.sock"
```
@ -150,7 +150,7 @@ In the `serverVersion` parameter you can specify the version of the PostgreSQL s
The `charset` parameter specify the character set of the database. It should be set to `utf8` to ensure that all characters are stored correctly.
If you want to use a unix socket for the connection instead of a TCP connnection, you can specify the socket path in the `host` parameter.
If you want to use a unix socket for the connection instead of a TCP connection, you can specify the socket path in the `host` parameter.
```shell
DATABASE_URL="postgresql://db_user@localhost/db_name?serverVersion=16.6&charset=utf8&host=/var/run/postgresql"
```
@ -177,6 +177,6 @@ In natural sorting, it would be sorted as:
Part-DB can sort names in part tables and tree views naturally. PostgreSQL and MariaDB 10.7+ support natural sorting natively,
and it is automatically used if available.
For SQLite and MySQL < 10.7 it has to be emulated if wanted, which is pretty slow. Therefore it has to be explicity enabled by setting the
For SQLite and MySQL < 10.7 it has to be emulated if wanted, which is pretty slow. Therefore it has to be explicitly enabled by setting the
`DATABASE_EMULATE_NATURAL_SORT` environment variable to `1`. If it is 0 the classical binary sorting is used, on these databases. The emulations
might have some quirks and issues, so it is recommended to use a database which supports natural sorting natively, if you want to use it.

View file

@ -19,7 +19,7 @@ automatic mail providers (like MailChimp or SendGrid). If you want to use one of
Mailer documentation for more information.
We will only cover the configuration of an SMTP provider here, which is sufficient for most use-cases.
You will need an email account, which you can use send emails from via password-bases SMTP authentication, this account
You will need an email account, which you can use to send emails from via password-based SMTP authentication, this account
should be dedicated to Part-DB.
To configure the SMTP provider, you have to set the following environment variables:

View file

@ -8,4 +8,4 @@ has_children: true
# Installation
Below you can find some guides to install Part-DB.
For the hobbyists without much experience, we recommend the docker installation or direct installation on debian.
For hobbyists without much experience, we recommend the Docker installation or direct installation on Debian.

View file

@ -136,18 +136,18 @@ services:
# In docker env logs will be redirected to stderr
- APP_ENV=docker
# Uncomment this, if you want to use the automatic database migration feature. With this you have you do not have to
# Uncomment this, if you want to use the automatic database migration feature. With this you do not have to
# run the doctrine:migrations:migrate commands on installation or upgrade. A database backup is written to the uploads/
# folder (under .automigration-backup), so you can restore it, if the migration fails.
# This feature is currently experimental, so use it at your own risk!
# - DB_AUTOMIGRATE=true
# You can configure Part-DB using the webUI or environment variables
# However you can add add any other environment configuration you want here
# However you can add any other environment configuration you want here
# See .env file for all available options or https://docs.part-db.de/configuration.html
# Override value if you want to show to show a given text on homepage.
# When this is outcommented the webUI can be used to configure the banner
# Override value if you want to show a given text on homepage.
# When this is commented out the webUI can be used to configure the banner
#- BANNER=This is a test banner<br>with a line break
database:

View file

@ -7,7 +7,7 @@ nav_order: 10
# Nginx
You can also use [nginx](https://www.nginx.com/) as webserver for Part-DB. Setup Part-DB with apache is a bit easier, so
You can also use [nginx](https://www.nginx.com/) as webserver for Part-DB. Setting up Part-DB with Apache is a bit easier, so
this is the method shown in the guides. This guide assumes that you already have a working nginx installation with PHP
configured.

View file

@ -21,7 +21,7 @@ LDAP or Active Directory server.
{: .warning }
> This feature is currently in beta. Please report any bugs you find.
> So far it has only tested with Keycloak, but it should work with any SAML 2.0 compatible identity provider.
> So far it has only been tested with Keycloak, but it should work with any SAML 2.0 compatible identity provider.
This guide will show you how to configure Part-DB with [Keycloak](https://www.keycloak.org/) as the SAML identity
provider, but it should work with any SAML 2.0 compatible identity provider.
@ -75,8 +75,8 @@ the [Keycloak Getting Started Guide](https://www.keycloak.org/docs/latest/gettin
### Configure Part-DB to use SAML
1. Open the `.env.local` file of Part-DB (or the docker-compose.yaml) for edit
2. Set the `SAMLP_SP_PRIVATE_KEY` environment variable to the content of the private key file you downloaded in the
1. Open the `.env.local` file of Part-DB (or the docker-compose.yaml) for editing
2. Set the `SAML_SP_PRIVATE_KEY` environment variable to the content of the private key file you downloaded in the
previous step. It should start with `MIEE` and end with `=`.
3. Set the `SAML_SP_X509_CERT` environment variable to the content of the Certificate field shown in the `Keys` tab of
the SAML client in Keycloak. It should start with `MIIC` and end with `=`.

View file

@ -9,7 +9,7 @@ Sometimes things go wrong and Part-DB shows an error message. This page should h
## Error messages
When a common, easy fixable error occurs (like a non-up-to-date database), Part-DB will show you some short instructions
When a common, easily fixable error occurs (like a non-up-to-date database), Part-DB will show you some short instructions
on how to fix the problem. If you have a problem that is not listed here, please open an issue on GitHub.
## General procedure
@ -28,9 +28,9 @@ php bin/console cache:clear
php bin/console doctrine:migrations:migrate
```
If this does not help, please [open an issue on GitHub](https://github.com/Part-DB/Part-DB-symfony).
If this does not help, please [open an issue on GitHub](https://github.com/Part-DB/Part-DB-server).
## Search for the user and reset the password:
## Search for a user and reset the password
You can list all users with the following command: `php bin/console partdb:users:list`
To reset the password of a user you can use the following
@ -52,4 +52,4 @@ Please include the error logs in your issue on GitHub, if you open an issue.
## Report Issue
If an error occurs, or you found a bug, please [open an issue on GitHub](https://github.com/Part-DB/Part-DB-symfony).
If an error occurs, or you found a bug, please [open an issue on GitHub](https://github.com/Part-DB/Part-DB-server).

View file

@ -27,7 +27,7 @@ about the requirements at all.
## Changes
* Configuration is now preferably done via a web settings interface. You can still use environment variables, these overwrite
the settings in the web interface. Existing configuration will still work, but you should consider migriting them to the
the settings in the web interface. Existing configuration will still work, but you should consider migrating them to the
web interface as described below.
* The `config/banner.md` file that could been used to customize the banner text, was removed. You can now set the banner text
directly in the admin interface, or by setting the `BANNER` environment variable. If you want to keep your existing
@ -43,7 +43,7 @@ The upgrade process works very similar to a normal (minor release) upgrade.
### Direct installation
**Be sure to execute the following steps as the user that owns the Part-DB files (e.g. `www-data`, or your webserver user). So prepend a `sudo -u wwww-data` where necessary.**
**Be sure to execute the following steps as the user that owns the Part-DB files (e.g. `www-data`, or your webserver user). So prepend a `sudo -u www-data` where necessary.**
1. Make a backup of your existing Part-DB installation, including the database, data directories and the configuration files and `.env.local` file.
The `php bin/console partdb:backup` command can help you with this.
@ -51,7 +51,7 @@ The `php bin/console partdb:backup` command can help you with this.
3. Remove the `var/cache/` directory inside the Part-DB installation to ensure that no old cache files remain.
4. Run `composer install --no-dev -o` to update the dependencies.
5. Run `yarn install` and `yarn build` to update the frontend assets.
6. Rund `php bin/console doctrine:migrations:migrate` to update the database schema.
6. Run `php bin/console doctrine:migrations:migrate` to update the database schema.
7. Clear the cache with `php bin/console cache:clear`.
8. Open your Part-DB instance in the browser and log in as an admin user.
9. Go to the user or group permissions page, and give yourself (and other administrators) the right to change system settings (under "System" and "Configuration").
@ -79,7 +79,7 @@ To change it, you must migrate your environment variable configuration to the ne
For this there is the new console command `settings:migrate-env-to-settings`, which reads in all environment variables used to overwrite
settings and write them to the database, so that you can safely delete them from your environment variable configuration afterwards, without
loosing your configuration.
losing your configuration.
To run the command, execute `php bin/console settings:migrate-env-to-settings --all` as webserver user (or run `docker exec --user=www-data -it partdb php bin/console settings:migrate-env-to-settings --all` for docker containers).
It will list you all environment variables, it found and ask you for confirmation to migrate them. Answer with `yes` to migrate them and hit enter.

View file

@ -5,5 +5,7 @@ nav_order: 7
has_children: true
---
# Upgrade
This section provides information on how to upgrade Part-DB to the latest version.
This is intended for major release upgrades, where requirements or things changes significantly.
This is intended for major release upgrades, where requirements or things change significantly.

View file

@ -24,7 +24,7 @@ sections carefully before proceeding to upgrade.
also more sensitive stuff like database migration works via CLI now, so you should have console access on your server.
* Markdown/HTML is now used instead of BBCode for rich text in description and command fields.
It is possible to migrate your existing BBCode to Markdown
via `php bin/console php bin/console partdb:migrations:convert-bbcode`.
via `php bin/console partdb:migrations:convert-bbcode`.
* Server exceptions are not logged into event log anymore. For security reasons (exceptions can contain sensitive
information) exceptions are only logged to server log (by default under './var/log'), so only the server admins can access it.
* Profile labels are now saved in the database (before they were saved in a separate JSON file). **The profiles of legacy

View file

@ -6,7 +6,7 @@ parent: Usage
# Backup and Restore Data
When working productively you should back up the data and configuration of Part-DB regularly to prevent data loss. This
When working productively, you should back up the data and configuration of Part-DB regularly to prevent data loss. This
is also useful if you want to migrate your Part-DB instance from one server to another. In that case, you just have to
back up the data on server 1, move the backup to server 2, install Part-DB on server 2, and restore the backup.
@ -27,7 +27,7 @@ for more info about these options.
## Backup (manual)
3 parts have to be backup-ed: The configuration files, which contain the instance-specific options, the
Three parts have to be backed up: The configuration files, which contain the instance-specific options, the
uploaded files of attachments, and the database containing the most data of Part-DB.
Everything else like thumbnails and cache files, are recreated automatically when needed.
@ -44,7 +44,7 @@ You have to recursively copy the `uploads/` folder and the `public/media` folder
#### SQLite
If you are using sqlite, it is sufficient to just copy your `app.db` from your database location (normally `var/app.db`)
If you are using SQLite, it is sufficient to just copy your `app.db` from your database location (normally `var/app.db`)
to your backup location.
#### MySQL / MariaDB
@ -56,7 +56,7 @@ interface (`mysqldump -uBACKUP -pPASSWORD DATABASE`)
## Restore
Install Part-DB as usual as described in the installation section, except for the database creation/migration part. You
have to use the same database type (SQLite or MySQL) as on the backuped server instance.
have to use the same database type (SQLite or MySQL) as on the backed up server instance.
### Restore configuration
@ -71,7 +71,7 @@ Copy the `uploads/` and the `public/media/` folder from your backup into your ne
#### SQLite
Copy the backup-ed `app.db` into the database folder normally `var/app.db` in Part-DB root folder.
Copy the backed up `app.db` into the database folder normally `var/app.db` in Part-DB root folder.
#### MySQL / MariaDB

View file

@ -8,7 +8,7 @@ parent: Usage
Part-DB provides some console commands to display various information or perform some tasks.
The commands are invoked from the main directory of Part-DB with the command `php bin/console [command]` in the context
of the database user (so usually the webserver user), so you maybe have to use `sudo` or `su` to execute the commands:
of the web server user (so usually the webserver user), so you may have to use `sudo` or `su` to execute the commands:
```bash
sudo -u www-data php bin/console [command]
@ -17,8 +17,8 @@ sudo -u www-data php bin/console [command]
You can get help for every command with the parameter `--help`. See `php bin/console` for a list of all available
commands.
If you are running Part-DB in a docker container, you must either execute the commands from a shell inside a container,
or use the `docker exec` command to execute the command directly inside the container. For example if you docker container
If you are running Part-DB in a Docker container, you must either execute the commands from a shell inside the container,
or use the `docker exec` command to execute the command directly inside the container. For example, if your Docker container
is named `partdb`, you can execute the command `php bin/console cache:clear` with the following command:
```bash
@ -61,7 +61,7 @@ docker exec --user=www-data partdb php bin/console cache:clear
* `partdb:attachments:clean-unused`: Remove all attachments which are not used by any database entry (e.g. orphaned
attachments)
* `partdb:cache:clear`: Clears all caches, so the next page load will be slower, but the cache will be rebuilt. This can
maybe fix some issues, when the cache were corrupted. This command is also needed after changing things in
maybe fix some issues when the cache was corrupted. This command is also needed after changing things in
the `parameters.yaml` file or upgrading Part-DB.
* `partdb:migrations:import-partkeepr`: Imports a mysqldump XML dump of a PartKeepr database into Part-DB. This is only
needed for users, which want to migrate from PartKeepr to Part-DB. *All existing data in the Part-DB database is
@ -76,6 +76,6 @@ The value of the environment variable is copied to the settings database, so the
## Attachment commands
* `php bin/console partdb:attachments:download`: Download all attachments, which are not already downloaded, to the
local filesystem. This is useful to create local backups of the attachments, no matter what happens on the remote and
also makes pictures thumbnails available for the frontend for them
* `php bin/console partdb:attachments:download`: Download all attachments that are not already downloaded to the
local filesystem. This is useful to create local backups of the attachments, no matter what happens on the remote, and
also makes picture thumbnails available for the frontend for them.

View file

@ -17,14 +17,14 @@ This also allows to configure available and usable parts and their properties in
## KiCad Setup
{: .important }
> Part-DB uses the HTTP library feature of KiCad, which is experimental and not part of the stable KiCad 7 releases. If you want to use this feature, you need to install a KiCad nightly build (7.99 version). This feature will most likely also be part of KiCad 8.
> Part-DB uses the HTTP library feature of KiCad, which was experimental in earlier versions. If you want to use this feature, you need to install KiCad 8 or newer.
Part-DB should be accessible from the PCs with KiCAD. The URL should be stable (so no dynamically changing IP).
You require a user account in Part-DB, which has permission to access Part-DB API and create API tokens. Every user can have its own account, or you set up a shared read-only account.
Part-DB should be accessible from the PCs with KiCad. The URL should be stable (so no dynamically changing IP).
You require a user account in Part-DB, which has permission to access the Part-DB API and create API tokens. Every user can have their own account, or you set up a shared read-only account.
To connect KiCad with Part-DB do the following steps:
1. Create an API token on the user settings page for the KiCAD application and copy/save it, when it is shown. Currently, KiCad can only read Part-DB database, so a token with a read-only scope is enough.
1. Create an API token on the user settings page for the KiCad application and copy/save it when it is shown. Currently, KiCad can only read the Part-DB database, so a token with a read-only scope is enough.
2. Add some EDA metadata to parts, categories, or footprints. Only parts with usable info will show up in KiCad. See below for more info.
3. Create a file `partd.kicad_httplib` (or similar, only the extension is important) with the following content:
```
@ -54,18 +54,18 @@ Part-DB doesn't save any concrete footprints or symbols for the part. Instead, P
You can define this on a per-part basis using the KiCad symbol and KiCad footprint field in the EDA tab of the part editor. Or you can define it at a category (symbol) or footprint level, to assign this value to all parts with this category and footprint.
For example, to configure the values for a BC547 transistor you would put `Transistor_BJT:BC547` on the parts Kicad symbol to give it the right schematic symbol in EEschema and `Package_TO_SOT_THT:TO-92` to give it the right footprint in PcbNew.
For example, to configure the values for a BC547 transistor you would put `Transistor_BJT:BC547` in the part's KiCad symbol field to give it the right schematic symbol in Eeschema and `Package_TO_SOT_THT:TO-92` to give it the right footprint in Pcbnew.
If you type in a character, you will get an autocomplete list of all symbols and footprints available in the KiCad standard library. You can also input your own value.
### Parts and category visibility
Only parts and their categories, on which there is any kind of EDA metadata are defined show up in KiCad. So if you want to see parts in KiCad,
Only parts and their categories on which there is any kind of EDA metadata defined show up in KiCad. So if you want to see parts in KiCad,
you need to define at least a symbol, footprint, reference prefix, or value on a part, category or footprint.
You can use the "Force visibility" checkbox on a part or category to override this behavior and force parts to be visible or hidden in KiCad.
*Please note that KiCad caches the library categories. So if you change something, which would change the visible categories in KiCad, you have to reload EEschema to see the changes.*
*Please note that KiCad caches the library categories. So if you change something that would change the visible categories in KiCad, you have to reload Eeschema to see the changes.*
### Category depth in KiCad

View file

@ -6,7 +6,7 @@ nav_order: 4
# Getting started
After Part-DB you should begin with customizing the settings, and setting up the basic structures.
After installing Part-DB, you should begin with customizing the settings and setting up the basic structures.
Before starting, it's useful to read a bit about the [concepts of Part-DB]({% link concepts.md %}).
1. TOC
@ -14,8 +14,8 @@ Before starting, it's useful to read a bit about the [concepts of Part-DB]({% li
## Customize system settings
Before starting creating datastructures, you should check the system settings to ensure that they fit your needs.
After login as an administrator, you can find the settings in the sidebar under `Tools -> System -> Settings`.
Before starting creating data structures, you should check the system settings to ensure that they fit your needs.
After logging in as an administrator, you can find the settings in the sidebar under `Tools -> System -> Settings`.
![image]({% link assets/getting_started/system_settings.png %})
Here you can change various settings, like the name of your Part-DB instance (which is shown in the title bar of the
@ -35,9 +35,9 @@ the navigation bar drop-down with the user symbol).
![image]({% link assets/getting_started/change_password.png %})
There you can also find the option, to set up Two-Factor Authentication methods like Google Authenticator. Using this is
There you can also find the option to set up Two-Factor Authentication methods like Google Authenticator. Using this is
highly recommended (especially if you have admin permissions) to increase the security of your account. (Two-factor authentication
even can be enforced for all members of a user group)
can even be enforced for all members of a user group)
In the user settings panel, you can change account info like your username, your first and last name (which will be
shown alongside your username to identify you better), department information, and your email address. The email address
@ -64,7 +64,7 @@ $E=mc^2$) or `$$` (like `$$E=mc^2$$`) which will be rendered as a block, like so
When logged in as administrator, you can open the users menu in the `Tools` section of the sidebar
under `System -> Users`.
On this page you can create new users, change their passwords and settings, and change their permissions.
For each user who should use Part-DB you should set up their own account so that tracking of what user did works
For each user who should use Part-DB, you should set up their own account so that tracking of what each user did works
properly.
![image]({% link assets/getting_started/user_admin.png %})
@ -207,7 +207,7 @@ You have to enter at least a name for the part and choose a category for it, the
However, it is recommended to fill out as much information as possible, as this will make it easier to find the part
later.
You can choose from your created datastructures to add manufacturer information, supplier information, etc. to the part.
You can also create new datastructures on the fly, if you want to add additional information to the part, by typing the
name of the new datastructure in the field and select the "New ..." option in the dropdown menu. See [tips]({% link
You can choose from your created data structures to add manufacturer information, supplier information, etc. to the part.
You can also create new data structures on the fly if you want to add additional information to the part, by typing the
name of the new data structure in the field and selecting the "New ..." option in the dropdown menu. See [tips]({% link
usage/tips_tricks.md %}) for more information.

View file

@ -20,7 +20,7 @@ Part-DB. Data can also be exported from Part-DB into various formats.
> individually in the permissions settings.
If you want to import data from PartKeepr you might want to look into the [PartKeepr migration guide]({% link
upgrade/upgrade_legacy.md %}).
partkeepr_migration.md %}).
### Import parts
@ -47,9 +47,9 @@ You can upload the file that should be imported here and choose various options
the import file (or the export will error, if no category is specified).
* **Mark all imported parts as "Needs review"**: If this is selected, all imported parts will be marked as "Needs
review" after the import. This can be useful if you want to review all imported parts before using them.
* **Create unknown data structures**: If this is selected Part-DB will create new data structures (like categories,
manufacturers, etc.) if no data structure(s) with the same name and path already exists. If this is not selected, only
existing data structures will be used and if no matching data strucure is found, the imported parts field will be empty.
* **Create unknown data structures**: If this is selected, Part-DB will create new data structures (like categories,
manufacturers, etc.) if no data structure(s) with the same name and path already exist. If this is not selected, only
existing data structures will be used, and if no matching data structure is found, the imported parts field will be empty.
* **Path delimiter**: Part-DB allows you to create/select nested data structures (like categories, manufacturers, etc.)
by using a path (e.g. `Category 1->Category 1.1`, which will select/create the `Category 1.1` whose parent
is `Category 1`). This path is separated by the path delimiter. If you want to use a different path delimiter than the

View file

@ -78,7 +78,7 @@ results will be shown.
## Data providers
The system tries to be as flexible as possible, so many different information sources can be used.
Each information source is called am "info provider" and handles the communication with the external source.
Each information source is called an "info provider" and handles the communication with the external source.
The providers are just a driver that handles the communication with the different external sources and converts them
into a common format Part-DB understands.
That way it is pretty easy to create new providers as they just need to do very little work.
@ -157,7 +157,7 @@ again, to establish a new connection.
### TME
The TME provider uses the API of [TME](https://www.tme.eu/) to search for parts and getting shopping information from
The TME provider uses the API of [TME](https://www.tme.eu/) to search for parts and get shopping information from
them.
To use it you have to create an account at TME and get an API key on the [TME API page](https://developers.tme.eu/en/).
You have to generate a new anonymous key there and enter the key and secret in the Part-DB env configuration (see
@ -176,10 +176,10 @@ The following env configuration options are available:
### Farnell / Element14 / Newark
The Farnell provider uses the [Farnell API](https://partner.element14.com/) to search for parts and getting shopping
The Farnell provider uses the [Farnell API](https://partner.element14.com/) to search for parts and get shopping
information from [Farnell](https://www.farnell.com/).
You have to create an account at Farnell and get an API key on the [Farnell API page](https://partner.element14.com/).
Register a new application there (settings does not matter, as long as you select the "Product Search API") and you will
Register a new application there (settings do not matter, as long as you select the "Product Search API") and you will
get an API key.
The following env configuration options are available:
@ -191,17 +191,13 @@ The following env configuration options are available:
### Mouser
The Mouser provider uses the [Mouser API](https://www.mouser.de/api-home/) to search for parts and getting shopping
The Mouser provider uses the [Mouser API](https://www.mouser.de/api-home/) to search for parts and get shopping
information from [Mouser](https://www.mouser.com/).
You have to create an account at Mouser and register for an API key for the Search API on
the [Mouser API page](https://www.mouser.de/api-home/).
You will receive an API token, which you have to put in the Part-DB env configuration (see below):
At the registration you choose a country, language, and currency in which you want to get the results.
*Attention*: Currently (January 2024) the mouser API seems to be somewhat broken, in the way that it does not return any
information about datasheets and part specifications. Therefore Part-DB can not retrieve them, even if they are shown
at the mouser page. See [issue #503](https://github.com/Part-DB/Part-DB-server/issues/503) for more info.
Following env configuration options are available:
* `PROVIDER_MOUSER_KEY`: The API key you got from Mouser (mandatory)
@ -217,7 +213,7 @@ Following env configuration options are available:
webshop uses an internal JSON based API to render the page. Part-DB can use this inofficial API to get part information
from LCSC.
**Please note, that the use of this internal API is not intended or endorsed by LCS and it could break at any time. So use it at your own risk.**
**Please note that the use of this internal API is not intended or endorsed by LCSC and it could break at any time. So use it at your own risk.**
An API key is not required, it is enough to enable the provider using the following env configuration options:
@ -226,7 +222,7 @@ An API key is not required, it is enough to enable the provider using the follow
### OEMsecrets
The oemsecrets provider uses the [oemsecrets API](https://www.oemsecrets.com/) to search for parts and getting shopping
The oemsecrets provider uses the [oemsecrets API](https://www.oemsecrets.com/) to search for parts and get shopping
information from them. Similar to octopart it aggregates offers from different distributors.
You can apply for a free API key on the [oemsecrets API page](https://www.oemsecrets.com/api/) and put the key you get

View file

@ -6,10 +6,10 @@ parent: Usage
# Labels
Part-DB support the generation and printing of labels for parts, part lots and storage locations.
Part-DB supports the generation and printing of labels for parts, part lots and storage locations.
You can use the "Tools -> Label generator" menu entry to create labels or click the label generation link on the part.
You can define label templates by creating Label profiles. This way you can create many similar-looking labels with for
You can define label templates by creating label profiles. This way you can create many similar-looking labels for
many parts.
The content of the labels is defined by the template's content field. You can use the WYSIWYG editor to create and style

View file

@ -65,7 +65,7 @@ $$E=mc^2$$
## Update currency exchange rates automatically
Part-DB can update the currency exchange rates of all defined currencies programmatically
by calling the `php bin/console partdb:currencies:update-exchange-rates`.
by calling `php bin/console partdb:currencies:update-exchange-rates`.
If you call this command regularly (e.g. with a cronjob), you can keep the exchange rates up-to-date.
@ -88,9 +88,9 @@ the user as "owner" of a part lot. This way, only he is allowed to add or remove
## Update notifications
Part-DB can show you a notification that there is a newer version than currently installed available. The notification
Part-DB can show you a notification that there is a newer version than currently installed. The notification
will be shown on the homepage and the server info page.
It is only be shown to users which has the `Show available Part-DB updates` permission.
It is only shown to users which have the `Show available Part-DB updates` permission.
For the notification to work, Part-DB queries the GitHub API every 2 days to check for new releases. No data is sent to
GitHub besides the metadata required for the connection (so the public IP address of your computer running Part-DB).
@ -98,6 +98,6 @@ If you don't want Part-DB to query the GitHub API, or if your server can not rea
update notifications by setting the `CHECK_FOR_UPDATES` option to `false`.
## Internet access via proxy
If you server running Part-DB does not have direct access to the internet, but has to use a proxy server, you can configure
If your server running Part-DB does not have direct access to the internet, but has to use a proxy server, you can configure
the proxy settings in the `.env.local` file (or docker env config). You can set the `HTTP_PROXY` and `HTTPS_PROXY` environment
variables to the URL of your proxy server. If your proxy server requires authentication, you can include the username and password in the URL.

View file

@ -0,0 +1,605 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use App\Migration\AbstractMultiPlatformMigration;
use Doctrine\DBAL\Schema\Schema;
final class Version20250321075747 extends AbstractMultiPlatformMigration
{
public function getDescription(): string
{
return 'Create entity table for custom part states and add custom state to parts';
}
public function mySQLUp(Schema $schema): void
{
$this->addSql(<<<'SQL'
CREATE TABLE part_custom_states (
id INT AUTO_INCREMENT NOT NULL,
parent_id INT DEFAULT NULL,
id_preview_attachment INT DEFAULT NULL,
name VARCHAR(255) NOT NULL,
comment LONGTEXT NOT NULL,
not_selectable TINYINT(1) NOT NULL,
alternative_names LONGTEXT DEFAULT NULL,
last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
INDEX IDX_F552745D727ACA70 (parent_id),
INDEX IDX_F552745DEA7100A1 (id_preview_attachment),
INDEX part_custom_state_name (name),
PRIMARY KEY(id)
)
DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci`
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE part_custom_states ADD CONSTRAINT FK_F552745D727ACA70 FOREIGN KEY (parent_id) REFERENCES part_custom_states (id)
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE part_custom_states ADD CONSTRAINT FK_F552745DEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES attachments (id) ON DELETE SET NULL
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE parts ADD id_part_custom_state INT DEFAULT NULL AFTER id_part_unit
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE parts ADD CONSTRAINT FK_6940A7FEA3ED1215 FOREIGN KEY (id_part_custom_state) REFERENCES part_custom_states (id)
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX IDX_6940A7FEA3ED1215 ON parts (id_part_custom_state)
SQL);
}
public function mySQLDown(Schema $schema): void
{
$this->addSql(<<<'SQL'
ALTER TABLE parts DROP FOREIGN KEY FK_6940A7FEA3ED1215
SQL);
$this->addSql(<<<'SQL'
DROP INDEX IDX_6940A7FEA3ED1215 ON parts
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE parts DROP id_part_custom_state
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE part_custom_states DROP FOREIGN KEY FK_F552745D727ACA70
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE part_custom_states DROP FOREIGN KEY FK_F552745DEA7100A1
SQL);
$this->addSql(<<<'SQL'
DROP TABLE part_custom_states
SQL);
}
public function sqLiteUp(Schema $schema): void
{
$this->addSql(<<<'SQL'
CREATE TABLE "part_custom_states" (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
parent_id INTEGER DEFAULT NULL,
id_preview_attachment INTEGER DEFAULT NULL,
name VARCHAR(255) NOT NULL,
comment CLOB NOT NULL,
not_selectable BOOLEAN NOT NULL,
alternative_names CLOB DEFAULT NULL,
last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
CONSTRAINT FK_F552745D727ACA70 FOREIGN KEY (parent_id) REFERENCES "part_custom_states" (id) NOT DEFERRABLE INITIALLY IMMEDIATE,
CONSTRAINT FK_F5AF83CFEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE
)
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX IDX_F552745D727ACA70 ON "part_custom_states" (parent_id)
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX part_custom_state_name ON "part_custom_states" (name)
SQL);
$this->addSql(<<<'SQL'
CREATE TEMPORARY TABLE __temp__parts AS
SELECT
id,
id_preview_attachment,
id_category,
id_footprint,
id_part_unit,
id_manufacturer,
order_orderdetails_id,
built_project_id,
datetime_added,
name,
last_modified,
needs_review,
tags,
mass,
description,
comment,
visible,
favorite,
minamount,
manufacturer_product_url,
manufacturer_product_number,
manufacturing_status,
order_quantity,
manual_order,
ipn,
provider_reference_provider_key,
provider_reference_provider_id,
provider_reference_provider_url,
provider_reference_last_updated,
eda_info_reference_prefix,
eda_info_value,
eda_info_invisible,
eda_info_exclude_from_bom,
eda_info_exclude_from_board,
eda_info_exclude_from_sim,
eda_info_kicad_symbol,
eda_info_kicad_footprint
FROM parts
SQL);
$this->addSql(<<<'SQL'
DROP TABLE parts
SQL);
$this->addSql(<<<'SQL'
CREATE TABLE parts (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
id_preview_attachment INTEGER DEFAULT NULL,
id_category INTEGER NOT NULL,
id_footprint INTEGER DEFAULT NULL,
id_part_unit INTEGER DEFAULT NULL,
id_manufacturer INTEGER DEFAULT NULL,
id_part_custom_state INTEGER DEFAULT NULL,
order_orderdetails_id INTEGER DEFAULT NULL,
built_project_id INTEGER DEFAULT NULL,
datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
name VARCHAR(255) NOT NULL,
last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
needs_review BOOLEAN NOT NULL,
tags CLOB NOT NULL,
mass DOUBLE PRECISION DEFAULT NULL,
description CLOB NOT NULL,
comment CLOB NOT NULL,
visible BOOLEAN NOT NULL,
favorite BOOLEAN NOT NULL,
minamount DOUBLE PRECISION NOT NULL,
manufacturer_product_url CLOB NOT NULL,
manufacturer_product_number VARCHAR(255) NOT NULL,
manufacturing_status VARCHAR(255) DEFAULT NULL,
order_quantity INTEGER NOT NULL,
manual_order BOOLEAN NOT NULL,
ipn VARCHAR(100) DEFAULT NULL,
provider_reference_provider_key VARCHAR(255) DEFAULT NULL,
provider_reference_provider_id VARCHAR(255) DEFAULT NULL,
provider_reference_provider_url VARCHAR(255) DEFAULT NULL,
provider_reference_last_updated DATETIME DEFAULT NULL,
eda_info_reference_prefix VARCHAR(255) DEFAULT NULL,
eda_info_value VARCHAR(255) DEFAULT NULL,
eda_info_invisible BOOLEAN DEFAULT NULL,
eda_info_exclude_from_bom BOOLEAN DEFAULT NULL,
eda_info_exclude_from_board BOOLEAN DEFAULT NULL,
eda_info_exclude_from_sim BOOLEAN DEFAULT NULL,
eda_info_kicad_symbol VARCHAR(255) DEFAULT NULL,
eda_info_kicad_footprint VARCHAR(255) DEFAULT NULL,
CONSTRAINT FK_6940A7FEEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES attachments (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE,
CONSTRAINT FK_6940A7FE5697F554 FOREIGN KEY (id_category) REFERENCES categories (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE,
CONSTRAINT FK_6940A7FE7E371A10 FOREIGN KEY (id_footprint) REFERENCES footprints (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE,
CONSTRAINT FK_6940A7FE2626CEF9 FOREIGN KEY (id_part_unit) REFERENCES measurement_units (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE,
CONSTRAINT FK_6940A7FE1ECB93AE FOREIGN KEY (id_manufacturer) REFERENCES manufacturers (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE,
CONSTRAINT FK_6940A7FEA3ED1215 FOREIGN KEY (id_part_custom_state) REFERENCES "part_custom_states" (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE,
CONSTRAINT FK_6940A7FE81081E9B FOREIGN KEY (order_orderdetails_id) REFERENCES orderdetails (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE,
CONSTRAINT FK_6940A7FEE8AE70D9 FOREIGN KEY (built_project_id) REFERENCES projects (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE
)
SQL);
$this->addSql(<<<'SQL'
INSERT INTO parts (
id,
id_preview_attachment,
id_category,
id_footprint,
id_part_unit,
id_manufacturer,
order_orderdetails_id,
built_project_id,
datetime_added,
name,
last_modified,
needs_review,
tags,
mass,
description,
comment,
visible,
favorite,
minamount,
manufacturer_product_url,
manufacturer_product_number,
manufacturing_status,
order_quantity,
manual_order,
ipn,
provider_reference_provider_key,
provider_reference_provider_id,
provider_reference_provider_url,
provider_reference_last_updated,
eda_info_reference_prefix,
eda_info_value,
eda_info_invisible,
eda_info_exclude_from_bom,
eda_info_exclude_from_board,
eda_info_exclude_from_sim,
eda_info_kicad_symbol,
eda_info_kicad_footprint)
SELECT
id,
id_preview_attachment,
id_category,
id_footprint,
id_part_unit,
id_manufacturer,
order_orderdetails_id,
built_project_id,
datetime_added,
name,
last_modified,
needs_review,
tags,
mass,
description,
comment,
visible,
favorite,
minamount,
manufacturer_product_url,
manufacturer_product_number,
manufacturing_status,
order_quantity,
manual_order,
ipn,
provider_reference_provider_key,
provider_reference_provider_id,
provider_reference_provider_url,
provider_reference_last_updated,
eda_info_reference_prefix,
eda_info_value,
eda_info_invisible,
eda_info_exclude_from_bom,
eda_info_exclude_from_board,
eda_info_exclude_from_sim,
eda_info_kicad_symbol,
eda_info_kicad_footprint
FROM __temp__parts
SQL);
$this->addSql(<<<'SQL'
DROP TABLE __temp__parts
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX parts_idx_name ON parts (name)
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX parts_idx_ipn ON parts (ipn)
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX parts_idx_datet_name_last_id_needs ON parts (datetime_added, name, last_modified, id, needs_review)
SQL);
$this->addSql(<<<'SQL'
CREATE UNIQUE INDEX UNIQ_6940A7FEE8AE70D9 ON parts (built_project_id)
SQL);
$this->addSql(<<<'SQL'
CREATE UNIQUE INDEX UNIQ_6940A7FE81081E9B ON parts (order_orderdetails_id)
SQL);
$this->addSql(<<<'SQL'
CREATE UNIQUE INDEX UNIQ_6940A7FE3D721C14 ON parts (ipn)
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX IDX_6940A7FEEA7100A1 ON parts (id_preview_attachment)
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX IDX_6940A7FE7E371A10 ON parts (id_footprint)
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX IDX_6940A7FE5697F554 ON parts (id_category)
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX IDX_6940A7FE2626CEF9 ON parts (id_part_unit)
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX IDX_6940A7FE1ECB93AE ON parts (id_manufacturer)
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX IDX_6940A7FEA3ED1215 ON parts (id_part_custom_state)
SQL);
}
public function sqLiteDown(Schema $schema): void
{
$this->addSql(<<<'SQL'
CREATE TEMPORARY TABLE __temp__parts AS
SELECT
id,
id_preview_attachment,
id_category,
id_footprint,
id_part_unit,
id_manufacturer,
order_orderdetails_id,
built_project_id,
datetime_added,
name,
last_modified,
needs_review,
tags,
mass,
description,
comment,
visible,
favorite,
minamount,
manufacturer_product_url,
manufacturer_product_number,
manufacturing_status,
order_quantity,
manual_order,
ipn,
provider_reference_provider_key,
provider_reference_provider_id,
provider_reference_provider_url,
provider_reference_last_updated,
eda_info_reference_prefix,
eda_info_value,
eda_info_invisible,
eda_info_exclude_from_bom,
eda_info_exclude_from_board,
eda_info_exclude_from_sim,
eda_info_kicad_symbol,
eda_info_kicad_footprint
FROM "parts"
SQL);
$this->addSql(<<<'SQL'
DROP TABLE "parts"
SQL);
$this->addSql(<<<'SQL'
CREATE TABLE "parts" (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
id_preview_attachment INTEGER DEFAULT NULL,
id_category INTEGER NOT NULL,
id_footprint INTEGER DEFAULT NULL,
id_part_unit INTEGER DEFAULT NULL,
id_manufacturer INTEGER DEFAULT NULL,
order_orderdetails_id INTEGER DEFAULT NULL,
built_project_id INTEGER DEFAULT NULL,
datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
name VARCHAR(255) NOT NULL,
last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
needs_review BOOLEAN NOT NULL,
tags CLOB NOT NULL,
mass DOUBLE PRECISION DEFAULT NULL,
description CLOB NOT NULL,
comment CLOB NOT NULL,
visible BOOLEAN NOT NULL,
favorite BOOLEAN NOT NULL,
minamount DOUBLE PRECISION NOT NULL,
manufacturer_product_url CLOB NOT NULL,
manufacturer_product_number VARCHAR(255) NOT NULL,
manufacturing_status VARCHAR(255) DEFAULT NULL,
order_quantity INTEGER NOT NULL,
manual_order BOOLEAN NOT NULL,
ipn VARCHAR(100) DEFAULT NULL,
provider_reference_provider_key VARCHAR(255) DEFAULT NULL,
provider_reference_provider_id VARCHAR(255) DEFAULT NULL,
provider_reference_provider_url VARCHAR(255) DEFAULT NULL,
provider_reference_last_updated DATETIME DEFAULT NULL,
eda_info_reference_prefix VARCHAR(255) DEFAULT NULL,
eda_info_value VARCHAR(255) DEFAULT NULL,
eda_info_invisible BOOLEAN DEFAULT NULL,
eda_info_exclude_from_bom BOOLEAN DEFAULT NULL,
eda_info_exclude_from_board BOOLEAN DEFAULT NULL,
eda_info_exclude_from_sim BOOLEAN DEFAULT NULL,
eda_info_kicad_symbol VARCHAR(255) DEFAULT NULL,
eda_info_kicad_footprint VARCHAR(255) DEFAULT NULL,
CONSTRAINT FK_6940A7FEEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE,
CONSTRAINT FK_6940A7FE5697F554 FOREIGN KEY (id_category) REFERENCES "categories" (id) NOT DEFERRABLE INITIALLY IMMEDIATE,
CONSTRAINT FK_6940A7FE7E371A10 FOREIGN KEY (id_footprint) REFERENCES "footprints" (id) NOT DEFERRABLE INITIALLY IMMEDIATE,
CONSTRAINT FK_6940A7FE2626CEF9 FOREIGN KEY (id_part_unit) REFERENCES "measurement_units" (id) NOT DEFERRABLE INITIALLY IMMEDIATE,
CONSTRAINT FK_6940A7FE1ECB93AE FOREIGN KEY (id_manufacturer) REFERENCES "manufacturers" (id) NOT DEFERRABLE INITIALLY IMMEDIATE,
CONSTRAINT FK_6940A7FE81081E9B FOREIGN KEY (order_orderdetails_id) REFERENCES "orderdetails" (id) NOT DEFERRABLE INITIALLY IMMEDIATE,
CONSTRAINT FK_6940A7FEE8AE70D9 FOREIGN KEY (built_project_id) REFERENCES projects (id) NOT DEFERRABLE INITIALLY IMMEDIATE
)
SQL);
$this->addSql(<<<'SQL'
INSERT INTO "parts" (
id,
id_preview_attachment,
id_category,
id_footprint,
id_part_unit,
id_manufacturer,
order_orderdetails_id,
built_project_id,
datetime_added,
name,
last_modified,
needs_review,
tags,
mass,
description,
comment,
visible,
favorite,
minamount,
manufacturer_product_url,
manufacturer_product_number,
manufacturing_status,
order_quantity,
manual_order,
ipn,
provider_reference_provider_key,
provider_reference_provider_id,
provider_reference_provider_url,
provider_reference_last_updated,
eda_info_reference_prefix,
eda_info_value,
eda_info_invisible,
eda_info_exclude_from_bom,
eda_info_exclude_from_board,
eda_info_exclude_from_sim,
eda_info_kicad_symbol,
eda_info_kicad_footprint
) SELECT
id,
id_preview_attachment,
id_category,
id_footprint,
id_part_unit,
id_manufacturer,
order_orderdetails_id,
built_project_id,
datetime_added,
name,
last_modified,
needs_review,
tags,
mass,
description,
comment,
visible,
favorite,
minamount,
manufacturer_product_url,
manufacturer_product_number,
manufacturing_status,
order_quantity,
manual_order,
ipn,
provider_reference_provider_key,
provider_reference_provider_id,
provider_reference_provider_url,
provider_reference_last_updated,
eda_info_reference_prefix,
eda_info_value,
eda_info_invisible,
eda_info_exclude_from_bom,
eda_info_exclude_from_board,
eda_info_exclude_from_sim,
eda_info_kicad_symbol,
eda_info_kicad_footprint
FROM __temp__parts
SQL);
$this->addSql(<<<'SQL'
DROP TABLE __temp__parts
SQL);
$this->addSql(<<<'SQL'
CREATE UNIQUE INDEX UNIQ_6940A7FE3D721C14 ON "parts" (ipn)
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX IDX_6940A7FEEA7100A1 ON "parts" (id_preview_attachment)
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX IDX_6940A7FE5697F554 ON "parts" (id_category)
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX IDX_6940A7FE7E371A10 ON "parts" (id_footprint)
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX IDX_6940A7FE2626CEF9 ON "parts" (id_part_unit)
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX IDX_6940A7FE1ECB93AE ON "parts" (id_manufacturer)
SQL);
$this->addSql(<<<'SQL'
CREATE UNIQUE INDEX UNIQ_6940A7FE81081E9B ON "parts" (order_orderdetails_id)
SQL);
$this->addSql(<<<'SQL'
CREATE UNIQUE INDEX UNIQ_6940A7FEE8AE70D9 ON "parts" (built_project_id)
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX parts_idx_datet_name_last_id_needs ON "parts" (datetime_added, name, last_modified, id, needs_review)
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX parts_idx_name ON "parts" (name)
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX parts_idx_ipn ON "parts" (ipn)
SQL);
$this->addSql(<<<'SQL'
DROP TABLE "part_custom_states"
SQL);
}
public function postgreSQLUp(Schema $schema): void
{
$this->addSql(<<<'SQL'
CREATE TABLE "part_custom_states" (
id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
parent_id INT DEFAULT NULL,
id_preview_attachment INT DEFAULT NULL, PRIMARY KEY(id),
name VARCHAR(255) NOT NULL,
comment TEXT NOT NULL,
not_selectable BOOLEAN NOT NULL,
alternative_names TEXT DEFAULT NULL,
last_modified TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
datetime_added TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL
)
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX IDX_F552745D727ACA70 ON "part_custom_states" (parent_id)
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX IDX_F552745DEA7100A1 ON "part_custom_states" (id_preview_attachment)
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE "part_custom_states"
ADD CONSTRAINT FK_F552745D727ACA70
FOREIGN KEY (parent_id) REFERENCES "part_custom_states" (id) NOT DEFERRABLE INITIALLY IMMEDIATE
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE "part_custom_states"
ADD CONSTRAINT FK_F552745DEA7100A1
FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE parts ADD id_part_custom_state INT DEFAULT NULL
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE parts ADD CONSTRAINT FK_6940A7FEA3ED1215 FOREIGN KEY (id_part_custom_state) REFERENCES "part_custom_states" (id) NOT DEFERRABLE INITIALLY IMMEDIATE
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX IDX_6940A7FEA3ED1215 ON parts (id_part_custom_state)
SQL);
}
public function postgreSQLDown(Schema $schema): void
{
$this->addSql(<<<'SQL'
ALTER TABLE "parts" DROP CONSTRAINT FK_6940A7FEA3ED1215
SQL);
$this->addSql(<<<'SQL'
DROP INDEX IDX_6940A7FEA3ED1215
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE "parts" DROP id_part_custom_state
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE "part_custom_states" DROP CONSTRAINT FK_F552745D727ACA70
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE "part_custom_states" DROP CONSTRAINT FK_F552745DEA7100A1
SQL);
$this->addSql(<<<'SQL'
DROP TABLE "part_custom_states"
SQL);
}
}

View file

@ -0,0 +1,307 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use App\Migration\AbstractMultiPlatformMigration;
use Doctrine\DBAL\Schema\Schema;
final class Version20250325073036 extends AbstractMultiPlatformMigration
{
public function getDescription(): string
{
return 'Add part_ipn_prefix column to categories table and remove unique constraint from parts table';
}
public function mySQLUp(Schema $schema): void
{
$this->addSql(<<<'SQL'
ALTER TABLE categories ADD COLUMN part_ipn_prefix VARCHAR(255) NOT NULL DEFAULT ''
SQL);
}
public function mySQLDown(Schema $schema): void
{
$this->addSql(<<<'SQL'
ALTER TABLE categories DROP part_ipn_prefix
SQL);
}
public function sqLiteUp(Schema $schema): void
{
$this->addSql(<<<'SQL'
CREATE TEMPORARY TABLE __temp__categories AS
SELECT
id,
parent_id,
id_preview_attachment,
partname_hint,
partname_regex,
disable_footprints,
disable_manufacturers,
disable_autodatasheets,
disable_properties,
default_description,
default_comment,
comment,
not_selectable,
name,
last_modified,
datetime_added,
alternative_names,
eda_info_reference_prefix,
eda_info_invisible,
eda_info_exclude_from_bom,
eda_info_exclude_from_board,
eda_info_exclude_from_sim,
eda_info_kicad_symbol
FROM categories
SQL);
$this->addSql('DROP TABLE categories');
$this->addSql(<<<'SQL'
CREATE TABLE categories (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
parent_id INTEGER DEFAULT NULL,
id_preview_attachment INTEGER DEFAULT NULL,
partname_hint CLOB NOT NULL,
partname_regex CLOB NOT NULL,
part_ipn_prefix VARCHAR(255) DEFAULT '' NOT NULL,
disable_footprints BOOLEAN NOT NULL,
disable_manufacturers BOOLEAN NOT NULL,
disable_autodatasheets BOOLEAN NOT NULL,
disable_properties BOOLEAN NOT NULL,
default_description CLOB NOT NULL,
default_comment CLOB NOT NULL,
comment CLOB NOT NULL,
not_selectable BOOLEAN NOT NULL,
name VARCHAR(255) NOT NULL,
last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
alternative_names CLOB DEFAULT NULL,
eda_info_reference_prefix VARCHAR(255) DEFAULT NULL,
eda_info_invisible BOOLEAN DEFAULT NULL,
eda_info_exclude_from_bom BOOLEAN DEFAULT NULL,
eda_info_exclude_from_board BOOLEAN DEFAULT NULL,
eda_info_exclude_from_sim BOOLEAN DEFAULT NULL,
eda_info_kicad_symbol VARCHAR(255) DEFAULT NULL,
CONSTRAINT FK_3AF34668727ACA70 FOREIGN KEY (parent_id) REFERENCES categories (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE,
CONSTRAINT FK_3AF34668EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES attachments (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE
)
SQL);
$this->addSql(<<<'SQL'
INSERT INTO categories (
id,
parent_id,
id_preview_attachment,
partname_hint,
partname_regex,
disable_footprints,
disable_manufacturers,
disable_autodatasheets,
disable_properties,
default_description,
default_comment,
comment,
not_selectable,
name,
last_modified,
datetime_added,
alternative_names,
eda_info_reference_prefix,
eda_info_invisible,
eda_info_exclude_from_bom,
eda_info_exclude_from_board,
eda_info_exclude_from_sim,
eda_info_kicad_symbol
) SELECT
id,
parent_id,
id_preview_attachment,
partname_hint,
partname_regex,
disable_footprints,
disable_manufacturers,
disable_autodatasheets,
disable_properties,
default_description,
default_comment,
comment,
not_selectable,
name,
last_modified,
datetime_added,
alternative_names,
eda_info_reference_prefix,
eda_info_invisible,
eda_info_exclude_from_bom,
eda_info_exclude_from_board,
eda_info_exclude_from_sim,
eda_info_kicad_symbol
FROM __temp__categories
SQL);
$this->addSql('DROP TABLE __temp__categories');
$this->addSql(<<<'SQL'
CREATE INDEX IDX_3AF34668727ACA70 ON categories (parent_id)
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX IDX_3AF34668EA7100A1 ON categories (id_preview_attachment)
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX category_idx_name ON categories (name)
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX category_idx_parent_name ON categories (parent_id, name)
SQL);
}
public function sqLiteDown(Schema $schema): void
{
$this->addSql(<<<'SQL'
CREATE TEMPORARY TABLE __temp__categories AS
SELECT
id,
parent_id,
id_preview_attachment,
partname_hint,
partname_regex,
disable_footprints,
disable_manufacturers,
disable_autodatasheets,
disable_properties,
default_description,
default_comment,
comment,
not_selectable,
name,
last_modified,
datetime_added,
alternative_names,
eda_info_reference_prefix,
eda_info_invisible,
eda_info_exclude_from_bom,
eda_info_exclude_from_board,
eda_info_exclude_from_sim,
eda_info_kicad_symbol
FROM categories
SQL);
$this->addSql('DROP TABLE categories');
$this->addSql(<<<'SQL'
CREATE TABLE categories (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
parent_id INTEGER DEFAULT NULL,
id_preview_attachment INTEGER DEFAULT NULL,
partname_hint CLOB NOT NULL,
partname_regex CLOB NOT NULL,
disable_footprints BOOLEAN NOT NULL,
disable_manufacturers BOOLEAN NOT NULL,
disable_autodatasheets BOOLEAN NOT NULL,
disable_properties BOOLEAN NOT NULL,
default_description CLOB NOT NULL,
default_comment CLOB NOT NULL,
comment CLOB NOT NULL,
not_selectable BOOLEAN NOT NULL,
name VARCHAR(255) NOT NULL,
last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
alternative_names CLOB DEFAULT NULL,
eda_info_reference_prefix VARCHAR(255) DEFAULT NULL,
eda_info_invisible BOOLEAN DEFAULT NULL,
eda_info_exclude_from_bom BOOLEAN DEFAULT NULL,
eda_info_exclude_from_board BOOLEAN DEFAULT NULL,
eda_info_exclude_from_sim BOOLEAN DEFAULT NULL,
eda_info_kicad_symbol VARCHAR(255) DEFAULT NULL,
CONSTRAINT FK_3AF34668727ACA70 FOREIGN KEY (parent_id) REFERENCES categories (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE,
CONSTRAINT FK_3AF34668EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES attachments (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE
)
SQL);
$this->addSql(<<<'SQL'
INSERT INTO categories (
id,
parent_id,
id_preview_attachment,
partname_hint,
partname_regex,
disable_footprints,
disable_manufacturers,
disable_autodatasheets,
disable_properties,
default_description,
default_comment,
comment,
not_selectable,
name,
last_modified,
datetime_added,
alternative_names,
eda_info_reference_prefix,
eda_info_invisible,
eda_info_exclude_from_bom,
eda_info_exclude_from_board,
eda_info_exclude_from_sim,
eda_info_kicad_symbol
) SELECT
id,
parent_id,
id_preview_attachment,
partname_hint,
partname_regex,
disable_footprints,
disable_manufacturers,
disable_autodatasheets,
disable_properties,
default_description,
default_comment,
comment,
not_selectable,
name,
last_modified,
datetime_added,
alternative_names,
eda_info_reference_prefix,
eda_info_invisible,
eda_info_exclude_from_bom,
eda_info_exclude_from_board,
eda_info_exclude_from_sim,
eda_info_kicad_symbol
FROM __temp__categories
SQL);
$this->addSql('DROP TABLE __temp__categories');
$this->addSql(<<<'SQL'
CREATE INDEX IDX_3AF34668727ACA70 ON categories (parent_id)
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX IDX_3AF34668EA7100A1 ON categories (id_preview_attachment)
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX category_idx_name ON categories (name)
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX category_idx_parent_name ON categories (parent_id, name)
SQL);
}
public function postgreSQLUp(Schema $schema): void
{
$this->addSql(<<<'SQL'
ALTER TABLE categories ADD part_ipn_prefix VARCHAR(255) DEFAULT '' NOT NULL
SQL);
}
public function postgreSQLDown(Schema $schema): void
{
$this->addSql(<<<'SQL'
ALTER TABLE "categories" DROP part_ipn_prefix
SQL);
}
}

View file

@ -0,0 +1,156 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use App\Migration\AbstractMultiPlatformMigration;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20251204215443 extends AbstractMultiPlatformMigration
{
public function getDescription(): string
{
return 'Increase URL field lengths to 2048 characters';
}
public function mySQLUp(Schema $schema): void
{
$this->addSql('ALTER TABLE attachments CHANGE external_path external_path VARCHAR(2048) DEFAULT NULL');
$this->addSql('ALTER TABLE manufacturers CHANGE website website VARCHAR(2048) NOT NULL, CHANGE auto_product_url auto_product_url VARCHAR(2048) NOT NULL');
$this->addSql('ALTER TABLE parts CHANGE provider_reference_provider_url provider_reference_provider_url VARCHAR(2048) DEFAULT NULL');
$this->addSql('ALTER TABLE suppliers CHANGE website website VARCHAR(2048) NOT NULL, CHANGE auto_product_url auto_product_url VARCHAR(2048) NOT NULL');
}
public function mySQLDown(Schema $schema): void
{
$this->addSql('ALTER TABLE `attachments` CHANGE external_path external_path VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE `manufacturers` CHANGE website website VARCHAR(255) NOT NULL, CHANGE auto_product_url auto_product_url VARCHAR(255) NOT NULL');
$this->addSql('ALTER TABLE `parts` CHANGE provider_reference_provider_url provider_reference_provider_url VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE `suppliers` CHANGE website website VARCHAR(255) NOT NULL, CHANGE auto_product_url auto_product_url VARCHAR(255) NOT NULL');
}
public function sqLiteUp(Schema $schema): void
{
$this->addSql('CREATE TEMPORARY TABLE __temp__attachments AS SELECT id, type_id, original_filename, show_in_table, name, last_modified, datetime_added, class_name, element_id, internal_path, external_path FROM attachments');
$this->addSql('DROP TABLE attachments');
$this->addSql('CREATE TABLE attachments (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, type_id INTEGER NOT NULL, original_filename VARCHAR(255) DEFAULT NULL, show_in_table BOOLEAN NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, class_name VARCHAR(255) NOT NULL, element_id INTEGER NOT NULL, internal_path VARCHAR(255) DEFAULT NULL, external_path VARCHAR(2048) DEFAULT NULL, CONSTRAINT FK_47C4FAD6C54C8C93 FOREIGN KEY (type_id) REFERENCES attachment_types (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('INSERT INTO attachments (id, type_id, original_filename, show_in_table, name, last_modified, datetime_added, class_name, element_id, internal_path, external_path) SELECT id, type_id, original_filename, show_in_table, name, last_modified, datetime_added, class_name, element_id, internal_path, external_path FROM __temp__attachments');
$this->addSql('DROP TABLE __temp__attachments');
$this->addSql('CREATE INDEX attachment_element_idx ON attachments (class_name, element_id)');
$this->addSql('CREATE INDEX attachment_name_idx ON attachments (name)');
$this->addSql('CREATE INDEX attachments_idx_class_name_id ON attachments (class_name, id)');
$this->addSql('CREATE INDEX attachments_idx_id_element_id_class_name ON attachments (id, element_id, class_name)');
$this->addSql('CREATE INDEX IDX_47C4FAD6C54C8C93 ON attachments (type_id)');
$this->addSql('CREATE INDEX IDX_47C4FAD61F1F2A24 ON attachments (element_id)');
$this->addSql('CREATE TEMPORARY TABLE __temp__manufacturers AS SELECT id, parent_id, id_preview_attachment, address, phone_number, fax_number, email_address, website, auto_product_url, comment, not_selectable, name, last_modified, datetime_added, alternative_names FROM manufacturers');
$this->addSql('DROP TABLE manufacturers');
$this->addSql('CREATE TABLE manufacturers (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, parent_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, address VARCHAR(255) NOT NULL, phone_number VARCHAR(255) NOT NULL, fax_number VARCHAR(255) NOT NULL, email_address VARCHAR(255) NOT NULL, website VARCHAR(2048) NOT NULL, auto_product_url VARCHAR(2048) NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, alternative_names CLOB DEFAULT NULL, CONSTRAINT FK_94565B12727ACA70 FOREIGN KEY (parent_id) REFERENCES manufacturers (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_94565B12EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES attachments (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('INSERT INTO manufacturers (id, parent_id, id_preview_attachment, address, phone_number, fax_number, email_address, website, auto_product_url, comment, not_selectable, name, last_modified, datetime_added, alternative_names) SELECT id, parent_id, id_preview_attachment, address, phone_number, fax_number, email_address, website, auto_product_url, comment, not_selectable, name, last_modified, datetime_added, alternative_names FROM __temp__manufacturers');
$this->addSql('DROP TABLE __temp__manufacturers');
$this->addSql('CREATE INDEX IDX_94565B12EA7100A1 ON manufacturers (id_preview_attachment)');
$this->addSql('CREATE INDEX IDX_94565B12727ACA70 ON manufacturers (parent_id)');
$this->addSql('CREATE INDEX manufacturer_name ON manufacturers (name)');
$this->addSql('CREATE INDEX manufacturer_idx_parent_name ON manufacturers (parent_id, name)');
$this->addSql('CREATE TEMPORARY TABLE __temp__parts AS SELECT id, id_preview_attachment, id_category, id_footprint, id_part_unit, id_manufacturer, id_part_custom_state, order_orderdetails_id, built_project_id, datetime_added, name, last_modified, needs_review, tags, mass, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order, ipn, provider_reference_provider_key, provider_reference_provider_id, provider_reference_provider_url, provider_reference_last_updated, eda_info_reference_prefix, eda_info_value, eda_info_invisible, eda_info_exclude_from_bom, eda_info_exclude_from_board, eda_info_exclude_from_sim, eda_info_kicad_symbol, eda_info_kicad_footprint FROM parts');
$this->addSql('DROP TABLE parts');
$this->addSql('CREATE TABLE parts (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id_preview_attachment INTEGER DEFAULT NULL, id_category INTEGER NOT NULL, id_footprint INTEGER DEFAULT NULL, id_part_unit INTEGER DEFAULT NULL, id_manufacturer INTEGER DEFAULT NULL, id_part_custom_state INTEGER DEFAULT NULL, order_orderdetails_id INTEGER DEFAULT NULL, built_project_id INTEGER DEFAULT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, needs_review BOOLEAN NOT NULL, tags CLOB NOT NULL, mass DOUBLE PRECISION DEFAULT NULL, description CLOB NOT NULL, comment CLOB NOT NULL, visible BOOLEAN NOT NULL, favorite BOOLEAN NOT NULL, minamount DOUBLE PRECISION NOT NULL, manufacturer_product_url CLOB NOT NULL, manufacturer_product_number VARCHAR(255) NOT NULL, manufacturing_status VARCHAR(255) DEFAULT NULL, order_quantity INTEGER NOT NULL, manual_order BOOLEAN NOT NULL, ipn VARCHAR(100) DEFAULT NULL, provider_reference_provider_key VARCHAR(255) DEFAULT NULL, provider_reference_provider_id VARCHAR(255) DEFAULT NULL, provider_reference_provider_url VARCHAR(2048) DEFAULT NULL, provider_reference_last_updated DATETIME DEFAULT NULL, eda_info_reference_prefix VARCHAR(255) DEFAULT NULL, eda_info_value VARCHAR(255) DEFAULT NULL, eda_info_invisible BOOLEAN DEFAULT NULL, eda_info_exclude_from_bom BOOLEAN DEFAULT NULL, eda_info_exclude_from_board BOOLEAN DEFAULT NULL, eda_info_exclude_from_sim BOOLEAN DEFAULT NULL, eda_info_kicad_symbol VARCHAR(255) DEFAULT NULL, eda_info_kicad_footprint VARCHAR(255) DEFAULT NULL, CONSTRAINT FK_6940A7FEEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES attachments (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE5697F554 FOREIGN KEY (id_category) REFERENCES categories (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE7E371A10 FOREIGN KEY (id_footprint) REFERENCES footprints (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE2626CEF9 FOREIGN KEY (id_part_unit) REFERENCES measurement_units (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE1ECB93AE FOREIGN KEY (id_manufacturer) REFERENCES manufacturers (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FEA3ED1215 FOREIGN KEY (id_part_custom_state) REFERENCES part_custom_states (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE81081E9B FOREIGN KEY (order_orderdetails_id) REFERENCES orderdetails (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FEE8AE70D9 FOREIGN KEY (built_project_id) REFERENCES projects (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('INSERT INTO parts (id, id_preview_attachment, id_category, id_footprint, id_part_unit, id_manufacturer, id_part_custom_state, order_orderdetails_id, built_project_id, datetime_added, name, last_modified, needs_review, tags, mass, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order, ipn, provider_reference_provider_key, provider_reference_provider_id, provider_reference_provider_url, provider_reference_last_updated, eda_info_reference_prefix, eda_info_value, eda_info_invisible, eda_info_exclude_from_bom, eda_info_exclude_from_board, eda_info_exclude_from_sim, eda_info_kicad_symbol, eda_info_kicad_footprint) SELECT id, id_preview_attachment, id_category, id_footprint, id_part_unit, id_manufacturer, id_part_custom_state, order_orderdetails_id, built_project_id, datetime_added, name, last_modified, needs_review, tags, mass, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order, ipn, provider_reference_provider_key, provider_reference_provider_id, provider_reference_provider_url, provider_reference_last_updated, eda_info_reference_prefix, eda_info_value, eda_info_invisible, eda_info_exclude_from_bom, eda_info_exclude_from_board, eda_info_exclude_from_sim, eda_info_kicad_symbol, eda_info_kicad_footprint FROM __temp__parts');
$this->addSql('DROP TABLE __temp__parts');
$this->addSql('CREATE INDEX IDX_6940A7FEA3ED1215 ON parts (id_part_custom_state)');
$this->addSql('CREATE INDEX IDX_6940A7FE1ECB93AE ON parts (id_manufacturer)');
$this->addSql('CREATE INDEX IDX_6940A7FE2626CEF9 ON parts (id_part_unit)');
$this->addSql('CREATE INDEX IDX_6940A7FE5697F554 ON parts (id_category)');
$this->addSql('CREATE INDEX IDX_6940A7FE7E371A10 ON parts (id_footprint)');
$this->addSql('CREATE INDEX IDX_6940A7FEEA7100A1 ON parts (id_preview_attachment)');
$this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FE3D721C14 ON parts (ipn)');
$this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FE81081E9B ON parts (order_orderdetails_id)');
$this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FEE8AE70D9 ON parts (built_project_id)');
$this->addSql('CREATE INDEX parts_idx_datet_name_last_id_needs ON parts (datetime_added, name, last_modified, id, needs_review)');
$this->addSql('CREATE INDEX parts_idx_ipn ON parts (ipn)');
$this->addSql('CREATE INDEX parts_idx_name ON parts (name)');
$this->addSql('CREATE TEMPORARY TABLE __temp__suppliers AS SELECT id, parent_id, default_currency_id, id_preview_attachment, shipping_costs, address, phone_number, fax_number, email_address, website, auto_product_url, comment, not_selectable, name, last_modified, datetime_added, alternative_names FROM suppliers');
$this->addSql('DROP TABLE suppliers');
$this->addSql('CREATE TABLE suppliers (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, parent_id INTEGER DEFAULT NULL, default_currency_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, shipping_costs NUMERIC(11, 5) DEFAULT NULL, address VARCHAR(255) NOT NULL, phone_number VARCHAR(255) NOT NULL, fax_number VARCHAR(255) NOT NULL, email_address VARCHAR(255) NOT NULL, website VARCHAR(2048) NOT NULL, auto_product_url VARCHAR(2048) NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, alternative_names CLOB DEFAULT NULL, CONSTRAINT FK_AC28B95C727ACA70 FOREIGN KEY (parent_id) REFERENCES suppliers (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_AC28B95CECD792C0 FOREIGN KEY (default_currency_id) REFERENCES currencies (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_AC28B95CEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES attachments (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('INSERT INTO suppliers (id, parent_id, default_currency_id, id_preview_attachment, shipping_costs, address, phone_number, fax_number, email_address, website, auto_product_url, comment, not_selectable, name, last_modified, datetime_added, alternative_names) SELECT id, parent_id, default_currency_id, id_preview_attachment, shipping_costs, address, phone_number, fax_number, email_address, website, auto_product_url, comment, not_selectable, name, last_modified, datetime_added, alternative_names FROM __temp__suppliers');
$this->addSql('DROP TABLE __temp__suppliers');
$this->addSql('CREATE INDEX IDX_AC28B95CECD792C0 ON suppliers (default_currency_id)');
$this->addSql('CREATE INDEX IDX_AC28B95C727ACA70 ON suppliers (parent_id)');
$this->addSql('CREATE INDEX supplier_idx_name ON suppliers (name)');
$this->addSql('CREATE INDEX supplier_idx_parent_name ON suppliers (parent_id, name)');
$this->addSql('CREATE INDEX IDX_AC28B95CEA7100A1 ON suppliers (id_preview_attachment)');
}
public function sqLiteDown(Schema $schema): void
{
$this->addSql('CREATE TEMPORARY TABLE __temp__attachments AS SELECT id, name, last_modified, datetime_added, original_filename, internal_path, external_path, show_in_table, type_id, class_name, element_id FROM "attachments"');
$this->addSql('DROP TABLE "attachments"');
$this->addSql('CREATE TABLE "attachments" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, original_filename VARCHAR(255) DEFAULT NULL, internal_path VARCHAR(255) DEFAULT NULL, external_path VARCHAR(255) DEFAULT NULL, show_in_table BOOLEAN NOT NULL, type_id INTEGER NOT NULL, class_name VARCHAR(255) NOT NULL, element_id INTEGER NOT NULL, CONSTRAINT FK_47C4FAD6C54C8C93 FOREIGN KEY (type_id) REFERENCES "attachment_types" (id) NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('INSERT INTO "attachments" (id, name, last_modified, datetime_added, original_filename, internal_path, external_path, show_in_table, type_id, class_name, element_id) SELECT id, name, last_modified, datetime_added, original_filename, internal_path, external_path, show_in_table, type_id, class_name, element_id FROM __temp__attachments');
$this->addSql('DROP TABLE __temp__attachments');
$this->addSql('CREATE INDEX IDX_47C4FAD6C54C8C93 ON "attachments" (type_id)');
$this->addSql('CREATE INDEX IDX_47C4FAD61F1F2A24 ON "attachments" (element_id)');
$this->addSql('CREATE INDEX attachments_idx_id_element_id_class_name ON "attachments" (id, element_id, class_name)');
$this->addSql('CREATE INDEX attachments_idx_class_name_id ON "attachments" (class_name, id)');
$this->addSql('CREATE INDEX attachment_name_idx ON "attachments" (name)');
$this->addSql('CREATE INDEX attachment_element_idx ON "attachments" (class_name, element_id)');
$this->addSql('CREATE TEMPORARY TABLE __temp__manufacturers AS SELECT id, name, last_modified, datetime_added, comment, not_selectable, alternative_names, address, phone_number, fax_number, email_address, website, auto_product_url, parent_id, id_preview_attachment FROM "manufacturers"');
$this->addSql('DROP TABLE "manufacturers"');
$this->addSql('CREATE TABLE "manufacturers" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, alternative_names CLOB DEFAULT NULL, address VARCHAR(255) NOT NULL, phone_number VARCHAR(255) NOT NULL, fax_number VARCHAR(255) NOT NULL, email_address VARCHAR(255) NOT NULL, website VARCHAR(255) NOT NULL, auto_product_url VARCHAR(255) NOT NULL, parent_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, CONSTRAINT FK_94565B12727ACA70 FOREIGN KEY (parent_id) REFERENCES "manufacturers" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_94565B12EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('INSERT INTO "manufacturers" (id, name, last_modified, datetime_added, comment, not_selectable, alternative_names, address, phone_number, fax_number, email_address, website, auto_product_url, parent_id, id_preview_attachment) SELECT id, name, last_modified, datetime_added, comment, not_selectable, alternative_names, address, phone_number, fax_number, email_address, website, auto_product_url, parent_id, id_preview_attachment FROM __temp__manufacturers');
$this->addSql('DROP TABLE __temp__manufacturers');
$this->addSql('CREATE INDEX IDX_94565B12727ACA70 ON "manufacturers" (parent_id)');
$this->addSql('CREATE INDEX IDX_94565B12EA7100A1 ON "manufacturers" (id_preview_attachment)');
$this->addSql('CREATE INDEX manufacturer_name ON "manufacturers" (name)');
$this->addSql('CREATE INDEX manufacturer_idx_parent_name ON "manufacturers" (parent_id, name)');
$this->addSql('CREATE TEMPORARY TABLE __temp__parts AS SELECT id, name, last_modified, datetime_added, needs_review, tags, mass, ipn, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order, provider_reference_provider_key, provider_reference_provider_id, provider_reference_provider_url, provider_reference_last_updated, eda_info_reference_prefix, eda_info_value, eda_info_invisible, eda_info_exclude_from_bom, eda_info_exclude_from_board, eda_info_exclude_from_sim, eda_info_kicad_symbol, eda_info_kicad_footprint, id_preview_attachment, id_part_custom_state, id_category, id_footprint, id_part_unit, id_manufacturer, order_orderdetails_id, built_project_id FROM "parts"');
$this->addSql('DROP TABLE "parts"');
$this->addSql('CREATE TABLE "parts" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, needs_review BOOLEAN NOT NULL, tags CLOB NOT NULL, mass DOUBLE PRECISION DEFAULT NULL, ipn VARCHAR(100) DEFAULT NULL, description CLOB NOT NULL, comment CLOB NOT NULL, visible BOOLEAN NOT NULL, favorite BOOLEAN NOT NULL, minamount DOUBLE PRECISION NOT NULL, manufacturer_product_url CLOB NOT NULL, manufacturer_product_number VARCHAR(255) NOT NULL, manufacturing_status VARCHAR(255) DEFAULT NULL, order_quantity INTEGER NOT NULL, manual_order BOOLEAN NOT NULL, provider_reference_provider_key VARCHAR(255) DEFAULT NULL, provider_reference_provider_id VARCHAR(255) DEFAULT NULL, provider_reference_provider_url VARCHAR(255) DEFAULT NULL, provider_reference_last_updated DATETIME DEFAULT NULL, eda_info_reference_prefix VARCHAR(255) DEFAULT NULL, eda_info_value VARCHAR(255) DEFAULT NULL, eda_info_invisible BOOLEAN DEFAULT NULL, eda_info_exclude_from_bom BOOLEAN DEFAULT NULL, eda_info_exclude_from_board BOOLEAN DEFAULT NULL, eda_info_exclude_from_sim BOOLEAN DEFAULT NULL, eda_info_kicad_symbol VARCHAR(255) DEFAULT NULL, eda_info_kicad_footprint VARCHAR(255) DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, id_part_custom_state INTEGER DEFAULT NULL, id_category INTEGER NOT NULL, id_footprint INTEGER DEFAULT NULL, id_part_unit INTEGER DEFAULT NULL, id_manufacturer INTEGER DEFAULT NULL, order_orderdetails_id INTEGER DEFAULT NULL, built_project_id INTEGER DEFAULT NULL, CONSTRAINT FK_6940A7FEEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FEA3ED1215 FOREIGN KEY (id_part_custom_state) REFERENCES "part_custom_states" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE5697F554 FOREIGN KEY (id_category) REFERENCES "categories" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE7E371A10 FOREIGN KEY (id_footprint) REFERENCES "footprints" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE2626CEF9 FOREIGN KEY (id_part_unit) REFERENCES "measurement_units" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE1ECB93AE FOREIGN KEY (id_manufacturer) REFERENCES "manufacturers" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE81081E9B FOREIGN KEY (order_orderdetails_id) REFERENCES "orderdetails" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FEE8AE70D9 FOREIGN KEY (built_project_id) REFERENCES projects (id) NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('INSERT INTO "parts" (id, name, last_modified, datetime_added, needs_review, tags, mass, ipn, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order, provider_reference_provider_key, provider_reference_provider_id, provider_reference_provider_url, provider_reference_last_updated, eda_info_reference_prefix, eda_info_value, eda_info_invisible, eda_info_exclude_from_bom, eda_info_exclude_from_board, eda_info_exclude_from_sim, eda_info_kicad_symbol, eda_info_kicad_footprint, id_preview_attachment, id_part_custom_state, id_category, id_footprint, id_part_unit, id_manufacturer, order_orderdetails_id, built_project_id) SELECT id, name, last_modified, datetime_added, needs_review, tags, mass, ipn, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order, provider_reference_provider_key, provider_reference_provider_id, provider_reference_provider_url, provider_reference_last_updated, eda_info_reference_prefix, eda_info_value, eda_info_invisible, eda_info_exclude_from_bom, eda_info_exclude_from_board, eda_info_exclude_from_sim, eda_info_kicad_symbol, eda_info_kicad_footprint, id_preview_attachment, id_part_custom_state, id_category, id_footprint, id_part_unit, id_manufacturer, order_orderdetails_id, built_project_id FROM __temp__parts');
$this->addSql('DROP TABLE __temp__parts');
$this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FE3D721C14 ON "parts" (ipn)');
$this->addSql('CREATE INDEX IDX_6940A7FEEA7100A1 ON "parts" (id_preview_attachment)');
$this->addSql('CREATE INDEX IDX_6940A7FEA3ED1215 ON "parts" (id_part_custom_state)');
$this->addSql('CREATE INDEX IDX_6940A7FE5697F554 ON "parts" (id_category)');
$this->addSql('CREATE INDEX IDX_6940A7FE7E371A10 ON "parts" (id_footprint)');
$this->addSql('CREATE INDEX IDX_6940A7FE2626CEF9 ON "parts" (id_part_unit)');
$this->addSql('CREATE INDEX IDX_6940A7FE1ECB93AE ON "parts" (id_manufacturer)');
$this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FE81081E9B ON "parts" (order_orderdetails_id)');
$this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FEE8AE70D9 ON "parts" (built_project_id)');
$this->addSql('CREATE INDEX parts_idx_datet_name_last_id_needs ON "parts" (datetime_added, name, last_modified, id, needs_review)');
$this->addSql('CREATE INDEX parts_idx_name ON "parts" (name)');
$this->addSql('CREATE INDEX parts_idx_ipn ON "parts" (ipn)');
$this->addSql('CREATE TEMPORARY TABLE __temp__suppliers AS SELECT id, name, last_modified, datetime_added, comment, not_selectable, alternative_names, address, phone_number, fax_number, email_address, website, auto_product_url, shipping_costs, parent_id, default_currency_id, id_preview_attachment FROM "suppliers"');
$this->addSql('DROP TABLE "suppliers"');
$this->addSql('CREATE TABLE "suppliers" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, alternative_names CLOB DEFAULT NULL, address VARCHAR(255) NOT NULL, phone_number VARCHAR(255) NOT NULL, fax_number VARCHAR(255) NOT NULL, email_address VARCHAR(255) NOT NULL, website VARCHAR(255) NOT NULL, auto_product_url VARCHAR(255) NOT NULL, shipping_costs NUMERIC(11, 5) DEFAULT NULL, parent_id INTEGER DEFAULT NULL, default_currency_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, CONSTRAINT FK_AC28B95C727ACA70 FOREIGN KEY (parent_id) REFERENCES "suppliers" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_AC28B95CECD792C0 FOREIGN KEY (default_currency_id) REFERENCES currencies (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_AC28B95CEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('INSERT INTO "suppliers" (id, name, last_modified, datetime_added, comment, not_selectable, alternative_names, address, phone_number, fax_number, email_address, website, auto_product_url, shipping_costs, parent_id, default_currency_id, id_preview_attachment) SELECT id, name, last_modified, datetime_added, comment, not_selectable, alternative_names, address, phone_number, fax_number, email_address, website, auto_product_url, shipping_costs, parent_id, default_currency_id, id_preview_attachment FROM __temp__suppliers');
$this->addSql('DROP TABLE __temp__suppliers');
$this->addSql('CREATE INDEX IDX_AC28B95C727ACA70 ON "suppliers" (parent_id)');
$this->addSql('CREATE INDEX IDX_AC28B95CECD792C0 ON "suppliers" (default_currency_id)');
$this->addSql('CREATE INDEX IDX_AC28B95CEA7100A1 ON "suppliers" (id_preview_attachment)');
$this->addSql('CREATE INDEX supplier_idx_name ON "suppliers" (name)');
$this->addSql('CREATE INDEX supplier_idx_parent_name ON "suppliers" (parent_id, name)');
}
public function postgreSQLUp(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE attachments ALTER external_path TYPE VARCHAR(2048)');
$this->addSql('ALTER TABLE manufacturers ALTER website TYPE VARCHAR(2048)');
$this->addSql('ALTER TABLE manufacturers ALTER auto_product_url TYPE VARCHAR(2048)');
$this->addSql('ALTER TABLE parts ALTER provider_reference_provider_url TYPE VARCHAR(2048)');
$this->addSql('ALTER TABLE suppliers ALTER website TYPE VARCHAR(2048)');
$this->addSql('ALTER TABLE suppliers ALTER auto_product_url TYPE VARCHAR(2048)');
}
public function postgreSQLDown(Schema $schema): void
{
$this->addSql('ALTER TABLE "attachments" ALTER external_path TYPE VARCHAR(255)');
$this->addSql('ALTER TABLE "manufacturers" ALTER website TYPE VARCHAR(255)');
$this->addSql('ALTER TABLE "manufacturers" ALTER auto_product_url TYPE VARCHAR(255)');
$this->addSql('ALTER TABLE "parts" ALTER provider_reference_provider_url TYPE VARCHAR(255)');
$this->addSql('ALTER TABLE "suppliers" ALTER website TYPE VARCHAR(255)');
$this->addSql('ALTER TABLE "suppliers" ALTER auto_product_url TYPE VARCHAR(255)');
}
}

View file

@ -9,9 +9,9 @@
"@symfony/stimulus-bridge": "^4.0.0",
"@symfony/ux-translator": "file:vendor/symfony/ux-translator/assets",
"@symfony/ux-turbo": "file:vendor/symfony/ux-turbo/assets",
"@symfony/webpack-encore": "^5.0.0",
"@symfony/webpack-encore": "^5.1.0",
"bootstrap": "^5.1.3",
"core-js": "^3.23.0",
"core-js": "^3.38.0",
"intl-messageformat": "^10.2.5",
"jquery": "^3.5.1",
"popper.js": "^1.14.7",

View file

@ -121,6 +121,11 @@ class ImportPartKeeprCommand extends Command
$count = $this->datastructureImporter->importPartUnits($data);
$io->success('Imported '.$count.' measurement units.');
//Import the custom states
$io->info('Importing custom states...');
$count = $this->datastructureImporter->importPartCustomStates($data);
$io->success('Imported '.$count.' custom states.');
//Import manufacturers
$io->info('Importing manufacturers...');
$count = $this->datastructureImporter->importManufacturers($data);

View file

@ -232,6 +232,7 @@ abstract class BaseAdminController extends AbstractController
'timeTravel' => $timeTravel_timestamp,
'repo' => $repo,
'partsContainingElement' => $repo instanceof PartsContainingRepositoryInterface,
'showParameters' => !($this instanceof PartCustomStateController),
]);
}
@ -382,6 +383,7 @@ abstract class BaseAdminController extends AbstractController
'import_form' => $import_form,
'mass_creation_form' => $mass_creation_form,
'route_base' => $this->route_base,
'showParameters' => !($this instanceof PartCustomStateController),
]);
}

View file

@ -0,0 +1,83 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace App\Controller\AdminPages;
use App\Entity\Attachments\PartCustomStateAttachment;
use App\Entity\Parameters\PartCustomStateParameter;
use App\Entity\Parts\PartCustomState;
use App\Form\AdminPages\PartCustomStateAdminForm;
use App\Services\ImportExportSystem\EntityExporter;
use App\Services\ImportExportSystem\EntityImporter;
use App\Services\Trees\StructuralElementRecursionHelper;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
/**
* @see \App\Tests\Controller\AdminPages\PartCustomStateControllerTest
*/
#[Route(path: '/part_custom_state')]
class PartCustomStateController extends BaseAdminController
{
protected string $entity_class = PartCustomState::class;
protected string $twig_template = 'admin/part_custom_state_admin.html.twig';
protected string $form_class = PartCustomStateAdminForm::class;
protected string $route_base = 'part_custom_state';
protected string $attachment_class = PartCustomStateAttachment::class;
protected ?string $parameter_class = PartCustomStateParameter::class;
#[Route(path: '/{id}', name: 'part_custom_state_delete', methods: ['DELETE'])]
public function delete(Request $request, PartCustomState $entity, StructuralElementRecursionHelper $recursionHelper): RedirectResponse
{
return $this->_delete($request, $entity, $recursionHelper);
}
#[Route(path: '/{id}/edit/{timestamp}', name: 'part_custom_state_edit', requirements: ['id' => '\d+'])]
#[Route(path: '/{id}', requirements: ['id' => '\d+'])]
public function edit(PartCustomState $entity, Request $request, EntityManagerInterface $em, ?string $timestamp = null): Response
{
return $this->_edit($entity, $request, $em, $timestamp);
}
#[Route(path: '/new', name: 'part_custom_state_new')]
#[Route(path: '/{id}/clone', name: 'part_custom_state_clone')]
#[Route(path: '/')]
public function new(Request $request, EntityManagerInterface $em, EntityImporter $importer, ?PartCustomState $entity = null): Response
{
return $this->_new($request, $em, $importer, $entity);
}
#[Route(path: '/export', name: 'part_custom_state_export_all')]
public function exportAll(EntityManagerInterface $em, EntityExporter $exporter, Request $request): Response
{
return $this->_exportAll($em, $exporter, $request);
}
#[Route(path: '/{id}/export', name: 'part_custom_state_export')]
public function exportEntity(PartCustomState $entity, EntityExporter $exporter, Request $request): Response
{
return $this->_exportEntity($entity, $exporter, $request);
}
}

View file

@ -47,6 +47,7 @@ use App\Services\Parts\PartLotWithdrawAddHelper;
use App\Services\Parts\PricedetailHelper;
use App\Services\ProjectSystem\ProjectBuildPartHelper;
use App\Settings\BehaviorSettings\PartInfoSettings;
use App\Settings\MiscSettings\IpnSuggestSettings;
use DateTime;
use Doctrine\ORM\EntityManagerInterface;
use Exception;
@ -74,6 +75,7 @@ final class PartController extends AbstractController
private readonly EntityManagerInterface $em,
private readonly EventCommentHelper $commentHelper,
private readonly PartInfoSettings $partInfoSettings,
private readonly IpnSuggestSettings $ipnSuggestSettings,
) {
}
@ -444,10 +446,13 @@ final class PartController extends AbstractController
$template = 'parts/edit/update_from_ip.html.twig';
}
$partRepository = $this->em->getRepository(Part::class);
return $this->render(
$template,
[
'part' => $new_part,
'ipnSuggestions' => $partRepository->autoCompleteIpn($data, $data->getDescription(), $this->ipnSuggestSettings->suggestPartDigits),
'form' => $form,
'merge_old_name' => $merge_infos['tname_before'] ?? null,
'merge_other' => $merge_infos['other_part'] ?? null,
@ -457,7 +462,6 @@ final class PartController extends AbstractController
);
}
#[Route(path: '/{id}/add_withdraw', name: 'part_add_withdraw', methods: ['POST'])]
public function withdrawAddHandler(Part $part, Request $request, EntityManagerInterface $em, PartLotWithdrawAddHelper $withdrawAddHelper): Response
{

View file

@ -64,7 +64,7 @@ class SettingsController extends AbstractController
$this->settingsManager->save($settings);
//It might be possible, that the tree settings have changed, so clear the cache
$cache->invalidateTags(['tree_treeview', 'sidebar_tree_update']);
$cache->invalidateTags(['tree_tools', 'tree_treeview', 'sidebar_tree_update', 'synonyms']);
$this->addFlash('success', t('settings.flash.saved'));
}

View file

@ -23,6 +23,7 @@ declare(strict_types=1);
namespace App\Controller;
use App\Entity\Parameters\AbstractParameter;
use App\Settings\MiscSettings\IpnSuggestSettings;
use Symfony\Component\HttpFoundation\Response;
use App\Entity\Attachments\Attachment;
use App\Entity\Parts\Category;
@ -60,8 +61,11 @@ use Symfony\Component\Serializer\Serializer;
#[Route(path: '/typeahead')]
class TypeaheadController extends AbstractController
{
public function __construct(protected AttachmentURLGenerator $urlGenerator, protected Packages $assets)
{
public function __construct(
protected AttachmentURLGenerator $urlGenerator,
protected Packages $assets,
protected IpnSuggestSettings $ipnSuggestSettings,
) {
}
#[Route(path: '/builtInResources/search', name: 'typeahead_builtInRessources')]
@ -183,4 +187,30 @@ class TypeaheadController extends AbstractController
return new JsonResponse($data, Response::HTTP_OK, [], true);
}
#[Route(path: '/parts/ipn-suggestions', name: 'ipn_suggestions', methods: ['GET'])]
public function ipnSuggestions(
Request $request,
EntityManagerInterface $entityManager
): JsonResponse {
$partId = $request->query->get('partId');
if ($partId === '0' || $partId === 'undefined' || $partId === 'null') {
$partId = null;
}
$categoryId = $request->query->getInt('categoryId');
$description = base64_decode($request->query->getString('description'), true);
/** @var Part $part */
$part = $partId !== null ? $entityManager->getRepository(Part::class)->find($partId) : new Part();
/** @var Category|null $category */
$category = $entityManager->getRepository(Category::class)->find($categoryId);
$clonedPart = clone $part;
$clonedPart->setCategory($category);
$partRepository = $entityManager->getRepository(Part::class);
$ipnSuggestions = $partRepository->autoCompleteIpn($clonedPart, $description, $this->ipnSuggestSettings->suggestPartDigits);
return new JsonResponse($ipnSuggestions);
}
}

View file

@ -24,6 +24,7 @@ namespace App\DataFixtures;
use App\Entity\Attachments\AttachmentType;
use App\Entity\Base\AbstractStructuralDBElement;
use App\Entity\Parts\PartCustomState;
use App\Entity\ProjectSystem\Project;
use App\Entity\Parts\Category;
use App\Entity\Parts\Footprint;
@ -50,7 +51,7 @@ class DataStructureFixtures extends Fixture implements DependentFixtureInterface
{
//Reset autoincrement
$types = [AttachmentType::class, Project::class, Category::class, Footprint::class, Manufacturer::class,
MeasurementUnit::class, StorageLocation::class, Supplier::class,];
MeasurementUnit::class, StorageLocation::class, Supplier::class, PartCustomState::class];
foreach ($types as $type) {
$this->createNodesForClass($type, $manager);

View file

@ -41,6 +41,7 @@ use App\Entity\Parts\Category;
use App\Entity\Parts\Footprint;
use App\Entity\Parts\Manufacturer;
use App\Entity\Parts\MeasurementUnit;
use App\Entity\Parts\PartCustomState;
use App\Entity\Parts\PartLot;
use App\Entity\Parts\StorageLocation;
use App\Entity\Parts\Supplier;
@ -86,6 +87,7 @@ class PartFilter implements FilterInterface
public readonly EntityConstraint $lotOwner;
public readonly EntityConstraint $measurementUnit;
public readonly EntityConstraint $partCustomState;
public readonly TextConstraint $manufacturer_product_url;
public readonly TextConstraint $manufacturer_product_number;
public readonly IntConstraint $attachmentsCount;
@ -128,6 +130,7 @@ class PartFilter implements FilterInterface
$this->favorite = new BooleanConstraint('part.favorite');
$this->needsReview = new BooleanConstraint('part.needs_review');
$this->measurementUnit = new EntityConstraint($nodesListBuilder, MeasurementUnit::class, 'part.partUnit');
$this->partCustomState = new EntityConstraint($nodesListBuilder, PartCustomState::class, 'part.partCustomState');
$this->mass = new NumberConstraint('part.mass');
$this->dbId = new IntConstraint('part.id');
$this->ipn = new TextConstraint('part.ipn');

View file

@ -174,6 +174,19 @@ final class PartsDataTable implements DataTableTypeInterface
return $tmp;
}
])
->add('partCustomState', TextColumn::class, [
'label' => $this->translator->trans('part.table.partCustomState'),
'orderField' => 'NATSORT(_partCustomState.name)',
'render' => function($value, Part $context): string {
$partCustomState = $context->getPartCustomState();
if ($partCustomState === null) {
return '';
}
return htmlspecialchars($partCustomState->getName());
}
])
->add('addedDate', LocaleDateTimeColumn::class, [
'label' => $this->translator->trans('part.table.addedDate'),
])
@ -309,6 +322,7 @@ final class PartsDataTable implements DataTableTypeInterface
->addSelect('footprint')
->addSelect('manufacturer')
->addSelect('partUnit')
->addSelect('partCustomState')
->addSelect('master_picture_attachment')
->addSelect('footprint_attachment')
->addSelect('partLots')
@ -327,6 +341,7 @@ final class PartsDataTable implements DataTableTypeInterface
->leftJoin('orderdetails.supplier', 'suppliers')
->leftJoin('part.attachments', 'attachments')
->leftJoin('part.partUnit', 'partUnit')
->leftJoin('part.partCustomState', 'partCustomState')
->leftJoin('part.parameters', 'parameters')
->where('part.id IN (:ids)')
->setParameter('ids', $ids)
@ -344,6 +359,7 @@ final class PartsDataTable implements DataTableTypeInterface
->addGroupBy('suppliers')
->addGroupBy('attachments')
->addGroupBy('partUnit')
->addGroupBy('partCustomState')
->addGroupBy('parameters');
//Get the results in the same order as the IDs were passed
@ -415,6 +431,10 @@ final class PartsDataTable implements DataTableTypeInterface
$builder->leftJoin('part.partUnit', '_partUnit');
$builder->addGroupBy('_partUnit');
}
if (str_contains($dql, '_partCustomState')) {
$builder->leftJoin('part.partCustomState', '_partCustomState');
$builder->addGroupBy('_partCustomState');
}
if (str_contains($dql, '_parameters')) {
$builder->leftJoin('part.parameters', '_parameters');
//Do not group by many-to-* relations, as it would restrict the COUNT having clauses to be maximum 1

View file

@ -97,7 +97,7 @@ use function in_array;
#[DiscriminatorMap(typeProperty: '_type', mapping: self::API_DISCRIMINATOR_MAP)]
abstract class Attachment extends AbstractNamedDBElement
{
private const ORM_DISCRIMINATOR_MAP = ['Part' => PartAttachment::class, 'Device' => ProjectAttachment::class,
private const ORM_DISCRIMINATOR_MAP = ['Part' => PartAttachment::class, 'PartCustomState' => PartCustomStateAttachment::class, 'Device' => ProjectAttachment::class,
'AttachmentType' => AttachmentTypeAttachment::class,
'Category' => CategoryAttachment::class, 'Footprint' => FootprintAttachment::class, 'Manufacturer' => ManufacturerAttachment::class,
'Currency' => CurrencyAttachment::class, 'Group' => GroupAttachment::class, 'MeasurementUnit' => MeasurementUnitAttachment::class,
@ -107,7 +107,8 @@ abstract class Attachment extends AbstractNamedDBElement
/*
* The discriminator map used for API platform. The key should be the same as the api platform short type (the @type JSONLD field).
*/
private const API_DISCRIMINATOR_MAP = ["Part" => PartAttachment::class, "Project" => ProjectAttachment::class, "AttachmentType" => AttachmentTypeAttachment::class,
private const API_DISCRIMINATOR_MAP = ["Part" => PartAttachment::class, "PartCustomState" => PartCustomStateAttachment::class, "Project" => ProjectAttachment::class,
"AttachmentType" => AttachmentTypeAttachment::class,
"Category" => CategoryAttachment::class, "Footprint" => FootprintAttachment::class, "Manufacturer" => ManufacturerAttachment::class,
"Currency" => CurrencyAttachment::class, "Group" => GroupAttachment::class, "MeasurementUnit" => MeasurementUnitAttachment::class,
"StorageLocation" => StorageLocationAttachment::class, "Supplier" => SupplierAttachment::class, "User" => UserAttachment::class, "LabelProfile" => LabelAttachment::class];
@ -165,9 +166,10 @@ abstract class Attachment extends AbstractNamedDBElement
* @var string|null The path to the external source if the file is stored externally or was downloaded from an
* external source. Null if there is no external source.
*/
#[ORM\Column(type: Types::STRING, nullable: true)]
#[ORM\Column(type: Types::STRING, length: 2048, nullable: true)]
#[Groups(['attachment:read'])]
#[ApiProperty(example: 'http://example.com/image.jpg')]
#[Assert\Length(2048)]
protected ?string $external_path = null;
/**

View file

@ -0,0 +1,45 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace App\Entity\Attachments;
use App\Entity\Parts\PartCustomState;
use App\Serializer\APIPlatform\OverrideClassDenormalizer;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Serializer\Attribute\Context;
/**
* An attachment attached to a part custom state element.
* @extends Attachment<PartCustomState>
*/
#[UniqueEntity(['name', 'attachment_type', 'element'])]
#[ORM\Entity]
class PartCustomStateAttachment extends Attachment
{
final public const ALLOWED_ELEMENT_CLASS = PartCustomState::class;
#[ORM\ManyToOne(targetEntity: PartCustomState::class, inversedBy: 'attachments')]
#[ORM\JoinColumn(name: 'element_id', nullable: false, onDelete: 'CASCADE')]
#[Context(denormalizationContext: [OverrideClassDenormalizer::CONTEXT_KEY => self::ALLOWED_ELEMENT_CLASS])]
protected ?AttachmentContainingDBElement $element = null;
}

View file

@ -83,8 +83,8 @@ abstract class AbstractCompany extends AbstractPartsContainingDBElement
*/
#[Assert\Url(requireTld: false)]
#[Groups(['full', 'company:read', 'company:write', 'import', 'extended'])]
#[ORM\Column(type: Types::STRING)]
#[Assert\Length(max: 255)]
#[ORM\Column(type: Types::STRING, length: 2048)]
#[Assert\Length(max: 2048)]
protected string $website = '';
#[Groups(['company:read', 'company:write', 'import', 'full', 'extended'])]
@ -93,8 +93,8 @@ abstract class AbstractCompany extends AbstractPartsContainingDBElement
/**
* @var string The link to the website of an article. Use %PARTNUMBER% as placeholder for the part number.
*/
#[ORM\Column(type: Types::STRING)]
#[Assert\Length(max: 255)]
#[ORM\Column(type: Types::STRING, length: 2048)]
#[Assert\Length(max: 2048)]
#[Groups(['full', 'company:read', 'company:write', 'import', 'extended'])]
protected string $auto_product_url = '';

View file

@ -33,6 +33,7 @@ use App\Entity\Attachments\LabelAttachment;
use App\Entity\Attachments\ManufacturerAttachment;
use App\Entity\Attachments\MeasurementUnitAttachment;
use App\Entity\Attachments\PartAttachment;
use App\Entity\Attachments\PartCustomStateAttachment;
use App\Entity\Attachments\ProjectAttachment;
use App\Entity\Attachments\StorageLocationAttachment;
use App\Entity\Attachments\SupplierAttachment;
@ -40,6 +41,7 @@ use App\Entity\Attachments\UserAttachment;
use App\Entity\Parameters\AbstractParameter;
use App\Entity\Parts\Category;
use App\Entity\PriceInformations\Pricedetail;
use App\Entity\Parts\PartCustomState;
use App\Entity\ProjectSystem\Project;
use App\Entity\ProjectSystem\ProjectBOMEntry;
use App\Entity\Parts\Footprint;
@ -68,7 +70,41 @@ use Symfony\Component\Serializer\Annotation\Groups;
* Every database table which are managed with this class (or a subclass of it)
* must have the table row "id"!! The ID is the unique key to identify the elements.
*/
#[DiscriminatorMap(typeProperty: 'type', mapping: ['attachment_type' => AttachmentType::class, 'attachment' => Attachment::class, 'attachment_type_attachment' => AttachmentTypeAttachment::class, 'category_attachment' => CategoryAttachment::class, 'currency_attachment' => CurrencyAttachment::class, 'footprint_attachment' => FootprintAttachment::class, 'group_attachment' => GroupAttachment::class, 'label_attachment' => LabelAttachment::class, 'manufacturer_attachment' => ManufacturerAttachment::class, 'measurement_unit_attachment' => MeasurementUnitAttachment::class, 'part_attachment' => PartAttachment::class, 'project_attachment' => ProjectAttachment::class, 'storelocation_attachment' => StorageLocationAttachment::class, 'supplier_attachment' => SupplierAttachment::class, 'user_attachment' => UserAttachment::class, 'category' => Category::class, 'project' => Project::class, 'project_bom_entry' => ProjectBOMEntry::class, 'footprint' => Footprint::class, 'group' => Group::class, 'manufacturer' => Manufacturer::class, 'orderdetail' => Orderdetail::class, 'part' => Part::class, 'pricedetail' => Pricedetail::class, 'storelocation' => StorageLocation::class, 'part_lot' => PartLot::class, 'currency' => Currency::class, 'measurement_unit' => MeasurementUnit::class, 'parameter' => AbstractParameter::class, 'supplier' => Supplier::class, 'user' => User::class])]
#[DiscriminatorMap(typeProperty: 'type', mapping: [
'attachment_type' => AttachmentType::class,
'attachment' => Attachment::class,
'attachment_type_attachment' => AttachmentTypeAttachment::class,
'category_attachment' => CategoryAttachment::class,
'currency_attachment' => CurrencyAttachment::class,
'footprint_attachment' => FootprintAttachment::class,
'group_attachment' => GroupAttachment::class,
'label_attachment' => LabelAttachment::class,
'manufacturer_attachment' => ManufacturerAttachment::class,
'measurement_unit_attachment' => MeasurementUnitAttachment::class,
'part_attachment' => PartAttachment::class,
'part_custom_state_attachment' => PartCustomStateAttachment::class,
'project_attachment' => ProjectAttachment::class,
'storelocation_attachment' => StorageLocationAttachment::class,
'supplier_attachment' => SupplierAttachment::class,
'user_attachment' => UserAttachment::class,
'category' => Category::class,
'project' => Project::class,
'project_bom_entry' => ProjectBOMEntry::class,
'footprint' => Footprint::class,
'group' => Group::class,
'manufacturer' => Manufacturer::class,
'orderdetail' => Orderdetail::class,
'part' => Part::class,
'part_custom_state' => PartCustomState::class,
'pricedetail' => Pricedetail::class,
'storelocation' => StorageLocation::class,
'part_lot' => PartLot::class,
'currency' => Currency::class,
'measurement_unit' => MeasurementUnit::class,
'parameter' => AbstractParameter::class,
'supplier' => Supplier::class,
'user' => User::class]
)]
#[ORM\MappedSuperclass(repositoryClass: DBElementRepository::class)]
abstract class AbstractDBElement implements JsonSerializable
{

View file

@ -46,6 +46,7 @@ use App\Entity\Attachments\AttachmentType;
use App\Entity\Attachments\AttachmentTypeAttachment;
use App\Entity\Attachments\CategoryAttachment;
use App\Entity\Attachments\CurrencyAttachment;
use App\Entity\Attachments\PartCustomStateAttachment;
use App\Entity\Attachments\ProjectAttachment;
use App\Entity\Attachments\FootprintAttachment;
use App\Entity\Attachments\GroupAttachment;
@ -58,6 +59,8 @@ use App\Entity\Attachments\UserAttachment;
use App\Entity\Base\AbstractDBElement;
use App\Entity\Contracts\LogWithEventUndoInterface;
use App\Entity\Contracts\NamedElementInterface;
use App\Entity\Parameters\PartCustomStateParameter;
use App\Entity\Parts\PartCustomState;
use App\Entity\ProjectSystem\Project;
use App\Entity\Parameters\AbstractParameter;
use App\Entity\Parameters\AttachmentTypeParameter;
@ -158,6 +161,7 @@ class CollectionElementDeleted extends AbstractLogEntry implements LogWithEventU
Part::class => PartParameter::class,
StorageLocation::class => StorageLocationParameter::class,
Supplier::class => SupplierParameter::class,
PartCustomState::class => PartCustomStateParameter::class,
default => throw new \RuntimeException('Unknown target class for parameter: '.$this->getTargetClass()),
};
}
@ -173,6 +177,7 @@ class CollectionElementDeleted extends AbstractLogEntry implements LogWithEventU
Manufacturer::class => ManufacturerAttachment::class,
MeasurementUnit::class => MeasurementUnitAttachment::class,
Part::class => PartAttachment::class,
PartCustomState::class => PartCustomStateAttachment::class,
StorageLocation::class => StorageLocationAttachment::class,
Supplier::class => SupplierAttachment::class,
User::class => UserAttachment::class,

View file

@ -34,6 +34,7 @@ use App\Entity\Parts\Manufacturer;
use App\Entity\Parts\MeasurementUnit;
use App\Entity\Parts\Part;
use App\Entity\Parts\PartAssociation;
use App\Entity\Parts\PartCustomState;
use App\Entity\Parts\PartLot;
use App\Entity\Parts\StorageLocation;
use App\Entity\Parts\Supplier;
@ -71,6 +72,7 @@ enum LogTargetType: int
case PART_ASSOCIATION = 20;
case BULK_INFO_PROVIDER_IMPORT_JOB = 21;
case BULK_INFO_PROVIDER_IMPORT_JOB_PART = 22;
case PART_CUSTOM_STATE = 23;
/**
* Returns the class name of the target type or null if the target type is NONE.
@ -102,6 +104,7 @@ enum LogTargetType: int
self::PART_ASSOCIATION => PartAssociation::class,
self::BULK_INFO_PROVIDER_IMPORT_JOB => BulkInfoProviderImportJob::class,
self::BULK_INFO_PROVIDER_IMPORT_JOB_PART => BulkInfoProviderImportJobPart::class,
self::PART_CUSTOM_STATE => PartCustomState::class
};
}

View file

@ -73,7 +73,8 @@ use function sprintf;
#[ORM\DiscriminatorMap([0 => CategoryParameter::class, 1 => CurrencyParameter::class, 2 => ProjectParameter::class,
3 => FootprintParameter::class, 4 => GroupParameter::class, 5 => ManufacturerParameter::class,
6 => MeasurementUnitParameter::class, 7 => PartParameter::class, 8 => StorageLocationParameter::class,
9 => SupplierParameter::class, 10 => AttachmentTypeParameter::class])]
9 => SupplierParameter::class, 10 => AttachmentTypeParameter::class,
12 => PartCustomStateParameter::class])]
#[ORM\Table('parameters')]
#[ORM\Index(columns: ['name'], name: 'parameter_name_idx')]
#[ORM\Index(columns: ['param_group'], name: 'parameter_group_idx')]
@ -105,7 +106,7 @@ abstract class AbstractParameter extends AbstractNamedDBElement implements Uniqu
"AttachmentType" => AttachmentTypeParameter::class, "Category" => CategoryParameter::class, "Currency" => CurrencyParameter::class,
"Project" => ProjectParameter::class, "Footprint" => FootprintParameter::class, "Group" => GroupParameter::class,
"Manufacturer" => ManufacturerParameter::class, "MeasurementUnit" => MeasurementUnitParameter::class,
"StorageLocation" => StorageLocationParameter::class, "Supplier" => SupplierParameter::class];
"StorageLocation" => StorageLocationParameter::class, "Supplier" => SupplierParameter::class, "PartCustomState" => PartCustomStateParameter::class];
/**
* @var string The class of the element that can be passed to this attachment. Must be overridden in subclasses.
@ -123,7 +124,7 @@ abstract class AbstractParameter extends AbstractNamedDBElement implements Uniqu
/**
* @var float|null the guaranteed minimum value of this property
*/
#[Assert\Type(['float', null])]
#[Assert\Type(['float', 'null'])]
#[Assert\LessThanOrEqual(propertyPath: 'value_typical', message: 'parameters.validator.min_lesser_typical')]
#[Assert\LessThan(propertyPath: 'value_max', message: 'parameters.validator.min_lesser_max')]
#[Groups(['full', 'parameter:read', 'parameter:write', 'import'])]
@ -133,7 +134,7 @@ abstract class AbstractParameter extends AbstractNamedDBElement implements Uniqu
/**
* @var float|null the typical value of this property
*/
#[Assert\Type([null, 'float'])]
#[Assert\Type(['null', 'float'])]
#[Groups(['full', 'parameter:read', 'parameter:write', 'import'])]
#[ORM\Column(type: Types::FLOAT, nullable: true)]
protected ?float $value_typical = null;
@ -141,7 +142,7 @@ abstract class AbstractParameter extends AbstractNamedDBElement implements Uniqu
/**
* @var float|null the maximum value of this property
*/
#[Assert\Type(['float', null])]
#[Assert\Type(['float', 'null'])]
#[Assert\GreaterThanOrEqual(propertyPath: 'value_typical', message: 'parameters.validator.max_greater_typical')]
#[Groups(['full', 'parameter:read', 'parameter:write', 'import'])]
#[ORM\Column(type: Types::FLOAT, nullable: true)]

View file

@ -0,0 +1,65 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Entity\Parameters;
use App\Entity\Base\AbstractDBElement;
use App\Entity\Parts\PartCustomState;
use App\Repository\ParameterRepository;
use App\Serializer\APIPlatform\OverrideClassDenormalizer;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Serializer\Attribute\Context;
#[UniqueEntity(fields: ['name', 'group', 'element'])]
#[ORM\Entity(repositoryClass: ParameterRepository::class)]
class PartCustomStateParameter extends AbstractParameter
{
final public const ALLOWED_ELEMENT_CLASS = PartCustomState::class;
/**
* @var PartCustomState the element this para is associated with
*/
#[ORM\ManyToOne(targetEntity: PartCustomState::class, inversedBy: 'parameters')]
#[ORM\JoinColumn(name: 'element_id', nullable: false, onDelete: 'CASCADE')]
#[Context(denormalizationContext: [OverrideClassDenormalizer::CONTEXT_KEY => self::ALLOWED_ELEMENT_CLASS])]
protected ?AbstractDBElement $element = null;
}

View file

@ -118,6 +118,13 @@ class Category extends AbstractPartsContainingDBElement
#[ORM\Column(type: Types::TEXT)]
protected string $partname_regex = '';
/**
* @var string The prefix for ipn generation for created parts in this category.
*/
#[Groups(['full', 'import', 'category:read', 'category:write'])]
#[ORM\Column(type: Types::STRING, length: 255, nullable: false, options: ['default' => ''])]
protected string $part_ipn_prefix = '';
/**
* @var bool Set to true, if the footprints should be disabled for parts this category (not implemented yet).
*/
@ -225,6 +232,16 @@ class Category extends AbstractPartsContainingDBElement
return $this;
}
public function getPartIpnPrefix(): string
{
return $this->part_ipn_prefix;
}
public function setPartIpnPrefix(string $part_ipn_prefix): void
{
$this->part_ipn_prefix = $part_ipn_prefix;
}
public function isDisableFootprints(): bool
{
return $this->disable_footprints;

View file

@ -50,7 +50,7 @@ class InfoProviderReference
/**
* @var string|null The url of this part inside the provider system or null if this info is not existing
*/
#[Column(type: Types::STRING, nullable: true)]
#[Column(type: Types::STRING, length: 2048, nullable: true)]
#[Groups(['provider_reference:read', 'full'])]
private ?string $provider_url = null;

View file

@ -61,7 +61,6 @@ use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
@ -75,7 +74,6 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
* @extends AttachmentContainingDBElement<PartAttachment>
* @template-use ParametersTrait<PartParameter>
*/
#[UniqueEntity(fields: ['ipn'], message: 'part.ipn.must_be_unique')]
#[ORM\Entity(repositoryClass: PartRepository::class)]
#[ORM\EntityListeners([TreeCacheInvalidationListener::class])]
#[ORM\Table('`parts`')]
@ -107,7 +105,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
denormalizationContext: ['groups' => ['part:write', 'api:basic:write', 'eda_info:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'],
)]
#[ApiFilter(PropertyFilter::class)]
#[ApiFilter(EntityFilter::class, properties: ["category", "footprint", "manufacturer", "partUnit"])]
#[ApiFilter(EntityFilter::class, properties: ["category", "footprint", "manufacturer", "partUnit", "partCustomState"])]
#[ApiFilter(PartStoragelocationFilter::class, properties: ["storage_location"])]
#[ApiFilter(LikeFilter::class, properties: ["name", "comment", "description", "ipn", "manufacturer_product_number"])]
#[ApiFilter(TagFilter::class, properties: ["tags"])]

View file

@ -0,0 +1,127 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace App\Entity\Parts;
use ApiPlatform\Metadata\ApiProperty;
use App\Entity\Attachments\Attachment;
use App\Entity\Attachments\PartCustomStateAttachment;
use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface;
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Serializer\Filter\PropertyFilter;
use App\ApiPlatform\Filter\LikeFilter;
use App\Entity\Base\AbstractPartsContainingDBElement;
use App\Entity\Base\AbstractStructuralDBElement;
use App\Entity\Parameters\PartCustomStateParameter;
use App\Repository\Parts\PartCustomStateRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
/**
* This entity represents a custom part state.
* If an organisation uses Part-DB and has its custom part states, this is useful.
*
* @extends AbstractPartsContainingDBElement<PartCustomStateAttachment,PartCustomStateParameter>
*/
#[ORM\Entity(repositoryClass: PartCustomStateRepository::class)]
#[ORM\Table('`part_custom_states`')]
#[ORM\Index(columns: ['name'], name: 'part_custom_state_name')]
#[ApiResource(
operations: [
new Get(security: 'is_granted("read", object)'),
new GetCollection(security: 'is_granted("@part_custom_states.read")'),
new Post(securityPostDenormalize: 'is_granted("create", object)'),
new Patch(security: 'is_granted("edit", object)'),
new Delete(security: 'is_granted("delete", object)'),
],
normalizationContext: ['groups' => ['part_custom_state:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'],
denormalizationContext: ['groups' => ['part_custom_state:write', 'api:basic:write'], 'openapi_definition_name' => 'Write'],
)]
#[ApiFilter(PropertyFilter::class)]
#[ApiFilter(LikeFilter::class, properties: ["name"])]
#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)]
#[ApiFilter(OrderFilter::class, properties: ['name', 'id', 'addedDate', 'lastModified'])]
class PartCustomState extends AbstractPartsContainingDBElement
{
/**
* @var string The comment info for this element as markdown
*/
#[Groups(['part_custom_state:read', 'part_custom_state:write', 'full', 'import'])]
protected string $comment = '';
#[ORM\OneToMany(mappedBy: 'parent', targetEntity: self::class, cascade: ['persist'])]
#[ORM\OrderBy(['name' => Criteria::ASC])]
protected Collection $children;
#[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')]
#[ORM\JoinColumn(name: 'parent_id')]
#[Groups(['part_custom_state:read', 'part_custom_state:write'])]
#[ApiProperty(readableLink: false, writableLink: false)]
protected ?AbstractStructuralDBElement $parent = null;
/**
* @var Collection<int, PartCustomStateAttachment>
*/
#[Assert\Valid]
#[ORM\OneToMany(targetEntity: PartCustomStateAttachment::class, mappedBy: 'element', cascade: ['persist', 'remove'], orphanRemoval: true)]
#[ORM\OrderBy(['name' => Criteria::ASC])]
#[Groups(['part_custom_state:read', 'part_custom_state:write'])]
protected Collection $attachments;
#[ORM\ManyToOne(targetEntity: PartCustomStateAttachment::class)]
#[ORM\JoinColumn(name: 'id_preview_attachment', onDelete: 'SET NULL')]
#[Groups(['part_custom_state:read', 'part_custom_state:write'])]
protected ?Attachment $master_picture_attachment = null;
/** @var Collection<int, PartCustomStateParameter>
*/
#[Assert\Valid]
#[ORM\OneToMany(mappedBy: 'element', targetEntity: PartCustomStateParameter::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
#[ORM\OrderBy(['name' => 'ASC'])]
#[Groups(['part_custom_state:read', 'part_custom_state:write'])]
protected Collection $parameters;
#[Groups(['part_custom_state:read'])]
protected ?\DateTimeImmutable $addedDate = null;
#[Groups(['part_custom_state:read'])]
protected ?\DateTimeImmutable $lastModified = null;
public function __construct()
{
parent::__construct();
$this->children = new ArrayCollection();
$this->attachments = new ArrayCollection();
$this->parameters = new ArrayCollection();
}
}

View file

@ -23,12 +23,14 @@ declare(strict_types=1);
namespace App\Entity\Parts\PartTraits;
use App\Entity\Parts\InfoProviderReference;
use App\Entity\Parts\PartCustomState;
use Doctrine\DBAL\Types\Types;
use App\Entity\Parts\Part;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Constraints\Length;
use App\Validator\Constraints\UniquePartIpnConstraint;
/**
* Advanced properties of a part, not related to a more specific group.
@ -64,6 +66,7 @@ trait AdvancedPropertyTrait
#[Groups(['extended', 'full', 'import', 'part:read', 'part:write'])]
#[ORM\Column(type: Types::STRING, length: 100, unique: true, nullable: true)]
#[Length(max: 100)]
#[UniquePartIpnConstraint]
protected ?string $ipn = null;
/**
@ -73,6 +76,14 @@ trait AdvancedPropertyTrait
#[Groups(['full', 'part:read'])]
protected InfoProviderReference $providerReference;
/**
* @var ?PartCustomState the custom state for the part
*/
#[Groups(['extended', 'full', 'import', 'part:read', 'part:write'])]
#[ORM\ManyToOne(targetEntity: PartCustomState::class)]
#[ORM\JoinColumn(name: 'id_part_custom_state')]
protected ?PartCustomState $partCustomState = null;
/**
* Checks if this part is marked, for that it needs further review.
*/
@ -180,7 +191,24 @@ trait AdvancedPropertyTrait
return $this;
}
/**
* Gets the custom part state for the part
* Returns null if no specific part state is set.
*/
public function getPartCustomState(): ?PartCustomState
{
return $this->partCustomState;
}
/**
* Sets the custom part state.
*
* @return $this
*/
public function setPartCustomState(?PartCustomState $partCustomState): self
{
$this->partCustomState = $partCustomState;
return $this;
}
}

View file

@ -0,0 +1,49 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2025 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace App\EnvVarProcessors;
use Symfony\Component\DependencyInjection\EnvVarProcessorInterface;
/**
* Env var processor that adds a trailing slash to a string if not already present.
*/
final class AddSlashEnvVarProcessor implements EnvVarProcessorInterface
{
public function getEnv(string $prefix, string $name, \Closure $getEnv): mixed
{
$env = $getEnv($name);
if (!is_string($env)) {
throw new \InvalidArgumentException(sprintf('The "addSlash" env var processor only works with strings, got %s.', gettype($env)));
}
return rtrim($env, '/') . '/';
}
public static function getProvidedTypes(): array
{
return [
'addSlash' => 'string',
];
}
}

View file

@ -20,7 +20,7 @@
declare(strict_types=1);
namespace App\Services;
namespace App\EnvVarProcessors;
use Closure;
use Symfony\Component\DependencyInjection\EnvVarProcessorInterface;

View file

@ -0,0 +1,93 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2025 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace App\EventListener;
use App\Services\ElementTypeNameGenerator;
use App\Services\ElementTypes;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\Translation\Translator;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
use Symfony\Contracts\Cache\TagAwareCacheInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
#[AsEventListener]
readonly class RegisterSynonymsAsTranslationParametersListener
{
private Translator $translator;
public function __construct(
#[Autowire(service: 'translator.default')] TranslatorInterface $translator,
private TagAwareCacheInterface $cache,
private ElementTypeNameGenerator $typeNameGenerator)
{
if (!$translator instanceof Translator) {
throw new \RuntimeException('Translator must be an instance of Symfony\Component\Translation\Translator or this listener cannot be used.');
}
$this->translator = $translator;
}
public function getSynonymPlaceholders(): array
{
return $this->cache->get('partdb_synonym_placeholders', function (ItemInterface $item) {
$item->tag('synonyms');
$placeholders = [];
//Generate a placeholder for each element type
foreach (ElementTypes::cases() as $elementType) {
//Versions with capitalized first letter
$capitalized = ucfirst($elementType->value); //We have only ASCII element type values, so this is sufficient
$placeholders['[' . $capitalized . ']'] = $this->typeNameGenerator->typeLabel($elementType);
$placeholders['[[' . $capitalized . ']]'] = $this->typeNameGenerator->typeLabelPlural($elementType);
//And we have lowercase versions for both
$placeholders['[' . $elementType->value . ']'] = mb_strtolower($this->typeNameGenerator->typeLabel($elementType));
$placeholders['[[' . $elementType->value . ']]'] = mb_strtolower($this->typeNameGenerator->typeLabelPlural($elementType));
}
return $placeholders;
});
}
public function __invoke(RequestEvent $event): void
{
//If we already added the parameters, skip adding them again
if (isset($this->translator->getGlobalParameters()['@@partdb_synonyms_registered@@'])) {
return;
}
//Register all placeholders for synonyms
$placeholders = $this->getSynonymPlaceholders();
foreach ($placeholders as $key => $value) {
$this->translator->addGlobalParameter($key, $value);
}
//Register the marker parameter to avoid double registration
$this->translator->addGlobalParameter('@@partdb_synonyms_registered@@', 'registered');
}
}

View file

@ -0,0 +1,97 @@
<?php
namespace App\EventSubscriber\UserSystem;
use App\Entity\Parts\Part;
use App\Settings\MiscSettings\IpnSuggestSettings;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Events;
use Doctrine\ORM\Event\OnFlushEventArgs;
class PartUniqueIpnSubscriber implements EventSubscriber
{
public function __construct(
private IpnSuggestSettings $ipnSuggestSettings
) {
}
public function getSubscribedEvents(): array
{
return [
Events::onFlush,
];
}
public function onFlush(OnFlushEventArgs $args): void
{
if (!$this->ipnSuggestSettings->autoAppendSuffix) {
return;
}
$em = $args->getObjectManager();
$uow = $em->getUnitOfWork();
$meta = $em->getClassMetadata(Part::class);
// Collect all IPNs already reserved in the current flush (so new entities do not collide with each other)
$reservedIpns = [];
// Helper to assign a collision-free IPN for a Part entity
$ensureUnique = function (Part $part) use ($em, $uow, $meta, &$reservedIpns) {
$ipn = $part->getIpn();
if ($ipn === null || $ipn === '') {
return;
}
// Check against IPNs already reserved in the current flush (except itself)
$originalIpn = $ipn;
$candidate = $originalIpn;
$increment = 1;
$conflicts = function (string $candidate) use ($em, $part, $reservedIpns) {
// Collision within the current flush session?
if (isset($reservedIpns[$candidate]) && $reservedIpns[$candidate] !== $part) {
return true;
}
// Collision with an existing DB row?
$existing = $em->getRepository(Part::class)->findOneBy(['ipn' => $candidate]);
return $existing !== null && $existing->getId() !== $part->getId();
};
while ($conflicts($candidate)) {
$candidate = $originalIpn . '_' . $increment;
$increment++;
}
if ($candidate !== $ipn) {
$before = $part->getIpn();
$part->setIpn($candidate);
// Recompute the change set so Doctrine writes the change
$uow->recomputeSingleEntityChangeSet($meta, $part);
$reservedIpns[$candidate] = $part;
// If the old IPN was reserved already, clean it up
if ($before !== null && isset($reservedIpns[$before]) && $reservedIpns[$before] === $part) {
unset($reservedIpns[$before]);
}
} else {
// Candidate unchanged, but reserve it so subsequent entities see it
$reservedIpns[$candidate] = $part;
}
};
// 1) Iterate over new entities
foreach ($uow->getScheduledEntityInsertions() as $entity) {
if ($entity instanceof Part) {
$ensureUnique($entity);
}
}
// 2) Iterate over updates (if IPN changed, ensure uniqueness again)
foreach ($uow->getScheduledEntityUpdates() as $entity) {
if ($entity instanceof Part) {
$ensureUnique($entity);
}
}
}
}

View file

@ -84,6 +84,17 @@ class CategoryAdminForm extends BaseEntityAdminForm
'disabled' => !$this->security->isGranted($is_new ? 'create' : 'edit', $entity),
]);
$builder->add('part_ipn_prefix', TextType::class, [
'required' => false,
'empty_data' => '',
'label' => 'category.edit.part_ipn_prefix',
'help' => 'category.edit.part_ipn_prefix.help',
'attr' => [
'placeholder' => 'category.edit.part_ipn_prefix.placeholder',
],
'disabled' => !$this->security->isGranted($is_new ? 'create' : 'edit', $entity),
]);
$builder->add('default_description', RichTextEditorType::class, [
'required' => false,
'empty_data' => '',

View file

@ -0,0 +1,27 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace App\Form\AdminPages;
class PartCustomStateAdminForm extends BaseEntityAdminForm
{
}

View file

@ -46,8 +46,8 @@ final class TogglePasswordTypeExtension extends AbstractTypeExtension
{
$resolver->setDefaults([
'toggle' => false,
'hidden_label' => 'Hide',
'visible_label' => 'Show',
'hidden_label' => new TranslatableMessage('password_toggle.hide'),
'visible_label' => new TranslatableMessage('password_toggle.show'),
'hidden_icon' => 'Default',
'visible_icon' => 'Default',
'button_classes' => ['toggle-password-button'],

View file

@ -130,6 +130,7 @@ class LogFilterType extends AbstractType
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',
LogTargetType::PART_CUSTOM_STATE => 'part_custom_state.label',
},
]);

View file

@ -32,6 +32,7 @@ use App\Entity\Parts\Category;
use App\Entity\Parts\Footprint;
use App\Entity\Parts\Manufacturer;
use App\Entity\Parts\MeasurementUnit;
use App\Entity\Parts\PartCustomState;
use App\Entity\Parts\StorageLocation;
use App\Entity\Parts\Supplier;
use App\Entity\ProjectSystem\Project;
@ -139,6 +140,11 @@ class PartFilterType extends AbstractType
'entity_class' => MeasurementUnit::class
]);
$builder->add('partCustomState', StructuralEntityConstraintType::class, [
'label' => 'part.edit.partCustomState',
'entity_class' => PartCustomState::class
]);
$builder->add('lastModified', DateTimeConstraintType::class, [
'label' => 'lastModified'
]);

View file

@ -30,6 +30,7 @@ use Symfony\Component\Form\ChoiceList\ChoiceList;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Translation\StaticMessage;
class ProviderSelectType extends AbstractType
{
@ -70,10 +71,10 @@ class ProviderSelectType extends AbstractType
//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 ChoiceList::label($this, static fn (?InfoProviderInterface $choice) => new StaticMessage($choice?->getProviderInfo()['name']));
}
return null;
return static fn ($choice, $key, $value) => new StaticMessage($key);
});
$resolver->setDefault('choice_value', function (Options $options) {
if ('object' === $options['input']) {

View file

@ -30,6 +30,7 @@ use App\Entity\Parts\Manufacturer;
use App\Entity\Parts\ManufacturingStatus;
use App\Entity\Parts\MeasurementUnit;
use App\Entity\Parts\Part;
use App\Entity\Parts\PartCustomState;
use App\Entity\PriceInformations\Orderdetail;
use App\Form\AttachmentFormType;
use App\Form\ParameterType;
@ -41,6 +42,7 @@ use App\Form\Type\StructuralEntityType;
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
use App\Services\LogSystem\EventCommentNeededHelper;
use App\Services\LogSystem\EventCommentType;
use App\Settings\MiscSettings\IpnSuggestSettings;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
@ -56,8 +58,12 @@ use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
class PartBaseType extends AbstractType
{
public function __construct(protected Security $security, protected UrlGeneratorInterface $urlGenerator, protected EventCommentNeededHelper $event_comment_needed_helper)
{
public function __construct(
protected Security $security,
protected UrlGeneratorInterface $urlGenerator,
protected EventCommentNeededHelper $event_comment_needed_helper,
protected IpnSuggestSettings $ipnSuggestSettings,
) {
}
public function buildForm(FormBuilderInterface $builder, array $options): void
@ -69,6 +75,39 @@ class PartBaseType extends AbstractType
/** @var PartDetailDTO|null $dto */
$dto = $options['info_provider_dto'];
$descriptionAttr = [
'placeholder' => 'part.edit.description.placeholder',
'rows' => 2,
];
if ($this->ipnSuggestSettings->useDuplicateDescription) {
// Only add attribute when duplicate description feature is enabled
$descriptionAttr['data-ipn-suggestion'] = 'descriptionField';
}
$ipnAttr = [
'class' => 'ipn-suggestion-field',
'data-elements--ipn-suggestion-target' => 'input',
'autocomplete' => 'off',
];
if ($this->ipnSuggestSettings->regex !== null && $this->ipnSuggestSettings->regex !== '') {
$ipnAttr['pattern'] = $this->ipnSuggestSettings->regex;
$ipnAttr['placeholder'] = $this->ipnSuggestSettings->regex;
$ipnAttr['title'] = $this->ipnSuggestSettings->regexHelp;
}
$ipnOptions = [
'required' => false,
'empty_data' => null,
'label' => 'part.edit.ipn',
'attr' => $ipnAttr,
];
if (isset($ipnAttr['pattern']) && $this->ipnSuggestSettings->regexHelp !== null && $this->ipnSuggestSettings->regexHelp !== '') {
$ipnOptions['help'] = $this->ipnSuggestSettings->regexHelp;
}
//Common section
$builder
->add('name', TextType::class, [
@ -83,10 +122,7 @@ class PartBaseType extends AbstractType
'empty_data' => '',
'label' => 'part.edit.description',
'mode' => 'markdown-single_line',
'attr' => [
'placeholder' => 'part.edit.description.placeholder',
'rows' => 2,
],
'attr' => $descriptionAttr,
])
->add('minAmount', SIUnitType::class, [
'attr' => [
@ -104,6 +140,9 @@ class PartBaseType extends AbstractType
'disable_not_selectable' => true,
//Do not require category for new parts, so that the user must select the category by hand and cannot forget it (the requirement is handled by the constraint in the entity)
'required' => !$new_part,
'attr' => [
'data-ipn-suggestion' => 'categoryField',
]
])
->add('footprint', StructuralEntityType::class, [
'class' => Footprint::class,
@ -171,11 +210,13 @@ class PartBaseType extends AbstractType
'disable_not_selectable' => true,
'label' => 'part.edit.partUnit',
])
->add('ipn', TextType::class, [
->add('partCustomState', StructuralEntityType::class, [
'class' => PartCustomState::class,
'required' => false,
'empty_data' => null,
'label' => 'part.edit.ipn',
]);
'disable_not_selectable' => true,
'label' => 'part.edit.partCustomState',
])
->add('ipn', TextType::class, $ipnOptions);
//Comment section
$builder->add('comment', RichTextEditorType::class, [

View file

@ -21,12 +21,11 @@
declare(strict_types=1);
namespace App\Form\Type;
namespace App\Form\Settings;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\LanguageType;
use Symfony\Component\Form\Extension\Core\Type\LocaleType;
use Symfony\Component\Intl\Languages;
use Symfony\Component\OptionsResolver\OptionsResolver;

View file

@ -0,0 +1,150 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2024 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace App\Form\Settings;
use App\Services\ElementTypes;
use App\Settings\SystemSettings\LocalizationSettings;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\EnumType;
use Symfony\Component\Form\Extension\Core\Type\LocaleType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Intl\Locales;
use Symfony\Component\Translation\StaticMessage;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* A single translation row: data source + language + translations (singular/plural).
*/
class TypeSynonymRowType extends AbstractType
{
private const PREFERRED_TYPES = [
ElementTypes::CATEGORY,
ElementTypes::STORAGE_LOCATION,
ElementTypes::FOOTPRINT,
ElementTypes::MANUFACTURER,
ElementTypes::SUPPLIER,
ElementTypes::PROJECT,
];
public function __construct(
private readonly LocalizationSettings $localizationSettings,
private readonly TranslatorInterface $translator,
#[Autowire(param: 'partdb.locale_menu')] private readonly array $preferredLanguagesParam,
) {
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('dataSource', EnumType::class, [
'class' => ElementTypes::class,
'label' => false,
'required' => true,
'constraints' => [
new Assert\NotBlank(),
],
'choice_label' => function (ElementTypes $choice) {
return new StaticMessage(
$this->translator->trans($choice->getDefaultLabelKey()) . ' (' . $this->translator->trans($choice->getDefaultPluralLabelKey()) . ')'
);
},
'row_attr' => ['class' => 'mb-0'],
'attr' => ['class' => 'form-select-sm'],
'preferred_choices' => self::PREFERRED_TYPES
])
->add('locale', LocaleType::class, [
'label' => false,
'required' => true,
// Restrict to languages configured in the language menu: disable ChoiceLoader and provide explicit choices
'choice_loader' => null,
'choices' => $this->buildLocaleChoices(true),
'preferred_choices' => $this->getPreferredLocales(),
'constraints' => [
new Assert\NotBlank(),
],
'row_attr' => ['class' => 'mb-0'],
'attr' => ['class' => 'form-select-sm']
])
->add('translation_singular', TextType::class, [
'label' => false,
'required' => true,
'empty_data' => '',
'constraints' => [
new Assert\NotBlank(),
],
'row_attr' => ['class' => 'mb-0'],
'attr' => ['class' => 'form-select-sm']
])
->add('translation_plural', TextType::class, [
'label' => false,
'required' => true,
'empty_data' => '',
'constraints' => [
new Assert\NotBlank(),
],
'row_attr' => ['class' => 'mb-0'],
'attr' => ['class' => 'form-select-sm']
]);
}
/**
* Returns only locales configured in the language menu (settings) or falls back to the parameter.
* Format: ['German (DE)' => 'de', ...]
*/
private function buildLocaleChoices(bool $returnPossible = false): array
{
$locales = $this->getPreferredLocales();
if ($returnPossible) {
$locales = $this->getPossibleLocales();
}
$choices = [];
foreach ($locales as $code) {
$label = Locales::getName($code);
$choices[$label . ' (' . strtoupper($code) . ')'] = $code;
}
return $choices;
}
/**
* Source of allowed locales:
* 1) LocalizationSettings->languageMenuEntries (if set)
* 2) Fallback: parameter partdb.locale_menu
*/
private function getPreferredLocales(): array
{
$fromSettings = $this->localizationSettings->languageMenuEntries ?? [];
return !empty($fromSettings) ? array_values($fromSettings) : array_values($this->preferredLanguagesParam);
}
private function getPossibleLocales(): array
{
return array_values($this->preferredLanguagesParam);
}
}

View file

@ -0,0 +1,223 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2025 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace App\Form\Settings;
use App\Services\ElementTypes;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Intl\Locales;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* Flat collection of translation rows.
* View data: list [{dataSource, locale, translation_singular, translation_plural}, ...]
* Model data: same structure (list). Optionally expands a nested map to a list.
*/
class TypeSynonymsCollectionType extends AbstractType
{
public function __construct(private readonly TranslatorInterface $translator)
{
}
private function flattenStructure(array $modelValue): array
{
//If the model is already flattened, return as is
if (array_is_list($modelValue)) {
return $modelValue;
}
$out = [];
foreach ($modelValue as $dataSource => $locales) {
if (!is_array($locales)) {
continue;
}
foreach ($locales as $locale => $translations) {
if (!is_array($translations)) {
continue;
}
$out[] = [
//Convert string to enum value
'dataSource' => ElementTypes::from($dataSource),
'locale' => $locale,
'translation_singular' => $translations['singular'] ?? '',
'translation_plural' => $translations['plural'] ?? '',
];
}
}
return $out;
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event): void {
//Flatten the structure
$data = $event->getData();
$event->setData($this->flattenStructure($data));
});
$builder->addModelTransformer(new CallbackTransformer(
// Model -> View
$this->flattenStructure(...),
// View -> Model (keep list; let existing behavior unchanged)
function (array $viewValue) {
//Turn our flat list back into the structured array
$out = [];
foreach ($viewValue as $row) {
if (!is_array($row)) {
continue;
}
$dataSource = $row['dataSource'] ?? null;
$locale = $row['locale'] ?? null;
$translation_singular = $row['translation_singular'] ?? null;
$translation_plural = $row['translation_plural'] ?? null;
if ($dataSource === null ||
!is_string($locale) || $locale === ''
) {
continue;
}
$out[$dataSource->value][$locale] = [
'singular' => is_string($translation_singular) ? $translation_singular : '',
'plural' => is_string($translation_plural) ? $translation_plural : '',
];
}
return $out;
}
));
// Validation and normalization (duplicates + sorting) during SUBMIT
$builder->addEventListener(FormEvents::SUBMIT, function (FormEvent $event): void {
$form = $event->getForm();
$rows = $event->getData();
if (!is_array($rows)) {
return;
}
// Duplicate check: (dataSource, locale) must be unique
$seen = [];
$hasDuplicate = false;
foreach ($rows as $idx => $row) {
if (!is_array($row)) {
continue;
}
$ds = $row['dataSource'] ?? null;
$loc = $row['locale'] ?? null;
if ($ds !== null && is_string($loc) && $loc !== '') {
$key = $ds->value . '|' . $loc;
if (isset($seen[$key])) {
$hasDuplicate = true;
if ($form->has((string)$idx)) {
$child = $form->get((string)$idx);
if ($child->has('dataSource')) {
$child->get('dataSource')->addError(
new FormError($this->translator->trans(
'settings.synonyms.type_synonyms.collection_type.duplicate',
[], 'validators'
))
);
}
if ($child->has('locale')) {
$child->get('locale')->addError(
new FormError($this->translator->trans(
'settings.synonyms.type_synonyms.collection_type.duplicate',
[], 'validators'
))
);
}
}
} else {
$seen[$key] = true;
}
}
}
if ($hasDuplicate) {
return;
}
// Overall sort: first by dataSource key, then by localized language name
$sortable = $rows;
usort($sortable, static function ($a, $b) {
$aDs = $a['dataSource']->value ?? '';
$bDs = $b['dataSource']->value ?? '';
$cmpDs = strcasecmp($aDs, $bDs);
if ($cmpDs !== 0) {
return $cmpDs;
}
$aLoc = (string)($a['locale'] ?? '');
$bLoc = (string)($b['locale'] ?? '');
$aName = Locales::getName($aLoc);
$bName = Locales::getName($bLoc);
return strcasecmp($aName, $bName);
});
$event->setData($sortable);
});
}
public function configureOptions(OptionsResolver $resolver): void
{
// Defaults for the collection and entry type
$resolver->setDefaults([
'entry_type' => TypeSynonymRowType::class,
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
'required' => false,
'prototype' => true,
'empty_data' => [],
'entry_options' => ['label' => false],
]);
}
public function getParent(): ?string
{
return CollectionType::class;
}
public function getBlockPrefix(): string
{
return 'type_synonyms_collection';
}
}

View file

@ -30,7 +30,6 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* A locale select field that uses the preferred languages from the configuration.
*/
class LocaleSelectType extends AbstractType
{

View file

@ -111,7 +111,9 @@ class StructuralEntityType extends AbstractType
$resolver->setDefault('help', fn(Options $options) => $this->dtoText($options['dto_value']));
$resolver->setDefault('help_html', fn(Options $options) => $options['dto_value'] !== null);
$resolver->setDefault('attr', function (Options $options) {
//Normalize the attr to merge custom attributes
$resolver->setNormalizer('attr', function (Options $options, $value) {
$tmp = [
'data-controller' => $options['controller'],
'data-allow-add' => $options['allow_add'] ? 'true' : 'false',
@ -121,7 +123,7 @@ class StructuralEntityType extends AbstractType
$tmp['data-empty-message'] = $options['empty_message'];
}
return $tmp;
return array_merge($tmp, $value);
});
}

View file

@ -22,17 +22,35 @@ declare(strict_types=1);
namespace App\Repository;
use App\Entity\Parts\Category;
use App\Entity\Parts\Part;
use App\Entity\Parts\PartLot;
use App\Settings\MiscSettings\IpnSuggestSettings;
use Doctrine\ORM\NonUniqueResultException;
use Doctrine\ORM\NoResultException;
use Doctrine\ORM\QueryBuilder;
use Symfony\Contracts\Translation\TranslatorInterface;
use Doctrine\ORM\EntityManagerInterface;
/**
* @extends NamedDBElementRepository<Part>
*/
class PartRepository extends NamedDBElementRepository
{
private TranslatorInterface $translator;
private IpnSuggestSettings $ipnSuggestSettings;
public function __construct(
EntityManagerInterface $em,
TranslatorInterface $translator,
IpnSuggestSettings $ipnSuggestSettings,
) {
parent::__construct($em, $em->getClassMetadata(Part::class));
$this->translator = $translator;
$this->ipnSuggestSettings = $ipnSuggestSettings;
}
/**
* Gets the summed up instock of all parts (only parts without a measurement unit).
*
@ -84,8 +102,7 @@ class PartRepository extends NamedDBElementRepository
->where('ILIKE(part.name, :query) = TRUE')
->orWhere('ILIKE(part.description, :query) = TRUE')
->orWhere('ILIKE(category.name, :query) = TRUE')
->orWhere('ILIKE(footprint.name, :query) = TRUE')
;
->orWhere('ILIKE(footprint.name, :query) = TRUE');
$qb->setParameter('query', '%'.$query.'%');
@ -94,4 +111,282 @@ class PartRepository extends NamedDBElementRepository
return $qb->getQuery()->getResult();
}
/**
* Provides IPN (Internal Part Number) suggestions for a given part based on its category, description,
* and configured autocomplete digit length.
*
* This function generates suggestions for common prefixes and incremented prefixes based on
* the part's current category and its hierarchy. If the part is unsaved, a default "n.a." prefix is returned.
*
* @param Part $part The part for which autocomplete suggestions are generated.
* @param string $description description to assist in generating suggestions.
* @param int $suggestPartDigits The number of digits used in autocomplete increments.
*
* @return array An associative array containing the following keys:
* - 'commonPrefixes': List of common prefixes found for the part.
* - 'prefixesPartIncrement': Increments for the generated prefixes, including hierarchical prefixes.
*/
public function autoCompleteIpn(Part $part, string $description, int $suggestPartDigits): array
{
$category = $part->getCategory();
$ipnSuggestions = ['commonPrefixes' => [], 'prefixesPartIncrement' => []];
//Show global prefix first if configured
if ($this->ipnSuggestSettings->globalPrefix !== null && $this->ipnSuggestSettings->globalPrefix !== '') {
$ipnSuggestions['commonPrefixes'][] = [
'title' => $this->ipnSuggestSettings->globalPrefix,
'description' => $this->translator->trans('part.edit.tab.advanced.ipn.prefix.global_prefix')
];
$increment = $this->generateNextPossibleGlobalIncrement();
$ipnSuggestions['prefixesPartIncrement'][] = [
'title' => $this->ipnSuggestSettings->globalPrefix . $increment,
'description' => $this->translator->trans('part.edit.tab.advanced.ipn.prefix.global_prefix')
];
}
if (strlen($description) > 150) {
$description = substr($description, 0, 150);
}
if ($description !== '' && $this->ipnSuggestSettings->useDuplicateDescription) {
// Check if the description is already used in another part,
$suggestionByDescription = $this->getIpnSuggestByDescription($description);
if ($suggestionByDescription !== null && $suggestionByDescription !== $part->getIpn() && $part->getIpn() !== null && $part->getIpn() !== '') {
$ipnSuggestions['prefixesPartIncrement'][] = [
'title' => $part->getIpn(),
'description' => $this->translator->trans('part.edit.tab.advanced.ipn.prefix.description.current-increment')
];
}
if ($suggestionByDescription !== null) {
$ipnSuggestions['prefixesPartIncrement'][] = [
'title' => $suggestionByDescription,
'description' => $this->translator->trans('part.edit.tab.advanced.ipn.prefix.description.increment')
];
}
}
// Validate the category and ensure it's an instance of Category
if ($category instanceof Category) {
$currentPath = $category->getPartIpnPrefix();
$directIpnPrefixEmpty = $category->getPartIpnPrefix() === '';
$currentPath = $currentPath === '' ? $this->ipnSuggestSettings->fallbackPrefix : $currentPath;
$increment = $this->generateNextPossiblePartIncrement($currentPath, $part, $suggestPartDigits);
$ipnSuggestions['commonPrefixes'][] = [
'title' => $currentPath . $this->ipnSuggestSettings->numberSeparator,
'description' => $directIpnPrefixEmpty ? $this->translator->trans('part.edit.tab.advanced.ipn.prefix_empty.direct_category', ['%name%' => $category->getName()]) : $this->translator->trans('part.edit.tab.advanced.ipn.prefix.direct_category')
];
$ipnSuggestions['prefixesPartIncrement'][] = [
'title' => $currentPath . $this->ipnSuggestSettings->numberSeparator . $increment,
'description' => $directIpnPrefixEmpty ? $this->translator->trans('part.edit.tab.advanced.ipn.prefix_empty.direct_category', ['%name%' => $category->getName()]) : $this->translator->trans('part.edit.tab.advanced.ipn.prefix.direct_category.increment')
];
// Process parent categories
$parentCategory = $category->getParent();
while ($parentCategory instanceof Category) {
// Prepend the parent category's prefix to the current path
$effectiveIPNPrefix = $parentCategory->getPartIpnPrefix() === '' ? $this->ipnSuggestSettings->fallbackPrefix : $parentCategory->getPartIpnPrefix();
$currentPath = $effectiveIPNPrefix . $this->ipnSuggestSettings->categorySeparator . $currentPath;
$ipnSuggestions['commonPrefixes'][] = [
'title' => $currentPath . $this->ipnSuggestSettings->numberSeparator,
'description' => $this->translator->trans('part.edit.tab.advanced.ipn.prefix.hierarchical.no_increment')
];
$increment = $this->generateNextPossiblePartIncrement($currentPath, $part, $suggestPartDigits);
$ipnSuggestions['prefixesPartIncrement'][] = [
'title' => $currentPath . $this->ipnSuggestSettings->numberSeparator . $increment,
'description' => $this->translator->trans('part.edit.tab.advanced.ipn.prefix.hierarchical.increment')
];
// Move to the next parent category
$parentCategory = $parentCategory->getParent();
}
} elseif ($part->getID() === null) {
$ipnSuggestions['commonPrefixes'][] = [
'title' => $this->ipnSuggestSettings->fallbackPrefix,
'description' => $this->translator->trans('part.edit.tab.advanced.ipn.prefix.not_saved')
];
}
return $ipnSuggestions;
}
/**
* Suggests the next IPN (Internal Part Number) based on the provided part description.
*
* Searches for parts with similar descriptions and retrieves their existing IPNs to calculate the next suggestion.
* Returns null if the description is empty or no suggestion can be generated.
*
* @param string $description The part description to search for.
*
* @return string|null The suggested IPN, or null if no suggestion is possible.
*
* @throws NonUniqueResultException
*/
public function getIpnSuggestByDescription(string $description): ?string
{
if ($description === '') {
return null;
}
$qb = $this->createQueryBuilder('part');
$qb->select('part')
->where('part.description LIKE :descriptionPattern')
->setParameter('descriptionPattern', $description.'%')
->orderBy('part.id', 'ASC');
$partsBySameDescription = $qb->getQuery()->getResult();
$givenIpnsWithSameDescription = [];
foreach ($partsBySameDescription as $part) {
if ($part->getIpn() === null || $part->getIpn() === '') {
continue;
}
$givenIpnsWithSameDescription[] = $part->getIpn();
}
return $this->getNextIpnSuggestion($givenIpnsWithSameDescription);
}
private function generateNextPossibleGlobalIncrement(): string
{
$qb = $this->createQueryBuilder('part');
$qb->select('part.ipn')
->where('REGEXP(part.ipn, :ipnPattern) = TRUE')
->setParameter('ipnPattern', '^' . preg_quote($this->ipnSuggestSettings->globalPrefix, '/') . '\d+$')
->orderBy('NATSORT(part.ipn)', 'DESC')
->setMaxResults(1)
;
$highestIPN = $qb->getQuery()->getOneOrNullResult();
if ($highestIPN !== null) {
//Remove the prefix and extract the increment part
$incrementPart = substr($highestIPN['ipn'], strlen($this->ipnSuggestSettings->globalPrefix));
//Extract a number using regex
preg_match('/(\d+)$/', $incrementPart, $matches);
$incrementInt = isset($matches[1]) ? (int) $matches[1] + 1 : 0;
} else {
$incrementInt = 1;
}
return str_pad((string) $incrementInt, $this->ipnSuggestSettings->suggestPartDigits, '0', STR_PAD_LEFT);
}
/**
* Generates the next possible increment for a part within a given category, while ensuring uniqueness.
*
* This method calculates the next available increment for a part's identifier (`ipn`) based on the current path
* and the number of digits specified for the autocomplete feature. It ensures that the generated identifier
* aligns with the expected length and does not conflict with already existing identifiers in the same category.
*
* @param string $currentPath The base path or prefix for the part's identifier.
* @param Part $currentPart The part entity for which the increment is being generated.
* @param int $suggestPartDigits The number of digits reserved for the increment.
*
* @return string The next possible increment as a zero-padded string.
*
* @throws NonUniqueResultException If the query returns non-unique results.
* @throws NoResultException If the query fails to return a result.
*/
private function generateNextPossiblePartIncrement(string $currentPath, Part $currentPart, int $suggestPartDigits): string
{
$qb = $this->createQueryBuilder('part');
$expectedLength = strlen($currentPath) + strlen($this->ipnSuggestSettings->categorySeparator) + $suggestPartDigits; // Path + '-' + $suggestPartDigits digits
// Fetch all parts in the given category, sorted by their ID in ascending order
$qb->select('part')
->where('part.ipn LIKE :ipnPattern')
->andWhere('LENGTH(part.ipn) = :expectedLength')
->setParameter('ipnPattern', $currentPath . '%')
->setParameter('expectedLength', $expectedLength)
->orderBy('part.id', 'ASC');
$parts = $qb->getQuery()->getResult();
// Collect all used increments in the category
$usedIncrements = [];
foreach ($parts as $part) {
if ($part->getIpn() === null || $part->getIpn() === '') {
continue;
}
if ($part->getId() === $currentPart->getId() && $currentPart->getID() !== null) {
// Extract and return the current part's increment directly
$incrementPart = substr($part->getIpn(), -$suggestPartDigits);
if (is_numeric($incrementPart)) {
return str_pad((string) $incrementPart, $suggestPartDigits, '0', STR_PAD_LEFT);
}
}
// Extract last $autocompletePartDigits digits for possible available part increment
$incrementPart = substr($part->getIpn(), -$suggestPartDigits);
if (is_numeric($incrementPart)) {
$usedIncrements[] = (int) $incrementPart;
}
}
// Generate the next free $autocompletePartDigits-digit increment
$nextIncrement = 1; // Start at the beginning
while (in_array($nextIncrement, $usedIncrements, true)) {
$nextIncrement++;
}
return str_pad((string) $nextIncrement, $suggestPartDigits, '0', STR_PAD_LEFT);
}
/**
* Generates the next IPN suggestion based on the maximum numeric suffix found in the given IPNs.
*
* The new IPN is constructed using the base format of the first provided IPN,
* incremented by the next free numeric suffix. If no base IPNs are found,
* returns null.
*
* @param array $givenIpns List of IPNs to analyze.
*
* @return string|null The next suggested IPN, or null if no base IPNs can be derived.
*/
private function getNextIpnSuggestion(array $givenIpns): ?string {
$maxSuffix = 0;
foreach ($givenIpns as $ipn) {
// Check whether the IPN contains a suffix "_ <number>"
if (preg_match('/_(\d+)$/', $ipn, $matches)) {
$suffix = (int)$matches[1];
if ($suffix > $maxSuffix) {
$maxSuffix = $suffix; // Höchste Nummer speichern
}
}
}
// Find the basic format (the IPN without suffix) from the first IPN
$baseIpn = $givenIpns[0] ?? '';
$baseIpn = preg_replace('/_\d+$/', '', $baseIpn); // Remove existing "_ <number>"
if ($baseIpn === '') {
return null;
}
// Generate next free possible IPN
return $baseIpn . '_' . ($maxSuffix + 1);
}
}

View file

@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Repository\Parts;
use App\Entity\Parts\PartCustomState;
use App\Repository\AbstractPartsContainingRepository;
use InvalidArgumentException;
class PartCustomStateRepository extends AbstractPartsContainingRepository
{
public function getParts(object $element, string $nameOrderDirection = "ASC"): array
{
if (!$element instanceof PartCustomState) {
throw new InvalidArgumentException('$element must be an PartCustomState!');
}
return $this->getPartsByField($element, $nameOrderDirection, 'partUnit');
}
public function getPartsCount(object $element): int
{
if (!$element instanceof PartCustomState) {
throw new InvalidArgumentException('$element must be an PartCustomState!');
}
return $this->getPartsCountByField($element, 'partUnit');
}
}

View file

@ -22,6 +22,7 @@ declare(strict_types=1);
namespace App\Security\Voter;
use App\Entity\Attachments\PartCustomStateAttachment;
use App\Services\UserSystem\VoterHelper;
use Symfony\Bundle\SecurityBundle\Security;
use App\Entity\Attachments\AttachmentContainingDBElement;
@ -99,6 +100,8 @@ final class AttachmentVoter extends Voter
$param = 'measurement_units';
} elseif (is_a($subject, PartAttachment::class, true)) {
$param = 'parts';
} elseif (is_a($subject, PartCustomStateAttachment::class, true)) {
$param = 'part_custom_states';
} elseif (is_a($subject, StorageLocationAttachment::class, true)) {
$param = 'storelocations';
} elseif (is_a($subject, SupplierAttachment::class, true)) {

View file

@ -22,6 +22,7 @@ declare(strict_types=1);
*/
namespace App\Security\Voter;
use App\Entity\Parameters\PartCustomStateParameter;
use App\Services\UserSystem\VoterHelper;
use Symfony\Bundle\SecurityBundle\Security;
use App\Entity\Base\AbstractDBElement;
@ -97,6 +98,8 @@ final class ParameterVoter extends Voter
$param = 'measurement_units';
} elseif (is_a($subject, PartParameter::class, true)) {
$param = 'parts';
} elseif (is_a($subject, PartCustomStateParameter::class, true)) {
$param = 'part_custom_states';
} elseif (is_a($subject, StorageLocationParameter::class, true)) {
$param = 'storelocations';
} elseif (is_a($subject, SupplierParameter::class, true)) {

View file

@ -23,6 +23,7 @@ declare(strict_types=1);
namespace App\Security\Voter;
use App\Entity\Attachments\AttachmentType;
use App\Entity\Parts\PartCustomState;
use App\Entity\ProjectSystem\Project;
use App\Entity\Parts\Category;
use App\Entity\Parts\Footprint;
@ -53,6 +54,7 @@ final class StructureVoter extends Voter
Supplier::class => 'suppliers',
Currency::class => 'currencies',
MeasurementUnit::class => 'measurement_units',
PartCustomState::class => 'part_custom_states',
];
public function __construct(private readonly VoterHelper $helper)

View file

@ -30,6 +30,7 @@ use App\Entity\Attachments\AttachmentUpload;
use App\Entity\Attachments\CategoryAttachment;
use App\Entity\Attachments\CurrencyAttachment;
use App\Entity\Attachments\LabelAttachment;
use App\Entity\Attachments\PartCustomStateAttachment;
use App\Entity\Attachments\ProjectAttachment;
use App\Entity\Attachments\FootprintAttachment;
use App\Entity\Attachments\GroupAttachment;
@ -80,6 +81,7 @@ class AttachmentSubmitHandler
//The mapping used to determine which folder will be used for an attachment type
$this->folder_mapping = [
PartAttachment::class => 'part',
PartCustomStateAttachment::class => 'part_custom_state',
AttachmentTypeAttachment::class => 'attachment_type',
CategoryAttachment::class => 'category',
CurrencyAttachment::class => 'currency',

View file

@ -23,6 +23,7 @@ declare(strict_types=1);
namespace App\Services\Attachments;
use App\Entity\Parts\Footprint;
use App\Entity\Parts\PartCustomState;
use App\Entity\ProjectSystem\Project;
use App\Entity\Parts\Category;
use App\Entity\Parts\StorageLocation;

View file

@ -233,6 +233,10 @@ class KiCadHelper
}
$result["fields"]["Part-DB Unit"] = $this->createField($unit);
}
if ($part->getPartCustomState() !== null) {
$customState = $part->getPartCustomState()->getName();
$result["fields"]["Part-DB Custom state"] = $this->createField($customState);
}
if ($part->getMass()) {
$result["fields"]["Mass"] = $this->createField($part->getMass() . ' g');
}

View file

@ -24,66 +24,31 @@ namespace App\Services;
use App\Entity\Attachments\Attachment;
use App\Entity\Attachments\AttachmentContainingDBElement;
use App\Entity\Attachments\AttachmentType;
use App\Entity\Base\AbstractDBElement;
use App\Entity\Contracts\NamedElementInterface;
use App\Entity\InfoProviderSystem\BulkInfoProviderImportJob;
use App\Entity\InfoProviderSystem\BulkInfoProviderImportJobPart;
use App\Entity\LabelSystem\LabelProfile;
use App\Entity\Parameters\AbstractParameter;
use App\Entity\Parts\Category;
use App\Entity\Parts\Footprint;
use App\Entity\Parts\Manufacturer;
use App\Entity\Parts\MeasurementUnit;
use App\Entity\Parts\Part;
use App\Entity\Parts\PartAssociation;
use App\Entity\Parts\PartLot;
use App\Entity\Parts\StorageLocation;
use App\Entity\Parts\Supplier;
use App\Entity\PriceInformations\Currency;
use App\Entity\PriceInformations\Orderdetail;
use App\Entity\PriceInformations\Pricedetail;
use App\Entity\ProjectSystem\Project;
use App\Entity\ProjectSystem\ProjectBOMEntry;
use App\Entity\UserSystem\Group;
use App\Entity\UserSystem\User;
use App\Exceptions\EntityNotSupportedException;
use App\Settings\SynonymSettings;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @see \App\Tests\Services\ElementTypeNameGeneratorTest
*/
class ElementTypeNameGenerator
final readonly class ElementTypeNameGenerator
{
protected array $mapping;
public function __construct(protected TranslatorInterface $translator, private readonly EntityURLGenerator $entityURLGenerator)
public function __construct(
private TranslatorInterface $translator,
private EntityURLGenerator $entityURLGenerator,
private SynonymSettings $synonymsSettings,
)
{
//Child classes has to become before parent classes
$this->mapping = [
Attachment::class => $this->translator->trans('attachment.label'),
Category::class => $this->translator->trans('category.label'),
AttachmentType::class => $this->translator->trans('attachment_type.label'),
Project::class => $this->translator->trans('project.label'),
ProjectBOMEntry::class => $this->translator->trans('project_bom_entry.label'),
Footprint::class => $this->translator->trans('footprint.label'),
Manufacturer::class => $this->translator->trans('manufacturer.label'),
MeasurementUnit::class => $this->translator->trans('measurement_unit.label'),
Part::class => $this->translator->trans('part.label'),
PartLot::class => $this->translator->trans('part_lot.label'),
StorageLocation::class => $this->translator->trans('storelocation.label'),
Supplier::class => $this->translator->trans('supplier.label'),
Currency::class => $this->translator->trans('currency.label'),
Orderdetail::class => $this->translator->trans('orderdetail.label'),
Pricedetail::class => $this->translator->trans('pricedetail.label'),
Group::class => $this->translator->trans('group.label'),
User::class => $this->translator->trans('user.label'),
AbstractParameter::class => $this->translator->trans('parameter.label'),
LabelProfile::class => $this->translator->trans('label_profile.label'),
PartAssociation::class => $this->translator->trans('part_association.label'),
BulkInfoProviderImportJob::class => $this->translator->trans('bulk_info_provider_import_job.label'),
BulkInfoProviderImportJobPart::class => $this->translator->trans('bulk_info_provider_import_job_part.label'),
];
}
/**
@ -97,27 +62,69 @@ class ElementTypeNameGenerator
* @return string the localized label for the entity type
*
* @throws EntityNotSupportedException when the passed entity is not supported
* @deprecated Use label() instead
*/
public function getLocalizedTypeLabel(object|string $entity): string
{
$class = is_string($entity) ? $entity : $entity::class;
//Check if we have a direct array entry for our entity class, then we can use it
if (isset($this->mapping[$class])) {
return $this->mapping[$class];
}
//Otherwise iterate over array and check for inheritance (needed when the proxy element from doctrine are passed)
foreach ($this->mapping as $class_to_check => $translation) {
if (is_a($entity, $class_to_check, true)) {
return $translation;
}
}
//When nothing was found throw an exception
throw new EntityNotSupportedException(sprintf('No localized label for the element with type %s was found!', is_object($entity) ? $entity::class : (string) $entity));
return $this->typeLabel($entity);
}
private function resolveSynonymLabel(ElementTypes $type, ?string $locale, bool $plural): ?string
{
$locale ??= $this->translator->getLocale();
if ($this->synonymsSettings->isSynonymDefinedForType($type)) {
if ($plural) {
$syn = $this->synonymsSettings->getPluralSynonymForType($type, $locale);
} else {
$syn = $this->synonymsSettings->getSingularSynonymForType($type, $locale);
}
if ($syn === null) {
//Try to fall back to english
if ($plural) {
$syn = $this->synonymsSettings->getPluralSynonymForType($type, 'en');
} else {
$syn = $this->synonymsSettings->getSingularSynonymForType($type, 'en');
}
}
return $syn;
}
return null;
}
/**
* Gets a localized label for the type of the entity. If user defined synonyms are defined,
* these are used instead of the default labels.
* @param object|string $entity
* @param string|null $locale
* @return string
*/
public function typeLabel(object|string $entity, ?string $locale = null): string
{
$type = ElementTypes::fromValue($entity);
return $this->resolveSynonymLabel($type, $locale, false)
?? $this->translator->trans($type->getDefaultLabelKey(), locale: $locale);
}
/**
* Similar to label(), but returns the plural version of the label.
* @param object|string $entity
* @param string|null $locale
* @return string
*/
public function typeLabelPlural(object|string $entity, ?string $locale = null): string
{
$type = ElementTypes::fromValue($entity);
return $this->resolveSynonymLabel($type, $locale, true)
?? $this->translator->trans($type->getDefaultPluralLabelKey(), locale: $locale);
}
/**
* Returns a string like in the format ElementType: ElementName.
* For example this could be something like: "Part: BC547".
@ -132,7 +139,7 @@ class ElementTypeNameGenerator
*/
public function getTypeNameCombination(NamedElementInterface $entity, bool $use_html = false): string
{
$type = $this->getLocalizedTypeLabel($entity);
$type = $this->typeLabel($entity);
if ($use_html) {
return '<i>' . $type . ':</i> ' . htmlspecialchars($entity->getName());
}
@ -142,7 +149,7 @@ class ElementTypeNameGenerator
/**
* Returns a HTML formatted label for the given enitity in the format "Type: Name" (on elements with a name) and
* Returns a HTML formatted label for the given entity in the format "Type: Name" (on elements with a name) and
* "Type: ID" (on elements without a name). If possible the value is given as a link to the element.
* @param AbstractDBElement $entity The entity for which the label should be generated
* @param bool $include_associated If set to true, the associated entity (like the part belonging to a part lot) is included in the label to give further information
@ -163,7 +170,7 @@ class ElementTypeNameGenerator
} else { //Target does not have a name
$tmp = sprintf(
'<i>%s</i>: %s',
$this->getLocalizedTypeLabel($entity),
$this->typeLabel($entity),
$entity->getID()
);
}
@ -207,7 +214,7 @@ class ElementTypeNameGenerator
{
return sprintf(
'<i>%s</i>: %s [%s]',
$this->getLocalizedTypeLabel($class),
$this->typeLabel($class),
$id,
$this->translator->trans('log.target_deleted')
);

Some files were not shown because too many files have changed in this diff Show more