Compare commits

...

5 commits

Author SHA1 Message Date
Jan Böhmer
ffe2f8004b
New Crowdin updates (#1419)
* New translations frontend.en.xlf (English)

[ci skip]

* New translations messages.en.xlf (English)

[ci skip]

* New translations frontend.en.xlf (English)

[ci skip]

* New translations messages.en.xlf (English)

[ci skip]

* New translations messages.en.xlf (English)

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

* New translations messages.en.xlf (English)

[ci skip]

* New translations validators.en.xlf (English)

[ci skip]

* New translations messages.en.xlf (German)

[ci skip]

* New translations validators.en.xlf (German)

[ci skip]

* New translations frontend.en.xlf (German)

[ci skip]
2026-06-29 00:12:20 +02:00
Jan Böhmer
7244803937 Fixed phpstan error 2026-06-29 00:08:44 +02:00
Jan Böhmer
214646caa6 Bumped version to 2.13.0 2026-06-28 23:58:38 +02:00
Jan Böhmer
ffcfdb793f Validate info provider references modified via part edit form 2026-06-28 23:56:55 +02:00
Jan Böhmer
e03eda84c5 Allow to edit info provider reference
Fixes   issue #1394
2026-06-28 23:47:49 +02:00
16 changed files with 399 additions and 29 deletions

View file

@ -1 +1 @@
2.12.3
2.13.0

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -55,6 +55,12 @@
<target>Los!</target>
</segment>
</unit>
<unit id="8d38e7538" name="user.password_strength.crack_time">
<segment state="translated">
<source>user.password_strength.crack_time</source>
<target>Geschätzte Zeit bis zum Knacken: %time%</target>
</segment>
</unit>
<unit id="dBtnOk01" name="dialog.btn.ok">
<segment state="translated">
<source>dialog.btn.ok</source>

View file

@ -56,11 +56,11 @@
</segment>
</unit>
<unit id="8d38e7538" name="user.password_strength.crack_time">
<segment state="translated">
<source>user.password_strength.crack_time</source>
<target>Estimated time to crack: %time%</target>
</segment>
</unit>
<segment state="translated">
<source>user.password_strength.crack_time</source>
<target>Estimated time to crack: %time%</target>
</segment>
</unit>
<unit id="dBtnOk01" name="dialog.btn.ok">
<segment state="translated">
<source>dialog.btn.ok</source>

View file

@ -56,11 +56,11 @@
</segment>
</unit>
<unit id="8d38e7538" name="user.password_strength.crack_time">
<segment state="translated">
<source>user.password_strength.crack_time</source>
<target>预计破解时间:%time%</target>
</segment>
</unit>
<segment state="translated">
<source>user.password_strength.crack_time</source>
<target>预计破解时间:%time%</target>
</segment>
</unit>
<unit id="dBtnOk01" name="dialog.btn.ok">
<segment state="translated">
<source>dialog.btn.ok</source>

View file

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

View file

@ -13697,5 +13697,53 @@ Buerklin-API Authentication server:
<target>You can use this randomly generated value (share it with nobody):</target>
</segment>
</unit>
<unit id="cEwxoSj" name="info_providers.provider_key">
<segment state="translated">
<source>info_providers.provider_key</source>
<target>Info provider</target>
</segment>
</unit>
<unit id="0sjPRNV" name="info_providers.provider_id">
<segment state="translated">
<source>info_providers.provider_id</source>
<target>Provider ID</target>
</segment>
</unit>
<unit id="2DzzAxZ" name="info_providers.provider_url">
<segment state="translated">
<source>info_providers.provider_url</source>
<target>Provider URL</target>
</segment>
</unit>
<unit id="4v3QmF6" name="part.edit.provider_reference">
<segment state="translated">
<source>part.edit.provider_reference</source>
<target>Info provider reference</target>
</segment>
</unit>
<unit id="9X2qEi7" name="log.element_edited.changed_fields.providerReference.provider_key">
<segment state="translated">
<source>log.element_edited.changed_fields.providerReference.provider_key</source>
<target>Information provider</target>
</segment>
</unit>
<unit id="MWXgWDb" name="log.element_edited.changed_fields.providerReference.provider_id">
<segment state="translated">
<source>log.element_edited.changed_fields.providerReference.provider_id</source>
<target>Provider ID</target>
</segment>
</unit>
<unit id="83fSFvo" name="part.info_provider_reference.updated_never">
<segment state="translated">
<source>part.info_provider_reference.updated_never</source>
<target>Never updated</target>
</segment>
</unit>
<unit id="yIK_Wtj" name="part.edit.provider_reference.warning">
<segment state="translated">
<source>part.edit.provider_reference.warning</source>
<target>Warning: Changing values here can break the info retrieval mechanism! You should use the "update from info provider" functionality whenever possible.</target>
</segment>
</unit>
</file>
</xliff>

View file

@ -385,7 +385,7 @@
<unit id="CPPZnUs" name="tfa_backup.generation_date">
<segment state="translated">
<source>tfa_backup.generation_date</source>
<target>备份代码的生成日期</target>
<target>备份生成日期</target>
</segment>
</unit>
<unit id="BrMV4pc" name="user.edit.tfa.disabled">
@ -911,7 +911,7 @@
</segment>
</unit>
<unit id="melYYg5" name="log.list.title">
<segment state="final">
<segment state="translated">
<source>log.list.title</source>
<target>系统日志</target>
</segment>
@ -2697,7 +2697,7 @@
</segment>
</unit>
<unit id="2J1kPwq" name="log.target_deleted">
<segment state="final">
<segment state="translated">
<source>log.target_deleted</source>
<target>已删除</target>
</segment>
@ -2742,7 +2742,7 @@
</segment>
</unit>
<unit id="i_eRuZ4" name="log.type">
<segment state="final">
<segment state="translated">
<source>log.type</source>
<target>事件</target>
</segment>
@ -2760,13 +2760,13 @@
</segment>
</unit>
<unit id="cDlFI_K" name="log.target_type">
<segment state="final">
<segment state="translated">
<source>log.target_type</source>
<target>目标类型</target>
</segment>
</unit>
<unit id="kRqoWSM" name="log.target">
<segment state="final">
<segment state="translated">
<source>log.target</source>
<target>目标</target>
</segment>
@ -7640,7 +7640,7 @@ Element1-&gt;Element1.2</target>
<unit id="MJoZl4f" name="homepage.forum.text">
<segment state="translated">
<source>homepage.forum.text</source>
<target>有关Part-DB的问题请使用 &lt;a href="%href%"class="link-external"target="_blank"&gt;讨论论坛&lt;/a&gt; </target>
<target>有关Part-DB的问题请使用 &lt;a href="%href%"class="link-external"target="_blank"&gt;讨论论坛&lt;/a&gt;</target>
</segment>
</unit>
<unit id="YsukbnK" name="log.element_edited.changed_fields.category">

View file

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

View file

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