diff --git a/VERSION b/VERSION index ccc99d02..fb2c0766 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.12.3 +2.13.0 diff --git a/src/Entity/Parts/InfoProviderReference.php b/src/Entity/Parts/InfoProviderReference.php index 810aef0c..8f6874b9 100644 --- a/src/Entity/Parts/InfoProviderReference.php +++ b/src/Entity/Parts/InfoProviderReference.php @@ -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(); + } + } } diff --git a/src/Entity/Parts/PartTraits/AdvancedPropertyTrait.php b/src/Entity/Parts/PartTraits/AdvancedPropertyTrait.php index 065469b5..9fa41f93 100644 --- a/src/Entity/Parts/PartTraits/AdvancedPropertyTrait.php +++ b/src/Entity/Parts/PartTraits/AdvancedPropertyTrait.php @@ -75,6 +75,7 @@ trait AdvancedPropertyTrait */ #[ORM\Embedded(class: InfoProviderReference::class, columnPrefix: 'provider_reference_')] #[Groups(['full', 'part:read'])] + #[Assert\Valid()] protected InfoProviderReference $providerReference; /** diff --git a/src/Form/InfoProviderSystem/InfoProviderReferenceType.php b/src/Form/InfoProviderSystem/InfoProviderReferenceType.php new file mode 100644 index 00000000..73fc8fe3 --- /dev/null +++ b/src/Form/InfoProviderSystem/InfoProviderReferenceType.php @@ -0,0 +1,113 @@ +. + */ + +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); + + } +} diff --git a/src/Form/InfoProviderSystem/ProviderSelectType.php b/src/Form/InfoProviderSystem/ProviderSelectType.php index bad3edaa..e59dc85f 100644 --- a/src/Form/InfoProviderSystem/ProviderSelectType.php +++ b/src/Form/InfoProviderSystem/ProviderSelectType.php @@ -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'); + }; + }); } } diff --git a/src/Form/Part/PartBaseType.php b/src/Form/Part/PartBaseType.php index a31f2469..afef8fdb 100644 --- a/src/Form/Part/PartBaseType.php +++ b/src/Form/Part/PartBaseType.php @@ -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 diff --git a/templates/parts/edit/_advanced.html.twig b/templates/parts/edit/_advanced.html.twig index 30479d11..f18dba58 100644 --- a/templates/parts/edit/_advanced.html.twig +++ b/templates/parts/edit/_advanced.html.twig @@ -15,3 +15,24 @@ {{ form_row(form.partUnit) }} {{ form_row(form.partCustomState) }} {{ form_row(form.gtin) }} + +
+
+
+

+ +

+
+
+
+ {% trans %}part.edit.provider_reference.warning{% endtrans %} +
+ + {{ form_widget(form.providerReference) }} +
+
+
+
+
diff --git a/templates/parts/info/_extended_infos.html.twig b/templates/parts/info/_extended_infos.html.twig index 9cb4e4e5..acc197e3 100644 --- a/templates/parts/info/_extended_infos.html.twig +++ b/templates/parts/info/_extended_infos.html.twig @@ -76,7 +76,7 @@ {% endif %} {{ info_provider_label(part.providerReference.providerKey)|default(part.providerReference.providerKey) }}: {{ part.providerReference.providerId }} - ({{ part.providerReference.lastUpdated | format_datetime() }}) + ({{ part.providerReference.lastUpdated ? (part.providerReference.lastUpdated | format_datetime()) : ("part.info_provider_reference.updated_never"|trans) }}) {% if part.providerReference.providerUrl %} {% endif %} diff --git a/translations/frontend.de.xlf b/translations/frontend.de.xlf index 6a4e7786..317b7c7f 100644 --- a/translations/frontend.de.xlf +++ b/translations/frontend.de.xlf @@ -55,6 +55,12 @@ Los! + + + user.password_strength.crack_time + Geschätzte Zeit bis zum Knacken: %time% + + dialog.btn.ok diff --git a/translations/frontend.en.xlf b/translations/frontend.en.xlf index 7f68558a..d0099493 100644 --- a/translations/frontend.en.xlf +++ b/translations/frontend.en.xlf @@ -56,11 +56,11 @@ - - user.password_strength.crack_time - Estimated time to crack: %time% - - + + user.password_strength.crack_time + Estimated time to crack: %time% + + dialog.btn.ok diff --git a/translations/frontend.zh.xlf b/translations/frontend.zh.xlf index bdb053d6..39ef238b 100644 --- a/translations/frontend.zh.xlf +++ b/translations/frontend.zh.xlf @@ -56,11 +56,11 @@ - - user.password_strength.crack_time - 预计破解时间:%time% - - + + user.password_strength.crack_time + 预计破解时间:%time% + + dialog.btn.ok diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index 26f6d40f..5542a6ed 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -13617,6 +13617,36 @@ Buerklin-API-Authentication-Server: Host-URL + + + settings.ai.ollama + Ollama + + + + + settings.ai.ollama.endpoint + Endpoint URL + + + + + settings.ai.ollama.apiKey + API Key + + + + + settings.ai.timeout + Timeout + + + + + settings.ai.timeout.help + 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. + + browser_plugin.recent_pages.title @@ -13665,5 +13695,53 @@ Buerklin-API-Authentication-Server: Sie können diesen zufällig generierten Wert verwenden (geben Sie ihn niemandem weiter): + + + info_providers.provider_key + Informationsquelle + + + + + info_providers.provider_id + Provider ID + + + + + info_providers.provider_url + Provider URL + + + + + part.edit.provider_reference + Referenz auf Informationsquelle + + + + + log.element_edited.changed_fields.providerReference.provider_key + Informationsquelle + + + + + log.element_edited.changed_fields.providerReference.provider_id + Provider ID + + + + + part.info_provider_reference.updated_never + Niemals aktualisiert + + + + + part.edit.provider_reference.warning + 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. + + diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index d828d3ee..36021868 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -13697,5 +13697,53 @@ Buerklin-API Authentication server: You can use this randomly generated value (share it with nobody): + + + info_providers.provider_key + Info provider + + + + + info_providers.provider_id + Provider ID + + + + + info_providers.provider_url + Provider URL + + + + + part.edit.provider_reference + Info provider reference + + + + + log.element_edited.changed_fields.providerReference.provider_key + Information provider + + + + + log.element_edited.changed_fields.providerReference.provider_id + Provider ID + + + + + part.info_provider_reference.updated_never + Never updated + + + + + part.edit.provider_reference.warning + Warning: Changing values here can break the info retrieval mechanism! You should use the "update from info provider" functionality whenever possible. + + diff --git a/translations/messages.zh.xlf b/translations/messages.zh.xlf index 7083c3ff..5cd93041 100644 --- a/translations/messages.zh.xlf +++ b/translations/messages.zh.xlf @@ -385,7 +385,7 @@ tfa_backup.generation_date - 备份代码的生成日期 + 备份生成日期 @@ -911,7 +911,7 @@ - + log.list.title 系统日志 @@ -2697,7 +2697,7 @@ - + log.target_deleted 已删除 @@ -2742,7 +2742,7 @@ - + log.type 事件 @@ -2760,13 +2760,13 @@ - + log.target_type 目标类型 - + log.target 目标 @@ -7640,7 +7640,7 @@ Element1->Element1.2 homepage.forum.text - 有关Part-DB的问题,请使用 <a href="%href%"class="link-external"target="_blank">讨论论坛</a> + 有关Part-DB的问题,请使用 <a href="%href%"class="link-external"target="_blank">讨论论坛</a> diff --git a/translations/validators.de.xlf b/translations/validators.de.xlf index 270ee62e..4cb0caca 100644 --- a/translations/validators.de.xlf +++ b/translations/validators.de.xlf @@ -253,5 +253,23 @@ Dies ist keine gültige GTIN / EAN! + + + info_providers.validation.provider_id_without_key + Wenn Sie eine Anbieter-ID angeben, müssen Sie auch einen Info-Anbieter angeben oder beide entfernen. + + + + + info_providers.validation.provider_url_without_key + Wenn Sie eine Anbieter-URL angeben, müssen Sie auch einen Info-Anbieter angeben. + + + + + info_providers.validation.provider_key_without_id + Wenn Sie einen Info-Anbieter angeben, müssen Sie auch eine Anbieter-ID angeben oder beides entfernen. + + diff --git a/translations/validators.en.xlf b/translations/validators.en.xlf index 624c6a89..cc062ad2 100644 --- a/translations/validators.en.xlf +++ b/translations/validators.en.xlf @@ -253,5 +253,23 @@ This is not an valid GTIN / EAN! + + + info_providers.validation.provider_id_without_key + If you specify an provider ID, you also need to specify an info provider or remove both. + + + + + info_providers.validation.provider_url_without_key + If you specify an provider URL, you also need to specify an info provider. + + + + + info_providers.validation.provider_key_without_id + If you specify an info provider, you also need to provide an provider id, or remove both. + +