Added an web page to quickly add a new part from a web URL

This commit is contained in:
Jan Böhmer 2026-02-01 18:18:58 +01:00
parent 722eb7ddab
commit 909cab0044
6 changed files with 124 additions and 5 deletions

View file

@ -30,6 +30,7 @@ use App\Form\InfoProviderSystem\PartSearchType;
use App\Services\InfoProviderSystem\ExistingPartFinder;
use App\Services\InfoProviderSystem\PartInfoRetriever;
use App\Services\InfoProviderSystem\ProviderRegistry;
use App\Services\InfoProviderSystem\Providers\GenericWebProvider;
use App\Settings\AppSettings;
use App\Settings\InfoProviderSystem\InfoProviderGeneralSettings;
use Doctrine\ORM\EntityManagerInterface;
@ -39,6 +40,7 @@ use Psr\Log\LoggerInterface;
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\UrlType;
use Symfony\Component\HttpClient\Exception\ClientException;
use Symfony\Component\HttpClient\Exception\TransportException;
use Symfony\Component\HttpFoundation\Request;
@ -208,4 +210,58 @@ class InfoProviderController extends AbstractController
'update_target' => $update_target
]);
}
#[Route('/from_url', name: 'info_providers_from_url')]
public function fromURL(Request $request, GenericWebProvider $provider): Response
{
$this->denyAccessUnlessGranted('@info_providers.create_parts');
if (!$provider->isActive()) {
$this->addFlash('error', "Generic Web Provider is not active. Please enable it in the provider settings.");
return $this->redirectToRoute('info_providers_list');
}
$formBuilder = $this->createFormBuilder();
$formBuilder->add('url', UrlType::class, [
'label' => 'info_providers.from_url.url.label',
'required' => true,
]);
$formBuilder->add('submit', SubmitType::class, [
'label' => 'info_providers.search.submit',
]);
$form = $formBuilder->getForm();
$form->handleRequest($request);
$partDetail = null;
if ($form->isSubmitted() && $form->isValid()) {
//Try to retrieve the part detail from the given URL
$url = $form->get('url')->getData();
try {
$searchResult = $this->infoRetriever->searchByKeyword(
keyword: $url,
providers: [$provider]
);
if (count($searchResult) === 0) {
$this->addFlash('warning', t('info_providers.from_url.no_part_found'));
} else {
$searchResult = $searchResult[0];
//Redirect to the part creation page with the found part detail
return $this->redirectToRoute('info_providers_create_part', [
'providerKey' => $searchResult->provider_key,
'providerId' => $searchResult->provider_id,
]);
}
} catch (ExceptionInterface $e) {
$this->addFlash('error', t('info_providers.search.error.general_exception', ['%type%' => (new \ReflectionClass($e))->getShortName()]));
}
}
return $this->render('info_providers/from_url/from_url.html.twig', [
'form' => $form,
'partDetail' => $partDetail,
]);
}
}

View file

@ -39,6 +39,8 @@ use App\Entity\UserSystem\User;
use App\Helpers\Trees\TreeViewNode;
use App\Services\Cache\UserCacheKeyGenerator;
use App\Services\ElementTypeNameGenerator;
use App\Services\InfoProviderSystem\Providers\GenericWebProvider;
use App\Settings\InfoProviderSystem\GenericWebProviderSettings;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Cache\ItemInterface;
@ -58,6 +60,7 @@ class ToolsTreeBuilder
protected UserCacheKeyGenerator $keyGenerator,
protected Security $security,
private readonly ElementTypeNameGenerator $elementTypeNameGenerator,
private readonly GenericWebProviderSettings $genericWebProviderSettings
) {
}
@ -147,6 +150,13 @@ class ToolsTreeBuilder
$this->urlGenerator->generate('info_providers_search')
))->setIcon('fa-treeview fa-fw fa-solid fa-cloud-arrow-down');
if ($this->genericWebProviderSettings->enabled) {
$nodes[] = (new TreeViewNode(
$this->translator->trans('info_providers.from_url.title'),
$this->urlGenerator->generate('info_providers_from_url')
))->setIcon('fa-treeview fa-fw fa-solid fa-book-atlas');
}
$nodes[] = (new TreeViewNode(
$this->translator->trans('info_providers.bulk_import.manage_jobs'),
$this->urlGenerator->generate('bulk_info_provider_manage')

View file

@ -30,7 +30,7 @@ use Jbtronics\SettingsBundle\Settings\SettingsParameter;
use Jbtronics\SettingsBundle\Settings\SettingsTrait;
use Symfony\Component\Translation\TranslatableMessage as TM;
#[Settings(label: new TM("settings.ips.generic_web_provider"), description: new TM("settings.ips.generic_web_provider.description"))]
#[Settings(name: "generic_web_provider", label: new TM("settings.ips.generic_web_provider"), description: new TM("settings.ips.generic_web_provider.description"))]
#[SettingsIcon("fa-plug")]
class GenericWebProviderSettings
{

View file

@ -10,9 +10,9 @@
<!-- <span class="navbar-toggler-icon"></span> -->
<i class="fas fa-folder-open fa-lg fa-fw"></i>
</button>
{% if is_granted("@tools.label_scanner") %}
{% if is_granted("@tools.label_scanner") %}
<a href="{{ path('scan_dialog') }}" class="navbar-toggler nav-link ms-3">
<i class="fas fa-camera-retro fa-fw"></i>
<i class="fas fa-camera-retro fa-fw"></i>
</a>
{% endif %}
</div>
@ -52,6 +52,14 @@
{% trans %}info_providers.search.title{% endtrans %}
</a>
</li>
{% if settings_instance('generic_web_provider').enabled %}
<li>
<a class="dropdown-item" href="{{ path('info_providers_from_url') }}">
<i class="fa-fw fa-solid fa-book-atlas"></i>
{% trans %}info_providers.from_url.title{% endtrans %}
</a>
</li>
{% endif %}
{% endif %}
{% if is_granted('@parts.import') %}
@ -69,7 +77,7 @@
{% if is_granted('@parts.read') %}
{{ search.search_form("navbar") }}
{# {% include "_navbar_search.html.twig" %} #}
{# {% include "_navbar_search.html.twig" %} #}
{% endif %}
@ -145,4 +153,4 @@
</ul>
</div>
</div>
</nav>
</nav>

View file

@ -0,0 +1,21 @@
{% extends "main_card.html.twig" %}
{% import "info_providers/providers.macro.html.twig" as providers_macro %}
{% import "helper.twig" as helper %}
{% block title %}
{% trans %}info_providers.from_url.title{% endtrans %}
{% endblock %}
{% block card_title %}
<i class="fas fa-book-atlas"></i> {% trans %}info_providers.from_url.title{% endtrans %}
{% endblock %}
{% block card_content %}
<p class="text-muted offset-3">{% trans %}info_providers.from_url.help{% endtrans %}</p>
{{ form_start(form) }}
{{ form_row(form.url) }}
{{ form_row(form.submit) }}
{{ form_end(form) }}
{% endblock %}

View file

@ -14334,5 +14334,29 @@ Buerklin-API Authentication server:
<target>When the provider is enabled, users can make requests to arbitary websites on behalf of the Part-DB server. Only enable this, if you are aware of the potential consequences.</target>
</segment>
</unit>
<unit id="IvIOYcn" name="info_providers.from_url.title">
<segment>
<source>info_providers.from_url.title</source>
<target>Create [part] from URL</target>
</segment>
</unit>
<unit id="QLL7vDC" name="info_providers.from_url.url.label">
<segment>
<source>info_providers.from_url.url.label</source>
<target>URL</target>
</segment>
</unit>
<unit id="JTbTQLl" name="info_providers.from_url.no_part_found">
<segment>
<source>info_providers.from_url.no_part_found</source>
<target>No part found from the given URL. Are you sure this is a valid shop URL?</target>
</segment>
</unit>
<unit id="xoSvJk0" name="info_providers.from_url.help">
<segment>
<source>info_providers.from_url.help</source>
<target>Creates a part based on the given URL. It tries to delegate it to an existing info provider if possible, otherwise it will be tried to extract rudimentary data from the webpage's metadata.</target>
</segment>
</unit>
</file>
</xliff>