mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-02-24 18:39:35 +00:00
Merge branch 'master' into amazon_info_provider
This commit is contained in:
commit
c6cbc17c66
34 changed files with 1625 additions and 687 deletions
4
.github/workflows/docker_build.yml
vendored
4
.github/workflows/docker_build.yml
vendored
|
|
@ -98,7 +98,7 @@ jobs:
|
|||
-
|
||||
name: Upload digest
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: digests-${{ matrix.platform-slug }}
|
||||
path: /tmp/digests/*
|
||||
|
|
@ -113,7 +113,7 @@ jobs:
|
|||
steps:
|
||||
-
|
||||
name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
|
|
|
|||
4
.github/workflows/docker_frankenphp.yml
vendored
4
.github/workflows/docker_frankenphp.yml
vendored
|
|
@ -99,7 +99,7 @@ jobs:
|
|||
-
|
||||
name: Upload digest
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: digests-${{ matrix.platform-slug }}
|
||||
path: /tmp/digests/*
|
||||
|
|
@ -114,7 +114,7 @@ jobs:
|
|||
steps:
|
||||
-
|
||||
name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
|
|
|
|||
|
|
@ -20,6 +20,10 @@
|
|||
import { Controller } from '@hotwired/stimulus';
|
||||
import { Toast } from 'bootstrap';
|
||||
|
||||
/**
|
||||
* The purpose of this controller, is to show all containers.
|
||||
* They should already be added via turbo-streams, but have to be called for to show them.
|
||||
*/
|
||||
export default class extends Controller {
|
||||
connect() {
|
||||
//Move all toasts from the page into our toast container and show them
|
||||
|
|
@ -33,4 +37,4 @@ export default class extends Controller {
|
|||
const toast = new Toast(this.element);
|
||||
toast.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,17 +21,31 @@ import {Controller} from "@hotwired/stimulus";
|
|||
//import * as ZXing from "@zxing/library";
|
||||
|
||||
import {Html5QrcodeScanner, Html5Qrcode} from "@part-db/html5-qrcode";
|
||||
import { generateCsrfToken, generateCsrfHeaders } from "../csrf_protection_controller";
|
||||
|
||||
/* stimulusFetch: 'lazy' */
|
||||
|
||||
export default class extends Controller {
|
||||
|
||||
//codeReader = null;
|
||||
|
||||
_scanner = null;
|
||||
|
||||
_submitting = false;
|
||||
_lastDecodedText = "";
|
||||
_onInfoChange = null;
|
||||
|
||||
connect() {
|
||||
console.log('Init Scanner');
|
||||
|
||||
// Prevent double init if connect fires twice
|
||||
if (this._scanner) return;
|
||||
|
||||
// clear last decoded barcode when state changes on info box
|
||||
const info = document.getElementById("scan_dialog_info_mode");
|
||||
if (info) {
|
||||
this._onInfoChange = () => {
|
||||
this._lastDecodedText = "";
|
||||
};
|
||||
info.addEventListener("change", this._onInfoChange);
|
||||
}
|
||||
|
||||
const isMobile = window.matchMedia("(max-width: 768px)").matches;
|
||||
|
||||
//This function ensures, that the qrbox is 70% of the total viewport
|
||||
let qrboxFunction = function(viewfinderWidth, viewfinderHeight) {
|
||||
|
|
@ -45,29 +59,61 @@ export default class extends Controller {
|
|||
}
|
||||
|
||||
//Try to get the number of cameras. If the number is 0, then the promise will fail, and we show the warning dialog
|
||||
Html5Qrcode.getCameras().catch((devices) => {
|
||||
document.getElementById('scanner-warning').classList.remove('d-none');
|
||||
Html5Qrcode.getCameras().catch(() => {
|
||||
document.getElementById("scanner-warning")?.classList.remove("d-none");
|
||||
});
|
||||
|
||||
this._scanner = new Html5QrcodeScanner(this.element.id, {
|
||||
fps: 10,
|
||||
qrbox: qrboxFunction,
|
||||
// Key change: shrink preview height on mobile
|
||||
...(isMobile ? { aspectRatio: 1.0 } : {}),
|
||||
experimentalFeatures: {
|
||||
//This option improves reading quality on android chrome
|
||||
useBarCodeDetectorIfSupported: true
|
||||
}
|
||||
useBarCodeDetectorIfSupported: true,
|
||||
},
|
||||
}, false);
|
||||
|
||||
this._scanner.render(this.onScanSuccess.bind(this));
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this._scanner.pause();
|
||||
this._scanner.clear();
|
||||
|
||||
// If we already stopped/cleared before submit, nothing to do.
|
||||
const scanner = this._scanner;
|
||||
this._scanner = null;
|
||||
this._lastDecodedText = "";
|
||||
|
||||
// Unbind info-mode change handler (always do this, even if scanner is null)
|
||||
const info = document.getElementById("scan_dialog_info_mode");
|
||||
if (info && this._onInfoChange) {
|
||||
info.removeEventListener("change", this._onInfoChange);
|
||||
}
|
||||
this._onInfoChange = null;
|
||||
|
||||
if (!scanner) return;
|
||||
|
||||
try {
|
||||
const p = scanner.clear?.();
|
||||
if (p && typeof p.then === "function") p.catch(() => {});
|
||||
} catch (_) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
onScanSuccess(decodedText, decodedResult) {
|
||||
//Put our decoded Text into the input box
|
||||
|
||||
onScanSuccess(decodedText) {
|
||||
if (!decodedText) return;
|
||||
|
||||
const normalized = String(decodedText).trim();
|
||||
if (!normalized) return;
|
||||
|
||||
// scan once per barcode
|
||||
if (normalized === this._lastDecodedText) return;
|
||||
|
||||
// Mark as handled immediately (prevents spam even if callback fires repeatedly)
|
||||
this._lastDecodedText = normalized;
|
||||
|
||||
document.getElementById('scan_dialog_input').value = decodedText;
|
||||
//Submit form
|
||||
document.getElementById('scan_dialog_form').requestSubmit();
|
||||
|
|
|
|||
|
|
@ -1,27 +0,0 @@
|
|||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Controller } from '@hotwired/stimulus';
|
||||
|
||||
export default class extends Controller {
|
||||
connect() {
|
||||
//If we encounter an element with global reload controller, then reload the whole page
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Controller } from '@hotwired/stimulus';
|
||||
|
||||
export default class extends Controller {
|
||||
connect() {
|
||||
const menu = document.getElementById('locale-select-menu');
|
||||
menu.innerHTML = this.element.innerHTML;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Controller } from '@hotwired/stimulus';
|
||||
|
||||
export default class extends Controller {
|
||||
connect() {
|
||||
//If we encounter an element with this, then change the title of our document according to data-title
|
||||
this.changeTitle(this.element.dataset.title);
|
||||
}
|
||||
|
||||
changeTitle(title) {
|
||||
document.title = title;
|
||||
}
|
||||
}
|
||||
|
|
@ -58,6 +58,12 @@
|
|||
object-fit: contain;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.part-info-image {
|
||||
max-height: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.object-fit-cover {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
|
|
|||
202
composer.lock
generated
202
composer.lock
generated
|
|
@ -968,7 +968,7 @@
|
|||
},
|
||||
{
|
||||
"name": "api-platform/doctrine-common",
|
||||
"version": "v4.2.16",
|
||||
"version": "v4.2.17",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/api-platform/doctrine-common.git",
|
||||
|
|
@ -1052,13 +1052,13 @@
|
|||
"rest"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/api-platform/doctrine-common/tree/v4.2.16"
|
||||
"source": "https://github.com/api-platform/doctrine-common/tree/v4.3.0-alpha.1"
|
||||
},
|
||||
"time": "2026-02-13T15:07:33+00:00"
|
||||
},
|
||||
{
|
||||
"name": "api-platform/doctrine-orm",
|
||||
"version": "v4.2.16",
|
||||
"version": "v4.2.17",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/api-platform/doctrine-orm.git",
|
||||
|
|
@ -1139,13 +1139,13 @@
|
|||
"rest"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/api-platform/doctrine-orm/tree/v4.2.16"
|
||||
"source": "https://github.com/api-platform/doctrine-orm/tree/v4.2.17"
|
||||
},
|
||||
"time": "2026-02-13T17:30:49+00:00"
|
||||
},
|
||||
{
|
||||
"name": "api-platform/documentation",
|
||||
"version": "v4.2.16",
|
||||
"version": "v4.2.17",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/api-platform/documentation.git",
|
||||
|
|
@ -1202,13 +1202,13 @@
|
|||
],
|
||||
"description": "API Platform documentation controller.",
|
||||
"support": {
|
||||
"source": "https://github.com/api-platform/documentation/tree/v4.2.16"
|
||||
"source": "https://github.com/api-platform/documentation/tree/v4.3.0-alpha.1"
|
||||
},
|
||||
"time": "2025-12-27T22:15:57+00:00"
|
||||
},
|
||||
{
|
||||
"name": "api-platform/http-cache",
|
||||
"version": "v4.2.16",
|
||||
"version": "v4.2.17",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/api-platform/http-cache.git",
|
||||
|
|
@ -1282,22 +1282,22 @@
|
|||
"rest"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/api-platform/http-cache/tree/v4.2.16"
|
||||
"source": "https://github.com/api-platform/http-cache/tree/v4.3.0-alpha.1"
|
||||
},
|
||||
"time": "2026-02-13T15:07:33+00:00"
|
||||
},
|
||||
{
|
||||
"name": "api-platform/hydra",
|
||||
"version": "v4.2.16",
|
||||
"version": "v4.2.17",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/api-platform/hydra.git",
|
||||
"reference": "ddba613f615caa8372df3d478a36a910b77f6d28"
|
||||
"reference": "392c44574a7746de03564d709c4eb5c652509440"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/api-platform/hydra/zipball/ddba613f615caa8372df3d478a36a910b77f6d28",
|
||||
"reference": "ddba613f615caa8372df3d478a36a910b77f6d28",
|
||||
"url": "https://api.github.com/repos/api-platform/hydra/zipball/392c44574a7746de03564d709c4eb5c652509440",
|
||||
"reference": "392c44574a7746de03564d709c4eb5c652509440",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -1369,13 +1369,13 @@
|
|||
"rest"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/api-platform/hydra/tree/v4.2.16"
|
||||
"source": "https://github.com/api-platform/hydra/tree/v4.2.17"
|
||||
},
|
||||
"time": "2026-02-13T15:07:33+00:00"
|
||||
"time": "2026-02-17T13:24:02+00:00"
|
||||
},
|
||||
{
|
||||
"name": "api-platform/json-api",
|
||||
"version": "v4.2.16",
|
||||
"version": "v4.2.17",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/api-platform/json-api.git",
|
||||
|
|
@ -1451,22 +1451,22 @@
|
|||
"rest"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/api-platform/json-api/tree/v4.2.16"
|
||||
"source": "https://github.com/api-platform/json-api/tree/v4.2.17"
|
||||
},
|
||||
"time": "2026-02-13T17:30:49+00:00"
|
||||
},
|
||||
{
|
||||
"name": "api-platform/json-schema",
|
||||
"version": "v4.2.16",
|
||||
"version": "v4.2.17",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/api-platform/json-schema.git",
|
||||
"reference": "3569ab8e3e5c01d77f00964683254809571fa078"
|
||||
"reference": "de96f482b6dcf913a2849a71ae102d2e45a61b52"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/api-platform/json-schema/zipball/3569ab8e3e5c01d77f00964683254809571fa078",
|
||||
"reference": "3569ab8e3e5c01d77f00964683254809571fa078",
|
||||
"url": "https://api.github.com/repos/api-platform/json-schema/zipball/de96f482b6dcf913a2849a71ae102d2e45a61b52",
|
||||
"reference": "de96f482b6dcf913a2849a71ae102d2e45a61b52",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -1532,13 +1532,13 @@
|
|||
"swagger"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/api-platform/json-schema/tree/v4.2.16"
|
||||
"source": "https://github.com/api-platform/json-schema/tree/v4.2.17"
|
||||
},
|
||||
"time": "2026-02-13T15:07:33+00:00"
|
||||
"time": "2026-02-20T20:33:01+00:00"
|
||||
},
|
||||
{
|
||||
"name": "api-platform/jsonld",
|
||||
"version": "v4.2.16",
|
||||
"version": "v4.2.17",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/api-platform/jsonld.git",
|
||||
|
|
@ -1612,13 +1612,13 @@
|
|||
"rest"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/api-platform/jsonld/tree/v4.2.16"
|
||||
"source": "https://github.com/api-platform/jsonld/tree/v4.2.17"
|
||||
},
|
||||
"time": "2026-02-13T17:30:49+00:00"
|
||||
},
|
||||
{
|
||||
"name": "api-platform/metadata",
|
||||
"version": "v4.2.16",
|
||||
"version": "v4.2.17",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/api-platform/metadata.git",
|
||||
|
|
@ -1710,13 +1710,13 @@
|
|||
"swagger"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/api-platform/metadata/tree/v4.2.16"
|
||||
"source": "https://github.com/api-platform/metadata/tree/v4.2.17"
|
||||
},
|
||||
"time": "2026-02-13T15:07:33+00:00"
|
||||
},
|
||||
{
|
||||
"name": "api-platform/openapi",
|
||||
"version": "v4.2.16",
|
||||
"version": "v4.2.17",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/api-platform/openapi.git",
|
||||
|
|
@ -1800,22 +1800,22 @@
|
|||
"swagger"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/api-platform/openapi/tree/v4.2.16"
|
||||
"source": "https://github.com/api-platform/openapi/tree/v4.2.17"
|
||||
},
|
||||
"time": "2026-01-26T15:38:30+00:00"
|
||||
},
|
||||
{
|
||||
"name": "api-platform/serializer",
|
||||
"version": "v4.2.16",
|
||||
"version": "v4.2.17",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/api-platform/serializer.git",
|
||||
"reference": "e01024d458c26d230eafbe8ac79dc8e28c3dc379"
|
||||
"reference": "f021a31e6a409e8c3fd24bee1e5e183b995f9137"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/api-platform/serializer/zipball/e01024d458c26d230eafbe8ac79dc8e28c3dc379",
|
||||
"reference": "e01024d458c26d230eafbe8ac79dc8e28c3dc379",
|
||||
"url": "https://api.github.com/repos/api-platform/serializer/zipball/f021a31e6a409e8c3fd24bee1e5e183b995f9137",
|
||||
"reference": "f021a31e6a409e8c3fd24bee1e5e183b995f9137",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -1893,22 +1893,22 @@
|
|||
"serializer"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/api-platform/serializer/tree/v4.2.16"
|
||||
"source": "https://github.com/api-platform/serializer/tree/v4.2.17"
|
||||
},
|
||||
"time": "2026-02-13T17:30:49+00:00"
|
||||
"time": "2026-02-20T09:35:37+00:00"
|
||||
},
|
||||
{
|
||||
"name": "api-platform/state",
|
||||
"version": "v4.2.16",
|
||||
"version": "v4.2.17",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/api-platform/state.git",
|
||||
"reference": "0fcd612696acac4632a626bb5dfc6bd99ec3b44a"
|
||||
"reference": "1b6f69c75579ab0f132cd45e45d5f43ed19a15a5"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/api-platform/state/zipball/0fcd612696acac4632a626bb5dfc6bd99ec3b44a",
|
||||
"reference": "0fcd612696acac4632a626bb5dfc6bd99ec3b44a",
|
||||
"url": "https://api.github.com/repos/api-platform/state/zipball/1b6f69c75579ab0f132cd45e45d5f43ed19a15a5",
|
||||
"reference": "1b6f69c75579ab0f132cd45e45d5f43ed19a15a5",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -1990,22 +1990,22 @@
|
|||
"swagger"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/api-platform/state/tree/v4.2.16"
|
||||
"source": "https://github.com/api-platform/state/tree/v4.2.17"
|
||||
},
|
||||
"time": "2026-02-13T15:07:33+00:00"
|
||||
"time": "2026-02-17T09:18:17+00:00"
|
||||
},
|
||||
{
|
||||
"name": "api-platform/symfony",
|
||||
"version": "v4.2.16",
|
||||
"version": "v4.2.17",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/api-platform/symfony.git",
|
||||
"reference": "769f5bc29ce59a5c68006ca5876c409072340e92"
|
||||
"reference": "04052b61e26a1c059bb595b78670302ad4517b7f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/api-platform/symfony/zipball/769f5bc29ce59a5c68006ca5876c409072340e92",
|
||||
"reference": "769f5bc29ce59a5c68006ca5876c409072340e92",
|
||||
"url": "https://api.github.com/repos/api-platform/symfony/zipball/04052b61e26a1c059bb595b78670302ad4517b7f",
|
||||
"reference": "04052b61e26a1c059bb595b78670302ad4517b7f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -2118,13 +2118,13 @@
|
|||
"symfony"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/api-platform/symfony/tree/v4.2.16"
|
||||
"source": "https://github.com/api-platform/symfony/tree/v4.2.17"
|
||||
},
|
||||
"time": "2026-02-13T17:30:49+00:00"
|
||||
"time": "2026-02-18T14:55:07+00:00"
|
||||
},
|
||||
{
|
||||
"name": "api-platform/validator",
|
||||
"version": "v4.2.16",
|
||||
"version": "v4.2.17",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/api-platform/validator.git",
|
||||
|
|
@ -2194,7 +2194,7 @@
|
|||
"validator"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/api-platform/validator/tree/v4.2.16"
|
||||
"source": "https://github.com/api-platform/validator/tree/v4.2.17"
|
||||
},
|
||||
"time": "2026-01-26T15:45:40+00:00"
|
||||
},
|
||||
|
|
@ -9500,33 +9500,35 @@
|
|||
},
|
||||
{
|
||||
"name": "sabberworm/php-css-parser",
|
||||
"version": "v9.1.0",
|
||||
"version": "v9.2.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/MyIntervals/PHP-CSS-Parser.git",
|
||||
"reference": "1b363fdbdc6dd0ca0f4bf98d3a4d7f388133f1fb"
|
||||
"reference": "59373045e11ad47b5c18fc615feee0219e42f6d3"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/1b363fdbdc6dd0ca0f4bf98d3a4d7f388133f1fb",
|
||||
"reference": "1b363fdbdc6dd0ca0f4bf98d3a4d7f388133f1fb",
|
||||
"url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/59373045e11ad47b5c18fc615feee0219e42f6d3",
|
||||
"reference": "59373045e11ad47b5c18fc615feee0219e42f6d3",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-iconv": "*",
|
||||
"php": "^7.2.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
|
||||
"thecodingmachine/safe": "^1.3 || ^2.5 || ^3.3"
|
||||
"thecodingmachine/safe": "^1.3 || ^2.5 || ^3.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"php-parallel-lint/php-parallel-lint": "1.4.0",
|
||||
"phpstan/extension-installer": "1.4.3",
|
||||
"phpstan/phpstan": "1.12.28 || 2.1.25",
|
||||
"phpstan/phpstan-phpunit": "1.4.2 || 2.0.7",
|
||||
"phpstan/phpstan-strict-rules": "1.6.2 || 2.0.6",
|
||||
"phpunit/phpunit": "8.5.46",
|
||||
"phpstan/phpstan": "1.12.32 || 2.1.32",
|
||||
"phpstan/phpstan-phpunit": "1.4.2 || 2.0.8",
|
||||
"phpstan/phpstan-strict-rules": "1.6.2 || 2.0.7",
|
||||
"phpunit/phpunit": "8.5.52",
|
||||
"rawr/phpunit-data-provider": "3.3.1",
|
||||
"rector/rector": "1.2.10 || 2.1.7",
|
||||
"rector/type-perfect": "1.0.0 || 2.1.0"
|
||||
"rector/rector": "1.2.10 || 2.2.8",
|
||||
"rector/type-perfect": "1.0.0 || 2.1.0",
|
||||
"squizlabs/php_codesniffer": "4.0.1",
|
||||
"thecodingmachine/phpstan-safe-rule": "1.2.0 || 1.4.1"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-mbstring": "for parsing UTF-8 CSS"
|
||||
|
|
@ -9534,10 +9536,14 @@
|
|||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "9.2.x-dev"
|
||||
"dev-main": "9.3.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"src/Rule/Rule.php",
|
||||
"src/RuleSet/RuleContainer.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Sabberworm\\CSS\\": "src/"
|
||||
}
|
||||
|
|
@ -9568,9 +9574,9 @@
|
|||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/MyIntervals/PHP-CSS-Parser/issues",
|
||||
"source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v9.1.0"
|
||||
"source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v9.2.0"
|
||||
},
|
||||
"time": "2025-09-14T07:37:21+00:00"
|
||||
"time": "2026-02-21T17:12:03+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sabre/uri",
|
||||
|
|
@ -17669,16 +17675,16 @@
|
|||
},
|
||||
{
|
||||
"name": "webmozart/assert",
|
||||
"version": "2.1.3",
|
||||
"version": "2.1.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/webmozarts/assert.git",
|
||||
"reference": "6976757ba8dd70bf8cbaea0914ad84d8b51a9f46"
|
||||
"reference": "79155f94852fa27e2f73b459f6503f5e87e2c188"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/webmozarts/assert/zipball/6976757ba8dd70bf8cbaea0914ad84d8b51a9f46",
|
||||
"reference": "6976757ba8dd70bf8cbaea0914ad84d8b51a9f46",
|
||||
"url": "https://api.github.com/repos/webmozarts/assert/zipball/79155f94852fa27e2f73b459f6503f5e87e2c188",
|
||||
"reference": "79155f94852fa27e2f73b459f6503f5e87e2c188",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -17725,9 +17731,9 @@
|
|||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/webmozarts/assert/issues",
|
||||
"source": "https://github.com/webmozarts/assert/tree/2.1.3"
|
||||
"source": "https://github.com/webmozarts/assert/tree/2.1.5"
|
||||
},
|
||||
"time": "2026-02-13T21:01:40+00:00"
|
||||
"time": "2026-02-18T14:09:36+00:00"
|
||||
},
|
||||
{
|
||||
"name": "willdurand/negotiation",
|
||||
|
|
@ -18417,16 +18423,16 @@
|
|||
},
|
||||
{
|
||||
"name": "phpstan/phpstan-doctrine",
|
||||
"version": "2.0.16",
|
||||
"version": "2.0.17",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpstan/phpstan-doctrine.git",
|
||||
"reference": "f4ff6084a26d91174b3f0b047589af293a893104"
|
||||
"reference": "734ef36c2709b51943f04aacadddb76f239562d3"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/f4ff6084a26d91174b3f0b047589af293a893104",
|
||||
"reference": "f4ff6084a26d91174b3f0b047589af293a893104",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/734ef36c2709b51943f04aacadddb76f239562d3",
|
||||
"reference": "734ef36c2709b51943f04aacadddb76f239562d3",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -18487,9 +18493,9 @@
|
|||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/phpstan/phpstan-doctrine/issues",
|
||||
"source": "https://github.com/phpstan/phpstan-doctrine/tree/2.0.16"
|
||||
"source": "https://github.com/phpstan/phpstan-doctrine/tree/2.0.17"
|
||||
},
|
||||
"time": "2026-02-11T08:54:45+00:00"
|
||||
"time": "2026-02-18T10:21:23+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan-strict-rules",
|
||||
|
|
@ -18965,16 +18971,16 @@
|
|||
},
|
||||
{
|
||||
"name": "phpunit/phpunit",
|
||||
"version": "11.5.53",
|
||||
"version": "11.5.55",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/phpunit.git",
|
||||
"reference": "a997a653a82845f1240d73ee73a8a4e97e4b0607"
|
||||
"reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a997a653a82845f1240d73ee73a8a4e97e4b0607",
|
||||
"reference": "a997a653a82845f1240d73ee73a8a4e97e4b0607",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/adc7262fccc12de2b30f12a8aa0b33775d814f00",
|
||||
"reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -19047,7 +19053,7 @@
|
|||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
|
||||
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.53"
|
||||
"source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.55"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -19071,20 +19077,20 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-02-10T12:28:25+00:00"
|
||||
"time": "2026-02-18T12:37:06+00:00"
|
||||
},
|
||||
{
|
||||
"name": "rector/rector",
|
||||
"version": "2.3.6",
|
||||
"version": "2.3.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/rectorphp/rector.git",
|
||||
"reference": "ca9ebb81d280cd362ea39474dabd42679e32ca6b"
|
||||
"reference": "bbd37aedd8df749916cffa2a947cfc4714d1ba2c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/rectorphp/rector/zipball/ca9ebb81d280cd362ea39474dabd42679e32ca6b",
|
||||
"reference": "ca9ebb81d280cd362ea39474dabd42679e32ca6b",
|
||||
"url": "https://api.github.com/repos/rectorphp/rector/zipball/bbd37aedd8df749916cffa2a947cfc4714d1ba2c",
|
||||
"reference": "bbd37aedd8df749916cffa2a947cfc4714d1ba2c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -19123,7 +19129,7 @@
|
|||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/rectorphp/rector/issues",
|
||||
"source": "https://github.com/rectorphp/rector/tree/2.3.6"
|
||||
"source": "https://github.com/rectorphp/rector/tree/2.3.8"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -19131,7 +19137,7 @@
|
|||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-02-06T14:25:06+00:00"
|
||||
"time": "2026-02-22T09:45:50+00:00"
|
||||
},
|
||||
{
|
||||
"name": "roave/security-advisories",
|
||||
|
|
@ -19139,12 +19145,12 @@
|
|||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Roave/SecurityAdvisories.git",
|
||||
"reference": "7f3e95c9ebf1b16e002dd2c913d30d962c2a6a16"
|
||||
"reference": "92c5ec5685cfbcd7ef721a502e6622516728011c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/7f3e95c9ebf1b16e002dd2c913d30d962c2a6a16",
|
||||
"reference": "7f3e95c9ebf1b16e002dd2c913d30d962c2a6a16",
|
||||
"url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/92c5ec5685cfbcd7ef721a502e6622516728011c",
|
||||
"reference": "92c5ec5685cfbcd7ef721a502e6622516728011c",
|
||||
"shasum": ""
|
||||
},
|
||||
"conflict": {
|
||||
|
|
@ -19312,6 +19318,7 @@
|
|||
"devgroup/dotplant": "<2020.09.14-dev",
|
||||
"digimix/wp-svg-upload": "<=1",
|
||||
"directmailteam/direct-mail": "<6.0.3|>=7,<7.0.3|>=8,<9.5.2",
|
||||
"directorytree/imapengine": "<1.22.3",
|
||||
"dl/yag": "<3.0.1",
|
||||
"dmk/webkitpdf": "<1.1.4",
|
||||
"dnadesign/silverstripe-elemental": "<5.3.12",
|
||||
|
|
@ -19414,7 +19421,7 @@
|
|||
"filegator/filegator": "<7.8",
|
||||
"filp/whoops": "<2.1.13",
|
||||
"fineuploader/php-traditional-server": "<=1.2.2",
|
||||
"firebase/php-jwt": "<6",
|
||||
"firebase/php-jwt": "<7",
|
||||
"fisharebest/webtrees": "<=2.1.18",
|
||||
"fixpunkt/fp-masterquiz": "<2.2.1|>=3,<3.5.2",
|
||||
"fixpunkt/fp-newsletter": "<1.1.1|>=1.2,<2.1.2|>=2.2,<3.2.6",
|
||||
|
|
@ -19452,7 +19459,7 @@
|
|||
"genix/cms": "<=1.1.11",
|
||||
"georgringer/news": "<1.3.3",
|
||||
"geshi/geshi": "<=1.0.9.1",
|
||||
"getformwork/formwork": "<2.2",
|
||||
"getformwork/formwork": "<=2.3.3",
|
||||
"getgrav/grav": "<1.11.0.0-beta1",
|
||||
"getkirby/cms": "<3.9.8.3-dev|>=3.10,<3.10.1.2-dev|>=4,<4.7.1|>=5,<=5.2.1",
|
||||
"getkirby/kirby": "<3.9.8.3-dev|>=3.10,<3.10.1.2-dev|>=4,<4.7.1",
|
||||
|
|
@ -19575,7 +19582,7 @@
|
|||
"leantime/leantime": "<3.3",
|
||||
"lexik/jwt-authentication-bundle": "<2.10.7|>=2.11,<2.11.3",
|
||||
"libreform/libreform": ">=2,<=2.0.8",
|
||||
"librenms/librenms": "<25.12",
|
||||
"librenms/librenms": "<26.2",
|
||||
"liftkit/database": "<2.13.2",
|
||||
"lightsaml/lightsaml": "<1.3.5",
|
||||
"limesurvey/limesurvey": "<6.5.12",
|
||||
|
|
@ -19783,7 +19790,7 @@
|
|||
"propel/propel": ">=2.0.0.0-alpha1,<=2.0.0.0-alpha7",
|
||||
"propel/propel1": ">=1,<=1.7.1",
|
||||
"psy/psysh": "<=0.11.22|>=0.12,<=0.12.18",
|
||||
"pterodactyl/panel": "<1.12",
|
||||
"pterodactyl/panel": "<1.12.1",
|
||||
"ptheofan/yii2-statemachine": ">=2.0.0.0-RC1-dev,<=2",
|
||||
"ptrofimov/beanstalk_console": "<1.7.14",
|
||||
"pubnub/pubnub": "<6.1",
|
||||
|
|
@ -19886,7 +19893,7 @@
|
|||
"starcitizentools/short-description": ">=4,<4.0.1",
|
||||
"starcitizentools/tabber-neue": ">=1.9.1,<2.7.2|>=3,<3.1.1",
|
||||
"starcitizenwiki/embedvideo": "<=4",
|
||||
"statamic/cms": "<5.73.6|>=6,<6.2.5",
|
||||
"statamic/cms": "<5.73.9|>=6,<6.3.2",
|
||||
"stormpath/sdk": "<9.9.99",
|
||||
"studio-42/elfinder": "<=2.1.64",
|
||||
"studiomitte/friendlycaptcha": "<0.1.4",
|
||||
|
|
@ -20058,7 +20065,7 @@
|
|||
"wpanel/wpanel4-cms": "<=4.3.1",
|
||||
"wpcloud/wp-stateless": "<3.2",
|
||||
"wpglobus/wpglobus": "<=1.9.6",
|
||||
"wwbn/avideo": "<14.3",
|
||||
"wwbn/avideo": "<21",
|
||||
"xataface/xataface": "<3",
|
||||
"xpressengine/xpressengine": "<3.0.15",
|
||||
"yab/quarx": "<2.4.5",
|
||||
|
|
@ -20117,7 +20124,8 @@
|
|||
"zf-commons/zfc-user": "<1.2.2",
|
||||
"zfcampus/zf-apigility-doctrine": ">=1,<1.0.3",
|
||||
"zfr/zfr-oauth2-server-module": "<0.1.2",
|
||||
"zoujingli/thinkadmin": "<=6.1.53"
|
||||
"zoujingli/thinkadmin": "<=6.1.53",
|
||||
"zumba/json-serializer": "<3.2.3"
|
||||
},
|
||||
"default-branch": true,
|
||||
"type": "metapackage",
|
||||
|
|
@ -20155,7 +20163,7 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-02-13T23:11:21+00:00"
|
||||
"time": "2026-02-20T22:06:39+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/cli-parser",
|
||||
|
|
|
|||
|
|
@ -41,11 +41,16 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Exceptions\InfoProviderNotActiveException;
|
||||
use App\Form\LabelSystem\ScanDialogType;
|
||||
use App\Services\LabelSystem\BarcodeScanner\BarcodeRedirector;
|
||||
use App\Services\InfoProviderSystem\Providers\LCSCProvider;
|
||||
use App\Services\LabelSystem\BarcodeScanner\BarcodeScanResultHandler;
|
||||
use App\Services\LabelSystem\BarcodeScanner\BarcodeScanHelper;
|
||||
use App\Services\LabelSystem\BarcodeScanner\BarcodeScanResultInterface;
|
||||
use App\Services\LabelSystem\BarcodeScanner\BarcodeSourceType;
|
||||
use App\Services\LabelSystem\BarcodeScanner\LocalBarcodeScanResult;
|
||||
use App\Services\LabelSystem\BarcodeScanner\LCSCBarcodeScanResult;
|
||||
use App\Services\LabelSystem\BarcodeScanner\EIGP114BarcodeScanResult;
|
||||
use Doctrine\ORM\EntityNotFoundException;
|
||||
use InvalidArgumentException;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
|
|
@ -53,6 +58,13 @@ use Symfony\Component\HttpFoundation\Request;
|
|||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use App\Services\InfoProviderSystem\PartInfoRetriever;
|
||||
use App\Services\InfoProviderSystem\ProviderRegistry;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use App\Entity\Parts\Part;
|
||||
use \App\Entity\Parts\StorageLocation;
|
||||
use Symfony\UX\Turbo\TurboBundle;
|
||||
|
||||
/**
|
||||
* @see \App\Tests\Controller\ScanControllerTest
|
||||
|
|
@ -60,9 +72,10 @@ use Symfony\Component\Routing\Attribute\Route;
|
|||
#[Route(path: '/scan')]
|
||||
class ScanController extends AbstractController
|
||||
{
|
||||
public function __construct(protected BarcodeRedirector $barcodeParser, protected BarcodeScanHelper $barcodeNormalizer)
|
||||
{
|
||||
}
|
||||
public function __construct(
|
||||
protected BarcodeScanResultHandler $resultHandler,
|
||||
protected BarcodeScanHelper $barcodeNormalizer,
|
||||
) {}
|
||||
|
||||
#[Route(path: '', name: 'scan_dialog')]
|
||||
public function dialog(Request $request, #[MapQueryParameter] ?string $input = null): Response
|
||||
|
|
@ -72,35 +85,86 @@ class ScanController extends AbstractController
|
|||
$form = $this->createForm(ScanDialogType::class);
|
||||
$form->handleRequest($request);
|
||||
|
||||
// If JS is working, scanning uses /scan/lookup and this action just renders the page.
|
||||
// This fallback only runs if user submits the form manually or uses ?input=...
|
||||
if ($input === null && $form->isSubmitted() && $form->isValid()) {
|
||||
$input = $form['input']->getData();
|
||||
$mode = $form['mode']->getData();
|
||||
}
|
||||
|
||||
$infoModeData = null;
|
||||
|
||||
if ($input !== null) {
|
||||
if ($input !== null && $input !== '') {
|
||||
$mode = $form->isSubmitted() ? $form['mode']->getData() : null;
|
||||
$infoMode = $form->isSubmitted() && $form['info_mode']->getData();
|
||||
|
||||
try {
|
||||
$scan_result = $this->barcodeNormalizer->scanBarcodeContent($input, $mode ?? null);
|
||||
//Perform a redirect if the info mode is not enabled
|
||||
if (!$form['info_mode']->getData()) {
|
||||
try {
|
||||
return $this->redirect($this->barcodeParser->getRedirectURL($scan_result));
|
||||
} catch (EntityNotFoundException) {
|
||||
$this->addFlash('success', 'scan.qr_not_found');
|
||||
$scan = $this->barcodeNormalizer->scanBarcodeContent($input, $mode ?? null);
|
||||
|
||||
// If not in info mode, mimic “normal scan” behavior: redirect if possible.
|
||||
if (!$infoMode) {
|
||||
|
||||
// Try to get an Info URL if possible
|
||||
$url = $this->resultHandler->getInfoURL($scan);
|
||||
if ($url !== null) {
|
||||
return $this->redirect($url);
|
||||
}
|
||||
|
||||
//Try to get an creation URL if possible (only for vendor codes)
|
||||
$createUrl = $this->buildCreateUrlForScanResult($scan);
|
||||
if ($createUrl !== null) {
|
||||
return $this->redirect($createUrl);
|
||||
}
|
||||
|
||||
//// Otherwise: show “not found” (not “format unknown”)
|
||||
$this->addFlash('warning', 'scan.qr_not_found');
|
||||
} else { // Info mode
|
||||
// Info mode fallback: render page with prefilled result
|
||||
$decoded = $scan->getDecodedForInfoMode();
|
||||
|
||||
//Try to resolve to an entity, to enhance info mode with entity-specific data
|
||||
$dbEntity = $this->resultHandler->resolveEntity($scan);
|
||||
$resolvedPart = $this->resultHandler->resolvePart($scan);
|
||||
$openUrl = $this->resultHandler->getInfoURL($scan);
|
||||
|
||||
//If no entity is found, try to create an URL for creating a new part (only for vendor codes)
|
||||
$createUrl = null;
|
||||
if ($dbEntity === null) {
|
||||
$createUrl = $this->buildCreateUrlForScanResult($scan);
|
||||
}
|
||||
|
||||
if (TurboBundle::STREAM_FORMAT === $request->getPreferredFormat()) {
|
||||
$request->setRequestFormat(TurboBundle::STREAM_FORMAT);
|
||||
return $this->renderBlock('label_system/scanner/scanner.html.twig', 'scan_results', [
|
||||
'decoded' => $decoded,
|
||||
'entity' => $dbEntity,
|
||||
'part' => $resolvedPart,
|
||||
'openUrl' => $openUrl,
|
||||
'createUrl' => $createUrl,
|
||||
]);
|
||||
}
|
||||
} else { //Otherwise retrieve infoModeData
|
||||
$infoModeData = $scan_result->getDecodedForInfoMode();
|
||||
|
||||
}
|
||||
} catch (InvalidArgumentException) {
|
||||
$this->addFlash('error', 'scan.format_unknown');
|
||||
} catch (\Throwable $e) {
|
||||
// Keep fallback user-friendly; avoid 500
|
||||
$this->addFlash('warning', 'scan.format_unknown');
|
||||
}
|
||||
}
|
||||
|
||||
//When we reach here, only the flash messages are relevant, so if it's a Turbo request, only send the flash message fragment, so the client can show it without a full page reload
|
||||
if (TurboBundle::STREAM_FORMAT === $request->getPreferredFormat()) {
|
||||
$request->setRequestFormat(TurboBundle::STREAM_FORMAT);
|
||||
//Only send our flash message, so the client can show it without a full page reload
|
||||
return $this->renderBlock('_turbo_control.html.twig', 'flashes');
|
||||
}
|
||||
|
||||
return $this->render('label_system/scanner/scanner.html.twig', [
|
||||
'form' => $form,
|
||||
'infoModeData' => $infoModeData,
|
||||
|
||||
//Info mode
|
||||
'decoded' => $decoded ?? null,
|
||||
'entity' => $dbEntity ?? null,
|
||||
'part' => $resolvedPart ?? null,
|
||||
'openUrl' => $openUrl ?? null,
|
||||
'createUrl' => $createUrl ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -125,11 +189,30 @@ class ScanController extends AbstractController
|
|||
source_type: BarcodeSourceType::INTERNAL
|
||||
);
|
||||
|
||||
return $this->redirect($this->barcodeParser->getRedirectURL($scan_result));
|
||||
return $this->redirect($this->resultHandler->getInfoURL($scan_result) ?? throw new EntityNotFoundException("Not found"));
|
||||
} catch (EntityNotFoundException) {
|
||||
$this->addFlash('success', 'scan.qr_not_found');
|
||||
|
||||
return $this->redirectToRoute('homepage');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a URL for creating a new part based on the barcode data, handles exceptions and shows user-friendly error messages if the provider is not active or if there is an error during URL generation.
|
||||
* @param BarcodeScanResultInterface $scanResult
|
||||
* @return string|null
|
||||
*/
|
||||
private function buildCreateUrlForScanResult(BarcodeScanResultInterface $scanResult): ?string
|
||||
{
|
||||
try {
|
||||
return $this->resultHandler->getCreationURL($scanResult);
|
||||
} catch (InfoProviderNotActiveException $e) {
|
||||
$this->addFlash('error', $e->getMessage());
|
||||
} catch (\Throwable) {
|
||||
// Don’t break scanning UX if provider lookup fails
|
||||
$this->addFlash('error', 'An error occurred while looking up the provider for this barcode. Please try again later.');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
48
src/Exceptions/InfoProviderNotActiveException.php
Normal file
48
src/Exceptions/InfoProviderNotActiveException.php
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
<?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\Exceptions;
|
||||
|
||||
use App\Services\InfoProviderSystem\Providers\InfoProviderInterface;
|
||||
|
||||
/**
|
||||
* An exception denoting that a required info provider is not active. This can be used to display a user-friendly error message,
|
||||
* when a user tries to use an info provider that is not active.
|
||||
*/
|
||||
class InfoProviderNotActiveException extends \RuntimeException
|
||||
{
|
||||
public function __construct(public readonly string $providerKey, public readonly string $friendlyName)
|
||||
{
|
||||
parent::__construct(sprintf('The info provider "%s" (%s) is not active.', $this->friendlyName, $this->providerKey));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance of this exception from an info provider instance
|
||||
* @param InfoProviderInterface $provider
|
||||
* @return self
|
||||
*/
|
||||
public static function fromProvider(InfoProviderInterface $provider): self
|
||||
{
|
||||
return new self($provider->getProviderKey(), $provider->getProviderInfo()['name'] ?? '???');
|
||||
}
|
||||
}
|
||||
|
|
@ -77,6 +77,7 @@ class ScanDialogType extends AbstractType
|
|||
BarcodeSourceType::USER_DEFINED => 'scan_dialog.mode.user',
|
||||
BarcodeSourceType::EIGP114 => 'scan_dialog.mode.eigp',
|
||||
BarcodeSourceType::GTIN => 'scan_dialog.mode.gtin',
|
||||
BarcodeSourceType::LCSC => 'scan_dialog.mode.lcsc',
|
||||
},
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -389,4 +389,69 @@ class PartRepository extends NamedDBElementRepository
|
|||
return $baseIpn . '_' . ($maxSuffix + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a part based on the provided info provider key and ID, with an option for case sensitivity.
|
||||
* If no part is found with the given provider key and ID, null is returned.
|
||||
* @param string $providerID
|
||||
* @param string|null $providerKey If null, the provider key will not be included in the search criteria, and only the provider ID will be used for matching.
|
||||
* @param bool $caseInsensitive If true, the provider ID comparison will be case-insensitive. Default is true.
|
||||
* @return Part|null
|
||||
*/
|
||||
public function getPartByProviderInfo(string $providerID, ?string $providerKey = null, bool $caseInsensitive = true): ?Part
|
||||
{
|
||||
$qb = $this->createQueryBuilder('part');
|
||||
$qb->select('part');
|
||||
|
||||
if ($providerKey) {
|
||||
$qb->where("part.providerReference.provider_key = :providerKey");
|
||||
$qb->setParameter('providerKey', $providerKey);
|
||||
}
|
||||
|
||||
|
||||
if ($caseInsensitive) {
|
||||
$qb->andWhere("LOWER(part.providerReference.provider_id) = LOWER(:providerID)");
|
||||
} else {
|
||||
$qb->andWhere("part.providerReference.provider_id = :providerID");
|
||||
}
|
||||
|
||||
$qb->setParameter('providerID', $providerID);
|
||||
|
||||
return $qb->getQuery()->getOneOrNullResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a part based on the provided MPN (Manufacturer Part Number), with an option for case sensitivity.
|
||||
* If no part is found with the given MPN, null is returned.
|
||||
* @param string $mpn
|
||||
* @param string|null $manufacturerName If provided, the search will also include a match for the manufacturer's name. If null, the manufacturer name will not be included in the search criteria.
|
||||
* @param bool $caseInsensitive If true, the MPN comparison will be case-insensitive. Default is true (case-insensitive).
|
||||
* @return Part|null
|
||||
*/
|
||||
public function getPartByMPN(string $mpn, ?string $manufacturerName = null, bool $caseInsensitive = true): ?Part
|
||||
{
|
||||
$qb = $this->createQueryBuilder('part');
|
||||
$qb->select('part');
|
||||
|
||||
if ($caseInsensitive) {
|
||||
$qb->where("LOWER(part.manufacturer_product_number) = LOWER(:mpn)");
|
||||
} else {
|
||||
$qb->where("part.manufacturer_product_number = :mpn");
|
||||
}
|
||||
|
||||
if ($manufacturerName !== null) {
|
||||
$qb->leftJoin('part.manufacturer', 'manufacturer');
|
||||
|
||||
if ($caseInsensitive) {
|
||||
$qb->andWhere("LOWER(manufacturer.name) = LOWER(:manufacturerName)");
|
||||
} else {
|
||||
$qb->andWhere("manufacturer.name = :manufacturerName");
|
||||
}
|
||||
$qb->setParameter('manufacturerName', $manufacturerName);
|
||||
}
|
||||
|
||||
$qb->setParameter('mpn', $mpn);
|
||||
|
||||
return $qb->getQuery()->getOneOrNullResult();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,10 +24,15 @@ declare(strict_types=1);
|
|||
namespace App\Services\InfoProviderSystem;
|
||||
|
||||
use App\Entity\Parts\Part;
|
||||
use App\Exceptions\InfoProviderNotActiveException;
|
||||
use App\Exceptions\OAuthReconnectRequiredException;
|
||||
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
|
||||
use App\Services\InfoProviderSystem\Providers\InfoProviderInterface;
|
||||
use Psr\Http\Client\ClientExceptionInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\HttpClient\Exception\ClientException;
|
||||
use Symfony\Component\HttpClient\Exception\TransportException;
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
use Symfony\Contracts\Cache\ItemInterface;
|
||||
|
||||
|
|
@ -49,6 +54,11 @@ final class PartInfoRetriever
|
|||
* @param string[]|InfoProviderInterface[] $providers A list of providers to search in, either as provider keys or as provider instances
|
||||
* @param string $keyword The keyword to search for
|
||||
* @return SearchResultDTO[] The search results
|
||||
* @throws InfoProviderNotActiveException if any of the given providers is not active
|
||||
* @throws ClientException if any of the providers throws an exception during the search
|
||||
* @throws \InvalidArgumentException if any of the given providers is not a valid provider key or instance
|
||||
* @throws TransportException if any of the providers throws an exception during the search
|
||||
* @throws OAuthReconnectRequiredException if any of the providers throws an exception during the search that indicates that the OAuth token needs to be refreshed
|
||||
*/
|
||||
public function searchByKeyword(string $keyword, array $providers): array
|
||||
{
|
||||
|
|
@ -61,7 +71,7 @@ final class PartInfoRetriever
|
|||
|
||||
//Ensure that the provider is active
|
||||
if (!$provider->isActive()) {
|
||||
throw new \RuntimeException("The provider with key {$provider->getProviderKey()} is not active!");
|
||||
throw InfoProviderNotActiveException::fromProvider($provider);
|
||||
}
|
||||
|
||||
if (!$provider instanceof InfoProviderInterface) {
|
||||
|
|
@ -97,6 +107,7 @@ final class PartInfoRetriever
|
|||
* @param string $provider_key
|
||||
* @param string $part_id
|
||||
* @return PartDetailDTO
|
||||
* @throws InfoProviderNotActiveException if the the given providers is not active
|
||||
*/
|
||||
public function getDetails(string $provider_key, string $part_id): PartDetailDTO
|
||||
{
|
||||
|
|
@ -104,7 +115,7 @@ final class PartInfoRetriever
|
|||
|
||||
//Ensure that the provider is active
|
||||
if (!$provider->isActive()) {
|
||||
throw new \RuntimeException("The provider with key $provider_key is not active!");
|
||||
throw InfoProviderNotActiveException::fromProvider($provider);
|
||||
}
|
||||
|
||||
//Generate key and escape reserved characters from the provider id
|
||||
|
|
|
|||
|
|
@ -1,180 +0,0 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace App\Services\LabelSystem\BarcodeScanner;
|
||||
|
||||
use App\Entity\LabelSystem\LabelSupportedElement;
|
||||
use App\Entity\Parts\Manufacturer;
|
||||
use App\Entity\Parts\Part;
|
||||
use App\Entity\Parts\PartLot;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityNotFoundException;
|
||||
use InvalidArgumentException;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
/**
|
||||
* @see \App\Tests\Services\LabelSystem\Barcodes\BarcodeRedirectorTest
|
||||
*/
|
||||
final class BarcodeRedirector
|
||||
{
|
||||
public function __construct(private readonly UrlGeneratorInterface $urlGenerator, private readonly EntityManagerInterface $em)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the URL to which the user should be redirected, when scanning a QR code.
|
||||
*
|
||||
* @param BarcodeScanResultInterface $barcodeScan The result of the barcode scan
|
||||
* @return string the URL to which should be redirected
|
||||
*
|
||||
* @throws EntityNotFoundException
|
||||
*/
|
||||
public function getRedirectURL(BarcodeScanResultInterface $barcodeScan): string
|
||||
{
|
||||
if($barcodeScan instanceof LocalBarcodeScanResult) {
|
||||
return $this->getURLLocalBarcode($barcodeScan);
|
||||
}
|
||||
|
||||
if ($barcodeScan instanceof EIGP114BarcodeScanResult) {
|
||||
return $this->getURLVendorBarcode($barcodeScan);
|
||||
}
|
||||
|
||||
if ($barcodeScan instanceof GTINBarcodeScanResult) {
|
||||
return $this->getURLGTINBarcode($barcodeScan);
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException('Unknown $barcodeScan type: '.get_class($barcodeScan));
|
||||
}
|
||||
|
||||
private function getURLLocalBarcode(LocalBarcodeScanResult $barcodeScan): string
|
||||
{
|
||||
switch ($barcodeScan->target_type) {
|
||||
case LabelSupportedElement::PART:
|
||||
return $this->urlGenerator->generate('app_part_show', ['id' => $barcodeScan->target_id]);
|
||||
case LabelSupportedElement::PART_LOT:
|
||||
//Try to determine the part to the given lot
|
||||
$lot = $this->em->find(PartLot::class, $barcodeScan->target_id);
|
||||
if (!$lot instanceof PartLot) {
|
||||
throw new EntityNotFoundException();
|
||||
}
|
||||
|
||||
return $this->urlGenerator->generate('app_part_show', ['id' => $lot->getPart()->getID(), 'highlightLot' => $lot->getID()]);
|
||||
|
||||
case LabelSupportedElement::STORELOCATION:
|
||||
return $this->urlGenerator->generate('part_list_store_location', ['id' => $barcodeScan->target_id]);
|
||||
|
||||
default:
|
||||
throw new InvalidArgumentException('Unknown $type: '.$barcodeScan->target_type->name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the URL to a part from a scan of a Vendor Barcode
|
||||
*/
|
||||
private function getURLVendorBarcode(EIGP114BarcodeScanResult $barcodeScan): string
|
||||
{
|
||||
$part = $this->getPartFromVendor($barcodeScan);
|
||||
return $this->urlGenerator->generate('app_part_show', ['id' => $part->getID()]);
|
||||
}
|
||||
|
||||
private function getURLGTINBarcode(GTINBarcodeScanResult $barcodeScan): string
|
||||
{
|
||||
$part = $this->em->getRepository(Part::class)->findOneBy(['gtin' => $barcodeScan->gtin]);
|
||||
if (!$part instanceof Part) {
|
||||
throw new EntityNotFoundException();
|
||||
}
|
||||
|
||||
return $this->urlGenerator->generate('app_part_show', ['id' => $part->getID()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a part from a scan of a Vendor Barcode by filtering for parts
|
||||
* with the same Info Provider Id or, if that fails, by looking for parts with a
|
||||
* matching manufacturer product number. Only returns the first matching part.
|
||||
*/
|
||||
private function getPartFromVendor(EIGP114BarcodeScanResult $barcodeScan) : Part
|
||||
{
|
||||
// first check via the info provider ID (e.g. Vendor ID). This might fail if the part was not added via
|
||||
// the info provider system or if the part was bought from a different vendor than the data was retrieved
|
||||
// from.
|
||||
if($barcodeScan->digikeyPartNumber) {
|
||||
$qb = $this->em->getRepository(Part::class)->createQueryBuilder('part');
|
||||
//Lower() to be case insensitive
|
||||
$qb->where($qb->expr()->like('LOWER(part.providerReference.provider_id)', 'LOWER(:vendor_id)'));
|
||||
$qb->setParameter('vendor_id', $barcodeScan->digikeyPartNumber);
|
||||
$results = $qb->getQuery()->getResult();
|
||||
if ($results) {
|
||||
return $results[0];
|
||||
}
|
||||
}
|
||||
|
||||
if(!$barcodeScan->supplierPartNumber){
|
||||
throw new EntityNotFoundException();
|
||||
}
|
||||
|
||||
//Fallback to the manufacturer part number. This may return false positives, since it is common for
|
||||
//multiple manufacturers to use the same part number for their version of a common product
|
||||
//We assume the user is able to realize when this returns the wrong part
|
||||
//If the barcode specifies the manufacturer we try to use that as well
|
||||
$mpnQb = $this->em->getRepository(Part::class)->createQueryBuilder('part');
|
||||
$mpnQb->where($mpnQb->expr()->like('LOWER(part.manufacturer_product_number)', 'LOWER(:mpn)'));
|
||||
$mpnQb->setParameter('mpn', $barcodeScan->supplierPartNumber);
|
||||
|
||||
if($barcodeScan->mouserManufacturer){
|
||||
$manufacturerQb = $this->em->getRepository(Manufacturer::class)->createQueryBuilder("manufacturer");
|
||||
$manufacturerQb->where($manufacturerQb->expr()->like("LOWER(manufacturer.name)", "LOWER(:manufacturer_name)"));
|
||||
$manufacturerQb->setParameter("manufacturer_name", $barcodeScan->mouserManufacturer);
|
||||
$manufacturers = $manufacturerQb->getQuery()->getResult();
|
||||
|
||||
if($manufacturers) {
|
||||
$mpnQb->andWhere($mpnQb->expr()->eq("part.manufacturer", ":manufacturer"));
|
||||
$mpnQb->setParameter("manufacturer", $manufacturers);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$results = $mpnQb->getQuery()->getResult();
|
||||
if($results){
|
||||
return $results[0];
|
||||
}
|
||||
throw new EntityNotFoundException();
|
||||
}
|
||||
}
|
||||
|
|
@ -92,10 +92,15 @@ final class BarcodeScanHelper
|
|||
if ($type === BarcodeSourceType::EIGP114) {
|
||||
return $this->parseEIGP114Barcode($input);
|
||||
}
|
||||
|
||||
if ($type === BarcodeSourceType::GTIN) {
|
||||
return $this->parseGTINBarcode($input);
|
||||
}
|
||||
|
||||
if ($type === BarcodeSourceType::LCSC) {
|
||||
return $this->parseLCSCBarcode($input);
|
||||
}
|
||||
|
||||
//Null means auto and we try the different formats
|
||||
$result = $this->parseInternalBarcode($input);
|
||||
|
||||
|
|
@ -125,6 +130,11 @@ final class BarcodeScanHelper
|
|||
return $this->parseGTINBarcode($input);
|
||||
}
|
||||
|
||||
// Try LCSC barcode
|
||||
if (LCSCBarcodeScanResult::isLCSCBarcode($input)) {
|
||||
return $this->parseLCSCBarcode($input);
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException('Unknown barcode');
|
||||
}
|
||||
|
||||
|
|
@ -138,6 +148,11 @@ final class BarcodeScanHelper
|
|||
return EIGP114BarcodeScanResult::parseFormat06Code($input);
|
||||
}
|
||||
|
||||
private function parseLCSCBarcode(string $input): LCSCBarcodeScanResult
|
||||
{
|
||||
return LCSCBarcodeScanResult::parse($input);
|
||||
}
|
||||
|
||||
private function parseUserDefinedBarcode(string $input): ?LocalBarcodeScanResult
|
||||
{
|
||||
$lot_repo = $this->entityManager->getRepository(PartLot::class);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,315 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace App\Services\LabelSystem\BarcodeScanner;
|
||||
|
||||
use App\Entity\LabelSystem\LabelSupportedElement;
|
||||
use App\Entity\Parts\Manufacturer;
|
||||
use App\Entity\Parts\Part;
|
||||
use App\Entity\Parts\PartLot;
|
||||
use App\Entity\Parts\StorageLocation;
|
||||
use App\Exceptions\InfoProviderNotActiveException;
|
||||
use App\Repository\Parts\PartRepository;
|
||||
use App\Services\InfoProviderSystem\PartInfoRetriever;
|
||||
use App\Services\InfoProviderSystem\ProviderRegistry;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityNotFoundException;
|
||||
use InvalidArgumentException;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
/**
|
||||
* This class handles the result of a barcode scan and determines further actions, like which URL the user should be redirected to.
|
||||
*
|
||||
* @see \App\Tests\Services\LabelSystem\Barcodes\BarcodeRedirectorTest
|
||||
*/
|
||||
final readonly class BarcodeScanResultHandler
|
||||
{
|
||||
public function __construct(private UrlGeneratorInterface $urlGenerator, private EntityManagerInterface $em, private PartInfoRetriever $infoRetriever,
|
||||
private ProviderRegistry $providerRegistry)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the URL to which the user should be redirected, when scanning a QR code.
|
||||
*
|
||||
* @param BarcodeScanResultInterface $barcodeScan The result of the barcode scan
|
||||
* @return string|null the URL to which should be redirected, or null if no suitable URL could be determined for the given barcode scan result
|
||||
*/
|
||||
public function getInfoURL(BarcodeScanResultInterface $barcodeScan): ?string
|
||||
{
|
||||
//For other barcodes try to resolve the part first and then redirect to the part page
|
||||
$entity = $this->resolveEntity($barcodeScan);
|
||||
|
||||
if ($entity === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($entity instanceof Part) {
|
||||
return $this->urlGenerator->generate('app_part_show', ['id' => $entity->getID()]);
|
||||
}
|
||||
|
||||
if ($entity instanceof PartLot) {
|
||||
return $this->urlGenerator->generate('app_part_show', ['id' => $entity->getPart()->getID(), 'highlightLot' => $entity->getID()]);
|
||||
}
|
||||
|
||||
if ($entity instanceof StorageLocation) {
|
||||
return $this->urlGenerator->generate('part_list_store_location', ['id' => $entity->getID()]);
|
||||
}
|
||||
|
||||
//@phpstan-ignore-next-line This should never happen, since resolveEntity should only return Part, PartLot or StorageLocation
|
||||
throw new \LogicException("Resolved entity is of unknown type: ".get_class($entity));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a URL to create a new part based on this barcode scan result, if possible.
|
||||
* @param BarcodeScanResultInterface $scanResult
|
||||
* @return string|null
|
||||
* @throws InfoProviderNotActiveException If the scan result contains information for a provider which is currently not active in the system
|
||||
*/
|
||||
public function getCreationURL(BarcodeScanResultInterface $scanResult): ?string
|
||||
{
|
||||
$infos = $this->getCreateInfos($scanResult);
|
||||
if ($infos === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
//Ensure that the provider is active, otherwise we should not generate a creation URL for it
|
||||
$provider = $this->providerRegistry->getProviderByKey($infos['providerKey']);
|
||||
if (!$provider->isActive()) {
|
||||
throw InfoProviderNotActiveException::fromProvider($provider);
|
||||
}
|
||||
|
||||
return $this->urlGenerator->generate('info_providers_create_part', ['providerKey' => $infos['providerKey'], 'providerId' => $infos['providerId']]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to resolve the given barcode scan result to a local entity. This can be a Part, a PartLot or a StorageLocation, depending on the type of the barcode and the information contained in it.
|
||||
* Returns null if no matching entity could be found.
|
||||
* @param BarcodeScanResultInterface $barcodeScan
|
||||
* @return Part|PartLot|StorageLocation|null
|
||||
*/
|
||||
public function resolveEntity(BarcodeScanResultInterface $barcodeScan): Part|PartLot|StorageLocation|null
|
||||
{
|
||||
if ($barcodeScan instanceof LocalBarcodeScanResult) {
|
||||
return $this->resolvePartFromLocal($barcodeScan);
|
||||
}
|
||||
|
||||
if ($barcodeScan instanceof EIGP114BarcodeScanResult) {
|
||||
return $this->resolvePartFromVendor($barcodeScan);
|
||||
}
|
||||
|
||||
if ($barcodeScan instanceof GTINBarcodeScanResult) {
|
||||
return $this->em->getRepository(Part::class)->findOneBy(['gtin' => $barcodeScan->gtin]);
|
||||
}
|
||||
|
||||
if ($barcodeScan instanceof LCSCBarcodeScanResult) {
|
||||
return $this->resolvePartFromLCSC($barcodeScan);
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException("Barcode does not support resolving to a local entity: ".get_class($barcodeScan));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to resolve a Part from the given barcode scan result. Returns null if no part could be found for the given barcode,
|
||||
* or the barcode doesn't contain information allowing to resolve to a local part.
|
||||
* @param BarcodeScanResultInterface $barcodeScan
|
||||
* @return Part|null
|
||||
* @throws \InvalidArgumentException if the barcode scan result type is unknown and cannot be handled this function
|
||||
*/
|
||||
public function resolvePart(BarcodeScanResultInterface $barcodeScan): ?Part
|
||||
{
|
||||
$entity = $this->resolveEntity($barcodeScan);
|
||||
if ($entity instanceof Part) {
|
||||
return $entity;
|
||||
}
|
||||
if ($entity instanceof PartLot) {
|
||||
return $entity->getPart();
|
||||
}
|
||||
//Storage locations are not associated with a specific part, so we cannot resolve a part for
|
||||
//a storage location barcode
|
||||
return null;
|
||||
}
|
||||
|
||||
private function resolvePartFromLocal(LocalBarcodeScanResult $barcodeScan): Part|PartLot|StorageLocation|null
|
||||
{
|
||||
return match ($barcodeScan->target_type) {
|
||||
LabelSupportedElement::PART => $this->em->find(Part::class, $barcodeScan->target_id),
|
||||
LabelSupportedElement::PART_LOT => $this->em->find(PartLot::class, $barcodeScan->target_id),
|
||||
LabelSupportedElement::STORELOCATION => $this->em->find(StorageLocation::class, $barcodeScan->target_id),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a part from a scan of a Vendor Barcode by filtering for parts
|
||||
* with the same Info Provider Id or, if that fails, by looking for parts with a
|
||||
* matching manufacturer product number. Only returns the first matching part.
|
||||
*/
|
||||
private function resolvePartFromVendor(EIGP114BarcodeScanResult $barcodeScan) : ?Part
|
||||
{
|
||||
// first check via the info provider ID (e.g. Vendor ID). This might fail if the part was not added via
|
||||
// the info provider system or if the part was bought from a different vendor than the data was retrieved
|
||||
// from.
|
||||
if($barcodeScan->digikeyPartNumber) {
|
||||
|
||||
$part = $this->em->getRepository(Part::class)->getPartByProviderInfo($barcodeScan->digikeyPartNumber);
|
||||
if ($part !== null) {
|
||||
return $part;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$barcodeScan->supplierPartNumber){
|
||||
return null;
|
||||
}
|
||||
|
||||
//Fallback to the manufacturer part number. This may return false positives, since it is common for
|
||||
//multiple manufacturers to use the same part number for their version of a common product
|
||||
//We assume the user is able to realize when this returns the wrong part
|
||||
//If the barcode specifies the manufacturer we try to use that as well
|
||||
|
||||
return $this->em->getRepository(Part::class)->getPartByMPN($barcodeScan->supplierPartNumber, $barcodeScan->mouserManufacturer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve LCSC barcode -> Part.
|
||||
* Strategy:
|
||||
* 1) Try providerReference.provider_id == pc (LCSC "Cxxxxxx") if you store it there
|
||||
* 2) Fallback to manufacturer_product_number == pm (MPN)
|
||||
* Returns first match (consistent with EIGP114 logic)
|
||||
*/
|
||||
private function resolvePartFromLCSC(LCSCBarcodeScanResult $barcodeScan): ?Part
|
||||
{
|
||||
// Try LCSC code (pc) as provider id if available
|
||||
$pc = $barcodeScan->lcscCode; // e.g. C138033
|
||||
if ($pc) {
|
||||
$part = $this->em->getRepository(Part::class)->getPartByProviderInfo($pc);
|
||||
if ($part !== null) {
|
||||
return $part;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to MPN (pm)
|
||||
$pm = $barcodeScan->mpn; // e.g. RC0402FR-071ML
|
||||
if (!$pm) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->em->getRepository(Part::class)->getPartByMPN($pm);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Tries to extract creation information for a part from the given barcode scan result. This can be used to
|
||||
* automatically fill in the info provider reference of a part, when creating a new part based on the scan result.
|
||||
* Returns null if no provider information could be extracted from the scan result, or if the scan result type is unknown and cannot be handled by this function.
|
||||
* It is not necessarily checked that the provider is active, or that the result actually exists on the provider side.
|
||||
* @param BarcodeScanResultInterface $scanResult
|
||||
* @return array{providerKey: string, providerId: string}|null
|
||||
* @throws InfoProviderNotActiveException If the scan result contains information for a provider which is currently not active in the system
|
||||
*/
|
||||
public function getCreateInfos(BarcodeScanResultInterface $scanResult): ?array
|
||||
{
|
||||
// LCSC
|
||||
if ($scanResult instanceof LCSCBarcodeScanResult) {
|
||||
return [
|
||||
'providerKey' => 'lcsc',
|
||||
'providerId' => $scanResult->lcscCode,
|
||||
];
|
||||
}
|
||||
|
||||
if ($scanResult instanceof EIGP114BarcodeScanResult) {
|
||||
return $this->getCreationInfoForEIGP114($scanResult);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param EIGP114BarcodeScanResult $scanResult
|
||||
* @return array{providerKey: string, providerId: string}|null
|
||||
*/
|
||||
private function getCreationInfoForEIGP114(EIGP114BarcodeScanResult $scanResult): ?array
|
||||
{
|
||||
$vendor = $scanResult->guessBarcodeVendor();
|
||||
|
||||
// Mouser: use supplierPartNumber -> search provider -> provider_id
|
||||
if ($vendor === 'mouser' && $scanResult->supplierPartNumber !== null
|
||||
) {
|
||||
// Search Mouser using the MPN
|
||||
$dtos = $this->infoRetriever->searchByKeyword(
|
||||
keyword: $scanResult->supplierPartNumber,
|
||||
providers: ["mouser"]
|
||||
);
|
||||
|
||||
// If there are results, provider_id is MouserPartNumber (per MouserProvider.php)
|
||||
$best = $dtos[0] ?? null;
|
||||
|
||||
if ($best !== null) {
|
||||
return [
|
||||
'providerKey' => 'mouser',
|
||||
'providerId' => $best->provider_id,
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Digi-Key: can use customerPartNumber or supplierPartNumber directly
|
||||
if ($vendor === 'digikey') {
|
||||
return [
|
||||
'providerKey' => 'digikey',
|
||||
'providerId' => $scanResult->customerPartNumber ?? $scanResult->supplierPartNumber,
|
||||
];
|
||||
}
|
||||
|
||||
// Element14: can use supplierPartNumber directly
|
||||
if ($vendor === 'element14') {
|
||||
return [
|
||||
'providerKey' => 'element14',
|
||||
'providerId' => $scanResult->supplierPartNumber,
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -33,4 +33,4 @@ interface BarcodeScanResultInterface
|
|||
* @return array<string, string|int|float|null>
|
||||
*/
|
||||
public function getDecodedForInfoMode(): array;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,25 +26,28 @@ namespace App\Services\LabelSystem\BarcodeScanner;
|
|||
/**
|
||||
* This enum represents the different types, where a barcode/QR-code can be generated from
|
||||
*/
|
||||
enum BarcodeSourceType
|
||||
enum BarcodeSourceType: string
|
||||
{
|
||||
/** This Barcode was generated using Part-DB internal recommended barcode generator */
|
||||
case INTERNAL;
|
||||
case INTERNAL = 'internal';
|
||||
/** This barcode is containing an internal part number (IPN) */
|
||||
case IPN;
|
||||
case IPN = 'ipn';
|
||||
|
||||
/**
|
||||
* This barcode is a user defined barcode defined on a part lot
|
||||
*/
|
||||
case USER_DEFINED;
|
||||
case USER_DEFINED = 'user_defined';
|
||||
|
||||
/**
|
||||
* EIGP114 formatted barcodes like used by digikey, mouser, etc.
|
||||
*/
|
||||
case EIGP114;
|
||||
case EIGP114 = 'eigp114';
|
||||
|
||||
/**
|
||||
* GTIN /EAN barcodes, which are used on most products in the world. These are checked with the GTIN field of a part.
|
||||
*/
|
||||
case GTIN;
|
||||
case GTIN = 'gtin';
|
||||
|
||||
/** For LCSC.com formatted QR codes */
|
||||
case LCSC = 'lcsc';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,40 +28,40 @@ namespace App\Services\LabelSystem\BarcodeScanner;
|
|||
* Based on PR 811, EIGP 114.2018 (https://www.ecianow.org/assets/docs/GIPC/EIGP-114.2018%20ECIA%20Labeling%20Specification%20for%20Product%20and%20Shipment%20Identification%20in%20the%20Electronics%20Industry%20-%202D%20Barcode.pdf),
|
||||
* , https://forum.digikey.com/t/digikey-product-labels-decoding-digikey-barcodes/41097
|
||||
*/
|
||||
class EIGP114BarcodeScanResult implements BarcodeScanResultInterface
|
||||
readonly class EIGP114BarcodeScanResult implements BarcodeScanResultInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* @var string|null Ship date in format YYYYMMDD
|
||||
*/
|
||||
public readonly ?string $shipDate;
|
||||
public ?string $shipDate;
|
||||
|
||||
/**
|
||||
* @var string|null Customer assigned part number – Optional based on
|
||||
* agreements between Distributor and Supplier
|
||||
*/
|
||||
public readonly ?string $customerPartNumber;
|
||||
public ?string $customerPartNumber;
|
||||
|
||||
/**
|
||||
* @var string|null Supplier assigned part number
|
||||
*/
|
||||
public readonly ?string $supplierPartNumber;
|
||||
public ?string $supplierPartNumber;
|
||||
|
||||
/**
|
||||
* @var int|null Quantity of product
|
||||
*/
|
||||
public readonly ?int $quantity;
|
||||
public ?int $quantity;
|
||||
|
||||
/**
|
||||
* @var string|null Customer assigned purchase order number
|
||||
*/
|
||||
public readonly ?string $customerPO;
|
||||
public ?string $customerPO;
|
||||
|
||||
/**
|
||||
* @var string|null Line item number from PO. Required on Logistic Label when
|
||||
* used on back of Packing Slip. See Section 4.9
|
||||
*/
|
||||
public readonly ?string $customerPOLine;
|
||||
public ?string $customerPOLine;
|
||||
|
||||
/**
|
||||
* 9D - YYWW (Year and Week of Manufacture). ) If no date code is used
|
||||
|
|
@ -69,7 +69,7 @@ class EIGP114BarcodeScanResult implements BarcodeScanResultInterface
|
|||
* to indicate the product is Not Traceable by this data field.
|
||||
* @var string|null
|
||||
*/
|
||||
public readonly ?string $dateCode;
|
||||
public ?string $dateCode;
|
||||
|
||||
/**
|
||||
* 10D - YYWW (Year and Week of Manufacture). ) If no date code is used
|
||||
|
|
@ -77,7 +77,7 @@ class EIGP114BarcodeScanResult implements BarcodeScanResultInterface
|
|||
* to indicate the product is Not Traceable by this data field.
|
||||
* @var string|null
|
||||
*/
|
||||
public readonly ?string $alternativeDateCode;
|
||||
public ?string $alternativeDateCode;
|
||||
|
||||
/**
|
||||
* Traceability number assigned to a batch or group of items. If
|
||||
|
|
@ -86,14 +86,14 @@ class EIGP114BarcodeScanResult implements BarcodeScanResultInterface
|
|||
* by this data field.
|
||||
* @var string|null
|
||||
*/
|
||||
public readonly ?string $lotCode;
|
||||
public ?string $lotCode;
|
||||
|
||||
/**
|
||||
* Country where part was manufactured. Two-letter code from
|
||||
* ISO 3166 country code list
|
||||
* @var string|null
|
||||
*/
|
||||
public readonly ?string $countryOfOrigin;
|
||||
public ?string $countryOfOrigin;
|
||||
|
||||
/**
|
||||
* @var string|null Unique alphanumeric number assigned by supplier
|
||||
|
|
@ -101,85 +101,85 @@ class EIGP114BarcodeScanResult implements BarcodeScanResultInterface
|
|||
* Carton. Always used in conjunction with a mixed logistic label
|
||||
* with a 5S data identifier for Package ID.
|
||||
*/
|
||||
public readonly ?string $packageId1;
|
||||
public ?string $packageId1;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
* 4S - Package ID for Logistic Carton with like items
|
||||
*/
|
||||
public readonly ?string $packageId2;
|
||||
public ?string $packageId2;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
* 5S - Package ID for Logistic Carton with mixed items
|
||||
*/
|
||||
public readonly ?string $packageId3;
|
||||
public ?string $packageId3;
|
||||
|
||||
/**
|
||||
* @var string|null Unique alphanumeric number assigned by supplier.
|
||||
*/
|
||||
public readonly ?string $packingListNumber;
|
||||
public ?string $packingListNumber;
|
||||
|
||||
/**
|
||||
* @var string|null Ship date in format YYYYMMDD
|
||||
*/
|
||||
public readonly ?string $serialNumber;
|
||||
public ?string $serialNumber;
|
||||
|
||||
/**
|
||||
* @var string|null Code for sorting and classifying LEDs. Use when applicable
|
||||
*/
|
||||
public readonly ?string $binCode;
|
||||
public ?string $binCode;
|
||||
|
||||
/**
|
||||
* @var int|null Sequential carton count in format “#/#” or “# of #”
|
||||
*/
|
||||
public readonly ?int $packageCount;
|
||||
public ?int $packageCount;
|
||||
|
||||
/**
|
||||
* @var string|null Alphanumeric string assigned by the supplier to distinguish
|
||||
* from one closely-related design variation to another. Use as
|
||||
* required or when applicable
|
||||
*/
|
||||
public readonly ?string $revisionNumber;
|
||||
public ?string $revisionNumber;
|
||||
|
||||
/**
|
||||
* @var string|null Digikey Extension: This is not represented in the ECIA spec, but the field being used is found in the ANSI MH10.8.2-2016 spec on which the ECIA spec is based. In the ANSI spec it is called First Level (Supplier Assigned) Part Number.
|
||||
*/
|
||||
public readonly ?string $digikeyPartNumber;
|
||||
public ?string $digikeyPartNumber;
|
||||
|
||||
/**
|
||||
* @var string|null Digikey Extension: This can be shared across multiple invoices and time periods and is generated as an order enters our system from any vector (web, API, phone order, etc.)
|
||||
*/
|
||||
public readonly ?string $digikeySalesOrderNumber;
|
||||
public ?string $digikeySalesOrderNumber;
|
||||
|
||||
/**
|
||||
* @var string|null Digikey extension: This is typically assigned per shipment as items are being released to be picked in the warehouse. A SO can have many Invoice numbers
|
||||
*/
|
||||
public readonly ?string $digikeyInvoiceNumber;
|
||||
public ?string $digikeyInvoiceNumber;
|
||||
|
||||
/**
|
||||
* @var string|null Digikey extension: This is for internal DigiKey purposes and defines the label type.
|
||||
*/
|
||||
public readonly ?string $digikeyLabelType;
|
||||
public ?string $digikeyLabelType;
|
||||
|
||||
/**
|
||||
* @var string|null You will also see this as the last part of a URL for a product detail page. Ex https://www.digikey.com/en/products/detail/w%C3%BCrth-elektronik/860010672008/5726907
|
||||
*/
|
||||
public readonly ?string $digikeyPartID;
|
||||
public ?string $digikeyPartID;
|
||||
|
||||
/**
|
||||
* @var string|null Digikey Extension: For internal use of Digikey. Probably not needed
|
||||
*/
|
||||
public readonly ?string $digikeyNA;
|
||||
public ?string $digikeyNA;
|
||||
|
||||
/**
|
||||
* @var string|null Digikey Extension: This is a field of varying length used to keep the barcode approximately the same size between labels. It is safe to ignore.
|
||||
*/
|
||||
public readonly ?string $digikeyPadding;
|
||||
public ?string $digikeyPadding;
|
||||
|
||||
public readonly ?string $mouserPositionInOrder;
|
||||
public ?string $mouserPositionInOrder;
|
||||
|
||||
public readonly ?string $mouserManufacturer;
|
||||
public ?string $mouserManufacturer;
|
||||
|
||||
|
||||
|
||||
|
|
@ -187,7 +187,7 @@ class EIGP114BarcodeScanResult implements BarcodeScanResultInterface
|
|||
*
|
||||
* @param array<string, string> $data The fields of the EIGP114 barcode, where the key is the field name and the value is the field content
|
||||
*/
|
||||
public function __construct(public readonly array $data)
|
||||
public function __construct(public array $data)
|
||||
{
|
||||
//IDs per EIGP 114.2018
|
||||
$this->shipDate = $data['6D'] ?? null;
|
||||
|
|
@ -329,4 +329,4 @@ class EIGP114BarcodeScanResult implements BarcodeScanResultInterface
|
|||
|
||||
return $tmp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,157 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\LabelSystem\BarcodeScanner;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* This class represents the content of a lcsc.com barcode
|
||||
* Its data structure is represented by {pbn:...,on:...,pc:...,pm:...,qty:...}
|
||||
*/
|
||||
readonly class LCSCBarcodeScanResult implements BarcodeScanResultInterface
|
||||
{
|
||||
|
||||
/** @var string|null (pbn) */
|
||||
public ?string $pickBatchNumber;
|
||||
|
||||
/** @var string|null (on) */
|
||||
public ?string $orderNumber;
|
||||
|
||||
/** @var string|null LCSC Supplier part number (pc) */
|
||||
public ?string $lcscCode;
|
||||
|
||||
/** @var string|null (pm) */
|
||||
public ?string $mpn;
|
||||
|
||||
/** @var int|null (qty) */
|
||||
public ?int $quantity;
|
||||
|
||||
/** @var string|null Country Channel as raw value (CC) */
|
||||
public ?string $countryChannel;
|
||||
|
||||
/**
|
||||
* @var string|null Warehouse code as raw value (WC)
|
||||
*/
|
||||
public ?string $warehouseCode;
|
||||
|
||||
/**
|
||||
* @var string|null Unknown numeric code (pdi)
|
||||
*/
|
||||
public ?string $pdi;
|
||||
|
||||
/**
|
||||
* @var string|null Unknown value (hp)
|
||||
*/
|
||||
public ?string $hp;
|
||||
|
||||
/**
|
||||
* @param array<string, string> $fields
|
||||
*/
|
||||
public function __construct(
|
||||
public array $fields,
|
||||
public string $rawInput,
|
||||
) {
|
||||
|
||||
$this->pickBatchNumber = $this->fields['pbn'] ?? null;
|
||||
$this->orderNumber = $this->fields['on'] ?? null;
|
||||
$this->lcscCode = $this->fields['pc'] ?? null;
|
||||
$this->mpn = $this->fields['pm'] ?? null;
|
||||
$this->quantity = isset($this->fields['qty']) ? (int)$this->fields['qty'] : null;
|
||||
$this->countryChannel = $this->fields['cc'] ?? null;
|
||||
$this->warehouseCode = $this->fields['wc'] ?? null;
|
||||
$this->pdi = $this->fields['pdi'] ?? null;
|
||||
$this->hp = $this->fields['hp'] ?? null;
|
||||
|
||||
}
|
||||
|
||||
public function getSourceType(): BarcodeSourceType
|
||||
{
|
||||
return BarcodeSourceType::LCSC;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|float[]|int[]|null[]|string[] An array of fields decoded from the barcode
|
||||
*/
|
||||
public function getDecodedForInfoMode(): array
|
||||
{
|
||||
// Keep it human-friendly
|
||||
return [
|
||||
'Barcode type' => 'LCSC',
|
||||
'MPN (pm)' => $this->mpn ?? '',
|
||||
'LCSC code (pc)' => $this->lcscCode ?? '',
|
||||
'Qty' => $this->quantity !== null ? (string) $this->quantity : '',
|
||||
'Order No (on)' => $this->orderNumber ?? '',
|
||||
'Pick Batch (pbn)' => $this->pickBatchNumber ?? '',
|
||||
'Warehouse (wc)' => $this->warehouseCode ?? '',
|
||||
'Country/Channel (cc)' => $this->countryChannel ?? '',
|
||||
'PDI (unknown meaning)' => $this->pdi ?? '',
|
||||
'HP (unknown meaning)' => $this->hp ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the barcode data to see if the input matches the expected format used by lcsc.com
|
||||
* @param string $input
|
||||
* @return bool
|
||||
*/
|
||||
public static function isLCSCBarcode(string $input): bool
|
||||
{
|
||||
$s = trim($input);
|
||||
|
||||
// Your example: {pbn:...,on:...,pc:...,pm:...,qty:...}
|
||||
if (!str_starts_with($s, '{') || !str_ends_with($s, '}')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Must contain at least pm: and pc: (common for LCSC labels)
|
||||
return (stripos($s, 'pm:') !== false) && (stripos($s, 'pc:') !== false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the barcode input string into the fields used by lcsc.com
|
||||
* @param string $input
|
||||
* @return self
|
||||
*/
|
||||
public static function parse(string $input): self
|
||||
{
|
||||
$raw = trim($input);
|
||||
|
||||
if (!self::isLCSCBarcode($raw)) {
|
||||
throw new InvalidArgumentException('Not an LCSC barcode');
|
||||
}
|
||||
|
||||
$inner = substr($raw, 1, -1); // remove { }
|
||||
|
||||
$fields = [];
|
||||
|
||||
// This format is comma-separated pairs, values do not contain commas in your sample.
|
||||
$pairs = array_filter(
|
||||
array_map(trim(...), explode(',', $inner)),
|
||||
static fn(string $s): bool => $s !== ''
|
||||
);
|
||||
|
||||
foreach ($pairs as $pair) {
|
||||
$pos = strpos($pair, ':');
|
||||
if ($pos === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$k = trim(substr($pair, 0, $pos));
|
||||
$v = trim(substr($pair, $pos + 1));
|
||||
|
||||
if ($k === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$fields[$k] = $v;
|
||||
}
|
||||
|
||||
if (!isset($fields['pm']) || trim($fields['pm']) === '') {
|
||||
throw new InvalidArgumentException('LCSC barcode missing pm field');
|
||||
}
|
||||
|
||||
return new self($fields, $raw);
|
||||
}
|
||||
}
|
||||
|
|
@ -29,12 +29,12 @@ use App\Entity\LabelSystem\LabelSupportedElement;
|
|||
* This class represents the result of a barcode scan of a barcode that uniquely identifies a local entity,
|
||||
* like an internally generated barcode or a barcode that was added manually to the system by a user
|
||||
*/
|
||||
class LocalBarcodeScanResult implements BarcodeScanResultInterface
|
||||
readonly class LocalBarcodeScanResult implements BarcodeScanResultInterface
|
||||
{
|
||||
public function __construct(
|
||||
public readonly LabelSupportedElement $target_type,
|
||||
public readonly int $target_id,
|
||||
public readonly BarcodeSourceType $source_type,
|
||||
public LabelSupportedElement $target_type,
|
||||
public int $target_id,
|
||||
public BarcodeSourceType $source_type,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -46,4 +46,4 @@ class LocalBarcodeScanResult implements BarcodeScanResultInterface
|
|||
'Target ID' => $this->target_id,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,10 @@ declare(strict_types=1);
|
|||
namespace App\Twig;
|
||||
|
||||
use App\Entity\Attachments\Attachment;
|
||||
use App\Entity\Attachments\AttachmentContainingDBElement;
|
||||
use App\Entity\Parts\Part;
|
||||
use App\Services\Attachments\AttachmentURLGenerator;
|
||||
use App\Services\Attachments\PartPreviewGenerator;
|
||||
use App\Services\Misc\FAIconGenerator;
|
||||
use Twig\Attribute\AsTwigFunction;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
|
|
@ -31,7 +34,7 @@ use Twig\TwigFunction;
|
|||
|
||||
final readonly class AttachmentExtension
|
||||
{
|
||||
public function __construct(private AttachmentURLGenerator $attachmentURLGenerator, private FAIconGenerator $FAIconGenerator)
|
||||
public function __construct(private AttachmentURLGenerator $attachmentURLGenerator, private FAIconGenerator $FAIconGenerator, private PartPreviewGenerator $partPreviewGenerator)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -44,6 +47,26 @@ final readonly class AttachmentExtension
|
|||
return $this->attachmentURLGenerator->getThumbnailURL($attachment, $filter_name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URL of the thumbnail of the given element. Returns null if no thumbnail is available.
|
||||
* For parts, a special preview image is generated, for other entities, the master picture is used as preview (if available).
|
||||
*/
|
||||
#[AsTwigFunction("entity_thumbnail")]
|
||||
public function entityThumbnail(AttachmentContainingDBElement $element, string $filter_name = 'thumbnail_sm'): ?string
|
||||
{
|
||||
if ($element instanceof Part) {
|
||||
$preview_attachment = $this->partPreviewGenerator->getTablePreviewAttachment($element);
|
||||
} else { // For other entities, we just use the master picture as preview, if available
|
||||
$preview_attachment = $element->getMasterPictureAttachment();
|
||||
}
|
||||
|
||||
if ($preview_attachment === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->attachmentURLGenerator->getThumbnailURL($preview_attachment, $filter_name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the font-awesome icon type for the given file extension. Returns "file" if no specific icon is available.
|
||||
* Null is allowed for files withot extension
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@
|
|||
<li role="separator" class="dropdown-divider"></li>
|
||||
<h6 class="dropdown-header">{% trans %}user.language_select{% endtrans %}</h6>
|
||||
<div id="locale-select-menu">
|
||||
{# This menu is filled by 'turbo/locale_menu' controller from the _turbo_control.html.twig template, to always have the correct path #}
|
||||
{# This menu is filled by a turbo-stream in _turbo_contro.html.twig #}
|
||||
</div>
|
||||
</ul>
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -1,35 +1,39 @@
|
|||
{# Insert flashes #}
|
||||
<div class="toasts-global d-none">
|
||||
{% for label, messages in app.flashes() %}
|
||||
{% for message in messages %}
|
||||
{{ include('_toast.html.twig', {
|
||||
'label': label,
|
||||
'message': message
|
||||
}) }}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% block flashes %}
|
||||
{# Insert flashes #}
|
||||
<turbo-stream action="replace" action="morph" target="toast-container">
|
||||
<template>
|
||||
<div class="toast-container" id="toast-container">
|
||||
{% for label, messages in app.flashes() %}
|
||||
{% for message in messages %}
|
||||
{{ include('_toast.html.twig', {
|
||||
'label': label,
|
||||
'message': message
|
||||
}) }}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</template>
|
||||
</turbo-stream>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{# Allow pages to request a fully reload of everything #}
|
||||
{% if global_reload_needed is defined and global_reload_needed %}
|
||||
<div {{ stimulus_controller('turbo/global_reload') }}></div>
|
||||
{% endif %}
|
||||
|
||||
{# Insert info about when the sidebar trees were updated last time, so the sidebar_tree_controller can decide if it needs to reload the tree #}
|
||||
<span id="sidebar-last-time-updated" style="display: none;" data-last-update="{{ sidebar_tree_updater.lastTreeUpdate.format("Y-m-d\\TH:i:sP") }}"></span>
|
||||
|
||||
{# The title block is already escaped, therefore we dont require any additional escaping here #}
|
||||
<div class="d-none" data-title="{{ current_page_title|trim|raw }}" {{ stimulus_controller('turbo/title') }}></div>
|
||||
<turbo-stream action="update" target="locale-select-menu">
|
||||
<template>
|
||||
{% set locales = settings_instance('localization').languageMenuEntries %}
|
||||
{% if locales is empty %}
|
||||
{% set locales = locale_menu %}
|
||||
{% endif %}
|
||||
|
||||
{% for locale in locales %}
|
||||
<a class="dropdown-item" data-turbo="false" data-turbo-frame="_top" href="{{ path(app.request.attributes.get('_route'),
|
||||
app.request.query.all|merge(app.request.attributes.get('_route_params'))|merge({'_locale': locale})) }}">
|
||||
{{ locale|language_name }} ({{ locale|upper }})</a>
|
||||
{% endfor %}
|
||||
</template>
|
||||
</turbo-stream>
|
||||
|
||||
<div class="d-none" {{ stimulus_controller('turbo/locale_menu') }}>
|
||||
{% set locales = settings_instance('localization').languageMenuEntries %}
|
||||
{% if locales is empty %}
|
||||
{% set locales = locale_menu %}
|
||||
{% endif %}
|
||||
|
||||
{% for locale in locales %}
|
||||
<a class="dropdown-item" data-turbo="false" data-turbo-frame="_top" href="{{ path(app.request.attributes.get('_route'),
|
||||
app.request.query.all|merge(app.request.attributes.get('_route_params'))|merge({'_locale': locale})) }}">
|
||||
{{ locale|language_name }} ({{ locale|upper }})</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -26,6 +26,11 @@
|
|||
<meta name="turbo-refresh-method" content="morph">
|
||||
<meta name="turbo-refresh-scroll" content="preserve">
|
||||
|
||||
{# Allow pages to request a fully reload of everything #}
|
||||
{% if global_reload_needed is defined and global_reload_needed %}
|
||||
<meta name="turbo-visit-control" content="reload">
|
||||
{% endif %}
|
||||
|
||||
<link rel="shortcut icon" type="image/x-icon" href="{{ asset('favicon.ico') }}">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="{{ asset('icon/apple-touch-icon.png') }}">
|
||||
<link rel="icon" type="image/png" href="{{ asset('icon/favicon-32x32.png') }}" sizes="32x32">
|
||||
|
|
|
|||
154
templates/label_system/scanner/_info_mode.html.twig
Normal file
154
templates/label_system/scanner/_info_mode.html.twig
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
{% import "helper.twig" as helper %}
|
||||
|
||||
{% if decoded is not empty %}
|
||||
<hr>
|
||||
|
||||
{% if part %} {# Show detailed info when it is a part #}
|
||||
<div class="card border-success">
|
||||
<h5 class="card-header text-bg-success">
|
||||
<small>{% trans %}label_scanner.db_part_found{% endtrans %}</small>
|
||||
{% if openUrl %}
|
||||
<div class="btn-group float-end">
|
||||
<a href="{{ openUrl }}" target="_blank" class="btn btn-sm btn-outline-light"
|
||||
title="{% trans %}label_scanner.open{% endtrans %}">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</h5>
|
||||
<div class="card-body row">
|
||||
<div class="col-sm-2">
|
||||
<img class="d-block w-100 img-fluid img-thumbnail bg-light part-info-image"
|
||||
src="{{ entity_thumbnail(part) ?? asset('img/part_placeholder.svg') }}" alt="">
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-sm-10">
|
||||
<h4 class="card-title mb-0">{{ part.name }}</h4>
|
||||
<div class="card-text text-muted">{{ part.description | format_markdown(true) }}</div>
|
||||
<div>
|
||||
<dt class="d-inline-block">
|
||||
<span class="visually-hidden">{% trans %}category.label{% endtrans %}</span>
|
||||
<i class="fas fa-tag fa-fw" title="{% trans %}category.label{% endtrans %}"></i>
|
||||
</dt>
|
||||
<dd class="d-inline">
|
||||
<span class="text-muted">{{ helper.structural_entity_link(part.category) }}</span>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt class="d-inline-block">
|
||||
<span class="visually-hidden">{% trans %}footprint.label{% endtrans %}</span>
|
||||
<i class="fas fa-microchip fa-fw" title="{% trans %}footprint.label{% endtrans %}"></i>
|
||||
</dt>
|
||||
<dd class="d-inline">
|
||||
<span class="text-muted">{{ helper.structural_entity_link(part.footprint) }}</span>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
{# Show part lots / locations #}
|
||||
{% if part.partLots is not empty %}
|
||||
<table class="table table-sm table-striped mb-2 w-auto">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{% trans %}part_lots.storage_location{% endtrans %}</th>
|
||||
<th scope="col" class="text-end" style="width: 6rem;">
|
||||
{% trans %}part_lots.amount{% endtrans %}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for lot in part.partLots %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if lot.storageLocation %}
|
||||
{{ helper.structural_entity_link(lot.storageLocation) }}
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-end" style="width: 6rem;">
|
||||
{% if lot.instockUnknown %}
|
||||
<span class="text-muted">?</span>
|
||||
{% else %}
|
||||
{{ lot.amount | format_amount(part.partUnit, {'decimals': 5}) }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="text-muted">{% trans %}label_scanner.no_locations{% endtrans %}</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% elseif entity %} {# If we have an entity but that is not an part #}
|
||||
|
||||
<div class="card border-success">
|
||||
<h5 class="card-header text-bg-success">
|
||||
<small>{% trans %}label_scanner.target_found{% endtrans %}: {{ type_label(entity) }}</small>
|
||||
{% if openUrl %}
|
||||
<div class="btn-group float-end">
|
||||
<a href="{{ openUrl }}" target="_blank" class="btn btn-sm btn-outline-light"
|
||||
title="{% trans %}label_scanner.open{% endtrans %}">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</h5>
|
||||
<div class="card-body row">
|
||||
<div class="col-sm-2">
|
||||
<img class="d-block w-100 img-fluid img-thumbnail bg-light part-info-image"
|
||||
src="{{ entity_thumbnail(entity) ?? asset('img/part_placeholder.svg') }}" alt="">
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-sm-10">
|
||||
<h4 class="card-title mb-0">{{ entity.name }}</h4>
|
||||
<p>{% trans %}id.label{% endtrans %}: {{ entity.id }} ({{ type_label(entity) }})</p>
|
||||
|
||||
{% if entity.fullPath is defined %}
|
||||
{{ helper.breadcrumb_entity_link(entity)}}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if createUrl %}
|
||||
<div class="alert alert-info mb-2">
|
||||
<h4 class="alert-heading mb-0">{% trans %}label_scanner.part_can_be_created{% endtrans %}</h4>
|
||||
<p class="text-muted mb-0"><small >{% trans %}label_scanner.part_can_be_created.help{% endtrans %}</small></p>
|
||||
<hr>
|
||||
<a class="btn btn-outline-success" href="{{ createUrl }}" target="_blank"><i class="fas fa-plus-square"></i> {% trans %}label_scanner.part_create_btn{% endtrans %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<h4 class="mt-2">
|
||||
{% trans %}label_scanner.scan_result.title{% endtrans %}
|
||||
</h4>
|
||||
|
||||
{# Decoded barcode fields #}
|
||||
<table class="table table-striped table-hover table-bordered table-sm">
|
||||
<tbody>
|
||||
{% for key, value in decoded %}
|
||||
<tr>
|
||||
<th class="text-nowrap">{{ key }}</th>
|
||||
<td><code>{{ value }}</code></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{# Whitespace under table and Input form fields #}
|
||||
<hr>
|
||||
|
||||
{% endif %}
|
||||
|
|
@ -10,35 +10,28 @@
|
|||
<div class="">
|
||||
<div class="form-group row">
|
||||
<div class="offset-sm-3 col-sm-9">
|
||||
|
||||
<div class="img-thumbnail" style="max-width: 600px;">
|
||||
<div id="reader-box" {{ stimulus_controller('pages/barcode_scan') }}></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="scan-augmented-result" class="mt-3">
|
||||
{% include "label_system/scanner/_info_mode.html.twig" %}
|
||||
</div>
|
||||
|
||||
{{ form_start(form, {'attr': {'id': 'scan_dialog_form'}}) }}
|
||||
|
||||
{{ form_end(form) }}
|
||||
</div>
|
||||
|
||||
{{ form_start(form, {'attr': {'id': 'scan_dialog_form'}}) }}
|
||||
|
||||
{{ form_end(form) }}
|
||||
|
||||
|
||||
{% if infoModeData %}
|
||||
<hr>
|
||||
<h4>{% trans %}label_scanner.decoded_info.title{% endtrans %}</h4>
|
||||
|
||||
<table class="table table-striped table-hover table-bordered table-sm">
|
||||
<tbody>
|
||||
{% for key, value in infoModeData %}
|
||||
<tr>
|
||||
<td>{{ key }}</td>
|
||||
<td><code>{{ value }}</code></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scan_results %}
|
||||
<turbo-stream action="replace" action="morph" target="scan-augmented-result">
|
||||
<template>
|
||||
<div id="scan-augmented-result" class="mt-3">
|
||||
{% include "label_system/scanner/_info_mode.html.twig" %}
|
||||
</div>
|
||||
</template>
|
||||
</turbo-stream>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,85 +0,0 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Services\LabelSystem\BarcodeScanner;
|
||||
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\Group;
|
||||
use App\Entity\LabelSystem\LabelSupportedElement;
|
||||
use App\Services\LabelSystem\BarcodeScanner\BarcodeRedirector;
|
||||
use App\Services\LabelSystem\BarcodeScanner\BarcodeSourceType;
|
||||
use App\Services\LabelSystem\BarcodeScanner\LocalBarcodeScanResult;
|
||||
use Doctrine\ORM\EntityNotFoundException;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
|
||||
final class BarcodeRedirectorTest extends KernelTestCase
|
||||
{
|
||||
private ?BarcodeRedirector $service = null;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
$this->service = self::getContainer()->get(BarcodeRedirector::class);
|
||||
}
|
||||
|
||||
public static function urlDataProvider(): \Iterator
|
||||
{
|
||||
yield [new LocalBarcodeScanResult(LabelSupportedElement::PART, 1, BarcodeSourceType::INTERNAL), '/en/part/1'];
|
||||
//Part lot redirects to Part info page (Part lot 1 is associated with part 3)
|
||||
yield [new LocalBarcodeScanResult(LabelSupportedElement::PART_LOT, 1, BarcodeSourceType::INTERNAL), '/en/part/3?highlightLot=1'];
|
||||
yield [new LocalBarcodeScanResult(LabelSupportedElement::STORELOCATION, 1, BarcodeSourceType::INTERNAL), '/en/store_location/1/parts'];
|
||||
}
|
||||
|
||||
#[DataProvider('urlDataProvider')]
|
||||
#[Group('DB')]
|
||||
public function testGetRedirectURL(LocalBarcodeScanResult $scanResult, string $url): void
|
||||
{
|
||||
$this->assertSame($url, $this->service->getRedirectURL($scanResult));
|
||||
}
|
||||
|
||||
public function testGetRedirectEntityNotFount(): void
|
||||
{
|
||||
$this->expectException(EntityNotFoundException::class);
|
||||
//If we encounter an invalid lot, we must throw an exception
|
||||
$this->service->getRedirectURL(new LocalBarcodeScanResult(LabelSupportedElement::PART_LOT,
|
||||
12_345_678, BarcodeSourceType::INTERNAL));
|
||||
}
|
||||
}
|
||||
|
|
@ -49,6 +49,7 @@ use App\Services\LabelSystem\BarcodeScanner\BarcodeSourceType;
|
|||
use App\Services\LabelSystem\BarcodeScanner\EIGP114BarcodeScanResult;
|
||||
use App\Services\LabelSystem\BarcodeScanner\LocalBarcodeScanResult;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
use App\Services\LabelSystem\BarcodeScanner\LCSCBarcodeScanResult;
|
||||
|
||||
final class BarcodeScanHelperTest extends WebTestCase
|
||||
{
|
||||
|
|
@ -124,6 +125,14 @@ final class BarcodeScanHelperTest extends WebTestCase
|
|||
]);
|
||||
|
||||
yield [$eigp114Result, "[)>\x1E06\x1DP596-777A1-ND\x1D1PXAF4444\x1DQ3\x1D10D1452\x1D1TBF1103\x1D4LUS\x1E\x04"];
|
||||
|
||||
$lcscInput = '{pc:C138033,pm:RC0402FR-071ML,qty:10}';
|
||||
$lcscResult = new LCSCBarcodeScanResult(
|
||||
['pc' => 'C138033', 'pm' => 'RC0402FR-071ML', 'qty' => '10'],
|
||||
$lcscInput
|
||||
);
|
||||
|
||||
yield [$lcscResult, $lcscInput];
|
||||
}
|
||||
|
||||
public static function invalidDataProvider(): \Iterator
|
||||
|
|
@ -153,4 +162,33 @@ final class BarcodeScanHelperTest extends WebTestCase
|
|||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->service->scanBarcodeContent($input);
|
||||
}
|
||||
|
||||
public function testAutoDetectLcscBarcode(): void
|
||||
{
|
||||
$input = '{pbn:PB1,on:ON1,pc:C138033,pm:RC0402FR-071ML,qty:10}';
|
||||
|
||||
$result = $this->service->scanBarcodeContent($input);
|
||||
|
||||
$this->assertInstanceOf(LCSCBarcodeScanResult::class, $result);
|
||||
$this->assertSame('C138033', $result->lcscCode);
|
||||
$this->assertSame('RC0402FR-071ML', $result->mpn);
|
||||
}
|
||||
|
||||
public function testLcscExplicitTypeParses(): void
|
||||
{
|
||||
$input = '{pc:C138033,pm:RC0402FR-071ML,qty:10}';
|
||||
|
||||
$result = $this->service->scanBarcodeContent($input, BarcodeSourceType::LCSC);
|
||||
|
||||
$this->assertInstanceOf(LCSCBarcodeScanResult::class, $result);
|
||||
$this->assertSame('C138033', $result->lcscCode);
|
||||
$this->assertSame('RC0402FR-071ML', $result->mpn);
|
||||
}
|
||||
|
||||
public function testLcscExplicitTypeRejectsNonLcsc(): void
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
|
||||
$this->service->scanBarcodeContent('not-an-lcsc', BarcodeSourceType::LCSC);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,183 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Services\LabelSystem\BarcodeScanner;
|
||||
|
||||
use App\Entity\Parts\Part;
|
||||
use App\Entity\Parts\PartLot;
|
||||
use App\Entity\Parts\StorageLocation;
|
||||
use App\Services\LabelSystem\BarcodeScanner\BarcodeScanResultHandler;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\Group;
|
||||
use App\Entity\LabelSystem\LabelSupportedElement;
|
||||
use App\Services\LabelSystem\BarcodeScanner\BarcodeSourceType;
|
||||
use App\Services\LabelSystem\BarcodeScanner\LocalBarcodeScanResult;
|
||||
use Doctrine\ORM\EntityNotFoundException;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use App\Services\LabelSystem\BarcodeScanner\EIGP114BarcodeScanResult;
|
||||
use App\Services\LabelSystem\BarcodeScanner\LCSCBarcodeScanResult;
|
||||
use App\Services\LabelSystem\BarcodeScanner\BarcodeScanResultInterface;
|
||||
use InvalidArgumentException;
|
||||
|
||||
|
||||
final class BarcodeScanResultHandlerTest extends KernelTestCase
|
||||
{
|
||||
private ?BarcodeScanResultHandler $service = null;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
$this->service = self::getContainer()->get(BarcodeScanResultHandler::class);
|
||||
}
|
||||
|
||||
public static function urlDataProvider(): \Iterator
|
||||
{
|
||||
yield [new LocalBarcodeScanResult(LabelSupportedElement::PART, 1, BarcodeSourceType::INTERNAL), '/en/part/1'];
|
||||
//Part lot redirects to Part info page (Part lot 1 is associated with part 3)
|
||||
yield [new LocalBarcodeScanResult(LabelSupportedElement::PART_LOT, 1, BarcodeSourceType::INTERNAL), '/en/part/3?highlightLot=1'];
|
||||
yield [new LocalBarcodeScanResult(LabelSupportedElement::STORELOCATION, 1, BarcodeSourceType::INTERNAL), '/en/store_location/1/parts'];
|
||||
}
|
||||
|
||||
#[DataProvider('urlDataProvider')]
|
||||
#[Group('DB')]
|
||||
public function testGetRedirectURL(LocalBarcodeScanResult $scanResult, string $url): void
|
||||
{
|
||||
$this->assertSame($url, $this->service->getInfoURL($scanResult));
|
||||
}
|
||||
|
||||
public function testGetRedirectEntityNotFound(): void
|
||||
{
|
||||
//If we encounter an invalid lot, we must get an null result
|
||||
$url = $this->service->getInfoURL(new LocalBarcodeScanResult(LabelSupportedElement::PART_LOT,
|
||||
12_345_678, BarcodeSourceType::INTERNAL));
|
||||
|
||||
$this->assertNull($url);
|
||||
}
|
||||
|
||||
public function testGetRedirectURLThrowsOnUnknownScanType(): void
|
||||
{
|
||||
$unknown = new class implements BarcodeScanResultInterface {
|
||||
public function getDecodedForInfoMode(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$this->service->getInfoURL($unknown);
|
||||
}
|
||||
|
||||
public function testEIGPBarcodeResolvePartOrNullReturnsNullWhenNotFound(): void
|
||||
{
|
||||
$scan = new EIGP114BarcodeScanResult([]);
|
||||
|
||||
$this->assertNull($this->service->resolvePart($scan));
|
||||
$this->assertNull($this->service->getInfoURL($scan));
|
||||
}
|
||||
|
||||
public function testLCSCBarcodeResolvePartOrNullReturnsNullWhenNotFound(): void
|
||||
{
|
||||
$scan = new LCSCBarcodeScanResult(
|
||||
fields: ['pc' => 'C0000000', 'pm' => ''],
|
||||
rawInput: '{pc:C0000000,pm:}'
|
||||
);
|
||||
|
||||
$this->assertNull($this->service->resolvePart($scan));
|
||||
$this->assertNull($this->service->getInfoURL($scan));
|
||||
}
|
||||
|
||||
public function testResolveEntityThrowsOnUnknownScanType(): void
|
||||
{
|
||||
$unknown = new class implements BarcodeScanResultInterface {
|
||||
public function getDecodedForInfoMode(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$this->service->resolvePart($unknown);
|
||||
}
|
||||
|
||||
public function testResolveEntity(): void
|
||||
{
|
||||
$scan = new LocalBarcodeScanResult(LabelSupportedElement::PART, 1, BarcodeSourceType::INTERNAL);
|
||||
$part = $this->service->resolveEntity($scan);
|
||||
|
||||
$this->assertSame(1, $part->getId());
|
||||
$this->assertInstanceOf(Part::class, $part);
|
||||
|
||||
$scan = new LocalBarcodeScanResult(LabelSupportedElement::PART_LOT, 1, BarcodeSourceType::INTERNAL);
|
||||
$entity = $this->service->resolveEntity($scan);
|
||||
$this->assertSame(1, $entity->getId());
|
||||
$this->assertInstanceOf(PartLot::class, $entity);
|
||||
|
||||
$scan = new LocalBarcodeScanResult(LabelSupportedElement::STORELOCATION, 1, BarcodeSourceType::INTERNAL);
|
||||
$entity = $this->service->resolveEntity($scan);
|
||||
$this->assertSame(1, $entity->getId());
|
||||
$this->assertInstanceOf(StorageLocation::class, $entity);
|
||||
}
|
||||
|
||||
public function testResolvePart(): void
|
||||
{
|
||||
$scan = new LocalBarcodeScanResult(LabelSupportedElement::PART, 1, BarcodeSourceType::INTERNAL);
|
||||
$part = $this->service->resolvePart($scan);
|
||||
|
||||
$this->assertSame(1, $part->getId());
|
||||
|
||||
$scan = new LocalBarcodeScanResult(LabelSupportedElement::PART_LOT, 1, BarcodeSourceType::INTERNAL);
|
||||
$part = $this->service->resolvePart($scan);
|
||||
$this->assertSame(3, $part->getId());
|
||||
|
||||
$scan = new LocalBarcodeScanResult(LabelSupportedElement::STORELOCATION, 1, BarcodeSourceType::INTERNAL);
|
||||
$part = $this->service->resolvePart($scan);
|
||||
$this->assertNull($part); //Store location does not resolve to a part
|
||||
}
|
||||
|
||||
public function testGetCreateInfos(): void
|
||||
{
|
||||
$lcscScan = LCSCBarcodeScanResult::parse('{pbn:PB1,on:ON1,pc:C138033,pm:RC0402FR-071ML,qty:10}');
|
||||
$infos = $this->service->getCreateInfos($lcscScan);
|
||||
|
||||
$this->assertSame('lcsc', $infos['providerKey']);
|
||||
$this->assertSame('C138033', $infos['providerId']);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
<?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/>.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Services\LabelSystem\BarcodeScanner;
|
||||
|
||||
use App\Services\LabelSystem\BarcodeScanner\LCSCBarcodeScanResult;
|
||||
use InvalidArgumentException;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class LCSCBarcodeScanResultTest extends TestCase
|
||||
{
|
||||
public function testIsLCSCBarcode(): void
|
||||
{
|
||||
$this->assertFalse(LCSCBarcodeScanResult::isLCSCBarcode('invalid'));
|
||||
$this->assertFalse(LCSCBarcodeScanResult::isLCSCBarcode('LCSC-12345'));
|
||||
$this->assertFalse(LCSCBarcodeScanResult::isLCSCBarcode(''));
|
||||
|
||||
$this->assertTrue(LCSCBarcodeScanResult::isLCSCBarcode('{pbn:PB1,on:ON1,pc:C138033,pm:RC0402FR-071ML,qty:10}'));
|
||||
$this->assertTrue(LCSCBarcodeScanResult::isLCSCBarcode('{pbn:PICK2506270148,on:GB2506270877,pc:C22437266,pm:IA0509S-2W,qty:3,mc:,cc:1,pdi:164234874,hp:null,wc:ZH}'));
|
||||
}
|
||||
|
||||
public function testConstruct(): void
|
||||
{
|
||||
$raw = '{pbn:PB1,on:ON1,pc:C138033,pm:RC0402FR-071ML,qty:10}';
|
||||
$fields = ['pbn' => 'PB1', 'on' => 'ON1', 'pc' => 'C138033', 'pm' => 'RC0402FR-071ML', 'qty' => '10'];
|
||||
$scan = new LCSCBarcodeScanResult($fields, $raw);
|
||||
//Splitting up should work and assign the correct values to the properties:
|
||||
$this->assertSame('RC0402FR-071ML', $scan->mpn);
|
||||
$this->assertSame('C138033', $scan->lcscCode);
|
||||
|
||||
//Fields and raw input should be preserved
|
||||
$this->assertSame($fields, $scan->fields);
|
||||
$this->assertSame($raw, $scan->rawInput);
|
||||
}
|
||||
|
||||
public function testLCSCParseInvalidFormatThrows(): void
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
LCSCBarcodeScanResult::parse('not-an-lcsc-barcode');
|
||||
}
|
||||
|
||||
public function testParse(): void
|
||||
{
|
||||
$scan = LCSCBarcodeScanResult::parse('{pbn:PICK2506270148,on:GB2506270877,pc:C22437266,pm:IA0509S-2W,qty:3,mc:,cc:1,pdi:164234874,hp:null,wc:ZH}');
|
||||
|
||||
$this->assertSame('IA0509S-2W', $scan->mpn);
|
||||
$this->assertSame('C22437266', $scan->lcscCode);
|
||||
$this->assertSame('PICK2506270148', $scan->pickBatchNumber);
|
||||
$this->assertSame('GB2506270877', $scan->orderNumber);
|
||||
$this->assertSame(3, $scan->quantity);
|
||||
$this->assertSame('1', $scan->countryChannel);
|
||||
$this->assertSame('164234874', $scan->pdi);
|
||||
$this->assertSame('null', $scan->hp);
|
||||
$this->assertSame('ZH', $scan->warehouseCode);
|
||||
}
|
||||
|
||||
public function testLCSCParseExtractsFields(): void
|
||||
{
|
||||
$scan = LCSCBarcodeScanResult::parse('{pbn:PB1,on:ON1,pc:C138033,pm:RC0402FR-071ML,qty:10}');
|
||||
|
||||
$this->assertSame('RC0402FR-071ML', $scan->mpn);
|
||||
$this->assertSame('C138033', $scan->lcscCode);
|
||||
|
||||
$decoded = $scan->getDecodedForInfoMode();
|
||||
$this->assertSame('LCSC', $decoded['Barcode type']);
|
||||
$this->assertSame('RC0402FR-071ML', $decoded['MPN (pm)']);
|
||||
$this->assertSame('C138033', $decoded['LCSC code (pc)']);
|
||||
}
|
||||
}
|
||||
|
|
@ -9500,6 +9500,12 @@ Please note, that you can not impersonate a disabled user. If you try you will g
|
|||
<target>EIGP 114 barcode (e.g. the datamatrix codes on digikey and mouser orders)</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="BnqcKWx" name="scan_dialog.mode.lcsc">
|
||||
<segment>
|
||||
<source>scan_dialog.mode.lcsc</source>
|
||||
<target>LCSC.com barcode</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="QSMS_Bd" name="scan_dialog.info_mode">
|
||||
<segment state="translated">
|
||||
<source>scan_dialog.info_mode</source>
|
||||
|
|
@ -9512,6 +9518,24 @@ Please note, that you can not impersonate a disabled user. If you try you will g
|
|||
<target>Decoded information</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="kQnodbA" name="label_scanner.target_found">
|
||||
<segment>
|
||||
<source>label_scanner.target_found</source>
|
||||
<target>Item found in database</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="7Arfw2q" name="label_scanner.scan_result.title">
|
||||
<segment>
|
||||
<source>label_scanner.scan_result.title</source>
|
||||
<target>Scan result</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="PTh4EK_" name="label_scanner.no_locations">
|
||||
<segment>
|
||||
<source>label_scanner.no_locations</source>
|
||||
<target>Part is not stored at any location.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="nmXQWcS" name="label_generator.edit_profiles">
|
||||
<segment state="translated">
|
||||
<source>label_generator.edit_profiles</source>
|
||||
|
|
@ -12509,5 +12533,35 @@ Buerklin-API Authentication server:
|
|||
<target>Last stocktake</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="aEgd0if" name="label_scanner.open">
|
||||
<segment>
|
||||
<source>label_scanner.open</source>
|
||||
<target>View details</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="vw_0Qws" name="label_scanner.db_part_found">
|
||||
<segment>
|
||||
<source>label_scanner.db_part_found</source>
|
||||
<target>Database [part] found for barcode</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="zntajcd" name="label_scanner.part_can_be_created">
|
||||
<segment>
|
||||
<source>label_scanner.part_can_be_created</source>
|
||||
<target>[Part] can be created</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="cLTbd9w" name="label_scanner.part_can_be_created.help">
|
||||
<segment>
|
||||
<source>label_scanner.part_can_be_created.help</source>
|
||||
<target>No matching [part] was found in the database, but you can create a new [part] based of this barcode.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="FfHA3Yf" name="label_scanner.part_create_btn">
|
||||
<segment>
|
||||
<source>label_scanner.part_create_btn</source>
|
||||
<target>Create [part] from barcode</target>
|
||||
</segment>
|
||||
</unit>
|
||||
</file>
|
||||
</xliff>
|
||||
|
|
|
|||
194
yarn.lock
194
yarn.lock
|
|
@ -2,58 +2,58 @@
|
|||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@algolia/autocomplete-core@1.19.5":
|
||||
version "1.19.5"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/autocomplete-core/-/autocomplete-core-1.19.5.tgz#52d99aafce19493161220e417071f0222eeea7d6"
|
||||
integrity sha512-/kAE3mMBage/9m0OGnKQteSa7/eIfvhiKx28OWj857+dJ6qYepEBuw5L8its2oTX8ZNM/6TA3fo49kMwgcwjlg==
|
||||
"@algolia/autocomplete-core@1.19.6":
|
||||
version "1.19.6"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/autocomplete-core/-/autocomplete-core-1.19.6.tgz#472ba8f84d3bd1d253d24759caeaac454db902e7"
|
||||
integrity sha512-6EoD7PeM2WBq5GY1jm0gGonDW2JVU4BaHT9tAwDcaPkc6gYIRZeY7X7aFuwdRvk9R/jwsh8sz4flDao0+Kua6g==
|
||||
dependencies:
|
||||
"@algolia/autocomplete-plugin-algolia-insights" "1.19.5"
|
||||
"@algolia/autocomplete-shared" "1.19.5"
|
||||
"@algolia/autocomplete-plugin-algolia-insights" "1.19.6"
|
||||
"@algolia/autocomplete-shared" "1.19.6"
|
||||
|
||||
"@algolia/autocomplete-js@1.19.5", "@algolia/autocomplete-js@^1.17.0":
|
||||
version "1.19.5"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/autocomplete-js/-/autocomplete-js-1.19.5.tgz#2ec3efd9d5efd505ea677775d0199e1207e4624e"
|
||||
integrity sha512-C2/bEQeqq4nZ4PH2rySRvU9B224KbiCXAPZIn3pmMII/7BiXkppPQyDd+Fdly3ubOmnGFDH6BTzGHamySeOYeg==
|
||||
"@algolia/autocomplete-js@1.19.6", "@algolia/autocomplete-js@^1.17.0":
|
||||
version "1.19.6"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/autocomplete-js/-/autocomplete-js-1.19.6.tgz#a81b3b40e7e6356f22af75bfc1c92116d4a86243"
|
||||
integrity sha512-rHYKT6P+2FZ1+7a1/JtWIuCmfioOt5eXsAcri6XTYsSutl3BIh8s2e98kbvjbhLfwEuuVDWtST1hdAY2pQdrKw==
|
||||
dependencies:
|
||||
"@algolia/autocomplete-core" "1.19.5"
|
||||
"@algolia/autocomplete-preset-algolia" "1.19.5"
|
||||
"@algolia/autocomplete-shared" "1.19.5"
|
||||
"@algolia/autocomplete-core" "1.19.6"
|
||||
"@algolia/autocomplete-preset-algolia" "1.19.6"
|
||||
"@algolia/autocomplete-shared" "1.19.6"
|
||||
htm "^3.1.1"
|
||||
preact "^10.13.2"
|
||||
|
||||
"@algolia/autocomplete-plugin-algolia-insights@1.19.5":
|
||||
version "1.19.5"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.19.5.tgz#05246356fe9837475b08664ff4d6f55960127edc"
|
||||
integrity sha512-5zbetV9h2VxH+Mxx27I7BH2EIACVRUBE1FNykBK+2c2M+mhXYMY4npHbbGYj6QDEw3VVvH2UxAnghFpCtC6B/w==
|
||||
"@algolia/autocomplete-plugin-algolia-insights@1.19.6":
|
||||
version "1.19.6"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.19.6.tgz#7db79ca4a107059477b56e31e8f7760513f265a2"
|
||||
integrity sha512-VD53DBixhEwDvOB00D03DtBVhh5crgb1N0oH3QTscfYk4TpBH+CKrwmN/XrN/VdJAdP+4K6SgwLii/3OwM9dHw==
|
||||
dependencies:
|
||||
"@algolia/autocomplete-shared" "1.19.5"
|
||||
"@algolia/autocomplete-shared" "1.19.6"
|
||||
|
||||
"@algolia/autocomplete-plugin-recent-searches@^1.17.0":
|
||||
version "1.19.5"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/autocomplete-plugin-recent-searches/-/autocomplete-plugin-recent-searches-1.19.5.tgz#afd80f8abb281c4c01817a1edfde9a8aa95ed5db"
|
||||
integrity sha512-lOEliMbohq0BsZJ7JXFHlfmGBNtuCsQW0PLq8m6X1SdMD4XAn8fFxiOO2Nk1A/IiymZcOoHQV71u6f14wiohDw==
|
||||
version "1.19.6"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/autocomplete-plugin-recent-searches/-/autocomplete-plugin-recent-searches-1.19.6.tgz#13b6617f03bfc8257d947a0d6cf0435de5677847"
|
||||
integrity sha512-HQdSxHXFlxPUx6okxYWrrSbVD2o3OrDstU/E83Qvdl3Pwya3eZKrjhBb84i3Tqkm71wuABRYmCMNjc/qGFX4hw==
|
||||
dependencies:
|
||||
"@algolia/autocomplete-core" "1.19.5"
|
||||
"@algolia/autocomplete-js" "1.19.5"
|
||||
"@algolia/autocomplete-preset-algolia" "1.19.5"
|
||||
"@algolia/autocomplete-shared" "1.19.5"
|
||||
"@algolia/autocomplete-core" "1.19.6"
|
||||
"@algolia/autocomplete-js" "1.19.6"
|
||||
"@algolia/autocomplete-preset-algolia" "1.19.6"
|
||||
"@algolia/autocomplete-shared" "1.19.6"
|
||||
|
||||
"@algolia/autocomplete-preset-algolia@1.19.5":
|
||||
version "1.19.5"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.19.5.tgz#a9d5756090314c16b8895fa0c74ffccca7f8a1e2"
|
||||
integrity sha512-afdgxUyBxgX1I34THLScCyC+ld2h8wnCTv7JndRxsRNIJjJpFtRNpnYDq0+HVcp+LYeNd1zksDu7CpltTSEsvA==
|
||||
"@algolia/autocomplete-preset-algolia@1.19.6":
|
||||
version "1.19.6"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.19.6.tgz#bf800e3e0e3f69f661476d9d1a3237b122e84aa5"
|
||||
integrity sha512-/uQlHGK5Q2x5Nvrp3W7JMg4YNGG/ygkHtQLTltDbkpd45wnhV9jUiQA6aCnBed9cq0BXhOJZRxh1zGVZ3yRhBg==
|
||||
dependencies:
|
||||
"@algolia/autocomplete-shared" "1.19.5"
|
||||
"@algolia/autocomplete-shared" "1.19.6"
|
||||
|
||||
"@algolia/autocomplete-shared@1.19.5":
|
||||
version "1.19.5"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/autocomplete-shared/-/autocomplete-shared-1.19.5.tgz#1a20f60fd400fd5641718358a2d5c3eb1893cf9c"
|
||||
integrity sha512-yblBczNXtm2cCVzX4UAY3KkjdefmZPn1gWbIi8Q7qfBw7FjcKq2EjEl/65x4kU9nUc/ZkB5SeUf/bkqLEnA5gA==
|
||||
"@algolia/autocomplete-shared@1.19.6":
|
||||
version "1.19.6"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/autocomplete-shared/-/autocomplete-shared-1.19.6.tgz#5261f04a1cadf82138b6feb5a6df383106f50d60"
|
||||
integrity sha512-DG1n2B6XQw6DWB5veO4RuzQ/N2oGNpG+sSzGT7gUbi7WhF+jN57abcv2QhB5flXZ0NgddE1i6h7dZuQmYBEorQ==
|
||||
|
||||
"@algolia/autocomplete-theme-classic@^1.17.0":
|
||||
version "1.19.5"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/autocomplete-theme-classic/-/autocomplete-theme-classic-1.19.5.tgz#7b0d3ac11f2dca33600fce9ac383056ab4202cdc"
|
||||
integrity sha512-LjjhOmDbEXmV2IqaA7Xe8jh6lSpG087yC79ffLpXMKJOib4xSHFvPavsXC8NW25pWVHJFoAfplAAmxmeM2/jhw==
|
||||
version "1.19.6"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/autocomplete-theme-classic/-/autocomplete-theme-classic-1.19.6.tgz#ba1c9760ac725283d086a9affd784823fdb72c71"
|
||||
integrity sha512-lJg8fGK7ucuapoCwFqciTAvAOb7lI/BgWXN0VP+nW/oG0xtig6FvJz/XXxHxfvfVWLCfDvmW5Dw+vEAnbxXiFA==
|
||||
|
||||
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.28.6", "@babel/code-frame@^7.29.0":
|
||||
version "7.29.0"
|
||||
|
|
@ -1857,11 +1857,6 @@
|
|||
resolved "https://registry.yarnpkg.com/@hotwired/turbo/-/turbo-8.0.23.tgz#a6eebc9ab4a5faadae265a4cbec8cfcb5731e77c"
|
||||
integrity sha512-GZ7cijxEZ6Ig71u7rD6LHaRv/wcE/hNsc+nEfiWOkLNqUgLOwo5MNGWOy5ZV9ZUDSiQx1no7YxjTNnT4O6//cQ==
|
||||
|
||||
"@isaacs/cliui@^9.0.0":
|
||||
version "9.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-9.0.0.tgz#4d0a3f127058043bf2e7ee169eaf30ed901302f3"
|
||||
integrity sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==
|
||||
|
||||
"@jbtronics/bs-treeview@^1.0.1":
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@jbtronics/bs-treeview/-/bs-treeview-1.0.6.tgz#7fe126a2ca4716c824d97ab6d1a5f2417750445a"
|
||||
|
|
@ -2165,11 +2160,11 @@
|
|||
integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==
|
||||
|
||||
"@types/node@*":
|
||||
version "25.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-25.2.3.tgz#9c18245be768bdb4ce631566c7da303a5c99a7f8"
|
||||
integrity sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==
|
||||
version "25.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-25.3.0.tgz#749b1bd4058e51b72e22bd41e9eab6ebd0180470"
|
||||
integrity sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==
|
||||
dependencies:
|
||||
undici-types "~7.16.0"
|
||||
undici-types "~7.18.0"
|
||||
|
||||
"@types/parse-json@^4.0.0":
|
||||
version "4.0.2"
|
||||
|
|
@ -2392,16 +2387,16 @@ acorn-import-phases@^1.0.3:
|
|||
integrity sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==
|
||||
|
||||
acorn-walk@^8.0.0:
|
||||
version "8.3.4"
|
||||
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.4.tgz#794dd169c3977edf4ba4ea47583587c5866236b7"
|
||||
integrity sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==
|
||||
version "8.3.5"
|
||||
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.5.tgz#8a6b8ca8fc5b34685af15dabb44118663c296496"
|
||||
integrity sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==
|
||||
dependencies:
|
||||
acorn "^8.11.0"
|
||||
|
||||
acorn@^8.0.4, acorn@^8.11.0, acorn@^8.15.0:
|
||||
version "8.15.0"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816"
|
||||
integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==
|
||||
version "8.16.0"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.16.0.tgz#4ce79c89be40afe7afe8f3adb902a1f1ce9ac08a"
|
||||
integrity sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==
|
||||
|
||||
adjust-sourcemap-loader@^4.0.0:
|
||||
version "4.0.0"
|
||||
|
|
@ -2439,9 +2434,9 @@ ajv-keywords@^5.1.0:
|
|||
fast-deep-equal "^3.1.3"
|
||||
|
||||
ajv@^6.12.5:
|
||||
version "6.12.6"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
|
||||
integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
|
||||
version "6.14.0"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.14.0.tgz#fd067713e228210636ebb08c60bd3765d6dbe73a"
|
||||
integrity sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==
|
||||
dependencies:
|
||||
fast-deep-equal "^3.1.1"
|
||||
fast-json-stable-stringify "^2.0.0"
|
||||
|
|
@ -2606,11 +2601,9 @@ balanced-match@^1.0.0:
|
|||
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
|
||||
|
||||
balanced-match@^4.0.2:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-4.0.2.tgz#241591ea634702bef9c482696f2469406e16d233"
|
||||
integrity sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg==
|
||||
dependencies:
|
||||
jackspeak "^4.2.3"
|
||||
version "4.0.4"
|
||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-4.0.4.tgz#bfb10662feed8196a2c62e7c68e17720c274179a"
|
||||
integrity sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==
|
||||
|
||||
barcode-detector@^3.0.0, barcode-detector@^3.0.5:
|
||||
version "3.0.8"
|
||||
|
|
@ -2630,9 +2623,9 @@ base64-js@^1.1.2, base64-js@^1.3.0:
|
|||
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
|
||||
|
||||
baseline-browser-mapping@^2.9.0:
|
||||
version "2.9.19"
|
||||
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz#3e508c43c46d961eb4d7d2e5b8d1dd0f9ee4f488"
|
||||
integrity sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==
|
||||
version "2.10.0"
|
||||
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz#5b09935025bf8a80e29130251e337c6a7fc8cbb9"
|
||||
integrity sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==
|
||||
|
||||
big.js@^5.2.2:
|
||||
version "5.2.2"
|
||||
|
|
@ -2678,9 +2671,9 @@ brace-expansion@^1.1.7:
|
|||
concat-map "0.0.1"
|
||||
|
||||
brace-expansion@^5.0.2:
|
||||
version "5.0.2"
|
||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.2.tgz#b6c16d0791087af6c2bc463f52a8142046c06b6f"
|
||||
integrity sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==
|
||||
version "5.0.3"
|
||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.3.tgz#6a9c6c268f85b53959ec527aeafe0f7300258eef"
|
||||
integrity sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==
|
||||
dependencies:
|
||||
balanced-match "^4.0.2"
|
||||
|
||||
|
|
@ -2800,9 +2793,9 @@ caniuse-api@^3.0.0:
|
|||
lodash.uniq "^4.5.0"
|
||||
|
||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001759:
|
||||
version "1.0.30001770"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz#4dc47d3b263a50fbb243448034921e0a88591a84"
|
||||
integrity sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==
|
||||
version "1.0.30001772"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001772.tgz#aa8a176eba0006e78c965a8215c7a1ceb030122d"
|
||||
integrity sha512-mIwLZICj+ntVTw4BT2zfp+yu/AqV6GMKfJVJMx3MwPxs+uk/uj2GLl2dH8LQbjiLDX66amCga5nKFyDgRR43kg==
|
||||
|
||||
ccount@^2.0.0:
|
||||
version "2.0.1"
|
||||
|
|
@ -3175,9 +3168,9 @@ css-loader@^5.2.7:
|
|||
semver "^7.3.5"
|
||||
|
||||
css-loader@^7.1.0:
|
||||
version "7.1.3"
|
||||
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-7.1.3.tgz#c0de715ceabe39b8531a85fcaf6734a430c4d99a"
|
||||
integrity sha512-frbERmjT0UC5lMheWpJmMilnt9GEhbZJN/heUb7/zaJYeIzj5St9HvDcfshzzOqbsS+rYpMk++2SD3vGETDSyA==
|
||||
version "7.1.4"
|
||||
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-7.1.4.tgz#8f6bf9f8fc8cbef7d2ef6e80acc6545eaefa90b1"
|
||||
integrity sha512-vv3J9tlOl04WjiMvHQI/9tmIrCxVrj6PFbHemBB1iihpeRbi/I4h033eoFIhwxBBqLhI0KYFS7yvynBFhIZfTw==
|
||||
dependencies:
|
||||
icss-utils "^5.1.0"
|
||||
postcss "^8.4.40"
|
||||
|
|
@ -3681,9 +3674,9 @@ dunder-proto@^1.0.0, dunder-proto@^1.0.1:
|
|||
gopd "^1.2.0"
|
||||
|
||||
electron-to-chromium@^1.5.263:
|
||||
version "1.5.286"
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz#142be1ab5e1cd5044954db0e5898f60a4960384e"
|
||||
integrity sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==
|
||||
version "1.5.302"
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz#032a5802b31f7119269959c69fe2015d8dad5edb"
|
||||
integrity sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==
|
||||
|
||||
emoji-regex@^7.0.1:
|
||||
version "7.0.3"
|
||||
|
|
@ -4843,13 +4836,6 @@ isobject@^3.0.1:
|
|||
resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
|
||||
integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==
|
||||
|
||||
jackspeak@^4.2.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-4.2.3.tgz#27ef80f33b93412037c3bea4f8eddf80e1931483"
|
||||
integrity sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==
|
||||
dependencies:
|
||||
"@isaacs/cliui" "^9.0.0"
|
||||
|
||||
javascript-stringify@^1.6.0:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/javascript-stringify/-/javascript-stringify-1.6.0.tgz#142d111f3a6e3dae8f4a9afd77d45855b5a9cce3"
|
||||
|
|
@ -4974,9 +4960,9 @@ jszip@^3.2.0:
|
|||
setimmediate "^1.0.5"
|
||||
|
||||
katex@^0.16.0:
|
||||
version "0.16.28"
|
||||
resolved "https://registry.yarnpkg.com/katex/-/katex-0.16.28.tgz#64068425b5a29b41b136aae0d51cbb2c71d64c39"
|
||||
integrity sha512-YHzO7721WbmAL6Ov1uzN/l5mY5WWWhJBSW+jq4tkfZfsxmo1hu6frS0EOswvjBUnWE6NtjEs48SFn5CQESRLZg==
|
||||
version "0.16.29"
|
||||
resolved "https://registry.yarnpkg.com/katex/-/katex-0.16.29.tgz#d6d2cc2e1840663c2ceb6fc764d4f0d9ca04fa4c"
|
||||
integrity sha512-ef+wYUDehNgScWoA0ZhEngsNqUv9uIj4ftd/PapQmT+E85lXI6Wx6BvJO48v80Vhj3t/IjEoZWw9/ZPe8kHwHg==
|
||||
dependencies:
|
||||
commander "^8.3.0"
|
||||
|
||||
|
|
@ -5119,9 +5105,9 @@ marked-mangle@^1.0.1:
|
|||
integrity sha512-bRrqNcfU9v3iRECb7YPvA+/xKZMjHojd9R92YwHbFjdPQ+Wc7vozkbGKAv4U8AUl798mNUuY3DTBQkedsV3TeQ==
|
||||
|
||||
marked@^17.0.1:
|
||||
version "17.0.2"
|
||||
resolved "https://registry.yarnpkg.com/marked/-/marked-17.0.2.tgz#a103f82bed9653dd1d74c15f74107c84ddbe749d"
|
||||
integrity sha512-s5HZGFQea7Huv5zZcAGhJLT3qLpAfnY7v7GWkICUr0+Wd5TFEtdlRR2XUL5Gg+RH7u2Df595ifrxR03mBaw7gA==
|
||||
version "17.0.3"
|
||||
resolved "https://registry.yarnpkg.com/marked/-/marked-17.0.3.tgz#0defa25b1ba288433aa847848475d11109e1b3fd"
|
||||
integrity sha512-jt1v2ObpyOKR8p4XaUJVk3YWRJ5n+i4+rjQopxvV32rSndTJXvIzuUdWWIy/1pFQMkQmvTXawzDNqOH/CUmx6A==
|
||||
|
||||
math-intrinsics@^1.1.0:
|
||||
version "1.1.0"
|
||||
|
|
@ -5139,9 +5125,9 @@ mdast-util-find-and-replace@^3.0.0:
|
|||
unist-util-visit-parents "^6.0.0"
|
||||
|
||||
mdast-util-from-markdown@^2.0.0:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz#4850390ca7cf17413a9b9a0fbefcd1bc0eb4160a"
|
||||
integrity sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz#c95822b91aab75f18a4cbe8b2f51b873ed2cf0c7"
|
||||
integrity sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==
|
||||
dependencies:
|
||||
"@types/mdast" "^4.0.0"
|
||||
"@types/unist" "^3.0.0"
|
||||
|
|
@ -5606,9 +5592,9 @@ mini-css-extract-plugin@^2.4.2, mini-css-extract-plugin@^2.6.0:
|
|||
tapable "^2.2.1"
|
||||
|
||||
minimatch@*:
|
||||
version "10.2.0"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.0.tgz#e710473e66e3e1aaf376d0aa82438375cac86e9e"
|
||||
integrity sha512-ugkC31VaVg9cF0DFVoADH12k6061zNZkZON+aX8AWsR9GhPcErkcMBceb6znR8wLERM2AkkOxy2nWRLpT9Jq5w==
|
||||
version "10.2.2"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.2.tgz#361603ee323cfb83496fea2ae17cc44ea4e1f99f"
|
||||
integrity sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==
|
||||
dependencies:
|
||||
brace-expansion "^5.0.2"
|
||||
|
||||
|
|
@ -5620,9 +5606,9 @@ minimatch@3.0.4:
|
|||
brace-expansion "^1.1.7"
|
||||
|
||||
minimatch@^3.0.4, minimatch@^3.1.1:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
|
||||
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.3.tgz#6a5cba9b31f503887018f579c89f81f61162e624"
|
||||
integrity sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
|
|
@ -6530,9 +6516,9 @@ postcss@^8.2.14, postcss@^8.2.15, postcss@^8.4.12, postcss@^8.4.40:
|
|||
source-map-js "^1.2.1"
|
||||
|
||||
preact@^10.13.2:
|
||||
version "10.28.3"
|
||||
resolved "https://registry.yarnpkg.com/preact/-/preact-10.28.3.tgz#3c2171526b3e29628ad1a6c56a9e3ca867bbdee8"
|
||||
integrity sha512-tCmoRkPQLpBeWzpmbhryairGnhW9tKV6c6gr/w+RhoRoKEJwsjzipwp//1oCpGPOchvSLaAPlpcJi9MwMmoPyA==
|
||||
version "10.28.4"
|
||||
resolved "https://registry.yarnpkg.com/preact/-/preact-10.28.4.tgz#8ffab01c5c0590535bdaecdd548801f44c6e483a"
|
||||
integrity sha512-uKFfOHWuSNpRFVTnljsCluEFq57OKT+0QdOiQo8XWnQ/pSvg7OpX5eNOejELXJMWy+BwM2nobz0FkvzmnpCNsQ==
|
||||
|
||||
pretty-error@^4.0.0:
|
||||
version "4.0.0"
|
||||
|
|
@ -7582,10 +7568,10 @@ unbox-primitive@^1.1.0:
|
|||
has-symbols "^1.1.0"
|
||||
which-boxed-primitive "^1.1.1"
|
||||
|
||||
undici-types@~7.16.0:
|
||||
version "7.16.0"
|
||||
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.16.0.tgz#ffccdff36aea4884cbfce9a750a0580224f58a46"
|
||||
integrity sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==
|
||||
undici-types@~7.18.0:
|
||||
version "7.18.2"
|
||||
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.18.2.tgz#29357a89e7b7ca4aef3bf0fd3fd0cd73884229e9"
|
||||
integrity sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==
|
||||
|
||||
unicode-canonical-property-names-ecmascript@^2.0.0:
|
||||
version "2.0.1"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue