mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-03-01 04:49:36 +00:00
Replaced the custom controller for fragment replacements with symfony streams
This does not require a complete new endpoint
This commit is contained in:
parent
05ee3157fb
commit
910ad939df
5 changed files with 65 additions and 198 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,20 @@
|
|||
{# 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 %}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@
|
|||
<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') }}
|
||||
data-lookup-url="{{ path('scan_lookup') }}"></div>
|
||||
|
|
@ -18,13 +17,22 @@
|
|||
</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>
|
||||
|
||||
<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) }}
|
||||
{% 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 %}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue