From 319a455bf2ce6f6f67077a8b042e8c5f2f9d7903 Mon Sep 17 00:00:00 2001
From: Sebastian Almberg <83243306+Sebbeben@users.noreply.github.com>
Date: Wed, 11 Mar 2026 12:35:03 +0100
Subject: [PATCH] Add Quick Apply and Apply All buttons to bulk info provider
import
Adds the ability to apply provider search results to parts directly
from the bulk import step 2 page without navigating to individual
part edit forms. Includes per-result Quick Apply buttons and an
Apply All button for batch operations.
---
assets/controllers/bulk_import_controller.js | 94 +++++-
.../BulkInfoProviderImportController.php | 167 ++++++++++
.../bulk_import/step2.html.twig | 21 +-
.../BulkInfoProviderImportControllerTest.php | 290 ++++++++++++++++++
translations/messages.en.xlf | 24 ++
5 files changed, 593 insertions(+), 3 deletions(-)
diff --git a/assets/controllers/bulk_import_controller.js b/assets/controllers/bulk_import_controller.js
index 49e4d60f..c57f4c9b 100644
--- a/assets/controllers/bulk_import_controller.js
+++ b/assets/controllers/bulk_import_controller.js
@@ -3,14 +3,16 @@ import { generateCsrfHeaders } from "./csrf_protection_controller"
export default class extends Controller {
static targets = ["progressBar", "progressText"]
- static values = {
+ static values = {
jobId: Number,
partId: Number,
researchUrl: String,
researchAllUrl: String,
markCompletedUrl: String,
markSkippedUrl: String,
- markPendingUrl: String
+ markPendingUrl: String,
+ quickApplyUrl: String,
+ quickApplyAllUrl: String
}
connect() {
@@ -321,6 +323,94 @@ export default class extends Controller {
}
}
+ async quickApply(event) {
+ event.preventDefault()
+ event.stopPropagation()
+
+ const partId = event.currentTarget.dataset.partId
+ const providerKey = event.currentTarget.dataset.providerKey
+ const providerId = event.currentTarget.dataset.providerId
+ const button = event.currentTarget
+ const originalHtml = button.innerHTML
+
+ button.disabled = true
+ button.innerHTML = ' Applying...'
+
+ try {
+ const url = this.quickApplyUrlValue.replace('__PART_ID__', partId)
+ const data = await this.fetchWithErrorHandling(url, {
+ method: 'POST',
+ body: JSON.stringify({ providerKey, providerId })
+ }, 60000)
+
+ if (data.success) {
+ this.updateProgressDisplay(data)
+ this.showSuccessMessage(data.message || 'Part updated successfully')
+ sessionStorage.setItem('bulkImportScrollPosition', window.scrollY.toString())
+ window.location.reload()
+ } else {
+ this.showErrorMessage(data.error || 'Quick apply failed')
+ button.innerHTML = originalHtml
+ button.disabled = false
+ }
+ } catch (error) {
+ console.error('Error in quick apply:', error)
+ this.showErrorMessage(error.message || 'Quick apply failed')
+ button.innerHTML = originalHtml
+ button.disabled = false
+ }
+ }
+
+ async quickApplyAll(event) {
+ event.preventDefault()
+ event.stopPropagation()
+
+ if (!confirm('This will apply the top search result to all pending parts without individual review. Continue?')) {
+ return
+ }
+
+ const button = event.currentTarget
+ const spinner = document.getElementById('quick-apply-all-spinner')
+ const originalHtml = button.innerHTML
+
+ button.disabled = true
+ if (spinner) {
+ spinner.style.display = 'inline-block'
+ }
+
+ try {
+ const data = await this.fetchWithErrorHandling(this.quickApplyAllUrlValue, {
+ method: 'POST'
+ }, 300000)
+
+ if (data.success) {
+ this.updateProgressDisplay(data)
+
+ let message = data.message || 'Bulk apply completed'
+ if (data.errors && data.errors.length > 0) {
+ message += '\nErrors:\n' + data.errors.join('\n')
+ }
+
+ this.showSuccessMessage(message)
+ sessionStorage.setItem('bulkImportScrollPosition', window.scrollY.toString())
+ window.location.reload()
+ } else {
+ this.showErrorMessage(data.error || 'Bulk apply failed')
+ button.innerHTML = originalHtml
+ button.disabled = false
+ }
+ } catch (error) {
+ console.error('Error in quick apply all:', error)
+ this.showErrorMessage(error.message || 'Bulk apply failed')
+ button.innerHTML = originalHtml
+ button.disabled = false
+ } finally {
+ if (spinner) {
+ spinner.style.display = 'none'
+ }
+ }
+ }
+
showSuccessMessage(message) {
this.showToast('success', message)
}
diff --git a/src/Controller/BulkInfoProviderImportController.php b/src/Controller/BulkInfoProviderImportController.php
index 2d3dd7f6..05a698dd 100644
--- a/src/Controller/BulkInfoProviderImportController.php
+++ b/src/Controller/BulkInfoProviderImportController.php
@@ -29,10 +29,12 @@ use App\Entity\Parts\Part;
use App\Entity\Parts\Supplier;
use App\Entity\UserSystem\User;
use App\Form\InfoProviderSystem\GlobalFieldMappingType;
+use App\Services\EntityMergers\Mergers\PartMerger;
use App\Services\InfoProviderSystem\BulkInfoProviderService;
use App\Services\InfoProviderSystem\DTOs\BulkSearchFieldMappingDTO;
use App\Services\InfoProviderSystem\DTOs\BulkSearchPartResultsDTO;
use App\Services\InfoProviderSystem\DTOs\BulkSearchResponseDTO;
+use App\Services\InfoProviderSystem\PartInfoRetriever;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@@ -515,6 +517,171 @@ class BulkInfoProviderImportController extends AbstractController
}
}
+ #[Route('/job/{jobId}/part/{partId}/quick-apply', name: 'bulk_info_provider_quick_apply', methods: ['POST'])]
+ public function quickApply(
+ int $jobId,
+ int $partId,
+ Request $request,
+ PartInfoRetriever $infoRetriever,
+ PartMerger $partMerger
+ ): JsonResponse {
+ $job = $this->validateJobAccess($jobId);
+ if (!$job) {
+ return $this->createErrorResponse('Job not found or access denied', 404, ['job_id' => $jobId]);
+ }
+
+ $part = $this->entityManager->getRepository(Part::class)->find($partId);
+ if (!$part) {
+ return $this->createErrorResponse('Part not found', 404, ['part_id' => $partId]);
+ }
+
+ $this->denyAccessUnlessGranted('edit', $part);
+
+ // Get provider key/id from request body, or fall back to top search result
+ $body = json_decode($request->getContent(), true) ?? [];
+ $providerKey = $body['providerKey'] ?? null;
+ $providerId = $body['providerId'] ?? null;
+
+ if (!$providerKey || !$providerId) {
+ $searchResults = $job->getSearchResults($this->entityManager);
+ foreach ($searchResults->partResults as $partResult) {
+ if ($partResult->part->getId() === $partId) {
+ $sorted = $partResult->getResultsSortedByPriority();
+ if (!empty($sorted)) {
+ $providerKey = $sorted[0]->searchResult->provider_key;
+ $providerId = $sorted[0]->searchResult->provider_id;
+ }
+ break;
+ }
+ }
+ }
+
+ if (!$providerKey || !$providerId) {
+ return $this->createErrorResponse('No search result available for this part', 400, ['part_id' => $partId]);
+ }
+
+ try {
+ $dto = $infoRetriever->getDetails($providerKey, $providerId);
+ $providerPart = $infoRetriever->dtoToPart($dto);
+ $partMerger->merge($part, $providerPart);
+
+ $this->entityManager->flush();
+
+ $job->markPartAsCompleted($partId);
+ if ($job->isAllPartsCompleted() && !$job->isCompleted()) {
+ $job->markAsCompleted();
+ }
+ $this->entityManager->flush();
+
+ return $this->json([
+ 'success' => true,
+ 'message' => sprintf('Applied provider data to "%s"', $part->getName()),
+ 'part_id' => $partId,
+ 'provider_key' => $providerKey,
+ 'provider_id' => $providerId,
+ 'progress' => $job->getProgressPercentage(),
+ 'completed_count' => $job->getCompletedPartsCount(),
+ 'total_count' => $job->getPartCount(),
+ 'job_completed' => $job->isCompleted(),
+ ]);
+ } catch (\Exception $e) {
+ return $this->createErrorResponse(
+ 'Quick apply failed: ' . $e->getMessage(),
+ 500,
+ ['job_id' => $jobId, 'part_id' => $partId, 'exception' => $e->getMessage()]
+ );
+ }
+ }
+
+ #[Route('/job/{jobId}/quick-apply-all', name: 'bulk_info_provider_quick_apply_all', methods: ['POST'])]
+ public function quickApplyAll(
+ int $jobId,
+ PartInfoRetriever $infoRetriever,
+ PartMerger $partMerger
+ ): JsonResponse {
+ set_time_limit(600);
+
+ $job = $this->validateJobAccess($jobId);
+ if (!$job) {
+ return $this->createErrorResponse('Job not found or access denied', 404, ['job_id' => $jobId]);
+ }
+
+ $searchResults = $job->getSearchResults($this->entityManager);
+ $applied = 0;
+ $failed = 0;
+ $noResults = 0;
+ $errors = [];
+
+ foreach ($job->getJobParts() as $jobPart) {
+ if ($jobPart->isCompleted() || $jobPart->isSkipped()) {
+ continue;
+ }
+
+ $part = $jobPart->getPart();
+
+ if (!$this->isGranted('edit', $part)) {
+ $errors[] = sprintf('No edit permission for "%s"', $part->getName());
+ $failed++;
+ continue;
+ }
+
+ // Find top search result for this part
+ $providerKey = null;
+ $providerId = null;
+ foreach ($searchResults->partResults as $partResult) {
+ if ($partResult->part->getId() === $part->getId()) {
+ $sorted = $partResult->getResultsSortedByPriority();
+ if (!empty($sorted)) {
+ $providerKey = $sorted[0]->searchResult->provider_key;
+ $providerId = $sorted[0]->searchResult->provider_id;
+ }
+ break;
+ }
+ }
+
+ if (!$providerKey || !$providerId) {
+ $noResults++;
+ continue;
+ }
+
+ try {
+ $dto = $infoRetriever->getDetails($providerKey, $providerId);
+ $providerPart = $infoRetriever->dtoToPart($dto);
+ $partMerger->merge($part, $providerPart);
+ $this->entityManager->flush();
+
+ $job->markPartAsCompleted($part->getId());
+ $applied++;
+ } catch (\Exception $e) {
+ $this->logger->error('Quick apply failed for part', [
+ 'part_id' => $part->getId(),
+ 'part_name' => $part->getName(),
+ 'error' => $e->getMessage(),
+ ]);
+ $errors[] = sprintf('Failed for "%s": %s', $part->getName(), $e->getMessage());
+ $failed++;
+ }
+ }
+
+ if ($job->isAllPartsCompleted() && !$job->isCompleted()) {
+ $job->markAsCompleted();
+ }
+ $this->entityManager->flush();
+
+ return $this->json([
+ 'success' => true,
+ 'applied' => $applied,
+ 'failed' => $failed,
+ 'no_results' => $noResults,
+ 'errors' => $errors,
+ 'message' => sprintf('Applied to %d parts, %d failed, %d had no results', $applied, $failed, $noResults),
+ 'progress' => $job->getProgressPercentage(),
+ 'completed_count' => $job->getCompletedPartsCount(),
+ 'total_count' => $job->getPartCount(),
+ 'job_completed' => $job->isCompleted(),
+ ]);
+ }
+
#[Route('/job/{jobId}/research-all', name: 'bulk_info_provider_research_all', methods: ['POST'])]
public function researchAllParts(int $jobId): JsonResponse
{
diff --git a/templates/info_providers/bulk_import/step2.html.twig b/templates/info_providers/bulk_import/step2.html.twig
index 559ca20a..4a12be18 100644
--- a/templates/info_providers/bulk_import/step2.html.twig
+++ b/templates/info_providers/bulk_import/step2.html.twig
@@ -20,7 +20,9 @@
'researchAllUrl': path('bulk_info_provider_research_all', {'jobId': job.id}),
'markCompletedUrl': path('bulk_info_provider_mark_completed', {'jobId': job.id, 'partId': '__PART_ID__'}),
'markSkippedUrl': path('bulk_info_provider_mark_skipped', {'jobId': job.id, 'partId': '__PART_ID__'}),
- 'markPendingUrl': path('bulk_info_provider_mark_pending', {'jobId': job.id, 'partId': '__PART_ID__'})
+ 'markPendingUrl': path('bulk_info_provider_mark_pending', {'jobId': job.id, 'partId': '__PART_ID__'}),
+ 'quickApplyUrl': path('bulk_info_provider_quick_apply', {'jobId': job.id, 'partId': '__PART_ID__'}),
+ 'quickApplyAllUrl': path('bulk_info_provider_quick_apply_all', {'jobId': job.id})
}) }}>
@@ -95,6 +97,13 @@
{% trans %}info_providers.bulk_import.research.all_pending{% endtrans %}
+
@@ -214,6 +223,16 @@
+ {% if not isCompleted %}
+
+ {% endif %}
{% set updateHref = path('info_providers_update_part',
{'id': part.id, 'providerKey': dto.provider_key, 'providerId': dto.provider_id}) ~ '?jobId=' ~ job.id %}
diff --git a/tests/Controller/BulkInfoProviderImportControllerTest.php b/tests/Controller/BulkInfoProviderImportControllerTest.php
index ec3629fe..f35c5f92 100644
--- a/tests/Controller/BulkInfoProviderImportControllerTest.php
+++ b/tests/Controller/BulkInfoProviderImportControllerTest.php
@@ -589,6 +589,296 @@ final class BulkInfoProviderImportControllerTest extends WebTestCase
return $parts;
}
+ public function testQuickApplyWithNonExistentJob(): void
+ {
+ $client = static::createClient();
+ $this->loginAsUser($client, 'admin');
+
+ $client->request('POST', '/en/tools/bulk_info_provider_import/job/999999/part/1/quick-apply');
+
+ $this->assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND);
+ $response = json_decode($client->getResponse()->getContent(), true);
+ $this->assertArrayHasKey('error', $response);
+ }
+
+ public function testQuickApplyWithNonExistentPart(): void
+ {
+ $client = static::createClient();
+ $this->loginAsUser($client, 'admin');
+
+ $entityManager = $client->getContainer()->get('doctrine')->getManager();
+ $userRepository = $entityManager->getRepository(User::class);
+ $user = $userRepository->findOneBy(['name' => 'admin']);
+
+ if (!$user) {
+ $this->markTestSkipped('Admin user not found in fixtures');
+ }
+
+ $parts = $this->getTestParts($entityManager, [1]);
+
+ $job = new BulkInfoProviderImportJob();
+ $job->setCreatedBy($user);
+ foreach ($parts as $part) {
+ $job->addPart($part);
+ }
+ $job->setStatus(BulkImportJobStatus::IN_PROGRESS);
+ $job->setSearchResults(new BulkSearchResponseDTO([]));
+
+ $entityManager->persist($job);
+ $entityManager->flush();
+
+ $client->request('POST', '/en/tools/bulk_info_provider_import/job/' . $job->getId() . '/part/999999/quick-apply');
+
+ $this->assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND);
+
+ // Clean up
+ $entityManager->remove($job);
+ $entityManager->flush();
+ }
+
+ public function testQuickApplyWithNoSearchResults(): void
+ {
+ $client = static::createClient();
+ $this->loginAsUser($client, 'admin');
+
+ $entityManager = $client->getContainer()->get('doctrine')->getManager();
+ $userRepository = $entityManager->getRepository(User::class);
+ $user = $userRepository->findOneBy(['name' => 'admin']);
+
+ if (!$user) {
+ $this->markTestSkipped('Admin user not found in fixtures');
+ }
+
+ $parts = $this->getTestParts($entityManager, [1]);
+
+ $job = new BulkInfoProviderImportJob();
+ $job->setCreatedBy($user);
+ foreach ($parts as $part) {
+ $job->addPart($part);
+ }
+ $job->setStatus(BulkImportJobStatus::IN_PROGRESS);
+ // Empty search results - no provider results for any parts
+ $job->setSearchResults(new BulkSearchResponseDTO([
+ new BulkSearchPartResultsDTO(part: $parts[0], searchResults: [], errors: [])
+ ]));
+
+ $entityManager->persist($job);
+ $entityManager->flush();
+
+ // Quick apply without providing providerKey/providerId and no search results available
+ $client->request('POST', '/en/tools/bulk_info_provider_import/job/' . $job->getId() . '/part/1/quick-apply', [], [], [
+ 'CONTENT_TYPE' => 'application/json',
+ ], json_encode([]));
+
+ $this->assertResponseStatusCodeSame(Response::HTTP_BAD_REQUEST);
+ $response = json_decode($client->getResponse()->getContent(), true);
+ $this->assertFalse($response['success']);
+
+ // Clean up
+ $entityManager->remove($job);
+ $entityManager->flush();
+ }
+
+ public function testQuickApplyAccessControl(): void
+ {
+ $client = static::createClient();
+ $this->loginAsUser($client, 'admin');
+
+ $entityManager = $client->getContainer()->get('doctrine')->getManager();
+ $userRepository = $entityManager->getRepository(User::class);
+ $admin = $userRepository->findOneBy(['name' => 'admin']);
+ $readonly = $userRepository->findOneBy(['name' => 'noread']);
+
+ if (!$admin || !$readonly) {
+ $this->markTestSkipped('Required test users not found in fixtures');
+ }
+
+ $parts = $this->getTestParts($entityManager, [1]);
+
+ // Create job owned by readonly user
+ $job = new BulkInfoProviderImportJob();
+ $job->setCreatedBy($readonly);
+ foreach ($parts as $part) {
+ $job->addPart($part);
+ }
+ $job->setStatus(BulkImportJobStatus::IN_PROGRESS);
+ $job->setSearchResults(new BulkSearchResponseDTO([]));
+
+ $entityManager->persist($job);
+ $entityManager->flush();
+
+ // Admin tries to quick apply on readonly user's job - should fail
+ $client->request('POST', '/en/tools/bulk_info_provider_import/job/' . $job->getId() . '/part/1/quick-apply');
+ $this->assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND);
+
+ // Clean up
+ $jobId = $job->getId();
+ $entityManager->clear();
+ $persistedJob = $entityManager->find(BulkInfoProviderImportJob::class, $jobId);
+ if ($persistedJob) {
+ $entityManager->remove($persistedJob);
+ $entityManager->flush();
+ }
+ }
+
+ public function testQuickApplyAllWithNonExistentJob(): void
+ {
+ $client = static::createClient();
+ $this->loginAsUser($client, 'admin');
+
+ $client->request('POST', '/en/tools/bulk_info_provider_import/job/999999/quick-apply-all');
+
+ $this->assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND);
+ $response = json_decode($client->getResponse()->getContent(), true);
+ $this->assertArrayHasKey('error', $response);
+ }
+
+ public function testQuickApplyAllWithNoResults(): void
+ {
+ $client = static::createClient();
+ $this->loginAsUser($client, 'admin');
+
+ $entityManager = $client->getContainer()->get('doctrine')->getManager();
+ $userRepository = $entityManager->getRepository(User::class);
+ $user = $userRepository->findOneBy(['name' => 'admin']);
+
+ if (!$user) {
+ $this->markTestSkipped('Admin user not found in fixtures');
+ }
+
+ $parts = $this->getTestParts($entityManager, [1, 2]);
+
+ $job = new BulkInfoProviderImportJob();
+ $job->setCreatedBy($user);
+ foreach ($parts as $part) {
+ $job->addPart($part);
+ }
+ $job->setStatus(BulkImportJobStatus::IN_PROGRESS);
+ // Empty search results for all parts
+ $job->setSearchResults(new BulkSearchResponseDTO([
+ new BulkSearchPartResultsDTO(part: $parts[0], searchResults: [], errors: []),
+ new BulkSearchPartResultsDTO(part: $parts[1], searchResults: [], errors: []),
+ ]));
+
+ $entityManager->persist($job);
+ $entityManager->flush();
+
+ $client->request('POST', '/en/tools/bulk_info_provider_import/job/' . $job->getId() . '/quick-apply-all');
+
+ $this->assertResponseStatusCodeSame(Response::HTTP_OK);
+ $response = json_decode($client->getResponse()->getContent(), true);
+ $this->assertTrue($response['success']);
+ $this->assertEquals(0, $response['applied']);
+ $this->assertEquals(2, $response['no_results']);
+
+ // Clean up
+ $entityManager->remove($job);
+ $entityManager->flush();
+ }
+
+ public function testQuickApplyAllAccessControl(): void
+ {
+ $client = static::createClient();
+ $this->loginAsUser($client, 'admin');
+
+ $entityManager = $client->getContainer()->get('doctrine')->getManager();
+ $userRepository = $entityManager->getRepository(User::class);
+ $readonly = $userRepository->findOneBy(['name' => 'noread']);
+
+ if (!$readonly) {
+ $this->markTestSkipped('Required test users not found in fixtures');
+ }
+
+ $parts = $this->getTestParts($entityManager, [1]);
+
+ $job = new BulkInfoProviderImportJob();
+ $job->setCreatedBy($readonly);
+ foreach ($parts as $part) {
+ $job->addPart($part);
+ }
+ $job->setStatus(BulkImportJobStatus::IN_PROGRESS);
+ $job->setSearchResults(new BulkSearchResponseDTO([]));
+
+ $entityManager->persist($job);
+ $entityManager->flush();
+
+ // Admin tries quick apply all on readonly user's job
+ $client->request('POST', '/en/tools/bulk_info_provider_import/job/' . $job->getId() . '/quick-apply-all');
+ $this->assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND);
+
+ // Clean up
+ $jobId = $job->getId();
+ $entityManager->clear();
+ $persistedJob = $entityManager->find(BulkInfoProviderImportJob::class, $jobId);
+ if ($persistedJob) {
+ $entityManager->remove($persistedJob);
+ $entityManager->flush();
+ }
+ }
+
+ public function testStep2TemplateRenderingWithQuickApplyButtons(): void
+ {
+ $client = static::createClient();
+ $this->loginAsUser($client, 'admin');
+
+ $entityManager = static::getContainer()->get('doctrine')->getManager();
+ $partRepository = $entityManager->getRepository(Part::class);
+ $part = $partRepository->find(1);
+
+ if (!$part) {
+ $this->markTestSkipped('Test part with ID 1 not found in fixtures');
+ }
+
+ $userRepository = $entityManager->getRepository(User::class);
+ $user = $userRepository->findOneBy(['name' => 'admin']);
+
+ if (!$user) {
+ $this->markTestSkipped('Admin user not found in fixtures');
+ }
+
+ $job = new BulkInfoProviderImportJob();
+ $job->setCreatedBy($user);
+ $job->addPart($part);
+ $job->setStatus(BulkImportJobStatus::IN_PROGRESS);
+
+ $searchResults = new BulkSearchResponseDTO(partResults: [
+ new BulkSearchPartResultsDTO(part: $part,
+ searchResults: [new BulkSearchPartResultDTO(
+ searchResult: new SearchResultDTO(provider_key: 'test_provider', provider_id: 'TEST123', name: 'Test Component', description: 'Test description', manufacturer: 'Test Mfg', mpn: 'TEST-MPN', provider_url: 'https://example.com/test', preview_image_url: null),
+ sourceField: 'mpn',
+ sourceKeyword: 'TEST-MPN',
+ )]
+ )
+ ]);
+
+ $job->setSearchResults($searchResults);
+
+ $entityManager->persist($job);
+ $entityManager->flush();
+
+ $client->request('GET', '/tools/bulk_info_provider_import/step2/' . $job->getId());
+
+ if ($client->getResponse()->isRedirect()) {
+ $client->followRedirect();
+ }
+
+ self::assertResponseStatusCodeSame(Response::HTTP_OK);
+
+ $content = (string) $client->getResponse()->getContent();
+ // Verify quick apply buttons are rendered (Stimulus renders camelCase as kebab-case data attributes)
+ $this->assertStringContainsString('quick-apply-url-value', $content);
+ $this->assertStringContainsString('quick-apply-all-url-value', $content);
+
+ // Clean up
+ $jobId = $job->getId();
+ $entityManager->clear();
+ $jobToRemove = $entityManager->find(BulkInfoProviderImportJob::class, $jobId);
+ if ($jobToRemove) {
+ $entityManager->remove($jobToRemove);
+ $entityManager->flush();
+ }
+ }
+
public function testStep1Form(): void
{
$client = static::createClient();
diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf
index 180d9e5e..e926b0c6 100644
--- a/translations/messages.en.xlf
+++ b/translations/messages.en.xlf
@@ -11103,6 +11103,30 @@ Please note, that you can not impersonate a disabled user. If you try you will g
Update Part
+
+
+ info_providers.bulk_import.quick_apply
+ Quick Apply
+
+
+
+
+ info_providers.bulk_import.quick_apply.tooltip
+ Apply this provider result to the part without opening the edit form
+
+
+
+
+ info_providers.bulk_import.quick_apply_all
+ Apply All (Top Results)
+
+
+
+
+ info_providers.bulk_import.quick_apply_all.tooltip
+ Apply the top-ranked search result to all pending parts without individual review
+
+
info_providers.bulk_import.prefetch_details
|