Compare commits

...

39 commits

Author SHA1 Message Date
Jan Böhmer
523b07ef2f
Bump version from 2.13.0 to 2.13.1
Some checks failed
Build assets artifact / Build assets artifact (push) Has been cancelled
Docker Image Build / build (linux/amd64, amd64, ubuntu-latest) (push) Has been cancelled
Docker Image Build / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Has been cancelled
Docker Image Build / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Has been cancelled
Docker Image Build (FrankenPHP) / build (linux/amd64, amd64, ubuntu-latest) (push) Has been cancelled
Docker Image Build (FrankenPHP) / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Has been cancelled
Docker Image Build (FrankenPHP) / build (linux/arm64, arm64, ubuntu-24.04-arm) (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
Docker Image Build / merge (push) Has been cancelled
Docker Image Build (FrankenPHP) / merge (push) Has been cancelled
Just an empty release to get the actions run correctly on tagging.
Fixes issue #1430
2026-06-29 17:01:04 +02:00
Jan Böhmer
ffe2f8004b
New Crowdin updates (#1419)
* New translations frontend.en.xlf (English)

[ci skip]

* New translations messages.en.xlf (English)

[ci skip]

* New translations frontend.en.xlf (English)

[ci skip]

* New translations messages.en.xlf (English)

[ci skip]

* New translations messages.en.xlf (English)

[ci skip]

* New translations messages.en.xlf (Chinese Simplified)

[ci skip]

* New translations validators.en.xlf (Chinese Simplified)

[ci skip]

* New translations security.en.xlf (Chinese Simplified)

[ci skip]

* New translations frontend.en.xlf (Chinese Simplified)

[ci skip]

* New translations messages.en.xlf (English)

[ci skip]

* New translations validators.en.xlf (English)

[ci skip]

* New translations messages.en.xlf (German)

[ci skip]

* New translations validators.en.xlf (German)

[ci skip]

* New translations frontend.en.xlf (German)

[ci skip]
2026-06-29 00:12:20 +02:00
Jan Böhmer
7244803937 Fixed phpstan error 2026-06-29 00:08:44 +02:00
Jan Böhmer
214646caa6 Bumped version to 2.13.0 2026-06-28 23:58:38 +02:00
Jan Böhmer
ffcfdb793f Validate info provider references modified via part edit form 2026-06-28 23:56:55 +02:00
Jan Böhmer
e03eda84c5 Allow to edit info provider reference
Fixes   issue #1394
2026-06-28 23:47:49 +02:00
Jan Böhmer
4d3e2e28a5 Updated dependencies
Some checks are pending
Build assets artifact / Build assets artifact (push) Waiting to run
Docker Image Build / build (linux/amd64, amd64, ubuntu-latest) (push) Waiting to run
Docker Image Build / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build / merge (push) Blocked by required conditions
Docker Image Build (FrankenPHP) / build (linux/amd64, amd64, ubuntu-latest) (push) Waiting to run
Docker Image Build (FrankenPHP) / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build (FrankenPHP) / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build (FrankenPHP) / merge (push) Blocked by required conditions
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
2026-06-28 19:31:20 +02:00
dependabot[bot]
8ed14ca708
Bump actions/checkout from 6 to 7 (#1421)
Some checks failed
Build assets artifact / Build assets artifact (push) Has been cancelled
Docker Image Build / build (linux/amd64, amd64, ubuntu-latest) (push) Has been cancelled
Docker Image Build / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Has been cancelled
Docker Image Build / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Has been cancelled
Docker Image Build (FrankenPHP) / build (linux/amd64, amd64, ubuntu-latest) (push) Has been cancelled
Docker Image Build (FrankenPHP) / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Has been cancelled
Docker Image Build (FrankenPHP) / build (linux/arm64, arm64, ubuntu-24.04-arm) (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
Docker Image Build / merge (push) Has been cancelled
Docker Image Build (FrankenPHP) / merge (push) Has been cancelled
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [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/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  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>
2026-06-25 20:44:44 +02:00
Jan Böhmer
34257332ff
Update KiCad symbols and footprints lists (#1420)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-25 20:42:27 +02:00
0x915
2503e63bb2
Perform a full translation of Simplified Chinese. (#1426)
* Perform a full translation of Simplified Chinese.

Based on the latest *.en.xlf file.

Translated automatically using DeepSinkV4 based on the source code, 
then manually reviewed and corrected line by line.

* Normalized formatting

---------

Co-authored-by: Jan Böhmer <mail@jan-boehmer.de>
2026-06-25 20:41:55 +02:00
Jan Böhmer
0eba7121aa Merged subchildren operations on APIPlatform into one APIResource, as havinng multiple is deprecated 2026-06-25 20:16:44 +02:00
Jan Böhmer
188444b30f Fixed deprecations
Some checks are pending
Build assets artifact / Build assets artifact (push) Waiting to run
Docker Image Build / build (linux/amd64, amd64, ubuntu-latest) (push) Waiting to run
Docker Image Build / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build / merge (push) Blocked by required conditions
Docker Image Build (FrankenPHP) / build (linux/amd64, amd64, ubuntu-latest) (push) Waiting to run
Docker Image Build (FrankenPHP) / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build (FrankenPHP) / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build (FrankenPHP) / merge (push) Blocked by required conditions
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
2026-06-25 12:44:06 +02:00
Jan Böhmer
cd87b59c15 Fixed small deprecations 2026-06-25 12:37:22 +02:00
Jan Böhmer
b3895c1e91 Fixed depprecation of fluent config builders 2026-06-25 12:33:52 +02:00
Jan Böhmer
23e22b19e2 Removed usesless easy_log_handler file 2026-06-25 12:27:37 +02:00
Jan Böhmer
9c919a9be9 Moved from phpunit annotation to attribute 2026-06-25 12:12:34 +02:00
Jan Böhmer
eb7da91c44 Fixed phpstan issue 2026-06-25 12:09:06 +02:00
Jan Böhmer
b8fc5d4ace Warn if ProjectBuild passed lot if null
Fixed caused deprecations for it
2026-06-25 12:07:54 +02:00
Jan Böhmer
fe0809230b Fixed sqlite deprecation on PHP8.5 2026-06-25 11:59:37 +02:00
Jan Böhmer
9d4dabbd20 Removed useless setAccessible() calls
They are noop since 8.1 and we only support 8.2+
2026-06-25 10:54:13 +02:00
Jan Böhmer
5e18ae2874 Fixed problem with changing last_stocktake date, when editing part
Some checks are pending
Build assets artifact / Build assets artifact (push) Waiting to run
Docker Image Build / build (linux/amd64, amd64, ubuntu-latest) (push) Waiting to run
Docker Image Build / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build / merge (push) Blocked by required conditions
Docker Image Build (FrankenPHP) / build (linux/amd64, amd64, ubuntu-latest) (push) Waiting to run
Docker Image Build (FrankenPHP) / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build (FrankenPHP) / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build (FrankenPHP) / merge (push) Blocked by required conditions
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
The database has UTC values, we need to set that in the form. A form extension was added to ensure that this does not happen again. Also an issue was fixed that the seconds were cut off. This fixes issue #1390
2026-06-25 00:08:57 +02:00
Jan Böhmer
3e725dd2ec Increased timeout for local AI inferences, and made AI timeout configurable per provider
Some checks failed
Build assets artifact / Build assets artifact (push) Has been cancelled
Docker Image Build / build (linux/amd64, amd64, ubuntu-latest) (push) Has been cancelled
Docker Image Build / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Has been cancelled
Docker Image Build / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Has been cancelled
Docker Image Build (FrankenPHP) / build (linux/amd64, amd64, ubuntu-latest) (push) Has been cancelled
Docker Image Build (FrankenPHP) / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Has been cancelled
Docker Image Build (FrankenPHP) / build (linux/arm64, arm64, ubuntu-24.04-arm) (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
Docker Image Build / merge (push) Has been cancelled
Docker Image Build (FrankenPHP) / merge (push) Has been cancelled
Fixes issue #1396
2026-06-22 22:06:30 +02:00
Jan Böhmer
ec80115d0a Renamed AI response schema from errornous "clock" to "part_detail" 2026-06-21 23:10:44 +02:00
Jan Böhmer
b60887c71d Added ollama as AI provider
Some checks are pending
Build assets artifact / Build assets artifact (push) Waiting to run
Docker Image Build / build (linux/amd64, amd64, ubuntu-latest) (push) Waiting to run
Docker Image Build / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build / merge (push) Blocked by required conditions
Docker Image Build (FrankenPHP) / build (linux/amd64, amd64, ubuntu-latest) (push) Waiting to run
Docker Image Build (FrankenPHP) / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build (FrankenPHP) / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build (FrankenPHP) / merge (push) Blocked by required conditions
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
Unlike the LMStudio one it also features an API key and proper model auto suggestion
2026-06-21 20:41:24 +02:00
Jan Böhmer
9f686c88fe Updated symfony ai bundle to 0.10.0 2026-06-21 16:15:36 +02:00
Jan Böhmer
36244ec63f Added brite bootswatch theme
Some checks are pending
Build assets artifact / Build assets artifact (push) Waiting to run
Docker Image Build / build (linux/amd64, amd64, ubuntu-latest) (push) Waiting to run
Docker Image Build / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build / merge (push) Blocked by required conditions
Docker Image Build (FrankenPHP) / build (linux/amd64, amd64, ubuntu-latest) (push) Waiting to run
Docker Image Build (FrankenPHP) / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build (FrankenPHP) / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build (FrankenPHP) / merge (push) Blocked by required conditions
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
2026-06-21 16:10:38 +02:00
Jan Böhmer
f45e3a9ef8 Upgraded compression-webpack-plugin 2026-06-21 15:54:09 +02:00
Jan Böhmer
a46f1713fe Use the correct yarn urls 2026-06-21 15:52:52 +02:00
Jan Böhmer
22f23d9c82 Upgraded jquery to 4.0.0 2026-06-21 15:51:22 +02:00
Jan Böhmer
b4cf5b57fa Translate Swal buttons 2026-06-21 15:10:39 +02:00
Jan Böhmer
3491559e9f Migrated register_events from jquery to native JS 2026-06-21 14:52:30 +02:00
Jan Böhmer
b83fc73e18 Remove jquery command from error_handler.js 2026-06-21 14:46:15 +02:00
Jan Böhmer
8c88df4ecf Use upstream version of dataTables.select as the fix was merged 2026-06-21 14:41:56 +02:00
Jan Böhmer
176d5ad2b6 Improved page load error dialog
We now show a more user friendly message
2026-06-21 14:37:40 +02:00
Jan Böhmer
99e56c4b1d Moved alerts and dialogs from unsupported bootbox to Sweetalert2 library 2026-06-21 14:21:01 +02:00
Jan Böhmer
a489380f49 Merge remote-tracking branch 'origin/master' 2026-06-21 12:49:06 +02:00
Jan Böhmer
9127bcf25e Added it and pl translations for password estimator, use lvenshtein distance and block partdb word 2026-06-21 12:49:00 +02:00
Jan Böhmer
c3af73daae Use dictonaries for german and english words for password estimator 2026-06-21 12:37:31 +02:00
Jan Böhmer
7e90f6d707 Updated password strenght estimator to latest version and show crack time estimate as tooltip 2026-06-21 12:33:30 +02:00
101 changed files with 8929 additions and 5351 deletions

View file

@ -27,7 +27,7 @@ jobs:
APP_ENV: prod APP_ENV: prod
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- name: Setup PHP - name: Setup PHP
uses: shivammathur/setup-php@v2 uses: shivammathur/setup-php@v2

View file

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

View file

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

View file

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

View file

@ -46,7 +46,7 @@ jobs:
if: matrix.db-type == 'postgres' if: matrix.db-type == 'postgres'
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v7
- name: Setup PHP - name: Setup PHP
uses: shivammathur/setup-php@v2 uses: shivammathur/setup-php@v2

View file

@ -1 +1 @@
2.12.3 2.13.1

View file

@ -19,8 +19,7 @@
import {Controller} from "@hotwired/stimulus"; import {Controller} from "@hotwired/stimulus";
import {visit} from "@hotwired/turbo"; import {visit} from "@hotwired/turbo";
import * as bootbox from "bootbox"; import {ConfirmSwal} from "../../helpers/swal";
import "../../css/components/bootbox_extensions.css";
import "../../css/components/dirty_form.css"; import "../../css/components/dirty_form.css";
/** /**
@ -207,11 +206,10 @@ export default class extends Controller {
} }
_confirmNavigation(onConfirm) { _confirmNavigation(onConfirm) {
bootbox.confirm({ ConfirmSwal.fire({
title: this.confirmTitleValue, titleText: this.confirmTitleValue,
message: this.confirmMessageValue, text: this.confirmMessageValue,
callback: (result) => { if (result) onConfirm(); } }).then(({isConfirmed}) => { if (isConfirmed) onConfirm(); });
});
} }
_handleLinkClick(event) { _handleLinkClick(event) {

View file

@ -19,8 +19,7 @@
import {Controller} from "@hotwired/stimulus"; import {Controller} from "@hotwired/stimulus";
import * as bootbox from "bootbox"; import {AlertSwal, ConfirmSwal} from "../../helpers/swal";
import "../../css/components/bootbox_extensions.css";
import accept from "attr-accept"; import accept from "attr-accept";
export default class extends Controller { export default class extends Controller {
@ -62,7 +61,7 @@ export default class extends Controller {
if(!prototype) { if(!prototype) {
console.warn("Prototype is not set, we cannot create a new element. This is most likely due to missing permissions."); console.warn("Prototype is not set, we cannot create a new element. This is most likely due to missing permissions.");
bootbox.alert("You do not have the permissions to create a new element. (No protoype element is set)"); AlertSwal.fire({"text": "You do not have the permissions to create a new element. (No protoype element is set)"});
return; return;
} }
@ -226,8 +225,10 @@ export default class extends Controller {
} }
if(this.deleteMessageValue) { if(this.deleteMessageValue) {
bootbox.confirm(this.deleteMessageValue, (result) => { ConfirmSwal.fire({
if (result) { text: this.deleteMessageValue,
}).then(({isConfirmed}) => {
if (isConfirmed) {
del(); del();
} }
}); });

View file

@ -38,9 +38,7 @@ import 'datatables.net-colreorder-bs5';
import 'datatables.net-responsive-bs5'; import 'datatables.net-responsive-bs5';
import '../../../js/lib/datatables'; import '../../../js/lib/datatables';
//import 'datatables.net-select-bs5'; import 'datatables.net-select-bs5';
//Use the local version containing the fix for the select extension
import '../../../js/lib/dataTables.select.mjs';
const EVENT_DT_LOADED = 'dt:loaded'; const EVENT_DT_LOADED = 'dt:loaded';

View file

@ -20,7 +20,7 @@
import DatatablesController from "./datatables_controller.js"; import DatatablesController from "./datatables_controller.js";
import TomSelect from "tom-select"; import TomSelect from "tom-select";
import * as bootbox from "bootbox"; import {ConfirmSwal} from "../../../helpers/swal";
/** /**
* This is the datatables controller for parts lists * This is the datatables controller for parts lists
@ -146,16 +146,18 @@ export default class extends DatatablesController {
bubbles: true, //This line is important, otherwise Turbo will not receive the event bubbles: true, //This line is important, otherwise Turbo will not receive the event
}); });
const confirm = bootbox.confirm({ ConfirmSwal.fire({
message: message, title: title, callback: function (result) { titleText: title,
text: message,
icon: "warning"
}).then(({isConfirmed}) => {
//If the dialog was confirmed, then submit the form. //If the dialog was confirmed, then submit the form.
if (result) { if (isConfirmed) {
that._confirmed = true; that._confirmed = true;
form.dispatchEvent(that._our_event); form.dispatchEvent(that._our_event);
} else { } else {
that._confirmed = false; that._confirmed = false;
} }
}
}); });
} }
} }

View file

@ -19,8 +19,7 @@
import {Controller} from "@hotwired/stimulus"; import {Controller} from "@hotwired/stimulus";
import * as bootbox from "bootbox"; import {ConfirmSwal} from "../../helpers/swal";
import "../../css/components/bootbox_extensions.css";
export default class extends Controller export default class extends Controller
{ {
@ -48,10 +47,12 @@ export default class extends Controller
const submitter = event.submitter; const submitter = event.submitter;
const that = this; const that = this;
const confirm = bootbox.confirm({ ConfirmSwal.fire({
message: message, title: title, callback: function (result) { titleText: title,
html: message, //Message contains a <br> tag and no user injectable HTML
}).then(({isConfirmed}) => {
//If the dialog was confirmed, then submit the form. //If the dialog was confirmed, then submit the form.
if (result) { if (isConfirmed) {
//Set a flag to prevent the dialog from popping up again and allowing turbo to submit the form //Set a flag to prevent the dialog from popping up again and allowing turbo to submit the form
that._confirmed = true; that._confirmed = true;
@ -73,7 +74,6 @@ export default class extends Controller
} else { } else {
that._confirmed = false; that._confirmed = false;
} }
}
}); });
} }
} }

View file

@ -19,8 +19,7 @@
import {Controller} from "@hotwired/stimulus"; import {Controller} from "@hotwired/stimulus";
import * as bootbox from "bootbox"; import {ConfirmSwal} from "../../helpers/swal";
import "../../css/components/bootbox_extensions.css";
export default class extends Controller export default class extends Controller
{ {
@ -53,11 +52,11 @@ export default class extends Controller
const that = this; const that = this;
bootbox.confirm({ ConfirmSwal.fire({
title: this.titleValue, titleText: this.titleValue,
message: this.messageValue, text: this.messageValue,
callback: (result) => { }).then(({isConfirmed}) => {
if (result) { if (isConfirmed) {
//Set a flag to prevent the dialog from popping up again and allowing turbo to submit the form //Set a flag to prevent the dialog from popping up again and allowing turbo to submit the form
that._confirmed = true; that._confirmed = true;
@ -66,7 +65,6 @@ export default class extends Controller
} else { } else {
that._confirmed = false; that._confirmed = false;
} }
}
}); });
} }
} }

View file

@ -19,12 +19,14 @@
import {Controller} from "@hotwired/stimulus"; import {Controller} from "@hotwired/stimulus";
import { zxcvbn, zxcvbnOptions } from '@zxcvbn-ts/core'; import { ZxcvbnFactory } from '@zxcvbn-ts/core';
import * as zxcvbnCommonPackage from '@zxcvbn-ts/language-common'; import * as zxcvbnCommonPackage from '@zxcvbn-ts/language-common';
import * as zxcvbnEnPackage from '@zxcvbn-ts/language-en'; import * as zxcvbnEnPackage from '@zxcvbn-ts/language-en';
import * as zxcvbnDePackage from '@zxcvbn-ts/language-de'; import * as zxcvbnDePackage from '@zxcvbn-ts/language-de';
import * as zxcvbnFrPackage from '@zxcvbn-ts/language-fr'; import * as zxcvbnFrPackage from '@zxcvbn-ts/language-fr';
import * as zxcvbnJaPackage from '@zxcvbn-ts/language-ja'; import * as zxcvbnJaPackage from '@zxcvbn-ts/language-ja';
import * as zxcvbnItPackage from '@zxcvbn-ts/language-it';
import * as zxcvbnPlPackage from '@zxcvbn-ts/language-pl';
import {trans} from '../../translator.js'; import {trans} from '../../translator.js';
/* stimulusFetch: 'lazy' */ /* stimulusFetch: 'lazy' */
@ -34,6 +36,8 @@ export default class extends Controller {
static targets = ["badge", "warning"] static targets = ["badge", "warning"]
_zxcvbnFactory;
_getTranslations() { _getTranslations() {
//Get the current locale //Get the current locale
const locale = document.documentElement.lang; const locale = document.documentElement.lang;
@ -43,6 +47,10 @@ export default class extends Controller {
return zxcvbnFrPackage.translations; return zxcvbnFrPackage.translations;
} else if (locale.includes('ja')) { } else if (locale.includes('ja')) {
return zxcvbnJaPackage.translations; return zxcvbnJaPackage.translations;
} else if (locale.includes('it')) {
return zxcvbnItPackage.translations;
} else if (locale.includes('pl')) {
return zxcvbnPlPackage.translations;
} }
//Fallback to english //Fallback to english
@ -56,34 +64,39 @@ export default class extends Controller {
//Configure zxcvbn //Configure zxcvbn
const options = { const options = {
graphs: zxcvbnCommonPackage.adjacencyGraphs, graphs: zxcvbnCommonPackage.adjacencyGraphs,
useLevenshtein: true,
dictionary: { dictionary: {
...zxcvbnCommonPackage.dictionary, ...zxcvbnCommonPackage.dictionary,
// We could use the english dictionary here too, but it is very big. So we just use the common words // We could use the english dictionary here too, but it is very big. So we just use the common words
//...zxcvbnEnPackage.dictionary, ...zxcvbnEnPackage.dictionary,
...zxcvbnDePackage.dictionary,
"partdb": ['part-db', 'partdb', 'part_db', 'part-db-symfony', 'partdb-symfony', 'part_db_symfony'],
}, },
translations: this._getTranslations(), translations: this._getTranslations(),
}; };
zxcvbnOptions.setOptions(options);
this._zxcvbnFactory = new ZxcvbnFactory(options);
//Add event listener to the password input field //Add event listener to the password input field
this._passwordInput.addEventListener('input', this._onPasswordInput.bind(this)); this._passwordInput.addEventListener('input', this._onPasswordInput.bind(this));
} }
_onPasswordInput() { async _onPasswordInput() {
//Retrieve the password //Retrieve the password
const password = this._passwordInput.value; const password = this._passwordInput.value;
//Estimate the password strength //Estimate the password strength
const result = zxcvbn(password); const result = await this._zxcvbnFactory.checkAsync(password);
//Update the badge //Update the badge
this.badgeTarget.parentElement.classList.remove("d-none"); this.badgeTarget.parentElement.classList.remove("d-none");
this._setBadgeToLevel(result.score); this._setBadgeToLevel(result.score, result.crackTimes.onlineNoThrottlingXPerSecond.display);
this.warningTarget.innerHTML = result.feedback.warning; this.warningTarget.innerHTML = result.feedback.warning;
} }
_setBadgeToLevel(level) { _setBadgeToLevel(level, time = null) {
let text, classes; let text, classes;
switch (level) { switch (level) {
@ -118,5 +131,11 @@ export default class extends Controller {
//Re-add the classes //Re-add the classes
this.badgeTarget.classList.add("badge"); this.badgeTarget.classList.add("badge");
this.badgeTarget.classList.add(...classes.split(" ")); this.badgeTarget.classList.add(...classes.split(" "));
if (time) {
this.badgeTarget.setAttribute("title", trans("user.password_strength.crack_time", {"%time%": time}));
} else {
this.badgeTarget.removeAttribute("title");
}
} }
} }

View file

@ -18,7 +18,7 @@
*/ */
import {Controller} from "@hotwired/stimulus"; import {Controller} from "@hotwired/stimulus";
import * as bootbox from "bootbox"; import {AlertSwal} from "../../helpers/swal";
export default class extends Controller { export default class extends Controller {
@ -35,12 +35,12 @@ export default class extends Controller {
const part_distance = document.getElementById('reel_part_distance').value; const part_distance = document.getElementById('reel_part_distance').value;
if (dia_inner == "" || dia_outer == "" || tape_thickness == "") { if (dia_inner == "" || dia_outer == "" || tape_thickness == "") {
bootbox.alert(this.errorMissingValuesValue); AlertSwal.fire({title: this.errorMissingValuesValue});
return; return;
} }
if (dia_outer**dia_outer < dia_inner**dia_inner) { if (dia_outer**dia_outer < dia_inner**dia_inner) {
bootbox.alert(this.errorOuterGreaterInnerValue); AlertSwal.fire({title: this.errorOuterGreaterInnerValue});
return; return;
} }
@ -61,12 +61,12 @@ export default class extends Controller {
return; return;
} }
var parts_per_meter = 1 / (part_distance / 1000); const parts_per_meter = 1 / (part_distance / 1000);
document.getElementById('result_parts_per_meter').textContent = parts_per_meter.toFixed(2) + ' 1/m'; document.getElementById('result_parts_per_meter').textContent = parts_per_meter.toFixed(2) + ' 1/m';
var parts_amount = (length/1000) * parts_per_meter; const parts_amount = (length / 1000) * parts_per_meter;
document.getElementById('result_amount').textContent = Math.floor(parts_amount); document.getElementById('result_amount').textContent = Math.floor(parts_amount).toString();
} }
} }

View file

@ -0,0 +1,50 @@
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/**
* Respect the dark mode of Bootstrap 5 set via data-bs-theme="dark" on the <html> element. This is done by overriding the CSS variables of the bootstrap-5 theme of SweetAlert2.
*/
html[data-bs-theme="dark"] [data-swal2-theme='bootstrap-5'] {
/* POPUP */
--swal2-background: #212529;
--swal2-color: #fff;
--swal2-border: 1px solid #495057;
/* INPUT */
--swal2-input-background: #2b3035;
--swal2-input-border: 1px solid #495057;
--swal2-input-focus-border: 1px solid #86b7fe;
--swal2-input-focus-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.075), 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
/* VALIDATION MESSAGE */
--swal2-validation-message-background: #2c0b0e;
--swal2-validation-message-color: #ea868f;
/* FOOTER */
--swal2-footer-border-color: #495057;
--swal2-footer-background: #343a40;
--swal2-footer-color: #adb5bd;
/* CLOSE BUTTON */
--swal2-close-button-color: #fff;
/* TOASTS */
--swal2-toast-border: 1px solid #495057;
}

44
assets/helpers/swal.js Normal file
View file

@ -0,0 +1,44 @@
/*
* 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 Swal from 'sweetalert2';
import 'sweetalert2/themes/bootstrap-5.css';
import '../css/components/swal.css'
import { trans } from '../translator';
const BaseSwal = Swal.mixin({
position: "top",
theme: "bootstrap-5",
confirmButtonText: trans('dialog.btn.ok'),
cancelButtonText: trans('dialog.btn.cancel'),
denyButtonText: trans('dialog.btn.deny'),
});
const ConfirmSwal = BaseSwal.mixin({
showCancelButton: true,
showCloseButton: true,
icon: "warning",
});
const AlertSwal = BaseSwal.mixin({
showCloseButton: true,
icon: "info",
});
export { ConfirmSwal, AlertSwal, BaseSwal, BaseSwal as default,};

View file

@ -30,21 +30,21 @@ import '../css/app/images.css';
// start the Stimulus application // start the Stimulus application
import '../stimulus_bootstrap'; import '../stimulus_bootstrap';
// Need jQuery? Install it with "yarn add jquery", then uncomment to require it. import $ from 'jquery';
const $ = require('jquery');
//Only include javascript //Only include javascript
import '@fortawesome/fontawesome-free/css/all.css' import '@fortawesome/fontawesome-free/css/all.css'
require('bootstrap'); import 'bootstrap';
import "./error_handler"; import "./error_handler";
import "./tab_remember"; import "./tab_remember";
import "./register_events"; import "./register_events";
import "./tristate_checkboxes"; import "./tristate_checkboxes";
//Define jquery globally // Expose jQuery globally so legacy plugins and Bootstrap's jQuery integration
global.$ = global.jQuery = require("jquery"); // can find it on window at runtime.
global.$ = global.jQuery = $;
//Use the local WASM file for the ZXing library //Use the local WASM file for the ZXing library
import { import {

View file

@ -17,7 +17,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import * as bootbox from "bootbox"; import Swal from "../helpers/swal";
/** /**
* If this class is imported the user is shown an error dialog if he calls an page via Turbo and an error is responded. * If this class is imported the user is shown an error dialog if he calls an page via Turbo and an error is responded.
@ -40,21 +40,6 @@ class ErrorHandlerHelper {
_showAlert(statusText, statusCode, location, responseHTML) _showAlert(statusText, statusCode, location, responseHTML)
{ {
const httpStatusToText = { const httpStatusToText = {
'200': 'OK',
'201': 'Created',
'202': 'Accepted',
'203': 'Non-Authoritative Information',
'204': 'No Content',
'205': 'Reset Content',
'206': 'Partial Content',
'300': 'Multiple Choices',
'301': 'Moved Permanently',
'302': 'Found',
'303': 'See Other',
'304': 'Not Modified',
'305': 'Use Proxy',
'306': 'Unused',
'307': 'Temporary Redirect',
'400': 'Bad Request', '400': 'Bad Request',
'401': 'Unauthorized', '401': 'Unauthorized',
'402': 'Payment Required', '402': 'Payment Required',
@ -83,49 +68,67 @@ class ErrorHandlerHelper {
'505': 'HTTP Version Not Supported', '505': 'HTTP Version Not Supported',
}; };
//If the statusText is empty, we use the status code as text const userFriendlyMessages = {
if (!statusText) { '400': 'The request was invalid or malformed.',
statusText = httpStatusToText[statusCode]; '401': 'You need to log in to access this resource.',
} '403': 'You don\'t have permission to access this resource.',
'404': 'The requested page or resource could not be found.',
//Create error text '408': 'The request timed out. Please check your connection and try again.',
const title = statusText + ' (Status ' + statusCode + ')'; '409': 'There was a conflict with the current state of the resource.',
'429': 'Too many requests sent. Please wait a moment and try again.',
let trimString = function (string, length) { '500': 'An internal server error occurred. This is not your fault.',
return string.length > length ? '502': 'The server received an invalid response from an upstream service.',
string.substring(0, length) + '...' : '503': 'The service is temporarily unavailable. Please try again later.',
string; '504': 'The server did not respond in time. Please try again later.',
}; };
const short_location = trimString(location, 50); if (!statusText) {
statusText = httpStatusToText[String(statusCode)] ?? 'Unknown Error';
const alert = bootbox.alert(
{
size: 'large',
message: function() {
let url = location;
let msg = `Error calling <a href="${url}">${short_location}</a>.<br>`;
msg += '<b>Try to reload the page or contact the administrator if this error persists.</b>';
msg += '<br><br><a class=\"btn btn-outline-secondary mb-2\" data-bs-toggle=\"collapse\" href=\"#iframe_div\" >' + 'View details' + "</a>";
msg += "<div class=\" collapse\" id='iframe_div'><iframe height='512' width='100%' id='error-iframe'></iframe></div>";
return msg;
},
title: title,
callback: function () {
//Remove blur
$('#content').removeClass('loading-content');
} }
}); const title = `${statusText} <small class="text-muted fs-6">(HTTP ${statusCode})</small>`;
const friendlyMsg = userFriendlyMessages[String(statusCode)]
?? 'An unexpected error occurred. Please try again or contact the administrator.';
alert.init(function (){ const short_location = location.length > 80
var dstFrame = document.getElementById('error-iframe'); ? location.substring(0, 80) + '…'
: location;
const msg = `
<p class="mb-3">${friendlyMsg}</p>
<p class="text-muted small mb-3">If this error keeps happening, please contact your administrator.</p>
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#swal-error-details" aria-expanded="false">
<i class="fas fa-code me-1"></i>Technical details
</button>
<div class="collapse mt-2" id="swal-error-details">
<iframe height="400" width="100%" id="error-iframe" style="border:1px solid var(--bs-border-color);border-radius:var(--bs-border-radius);"></iframe>
</div>`;
const footer = `<span class="text-muted small">Error while loading: <a href="${location}" class="text-muted text-decoration-none" style="opacity:0.7;">${short_location}</a></span>`;
Swal.fire({
icon: 'error',
title: title,
html: msg,
footer: footer,
width: '90%',
confirmButtonText: '<i class="fas fa-rotate-right me-1"></i>Reload page',
showCancelButton: true,
cancelButtonText: 'Close',
showCloseButton: true,
reverseButtons: true,
didOpen: () => {
const dstFrame = document.getElementById('error-iframe');
//@ts-ignore //@ts-ignore
var dstDoc = dstFrame.contentDocument || dstFrame.contentWindow.document; const dstDoc = dstFrame.contentDocument || dstFrame.contentWindow.document;
dstDoc.write(responseHTML) dstDoc.write(responseHTML);
dstDoc.close(); dstDoc.close();
},
}).then((result) => {
document.getElementById('content').classList.remove('loading-content');
if (result.isConfirmed) {
window.location.reload();
}
}); });
} }

File diff suppressed because it is too large Load diff

View file

@ -19,9 +19,8 @@
'use strict'; 'use strict';
import {Dropdown} from "bootstrap"; import {Dropdown, Modal, Tooltip} from "bootstrap";
import ClipboardJS from "clipboard"; import ClipboardJS from "clipboard";
import {Modal} from "bootstrap";
class RegisterEventHelper { class RegisterEventHelper {
constructor() { constructor() {
@ -40,8 +39,6 @@ class RegisterEventHelper {
}); });
this.registerModalDropRemovalOnFormSubmit(); this.registerModalDropRemovalOnFormSubmit();
} }
registerModalDropRemovalOnFormSubmit() { registerModalDropRemovalOnFormSubmit() {
@ -83,11 +80,17 @@ class RegisterEventHelper {
registerTooltips() { registerTooltips() {
const handler = () => { const handler = () => {
$(".tooltip").remove(); document.querySelectorAll('.tooltip').forEach(el => el.remove());
//Exclude dropdown buttons from tooltips, otherwise we run into endless errors from bootstrap (bootstrap.esm.js:614 Bootstrap doesn't allow more than one instance per element. Bound instance: bs.dropdown.) //Exclude dropdown buttons from tooltips, otherwise we run into endless errors from bootstrap (bootstrap.esm.js:614 Bootstrap doesn't allow more than one instance per element. Bound instance: bs.dropdown.)
$('a[title], label[title], button[title]:not([data-bs-toggle="dropdown"]), p[title], span[title], h6[title], h3[title], i[title], small[title]') const tooltipSelector = 'a[title], label[title], button[title]:not([data-bs-toggle="dropdown"]), p[title], span[title], h6[title], h3[title], i[title], small[title]';
//@ts-ignore document.querySelectorAll(tooltipSelector).forEach(el => {
.tooltip("hide").tooltip({container: "body", placement: "auto", boundary: 'window'}); const existing = Tooltip.getInstance(el);
if (existing) {
existing.dispose();
}
new Tooltip(el, {container: 'body', placement: 'auto', boundary: 'window'});
});
}; };
this.registerLoadHandler(handler); this.registerLoadHandler(handler);
@ -95,9 +98,7 @@ class RegisterEventHelper {
} }
registerSpecialCharInput() { registerSpecialCharInput() {
this.registerLoadHandler(() => { const keydownHandler = function(event) {
//@ts-ignore
$("input[type=text], input[type=search]").unbind("keydown").keydown(function (event) {
let use_special_char = event.altKey; let use_special_char = event.altKey;
let greek_char = ""; let greek_char = "";
@ -106,148 +107,148 @@ class RegisterEventHelper {
switch(event.key) { switch(event.key) {
//Greek letters //Greek letters
case "a": //Alpha (lowercase) case "a": //Alpha (lowercase)
greek_char = "\u03B1"; greek_char = "α";
break; break;
case "A": //Alpha (uppercase) case "A": //Alpha (uppercase)
greek_char = "\u0391"; greek_char = "Α";
break; break;
case "b": //Beta (lowercase) case "b": //Beta (lowercase)
greek_char = "\u03B2"; greek_char = ";
break; break;
case "B": //Beta (uppercase) case "B": //Beta (uppercase)
greek_char = "\u0392"; greek_char = "Β";
break; break;
case "g": //Gamma (lowercase) case "g": //Gamma (lowercase)
greek_char = "\u03B3"; greek_char = "γ";
break; break;
case "G": //Gamma (uppercase) case "G": //Gamma (uppercase)
greek_char = "\u0393"; greek_char = ";
break; break;
case "d": //Delta (lowercase) case "d": //Delta (lowercase)
greek_char = "\u03B4"; greek_char = ";
break; break;
case "D": //Delta (uppercase) case "D": //Delta (uppercase)
greek_char = "\u0394"; greek_char = ";
break; break;
case "e": //Epsilon (lowercase) case "e": //Epsilon (lowercase)
greek_char = "\u03B5"; greek_char = ";
break; break;
case "E": //Epsilon (uppercase) case "E": //Epsilon (uppercase)
greek_char = "\u0395"; greek_char = "Ε";
break; break;
case "z": //Zeta (lowercase) case "z": //Zeta (lowercase)
greek_char = "\u03B6"; greek_char = ";
break; break;
case "Z": //Zeta (uppercase) case "Z": //Zeta (uppercase)
greek_char = "\u0396"; greek_char = "Ζ";
break; break;
case "h": //Eta (lowercase) case "h": //Eta (lowercase)
greek_char = "\u03B7"; greek_char = ";
break; break;
case "H": //Eta (uppercase) case "H": //Eta (uppercase)
greek_char = "\u0397"; greek_char = "Η";
break; break;
case "q": //Theta (lowercase) case "q": //Theta (lowercase)
greek_char = "\u03B8"; greek_char = ";
break; break;
case "Q": //Theta (uppercase) case "Q": //Theta (uppercase)
greek_char = "\u0398"; greek_char = ";
break; break;
case "i": //Iota (lowercase) case "i": //Iota (lowercase)
greek_char = "\u03B9"; greek_char = "ι";
break; break;
case "I": //Iota (uppercase) case "I": //Iota (uppercase)
greek_char = "\u0399"; greek_char = "Ι";
break; break;
case "k": //Kappa (lowercase) case "k": //Kappa (lowercase)
greek_char = "\u03BA"; greek_char = ";
break; break;
case "K": //Kappa (uppercase) case "K": //Kappa (uppercase)
greek_char = "\u039A"; greek_char = "Κ";
break; break;
case "l": //Lambda (lowercase) case "l": //Lambda (lowercase)
greek_char = "\u03BB"; greek_char = ";
break; break;
case "L": //Lambda (uppercase) case "L": //Lambda (uppercase)
greek_char = "\u039B"; greek_char = ";
break; break;
case "m": //Mu (lowercase) case "m": //Mu (lowercase)
greek_char = "\u03BC"; greek_char = ";
break; break;
case "M": //Mu (uppercase) case "M": //Mu (uppercase)
greek_char = "\u039C"; greek_char = "Μ";
break; break;
case "n": //Nu (lowercase) case "n": //Nu (lowercase)
greek_char = "\u03BD"; greek_char = "ν";
break; break;
case "N": //Nu (uppercase) case "N": //Nu (uppercase)
greek_char = "\u039D"; greek_char = "Ν";
break; break;
case "x": //Xi (lowercase) case "x": //Xi (lowercase)
greek_char = "\u03BE"; greek_char = ";
break; break;
case "X": //Xi (uppercase) case "X": //Xi (uppercase)
greek_char = "\u039E"; greek_char = ";
break; break;
case "o": //Omicron (lowercase) case "o": //Omicron (lowercase)
greek_char = "\u03BF"; greek_char = "ο";
break; break;
case "O": //Omicron (uppercase) case "O": //Omicron (uppercase)
greek_char = "\u039F"; greek_char = "Ο";
break; break;
case "p": //Pi (lowercase) case "p": //Pi (lowercase)
greek_char = "\u03C0"; greek_char = ";
break; break;
case "P": //Pi (uppercase) case "P": //Pi (uppercase)
greek_char = "\u03A0"; greek_char = ";
break; break;
case "r": //Rho (lowercase) case "r": //Rho (lowercase)
greek_char = "\u03C1"; greek_char = "ρ";
break; break;
case "R": //Rho (uppercase) case "R": //Rho (uppercase)
greek_char = "\u03A1"; greek_char = "Ρ";
break; break;
case "s": //Sigma (lowercase) case "s": //Sigma (lowercase)
greek_char = "\u03C3"; greek_char = "σ";
break; break;
case "S": //Sigma (uppercase) case "S": //Sigma (uppercase)
greek_char = "\u03A3"; greek_char = ";
break; break;
case "t": //Tau (lowercase) case "t": //Tau (lowercase)
greek_char = "\u03C4"; greek_char = ";
break; break;
case "T": //Tau (uppercase) case "T": //Tau (uppercase)
greek_char = "\u03A4"; greek_char = "Τ";
break; break;
case "u": //Upsilon (lowercase) case "u": //Upsilon (lowercase)
greek_char = "\u03C5"; greek_char = "υ";
break; break;
case "U": //Upsilon (uppercase) case "U": //Upsilon (uppercase)
greek_char = "\u03A5"; greek_char = "Υ";
break; break;
case "f": //Phi (lowercase) case "f": //Phi (lowercase)
greek_char = "\u03C6"; greek_char = ";
break; break;
case "F": //Phi (uppercase) case "F": //Phi (uppercase)
greek_char = "\u03A6"; greek_char = ";
break; break;
case "c": //Chi (lowercase) case "c": //Chi (lowercase)
greek_char = "\u03C7"; greek_char = ";
break; break;
case "C": //Chi (uppercase) case "C": //Chi (uppercase)
greek_char = "\u03A7"; greek_char = "Χ";
break; break;
case "y": //Psi (lowercase) case "y": //Psi (lowercase)
greek_char = "\u03C8"; greek_char = ";
break; break;
case "Y": //Psi (uppercase) case "Y": //Psi (uppercase)
greek_char = "\u03A8"; greek_char = ";
break; break;
case "w": //Omega (lowercase) case "w": //Omega (lowercase)
greek_char = "\u03C9"; greek_char = ";
break; break;
case "W": //Omega (uppercase) case "W": //Omega (uppercase)
greek_char = "\u03A9"; greek_char = ";
break; break;
} }
@ -255,81 +256,81 @@ class RegisterEventHelper {
switch (event.keyCode) { switch (event.keyCode) {
case 49: //1 key case 49: //1 key
//Product symbol on shift, sum on no shift //Product symbol on shift, sum on no shift
greek_char = event.shiftKey ? "\u220F" : "\u2211"; greek_char = event.shiftKey ? "∏" : "∑";
break; break;
case 50: //2 key case 50: //2 key
//Integral on no shift, partial derivative on shift //Integral on no shift, partial derivative on shift
greek_char = event.shiftKey ? "\u2202" : "\u222B"; greek_char = event.shiftKey ? "∂" : "∫";
break; break;
case 51: //3 key case 51: //3 key
//Less than or equal on no shift, greater than or equal on shift //Less than or equal on no shift, greater than or equal on shift
greek_char = event.shiftKey ? "\u2265" : "\u2264"; greek_char = event.shiftKey ? "≥" : "≤";
break; break;
case 52: //4 key case 52: //4 key
//Empty set on shift, infinity on no shift //Empty set on shift, infinity on no shift
greek_char = event.shiftKey ? "\u2205" : "\u221E"; greek_char = event.shiftKey ? "∅" : "∞";
break; break;
case 53: //5 key case 53: //5 key
//Not equal on shift, approx equal on no shift //Not equal on shift, approx equal on no shift
greek_char = event.shiftKey ? "\u2260" : "\u2248"; greek_char = event.shiftKey ? "≠" : "≈";
break; break;
case 54: //6 key case 54: //6 key
//Element of on no shift, not element of on shift //Element of on no shift, not element of on shift
greek_char = event.shiftKey ? "\u2209" : "\u2208"; greek_char = event.shiftKey ? "∉" : "∈";
break; break;
case 55: //7 key case 55: //7 key
//And on shift, or on no shift //And on shift, or on no shift
greek_char = event.shiftKey ? "\u2227" : "\u2228"; greek_char = event.shiftKey ? "∧" : "";
break; break;
case 56: //8 key case 56: //8 key
//Proportional to on shift, angle on no shift //Proportional to on shift, angle on no shift
greek_char = event.shiftKey ? "\u221D" : "\u2220"; greek_char = event.shiftKey ? "∝" : "∠";
break; break;
case 57: //9 key case 57: //9 key
//Cube root on shift, square root on no shift //Cube root on shift, square root on no shift
greek_char = event.shiftKey ? "\u221B" : "\u221A"; greek_char = event.shiftKey ? "∛" : "√";
break; break;
case 48: //0 key case 48: //0 key
//Minus-Plus on shift, plus-minus on no shift //Minus-Plus on shift, plus-minus on no shift
greek_char = event.shiftKey ? "\u2213" : "\u00B1"; greek_char = event.shiftKey ? "∓" : ";
break; break;
//Special characters //Special characters
case 219: //hyphen (or ß on german layout) case 219: //hyphen (or ß on german layout)
//Copyright on no shift, TM on shift //Copyright on no shift, TM on shift
greek_char = event.shiftKey ? "\u2122" : "\u00A9"; greek_char = event.shiftKey ? "™" : ";
break; break;
case 191: //forward slash (or # on german layout) case 191: //forward slash (or # on german layout)
//Generic currency on no shift, paragraph on shift //Generic currency on no shift, paragraph on shift
greek_char = event.shiftKey ? "\u00B6" : "\u00A4"; greek_char = event.shiftKey ? "¶" : ";
break; break;
//Currency symbols //Currency symbols
case 192: //: or (ö on german layout) case 192: //: or (ö on german layout)
//Euro on no shift, pound on shift //Euro on no shift, pound on shift
greek_char = event.shiftKey ? "\u00A3" : "\u20AC"; greek_char = event.shiftKey ? "£" : "€";
break; break;
case 221: //; or (ä on german layout) case 221: //; or (ä on german layout)
//Yen on no shift, dollar on shift //Yen on no shift, dollar on shift
greek_char = event.shiftKey ? "\u0024" : "\u00A5"; greek_char = event.shiftKey ? "$" : ";
break; break;
} }
if(greek_char=="") return; if(greek_char=="") return;
let $txt = $(this); const txt = event.currentTarget;
//@ts-ignore const caretPos = txt.selectionStart;
let caretPos = $txt[0].selectionStart; const textAreaTxt = txt.value;
let textAreaTxt = $txt.val().toString(); txt.value = textAreaTxt.substring(0, caretPos) + greek_char + textAreaTxt.substring(caretPos);
$txt.val(textAreaTxt.substring(0, caretPos) + greek_char + textAreaTxt.substring(caretPos) );
} }
};
this.registerLoadHandler(() => {
document.querySelectorAll('input[type=text], input[type=search]').forEach(input => {
input.removeEventListener('keydown', keydownHandler);
input.addEventListener('keydown', keydownHandler);
});
}); });
//@ts-ignore
this.greek_once = true;
})
} }
} }

View file

@ -1,7 +1,7 @@
/* /*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony). * 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) * Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU Affero General Public License as published
@ -17,30 +17,4 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
.modal-body > .bootbox-close-button { import "bootswatch/dist/brite/bootstrap.css";
position: absolute;
top: 0;
right: 0;
padding: 0.5rem 0.75rem;
z-index: 1;
}
.modal .bootbox-close-button {
font-weight: 100;
}
button.bootbox-close-button {
padding: 0;
background-color: transparent;
border: 0;
-webkit-appearance: none;
}
.bootbox-close-button {
/* float: right; */
font-size: 1.40625rem;
font-weight: 600;
line-height: 1;
color: #000;
text-shadow: none;
opacity: .5;
}

View file

@ -57,9 +57,10 @@
"scheb/2fa-trusted-device": "^v7.11.0", "scheb/2fa-trusted-device": "^v7.11.0",
"shivas/versioning-bundle": "^4.0", "shivas/versioning-bundle": "^4.0",
"spatie/db-dumper": "^3.3.1", "spatie/db-dumper": "^3.3.1",
"symfony/ai-bundle": "^0.9.0", "symfony/ai-bundle": "^0.10.0",
"symfony/ai-lm-studio-platform": "^0.9.0", "symfony/ai-lm-studio-platform": "^v0.10.0",
"symfony/ai-open-router-platform": "^0.9.0", "symfony/ai-ollama-platform": "^0.10.0",
"symfony/ai-open-router-platform": "^0.10.0",
"symfony/apache-pack": "^1.0", "symfony/apache-pack": "^1.0",
"symfony/asset": "7.4.*", "symfony/asset": "7.4.*",
"symfony/console": "7.4.*", "symfony/console": "7.4.*",

1031
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -2,3 +2,4 @@ ai:
platform: platform:
lmstudio: lmstudio:
host_url: '%env(string:settings:ai_lmstudio:hostURL)%' host_url: '%env(string:settings:ai_lmstudio:hostURL)%'
http_client: 'app.http_client.ai_lmstudio'

View file

@ -0,0 +1,6 @@
ai:
platform:
ollama:
endpoint: '%env(string:settings:ai_ollama:endpoint)%'
api_key: '%env(string:settings:ai_ollama:apiKey)%'
http_client: 'app.http_client.ai_ollama'

View file

@ -2,3 +2,4 @@ ai:
platform: platform:
openrouter: openrouter:
api_key: '%env(string:settings:ai_openrouter:apiKey)%' api_key: '%env(string:settings:ai_openrouter:apiKey)%'
http_client: 'app.http_client.ai_openrouter'

View file

@ -1,16 +0,0 @@
services:
EasyCorp\EasyLog\EasyLogHandler:
public: false
arguments: ['%kernel.logs_dir%/%kernel.environment%.log']
#// FIXME: How to add this configuration automatically without messing up with the monolog configuration?
#monolog:
# handlers:
# buffered:
# type: buffer
# handler: easylog
# channels: ['!event']
# level: debug
# easylog:
# type: service
# id: EasyCorp\EasyLog\EasyLogHandler

View file

@ -20,16 +20,16 @@
declare(strict_types=1); declare(strict_types=1);
use Symfony\Config\DoctrineConfig;
/** /**
* This class extends the default doctrine ORM configuration to enable native lazy objects on PHP 8.4+. * This file enables native lazy objects on PHP 8.4+.
* We have to do this in a PHP file, because the yaml file does not support conditionals on PHP version. * We have to do this in a PHP file, because the yaml file does not support conditionals on PHP version.
*
* TODO: Remove this file when we drop support for PHP < 8.4
*/ */
return static function(DoctrineConfig $doctrine) { // On PHP 8.4+ we can use native lazy objects, which are much more efficient than proxies.
//On PHP 8.4+ we can use native lazy objects, which are much more efficient than proxies. if (PHP_VERSION_ID >= 80400) {
if (PHP_VERSION_ID >= 80400) { return ['doctrine' => ['orm' => ['enable_native_lazy_objects' => true]]];
$doctrine->orm()->enableNativeLazyObjects(true); }
}
}; return [];

View file

@ -53,6 +53,7 @@ parameters:
# Themes commented here by default, are not really usable, because of display problems. Enable them at your own risk! # Themes commented here by default, are not really usable, because of display problems. Enable them at your own risk!
partdb.available_themes: partdb.available_themes:
- bootstrap - bootstrap
- brite
- cerulean - cerulean
- cosmo - cosmo
- cyborg - cyborg

View file

@ -121,7 +121,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* } * }
* @psalm-type ServicesConfig = array{ * @psalm-type ServicesConfig = array{
* _defaults?: DefaultsType, * _defaults?: DefaultsType,
* _instanceof?: InstanceofType, * _instanceof?: array<class-string, InstanceofType>,
* ...<string, DefinitionType|AliasType|PrototypeType|StackType|ArgumentsType|null> * ...<string, DefinitionType|AliasType|PrototypeType|StackType|ArgumentsType|null>
* } * }
* @psalm-type ExtensionType = array<string, mixed> * @psalm-type ExtensionType = array<string, mixed>
@ -2874,8 +2874,8 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* enable_translation?: bool|Param, // Enable translation for the system prompt // Default: false * enable_translation?: bool|Param, // Enable translation for the system prompt // Default: false
* translation_domain?: string|Param, // The translation domain for the system prompt // Default: null * translation_domain?: string|Param, // The translation domain for the system prompt // Default: null
* }, * },
* tools?: bool|array{ * tools?: bool|array{ // Tools are opt-in: set to true to inject all services tagged with "ai.tool", or configure an explicit list of tools. When the option is omitted (or set to null or false), no tools are registered.
* enabled?: bool|Param, // Default: true * enabled?: bool|Param, // Default: false
* services?: list<string|array{ // Default: [] * services?: list<string|array{ // Default: []
* service?: string|Param, * service?: string|Param,
* agent?: string|Param, * agent?: string|Param,
@ -2886,6 +2886,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* }, * },
* keep_tool_messages?: bool|Param, // Keep tool messages in the conversation history // Default: false * keep_tool_messages?: bool|Param, // Keep tool messages in the conversation history // Default: false
* include_sources?: bool|Param, // Include sources exposed by tools as part of the tool result metadata // Default: false * include_sources?: bool|Param, // Include sources exposed by tools as part of the tool result metadata // Default: false
* max_tool_calls?: scalar|Param|null, // Maximum number of tool calls per agent call, null to disable // Default: 50
* fault_tolerant_toolbox?: bool|Param, // Continue the agent run even if a tool call fails // Default: true * fault_tolerant_toolbox?: bool|Param, // Continue the agent run even if a tool call fails // Default: true
* speech?: bool|array{ // Speech (TTS/STT) decorator configuration * speech?: bool|array{ // Speech (TTS/STT) decorator configuration
* enabled?: bool|Param, // Default: true * enabled?: bool|Param, // Default: true

View file

@ -52,6 +52,28 @@ services:
alias: 'doctrine.migrations.dependency_factory' alias: 'doctrine.migrations.dependency_factory'
####################################################################################################################
# AI provider HTTP clients (with configurable timeouts)
####################################################################################################################
app.http_client.ai_ollama:
class: Symfony\Contracts\HttpClient\HttpClientInterface
factory: ['@http_client', 'withOptions']
arguments:
- { timeout: '%env(int:settings:ai_ollama:timeout)%' }
app.http_client.ai_lmstudio:
class: Symfony\Contracts\HttpClient\HttpClientInterface
factory: ['@http_client', 'withOptions']
arguments:
- { timeout: '%env(int:settings:ai_lmstudio:timeout)%' }
app.http_client.ai_openrouter:
class: Symfony\Contracts\HttpClient\HttpClientInterface
factory: ['@http_client', 'withOptions']
arguments:
- { timeout: '%env(int:settings:ai_openrouter:timeout)%' }
#################################################################################################################### ####################################################################################################################
# Email # Email
#################################################################################################################### ####################################################################################################################

View file

@ -25,3 +25,10 @@ You need to supply an API key for OpenRouter to use it as an AI platform in Part
[LMStudio](https://lmstudio.ai/) is a local LLM hosting solution that allows you to run LLMs on your own hardware. You can use LMStudio to host your own LLM and connect it to Part-DB for AI features. [LMStudio](https://lmstudio.ai/) is a local LLM hosting solution that allows you to run LLMs on your own hardware. You can use LMStudio to host your own LLM and connect it to Part-DB for AI features.
Currently only LMStudio without any authentication is supported. Supply your LMStudio instance URL (including the port) to use it as an AI platform in Part-DB. Currently only LMStudio without any authentication is supported. Supply your LMStudio instance URL (including the port) to use it as an AI platform in Part-DB.
You have to set a model by hand, as suggestions currently do not work yet. Ensure the context length is suitable for your application.
### Ollama
[Ollama](https://ollama.com/) is another local LLM hosting solution that allows you to run LLMs on your own hardware. You can use Ollama to host your own LLM and connect it to Part-DB for AI features.
Supply your Ollama instance URL (including the port) and an optional API key for authentication to use it as an AI platform in Part-DB. The model selector should give you suggestions about available models.
Ensure the context length is suitable for your application.

View file

@ -13,7 +13,7 @@
"bootstrap": "^5.1.3", "bootstrap": "^5.1.3",
"core-js": "^3.38.0", "core-js": "^3.38.0",
"intl-messageformat": "^10.5.11", "intl-messageformat": "^10.5.11",
"jquery": "^3.5.1", "jquery": "^4.0.0",
"popper.js": "^1.14.7", "popper.js": "^1.14.7",
"regenerator-runtime": "^0.14.1", "regenerator-runtime": "^0.14.1",
"webpack": "^5.74.0", "webpack": "^5.74.0",
@ -38,20 +38,21 @@
"@algolia/autocomplete-theme-classic": "^1.17.0", "@algolia/autocomplete-theme-classic": "^1.17.0",
"@jbtronics/bs-treeview": "^1.0.1", "@jbtronics/bs-treeview": "^1.0.1",
"@part-db/html5-qrcode": "^4.0.0", "@part-db/html5-qrcode": "^4.0.0",
"@zxcvbn-ts/core": "^3.0.2", "@zxcvbn-ts/core": "^4.1.2",
"@zxcvbn-ts/language-common": "^3.0.3", "@zxcvbn-ts/language-common": "^4.1.2",
"@zxcvbn-ts/language-de": "^3.0.1", "@zxcvbn-ts/language-de": "^4.1.1",
"@zxcvbn-ts/language-en": "^3.0.1", "@zxcvbn-ts/language-en": "^4.1.1",
"@zxcvbn-ts/language-fr": "^3.0.1", "@zxcvbn-ts/language-fr": "^4.1.1",
"@zxcvbn-ts/language-ja": "^3.0.1", "@zxcvbn-ts/language-it": "^4.1.1",
"@zxcvbn-ts/language-ja": "^4.1.1",
"@zxcvbn-ts/language-pl": "^4.1.1",
"attr-accept": "^2.2.5", "attr-accept": "^2.2.5",
"barcode-detector": "^3.0.5", "barcode-detector": "^3.0.5",
"bootbox": "^6.0.0",
"bootswatch": "^5.1.3", "bootswatch": "^5.1.3",
"bs-custom-file-input": "^1.3.4", "bs-custom-file-input": "^1.3.4",
"ckeditor5": "^48.0.0", "ckeditor5": "^48.0.0",
"clipboard": "^2.0.4", "clipboard": "^2.0.4",
"compression-webpack-plugin": "^11.1.0", "compression-webpack-plugin": "^12.0.0",
"datatables.net": "^2.0.0", "datatables.net": "^2.0.0",
"datatables.net-bs5": "^2.0.0", "datatables.net-bs5": "^2.0.0",
"datatables.net-buttons-bs5": "^3.0.0", "datatables.net-buttons-bs5": "^3.0.0",
@ -69,11 +70,12 @@
"marked-mangle": "^1.0.1", "marked-mangle": "^1.0.1",
"pdfmake": "^0.3.7", "pdfmake": "^0.3.7",
"stimulus-use": "^0.52.0", "stimulus-use": "^0.52.0",
"sweetalert2": "^11.26.25",
"tom-select": "^2.1.0", "tom-select": "^2.1.0",
"ts-loader": "^9.2.6", "ts-loader": "^9.2.6",
"typescript": "^6.0.2" "typescript": "^6.0.2"
}, },
"resolutions": { "resolutions": {
"jquery": "^3.5.1" "jquery": "^4.0.0"
} }
} }

View file

@ -1,4 +1,4 @@
# Generated on Mon Jun 15 07:28:00 UTC 2026 # Generated on Mon Jun 22 07:31:48 UTC 2026
# This file contains all footprints available in the offical KiCAD library # This file contains all footprints available in the offical KiCAD library
Audio_Module:Reverb_BTDR-1H Audio_Module:Reverb_BTDR-1H
Audio_Module:Reverb_BTDR-1V Audio_Module:Reverb_BTDR-1V
@ -8293,6 +8293,7 @@ Converter_DCDC:Converter_DCDC_Hamamatsu_C11204-1_THT
Converter_DCDC:Converter_DCDC_MeanWell_NID30_THT Converter_DCDC:Converter_DCDC_MeanWell_NID30_THT
Converter_DCDC:Converter_DCDC_MeanWell_NID60_THT Converter_DCDC:Converter_DCDC_MeanWell_NID60_THT
Converter_DCDC:Converter_DCDC_MeanWell_NSD10_THT Converter_DCDC:Converter_DCDC_MeanWell_NSD10_THT
Converter_DCDC:Converter_DCDC_MeanWell_SMU02x-xxN_THT
Converter_DCDC:Converter_DCDC_Murata_CRE1xxxxxx3C_THT Converter_DCDC:Converter_DCDC_Murata_CRE1xxxxxx3C_THT
Converter_DCDC:Converter_DCDC_Murata_CRE1xxxxxxDC_THT Converter_DCDC:Converter_DCDC_Murata_CRE1xxxxxxDC_THT
Converter_DCDC:Converter_DCDC_Murata_CRE1xxxxxxSC_THT Converter_DCDC:Converter_DCDC_Murata_CRE1xxxxxxSC_THT
@ -13030,8 +13031,6 @@ Package_SON:WSON-6-1EP_2x2mm_P0.65mm_EP1x1.6mm
Package_SON:WSON-6-1EP_2x2mm_P0.65mm_EP1x1.6mm_ThermalVias Package_SON:WSON-6-1EP_2x2mm_P0.65mm_EP1x1.6mm_ThermalVias
Package_SON:WSON-6-1EP_3x3mm_P0.95mm Package_SON:WSON-6-1EP_3x3mm_P0.95mm
Package_SON:WSON-6_1.5x1.5mm_P0.5mm Package_SON:WSON-6_1.5x1.5mm_P0.5mm
Package_SON:WSON-8-1EP_2x2mm_P0.5mm_EP0.9x1.6mm
Package_SON:WSON-8-1EP_2x2mm_P0.5mm_EP0.9x1.6mm_ThermalVias
Package_SON:WSON-8-1EP_3x2.5mm_P0.5mm_EP1.2x1.5mm_PullBack Package_SON:WSON-8-1EP_3x2.5mm_P0.5mm_EP1.2x1.5mm_PullBack
Package_SON:WSON-8-1EP_3x2.5mm_P0.5mm_EP1.2x1.5mm_PullBack_ThermalVias Package_SON:WSON-8-1EP_3x2.5mm_P0.5mm_EP1.2x1.5mm_PullBack_ThermalVias
Package_SON:WSON-8-1EP_3x3mm_P0.5mm_EP1.2x2mm Package_SON:WSON-8-1EP_3x3mm_P0.5mm_EP1.2x2mm

View file

@ -1,4 +1,4 @@
# Generated on Mon Jun 15 07:28:38 UTC 2026 # Generated on Mon Jun 22 07:32:26 UTC 2026
# This file contains all symbols available in the offical KiCAD library # This file contains all symbols available in the offical KiCAD library
4xxx:14528 4xxx:14528
4xxx:14529 4xxx:14529
@ -4954,6 +4954,7 @@ Converter_DCDC:RPMH15-1.5
Converter_DCDC:RPMH24-1.5 Converter_DCDC:RPMH24-1.5
Converter_DCDC:RPMH3.3-1.5 Converter_DCDC:RPMH3.3-1.5
Converter_DCDC:RPMH5.0-1.5 Converter_DCDC:RPMH5.0-1.5
Converter_DCDC:SMU02L-24N
Converter_DCDC:TBA1-0310 Converter_DCDC:TBA1-0310
Converter_DCDC:TBA1-0311 Converter_DCDC:TBA1-0311
Converter_DCDC:TBA1-0510 Converter_DCDC:TBA1-0510
@ -6142,6 +6143,7 @@ Device:SparkGap
Device:Speaker Device:Speaker
Device:Speaker_Crystal Device:Speaker_Crystal
Device:Speaker_Ultrasound Device:Speaker_Ultrasound
Device:Thermal_Jumper
Device:Thermistor Device:Thermistor
Device:Thermistor_NTC Device:Thermistor_NTC
Device:Thermistor_NTC_3Wire Device:Thermistor_NTC_3Wire
@ -19297,6 +19299,7 @@ Regulator_Switching:LT1373HVCN8
Regulator_Switching:LT1373HVCS8 Regulator_Switching:LT1373HVCS8
Regulator_Switching:LT1377CN8 Regulator_Switching:LT1377CN8
Regulator_Switching:LT1377CS8 Regulator_Switching:LT1377CS8
Regulator_Switching:LT1931
Regulator_Switching:LT1945 Regulator_Switching:LT1945
Regulator_Switching:LT3430 Regulator_Switching:LT3430
Regulator_Switching:LT3430-1 Regulator_Switching:LT3430-1
@ -22516,6 +22519,7 @@ Transistor_FET_Other:Q_NMOS_Depletion_GDS
Transistor_FET_Other:Q_NMOS_Depletion_GSD Transistor_FET_Other:Q_NMOS_Depletion_GSD
Transistor_FET_Other:Q_NMOS_Depletion_SDG Transistor_FET_Other:Q_NMOS_Depletion_SDG
Transistor_FET_Other:Q_NMOS_Depletion_SGD Transistor_FET_Other:Q_NMOS_Depletion_SGD
Transistor_FET_Other:SP010N70T8
Transistor_FET_Other:VNB35N07xx-E Transistor_FET_Other:VNB35N07xx-E
Transistor_FET_Other:VNP10N07 Transistor_FET_Other:VNP10N07
Transistor_FET_Other:VNP35N07xx-E Transistor_FET_Other:VNP35N07xx-E

View file

@ -27,6 +27,7 @@ use App\Doctrine\Functions\SiValueSort;
use App\Exceptions\InvalidRegexException; use App\Exceptions\InvalidRegexException;
use Doctrine\DBAL\Driver\Connection; use Doctrine\DBAL\Driver\Connection;
use Doctrine\DBAL\Driver\Middleware\AbstractDriverMiddleware; use Doctrine\DBAL\Driver\Middleware\AbstractDriverMiddleware;
use Pdo\Sqlite;
/** /**
* This middleware is used to add the regexp operator to the SQLite platform. * This middleware is used to add the regexp operator to the SQLite platform.
@ -44,8 +45,20 @@ class SQLiteRegexExtensionMiddlewareDriver extends AbstractDriverMiddleware
if ($params['driver'] === 'pdo_sqlite') { if ($params['driver'] === 'pdo_sqlite') {
$native_connection = $connection->getNativeConnection(); $native_connection = $connection->getNativeConnection();
//Ensure that the function really exists on the connection, as it is marked as experimental according to PHP documentation
if($native_connection instanceof \PDO) { if($native_connection instanceof \PDO) {
//Use the new PDO::createFunction and PDO::createCollation methods if available (PHP 8.4+)
if (is_a($native_connection, Sqlite::class)) { #TODO: Remove this check when PHP 8.4 is the minimum requirement
$native_connection->createFunction('REGEXP', self::regexp(...), 2, Sqlite::DETERMINISTIC);
$native_connection->createFunction('FIELD', self::field(...), -1, Sqlite::DETERMINISTIC);
$native_connection->createFunction('FIELD2', self::field2(...), 2, Sqlite::DETERMINISTIC);
//Create a new collation for natural sorting
$native_connection->createCollation('NATURAL_CMP', strnatcmp(...));
//Create a function for SI prefix value sorting
$native_connection->createFunction('SI_VALUE', SiValueSort::sqliteSiValue(...), 1, Sqlite::DETERMINISTIC);
} else {
$native_connection->sqliteCreateFunction('REGEXP', self::regexp(...), 2, \PDO::SQLITE_DETERMINISTIC); $native_connection->sqliteCreateFunction('REGEXP', self::regexp(...), 2, \PDO::SQLITE_DETERMINISTIC);
$native_connection->sqliteCreateFunction('FIELD', self::field(...), -1, \PDO::SQLITE_DETERMINISTIC); $native_connection->sqliteCreateFunction('FIELD', self::field(...), -1, \PDO::SQLITE_DETERMINISTIC);
$native_connection->sqliteCreateFunction('FIELD2', self::field2(...), 2, \PDO::SQLITE_DETERMINISTIC); $native_connection->sqliteCreateFunction('FIELD2', self::field2(...), 2, \PDO::SQLITE_DETERMINISTIC);
@ -57,6 +70,7 @@ class SQLiteRegexExtensionMiddlewareDriver extends AbstractDriverMiddleware
$native_connection->sqliteCreateFunction('SI_VALUE', SiValueSort::sqliteSiValue(...), 1, \PDO::SQLITE_DETERMINISTIC); $native_connection->sqliteCreateFunction('SI_VALUE', SiValueSort::sqliteSiValue(...), 1, \PDO::SQLITE_DETERMINISTIC);
} }
} }
}
return $connection; return $connection;

View file

@ -65,21 +65,16 @@ use Symfony\Component\Validator\Constraints as Assert;
new Post(securityPostDenormalize: 'is_granted("create", object)'), new Post(securityPostDenormalize: 'is_granted("create", object)'),
new Patch(security: 'is_granted("edit", object)'), new Patch(security: 'is_granted("edit", object)'),
new Delete(security: 'is_granted("delete", object)'), new Delete(security: 'is_granted("delete", object)'),
new GetCollection(
uriTemplate: '/attachment_types/{id}/children.{_format}',
uriVariables: ['id' => new Link(fromProperty: 'children', fromClass: AttachmentType::class)],
openapi: new Operation(summary: 'Retrieves the children elements of an attachment type.'),
security: 'is_granted("@attachment_types.read")'
),
], ],
normalizationContext: ['groups' => ['attachment_type:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'], normalizationContext: ['groups' => ['attachment_type:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'],
denormalizationContext: ['groups' => ['attachment_type:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'], denormalizationContext: ['groups' => ['attachment_type:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'],
)] )]
#[ApiResource(
uriTemplate: '/attachment_types/{id}/children.{_format}',
operations: [
new GetCollection(openapi: new Operation(summary: 'Retrieves the children elements of an attachment type.'),
security: 'is_granted("@attachment_types.read")')
],
uriVariables: [
'id' => new Link(fromProperty: 'children', fromClass: AttachmentType::class)
],
normalizationContext: ['groups' => ['attachment_type:read', 'api:basic:read'], 'openapi_definition_name' => 'Read']
)]
#[ApiFilter(PropertyFilter::class)] #[ApiFilter(PropertyFilter::class)]
#[ApiFilter(LikeFilter::class, properties: ["name", "comment"])] #[ApiFilter(LikeFilter::class, properties: ["name", "comment"])]
#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)] #[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)]

View file

@ -68,23 +68,16 @@ use Symfony\Component\Validator\Constraints as Assert;
new Post(securityPostDenormalize: 'is_granted("create", object)'), new Post(securityPostDenormalize: 'is_granted("create", object)'),
new Patch(security: 'is_granted("edit", object)'), new Patch(security: 'is_granted("edit", object)'),
new Delete(security: 'is_granted("delete", object)'), new Delete(security: 'is_granted("delete", object)'),
new GetCollection(
uriTemplate: '/categories/{id}/children.{_format}',
uriVariables: ['id' => new Link(fromProperty: 'children', fromClass: Category::class)],
openapi: new Operation(summary: 'Retrieves the children elements of a category.'),
security: 'is_granted("@categories.read")'
),
], ],
normalizationContext: ['groups' => ['category:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'], normalizationContext: ['groups' => ['category:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'],
denormalizationContext: ['groups' => ['category:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'], denormalizationContext: ['groups' => ['category:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'],
)] )]
#[ApiResource(
uriTemplate: '/categories/{id}/children.{_format}',
operations: [
new GetCollection(
openapi: new Operation(summary: 'Retrieves the children elements of a category.'),
security: 'is_granted("@categories.read")'
)
],
uriVariables: [
'id' => new Link(fromProperty: 'children', fromClass: Category::class)
],
normalizationContext: ['groups' => ['category:read', 'api:basic:read'], 'openapi_definition_name' => 'Read']
)]
#[ApiFilter(PropertyFilter::class)] #[ApiFilter(PropertyFilter::class)]
#[ApiFilter(LikeFilter::class, properties: ["name", "comment"])] #[ApiFilter(LikeFilter::class, properties: ["name", "comment"])]
#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)] #[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)]

View file

@ -67,23 +67,16 @@ use Symfony\Component\Validator\Constraints as Assert;
new Post(securityPostDenormalize: 'is_granted("create", object)'), new Post(securityPostDenormalize: 'is_granted("create", object)'),
new Patch(security: 'is_granted("edit", object)'), new Patch(security: 'is_granted("edit", object)'),
new Delete(security: 'is_granted("delete", object)'), new Delete(security: 'is_granted("delete", object)'),
new GetCollection(
uriTemplate: '/footprints/{id}/children.{_format}',
uriVariables: ['id' => new Link(fromProperty: 'children', fromClass: Footprint::class)],
openapi: new Operation(summary: 'Retrieves the children elements of a footprint.'),
security: 'is_granted("@footprints.read")'
),
], ],
normalizationContext: ['groups' => ['footprint:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'], normalizationContext: ['groups' => ['footprint:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'],
denormalizationContext: ['groups' => ['footprint:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'], denormalizationContext: ['groups' => ['footprint:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'],
)] )]
#[ApiResource(
uriTemplate: '/footprints/{id}/children.{_format}',
operations: [
new GetCollection(
openapi: new Operation(summary: 'Retrieves the children elements of a footprint.'),
security: 'is_granted("@footprints.read")'
)
],
uriVariables: [
'id' => new Link(fromProperty: 'children', fromClass: Footprint::class)
],
normalizationContext: ['groups' => ['footprint:read', 'api:basic:read'], 'openapi_definition_name' => 'Read']
)]
#[ApiFilter(PropertyFilter::class)] #[ApiFilter(PropertyFilter::class)]
#[ApiFilter(LikeFilter::class, properties: ["name", "comment"])] #[ApiFilter(LikeFilter::class, properties: ["name", "comment"])]
#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)] #[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)]

View file

@ -28,9 +28,11 @@ use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping\Column; use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Embeddable; use Doctrine\ORM\Mapping\Embeddable;
use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
/** /**
* This class represents a reference to a info provider inside a part. * This class represents a reference to an info provider inside a part.
* @see \App\Tests\Entity\Parts\InfoProviderReferenceTest * @see \App\Tests\Entity\Parts\InfoProviderReferenceTest
*/ */
#[Embeddable] #[Embeddable]
@ -157,4 +159,44 @@ class InfoProviderReference
$ref->last_updated = new \DateTimeImmutable(); $ref->last_updated = new \DateTimeImmutable();
return $ref; return $ref;
} }
/**
* Creates a reference to an info provider based on the given parameters.
* @param string|null $provider_key
* @param string|null $provider_id
* @param string|null $provider_url
* @param \DateTimeImmutable|null $last_updated
* @return self
*/
public static function create(?string $provider_key, ?string $provider_id, ?string $provider_url, ?\DateTimeImmutable $last_updated): self
{
$ref = new InfoProviderReference();
$ref->provider_key = $provider_key;
$ref->provider_id = $provider_id;
$ref->provider_url = $provider_url;
$ref->last_updated = $last_updated;
return $ref;
}
#[Assert\Callback()]
public function validate(ExecutionContextInterface $context, mixed $payload): void
{
if ($this->provider_key === null && $this->provider_id !== null) {
$context->buildViolation('info_providers.validation.provider_id_without_key')
->atPath('provider_key')
->addViolation();
}
if ($this->provider_key === null && $this->provider_url !== null) {
$context->buildViolation('info_providers.validation.provider_url_without_key')
->atPath('provider_url')
->addViolation();
}
if ($this->provider_key !== null && $this->provider_id === null) {
$context->buildViolation('info_providers.validation.provider_key_without_id')
->atPath('provider_id')
->addViolation();
}
}
} }

View file

@ -66,23 +66,16 @@ use Symfony\Component\Validator\Constraints as Assert;
new Post(securityPostDenormalize: 'is_granted("create", object)'), new Post(securityPostDenormalize: 'is_granted("create", object)'),
new Patch(security: 'is_granted("edit", object)'), new Patch(security: 'is_granted("edit", object)'),
new Delete(security: 'is_granted("delete", object)'), new Delete(security: 'is_granted("delete", object)'),
new GetCollection(
uriTemplate: '/manufacturers/{id}/children.{_format}',
uriVariables: ['id' => new Link(fromProperty: 'children', fromClass: Manufacturer::class)],
openapi: new Operation(summary: 'Retrieves the children elements of a manufacturer.'),
security: 'is_granted("@manufacturers.read")'
),
], ],
normalizationContext: ['groups' => ['manufacturer:read', 'company:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'], normalizationContext: ['groups' => ['manufacturer:read', 'company:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'],
denormalizationContext: ['groups' => ['manufacturer:write', 'company:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'], denormalizationContext: ['groups' => ['manufacturer:write', 'company:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'],
)] )]
#[ApiResource(
uriTemplate: '/manufacturers/{id}/children.{_format}',
operations: [
new GetCollection(
openapi: new Operation(summary: 'Retrieves the children elements of a manufacturer.'),
security: 'is_granted("@manufacturers.read")'
)
],
uriVariables: [
'id' => new Link(fromProperty: 'children', fromClass: Manufacturer::class)
],
normalizationContext: ['groups' => ['manufacturer:read', 'company:read', 'api:basic:read'], 'openapi_definition_name' => 'Read']
)]
#[ApiFilter(PropertyFilter::class)] #[ApiFilter(PropertyFilter::class)]
#[ApiFilter(LikeFilter::class, properties: ["name", "comment"])] #[ApiFilter(LikeFilter::class, properties: ["name", "comment"])]
#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)] #[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)]

View file

@ -71,23 +71,16 @@ use Symfony\Component\Validator\Constraints\Length;
new Post(securityPostDenormalize: 'is_granted("create", object)'), new Post(securityPostDenormalize: 'is_granted("create", object)'),
new Patch(security: 'is_granted("edit", object)'), new Patch(security: 'is_granted("edit", object)'),
new Delete(security: 'is_granted("delete", object)'), new Delete(security: 'is_granted("delete", object)'),
new GetCollection(
uriTemplate: '/measurement_units/{id}/children.{_format}',
uriVariables: ['id' => new Link(fromProperty: 'children', fromClass: MeasurementUnit::class)],
openapi: new Operation(summary: 'Retrieves the children elements of a MeasurementUnit.'),
security: 'is_granted("@measurement_units.read")'
),
], ],
normalizationContext: ['groups' => ['measurement_unit:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'], normalizationContext: ['groups' => ['measurement_unit:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'],
denormalizationContext: ['groups' => ['measurement_unit:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'], denormalizationContext: ['groups' => ['measurement_unit:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'],
)] )]
#[ApiResource(
uriTemplate: '/measurement_units/{id}/children.{_format}',
operations: [
new GetCollection(
openapi: new Operation(summary: 'Retrieves the children elements of a MeasurementUnit.'),
security: 'is_granted("@measurement_units.read")'
)
],
uriVariables: [
'id' => new Link(fromProperty: 'children', fromClass: MeasurementUnit::class)
],
normalizationContext: ['groups' => ['measurement_unit:read', 'api:basic:read'], 'openapi_definition_name' => 'Read']
)]
#[ApiFilter(PropertyFilter::class)] #[ApiFilter(PropertyFilter::class)]
#[ApiFilter(LikeFilter::class, properties: ["name", "comment", "unit"])] #[ApiFilter(LikeFilter::class, properties: ["name", "comment", "unit"])]
#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)] #[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)]

