Merge branch 'master' into feature/kicad-enhancements

This commit is contained in:
Jan Böhmer 2026-03-01 22:09:47 +01:00 committed by GitHub
commit 6ab75488b4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
56 changed files with 7142 additions and 2997 deletions

View file

@ -60,6 +60,15 @@
</a>
</li>
{% endif %}
{% if is_granted("@tools.label_scanner") %}
<li>
<a class="dropdown-item" href="{{ path('scan_dialog') }}">
<i class="fa-fw fa-solid fa-camera-retro"></i>
{% trans %}parts.create_from_scan.title{% endtrans %}
</a>
</li>
{% endif %}
{% endif %}
{% if is_granted('@parts.import') %}
@ -159,7 +168,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>

View file

@ -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>

View file

@ -0,0 +1,78 @@
<!DOCTYPE html>
<html lang="{{ app.request.locale | replace({"_": "-"}) }}"
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>️⚠️️</text></svg>">
{# The content block is already escaped. so we must not escape it again. #}
<title>{% trans %}attachment.sandbox.title{% endtrans %}: {{ attachment.filename }}</title>
<style>
/* Reset margins and stop the page from scrolling */
body, html {
margin: 0;
padding: 0;
height: 100%;
overflow: hidden;
font-family: sans-serif;
}
/* The Flex Container */
.wrapper {
display: flex;
flex-direction: column;
height: 100vh;
}
/* The Warning Header */
.warning-bar {
background-color: #ff4d4d;
color: white;
padding: 10px 20px;
text-align: center;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
z-index: 10; /* Keep it above the iframe */
}
/* The Iframe: The 'flex: 1' makes it fill all remaining space */
.content-frame {
flex: 1;
border: none;
width: 100%;
}
</style>
</head>
<body>
{% block body %}
{# We have a fullscreen iframe, with an warning on top #}
<div class="wrapper">
<header>
<header class="warning-bar">
<b>⚠️ {% trans%}attachment.sandbox.warning{% endtrans %}</b>
<br>
<small>
{% trans%}[Attachment]{% endtrans%}: <b>{{ attachment.name }}</b> / <b>{{ attachment.filename ?? "" }}</b> ({% trans%}id.label{% endtrans %}: {{ attachment.id }})
<a href="{{ path("attachment_view", {id: attachment.id}) }}" style="color: white; margin-left: 15px;">{% trans%}attachment.sandbox.as_plain_text{% endtrans %}</a>
<a href="{{ path("homepage") }}" style="color: white; margin-left: 15px;">{% trans%}attachment.sandbox.back_to_partdb{% endtrans %}</a>
</small>
</header>
</header>
<iframe referrerpolicy="no-referrer" class="content-frame"
{# When changing this sandbox, also change the sandbox CSP in the controller #}
sandbox="allow-scripts allow-downloads allow-modals"
srcdoc="{{ content|e('html_attr') }}"
></iframe>
</div>
{% endblock %}
</body>
</html>

View file

@ -2,7 +2,7 @@
<!DOCTYPE html>
<html lang="{{ app.request.locale | replace({"_": "-"}) }}"
{# For the UX translator, just use the language part (before the _. should be 2 chars), otherwise it finds no translations #}
{# For the UX translator, just use the language part (before the _. should be 2 chars), otherwise it finds no translations #}
data-symfony-ux-translator-locale="{{ app.request.locale|u.truncate(2) }}">
<head>
<meta charset="UTF-8">
@ -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">
@ -68,7 +73,17 @@
{{ encore_entry_script_tags('webauthn_tfa') }}
{% endblock %}
</head>
<body data-base-url="{{ path('homepage', {'_locale': app.request.locale}) }}" data-locale="{{ app.request.locale|default("en")|slice(0,2) }}">
<body data-base-url="{{ path('homepage', {'_locale': app.request.locale}) }}"
data-locale="{{ app.request.locale|default("en")|slice(0,2) }}"
data-keybindings-special-characters="{{ settings_instance('keybindings').enableSpecialCharacters ? 'true' : 'false' }}">
{# Listen for the special #}
{% if is_granted("@tools.label_scanner") %}
<form class="d-none" {{ stimulus_controller('helpers/scan_special_char') }} action="{{ path("scan_dialog") }}" data-turbo-frame="content">
<input name="input" type="hidden">
</form>
{% endif %}
{% block body %}
<header>
<turbo-frame id="navbar-frame" target="content" data-turbo-action="advance">
@ -114,13 +129,13 @@
<!-- Back to top button -->
<button id="back-to-top" class="btn btn-primary back-to-top btn-sm" role="button" title="{% trans %}back_to_top{% endtrans %}"
{{ stimulus_controller('common/back_to_top') }} {{ stimulus_action('common/back_to_top', 'backToTop') }}>
{{ stimulus_controller('common/back_to_top') }} {{ stimulus_action('common/back_to_top', 'backToTop') }}>
<i class="fas fa-angle-up fa-fw"></i>
</button>
{# Must be outside of the sidebar or it will be hidden too #}
<button class="btn btn-outline-secondary btn-sm p-0 d-md-block d-none" type="button" id="sidebar-toggle-button" title="{% trans %}sidebar.big.toggle{% endtrans %}"
{{ stimulus_controller('common/hide_sidebar') }} {{ stimulus_action('common/hide_sidebar', 'toggleSidebar') }} style="--fa-width: 10px;">
{{ stimulus_controller('common/hide_sidebar') }} {{ stimulus_action('common/hide_sidebar', 'toggleSidebar') }} style="--fa-width: 10px;">
<i class="fas fa-angle-left"></i>
</button>

View 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 %}

View file

@ -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 %}