diff --git a/assets/controllers/common/toast_controller.js b/assets/controllers/common/toast_controller.js index 36b7f3cc..196692fb 100644 --- a/assets/controllers/common/toast_controller.js +++ b/assets/controllers/common/toast_controller.js @@ -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(); } -} \ No newline at end of file +} diff --git a/assets/controllers/pages/barcode_scan_controller.js b/assets/controllers/pages/barcode_scan_controller.js index 352c527c..ae51e951 100644 --- a/assets/controllers/pages/barcode_scan_controller.js +++ b/assets/controllers/pages/barcode_scan_controller.js @@ -30,9 +30,9 @@ export default class extends Controller { _submitting = false; _lastDecodedText = ""; _onInfoChange = null; - _onFormSubmit = null; connect() { + // Prevent double init if connect fires twice if (this._scanner) return; @@ -45,22 +45,6 @@ export default class extends Controller { info.addEventListener("change", this._onInfoChange); } - // Stop camera cleanly before manual form submit (prevents broken camera after reload) - const form = document.getElementById("scan_dialog_form"); - if (form) { - this._onFormSubmit = () => { - try { - const p = this._scanner?.clear?.(); - if (p && typeof p.then === "function") p.catch(() => {}); - } catch (_) { - // ignore - } - }; - - // capture=true so we run before other handlers / navigation - form.addEventListener("submit", this._onFormSubmit, { capture: true }); - } - const isMobile = window.matchMedia("(max-width: 768px)").matches; //This function ensures, that the qrbox is 70% of the total viewport @@ -94,10 +78,10 @@ export default class extends Controller { } disconnect() { + // If we already stopped/cleared before submit, nothing to do. const scanner = this._scanner; this._scanner = null; - this._submitting = false; this._lastDecodedText = ""; // Unbind info-mode change handler (always do this, even if scanner is null) @@ -107,13 +91,6 @@ export default class extends Controller { } this._onInfoChange = null; - // remove the onForm submit handler - const form = document.getElementById("scan_dialog_form"); - if (form && this._onFormSubmit) { - form.removeEventListener("submit", this._onFormSubmit, { capture: true }); - } - this._onFormSubmit = null; - if (!scanner) return; try { @@ -125,7 +102,7 @@ export default class extends Controller { } - async onScanSuccess(decodedText) { + onScanSuccess(decodedText) { if (!decodedText) return; const normalized = String(decodedText).trim(); @@ -134,94 +111,11 @@ export default class extends Controller { // scan once per barcode if (normalized === this._lastDecodedText) return; - // If a request/submit is in-flight, ignore scans. - if (this._submitting) return; - // Mark as handled immediately (prevents spam even if callback fires repeatedly) this._lastDecodedText = normalized; - this._submitting = true; - // Clear previous augmented result immediately to avoid stale info - // lingering when the next scan is not augmented (or is transient/junk). - const el = document.getElementById("scan-augmented-result"); - if (el) el.innerHTML = ""; - - //Put our decoded Text into the input box - const input = document.getElementById("scan_dialog_input"); - if (input) input.value = decodedText; - - const infoMode = !!document.getElementById("scan_dialog_info_mode")?.checked; - - try { - const data = await this.lookup(normalized, infoMode); - - // ok:false = transient junk decode; ignore without wiping UI - if (!data || data.ok !== true) { - this._lastDecodedText = ""; // allow retry - return; - } - - // If info mode is OFF and part was found -> redirect - if (!infoMode && data.found && data.redirectUrl) { - window.location.assign(data.redirectUrl); - return; - } - - // If info mode is OFF and part was NOT found, redirect to create part URL - if (!infoMode && !data.found && data.createUrl) { - window.location.assign(data.createUrl); - return; - } - - // Otherwise render returned fragment HTML - if (typeof data.html === "string" && data.html !== "") { - const el = document.getElementById("scan-augmented-result"); - if (el) el.innerHTML = data.html; - } - } catch (e) { - console.warn("[barcode_scan] lookup failed", e); - // allow retry on failure - this._lastDecodedText = ""; - } finally { - this._submitting = false; - } - } - - - async lookup(decodedText, infoMode) { - const form = document.getElementById("scan_dialog_form"); - if (!form) return { ok: false }; - - generateCsrfToken(form); - - const mode = - document.querySelector('input[name="scan_dialog[mode]"]:checked')?.value ?? ""; - - const body = new URLSearchParams(); - body.set("input", decodedText); - if (mode !== "") body.set("mode", mode); - body.set("info_mode", infoMode ? "1" : "0"); - - const headers = { - "Accept": "application/json", - "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", - ...generateCsrfHeaders(form), - }; - - const url = this.element.dataset.lookupUrl; - if (!url) throw new Error("Missing data-lookup-url on #reader-box"); - - const resp = await fetch(url, { - method: "POST", - headers, - body: body.toString(), - credentials: "same-origin", - }); - - if (!resp.ok) { - throw new Error(`lookup failed: HTTP ${resp.status}`); - } - - return await resp.json(); + document.getElementById('scan_dialog_input').value = decodedText; + //Submit form + document.getElementById('scan_dialog_form').requestSubmit(); } } diff --git a/src/Controller/ScanController.php b/src/Controller/ScanController.php index af043783..6acdc16c 100644 --- a/src/Controller/ScanController.php +++ b/src/Controller/ScanController.php @@ -64,6 +64,7 @@ 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 @@ -131,6 +132,18 @@ class ScanController extends AbstractController 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, + ]); + } + } } catch (\Throwable $e) { // Keep fallback user-friendly; avoid 500 @@ -138,6 +151,13 @@ class ScanController extends AbstractController } } + //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, @@ -197,69 +217,4 @@ class ScanController extends AbstractController return null; } - - /** - * Provides XHR endpoint for looking up barcode information and return JSON response - * @param Request $request - * @return JsonResponse - */ - #[Route(path: '/lookup', name: 'scan_lookup', methods: ['POST'])] - public function lookup(Request $request): JsonResponse - { - $this->denyAccessUnlessGranted('@tools.label_scanner'); - - $input = trim($request->request->getString('input', '')); - - // We cannot use getEnum here, because we get an empty string for mode, when auto mode is selected - $mode = $request->request->getString('mode', ''); - if ($mode === '') { - $modeEnum = null; - } else { - $modeEnum = BarcodeSourceType::from($mode); // validate enum value; will throw if invalid - } - - $infoMode = $request->request->getBoolean('info_mode', false); - - if ($input === '') { - return new JsonResponse(['ok' => false], 200); - } - - try { - $scan = $this->barcodeNormalizer->scanBarcodeContent($input, $modeEnum); - } catch (InvalidArgumentException) { - // Camera sometimes produces garbage decodes for a frame; ignore those. - return new JsonResponse(['ok' => false], 200); - } - - $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); - } - - // Render fragment (use openUrl for universal "Open" link) - $html = $this->renderView('label_system/scanner/_info_mode.html.twig', [ - 'decoded' => $decoded, - 'entity' => $dbEntity, - 'part' => $resolvedPart, - 'openUrl' => $openUrl, - 'createUrl' => $createUrl, - ]); - - return new JsonResponse([ - 'ok' => true, - 'found' => $openUrl !== null, // we consider the code "found", if we can at least show an info page (even if the part is not found, but we can show the decoded data and a "create" button) - 'redirectUrl' => $openUrl, // client redirects only when infoMode=false - 'createUrl' => $createUrl, - 'html' => $html, - 'infoMode' => $infoMode, - ], 200); - } } diff --git a/templates/_turbo_control.html.twig b/templates/_turbo_control.html.twig index 90ae8d9a..cf65f0da 100644 --- a/templates/_turbo_control.html.twig +++ b/templates/_turbo_control.html.twig @@ -1,14 +1,20 @@ -{# Insert flashes #} -
- {% for label, messages in app.flashes() %} - {% for message in messages %} - {{ include('_toast.html.twig', { - 'label': label, - 'message': message - }) }} - {% endfor %} - {% endfor %} -
+{% block flashes %} + {# Insert flashes #} + + + +{% endblock %} {# Allow pages to request a fully reload of everything #} {% if global_reload_needed is defined and global_reload_needed %} diff --git a/templates/label_system/scanner/scanner.html.twig b/templates/label_system/scanner/scanner.html.twig index 7275f89d..95059eb3 100644 --- a/templates/label_system/scanner/scanner.html.twig +++ b/templates/label_system/scanner/scanner.html.twig @@ -10,7 +10,6 @@
-
@@ -18,13 +17,22 @@
+
+ {% include "label_system/scanner/_info_mode.html.twig" %} +
+ + {{ form_start(form, {'attr': {'id': 'scan_dialog_form'}}) }} + + {{ form_end(form) }}
- -
- {% include "label_system/scanner/_info_mode.html.twig" %} -
- - {{ form_start(form, {'attr': {'id': 'scan_dialog_form'}}) }} - - {{ form_end(form) }} +{% endblock %} + +{% block scan_results %} + + + {% endblock %}