View file

@ -75,6 +75,7 @@ trait AdvancedPropertyTrait
*/ */
#[ORM\Embedded(class: InfoProviderReference::class, columnPrefix: 'provider_reference_')] #[ORM\Embedded(class: InfoProviderReference::class, columnPrefix: 'provider_reference_')]
#[Groups(['full', 'part:read'])] #[Groups(['full', 'part:read'])]
#[Assert\Valid()]
protected InfoProviderReference $providerReference; protected InfoProviderReference $providerReference;
/** /**

View file

@ -67,23 +67,16 @@ use Symfony\Component\Validator\Constraints as Assert;
new Post(securityPostDenormalize: 'is_granted("create", object)'), new Post(securityPostDenormalize: 'is_granted("create", object)'),
new Patch(security: 'is_granted("edit", object)'), new Patch(security: 'is_granted("edit", object)'),
new Delete(security: 'is_granted("delete", object)'), new Delete(security: 'is_granted("delete", object)'),
new GetCollection(
uriTemplate: '/storage_locations/{id}/children.{_format}',
uriVariables: ['id' => new Link(fromProperty: 'children', fromClass: StorageLocation::class)],
openapi: new Operation(summary: 'Retrieves the children elements of a storage location.'),
security: 'is_granted("@storelocations.read")'
),
], ],
normalizationContext: ['groups' => ['location:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'], normalizationContext: ['groups' => ['location:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'],
denormalizationContext: ['groups' => ['location:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'], denormalizationContext: ['groups' => ['location:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'],
)] )]
#[ApiResource(
uriTemplate: '/storage_locations/{id}/children.{_format}',
operations: [
new GetCollection(
openapi: new Operation(summary: 'Retrieves the children elements of a storage location.'),
security: 'is_granted("@storelocations.read")'
)
],
uriVariables: [
'id' => new Link(fromProperty: 'children', fromClass: StorageLocation::class)
],
normalizationContext: ['groups' => ['location:read', 'api:basic:read'], 'openapi_definition_name' => 'Read']
)]
#[ApiFilter(PropertyFilter::class)] #[ApiFilter(PropertyFilter::class)]
#[ApiFilter(LikeFilter::class, properties: ["name", "comment"])] #[ApiFilter(LikeFilter::class, properties: ["name", "comment"])]
#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)] #[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)]

View file

@ -71,21 +71,16 @@ use Symfony\Component\Validator\Constraints as Assert;
new Post(securityPostDenormalize: 'is_granted("create", object)'), new Post(securityPostDenormalize: 'is_granted("create", object)'),
new Patch(security: 'is_granted("edit", object)'), new Patch(security: 'is_granted("edit", object)'),
new Delete(security: 'is_granted("delete", object)'), new Delete(security: 'is_granted("delete", object)'),
new GetCollection(
uriTemplate: '/suppliers/{id}/children.{_format}',
uriVariables: ['id' => new Link(fromProperty: 'children', fromClass: Supplier::class)],
openapi: new Operation(summary: 'Retrieves the children elements of a supplier.'),
security: 'is_granted("@manufacturers.read")'
),
], ],
normalizationContext: ['groups' => ['supplier:read', 'company:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'], normalizationContext: ['groups' => ['supplier:read', 'company:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'],
denormalizationContext: ['groups' => ['supplier:write', 'company:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'], denormalizationContext: ['groups' => ['supplier:write', 'company:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'],
)] )]
#[ApiResource(
uriTemplate: '/suppliers/{id}/children.{_format}',
operations: [new GetCollection(
openapi: new Operation(summary: 'Retrieves the children elements of a supplier.'),
security: 'is_granted("@manufacturers.read")'
)],
uriVariables: [
'id' => new Link(fromProperty: 'children', fromClass: Supplier::class)
],
normalizationContext: ['groups' => ['supplier:read', 'company:read', 'api:basic:read'], 'openapi_definition_name' => 'Read']
)]
#[ApiFilter(PropertyFilter::class)] #[ApiFilter(PropertyFilter::class)]
#[ApiFilter(LikeFilter::class, properties: ["name", "comment"])] #[ApiFilter(LikeFilter::class, properties: ["name", "comment"])]
#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)] #[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)]

View file

@ -71,23 +71,16 @@ use Symfony\Component\Validator\Constraints as Assert;
new Post(securityPostDenormalize: 'is_granted("create", object)'), new Post(securityPostDenormalize: 'is_granted("create", object)'),
new Patch(security: 'is_granted("edit", object)'), new Patch(security: 'is_granted("edit", object)'),
new Delete(security: 'is_granted("delete", object)'), new Delete(security: 'is_granted("delete", object)'),
new GetCollection(
uriTemplate: '/currencies/{id}/children.{_format}',
uriVariables: ['id' => new Link(fromProperty: 'children', fromClass: Currency::class)],
openapi: new Operation(summary: 'Retrieves the children elements of a currency.'),
security: 'is_granted("@currencies.read")'
),
], ],
normalizationContext: ['groups' => ['currency:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'], normalizationContext: ['groups' => ['currency:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'],
denormalizationContext: ['groups' => ['currency:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'], denormalizationContext: ['groups' => ['currency:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'],
)] )]
#[ApiResource(
uriTemplate: '/currencies/{id}/children.{_format}',
operations: [
new GetCollection(
openapi: new Operation(summary: 'Retrieves the children elements of a currency.'),
security: 'is_granted("@currencies.read")'
)
],
uriVariables: [
'id' => new Link(fromProperty: 'children', fromClass: Currency::class)
],
normalizationContext: ['groups' => ['currency:read', 'api:basic:read'], 'openapi_definition_name' => 'Read']
)]
#[ApiFilter(PropertyFilter::class)] #[ApiFilter(PropertyFilter::class)]
#[ApiFilter(LikeFilter::class, properties: ["name", "comment", "iso_code"])] #[ApiFilter(LikeFilter::class, properties: ["name", "comment", "iso_code"])]
#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)] #[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)]

View file

@ -71,23 +71,17 @@ use Symfony\Component\Validator\Constraints\Length;
new Post(securityPostDenormalize: 'is_granted("create", object)'), new Post(securityPostDenormalize: 'is_granted("create", object)'),
new Patch(security: 'is_granted("edit", object)'), new Patch(security: 'is_granted("edit", object)'),
new Delete(security: 'is_granted("delete", object)'), new Delete(security: 'is_granted("delete", object)'),
new GetCollection(
uriTemplate: '/parts/{id}/orderdetails.{_format}',
uriVariables: ['id' => new Link(toProperty: 'part', fromClass: Part::class)],
normalizationContext: ['groups' => ['orderdetail:read', 'pricedetail:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'],
openapi: new Operation(summary: 'Retrieves the orderdetails of a part.'),
security: 'is_granted("@parts.read")'
),
], ],
normalizationContext: ['groups' => ['orderdetail:read', 'orderdetail:read:standalone', 'api:basic:read', 'pricedetail:read'], 'openapi_definition_name' => 'Read'], normalizationContext: ['groups' => ['orderdetail:read', 'orderdetail:read:standalone', 'api:basic:read', 'pricedetail:read'], 'openapi_definition_name' => 'Read'],
denormalizationContext: ['groups' => ['orderdetail:write', 'api:basic:write'], 'openapi_definition_name' => 'Write'], denormalizationContext: ['groups' => ['orderdetail:write', 'api:basic:write'], 'openapi_definition_name' => 'Write'],
)] )]
#[ApiResource(
uriTemplate: '/parts/{id}/orderdetails.{_format}',
operations: [
new GetCollection(
openapi: new Operation(summary: 'Retrieves the orderdetails of a part.'),
security: 'is_granted("@parts.read")'
)
],
uriVariables: [
'id' => new Link(toProperty: 'part', fromClass: Part::class)
],
normalizationContext: ['groups' => ['orderdetail:read', 'pricedetail:read', 'api:basic:read'], 'openapi_definition_name' => 'Read']
)]
#[ApiFilter(PropertyFilter::class)] #[ApiFilter(PropertyFilter::class)]
#[ApiFilter(PropertyFilter::class)] #[ApiFilter(PropertyFilter::class)]
#[ApiFilter(LikeFilter::class, properties: ["supplierpartnr", "supplier_product_url"])] #[ApiFilter(LikeFilter::class, properties: ["supplierpartnr", "supplier_product_url"])]

View file

@ -66,23 +66,16 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
new Post(securityPostDenormalize: 'is_granted("create", object)'), new Post(securityPostDenormalize: 'is_granted("create", object)'),
new Patch(security: 'is_granted("edit", object)'), new Patch(security: 'is_granted("edit", object)'),
new Delete(security: 'is_granted("delete", object)'), new Delete(security: 'is_granted("delete", object)'),
new GetCollection(
uriTemplate: '/projects/{id}/children.{_format}',
uriVariables: ['id' => new Link(fromProperty: 'children', fromClass: Project::class)],
openapi: new Operation(summary: 'Retrieves the children elements of a project.'),
security: 'is_granted("@projects.read")'
),
], ],
normalizationContext: ['groups' => ['project:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'], normalizationContext: ['groups' => ['project:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'],
denormalizationContext: ['groups' => ['project:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'], denormalizationContext: ['groups' => ['project:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'],
)] )]
#[ApiResource(
uriTemplate: '/projects/{id}/children.{_format}',
operations: [
new GetCollection(
openapi: new Operation(summary: 'Retrieves the children elements of a project.'),
security: 'is_granted("@projects.read")'
)
],
uriVariables: [
'id' => new Link(fromProperty: 'children', fromClass: Project::class)
],
normalizationContext: ['groups' => ['project:read', 'api:basic:read'], 'openapi_definition_name' => 'Read']
)]
#[ApiFilter(PropertyFilter::class)] #[ApiFilter(PropertyFilter::class)]
#[ApiFilter(LikeFilter::class, properties: ["name", "comment"])] #[ApiFilter(LikeFilter::class, properties: ["name", "comment"])]
#[ApiFilter(OrderFilter::class, properties: ['name', 'id', 'addedDate', 'lastModified'])] #[ApiFilter(OrderFilter::class, properties: ['name', 'id', 'addedDate', 'lastModified'])]
@ -117,7 +110,7 @@ class Project extends AbstractStructuralDBElement
/** /**
* @var string|null The current status of the project * @var string|null The current status of the project
*/ */
#[Assert\Choice(['draft', 'planning', 'in_production', 'finished', 'archived'])] #[Assert\Choice(choices: ['draft', 'planning', 'in_production', 'finished', 'archived'])]
#[Groups(['extended', 'full', 'project:read', 'project:write', 'import'])] #[Groups(['extended', 'full', 'project:read', 'project:write', 'import'])]
#[ORM\Column(type: Types::STRING, length: 64, nullable: true)] #[ORM\Column(type: Types::STRING, length: 64, nullable: true)]
protected ?string $status = null; protected ?string $status = null;

View file

@ -63,23 +63,16 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
new Post(uriTemplate: '/project_bom_entries.{_format}', securityPostDenormalize: 'is_granted("create", object)',), new Post(uriTemplate: '/project_bom_entries.{_format}', securityPostDenormalize: 'is_granted("create", object)',),
new Patch(uriTemplate: '/project_bom_entries/{id}.{_format}', security: 'is_granted("edit", object)',), new Patch(uriTemplate: '/project_bom_entries/{id}.{_format}', security: 'is_granted("edit", object)',),
new Delete(uriTemplate: '/project_bom_entries/{id}.{_format}', security: 'is_granted("delete", object)',), new Delete(uriTemplate: '/project_bom_entries/{id}.{_format}', security: 'is_granted("delete", object)',),
new GetCollection(
uriTemplate: '/projects/{id}/bom.{_format}',
uriVariables: ['id' => new Link(fromProperty: 'bom_entries', fromClass: Project::class)],
openapi: new Operation(summary: 'Retrieves the BOM entries of the given project.'),
security: 'is_granted("@projects.read")'
),
], ],
normalizationContext: ['groups' => ['bom_entry:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'], normalizationContext: ['groups' => ['bom_entry:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'],
denormalizationContext: ['groups' => ['bom_entry:write', 'api:basic:write'], 'openapi_definition_name' => 'Write'], denormalizationContext: ['groups' => ['bom_entry:write', 'api:basic:write'], 'openapi_definition_name' => 'Write'],
)] )]
#[ApiResource(
uriTemplate: '/projects/{id}/bom.{_format}',
operations: [
new GetCollection(
openapi: new Operation(summary: 'Retrieves the BOM entries of the given project.'),
security: 'is_granted("@projects.read")'
)
],
uriVariables: [
'id' => new Link(fromProperty: 'bom_entries', fromClass: Project::class)
],
normalizationContext: ['groups' => ['bom_entry:read', 'api:basic:read'], 'openapi_definition_name' => 'Read']
)]
#[ApiFilter(PropertyFilter::class)] #[ApiFilter(PropertyFilter::class)]
#[ApiFilter(LikeFilter::class, properties: ["name", "comment", 'mountnames'])] #[ApiFilter(LikeFilter::class, properties: ["name", "comment", 'mountnames'])]
#[ApiFilter(RangeFilter::class, properties: ['quantity'])] #[ApiFilter(RangeFilter::class, properties: ['quantity'])]

View file

@ -0,0 +1,79 @@
<?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\Extension;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
/**
* Catches timezone mismatches between a DateTimeInterface model value and the effective
* model_timezone configured on the field.
*
* Doctrine's UTCDateTimeImmutableType always returns UTC DateTimeImmutable objects, so any
* date/datetime field that omits `model_timezone: 'UTC'` will silently corrupt stored values
* (the transformer treats the UTC instant as if it were in the user's local timezone).
* This extension throws a \LogicException early so the mistake is caught at development time.
*/
class DateTimeModelTimezoneExtension extends AbstractTypeExtension
{
public static function getExtendedTypes(): iterable
{
return [DateTimeType::class, DateType::class];
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->addEventListener(FormEvents::POST_SET_DATA, static function (FormEvent $event) use ($options): void {
$data = $event->getData();
if (!$data instanceof \DateTimeInterface) {
return;
}
// Resolve the effective model timezone: explicit option or the PHP default set at build time.
// This mirrors what BaseDateTimeTransformer does in its constructor.
$modelTimezone = $options['model_timezone'] ?? date_default_timezone_get();
$dataOffset = $data->getTimezone()->getOffset($data);
$modelOffset = (new \DateTimeZone($modelTimezone))->getOffset($data);
if ($dataOffset !== $modelOffset) {
throw new \LogicException(sprintf(
'Form field "%s" received a %s with timezone "%s" (UTC offset %+d s), '
. 'but the effective model_timezone is "%s" (UTC offset %+d s). '
. 'Set the "model_timezone" option to match the timezone of your data source.',
$event->getForm()->getName(),
get_debug_type($data),
$data->getTimezone()->getName(),
$dataOffset,
$modelTimezone,
$modelOffset
));
}
});
}
}

View file

@ -0,0 +1,113 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace App\Form\InfoProviderSystem;
use App\Entity\Parts\InfoProviderReference;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\DataMapperInterface;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\UrlType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class InfoProviderReferenceType extends AbstractType implements DataMapperInterface
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->setDataMapper($this)
->add('provider_key', ProviderSelectType::class, [
'label' => 'info_providers.provider_key',
'input' => 'string',
'multiple' => false,
'required' => false,
'only_active' => false,
])
->add('provider_id', TextType::class, [
'label' => 'info_providers.provider_id',
'required' => false,
])
->add('provider_url', UrlType::class, [
'label' => 'info_providers.provider_url',
'required' => false,
])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => InfoProviderReference::class,
]);
}
public function mapDataToForms(mixed $viewData, \Traversable $forms): void
{
if ($viewData === null) {
return;
}
if (!$viewData instanceof InfoProviderReference) {
return;
}
/** @var FormInterface[] $forms */
$forms = iterator_to_array($forms);
$forms['provider_key']->setData($viewData->getProviderKey());
$forms['provider_id']->setData($viewData->getProviderId());
$forms['provider_url']->setData($viewData->getProviderUrl());
}
public function mapFormsToData(\Traversable $forms, mixed &$viewData): void
{
/** @var FormInterface[] $forms */
$forms = iterator_to_array($forms);
$providerKey = $forms['provider_key']->getData();
$providerId = $forms['provider_id']->getData();
$providerUrl = $forms['provider_url']->getData();
if ($viewData === null) {
$viewData = InfoProviderReference::noProvider();
}
if (!$viewData instanceof InfoProviderReference) {
return;
}
$oldDate = $viewData->getLastUpdated();
//If all fields are empty, we set the view data to a new instance without provider information
if ($providerKey === null && $providerId === null && $providerUrl === null) {
$viewData = InfoProviderReference::noProvider();
return;
}
$viewData = InfoProviderReference::create($providerKey, $providerId, $providerUrl, $oldDate);
}
}

View file

@ -31,12 +31,12 @@ use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Translation\StaticMessage; use Symfony\Component\Translation\StaticMessage;
use Symfony\Component\Translation\TranslatableMessage;
class ProviderSelectType extends AbstractType class ProviderSelectType extends AbstractType
{ {
public function __construct(private readonly ProviderRegistry $providerRegistry) public function __construct(private readonly ProviderRegistry $providerRegistry)
{ {
} }
public function getParent(): string public function getParent(): string
@ -46,17 +46,22 @@ class ProviderSelectType extends AbstractType
public function configureOptions(OptionsResolver $resolver): void public function configureOptions(OptionsResolver $resolver): void
{ {
$providers = $this->providerRegistry->getActiveProviders();
$resolver->setDefault('input', 'object'); $resolver->setDefault('input', 'object');
$resolver->setAllowedTypes('input', 'string'); $resolver->setAllowedTypes('input', 'string');
//Either the form returns the provider objects or their keys //Either the form returns the provider objects or their keys
$resolver->setAllowedValues('input', ['object', 'string']); $resolver->setAllowedValues('input', ['object', 'string']);
$resolver->setDefault('multiple', true); $resolver->setDefault('multiple', true);
$resolver->setDefault('choices', function (Options $options) use ($providers) { //Only show active providers in the list, or also inactive ones
$resolver->setDefault('only_active', true);
$resolver->setAllowedTypes('only_active', 'bool');
$resolver->setDefault('choices', function (Options $options) {
$providers = $options['only_active'] ? $this->providerRegistry->getActiveProviders() : $this->providerRegistry->getProviders();
if ('object' === $options['input']) { if ('object' === $options['input']) {
return $this->providerRegistry->getActiveProviders(); return $providers;
} }
$tmp = []; $tmp = [];
@ -69,20 +74,35 @@ class ProviderSelectType extends AbstractType
}); });
//The choice_label and choice_value only needs to be set if we want the objects //The choice_label and choice_value only needs to be set if we want the objects
$resolver->setDefault('choice_label', function (Options $options){ $resolver->setDefault('choice_label', function (Options $options) {
if ('object' === $options['input']) { if ('object' === $options['input']) {
return ChoiceList::label($this, static fn (?InfoProviderInterface $choice) => new StaticMessage($choice?->getProviderInfo()['name'])); return ChoiceList::label($this, static fn(?InfoProviderInterface $choice
) => new StaticMessage($choice?->getProviderInfo()['name']));
} }
return static fn ($choice, $key, $value) => new StaticMessage($key); return static fn($choice, $key, $value) => new StaticMessage($key);
}); });
$resolver->setDefault('choice_value', function (Options $options) { $resolver->setDefault('choice_value', function (Options $options) {
if ('object' === $options['input']) { if ('object' === $options['input']) {
return ChoiceList::value($this, static fn(?InfoProviderInterface $choice) => $choice?->getProviderKey()); return ChoiceList::value($this,
static fn(?InfoProviderInterface $choice) => $choice?->getProviderKey());
} }
return null; return null;
}); });
$resolver->setDefault('group_by', function (Options $options) {
//Do not show groups when only active providers are shown, because then all providers are active and the group would be useless
if ($options['only_active']) {
return null;
}
return function ($choice, $key, string $value) {
if ($this->providerRegistry->getProviderByKey($value)->isActive()) {
return new TranslatableMessage('info_providers.providers_list.active');
}
return new TranslatableMessage('info_providers.providers_list.disabled');
};
});
} }
} }

View file

@ -33,6 +33,7 @@ use App\Entity\Parts\Part;
use App\Entity\Parts\PartCustomState; use App\Entity\Parts\PartCustomState;
use App\Entity\PriceInformations\Orderdetail; use App\Entity\PriceInformations\Orderdetail;
use App\Form\AttachmentFormType; use App\Form\AttachmentFormType;
use App\Form\InfoProviderSystem\InfoProviderReferenceType;
use App\Form\ParameterType; use App\Form\ParameterType;
use App\Form\Part\EDA\EDAPartInfoType; use App\Form\Part\EDA\EDAPartInfoType;
use App\Form\Type\MasterPictureAttachmentType; use App\Form\Type\MasterPictureAttachmentType;
@ -225,6 +226,10 @@ class PartBaseType extends AbstractType
'empty_data' => null, 'empty_data' => null,
'label' => 'part.gtin', 'label' => 'part.gtin',
]) ])
->add('providerReference', InfoProviderReferenceType::class, [
'label' => false,
'required' => false,
])
; ;
//Comment section //Comment section

View file

@ -115,8 +115,10 @@ class PartLotType extends AbstractType
$builder->add('last_stocktake_at', DateTimeType::class, [ $builder->add('last_stocktake_at', DateTimeType::class, [
'label' => 'part_lot.edit.last_stocktake_at', 'label' => 'part_lot.edit.last_stocktake_at',
'widget' => 'single_text', 'widget' => 'single_text',
'model_timezone' => 'UTC', // The database stores the datetime in UTC, so we need to set the model timezone to UTC
'disabled' => !$this->security->isGranted('@parts_stock.stocktake'), 'disabled' => !$this->security->isGranted('@parts_stock.stocktake'),
'required' => false, 'required' => false,
'with_seconds' => true,
]); ]);
} }

View file

@ -38,7 +38,7 @@ class AttachmentTypeType extends AbstractType
return StructuralEntityType::class; return StructuralEntityType::class;
} }
public function configureOptions(OptionsResolver $resolver) public function configureOptions(OptionsResolver $resolver): void
{ {
$resolver->define('attachment_filter_class')->allowedTypes('null', 'string')->default(null); $resolver->define('attachment_filter_class')->allowedTypes('null', 'string')->default(null);

View file

@ -84,8 +84,10 @@ final class ProjectBuildRequest
$remaining_amount = $this->getNeededAmountForBOMEntry($bom_entry); $remaining_amount = $this->getNeededAmountForBOMEntry($bom_entry);
foreach($this->getPartLotsForBOMEntry($bom_entry) as $lot) { foreach($this->getPartLotsForBOMEntry($bom_entry) as $lot) {
//If the lot has instock use it for the build //If the lot has instock use it for the build
$this->withdraw_amounts[$lot->getID()] = min($remaining_amount, $lot->getAmount()); $id = $lot->getID() ?? throw new \RuntimeException("Part lot needs to have an ID!");
$remaining_amount -= max(0, $this->withdraw_amounts[$lot->getID()]);
$this->withdraw_amounts[$id] = min($remaining_amount, $lot->getAmount());
$remaining_amount -= max(0, $this->withdraw_amounts[$id]);
} }
} }
} }
@ -176,6 +178,10 @@ final class ProjectBuildRequest
{ {
$lot_id = $lot instanceof PartLot ? $lot->getID() : $lot; $lot_id = $lot instanceof PartLot ? $lot->getID() : $lot;
if ($lot_id === null) {
throw new \InvalidArgumentException('The given lot must have an ID!');
}
if (! array_key_exists($lot_id, $this->withdraw_amounts)) { if (! array_key_exists($lot_id, $this->withdraw_amounts)) {
throw new \InvalidArgumentException('The given lot is not in the withdraw amounts array!'); throw new \InvalidArgumentException('The given lot is not in the withdraw amounts array!');
} }
@ -192,10 +198,12 @@ final class ProjectBuildRequest
{ {
if ($lot instanceof PartLot) { if ($lot instanceof PartLot) {
$lot_id = $lot->getID(); $lot_id = $lot->getID();
} elseif (is_int($lot)) {
$lot_id = $lot;
} else { } else {
throw new \InvalidArgumentException('The given lot must be an instance of PartLot or an ID of a PartLot!'); $lot_id = $lot;
}
if ($lot_id === null) {
throw new \InvalidArgumentException('The given lot must have an ID!');
} }
$this->withdraw_amounts[$lot_id] = $amount; $this->withdraw_amounts[$lot_id] = $amount;
@ -296,7 +304,7 @@ final class ProjectBuildRequest
* @param bool $dont_check_quantity * @param bool $dont_check_quantity
* @return $this * @return $this
*/ */
public function setDontCheckQuantity(bool $dont_check_quantity): ProjectBuildRequest public function setDontCheckQuantity(bool $dont_check_quantity): self
{ {
$this->dont_check_quantity = $dont_check_quantity; $this->dont_check_quantity = $dont_check_quantity;
return $this; return $this;

View file

@ -158,7 +158,6 @@ class DBElementRepository extends EntityRepository
{ {
$reflection = new ReflectionClass($element::class); $reflection = new ReflectionClass($element::class);
$property = $reflection->getProperty($field); $property = $reflection->getProperty($field);
$property->setAccessible(true);
$property->setValue($element, $new_value); $property->setValue($element, $new_value);
} }
} }

View file

@ -24,6 +24,7 @@ declare(strict_types=1);
namespace App\Services\AI; namespace App\Services\AI;
use App\Settings\AISettings\LMStudioSettings; use App\Settings\AISettings\LMStudioSettings;
use App\Settings\AISettings\OllamaSettings;
use App\Settings\AISettings\OpenRouterSettings; use App\Settings\AISettings\OpenRouterSettings;
use Symfony\Contracts\Translation\TranslatableInterface; use Symfony\Contracts\Translation\TranslatableInterface;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
@ -32,6 +33,7 @@ enum AIPlatforms: string implements TranslatableInterface
{ {
case OPENROUTER = 'openrouter'; case OPENROUTER = 'openrouter';
case LMSTUDIO = 'lmstudio'; case LMSTUDIO = 'lmstudio';
case OLLAMA = 'ollama';
/** /**
* Returns the name attribute of the service tag for this platform, which is used to register the platform in the AIPlatformRegistry * Returns the name attribute of the service tag for this platform, which is used to register the platform in the AIPlatformRegistry
@ -52,6 +54,7 @@ enum AIPlatforms: string implements TranslatableInterface
return match ($this) { return match ($this) {
self::LMSTUDIO => LMStudioSettings::class, self::LMSTUDIO => LMStudioSettings::class,
self::OPENROUTER => OpenRouterSettings::class, self::OPENROUTER => OpenRouterSettings::class,
self::OLLAMA => OllamaSettings::class,
}; };
} }

View file

@ -233,7 +233,6 @@ trait PKImportHelperTrait
$reflectionClass = new \ReflectionClass($entity); $reflectionClass = new \ReflectionClass($entity);
$property = $reflectionClass->getProperty('addedDate'); $property = $reflectionClass->getProperty('addedDate');
$property->setAccessible(true);
$property->setValue($entity, $date); $property->setValue($entity, $date);
} }

View file

@ -42,7 +42,7 @@ final class DTOJsonSchemaConverter
public function getJSONSchema(): array public function getJSONSchema(): array
{ {
return [ return [
'name' => 'clock', 'name' => 'part_detail',
'strict' => true, 'strict' => true,
'schema' => [ 'schema' => [
'type' => 'object', 'type' => 'object',

View file

@ -282,6 +282,10 @@ final class AIWebProvider implements InfoProviderInterface
try { try {
$aiPlatform = $this->AIPlatformRegistry->getPlatform($this->settings->platform ?? throw new \RuntimeException('No AI platform selected') ); $aiPlatform = $this->AIPlatformRegistry->getPlatform($this->settings->platform ?? throw new \RuntimeException('No AI platform selected') );
// AI inference can take much longer than PHP's default max_execution_time (typically 30s).
// The HTTP client timeout already enforces the configured limit; disable PHP's constraint here.
set_time_limit(0);
//'openai/gpt-5-mini' //'openai/gpt-5-mini'
$result = $aiPlatform->invoke($this->settings->model ?? throw new \RuntimeException('No model selected'), $input, [ $result = $aiPlatform->invoke($this->settings->model ?? throw new \RuntimeException('No model selected'), $input, [
'response_format' => [ 'response_format' => [

View file

@ -35,9 +35,14 @@ class AISettings
{ {
use SettingsTrait; use SettingsTrait;
public const TIMEOUT_LIMIT = 600;
#[EmbeddedSettings] #[EmbeddedSettings]
public ?OpenRouterSettings $openRouter = null; public ?OpenRouterSettings $openRouter = null;
#[EmbeddedSettings] #[EmbeddedSettings]
public ?LMStudioSettings $lmstudio = null; public ?LMStudioSettings $lmstudio = null;
#[EmbeddedSettings]
public ?OllamaSettings $ollama = null;
} }

View file

@ -23,16 +23,17 @@ declare(strict_types=1);
namespace App\Settings\AISettings; namespace App\Settings\AISettings;
use App\Form\Type\APIKeyType;
use App\Services\AI\AIPlatformSettingsInterface; use App\Services\AI\AIPlatformSettingsInterface;
use App\Settings\SettingsIcon; use App\Settings\SettingsIcon;
use Jbtronics\SettingsBundle\Metadata\EnvVarMode; use Jbtronics\SettingsBundle\Metadata\EnvVarMode;
use Jbtronics\SettingsBundle\Settings\Settings; use Jbtronics\SettingsBundle\Settings\Settings;
use Jbtronics\SettingsBundle\Settings\SettingsParameter; use Jbtronics\SettingsBundle\Settings\SettingsParameter;
use Jbtronics\SettingsBundle\Settings\SettingsTrait; use Jbtronics\SettingsBundle\Settings\SettingsTrait;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\Extension\Core\Type\UrlType; use Symfony\Component\Form\Extension\Core\Type\UrlType;
use Symfony\Component\Translation\StaticMessage; use Symfony\Component\Translation\StaticMessage;
use Symfony\Component\Translation\TranslatableMessage as TM; use Symfony\Component\Translation\TranslatableMessage as TM;
use Symfony\Component\Validator\Constraints as Assert;
#[Settings(name: 'ai_lmstudio', label: new TM("settings.ai.lmstudio"))] #[Settings(name: 'ai_lmstudio', label: new TM("settings.ai.lmstudio"))]
#[SettingsIcon("fa-robot")] #[SettingsIcon("fa-robot")]
@ -46,6 +47,14 @@ class LMStudioSettings implements AIPlatformSettingsInterface
envVar: "AI_LMSTUDIO_HOSTURL", envVarMode: EnvVarMode::OVERWRITE)] envVar: "AI_LMSTUDIO_HOSTURL", envVarMode: EnvVarMode::OVERWRITE)]
public ?string $hostURL = null; public ?string $hostURL = null;
#[SettingsParameter(label: new TM("settings.ai.timeout"),
description: new TM("settings.ai.timeout.help"),
formType: NumberType::class,
formOptions: ["scale" => 0, "attr" => ["min" => 1]],
)]
#[Assert\Range(min: 1, max: AISettings::TIMEOUT_LIMIT)]
public int $timeout = 180;
public function isAIPlatformEnabled(): bool public function isAIPlatformEnabled(): bool
{ {
return $this->hostURL !== null && $this->hostURL !== ""; return $this->hostURL !== null && $this->hostURL !== "";

View file

@ -0,0 +1,68 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace App\Settings\AISettings;
use App\Form\Type\APIKeyType;
use App\Services\AI\AIPlatformSettingsInterface;
use App\Settings\SettingsIcon;
use Jbtronics\SettingsBundle\Metadata\EnvVarMode;
use Jbtronics\SettingsBundle\Settings\Settings;
use Jbtronics\SettingsBundle\Settings\SettingsParameter;
use Jbtronics\SettingsBundle\Settings\SettingsTrait;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\Extension\Core\Type\UrlType;
use Symfony\Component\Translation\StaticMessage;
use Symfony\Component\Translation\TranslatableMessage as TM;
use Symfony\Component\Validator\Constraints as Assert;
#[Settings(name: 'ai_ollama', label: new TM("settings.ai.ollama"))]
#[SettingsIcon("fa-robot")]
class OllamaSettings implements AIPlatformSettingsInterface
{
use SettingsTrait;
#[SettingsParameter(label: new TM("settings.ai.ollama.endpoint"),
formType: UrlType::class,
formOptions: ["attr" => ["placeholder" => new StaticMessage("http://localhost:11434")]],
envVar: "AI_OLLAMA_ENDPOINT", envVarMode: EnvVarMode::OVERWRITE)]
public ?string $endpoint = null;
#[SettingsParameter(label: new TM("settings.ai.ollama.apiKey"),
formType: APIKeyType::class,
envVar: "AI_OLLAMA_API_KEY", envVarMode: EnvVarMode::OVERWRITE)]
public ?string $apiKey = null;
#[SettingsParameter(label: new TM("settings.ai.timeout"),
description: new TM("settings.ai.timeout.help"),
formType: NumberType::class,
formOptions: ["scale" => 0, "attr" => ["min" => 1]]
)]
#[Assert\Range(min: 1, max: AISettings::TIMEOUT_LIMIT)]
public int $timeout = 180;
public function isAIPlatformEnabled(): bool
{
return $this->endpoint !== null && $this->endpoint !== "";
}
}

View file

@ -30,7 +30,9 @@ use Jbtronics\SettingsBundle\Metadata\EnvVarMode;
use Jbtronics\SettingsBundle\Settings\Settings; use Jbtronics\SettingsBundle\Settings\Settings;
use Jbtronics\SettingsBundle\Settings\SettingsParameter; use Jbtronics\SettingsBundle\Settings\SettingsParameter;
use Jbtronics\SettingsBundle\Settings\SettingsTrait; use Jbtronics\SettingsBundle\Settings\SettingsTrait;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Translation\TranslatableMessage as TM; use Symfony\Component\Translation\TranslatableMessage as TM;
use Symfony\Component\Validator\Constraints as Assert;
#[Settings(name: 'ai_openrouter', label: new TM("settings.ai.openrouter"), description: "settings.ai.openrouter.help")] #[Settings(name: 'ai_openrouter', label: new TM("settings.ai.openrouter"), description: "settings.ai.openrouter.help")]
#[SettingsIcon("fa-robot")] #[SettingsIcon("fa-robot")]
@ -43,6 +45,14 @@ class OpenRouterSettings implements AIPlatformSettingsInterface
formOptions: ["help_html" => true], envVar: "AI_OPENROUTER_KEY", envVarMode: EnvVarMode::OVERWRITE)] formOptions: ["help_html" => true], envVar: "AI_OPENROUTER_KEY", envVarMode: EnvVarMode::OVERWRITE)]
public ?string $apiKey = null; public ?string $apiKey = null;
#[SettingsParameter(label: new TM("settings.ai.timeout"),
description: new TM("settings.ai.timeout.help"),
formType: NumberType::class,
formOptions: ["scale" => 0, "attr" => ["min" => 1]],
envVar: "int:AI_OPENROUTER_TIMEOUT", envVarMode: EnvVarMode::OVERWRITE)]
#[Assert\Range(min: 1, max: AISettings::TIMEOUT_LIMIT)]
public int $timeout = 90;
public function isAIPlatformEnabled(): bool public function isAIPlatformEnabled(): bool
{ {
return $this->apiKey !== null && $this->apiKey !== ""; return $this->apiKey !== null && $this->apiKey !== "";

View file

@ -411,6 +411,18 @@
"config/packages/ai_lm_studio_platform.yaml" "config/packages/ai_lm_studio_platform.yaml"
] ]
}, },
"symfony/ai-ollama-platform": {
"version": "0.10",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "0.1",
"ref": "2f0ac0a8bc59c4e46b47a962a3ad7fe8104457d6"
},
"files": [
"config/packages/ai_ollama_platform.yaml"
]
},
"symfony/ai-open-router-platform": { "symfony/ai-open-router-platform": {
"version": "0.8", "version": "0.8",
"recipe": { "recipe": {

View file

@ -4,7 +4,7 @@
<p class="m-0"> <p class="m-0">
<b>{% trans %}log.collection_deleted.deleted{% endtrans %}</b>: <b>{% trans %}log.collection_deleted.deleted{% endtrans %}</b>:
{{ entity_type_label(entry.deletedElementClass) }} #{{ entry.deletedElementID }} {{ type_label(entry.deletedElementClass) }} #{{ entry.deletedElementID }}
{% if entry.oldName is not empty %} {% if entry.oldName is not empty %}
({{ entry.oldName }}) ({{ entry.oldName }})
{% endif %} {% endif %}

View file

@ -15,3 +15,24 @@
{{ form_row(form.partUnit) }} {{ form_row(form.partUnit) }}
{{ form_row(form.partCustomState) }} {{ form_row(form.partCustomState) }}
{{ form_row(form.gtin) }} {{ form_row(form.gtin) }}
<div class="{{ offset_label }} {{ col_input }} ps-1">
<div class="accordion" id="accordionProviderReference">
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed py-2" type="button" data-bs-toggle="collapse" data-bs-target="#collapseProviderReference" aria-expanded="true" aria-controls="collapseProviderReference">
<span>{% trans %}part.edit.provider_reference{% endtrans %}</span>
</button>
</h2>
<div id="collapseProviderReference" class="accordion-collapse collapse" data-bs-parent="#accordionProviderReference">
<div class="accordion-body">
<div class="alert alert-warning">
{% trans %}part.edit.provider_reference.warning{% endtrans %}
</div>
{{ form_widget(form.providerReference) }}
</div>
</div>
</div>
</div>
</div>

View file

@ -76,7 +76,7 @@
<a href="{{ part.providerReference.providerUrl }}" rel="noopener"> <a href="{{ part.providerReference.providerUrl }}" rel="noopener">
{% endif %} {% endif %}
<span title="{{ part.providerReference.providerKey }}">{{ info_provider_label(part.providerReference.providerKey)|default(part.providerReference.providerKey) }}</span>: {{ part.providerReference.providerId }} <span title="{{ part.providerReference.providerKey }}">{{ info_provider_label(part.providerReference.providerKey)|default(part.providerReference.providerKey) }}</span>: {{ part.providerReference.providerId }}
<span> ({{ part.providerReference.lastUpdated | format_datetime() }})</span> <span> ({{ part.providerReference.lastUpdated ? (part.providerReference.lastUpdated | format_datetime()) : ("part.info_provider_reference.updated_never"|trans) }})</span>
{% if part.providerReference.providerUrl %} {% if part.providerReference.providerUrl %}
</a> </a>
{% endif %} {% endif %}

View file

@ -19,7 +19,7 @@
<div class="carousel-caption text-white"> <div class="carousel-caption text-white">
<div><b>{{ pic.name }}</b></div> <div><b>{{ pic.name }}</b></div>
<div>{% if pic.filename %}({{ pic.filename }}) {% endif %}</div> <div>{% if pic.filename %}({{ pic.filename }}) {% endif %}</div>
<div>{{ entity_type_label(pic.element) }}</div> <div>{{ type_label(pic.element) }}</div>
</div> </div>
</div> </div>
{% endif %} {% endif %}

View file

@ -22,7 +22,7 @@
<i>({{ timeTravel | format_datetime('short') }})</i> <i>({{ timeTravel | format_datetime('short') }})</i>
{% endif %} {% endif %}
{% if part.projectBuildPart %} {% if part.projectBuildPart %}
(<i>{{ entity_type_label(part.builtProject) }}</i>: <a class="text-white" href="{{ entity_url(part.builtProject) }}">{{ part.builtProject.name }}</a>) (<i>{{ type_label(part.builtProject) }}</i>: <a class="text-white" href="{{ entity_url(part.builtProject) }}">{{ part.builtProject.name }}</a>)
{% endif %} {% endif %}
</span> </span>
<span class="float-end"> <span class="float-end">

View file

@ -26,6 +26,7 @@ use App\Doctrine\Functions\SiValueSort;
use Doctrine\DBAL\Platforms\MySQLPlatform; use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Platforms\PostgreSQLPlatform; use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use Doctrine\DBAL\Platforms\SQLitePlatform; use Doctrine\DBAL\Platforms\SQLitePlatform;
use PHPUnit\Framework\Attributes\DataProvider;
final class SiValueSortTest extends AbstractDoctrineFunctionTestCase final class SiValueSortTest extends AbstractDoctrineFunctionTestCase
{ {
@ -71,9 +72,7 @@ final class SiValueSortTest extends AbstractDoctrineFunctionTestCase
$this->assertSame('SI_VALUE(part_name)', $sql); $this->assertSame('SI_VALUE(part_name)', $sql);
} }
/** #[DataProvider('sqliteSiValueProvider')]
* @dataProvider sqliteSiValueProvider
*/
public function testSqliteSiValue(?string $input, ?float $expected): void public function testSqliteSiValue(?string $input, ?float $expected): void
{ {
$result = SiValueSort::sqliteSiValue($input); $result = SiValueSort::sqliteSiValue($input);

View file

@ -276,7 +276,6 @@ final class AttachmentTest extends TestCase
{ {
$reflection = new ReflectionClass($object); $reflection = new ReflectionClass($object);
$reflection_property = $reflection->getProperty($property); $reflection_property = $reflection->getProperty($property);
$reflection_property->setAccessible(true);
$reflection_property->setValue($object, $value); $reflection_property->setValue($object, $value);
} }

View file

@ -82,7 +82,12 @@ final class ProjectBuildRequestTest extends TestCase
$part2->setName('Part 2'); $part2->setName('Part 2');
$part2->setPartUnit($float_unit); $part2->setPartUnit($float_unit);
$this->lot2 = new PartLot(); $this->lot2 = new class extends PartLot {
public function getID(): ?int
{
return 3;
}
};;
$part2->addPartLot($this->lot2); $part2->addPartLot($this->lot2);
$this->lot2->setAmount(2.5); $this->lot2->setAmount(2.5);
$this->lot2->setDescription('Lot 2'); $this->lot2->setDescription('Lot 2');

View file

@ -77,7 +77,6 @@ final class BuerklinProviderTest extends TestCase
public function testAttributesToParametersParsesUnitsAndValues(): void public function testAttributesToParametersParsesUnitsAndValues(): void
{ {
$method = new \ReflectionMethod(BuerklinProvider::class, 'attributesToParameters'); $method = new \ReflectionMethod(BuerklinProvider::class, 'attributesToParameters');
$method->setAccessible(true);
$features = [ $features = [
[ [
@ -127,7 +126,6 @@ final class BuerklinProviderTest extends TestCase
public function testComplianceParameters(): void public function testComplianceParameters(): void
{ {
$method = new \ReflectionMethod(BuerklinProvider::class, 'complianceToParameters'); $method = new \ReflectionMethod(BuerklinProvider::class, 'complianceToParameters');
$method->setAccessible(true);
$product = [ $product = [
'labelRoHS' => 'Yes', 'labelRoHS' => 'Yes',
@ -158,7 +156,6 @@ final class BuerklinProviderTest extends TestCase
public function testImageSelectionPrefersZoomAndDeduplicates(): void public function testImageSelectionPrefersZoomAndDeduplicates(): void
{ {
$method = new \ReflectionMethod(BuerklinProvider::class, 'getProductImages'); $method = new \ReflectionMethod(BuerklinProvider::class, 'getProductImages');
$method->setAccessible(true);
$images = [ $images = [
['format' => 'product', 'url' => '/img/a.webp'], ['format' => 'product', 'url' => '/img/a.webp'],
@ -176,7 +173,6 @@ final class BuerklinProviderTest extends TestCase
public function testFootprintExtraction(): void public function testFootprintExtraction(): void
{ {
$method = new \ReflectionMethod(BuerklinProvider::class, 'getPartDetail'); $method = new \ReflectionMethod(BuerklinProvider::class, 'getPartDetail');
$method->setAccessible(true);
$product = [ $product = [
'code' => 'TEST1', 'code' => 'TEST1',
@ -212,7 +208,6 @@ final class BuerklinProviderTest extends TestCase
]; ];
$method = new \ReflectionMethod(BuerklinProvider::class, 'pricesToVendorInfo'); $method = new \ReflectionMethod(BuerklinProvider::class, 'pricesToVendorInfo');
$method->setAccessible(true);
$vendorInfo = $method->invoke($this->provider, 'SKU1', 'https://x', $detailPrice); $vendorInfo = $method->invoke($this->provider, 'SKU1', 'https://x', $detailPrice);
@ -260,7 +255,6 @@ final class BuerklinProviderTest extends TestCase
); );
$method = new \ReflectionMethod(BuerklinProvider::class, 'convertPartDetailToSearchResult'); $method = new \ReflectionMethod(BuerklinProvider::class, 'convertPartDetailToSearchResult');
$method->setAccessible(true);
$dto = $method->invoke($this->provider, $detail); $dto = $method->invoke($this->provider, $detail);

View file

@ -367,7 +367,6 @@ final class LCSCProviderTest extends TestCase
{ {
$reflection = new \ReflectionClass($this->provider); $reflection = new \ReflectionClass($this->provider);
$method = $reflection->getMethod('sanitizeField'); $method = $reflection->getMethod('sanitizeField');
$method->setAccessible(true);
$this->assertNull($method->invokeArgs($this->provider, [null])); $this->assertNull($method->invokeArgs($this->provider, [null]));
$this->assertEquals('Clean text', $method->invokeArgs($this->provider, ['Clean text'])); $this->assertEquals('Clean text', $method->invokeArgs($this->provider, ['Clean text']));
@ -378,7 +377,6 @@ final class LCSCProviderTest extends TestCase
{ {
$reflection = new \ReflectionClass($this->provider); $reflection = new \ReflectionClass($this->provider);
$method = $reflection->getMethod('getUsedCurrency'); $method = $reflection->getMethod('getUsedCurrency');
$method->setAccessible(true);
$this->assertEquals('USD', $method->invokeArgs($this->provider, ['US$'])); $this->assertEquals('USD', $method->invokeArgs($this->provider, ['US$']));
$this->assertEquals('USD', $method->invokeArgs($this->provider, ['$'])); $this->assertEquals('USD', $method->invokeArgs($this->provider, ['$']));
@ -391,7 +389,6 @@ final class LCSCProviderTest extends TestCase
{ {
$reflection = new \ReflectionClass($this->provider); $reflection = new \ReflectionClass($this->provider);
$method = $reflection->getMethod('getProductShortURL'); $method = $reflection->getMethod('getProductShortURL');
$method->setAccessible(true);
$result = $method->invokeArgs($this->provider, ['C123456']); $result = $method->invokeArgs($this->provider, ['C123456']);
$this->assertEquals('https://www.lcsc.com/product-detail/C123456.html', $result); $this->assertEquals('https://www.lcsc.com/product-detail/C123456.html', $result);
@ -401,7 +398,6 @@ final class LCSCProviderTest extends TestCase
{ {
$reflection = new \ReflectionClass($this->provider); $reflection = new \ReflectionClass($this->provider);
$method = $reflection->getMethod('getProductDatasheets'); $method = $reflection->getMethod('getProductDatasheets');
$method->setAccessible(true);
$result = $method->invokeArgs($this->provider, [null]); $result = $method->invokeArgs($this->provider, [null]);
$this->assertIsArray($result); $this->assertIsArray($result);
@ -417,7 +413,6 @@ final class LCSCProviderTest extends TestCase
{ {
$reflection = new \ReflectionClass($this->provider); $reflection = new \ReflectionClass($this->provider);
$method = $reflection->getMethod('getProductImages'); $method = $reflection->getMethod('getProductImages');
$method->setAccessible(true);
$result = $method->invokeArgs($this->provider, [null]); $result = $method->invokeArgs($this->provider, [null]);
$this->assertIsArray($result); $this->assertIsArray($result);
@ -434,7 +429,6 @@ final class LCSCProviderTest extends TestCase
{ {
$reflection = new \ReflectionClass($this->provider); $reflection = new \ReflectionClass($this->provider);
$method = $reflection->getMethod('attributesToParameters'); $method = $reflection->getMethod('attributesToParameters');
$method->setAccessible(true);
$attributes = [ $attributes = [
['paramNameEn' => 'Resistance', 'paramValueEn' => '1kΩ'], ['paramNameEn' => 'Resistance', 'paramValueEn' => '1kΩ'],
@ -454,7 +448,6 @@ final class LCSCProviderTest extends TestCase
{ {
$reflection = new \ReflectionClass($this->provider); $reflection = new \ReflectionClass($this->provider);
$method = $reflection->getMethod('pricesToVendorInfo'); $method = $reflection->getMethod('pricesToVendorInfo');
$method->setAccessible(true);
$prices = [ $prices = [
['ladder' => 1, 'productPrice' => '0.10', 'currencySymbol' => 'US$'], ['ladder' => 1, 'productPrice' => '0.10', 'currencySymbol' => 'US$'],

View file

@ -55,5 +55,23 @@
<target>Jdi!</target> <target>Jdi!</target>
</segment> </segment>
</unit> </unit>
<unit id="dBtnOk01" name="dialog.btn.ok">
<segment state="translated">
<source>dialog.btn.ok</source>
<target>OK</target>
</segment>
</unit>
<unit id="dBtnCcl1" name="dialog.btn.cancel">
<segment state="translated">
<source>dialog.btn.cancel</source>
<target>Zrušit</target>
</segment>
</unit>
<unit id="dBtnDny1" name="dialog.btn.deny">
<segment state="translated">
<source>dialog.btn.deny</source>
<target>Ne</target>
</segment>
</unit>
</file> </file>
</xliff> </xliff>

View file

@ -55,5 +55,23 @@
<target>Kom nu!</target> <target>Kom nu!</target>
</segment> </segment>
</unit> </unit>
<unit id="dBtnOk01" name="dialog.btn.ok">
<segment state="translated">
<source>dialog.btn.ok</source>
<target>OK</target>
</segment>
</unit>
<unit id="dBtnCcl1" name="dialog.btn.cancel">
<segment state="translated">
<source>dialog.btn.cancel</source>
<target>Annuller</target>
</segment>
</unit>
<unit id="dBtnDny1" name="dialog.btn.deny">
<segment state="translated">
<source>dialog.btn.deny</source>
<target>Nej</target>
</segment>
</unit>
</file> </file>
</xliff> </xliff>

View file

@ -55,5 +55,29 @@
<target>Los!</target> <target>Los!</target>
</segment> </segment>
</unit> </unit>
<unit id="8d38e7538" name="user.password_strength.crack_time">
<segment state="translated">
<source>user.password_strength.crack_time</source>
<target>Geschätzte Zeit bis zum Knacken: %time%</target>
</segment>
</unit>
<unit id="dBtnOk01" name="dialog.btn.ok">
<segment state="translated">
<source>dialog.btn.ok</source>
<target>OK</target>
</segment>
</unit>
<unit id="dBtnCcl1" name="dialog.btn.cancel">
<segment state="translated">
<source>dialog.btn.cancel</source>
<target>Abbrechen</target>
</segment>
</unit>
<unit id="dBtnDny1" name="dialog.btn.deny">
<segment state="translated">
<source>dialog.btn.deny</source>
<target>Nein</target>
</segment>
</unit>
</file> </file>
</xliff> </xliff>

View file

@ -7,5 +7,23 @@
<target>Αναζήτηση</target> <target>Αναζήτηση</target>
</segment> </segment>
</unit> </unit>
<unit id="dBtnOk01" name="dialog.btn.ok">
<segment state="translated">
<source>dialog.btn.ok</source>
<target>OK</target>
</segment>
</unit>
<unit id="dBtnCcl1" name="dialog.btn.cancel">
<segment state="translated">
<source>dialog.btn.cancel</source>
<target>Ακύρωση</target>
</segment>
</unit>
<unit id="dBtnDny1" name="dialog.btn.deny">
<segment state="translated">
<source>dialog.btn.deny</source>
<target>Όχι</target>
</segment>
</unit>
</file> </file>
</xliff> </xliff>

View file

@ -55,5 +55,29 @@
<target>Go!</target> <target>Go!</target>
</segment> </segment>
</unit> </unit>
<unit id="8d38e7538" name="user.password_strength.crack_time">
<segment state="translated">
<source>user.password_strength.crack_time</source>
<target>Estimated time to crack: %time%</target>
</segment>
</unit>
<unit id="dBtnOk01" name="dialog.btn.ok">
<segment state="translated">
<source>dialog.btn.ok</source>
<target>OK</target>
</segment>
</unit>
<unit id="dBtnCcl1" name="dialog.btn.cancel">
<segment state="translated">
<source>dialog.btn.cancel</source>
<target>Cancel</target>
</segment>
</unit>
<unit id="dBtnDny1" name="dialog.btn.deny">
<segment state="translated">
<source>dialog.btn.deny</source>
<target>No</target>
</segment>
</unit>
</file> </file>
</xliff> </xliff>

View file

@ -55,5 +55,23 @@
<target>¡Vamos!</target> <target>¡Vamos!</target>
</segment> </segment>
</unit> </unit>
<unit id="dBtnOk01" name="dialog.btn.ok">
<segment state="translated">
<source>dialog.btn.ok</source>
<target>OK</target>
</segment>
</unit>
<unit id="dBtnCcl1" name="dialog.btn.cancel">
<segment state="translated">
<source>dialog.btn.cancel</source>
<target>Cancelar</target>
</segment>
</unit>
<unit id="dBtnDny1" name="dialog.btn.deny">
<segment state="translated">
<source>dialog.btn.deny</source>
<target>No</target>
</segment>
</unit>
</file> </file>
</xliff> </xliff>

View file

@ -55,5 +55,23 @@
<target>Rechercher!</target> <target>Rechercher!</target>
</segment> </segment>
</unit> </unit>
<unit id="dBtnOk01" name="dialog.btn.ok">
<segment state="translated">
<source>dialog.btn.ok</source>
<target>OK</target>
</segment>
</unit>
<unit id="dBtnCcl1" name="dialog.btn.cancel">
<segment state="translated">
<source>dialog.btn.cancel</source>
<target>Annuler</target>
</segment>
</unit>
<unit id="dBtnDny1" name="dialog.btn.deny">
<segment state="translated">
<source>dialog.btn.deny</source>
<target>Non</target>
</segment>
</unit>
</file> </file>
</xliff> </xliff>

View file

@ -55,5 +55,23 @@
<target>Indítás!</target> <target>Indítás!</target>
</segment> </segment>
</unit> </unit>
<unit id="dBtnOk01" name="dialog.btn.ok">
<segment state="translated">
<source>dialog.btn.ok</source>
<target>OK</target>
</segment>
</unit>
<unit id="dBtnCcl1" name="dialog.btn.cancel">
<segment state="translated">
<source>dialog.btn.cancel</source>
<target>Mégse</target>
</segment>
</unit>
<unit id="dBtnDny1" name="dialog.btn.deny">
<segment state="translated">
<source>dialog.btn.deny</source>
<target>Nem</target>
</segment>
</unit>
</file> </file>
</xliff> </xliff>

View file

@ -55,5 +55,23 @@
<target>Cerca!</target> <target>Cerca!</target>
</segment> </segment>
</unit> </unit>
<unit id="dBtnOk01" name="dialog.btn.ok">
<segment state="translated">
<source>dialog.btn.ok</source>
<target>OK</target>
</segment>
</unit>
<unit id="dBtnCcl1" name="dialog.btn.cancel">
<segment state="translated">
<source>dialog.btn.cancel</source>
<target>Annulla</target>
</segment>
</unit>
<unit id="dBtnDny1" name="dialog.btn.deny">
<segment state="translated">
<source>dialog.btn.deny</source>
<target>No</target>
</segment>
</unit>
</file> </file>
</xliff> </xliff>

View file

@ -19,5 +19,23 @@
<target>検索</target> <target>検索</target>
</segment> </segment>
</unit> </unit>
<unit id="dBtnOk01" name="dialog.btn.ok">
<segment state="translated">
<source>dialog.btn.ok</source>
<target>OK</target>
</segment>
</unit>
<unit id="dBtnCcl1" name="dialog.btn.cancel">
<segment state="translated">
<source>dialog.btn.cancel</source>
<target>キャンセル</target>
</segment>
</unit>
<unit id="dBtnDny1" name="dialog.btn.deny">
<segment state="translated">
<source>dialog.btn.deny</source>
<target>いいえ</target>
</segment>
</unit>
</file> </file>
</xliff> </xliff>

View file

@ -55,5 +55,23 @@
<target>Ga!</target> <target>Ga!</target>
</segment> </segment>
</unit> </unit>
<unit id="dBtnOk01" name="dialog.btn.ok">
<segment state="translated">
<source>dialog.btn.ok</source>
<target>OK</target>
</segment>
</unit>
<unit id="dBtnCcl1" name="dialog.btn.cancel">
<segment state="translated">
<source>dialog.btn.cancel</source>
<target>Annuleren</target>
</segment>
</unit>
<unit id="dBtnDny1" name="dialog.btn.deny">
<segment state="translated">
<source>dialog.btn.deny</source>
<target>Nee</target>
</segment>
</unit>
</file> </file>
</xliff> </xliff>

View file

@ -55,5 +55,23 @@
<target>Idź!</target> <target>Idź!</target>
</segment> </segment>
</unit> </unit>
<unit id="dBtnOk01" name="dialog.btn.ok">
<segment state="translated">
<source>dialog.btn.ok</source>
<target>OK</target>
</segment>
</unit>
<unit id="dBtnCcl1" name="dialog.btn.cancel">
<segment state="translated">
<source>dialog.btn.cancel</source>
<target>Anuluj</target>
</segment>
</unit>
<unit id="dBtnDny1" name="dialog.btn.deny">
<segment state="translated">
<source>dialog.btn.deny</source>
<target>Nie</target>
</segment>
</unit>
</file> </file>
</xliff> </xliff>

View file

@ -55,5 +55,23 @@
<target>Vá!</target> <target>Vá!</target>
</segment> </segment>
</unit> </unit>
<unit id="dBtnOk01" name="dialog.btn.ok">
<segment state="translated">
<source>dialog.btn.ok</source>
<target>OK</target>
</segment>
</unit>
<unit id="dBtnCcl1" name="dialog.btn.cancel">
<segment state="translated">
<source>dialog.btn.cancel</source>
<target>Cancelar</target>
</segment>
</unit>
<unit id="dBtnDny1" name="dialog.btn.deny">
<segment state="translated">
<source>dialog.btn.deny</source>
<target>Não</target>
</segment>
</unit>
</file> </file>
</xliff> </xliff>

View file

@ -55,5 +55,23 @@
<target>Поехали!</target> <target>Поехали!</target>
</segment> </segment>
</unit> </unit>
<unit id="dBtnOk01" name="dialog.btn.ok">
<segment state="translated">
<source>dialog.btn.ok</source>
<target>ОК</target>
</segment>
</unit>
<unit id="dBtnCcl1" name="dialog.btn.cancel">
<segment state="translated">
<source>dialog.btn.cancel</source>
<target>Отмена</target>
</segment>
</unit>
<unit id="dBtnDny1" name="dialog.btn.deny">
<segment state="translated">
<source>dialog.btn.deny</source>
<target>Нет</target>
</segment>
</unit>
</file> </file>
</xliff> </xliff>

View file

@ -55,5 +55,23 @@
<target>Почати!</target> <target>Почати!</target>
</segment> </segment>
</unit> </unit>
<unit id="dBtnOk01" name="dialog.btn.ok">
<segment state="translated">
<source>dialog.btn.ok</source>
<target>ОК</target>
</segment>
</unit>
<unit id="dBtnCcl1" name="dialog.btn.cancel">
<segment state="translated">
<source>dialog.btn.cancel</source>
<target>Скасувати</target>
</segment>
</unit>
<unit id="dBtnDny1" name="dialog.btn.deny">
<segment state="translated">
<source>dialog.btn.deny</source>
<target>Ні</target>
</segment>
</unit>
</file> </file>
</xliff> </xliff>

View file

@ -10,7 +10,7 @@
<unit id="R4hoCqe" name="part.labelp"> <unit id="R4hoCqe" name="part.labelp">
<segment state="translated"> <segment state="translated">
<source>part.labelp</source> <source>part.labelp</source>
<target>部件</target> <target>物料</target>
</segment> </segment>
</unit> </unit>
<unit id="S4CxO.T" name="entity.select.group.new_not_added_to_DB"> <unit id="S4CxO.T" name="entity.select.group.new_not_added_to_DB">
@ -34,7 +34,7 @@
<unit id="c44gN8b" name="user.password_strength.medium"> <unit id="c44gN8b" name="user.password_strength.medium">
<segment state="translated"> <segment state="translated">
<source>user.password_strength.medium</source> <source>user.password_strength.medium</source>
<target>中</target> <target>中</target>
</segment> </segment>
</unit> </unit>
<unit id="NwiBLHc" name="user.password_strength.strong"> <unit id="NwiBLHc" name="user.password_strength.strong">
@ -52,7 +52,31 @@
<unit id="U5IhkwB" name="search.submit"> <unit id="U5IhkwB" name="search.submit">
<segment state="translated"> <segment state="translated">
<source>search.submit</source> <source>search.submit</source>
<target>GO!</target> <target>搜索</target>
</segment>
</unit>
<unit id="8d38e7538" name="user.password_strength.crack_time">
<segment state="translated">
<source>user.password_strength.crack_time</source>
<target>预计破解时间:%time%</target>
</segment>
</unit>
<unit id="dBtnOk01" name="dialog.btn.ok">
<segment state="translated">
<source>dialog.btn.ok</source>
<target>确定</target>
</segment>
</unit>
<unit id="dBtnCcl1" name="dialog.btn.cancel">
<segment state="translated">
<source>dialog.btn.cancel</source>
<target>取消</target>
</segment>
</unit>
<unit id="dBtnDny1" name="dialog.btn.deny">
<segment state="translated">
<source>dialog.btn.deny</source>
<target>否</target>
</segment> </segment>
</unit> </unit>
</file> </file>

View file

@ -13617,6 +13617,36 @@ Buerklin-API-Authentication-Server:
<target>Host-URL</target> <target>Host-URL</target>
</segment> </segment>
</unit> </unit>
<unit id="UeFbCNh" name="settings.ai.ollama">
<segment state="translated">
<source>settings.ai.ollama</source>
<target>Ollama</target>
</segment>
</unit>
<unit id="9nfJ_vr" name="settings.ai.ollama.endpoint">
<segment state="translated">
<source>settings.ai.ollama.endpoint</source>
<target>Endpoint URL</target>
</segment>
</unit>
<unit id="67dSwE5" name="settings.ai.ollama.apiKey">
<segment state="translated">
<source>settings.ai.ollama.apiKey</source>
<target>API Key</target>
</segment>
</unit>
<unit id="VxXEQUD" name="settings.ai.timeout">
<segment state="translated">
<source>settings.ai.timeout</source>
<target>Timeout</target>
</segment>
</unit>
<unit id="vRgtpoJ" name="settings.ai.timeout.help">
<segment state="translated">
<source>settings.ai.timeout.help</source>
<target>Maximale Wartezeit in Sekunden auf eine Antwort. Die lokale KI-Inferenz kann mehrere Minuten dauern, die Inferenz in der Cloud ist in der Regel schneller.</target>
</segment>
</unit>
<unit id="kuDv.So" name="browser_plugin.recent_pages.title"> <unit id="kuDv.So" name="browser_plugin.recent_pages.title">
<segment state="translated"> <segment state="translated">
<source>browser_plugin.recent_pages.title</source> <source>browser_plugin.recent_pages.title</source>
@ -13665,5 +13695,53 @@ Buerklin-API-Authentication-Server:
<target>Sie können diesen zufällig generierten Wert verwenden (geben Sie ihn niemandem weiter):</target> <target>Sie können diesen zufällig generierten Wert verwenden (geben Sie ihn niemandem weiter):</target>
</segment> </segment>
</unit> </unit>
<unit id="cEwxoSj" name="info_providers.provider_key">
<segment state="translated">
<source>info_providers.provider_key</source>
<target>Informationsquelle</target>
</segment>
</unit>
<unit id="0sjPRNV" name="info_providers.provider_id">
<segment state="translated">
<source>info_providers.provider_id</source>
<target>Provider ID</target>
</segment>
</unit>
<unit id="2DzzAxZ" name="info_providers.provider_url">
<segment state="translated">
<source>info_providers.provider_url</source>
<target>Provider URL</target>
</segment>
</unit>
<unit id="4v3QmF6" name="part.edit.provider_reference">
<segment state="translated">
<source>part.edit.provider_reference</source>
<target>Referenz auf Informationsquelle</target>
</segment>
</unit>
<unit id="9X2qEi7" name="log.element_edited.changed_fields.providerReference.provider_key">
<segment state="translated">
<source>log.element_edited.changed_fields.providerReference.provider_key</source>
<target>Informationsquelle</target>
</segment>
</unit>
<unit id="MWXgWDb" name="log.element_edited.changed_fields.providerReference.provider_id">
<segment state="translated">
<source>log.element_edited.changed_fields.providerReference.provider_id</source>
<target>Provider ID</target>
</segment>
</unit>
<unit id="83fSFvo" name="part.info_provider_reference.updated_never">
<segment state="translated">
<source>part.info_provider_reference.updated_never</source>
<target>Niemals aktualisiert</target>
</segment>
</unit>
<unit id="yIK_Wtj" name="part.edit.provider_reference.warning">
<segment state="translated">
<source>part.edit.provider_reference.warning</source>
<target>Warnung: Das Ändern der Werte an dieser Stelle kann den Mechanismus zum Abrufen von Informationen beeinträchtigen! Sie sollten nach Möglichkeit die Funktion „Von Informationsquelle aktualisieren“ verwenden.</target>
</segment>
</unit>
</file> </file>
</xliff> </xliff>

View file

@ -490,7 +490,7 @@ The user will have to set up all two-factor authentication methods again and pri
<unit id="BtSvnI0" name="entity.delete.confirm_title"> <unit id="BtSvnI0" name="entity.delete.confirm_title">
<segment state="translated"> <segment state="translated">
<source>entity.delete.confirm_title</source> <source>entity.delete.confirm_title</source>
<target>You really want to delete %name%?</target> <target>Do you really want to delete %name%?</target>
</segment> </segment>
</unit> </unit>
<unit id="WR9DNyZ" name="entity.delete.message"> <unit id="WR9DNyZ" name="entity.delete.message">
@ -13619,6 +13619,36 @@ Buerklin-API Authentication server:
<target>Host URL</target> <target>Host URL</target>
</segment> </segment>
</unit> </unit>
<unit id="UeFbCNh" name="settings.ai.ollama">
<segment state="translated">
<source>settings.ai.ollama</source>
<target>Ollama</target>
</segment>
</unit>
<unit id="9nfJ_vr" name="settings.ai.ollama.endpoint">
<segment state="translated">
<source>settings.ai.ollama.endpoint</source>
<target>Endpoint URL</target>
</segment>
</unit>
<unit id="67dSwE5" name="settings.ai.ollama.apiKey">
<segment state="translated">
<source>settings.ai.ollama.apiKey</source>
<target>API Key</target>
</segment>
</unit>
<unit id="VxXEQUD" name="settings.ai.timeout">
<segment state="translated">
<source>settings.ai.timeout</source>
<target>Timeout</target>
</segment>
</unit>
<unit id="vRgtpoJ" name="settings.ai.timeout.help">
<segment state="translated">
<source>settings.ai.timeout.help</source>
<target>Maximum time in seconds to wait for a response. Local AI inference might take multiple minutes, cloud inference is normally faster.</target>
</segment>
</unit>
<unit id="kuDv.So" name="browser_plugin.recent_pages.title"> <unit id="kuDv.So" name="browser_plugin.recent_pages.title">
<segment state="translated"> <segment state="translated">
<source>browser_plugin.recent_pages.title</source> <source>browser_plugin.recent_pages.title</source>
@ -13667,5 +13697,53 @@ Buerklin-API Authentication server:
<target>You can use this randomly generated value (share it with nobody):</target> <target>You can use this randomly generated value (share it with nobody):</target>
</segment> </segment>
</unit> </unit>
<unit id="cEwxoSj" name="info_providers.provider_key">
<segment state="translated">
<source>info_providers.provider_key</source>
<target>Info provider</target>
</segment>
</unit>
<unit id="0sjPRNV" name="info_providers.provider_id">
<segment state="translated">
<source>info_providers.provider_id</source>
<target>Provider ID</target>
</segment>
</unit>
<unit id="2DzzAxZ" name="info_providers.provider_url">
<segment state="translated">
<source>info_providers.provider_url</source>
<target>Provider URL</target>
</segment>
</unit>
<unit id="4v3QmF6" name="part.edit.provider_reference">
<segment state="translated">
<source>part.edit.provider_reference</source>
<target>Info provider reference</target>
</segment>
</unit>
<unit id="9X2qEi7" name="log.element_edited.changed_fields.providerReference.provider_key">
<segment state="translated">
<source>log.element_edited.changed_fields.providerReference.provider_key</source>
<target>Information provider</target>
</segment>
</unit>
<unit id="MWXgWDb" name="log.element_edited.changed_fields.providerReference.provider_id">
<segment state="translated">
<source>log.element_edited.changed_fields.providerReference.provider_id</source>
<target>Provider ID</target>
</segment>
</unit>
<unit id="83fSFvo" name="part.info_provider_reference.updated_never">
<segment state="translated">
<source>part.info_provider_reference.updated_never</source>
<target>Never updated</target>
</segment>
</unit>
<unit id="yIK_Wtj" name="part.edit.provider_reference.warning">
<segment state="translated">
<source>part.edit.provider_reference.warning</source>
<target>Warning: Changing values here can break the info retrieval mechanism! You should use the "update from info provider" functionality whenever possible.</target>
</segment>
</unit>
</file> </file>
</xliff> </xliff>

File diff suppressed because it is too large Load diff

View file

@ -4,19 +4,19 @@
<unit id="GrLNa9P" name="user.login_error.user_disabled"> <unit id="GrLNa9P" name="user.login_error.user_disabled">
<segment state="translated"> <segment state="translated">
<source>user.login_error.user_disabled</source> <source>user.login_error.user_disabled</source>
<target>账户已被禁用。请联系管理员</target> <target>此账户已被禁用!如果这是个错误请联系管理员。</target>
</segment> </segment>
</unit> </unit>
<unit id="IFQ5XrG" name="saml.error.cannot_login_local_user_per_saml"> <unit id="IFQ5XrG" name="saml.error.cannot_login_local_user_per_saml">
<segment state="translated"> <segment state="translated">
<source>saml.error.cannot_login_local_user_per_saml</source> <source>saml.error.cannot_login_local_user_per_saml</source>
<target>无法通过 SSO 以本地用户身份登录。请使用本地用户密码</target> <target>无法通过SSO以本地用户身份登录请使用本地用户和密码登录。</target>
</segment> </segment>
</unit> </unit>
<unit id="wOYPZmb" name="saml.error.cannot_login_saml_user_locally"> <unit id="wOYPZmb" name="saml.error.cannot_login_saml_user_locally">
<segment state="translated"> <segment state="translated">
<source>saml.error.cannot_login_saml_user_locally</source> <source>saml.error.cannot_login_saml_user_locally</source>
<target>无法使用本地身份验证器以SAML用户身份登录请改用SSO登录</target> <target>禁止使用本地认证以SAML用户身份登录请使用SSO登录。</target>
</segment> </segment>
</unit> </unit>
</file> </file>

View file

@ -253,5 +253,23 @@
<target>Dies ist keine gültige GTIN / EAN!</target> <target>Dies ist keine gültige GTIN / EAN!</target>
</segment> </segment>
</unit> </unit>
<unit id="vnpejmb" name="info_providers.validation.provider_id_without_key">
<segment state="translated">
<source>info_providers.validation.provider_id_without_key</source>
<target>Wenn Sie eine Anbieter-ID angeben, müssen Sie auch einen Info-Anbieter angeben oder beide entfernen.</target>
</segment>
</unit>
<unit id="yFlA5OA" name="info_providers.validation.provider_url_without_key">
<segment state="translated">
<source>info_providers.validation.provider_url_without_key</source>
<target>Wenn Sie eine Anbieter-URL angeben, müssen Sie auch einen Info-Anbieter angeben.</target>
</segment>
</unit>
<unit id="gUHUXoV" name="info_providers.validation.provider_key_without_id">
<segment state="translated">
<source>info_providers.validation.provider_key_without_id</source>
<target>Wenn Sie einen Info-Anbieter angeben, müssen Sie auch eine Anbieter-ID angeben oder beides entfernen.</target>
</segment>
</unit>
</file> </file>
</xliff> </xliff>

View file

@ -253,5 +253,23 @@
<target>This is not an valid GTIN / EAN!</target> <target>This is not an valid GTIN / EAN!</target>
</segment> </segment>
</unit> </unit>
<unit id="vnpejmb" name="info_providers.validation.provider_id_without_key">
<segment state="translated">
<source>info_providers.validation.provider_id_without_key</source>
<target>If you specify an provider ID, you also need to specify an info provider or remove both.</target>
</segment>
</unit>
<unit id="yFlA5OA" name="info_providers.validation.provider_url_without_key">
<segment state="translated">
<source>info_providers.validation.provider_url_without_key</source>
<target>If you specify an provider URL, you also need to specify an info provider.</target>
</segment>
</unit>
<unit id="gUHUXoV" name="info_providers.validation.provider_key_without_id">
<segment state="translated">
<source>info_providers.validation.provider_key_without_id</source>
<target>If you specify an info provider, you also need to provide an provider id, or remove both.</target>
</segment>
</unit>
</file> </file>
</xliff> </xliff>

View file

@ -4,31 +4,31 @@
<unit id="cRbk.cm" name="part.master_attachment.must_be_picture"> <unit id="cRbk.cm" name="part.master_attachment.must_be_picture">
<segment state="translated"> <segment state="translated">
<source>part.master_attachment.must_be_picture</source> <source>part.master_attachment.must_be_picture</source>
<target>预览附件必须是有效的图片</target> <target>预览附件必须是有效的图片</target>
</segment> </segment>
</unit> </unit>
<unit id="v8HkcJB" name="structural.entity.unique_name"> <unit id="v8HkcJB" name="structural.entity.unique_name">
<segment state="translated"> <segment state="translated">
<source>structural.entity.unique_name</source> <source>structural.entity.unique_name</source>
<target>相同层下已存在同名元素</target> <target>当前层级已存在同名元素!</target>
</segment> </segment>
</unit> </unit>
<unit id="dW7b2B_" name="parameters.validator.min_lesser_typical"> <unit id="dW7b2B_" name="parameters.validator.min_lesser_typical">
<segment state="translated"> <segment state="translated">
<source>parameters.validator.min_lesser_typical</source> <source>parameters.validator.min_lesser_typical</source>
<target>值必须小于或等于标称值 ({{compare_value}})。</target> <target>值必须小于或等于标称值{{ compared_value }}。</target>
</segment> </segment>
</unit> </unit>
<unit id="Yfp2uC5" name="parameters.validator.min_lesser_max"> <unit id="Yfp2uC5" name="parameters.validator.min_lesser_max">
<segment state="translated"> <segment state="translated">
<source>parameters.validator.min_lesser_max</source> <source>parameters.validator.min_lesser_max</source>
<target>值必须小于最大值 ({{compare_value}})。</target> <target>值必须小于最大值{{ compared_value }}。</target>
</segment> </segment>
</unit> </unit>
<unit id="P6b.8Ou" name="parameters.validator.max_greater_typical"> <unit id="P6b.8Ou" name="parameters.validator.max_greater_typical">
<segment state="translated"> <segment state="translated">
<source>parameters.validator.max_greater_typical</source> <source>parameters.validator.max_greater_typical</source>
<target>值必须大于或等于标称值 ({{compare_value}})。</target> <target>值必须大于或等于标称值{{ compared_value }}。</target>
</segment> </segment>
</unit> </unit>
<unit id="P41193Y" name="validator.user.username_already_used"> <unit id="P41193Y" name="validator.user.username_already_used">
@ -40,7 +40,7 @@
<unit id="EKPQiyf" name="user.invalid_username"> <unit id="EKPQiyf" name="user.invalid_username">
<segment state="translated"> <segment state="translated">
<source>user.invalid_username</source> <source>user.invalid_username</source>
<target>用户名只能包含字母、数字、下划线、点、加号或减号</target> <target>用户名只能包含字母、数字、下划线、点、加号或减号,且不能以@开头!</target>
</segment> </segment>
</unit> </unit>
<unit id="_v.DMg." name="validator.noneofitschild.self"> <unit id="_v.DMg." name="validator.noneofitschild.self">
@ -49,7 +49,7 @@
</notes> </notes>
<segment state="translated"> <segment state="translated">
<source>validator.noneofitschild.self</source> <source>validator.noneofitschild.self</source>
<target>一个元素不能是它自己的父元素。</target> <target>元素不能成为自生的父级!</target>
</segment> </segment>
</unit> </unit>
<unit id="W90LyFQ" name="validator.noneofitschild.children"> <unit id="W90LyFQ" name="validator.noneofitschild.children">
@ -58,199 +58,199 @@
</notes> </notes>
<segment state="translated"> <segment state="translated">
<source>validator.noneofitschild.children</source> <source>validator.noneofitschild.children</source>
<target>不能将子元素指定为父元素(会导致循环)。</target> <target>不能将子元素指定为自身父级,这会导致循环引用!</target>
</segment> </segment>
</unit> </unit>
<unit id="GAUS.LK" name="validator.select_valid_category"> <unit id="GAUS.LK" name="validator.select_valid_category">
<segment state="translated"> <segment state="translated">
<source>validator.select_valid_category</source> <source>validator.select_valid_category</source>
<target>请选择一个有效的类别。</target> <target>请选择有效的分类!</target>
</segment> </segment>
</unit> </unit>
<unit id="h6qELde" name="validator.part_lot.only_existing"> <unit id="h6qELde" name="validator.part_lot.only_existing">
<segment state="translated"> <segment state="translated">
<source>validator.part_lot.only_existing</source> <source>validator.part_lot.only_existing</source>
<target>无法将新部件添加到此位置,因为它被标记为 "仅现有"</target> <target>无法向此位置增加新物料,因为它被标记为"仅限已有物料"</target>
</segment> </segment>
</unit> </unit>
<unit id="Prriyy0" name="validator.part_lot.location_full.no_increase"> <unit id="Prriyy0" name="validator.part_lot.location_full.no_increase">
<segment state="translated"> <segment state="translated">
<source>validator.part_lot.location_full.no_increase</source> <source>validator.part_lot.location_full.no_increase</source>
<target>位置已满。数量无法增加(增加值必须小于 {{ old_amount }})。</target> <target>位置已满。数量无法增加(要求必须小于 {{ old_amount }})。</target>
</segment> </segment>
</unit> </unit>
<unit id="eeEjB4s" name="validator.part_lot.location_full"> <unit id="eeEjB4s" name="validator.part_lot.location_full">
<segment state="translated"> <segment state="translated">
<source>validator.part_lot.location_full</source> <source>validator.part_lot.location_full</source>
<target>位置已满。无法添加新部件。</target> <target>位置已满。无法向其中增加新物料。</target>
</segment> </segment>
</unit> </unit>
<unit id="2yWi8eP" name="validator.part_lot.single_part"> <unit id="2yWi8eP" name="validator.part_lot.single_part">
<segment state="translated"> <segment state="translated">
<source>validator.part_lot.single_part</source> <source>validator.part_lot.single_part</source>
<target>该位置只能储存一个部件。</target> <target>此位置只能包含单个物料并且已满!</target>
</segment> </segment>
</unit> </unit>
<unit id="A.TFhbb" name="validator.attachment.must_not_be_null"> <unit id="A.TFhbb" name="validator.attachment.must_not_be_null">
<segment state="translated"> <segment state="translated">
<source>validator.attachment.must_not_be_null</source> <source>validator.attachment.must_not_be_null</source>
<target>必须选择附件类型</target> <target>必须选择附件类型</target>
</segment> </segment>
</unit> </unit>
<unit id=".lqKoij" name="validator.orderdetail.supplier_must_not_be_null"> <unit id=".lqKoij" name="validator.orderdetail.supplier_must_not_be_null">
<segment state="translated"> <segment state="translated">
<source>validator.orderdetail.supplier_must_not_be_null</source> <source>validator.orderdetail.supplier_must_not_be_null</source>
<target>必须选择供应商</target> <target>必须选择供应商</target>
</segment> </segment>
</unit> </unit>
<unit id="bcNZzK." name="validator.measurement_unit.use_si_prefix_needs_unit"> <unit id="bcNZzK." name="validator.measurement_unit.use_si_prefix_needs_unit">
<segment state="translated"> <segment state="translated">
<source>validator.measurement_unit.use_si_prefix_needs_unit</source> <source>validator.measurement_unit.use_si_prefix_needs_unit</source>
<target>要启用 SI 前缀,必须设置单位符号。</target> <target>要启用SI前缀必须设置单位符号</target>
</segment> </segment>
</unit> </unit>
<unit id="gZ5FFL1" name="part.ipn.must_be_unique"> <unit id="gZ5FFL1" name="part.ipn.must_be_unique">
<segment state="translated"> <segment state="translated">
<source>part.ipn.must_be_unique</source> <source>part.ipn.must_be_unique</source>
<target>内部部件号是唯一的。{{ value }} 已被使用!</target> <target>内部物料号必须唯一。{{ value }} 已被使用!</target>
</segment> </segment>
</unit> </unit>
<unit id="P31Yg.d" name="validator.project.bom_entry.name_or_part_needed"> <unit id="P31Yg.d" name="validator.project.bom_entry.name_or_part_needed">
<segment state="translated"> <segment state="translated">
<source>validator.project.bom_entry.name_or_part_needed</source> <source>validator.project.bom_entry.name_or_part_needed</source>
<target>您必须为 BOM 条目选择部件,或为非部件 BOM 条目设置名称。</target> <target>必须为[物料BOM条目]选择一个物料,或为[非物料BOM条目]设定名称。</target>
</segment> </segment>
</unit> </unit>
<unit id="5CEup_N" name="project.bom_entry.name_already_in_bom"> <unit id="5CEup_N" name="project.bom_entry.name_already_in_bom">
<segment state="translated"> <segment state="translated">
<source>project.bom_entry.name_already_in_bom</source> <source>project.bom_entry.name_already_in_bom</source>
<target>已存在具有该名称的 BOM 条目。</target> <target>已存在同名BOM条目</target>
</segment> </segment>
</unit> </unit>
<unit id="jB3B50E" name="project.bom_entry.part_already_in_bom"> <unit id="jB3B50E" name="project.bom_entry.part_already_in_bom">
<segment state="translated"> <segment state="translated">
<source>project.bom_entry.part_already_in_bom</source> <source>project.bom_entry.part_already_in_bom</source>
<target>该部件已存在于 BOM 中。</target> <target>此物料已存在于BOM中</target>
</segment> </segment>
</unit> </unit>
<unit id="NdkzP1n" name="project.bom_entry.mountnames_quantity_mismatch"> <unit id="NdkzP1n" name="project.bom_entry.mountnames_quantity_mismatch">
<segment state="translated"> <segment state="translated">
<source>project.bom_entry.mountnames_quantity_mismatch</source> <source>project.bom_entry.mountnames_quantity_mismatch</source>
<target>挂载名称的数量必须与 BOM 数量匹配。</target> <target>装配名称数量必须与BOM数量匹配</target>
</segment> </segment>
</unit> </unit>
<unit id="8teRCgR" name="project.bom_entry.can_not_add_own_builds_part"> <unit id="8teRCgR" name="project.bom_entry.can_not_add_own_builds_part">
<segment state="translated"> <segment state="translated">
<source>project.bom_entry.can_not_add_own_builds_part</source> <source>project.bom_entry.can_not_add_own_builds_part</source>
<target>您无法将项目自己的生产映射部件添加到 BOM 中。</target> <target>不能将[项目自身]的[组装输出物料]增加到BOM中。</target>
</segment> </segment>
</unit> </unit>
<unit id="asBxPxe" name="project.bom_has_to_include_all_subelement_parts"> <unit id="asBxPxe" name="project.bom_has_to_include_all_subelement_parts">
<segment state="translated"> <segment state="translated">
<source>project.bom_has_to_include_all_subelement_parts</source> <source>project.bom_has_to_include_all_subelement_parts</source>
<target>项目 BOM 必须包括所有子项目生产的部件。项目 %project_name% 的 %part_name% 部件丢失。</target> <target>项目BOM必须包含所有子项目的组装物料。缺少项目 %project_name% 的物料 %part_name%</target>
</segment> </segment>
</unit> </unit>
<unit id="uxaE9Ct" name="project.bom_entry.price_not_allowed_on_parts"> <unit id="uxaE9Ct" name="project.bom_entry.price_not_allowed_on_parts">
<segment state="translated"> <segment state="translated">
<source>project.bom_entry.price_not_allowed_on_parts</source> <source>project.bom_entry.price_not_allowed_on_parts</source>
<target>与部件关联的 BOM 条目上不允许有价格。请在部件上定义价格。</target> <target>与物料关联的BOM条目不允许设置价格。请在物料上定义价格。</target>
</segment> </segment>
</unit> </unit>
<unit id="xZ68Nzl" name="validator.project_build.lot_bigger_than_needed"> <unit id="xZ68Nzl" name="validator.project_build.lot_bigger_than_needed">
<segment state="translated"> <segment state="translated">
<source>validator.project_build.lot_bigger_than_needed</source> <source>validator.project_build.lot_bigger_than_needed</source>
<target>选择的提取数量超出所需数量。</target> <target>选择的数量超过所需数量!请减少到所需数量。</target>
</segment> </segment>
</unit> </unit>
<unit id="68_.V_X" name="validator.project_build.lot_smaller_than_needed"> <unit id="68_.V_X" name="validator.project_build.lot_smaller_than_needed">
<segment state="translated"> <segment state="translated">
<source>validator.project_build.lot_smaller_than_needed</source> <source>validator.project_build.lot_smaller_than_needed</source>
<target>选择的提取数量少于所需数量。</target> <target>选择的数量少于所需数量!请增加到所需数量。</target>
</segment> </segment>
</unit> </unit>
<unit id="yZGS8uZ" name="part.name.must_match_category_regex"> <unit id="yZGS8uZ" name="part.name.must_match_category_regex">
<segment state="translated"> <segment state="translated">
<source>part.name.must_match_category_regex</source> <source>part.name.must_match_category_regex</source>
<target>部件名称与类别指定的正则表达式不匹配:%regex%</target> <target>物料名称与分类指定的正则表达式不匹配:%regex%</target>
</segment> </segment>
</unit> </unit>
<unit id="Q8wP5Jd" name="validator.attachment.name_not_blank"> <unit id="Q8wP5Jd" name="validator.attachment.name_not_blank">
<segment state="translated"> <segment state="translated">
<source>validator.attachment.name_not_blank</source> <source>validator.attachment.name_not_blank</source>
<target>手动设置值,或上传文件使用其文件名作为附件的名称。</target> <target>在此处设置值,或上传文件后自动使用其文件名作为附件名称。</target>
</segment> </segment>
</unit> </unit>
<unit id="DH0IkNR" name="validator.part_lot.owner_must_match_storage_location_owner"> <unit id="DH0IkNR" name="validator.part_lot.owner_must_match_storage_location_owner">
<segment state="translated"> <segment state="translated">
<source>validator.part_lot.owner_must_match_storage_location_owner</source> <source>validator.part_lot.owner_must_match_storage_location_owner</source>
<target>该 批次的所有者 必须与 所选存储位置的所有者(%owner_name%) 匹配!</target> <target>此物料批号的所有者必须与所选存储位置的所有者(%owner_name%匹配!</target>
</segment> </segment>
</unit> </unit>
<unit id="TzySicw" name="validator.part_lot.owner_must_not_be_anonymous"> <unit id="TzySicw" name="validator.part_lot.owner_must_not_be_anonymous">
<segment state="translated"> <segment state="translated">
<source>validator.part_lot.owner_must_not_be_anonymous</source> <source>validator.part_lot.owner_must_not_be_anonymous</source>
<target>批次所有者不能是匿名用户。</target> <target>物料批号所有者不能是匿名用户!</target>
</segment> </segment>
</unit> </unit>
<unit id="GthNWUb" name="validator.part_association.must_set_an_value_if_type_is_other"> <unit id="GthNWUb" name="validator.part_association.must_set_an_value_if_type_is_other">
<segment state="translated"> <segment state="translated">
<source>validator.part_association.must_set_an_value_if_type_is_other</source> <source>validator.part_association.must_set_an_value_if_type_is_other</source>
<target>如果将类型设置为 "other" 则必须为其设置一个描述性值。</target> <target>当类型设置为"其他"时必须为其设定描述性值!</target>
</segment> </segment>
</unit> </unit>
<unit id="Be4Im81" name="validator.part_association.part_cannot_be_associated_with_itself"> <unit id="Be4Im81" name="validator.part_association.part_cannot_be_associated_with_itself">
<segment state="translated"> <segment state="translated">
<source>validator.part_association.part_cannot_be_associated_with_itself</source> <source>validator.part_association.part_cannot_be_associated_with_itself</source>
<target>部件不能与自己关联。</target> <target>物料不能与自身关联!</target>
</segment> </segment>
</unit> </unit>
<unit id="q5Ej6Xm" name="validator.part_association.already_exists"> <unit id="q5Ej6Xm" name="validator.part_association.already_exists">
<segment state="translated"> <segment state="translated">
<source>validator.part_association.already_exists</source> <source>validator.part_association.already_exists</source>
<target>与此部件的关联已存在。</target> <target>与此物料的关联已存在!</target>
</segment> </segment>
</unit> </unit>
<unit id="HbI5bga" name="validator.part_lot.vendor_barcode_must_be_unique"> <unit id="HbI5bga" name="validator.part_lot.vendor_barcode_must_be_unique">
<segment state="translated"> <segment state="translated">
<source>validator.part_lot.vendor_barcode_must_be_unique</source> <source>validator.part_lot.vendor_barcode_must_be_unique</source>
<target>该供应商条码已在另一批次中使用。条形码必须是唯一的</target> <target>此供应商条码值已在另一物料批号中使用。条码必须唯一!</target>
</segment> </segment>
</unit> </unit>
<unit id="ufQJh7E" name="validator.year_2038_bug_on_32bit"> <unit id="ufQJh7E" name="validator.year_2038_bug_on_32bit">
<segment state="translated"> <segment state="translated">
<source>validator.year_2038_bug_on_32bit</source> <source>validator.year_2038_bug_on_32bit</source>
<target>由于技术限制在32位系统中无法选择2038年1月19日之后的日期!</target> <target>由于技术限制在32位系统上无法选择2038-01-19之后的日期!</target>
</segment> </segment>
</unit> </unit>
<unit id="iM9yb_p" name="validator.fileSize.invalidFormat"> <unit id="iM9yb_p" name="validator.fileSize.invalidFormat">
<segment state="translated"> <segment state="translated">
<source>validator.fileSize.invalidFormat</source> <source>validator.fileSize.invalidFormat</source>
<target>文件大小格式无效。请使用整数并以 K、M 或 G 作为后缀,分别代表 KB、MB 或 GB。</target> <target>无效的文件大小格式。请使用整数后加 KKByte、MMByte、GGByte的后缀。</target>
</segment> </segment>
</unit> </unit>
<unit id="ZFxQ0BZ" name="validator.invalid_range"> <unit id="ZFxQ0BZ" name="validator.invalid_range">
<segment state="translated"> <segment state="translated">
<source>validator.invalid_range</source> <source>validator.invalid_range</source>
<target>给定的范围无效</target> <target>指定的范围无效!</target>
</segment> </segment>
</unit> </unit>
<unit id="m4gp2P_" name="validator.google_code.wrong_code"> <unit id="m4gp2P_" name="validator.google_code.wrong_code">
<segment state="translated"> <segment state="translated">
<source>validator.google_code.wrong_code</source> <source>validator.google_code.wrong_code</source>
<target>验证码无效。请检查验证器应用设置是否正确,并确保服务器与认证设备的时间均已同步。</target> <target>验证码无效。请检查验证器应用是否正确设置,并且切薄服务器和验证设备的时间均正确。</target>
</segment> </segment>
</unit> </unit>
<unit id="I330cr5" name="settings.synonyms.type_synonyms.collection_type.duplicate"> <unit id="I330cr5" name="settings.synonyms.type_synonyms.collection_type.duplicate">
<segment state="translated"> <segment state="translated">
<source>settings.synonyms.type_synonyms.collection_type.duplicate</source> <source>settings.synonyms.type_synonyms.collection_type.duplicate</source>
<target>该类型在此语言下已存在翻译定义!</target> <target>已为此类型和语言定义了翻译!</target>
</segment> </segment>
</unit> </unit>
<unit id="zT_j_oQ" name="validator.invalid_gtin"> <unit id="zT_j_oQ" name="validator.invalid_gtin">
<segment state="translated"> <segment state="translated">
<source>validator.invalid_gtin</source> <source>validator.invalid_gtin</source>
<target>无效的GTIN / EAN 码。</target> <target>这不是有效的GTIN/EAN</target>
</segment> </segment>
</unit> </unit>
</file> </file>

View file

@ -119,7 +119,14 @@ Encore
// requires WebpackEncoreBundle 1.4 or higher // requires WebpackEncoreBundle 1.4 or higher
.enableIntegrityHashes(Encore.isProduction()) .enableIntegrityHashes(Encore.isProduction())
// uncomment if you're having problems with a jQuery plugin // Force all jquery imports to the UMD build so webpack always receives the
// jQuery function directly instead of an ESM namespace object. Without this,
// webpack's ESM interop wraps jquery.module.js in a namespace
// { default, jQuery, $ } which has no .fn, crashing Bootstrap's
// defineJQueryPlugin when it tries to access $.fn.alert.
.addAliases({
'jquery': path.resolve(__dirname, 'node_modules/jquery/dist/jquery.js')
})
.autoProvidejQuery() .autoProvidejQuery()
@ -142,7 +149,7 @@ Encore
; ;
//These are all the themes that are available in bootswatch //These are all the themes that are available in bootswatch
const AVAILABLE_THEMES = ['bootstrap', 'cerulean', 'cosmo', 'cyborg', 'darkly', 'flatly', 'journal', const AVAILABLE_THEMES = ['bootstrap', 'brite', 'cerulean', 'cosmo', 'cyborg', 'darkly', 'flatly', 'journal',
'litera', 'lumen', 'lux', 'materia', 'minty', 'morph', 'pulse', 'quartz', 'sandstone', 'simplex', 'sketchy', 'slate', 'solar', 'litera', 'lumen', 'lux', 'materia', 'minty', 'morph', 'pulse', 'quartz', 'sandstone', 'simplex', 'sketchy', 'slate', 'solar',
'spacelab', 'superhero', 'united', 'vapor', 'yeti', 'zephyr']; 'spacelab', 'superhero', 'united', 'vapor', 'yeti', 'zephyr'];

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