diff --git a/assets/controllers/bulk_import_controller.js b/assets/controllers/bulk_import_controller.js index a04ff13e..49e4d60f 100644 --- a/assets/controllers/bulk_import_controller.js +++ b/assets/controllers/bulk_import_controller.js @@ -3,16 +3,14 @@ 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, - quickApplyUrl: String, - quickApplyAllUrl: String + markPendingUrl: String } connect() { @@ -121,11 +119,13 @@ export default class extends Controller { async markSkipped(event) { const partId = event.currentTarget.dataset.partId - + const reason = prompt('Reason for skipping (optional):') || '' + try { const url = this.markSkippedUrlValue.replace('__PART_ID__', partId) const data = await this.fetchWithErrorHandling(url, { - method: 'POST' + method: 'POST', + body: JSON.stringify({ reason }) }) if (data.success) { @@ -321,94 +321,6 @@ 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/assets/controllers/field_mapping_controller.js b/assets/controllers/field_mapping_controller.js index 50c19a0d..9c9c8ac6 100644 --- a/assets/controllers/field_mapping_controller.js +++ b/assets/controllers/field_mapping_controller.js @@ -70,13 +70,6 @@ export default class extends Controller { newFieldSelect.addEventListener('change', this.updateFieldOptions.bind(this)) } - // Auto-increment priority based on existing mappings - const nextPriority = this.getNextPriority() - const priorityInput = newRow.querySelector('input[name*="[priority]"]') - if (priorityInput) { - priorityInput.value = nextPriority - } - this.updateFieldOptions() this.updateAddButtonState() } @@ -126,18 +119,6 @@ export default class extends Controller { } } - getNextPriority() { - const priorityInputs = this.tbodyTarget.querySelectorAll('input[name*="[priority]"]') - let maxPriority = 0 - priorityInputs.forEach(input => { - const val = parseInt(input.value, 10) - if (!isNaN(val) && val > maxPriority) { - maxPriority = val - } - }) - return Math.min(maxPriority + 1, 10) - } - handleFormSubmit(event) { if (this.hasSubmitButtonTarget) { this.submitButtonTarget.disabled = true diff --git a/composer.lock b/composer.lock index f25634d4..d9795640 100644 --- a/composer.lock +++ b/composer.lock @@ -318,16 +318,16 @@ }, { "name": "amphp/hpack", - "version": "v3.2.2", + "version": "v3.2.1", "source": { "type": "git", "url": "https://github.com/amphp/hpack.git", - "reference": "291da27078e7e149a9bad4d08ff05bf7d81c89f4" + "reference": "4f293064b15682a2b178b1367ddf0b8b5feb0239" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/hpack/zipball/291da27078e7e149a9bad4d08ff05bf7d81c89f4", - "reference": "291da27078e7e149a9bad4d08ff05bf7d81c89f4", + "url": "https://api.github.com/repos/amphp/hpack/zipball/4f293064b15682a2b178b1367ddf0b8b5feb0239", + "reference": "4f293064b15682a2b178b1367ddf0b8b5feb0239", "shasum": "" }, "require": { @@ -336,7 +336,7 @@ "require-dev": { "amphp/php-cs-fixer-config": "^2", "http2jp/hpack-test-case": "^1", - "nikic/php-fuzzer": "^0.0.11", + "nikic/php-fuzzer": "^0.0.10", "phpunit/phpunit": "^7 | ^8 | ^9" }, "type": "library", @@ -380,7 +380,7 @@ ], "support": { "issues": "https://github.com/amphp/hpack/issues", - "source": "https://github.com/amphp/hpack/tree/v3.2.2" + "source": "https://github.com/amphp/hpack/tree/v3.2.1" }, "funding": [ { @@ -388,7 +388,7 @@ "type": "github" } ], - "time": "2026-05-03T19:28:59+00:00" + "time": "2024-03-21T19:00:16+00:00" }, { "name": "amphp/http", @@ -17374,16 +17374,16 @@ }, { "name": "symplify/easy-coding-standard", - "version": "13.1.2", + "version": "13.0.4", "source": { "type": "git", - "url": "https://github.com/easy-coding-standard/ecs.git", - "reference": "6d22473d1f36945884d8cb291777166020a47770" + "url": "https://github.com/easy-coding-standard/easy-coding-standard.git", + "reference": "5c7e7a07e5d6a98b9dd2e6fc0a9155efb7c166c8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/easy-coding-standard/ecs/zipball/6d22473d1f36945884d8cb291777166020a47770", - "reference": "6d22473d1f36945884d8cb291777166020a47770", + "url": "https://api.github.com/repos/easy-coding-standard/easy-coding-standard/zipball/5c7e7a07e5d6a98b9dd2e6fc0a9155efb7c166c8", + "reference": "5c7e7a07e5d6a98b9dd2e6fc0a9155efb7c166c8", "shasum": "" }, "require": { @@ -17418,8 +17418,8 @@ "static analysis" ], "support": { - "issues": "https://github.com/easy-coding-standard/ecs/issues", - "source": "https://github.com/easy-coding-standard/ecs/tree/13.1.2" + "issues": "https://github.com/easy-coding-standard/easy-coding-standard/issues", + "source": "https://github.com/easy-coding-standard/easy-coding-standard/tree/13.0.4" }, "funding": [ { @@ -17431,7 +17431,7 @@ "type": "github" } ], - "time": "2026-05-03T22:05:09+00:00" + "time": "2026-01-05T09:10:04+00:00" }, { "name": "tecnickcom/tc-lib-barcode", @@ -18479,16 +18479,16 @@ }, { "name": "web-auth/cose-lib", - "version": "4.5.2", + "version": "4.5.1", "source": { "type": "git", "url": "https://github.com/web-auth/cose-lib.git", - "reference": "5b38660f90070a8e45f3dbc9528ade3b608dd77d" + "reference": "3185af4df10dc537b65c140c315b88d15ae15b80" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/web-auth/cose-lib/zipball/5b38660f90070a8e45f3dbc9528ade3b608dd77d", - "reference": "5b38660f90070a8e45f3dbc9528ade3b608dd77d", + "url": "https://api.github.com/repos/web-auth/cose-lib/zipball/3185af4df10dc537b65c140c315b88d15ae15b80", + "reference": "3185af4df10dc537b65c140c315b88d15ae15b80", "shasum": "" }, "require": { @@ -18534,7 +18534,7 @@ ], "support": { "issues": "https://github.com/web-auth/cose-lib/issues", - "source": "https://github.com/web-auth/cose-lib/tree/4.5.2" + "source": "https://github.com/web-auth/cose-lib/tree/4.5.1" }, "funding": [ { @@ -18546,11 +18546,11 @@ "type": "patreon" } ], - "time": "2026-05-03T09:49:50+00:00" + "time": "2026-04-01T12:47:39+00:00" }, { "name": "web-auth/webauthn-lib", - "version": "5.3.2", + "version": "5.3.1", "source": { "type": "git", "url": "https://github.com/web-auth/webauthn-lib.git", @@ -18620,7 +18620,7 @@ "webauthn" ], "support": { - "source": "https://github.com/web-auth/webauthn-lib/tree/5.3.2" + "source": "https://github.com/web-auth/webauthn-lib/tree/5.3.1" }, "funding": [ { @@ -18636,16 +18636,16 @@ }, { "name": "web-auth/webauthn-symfony-bundle", - "version": "5.3.2", + "version": "5.3.1", "source": { "type": "git", "url": "https://github.com/web-auth/webauthn-symfony-bundle.git", - "reference": "1d20af98b50810e8776c52b671201b6bb73ea981" + "reference": "40f5ae033d4bea090aaa395b906ebfa7d7fe6055" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/web-auth/webauthn-symfony-bundle/zipball/1d20af98b50810e8776c52b671201b6bb73ea981", - "reference": "1d20af98b50810e8776c52b671201b6bb73ea981", + "url": "https://api.github.com/repos/web-auth/webauthn-symfony-bundle/zipball/40f5ae033d4bea090aaa395b906ebfa7d7fe6055", + "reference": "40f5ae033d4bea090aaa395b906ebfa7d7fe6055", "shasum": "" }, "require": { @@ -18703,7 +18703,7 @@ "webauthn" ], "support": { - "source": "https://github.com/web-auth/webauthn-symfony-bundle/tree/5.3.2" + "source": "https://github.com/web-auth/webauthn-symfony-bundle/tree/5.3.1" }, "funding": [ { @@ -18715,7 +18715,7 @@ "type": "patreon" } ], - "time": "2026-05-04T08:08:16+00:00" + "time": "2026-05-01T12:14:37+00:00" }, { "name": "webmozart/assert", diff --git a/public/kicad/footprints.txt b/public/kicad/footprints.txt index c893ad4b..551d7d9c 100644 --- a/public/kicad/footprints.txt +++ b/public/kicad/footprints.txt @@ -1,4 +1,4 @@ -# Generated on Mon May 4 05:40:05 UTC 2026 +# Generated on Mon Apr 13 05:19:27 UTC 2026 # This file contains all footprints available in the offical KiCAD library Audio_Module:Reverb_BTDR-1H Audio_Module:Reverb_BTDR-1V @@ -8366,7 +8366,6 @@ Converter_DCDC:Converter_DCDC_TRACO_TMR-1SM_SMD Converter_DCDC:Converter_DCDC_TRACO_TMR10-24xxWIR_48xxWIR_72xxWIR_THT Converter_DCDC:Converter_DCDC_TRACO_TMR2-xxxxWI_THT Converter_DCDC:Converter_DCDC_TRACO_TMR4-xxxxWI_THT -Converter_DCDC:Converter_DCDC_TRACO_TMR8-xxxxWI_THT Converter_DCDC:Converter_DCDC_TRACO_TMU3-05xx_12xx_THT Converter_DCDC:Converter_DCDC_TRACO_TMU3-24xx_THT Converter_DCDC:Converter_DCDC_TRACO_TMV-051xD_121xD_Dual_THT @@ -11979,8 +11978,6 @@ Package_DFN_QFN:VQFN-48-1EP_7x7mm_P0.5mm_EP4.2x4.2mm Package_DFN_QFN:VQFN-48-1EP_7x7mm_P0.5mm_EP4.2x4.2mm_ThermalVias Package_DFN_QFN:VQFN-48-1EP_7x7mm_P0.5mm_EP5.15x5.15mm Package_DFN_QFN:VQFN-48-1EP_7x7mm_P0.5mm_EP5.15x5.15mm_ThermalVias -Package_DFN_QFN:VQFN-52-1EP_6x6mm_P0.4mm_EP4.7x4.7mm -Package_DFN_QFN:VQFN-52-1EP_6x6mm_P0.4mm_EP4.7x4.7mm_ThermalVias Package_DFN_QFN:VQFN-56-1EP_8x8mm_P0.5mm_EP5.1x4.96mm Package_DFN_QFN:VQFN-56-1EP_8x8mm_P0.5mm_EP5.1x4.96mm_ThermalVias Package_DFN_QFN:VQFN-56-1EP_8x8mm_P0.5mm_EP5.5x5.06mm @@ -12031,8 +12028,6 @@ Package_DFN_QFN:WQFN-24-1EP_4x4mm_P0.5mm_EP2.45x2.45mm Package_DFN_QFN:WQFN-24-1EP_4x4mm_P0.5mm_EP2.45x2.45mm_ThermalVias Package_DFN_QFN:WQFN-24-1EP_4x4mm_P0.5mm_EP2.6x2.6mm Package_DFN_QFN:WQFN-24-1EP_4x4mm_P0.5mm_EP2.6x2.6mm_ThermalVias -Package_DFN_QFN:WQFN-28-1EP_3.5x5.5mm_P0.5mm_EP2.05x4.05mm -Package_DFN_QFN:WQFN-28-1EP_3.5x5.5mm_P0.5mm_EP2.05x4.05mm_ThermalVias Package_DFN_QFN:WQFN-28-1EP_4x4mm_P0.4mm_EP2.7x2.7mm Package_DFN_QFN:WQFN-28-1EP_4x4mm_P0.4mm_EP2.7x2.7mm_ThermalVias Package_DFN_QFN:WQFN-32-1EP_5x5mm_P0.5mm_EP3.1x3.1mm diff --git a/public/kicad/symbols.txt b/public/kicad/symbols.txt index f41aa152..34e246a5 100644 --- a/public/kicad/symbols.txt +++ b/public/kicad/symbols.txt @@ -1,4 +1,4 @@ -# Generated on Mon May 4 05:40:43 UTC 2026 +# Generated on Mon Apr 13 05:20:06 UTC 2026 # This file contains all symbols available in the offical KiCAD library 4xxx:14528 4xxx:14529 @@ -8845,7 +8845,6 @@ Interface_USB:CH343G Interface_USB:CH343P Interface_USB:CH344Q Interface_USB:CH9102F -Interface_USB:CP2102C-Axx-xQFN24 Interface_USB:CP2102N-Axx-xQFN20 Interface_USB:CP2102N-Axx-xQFN24 Interface_USB:CP2102N-Axx-xQFN28 diff --git a/src/Command/Migrations/DBPlatformConvertCommand.php b/src/Command/Migrations/DBPlatformConvertCommand.php index d1215da4..86052bf7 100644 --- a/src/Command/Migrations/DBPlatformConvertCommand.php +++ b/src/Command/Migrations/DBPlatformConvertCommand.php @@ -229,37 +229,24 @@ class DBPlatformConvertCommand extends Command if ($platform instanceof PostgreSQLPlatform) { $connection->executeStatement( - //See https://github.com/Part-DB/Part-DB-server/issues/1362 + //From: https://wiki.postgresql.org/wiki/Fixing_Sequences <<getResultCount() === 0 && ($job->isInProgress() || $job->isPending())) { + // Mark jobs with no results for deletion (failed searches) + if ($job->getResultCount() === 0 && $job->isInProgress()) { $jobsToDelete[] = $job; } } @@ -304,23 +297,9 @@ class BulkInfoProviderImportController extends AbstractController } } - // Refetch after cleanup and split into active vs finished - $allJobs = $this->entityManager->getRepository(BulkInfoProviderImportJob::class) - ->findBy([], ['createdAt' => 'DESC']); - - $activeJobs = []; - $finishedJobs = []; - foreach ($allJobs as $job) { - if ($job->isCompleted() || $job->isFailed() || $job->isStopped()) { - $finishedJobs[] = $job; - } else { - $activeJobs[] = $job; - } - } - return $this->render('info_providers/bulk_import/manage.html.twig', [ - 'active_jobs' => $activeJobs, - 'finished_jobs' => $finishedJobs, + 'jobs' => $this->entityManager->getRepository(BulkInfoProviderImportJob::class) + ->findBy([], ['createdAt' => 'DESC']) // Refetch after cleanup ]); } @@ -491,13 +470,22 @@ class BulkInfoProviderImportController extends AbstractController $fieldMappingDtos = $job->getFieldMappings(); $prefetchDetails = $job->isPrefetchDetails(); - $searchResultsDto = $this->bulkService->performBulkSearch([$part], $fieldMappingDtos, $prefetchDetails); + try { + $searchResultsDto = $this->bulkService->performBulkSearch([$part], $fieldMappingDtos, $prefetchDetails); + } catch (\Exception $searchException) { + // Handle "no search results found" as a normal case, not an error + if (str_contains($searchException->getMessage(), 'No search results found')) { + $searchResultsDto = null; + } else { + throw $searchException; + } + } // Update the job's search results for this specific part efficiently $this->updatePartSearchResults($job, $searchResultsDto[0] ?? null); // Prefetch details if requested - if ($prefetchDetails) { + if ($prefetchDetails && $searchResultsDto !== null) { $this->bulkService->prefetchDetailsForResults($searchResultsDto); } @@ -527,191 +515,6 @@ 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]); - } - - /** @var Part $part */ - $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 = $request->toArray(); - $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); - - //Persist part manufacturer and supplier if they are new, to avoid issues with detached entities during merge - //Do not footprints here, as it might pollute the database with unwanted formatting footprints from the provider, - $this->entityManager->persist($part->getManufacturer()); - foreach ($part->getOrderdetails() as $orderdetail) { - $this->entityManager->persist($orderdetail->getSupplier()); - } - - try { - $this->entityManager->flush(); - } catch (ORMInvalidArgumentException $exception) { - if (str_contains($exception->getMessage(), 'not configured to cascade persist operations')) { - throw new \RuntimeException('Failed to persist merged part, as it would create new datastructures! Review the provider data by yourself.'); - } - - throw $exception; // Re-throw if it's a different ORM error - } - - $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) { - $this->logger->error($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/src/Controller/PartController.php b/src/Controller/PartController.php index 735a48f8..ab424f50 100644 --- a/src/Controller/PartController.php +++ b/src/Controller/PartController.php @@ -36,7 +36,6 @@ use App\Entity\PriceInformations\Orderdetail; use App\Entity\ProjectSystem\Project; use App\Exceptions\AttachmentDownloadException; use App\Form\Part\PartBaseType; -use App\Form\Part\PartLotType; use App\Services\Attachments\AttachmentSubmitHandler; use App\Services\Attachments\PartPreviewGenerator; use App\Services\EntityMergers\Mergers\PartMerger; @@ -129,17 +128,6 @@ final class PartController extends AbstractController $table = null; } - // Build the add-lot form for the INFO page modal (only when not in time-travel mode) - $addLotForm = null; - if ($timeTravel_timestamp === null && $this->isGranted('edit', $part)) { - $newLot = new PartLot(); - $newLot->setPart($part); - $addLotForm = $this->createForm(PartLotType::class, $newLot, [ - 'measurement_unit' => $part->getPartUnit(), - 'action' => $this->generateUrl('part_lot_add', ['id' => $part->getID()]), - ]); - } - return $this->render( 'parts/info/show_part_info.html.twig', [ @@ -152,39 +140,10 @@ final class PartController extends AbstractController 'comment_params' => $this->partInfoSettings->extractParamsFromNotes ? $parameterExtractor->extractParameters($part->getComment()) : [], 'withdraw_add_helper' => $withdrawAddHelper, 'highlightLotId' => $request->query->getInt('highlightLot', 0), - 'add_lot_form' => $addLotForm, ] ); } - #[Route(path: '/{id}/add_lot', name: 'part_lot_add', methods: ['POST'])] - public function addLot(Part $part, Request $request, EntityManagerInterface $em): Response - { - $this->denyAccessUnlessGranted('edit', $part); - - $newLot = new PartLot(); - $newLot->setPart($part); - - $form = $this->createForm(PartLotType::class, $newLot, [ - 'measurement_unit' => $part->getPartUnit(), - ]); - - $form->handleRequest($request); - - if ($form->isSubmitted() && $form->isValid()) { - $em->persist($newLot); - $em->flush(); - $this->addFlash('success', 'part.edited_flash'); - return $this->redirectToRoute('part_info', [ - 'id' => $part->getID(), - 'highlightLot' => $newLot->getID(), - ]); - } - - $this->addFlash('error', 'part.created_flash.invalid'); - return $this->redirectToRoute('part_info', ['id' => $part->getID()]); - } - #[Route(path: '/{id}/edit', name: 'part_edit')] public function edit(Part $part, Request $request): Response { diff --git a/src/Services/InfoProviderSystem/BulkInfoProviderService.php b/src/Services/InfoProviderSystem/BulkInfoProviderService.php index 79420134..586fb873 100644 --- a/src/Services/InfoProviderSystem/BulkInfoProviderService.php +++ b/src/Services/InfoProviderSystem/BulkInfoProviderService.php @@ -46,6 +46,7 @@ final class BulkInfoProviderService } $partResults = []; + $hasAnyResults = false; // Group providers by batch capability $batchProviders = []; @@ -87,6 +88,7 @@ final class BulkInfoProviderService ); if (!empty($allResults)) { + $hasAnyResults = true; $searchResults = $this->formatSearchResults($allResults); } @@ -97,6 +99,10 @@ final class BulkInfoProviderService ); } + if (!$hasAnyResults) { + throw new \RuntimeException('No search results found for any of the selected parts'); + } + $response = new BulkSearchResponseDTO($partResults); // Prefetch details if requested diff --git a/src/Services/InfoProviderSystem/Providers/LCSCProvider.php b/src/Services/InfoProviderSystem/Providers/LCSCProvider.php index 5f251b43..8bdd776e 100755 --- a/src/Services/InfoProviderSystem/Providers/LCSCProvider.php +++ b/src/Services/InfoProviderSystem/Providers/LCSCProvider.php @@ -397,7 +397,6 @@ class LCSCProvider implements BatchInfoProviderInterface, URLHandlerInfoProvider // Now collect all results (like .then() in JavaScript) foreach ($responses as $keyword => $response) { try { - $keyword = (string) $keyword; $arr = $response->toArray(); // This waits for the response $results[$keyword] = $this->processSearchResponse($arr, $keyword); } catch (\Exception $e) { diff --git a/templates/info_providers/bulk_import/manage.html.twig b/templates/info_providers/bulk_import/manage.html.twig index b31dd650..9bbed906 100644 --- a/templates/info_providers/bulk_import/manage.html.twig +++ b/templates/info_providers/bulk_import/manage.html.twig @@ -22,130 +22,103 @@

- {% if active_jobs is empty and finished_jobs is empty %} + {% if jobs is not empty %} +
+ + + + + + + + + + + + + + + + {% for job in jobs %} + + + + + + + + + + + + {% endfor %} + +
{% trans %}info_providers.bulk_import.job_name{% endtrans %}{% trans %}info_providers.bulk_import.parts_count{% endtrans %}{% trans %}info_providers.bulk_import.results_count{% endtrans %}{% trans %}info_providers.bulk_import.progress{% endtrans %}{% trans %}info_providers.bulk_import.status{% endtrans %}{% trans %}info_providers.bulk_import.created_by{% endtrans %}{% trans %}info_providers.bulk_import.created_at{% endtrans %}{% trans %}info_providers.bulk_import.completed_at{% endtrans %}{% trans %}info_providers.bulk_import.action.label{% endtrans %}
+ {{ job.displayNameKey|trans(job.displayNameParams) }} - {{ job.formattedTimestamp }} + {% if job.isInProgress %} + Active + {% endif %} + {{ job.partCount }}{{ job.resultCount }} +
+
+
+
+
+ {{ job.progressPercentage }}% +
+ + {% trans with {'%current%': job.completedPartsCount + job.skippedPartsCount, '%total%': job.partCount} %}info_providers.bulk_import.progress_label{% endtrans %} + +
+ {% if job.isPending %} + {% trans %}info_providers.bulk_import.status.pending{% endtrans %} + {% elseif job.isInProgress %} + {% trans %}info_providers.bulk_import.status.in_progress{% endtrans %} + {% elseif job.isCompleted %} + {% trans %}info_providers.bulk_import.status.completed{% endtrans %} + {% elseif job.isStopped %} + {% trans %}info_providers.bulk_import.status.stopped{% endtrans %} + {% elseif job.isFailed %} + {% trans %}info_providers.bulk_import.status.failed{% endtrans %} + {% endif %} + {{ job.createdBy.fullName(true) }}{{ job.createdAt|format_datetime('short') }} + {% if job.completedAt %} + {{ job.completedAt|format_datetime('short') }} + {% else %} + - + {% endif %} + +
+ {% if job.isInProgress or job.isCompleted or job.isStopped %} + + {% trans %}info_providers.bulk_import.view_results{% endtrans %} + + {% endif %} + {% if job.canBeStopped %} + + {% endif %} + {% if job.isCompleted or job.isFailed or job.isStopped %} + + {% endif %} +
+
+
+ {% else %} - {% else %} - {# Active Jobs #} - {% if active_jobs is not empty %} -
- {% trans %}info_providers.bulk_import.active_jobs{% endtrans %} - {{ active_jobs|length }} -
- {{ _self.job_table(active_jobs, false) }} - {% endif %} - - {# Finished Jobs (History) #} - {% if finished_jobs is not empty %} -
- {% trans %}info_providers.bulk_import.finished_jobs{% endtrans %} - {{ finished_jobs|length }} -
- {{ _self.job_table(finished_jobs, true) }} - {% endif %} {% endif %} {% endblock %} - -{% macro job_table(jobs, showCompletedAt) %} -
- - - - - - - - - - - {% if showCompletedAt %} - - {% endif %} - - - - - {% for job in jobs %} - {{ _self.job_row(job, showCompletedAt) }} - {% endfor %} - -
{% trans %}info_providers.bulk_import.job_name{% endtrans %}{% trans %}info_providers.bulk_import.parts_count{% endtrans %}{% trans %}info_providers.bulk_import.results_count{% endtrans %}{% trans %}info_providers.bulk_import.progress{% endtrans %}{% trans %}info_providers.bulk_import.status{% endtrans %}{% trans %}info_providers.bulk_import.created_by{% endtrans %}{% trans %}info_providers.bulk_import.created_at{% endtrans %}{% trans %}info_providers.bulk_import.completed_at{% endtrans %}{% trans %}info_providers.bulk_import.action.label{% endtrans %}
-
-{% endmacro %} - -{% macro job_row(job, showCompletedAt) %} - {% set showCompletedAt = showCompletedAt|default(false) %} - - - #{{ job.id }} - {{ job.displayNameKey|trans(job.displayNameParams) }} -
{{ job.formattedTimestamp }} - - {{ job.partCount }} - {{ job.resultCount }} - -
-
-
-
-
- {{ job.progressPercentage }}% -
- - {% trans with {'%current%': job.completedPartsCount + job.skippedPartsCount, '%total%': job.partCount} %}info_providers.bulk_import.progress_label{% endtrans %} - - - - {% if job.isPending %} - {% trans %}info_providers.bulk_import.status.pending{% endtrans %} - {% elseif job.isInProgress %} - {% trans %}info_providers.bulk_import.status.in_progress{% endtrans %} - {% elseif job.isCompleted %} - {% trans %}info_providers.bulk_import.status.completed{% endtrans %} - {% elseif job.isStopped %} - {% trans %}info_providers.bulk_import.status.stopped{% endtrans %} - {% elseif job.isFailed %} - {% trans %}info_providers.bulk_import.status.failed{% endtrans %} - {% endif %} - - {{ job.createdBy.fullName(true) }} - {{ job.createdAt|format_datetime('short') }} - {% if showCompletedAt %} - - {% if job.completedAt %} - {{ job.completedAt|format_datetime('short') }} - {% else %} - - - {% endif %} - - {% endif %} - -
- {% if job.isInProgress or job.isCompleted or job.isStopped %} - - {% trans %}info_providers.bulk_import.view_results{% endtrans %} - - {% endif %} - {% if job.canBeStopped %} - - {% endif %} - {% if job.isCompleted or job.isFailed or job.isStopped %} - - {% endif %} -
- - -{% endmacro %} diff --git a/templates/info_providers/bulk_import/step2.html.twig b/templates/info_providers/bulk_import/step2.html.twig index e68202e0..559ca20a 100644 --- a/templates/info_providers/bulk_import/step2.html.twig +++ b/templates/info_providers/bulk_import/step2.html.twig @@ -9,42 +9,22 @@ {% block card_title %} {% trans %}info_providers.bulk_import.step2.title{% endtrans %} - #{{ job.id }} - {{ job.displayNameKey|trans(job.displayNameParams) }} + {{ job.displayNameKey|trans(job.displayNameParams) }} - {{ job.formattedTimestamp }} {% endblock %} {% block card_content %} - -
- - {% trans %}info_providers.bulk_import.back_to_jobs{% endtrans %} - - - {% trans %}info_providers.bulk_import.back_to_parts{% endtrans %} - -
- - {% if job.isCompleted %} - - {% endif %} -
-
#{{ job.id }} - {{ job.displayNameKey|trans(job.displayNameParams) }}
+
{{ job.displayNameKey|trans(job.displayNameParams) }} - {{ job.formattedTimestamp }}
{{ job.partCount }} {% trans %}info_providers.bulk_import.parts{% endtrans %} • {{ job.resultCount }} {% trans %}info_providers.bulk_import.results{% endtrans %} • @@ -115,13 +95,6 @@ {% trans %}info_providers.bulk_import.research.all_pending{% endtrans %} -
@@ -208,74 +181,39 @@ - {% set sortedResults = part_result.resultsSortedByPriority %} - {% for result in sortedResults %} + {% for result in part_result.searchResults %} {# @var result \App\Services\InfoProviderSystem\DTOs\BulkSearchPartResultDTO #} {% set dto = result.searchResult %} {% set localPart = result.localPart %} - {% set isTopResult = loop.first %} - + - {% if dto.preview_image_url %} - - {% endif %} + - {# Check for matches against source keyword (what was searched) #} - {% set sourceKw = result.sourceKeyword|default('')|lower %} - {% set nameMatch = sourceKw is not empty and dto.name is not null and dto.name|lower == sourceKw %} - {% set mpnMatch = sourceKw is not empty and dto.mpn is not null and dto.mpn|lower == sourceKw %} - {% set spnMatch = sourceKw is not empty and dto.provider_id is not null and dto.provider_id|lower == sourceKw %} - {% set anyMatch = nameMatch or mpnMatch or spnMatch %} {% if dto.provider_url is not null %} - {{ dto.name }} + {{ dto.name }} {% else %} - {{ dto.name }} - {% endif %} - {% if nameMatch %} - + {{ dto.name }} {% endif %} {% if dto.mpn is not null %} -
{{ dto.mpn }}
- {% if mpnMatch %} - MPN - {% endif %} +
{{ dto.mpn }} {% endif %} {{ dto.description }} {{ dto.manufacturer ?? '' }} {{ info_provider_label(dto.provider_key)|default(dto.provider_key) }} -
{{ dto.provider_id }} - {% if spnMatch %} - SPN - {% endif %} +
{{ dto.provider_id }} - {% if anyMatch %} - {% trans %}info_providers.bulk_import.match{% endtrans %} - {% else %} - {{ result.sourceField ?? 'unknown' }} - {% endif %} + {{ result.sourceField ?? 'unknown' }} {% if result.sourceKeyword %} -
{{ result.sourceKeyword }} - {% endif %} +
{{ result.sourceKeyword }} + {% endif %}
- {% 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/templates/parts/info/_add_lot_modal.html.twig b/templates/parts/info/_add_lot_modal.html.twig deleted file mode 100644 index b31f368f..00000000 --- a/templates/parts/info/_add_lot_modal.html.twig +++ /dev/null @@ -1,46 +0,0 @@ -{% if add_lot_form is not null %} -{% form_theme add_lot_form 'form/extended_bootstrap_layout.html.twig' %} - - -{% endif %} diff --git a/templates/parts/info/_part_lots.html.twig b/templates/parts/info/_part_lots.html.twig index 7e53aec1..70e5dc4e 100644 --- a/templates/parts/info/_part_lots.html.twig +++ b/templates/parts/info/_part_lots.html.twig @@ -3,7 +3,6 @@ {% include "parts/info/_withdraw_modal.html.twig" %} {% include "parts/info/_stocktake_modal.html.twig" %} -{% include "parts/info/_add_lot_modal.html.twig" %}
@@ -127,10 +126,3 @@
- -{% if add_lot_form is not null %} - -{% endif %} diff --git a/tests/Controller/BulkInfoProviderImportControllerTest.php b/tests/Controller/BulkInfoProviderImportControllerTest.php index d768f55c..ec3629fe 100644 --- a/tests/Controller/BulkInfoProviderImportControllerTest.php +++ b/tests/Controller/BulkInfoProviderImportControllerTest.php @@ -589,296 +589,6 @@ 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(); @@ -1025,9 +735,13 @@ final class BulkInfoProviderImportControllerTest extends WebTestCase new BulkSearchFieldMappingDTO('test_supplier_spn', ['test'], 2) ]; - // The service should return an empty response DTO when no results are found - $response = $bulkService->performBulkSearch([$part], $fieldMappings, false); - $this->assertFalse($response->hasAnyResults()); + // The service should be able to process the request and throw an exception when no results are found + try { + $bulkService->performBulkSearch([$part], $fieldMappings, false); + $this->fail('Expected RuntimeException to be thrown when no search results are found'); + } catch (\RuntimeException $e) { + $this->assertStringContainsString('No search results found', $e->getMessage()); + } } public function testBulkInfoProviderServiceBatchProcessing(): void @@ -1051,9 +765,13 @@ final class BulkInfoProviderImportControllerTest extends WebTestCase new BulkSearchFieldMappingDTO('empty', ['test'], 1) ]; - // The service should return an empty response DTO when no results are found - $response = $bulkService->performBulkSearch([$part], $fieldMappings, false); - $this->assertFalse($response->hasAnyResults()); + // The service should be able to process the request and throw an exception when no results are found + try { + $response = $bulkService->performBulkSearch([$part], $fieldMappings, false); + $this->fail('Expected RuntimeException to be thrown when no search results are found'); + } catch (\RuntimeException $e) { + $this->assertStringContainsString('No search results found', $e->getMessage()); + } } public function testBulkInfoProviderServicePrefetchDetails(): void @@ -1169,684 +887,4 @@ final class BulkInfoProviderImportControllerTest extends WebTestCase $entityManager->remove($job); $entityManager->flush(); } - - /** - * Helper to create a job with search results for testing. - */ - private function createJobWithSearchResults(object $entityManager, object $user, array $parts, string $status = 'in_progress'): BulkInfoProviderImportJob - { - $job = new BulkInfoProviderImportJob(); - $job->setCreatedBy($user); - foreach ($parts as $part) { - $job->addPart($part); - } - - $statusEnum = match ($status) { - 'pending' => BulkImportJobStatus::PENDING, - 'completed' => BulkImportJobStatus::COMPLETED, - 'stopped' => BulkImportJobStatus::STOPPED, - default => BulkImportJobStatus::IN_PROGRESS, - }; - $job->setStatus($statusEnum); - - // Create search results with a result per part - $partResults = []; - foreach ($parts as $part) { - $partResults[] = new BulkSearchPartResultsDTO( - part: $part, - searchResults: [ - new BulkSearchPartResultDTO( - searchResult: new SearchResultDTO( - provider_key: 'test_provider', - provider_id: 'TEST_' . $part->getId(), - name: $part->getName() ?? 'Test Part', - description: 'Test description', - manufacturer: 'Test Mfg', - mpn: 'MPN-' . $part->getId(), - provider_url: 'https://example.com/' . $part->getId(), - preview_image_url: null, - ), - sourceField: 'mpn', - sourceKeyword: $part->getName() ?? 'test', - localPart: null, - ), - ] - ); - } - - $job->setSearchResults(new BulkSearchResponseDTO($partResults)); - $entityManager->persist($job); - $entityManager->flush(); - - return $job; - } - - private function cleanupJob(object $entityManager, int $jobId): void - { - $entityManager->clear(); - $persistedJob = $entityManager->find(BulkInfoProviderImportJob::class, $jobId); - if ($persistedJob) { - $entityManager->remove($persistedJob); - $entityManager->flush(); - } - } - - public function testDeleteCompletedJob(): void - { - $client = static::createClient(); - $this->loginAsUser($client, 'admin'); - - $entityManager = $client->getContainer()->get('doctrine')->getManager(); - $user = $entityManager->getRepository(User::class)->findOneBy(['name' => 'admin']); - $parts = $this->getTestParts($entityManager, [1]); - - if (!$user || empty($parts)) { - $this->markTestSkipped('Required fixtures not found'); - } - - $job = $this->createJobWithSearchResults($entityManager, $user, $parts, 'completed'); - $jobId = $job->getId(); - - $client->request('DELETE', '/en/tools/bulk_info_provider_import/job/' . $jobId . '/delete'); - $this->assertResponseStatusCodeSame(Response::HTTP_OK); - $response = json_decode($client->getResponse()->getContent(), true); - $this->assertTrue($response['success']); - - // Verify job was deleted - $entityManager->clear(); - $this->assertNull($entityManager->find(BulkInfoProviderImportJob::class, $jobId)); - } - - public function testDeleteActiveJobFails(): void - { - $client = static::createClient(); - $this->loginAsUser($client, 'admin'); - - $entityManager = $client->getContainer()->get('doctrine')->getManager(); - $user = $entityManager->getRepository(User::class)->findOneBy(['name' => 'admin']); - $parts = $this->getTestParts($entityManager, [1]); - - if (!$user || empty($parts)) { - $this->markTestSkipped('Required fixtures not found'); - } - - $job = $this->createJobWithSearchResults($entityManager, $user, $parts, 'in_progress'); - $jobId = $job->getId(); - - $client->request('DELETE', '/en/tools/bulk_info_provider_import/job/' . $jobId . '/delete'); - $this->assertResponseStatusCodeSame(Response::HTTP_BAD_REQUEST); - - $this->cleanupJob($entityManager, $jobId); - } - - public function testDeleteNonExistentJob(): void - { - $client = static::createClient(); - $this->loginAsUser($client, 'admin'); - - $client->request('DELETE', '/en/tools/bulk_info_provider_import/job/999999/delete'); - $this->assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND); - } - - public function testStopInProgressJob(): void - { - $client = static::createClient(); - $this->loginAsUser($client, 'admin'); - - $entityManager = $client->getContainer()->get('doctrine')->getManager(); - $user = $entityManager->getRepository(User::class)->findOneBy(['name' => 'admin']); - $parts = $this->getTestParts($entityManager, [1]); - - if (!$user || empty($parts)) { - $this->markTestSkipped('Required fixtures not found'); - } - - $job = $this->createJobWithSearchResults($entityManager, $user, $parts, 'in_progress'); - $jobId = $job->getId(); - - $client->request('POST', '/en/tools/bulk_info_provider_import/job/' . $jobId . '/stop'); - $this->assertResponseStatusCodeSame(Response::HTTP_OK); - $response = json_decode($client->getResponse()->getContent(), true); - $this->assertTrue($response['success']); - - // Verify job is stopped - $entityManager->clear(); - $stoppedJob = $entityManager->find(BulkInfoProviderImportJob::class, $jobId); - $this->assertTrue($stoppedJob->isStopped()); - - $entityManager->remove($stoppedJob); - $entityManager->flush(); - } - - public function testStopNonExistentJob(): void - { - $client = static::createClient(); - $this->loginAsUser($client, 'admin'); - - $client->request('POST', '/en/tools/bulk_info_provider_import/job/999999/stop'); - $this->assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND); - } - - public function testMarkPartCompletedAutoCompletesJob(): void - { - $client = static::createClient(); - $this->loginAsUser($client, 'admin'); - - $entityManager = $client->getContainer()->get('doctrine')->getManager(); - $user = $entityManager->getRepository(User::class)->findOneBy(['name' => 'admin']); - $parts = $this->getTestParts($entityManager, [1]); - - if (!$user || empty($parts)) { - $this->markTestSkipped('Required fixtures not found'); - } - - $job = $this->createJobWithSearchResults($entityManager, $user, $parts); - $jobId = $job->getId(); - $partId = $parts[0]->getId(); - - $client->request('POST', '/en/tools/bulk_info_provider_import/job/' . $jobId . '/part/' . $partId . '/mark-completed'); - $this->assertResponseStatusCodeSame(Response::HTTP_OK); - $response = json_decode($client->getResponse()->getContent(), true); - $this->assertTrue($response['success']); - $this->assertEquals(1, $response['completed_count']); - $this->assertTrue($response['job_completed']); - - $this->cleanupJob($entityManager, $jobId); - } - - public function testMarkPartSkippedWithReason(): void - { - $client = static::createClient(); - $this->loginAsUser($client, 'admin'); - - $entityManager = $client->getContainer()->get('doctrine')->getManager(); - $user = $entityManager->getRepository(User::class)->findOneBy(['name' => 'admin']); - $parts = $this->getTestParts($entityManager, [1]); - - if (!$user || empty($parts)) { - $this->markTestSkipped('Required fixtures not found'); - } - - $job = $this->createJobWithSearchResults($entityManager, $user, $parts); - $jobId = $job->getId(); - $partId = $parts[0]->getId(); - - $client->request('POST', '/en/tools/bulk_info_provider_import/job/' . $jobId . '/part/' . $partId . '/mark-skipped', [ - 'reason' => 'Not needed' - ]); - $this->assertResponseStatusCodeSame(Response::HTTP_OK); - $response = json_decode($client->getResponse()->getContent(), true); - $this->assertTrue($response['success']); - $this->assertEquals(1, $response['skipped_count']); - - $this->cleanupJob($entityManager, $jobId); - } - - public function testMarkPartPendingAfterCompleted(): void - { - $client = static::createClient(); - $this->loginAsUser($client, 'admin'); - - $entityManager = $client->getContainer()->get('doctrine')->getManager(); - $user = $entityManager->getRepository(User::class)->findOneBy(['name' => 'admin']); - $parts = $this->getTestParts($entityManager, [1]); - - if (!$user || empty($parts)) { - $this->markTestSkipped('Required fixtures not found'); - } - - $job = $this->createJobWithSearchResults($entityManager, $user, $parts); - $jobId = $job->getId(); - $partId = $parts[0]->getId(); - - // First mark as completed - $client->request('POST', '/en/tools/bulk_info_provider_import/job/' . $jobId . '/part/' . $partId . '/mark-completed'); - $this->assertResponseStatusCodeSame(Response::HTTP_OK); - - // Then mark as pending again - $client->request('POST', '/en/tools/bulk_info_provider_import/job/' . $jobId . '/part/' . $partId . '/mark-pending'); - $this->assertResponseStatusCodeSame(Response::HTTP_OK); - $response = json_decode($client->getResponse()->getContent(), true); - $this->assertTrue($response['success']); - $this->assertEquals(0, $response['completed_count']); - - $this->cleanupJob($entityManager, $jobId); - } - - public function testMarkPartCompletedNonExistentJob(): void - { - $client = static::createClient(); - $this->loginAsUser($client, 'admin'); - - $client->request('POST', '/en/tools/bulk_info_provider_import/job/999999/part/1/mark-completed'); - $this->assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND); - } - - public function testQuickApplyWithValidJob(): void - { - $client = static::createClient(); - $this->loginAsUser($client, 'admin'); - - $entityManager = $client->getContainer()->get('doctrine')->getManager(); - $user = $entityManager->getRepository(User::class)->findOneBy(['name' => 'admin']); - $parts = $this->getTestParts($entityManager, [1]); - - if (!$user || empty($parts)) { - $this->markTestSkipped('Required fixtures not found'); - } - - $job = $this->createJobWithSearchResults($entityManager, $user, $parts); - $jobId = $job->getId(); - $partId = $parts[0]->getId(); - - // Quick apply will fail because test_provider doesn't exist, but it exercises the code path - $client->request('POST', '/en/tools/bulk_info_provider_import/job/' . $jobId . '/part/' . $partId . '/quick-apply', [], [], [ - 'CONTENT_TYPE' => 'application/json', - ], json_encode(['providerKey' => 'test_provider', 'providerId' => 'TEST_1'])); - - // Will get 500 because test_provider doesn't exist, which exercises the catch block - $this->assertResponseStatusCodeSame(Response::HTTP_INTERNAL_SERVER_ERROR); - $response = json_decode($client->getResponse()->getContent(), true); - $this->assertFalse($response['success']); - $this->assertStringContainsString('Quick apply failed', $response['error']); - - $this->cleanupJob($entityManager, $jobId); - } - - public function testQuickApplyFallsBackToTopResult(): void - { - $client = static::createClient(); - $this->loginAsUser($client, 'admin'); - - $entityManager = $client->getContainer()->get('doctrine')->getManager(); - $user = $entityManager->getRepository(User::class)->findOneBy(['name' => 'admin']); - $parts = $this->getTestParts($entityManager, [1]); - - if (!$user || empty($parts)) { - $this->markTestSkipped('Required fixtures not found'); - } - - $job = $this->createJobWithSearchResults($entityManager, $user, $parts); - $jobId = $job->getId(); - $partId = $parts[0]->getId(); - - // No providerKey/providerId in body - should fall back to top search result - $client->request('POST', '/en/tools/bulk_info_provider_import/job/' . $jobId . '/part/' . $partId . '/quick-apply', [], [], [ - 'CONTENT_TYPE' => 'application/json', - ], '{}'); - - // Will get 500 because test_provider doesn't exist, but exercises the fallback code path - $this->assertResponseStatusCodeSame(Response::HTTP_INTERNAL_SERVER_ERROR); - $response = json_decode($client->getResponse()->getContent(), true); - $this->assertStringContainsString('Quick apply failed', $response['error']); - - $this->cleanupJob($entityManager, $jobId); - } - - public function testQuickApplyEmptyResultsReturns400(): void - { - $client = static::createClient(); - $this->loginAsUser($client, 'admin'); - - $entityManager = $client->getContainer()->get('doctrine')->getManager(); - $user = $entityManager->getRepository(User::class)->findOneBy(['name' => 'admin']); - $parts = $this->getTestParts($entityManager, [1]); - - if (!$user || empty($parts)) { - $this->markTestSkipped('Required fixtures not found'); - } - - // Create job with empty search results - $job = new BulkInfoProviderImportJob(); - $job->setCreatedBy($user); - foreach ($parts as $part) { - $job->addPart($part); - } - $job->setStatus(BulkImportJobStatus::IN_PROGRESS); - $job->setSearchResults(new BulkSearchResponseDTO([ - new BulkSearchPartResultsDTO(part: $parts[0], searchResults: []) - ])); - $entityManager->persist($job); - $entityManager->flush(); - - $jobId = $job->getId(); - $partId = $parts[0]->getId(); - - // No provider specified and no search results - should return 400 - $client->request('POST', '/en/tools/bulk_info_provider_import/job/' . $jobId . '/part/' . $partId . '/quick-apply', [], [], [ - 'CONTENT_TYPE' => 'application/json', - ], '{}'); - $this->assertResponseStatusCodeSame(Response::HTTP_BAD_REQUEST); - $response = json_decode($client->getResponse()->getContent(), true); - $this->assertStringContainsString('No search result available', $response['error']); - - $this->cleanupJob($entityManager, $jobId); - } - - public function testQuickApplyNonExistentPart(): void - { - $client = static::createClient(); - $this->loginAsUser($client, 'admin'); - - $entityManager = $client->getContainer()->get('doctrine')->getManager(); - $user = $entityManager->getRepository(User::class)->findOneBy(['name' => 'admin']); - $parts = $this->getTestParts($entityManager, [1]); - - if (!$user || empty($parts)) { - $this->markTestSkipped('Required fixtures not found'); - } - - $job = $this->createJobWithSearchResults($entityManager, $user, $parts); - $jobId = $job->getId(); - - $client->request('POST', '/en/tools/bulk_info_provider_import/job/' . $jobId . '/part/999999/quick-apply'); - $this->assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND); - - $this->cleanupJob($entityManager, $jobId); - } - - public function testQuickApplyAllWithValidJob(): void - { - $client = static::createClient(); - $this->loginAsUser($client, 'admin'); - - $entityManager = $client->getContainer()->get('doctrine')->getManager(); - $user = $entityManager->getRepository(User::class)->findOneBy(['name' => 'admin']); - $parts = $this->getTestParts($entityManager, [1]); - - if (!$user || empty($parts)) { - $this->markTestSkipped('Required fixtures not found'); - } - - $job = $this->createJobWithSearchResults($entityManager, $user, $parts); - $jobId = $job->getId(); - - // Quick apply all - will fail for test_provider but exercises the code path - $client->request('POST', '/en/tools/bulk_info_provider_import/job/' . $jobId . '/quick-apply-all'); - $this->assertResponseStatusCodeSame(Response::HTTP_OK); - $response = json_decode($client->getResponse()->getContent(), true); - $this->assertTrue($response['success']); - // Should have 1 failed (because test_provider doesn't exist) - $this->assertEquals(1, $response['failed']); - $this->assertNotEmpty($response['errors']); - - $this->cleanupJob($entityManager, $jobId); - } - - public function testQuickApplyAllWithNoSearchResults(): void - { - $client = static::createClient(); - $this->loginAsUser($client, 'admin'); - - $entityManager = $client->getContainer()->get('doctrine')->getManager(); - $user = $entityManager->getRepository(User::class)->findOneBy(['name' => 'admin']); - $parts = $this->getTestParts($entityManager, [1]); - - if (!$user || empty($parts)) { - $this->markTestSkipped('Required fixtures not found'); - } - - // Create job with empty results - $job = new BulkInfoProviderImportJob(); - $job->setCreatedBy($user); - foreach ($parts as $part) { - $job->addPart($part); - } - $job->setStatus(BulkImportJobStatus::IN_PROGRESS); - $job->setSearchResults(new BulkSearchResponseDTO([ - new BulkSearchPartResultsDTO(part: $parts[0], searchResults: []) - ])); - $entityManager->persist($job); - $entityManager->flush(); - - $jobId = $job->getId(); - - $client->request('POST', '/en/tools/bulk_info_provider_import/job/' . $jobId . '/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(1, $response['no_results']); - - $this->cleanupJob($entityManager, $jobId); - } - - public function testQuickApplyAllNonExistentJob(): 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); - } - - public function testQuickApplyAllSkipsCompletedParts(): void - { - $client = static::createClient(); - $this->loginAsUser($client, 'admin'); - - $entityManager = $client->getContainer()->get('doctrine')->getManager(); - $user = $entityManager->getRepository(User::class)->findOneBy(['name' => 'admin']); - $parts = $this->getTestParts($entityManager, [1]); - - if (!$user || empty($parts)) { - $this->markTestSkipped('Required fixtures not found'); - } - - $job = $this->createJobWithSearchResults($entityManager, $user, $parts); - $jobId = $job->getId(); - - // Mark the part as completed first - $job->markPartAsCompleted($parts[0]->getId()); - $entityManager->flush(); - - // Quick apply all should skip already-completed parts - $client->request('POST', '/en/tools/bulk_info_provider_import/job/' . $jobId . '/quick-apply-all'); - $this->assertResponseStatusCodeSame(Response::HTTP_OK); - $response = json_decode($client->getResponse()->getContent(), true); - $this->assertEquals(0, $response['applied']); - $this->assertEquals(0, $response['failed']); - $this->assertEquals(0, $response['no_results']); - - $this->cleanupJob($entityManager, $jobId); - } - - public function testDeleteStoppedJob(): void - { - $client = static::createClient(); - $this->loginAsUser($client, 'admin'); - - $entityManager = $client->getContainer()->get('doctrine')->getManager(); - $user = $entityManager->getRepository(User::class)->findOneBy(['name' => 'admin']); - $parts = $this->getTestParts($entityManager, [1]); - - if (!$user || empty($parts)) { - $this->markTestSkipped('Required fixtures not found'); - } - - $job = $this->createJobWithSearchResults($entityManager, $user, $parts, 'stopped'); - $jobId = $job->getId(); - - $client->request('DELETE', '/en/tools/bulk_info_provider_import/job/' . $jobId . '/delete'); - $this->assertResponseStatusCodeSame(Response::HTTP_OK); - $response = json_decode($client->getResponse()->getContent(), true); - $this->assertTrue($response['success']); - - $entityManager->clear(); - $this->assertNull($entityManager->find(BulkInfoProviderImportJob::class, $jobId)); - } - - public function testManagePageSplitsActiveAndHistory(): void - { - $client = static::createClient(); - $this->loginAsUser($client, 'admin'); - - $entityManager = $client->getContainer()->get('doctrine')->getManager(); - $user = $entityManager->getRepository(User::class)->findOneBy(['name' => 'admin']); - $parts = $this->getTestParts($entityManager, [1]); - - if (!$user || empty($parts)) { - $this->markTestSkipped('Required fixtures not found'); - } - - // Create one active and one completed job - $activeJob = $this->createJobWithSearchResults($entityManager, $user, $parts, 'in_progress'); - $completedJob = $this->createJobWithSearchResults($entityManager, $user, $parts, 'completed'); - - $client->request('GET', '/en/tools/bulk_info_provider_import/manage'); - if ($client->getResponse()->isRedirect()) { - $client->followRedirect(); - } - $this->assertResponseStatusCodeSame(Response::HTTP_OK); - - $content = (string) $client->getResponse()->getContent(); - $this->assertStringContainsString('Active Jobs', $content); - $this->assertStringContainsString('History', $content); - - $this->cleanupJob($entityManager, $activeJob->getId()); - $this->cleanupJob($entityManager, $completedJob->getId()); - } - - public function testManagePageCleansUpPendingJobsWithNoResults(): void - { - $client = static::createClient(); - $this->loginAsUser($client, 'admin'); - - $entityManager = $client->getContainer()->get('doctrine')->getManager(); - $user = $entityManager->getRepository(User::class)->findOneBy(['name' => 'admin']); - $parts = $this->getTestParts($entityManager, [1]); - - if (!$user || empty($parts)) { - $this->markTestSkipped('Required fixtures not found'); - } - - // Create a pending job with no results (should be cleaned up) - $job = new BulkInfoProviderImportJob(); - $job->setCreatedBy($user); - foreach ($parts as $part) { - $job->addPart($part); - } - $job->setStatus(BulkImportJobStatus::PENDING); - $job->setSearchResults(new BulkSearchResponseDTO([])); - $entityManager->persist($job); - $entityManager->flush(); - $jobId = $job->getId(); - - // Visit manage page - should trigger cleanup - $client->request('GET', '/en/tools/bulk_info_provider_import/manage'); - if ($client->getResponse()->isRedirect()) { - $client->followRedirect(); - } - $this->assertResponseStatusCodeSame(Response::HTTP_OK); - - // Verify the stale job was cleaned up - $entityManager->clear(); - $this->assertNull($entityManager->find(BulkInfoProviderImportJob::class, $jobId)); - } - - public function testStep2RedirectsForNonExistentJob(): void - { - $client = static::createClient(); - $this->loginAsUser($client, 'admin'); - - $client->request('GET', '/en/tools/bulk_info_provider_import/step2/999999'); - - // Should redirect with error flash - $this->assertResponseRedirects(); - } - - public function testStep2WithOtherUsersJob(): void - { - $client = static::createClient(); - $this->loginAsUser($client, 'admin'); - - $entityManager = $client->getContainer()->get('doctrine')->getManager(); - $otherUser = $entityManager->getRepository(User::class)->findOneBy(['name' => 'noread']); - $parts = $this->getTestParts($entityManager, [1]); - - if (!$otherUser || empty($parts)) { - $this->markTestSkipped('Required fixtures not found'); - } - - $job = $this->createJobWithSearchResults($entityManager, $otherUser, $parts); - $jobId = $job->getId(); - - $client->request('GET', '/en/tools/bulk_info_provider_import/step2/' . $jobId); - - // Should redirect with access denied - $this->assertResponseRedirects(); - - $this->cleanupJob($entityManager, $jobId); - } - - public function testResearchPartNonExistentJob(): void - { - $client = static::createClient(); - $this->loginAsUser($client, 'admin'); - - $client->request('POST', '/en/tools/bulk_info_provider_import/job/999999/part/1/research'); - $this->assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND); - } - - public function testResearchPartNonExistentPart(): void - { - $client = static::createClient(); - $this->loginAsUser($client, 'admin'); - - $entityManager = $client->getContainer()->get('doctrine')->getManager(); - $user = $entityManager->getRepository(User::class)->findOneBy(['name' => 'admin']); - $parts = $this->getTestParts($entityManager, [1]); - - if (!$user || empty($parts)) { - $this->markTestSkipped('Required fixtures not found'); - } - - $job = $this->createJobWithSearchResults($entityManager, $user, $parts); - $jobId = $job->getId(); - - $client->request('POST', '/en/tools/bulk_info_provider_import/job/' . $jobId . '/part/999999/research'); - $this->assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND); - - $this->cleanupJob($entityManager, $jobId); - } - - public function testResearchAllNonExistentJob(): void - { - $client = static::createClient(); - $this->loginAsUser($client, 'admin'); - - $client->request('POST', '/en/tools/bulk_info_provider_import/job/999999/research-all'); - $this->assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND); - } - - public function testResearchAllWithAllPartsCompleted(): void - { - $client = static::createClient(); - $this->loginAsUser($client, 'admin'); - - $entityManager = $client->getContainer()->get('doctrine')->getManager(); - $user = $entityManager->getRepository(User::class)->findOneBy(['name' => 'admin']); - $parts = $this->getTestParts($entityManager, [1]); - - if (!$user || empty($parts)) { - $this->markTestSkipped('Required fixtures not found'); - } - - $job = $this->createJobWithSearchResults($entityManager, $user, $parts); - $jobId = $job->getId(); - - // Mark all parts as completed - foreach ($parts as $part) { - $job->markPartAsCompleted($part->getId()); - } - $entityManager->flush(); - - $client->request('POST', '/en/tools/bulk_info_provider_import/job/' . $jobId . '/research-all'); - $this->assertResponseStatusCodeSame(Response::HTTP_OK); - $response = json_decode($client->getResponse()->getContent(), true); - $this->assertTrue($response['success']); - $this->assertEquals(0, $response['researched_count']); - - $this->cleanupJob($entityManager, $jobId); - } } diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 0044edcc..d5f5c183 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -11211,96 +11211,6 @@ Please note, that you can not impersonate a disabled user. If you try you will g Update Part - - - info_providers.bulk_import.back_to_jobs - Back to Jobs - - - - - info_providers.bulk_import.back_to_parts - Back to Parts - - - - - info_providers.bulk_import.job_completed - Job completed! - - - - - info_providers.bulk_import.job_completed.description - All parts have been processed. You can review the results below or navigate back to the parts list. - - - - - info_providers.bulk_import.recommended - Top - - - - - info_providers.bulk_import.exact_match - Exact name match - - - - - info_providers.bulk_import.mpn_match - MPN matches - - - - - info_providers.bulk_import.active_jobs - Active Jobs - - - - - info_providers.bulk_import.finished_jobs - History - - - - - info_providers.bulk_import.spn_match - SPN matches - - - - - info_providers.bulk_import.match - Match - - - - - 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 diff --git a/yarn.lock b/yarn.lock index 4d43533a..3ea1a62b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2221,11 +2221,11 @@ bail@^2.0.0: integrity sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw== barcode-detector@^3.0.0, barcode-detector@^3.0.5: - version "3.1.3" - resolved "https://registry.yarnpkg.com/barcode-detector/-/barcode-detector-3.1.3.tgz#ea3224c8cf106b91e4f05a25ff0d798cb2b380a9" - integrity sha512-omL3/x26oU9jlR0gUQcGdXIjQtMlrUGKF7xRFO1RwrQkRkRU7WLz0mgQEsdUtYBm2uX3JH+HQLrKlyTS/BxZRw== + version "3.1.2" + resolved "https://registry.yarnpkg.com/barcode-detector/-/barcode-detector-3.1.2.tgz#8032a211ebb6cb5cc25724c5c56322c77ed02503" + integrity sha512-Q5kjXpVH5I3ItykNzbWmfWnNryFN1ZTWp10k9/PKJuS0RnoKR7jTrHEJODR4fn04bRomq7TJwie/Dr9fj/GoGQ== dependencies: - zxing-wasm "3.0.3" + zxing-wasm "3.0.2" base64-js@0.0.8: version "0.0.8" @@ -2238,9 +2238,9 @@ base64-js@^1.1.2, base64-js@^1.3.0: integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== baseline-browser-mapping@^2.10.12: - version "2.10.27" - resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.27.tgz#fee941c2a0b42cdf83c6427e4c830b1d0bdab2c3" - integrity sha512-zEs/ufmZoUd7WftKpKyXaT6RFxpQ5Qm9xytKRHvJfxFV9DFJkZph9RvJ1LcOUi0Z1ZVijMte65JbILeV+8QQEA== + version "2.10.25" + resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.25.tgz#183b45c0d3bdd12addb352426555fb3627eb022a" + integrity sha512-QO/VHsXCQdnzADMfmkeOPvHdIAkoB7i0/rGjINPJEetLx75hNttVWGQ/jycHUDP9zZ9rupbm60WRxcwViB0MiA== big.js@^5.2.2: version "5.2.2" @@ -2650,10 +2650,10 @@ cssesc@^3.0.0: resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== -cssnano-preset-default@^7.0.17: - version "7.0.17" - resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-7.0.17.tgz#6c239741cb8fd77556d0c55575de95c38f3a2537" - integrity sha512-11qO63A+czwguQFJCaTdICvbaxn0pJzz/XghLlv+OT7WyToDxAMR0Xb3/26/l0y0hQJywwNbj/SLSQlGBHE1OA== +cssnano-preset-default@^7.0.16: + version "7.0.16" + resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-7.0.16.tgz#b0c9576c87488dfa00388e3a0c8b27ee946aa352" + integrity sha512-W0hiFi/ca/u2OTptL11OdApaz1vh9jyfd2ku9dMjou6KdpdgbMTagaXHKNl5kaEyRSCu9GIIaPRp5YLdqRAZMw== dependencies: browserslist "^4.28.2" css-declaration-sorter "^7.2.0" @@ -2670,7 +2670,7 @@ cssnano-preset-default@^7.0.17: postcss-minify-font-values "^7.0.3" postcss-minify-gradients "^7.0.5" postcss-minify-params "^7.0.9" - postcss-minify-selectors "^7.1.2" + postcss-minify-selectors "^7.1.1" postcss-normalize-charset "^7.0.3" postcss-normalize-display-values "^7.0.3" postcss-normalize-positions "^7.0.4" @@ -2692,11 +2692,11 @@ cssnano-utils@^5.0.3: integrity sha512-ynIREMICLxkxm7e9bCR9sh75s4Q5drICi0ua1yxo5jH2XPBqSKkl4dOh4EbFqtUmnTMhRffHgYL0EKKkMjtJTg== cssnano@^7.0.4: - version "7.1.9" - resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-7.1.9.tgz#1e8b5db528ae7cb175da0197adfbc559170f845e" - integrity sha512-uPR75+5Dk/WJ/YSPR1/YDHdwMM9c5FsaARljfKWgeCKLKOtJ0we21xy/RcCjn53fZnD/f6yYEIZ8pu18+GnbNQ== + version "7.1.8" + resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-7.1.8.tgz#a6508bd13f3b206676c6594fa19ef45a89acc9dc" + integrity sha512-OGXtXqXmwEoIGfXM2QoD35vweUAtx+J8ZvLSZHOEV0Jv9Hs9ScTdGGjRzZXun5J4PEZhEoytKig2O2NR8NXxKw== dependencies: - cssnano-preset-default "^7.0.17" + cssnano-preset-default "^7.0.16" lilconfig "^3.1.3" csso@^5.0.5: @@ -3039,9 +3039,9 @@ fast-deep-equal@^3.1.3: integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== fast-uri@^3.0.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.1.tgz#dd085fec2494a2a33bac6e61277374669e1dd774" - integrity sha512-h2r7rcm6Ee/J8o0LD5djLuFVcfbZxhvho4vvsbeV0aMvXjUgqv4YpxpkEx0d68l6+IleVfLAdVEfhR7QNMkGHQ== + version "3.1.0" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.0.tgz#66eecff6c764c0df9b762e62ca7edcfb53b4edfa" + integrity sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA== fastest-levenshtein@1.0.16, fastest-levenshtein@^1.0.12, fastest-levenshtein@^1.0.16: version "1.0.16" @@ -4314,10 +4314,10 @@ postcss-minify-params@^7.0.9: cssnano-utils "^5.0.3" postcss-value-parser "^4.2.0" -postcss-minify-selectors@^7.1.2: - version "7.1.2" - resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-7.1.2.tgz#9cef2eb836fb95c49c11ea6d0921743c9d2f7eb3" - integrity sha512-aQtrEWKwqafNlExcKHQvPGsXR2+vlUqqJtf5XsCQcgsSb5PL4wlujWBYDJuWsP4UnQX1YHDHU8qRlD+1PzTQ+Q== +postcss-minify-selectors@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-7.1.1.tgz#2de3a0f9fc07d745d4164adc59865b3a0c9b1b87" + integrity sha512-MZWXwSTfcpmNVJIs7tddar/275a4/zT5nG9/gEndHPRZGTAQNpiSkk8s/dq+yZVX2jKfvVn1d5X8Z5SJHWnDoQ== dependencies: browserslist "^4.28.1" caniuse-api "^3.0.0" @@ -4466,9 +4466,9 @@ postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== postcss@^8.2.14, postcss@^8.4.40: - version "8.5.14" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.14.tgz#a66c2d7808fadf69ebb5b84a03f8bafd76c4919c" - integrity sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg== + version "8.5.13" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.13.tgz#6cfaf647f2e7ef69850208eccd849e0d3f65d420" + integrity sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag== dependencies: nanoid "^3.3.11" picocolors "^1.1.1" @@ -5017,7 +5017,7 @@ tslib@^2.8.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== -type-fest@^5.6.0: +type-fest@^5.5.0: version "5.6.0" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-5.6.0.tgz#502f7a003b7309e96a7e17052cc2ab2c7e5c7a31" integrity sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA== @@ -5345,10 +5345,10 @@ zwitch@^2.0.0, zwitch@^2.0.4: resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7" integrity sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A== -zxing-wasm@3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/zxing-wasm/-/zxing-wasm-3.0.3.tgz#f87a45f7f90420e0f8c1a587147384d1cfeb4759" - integrity sha512-DdOn/G5F+qvZELWeO5ZFFwcN611TfMybxPV0LUUoutUmiH2t47MZSB7gLV9O9YLhvudBdnzQNAoFOu4Xz8eOrQ== +zxing-wasm@3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/zxing-wasm/-/zxing-wasm-3.0.2.tgz#3c39821f4a5d20b02bc5bacaefe7e6725d9520dd" + integrity sha512-2YMAriaYHX9wrBY2k7H0epSo+dyCaCZg/vOtt+nEDXM9ul480gkXz/9SkwpOeHcD2H5qqDG8lWDSBwpTcZpa6w== dependencies: "@types/emscripten" "^1.41.5" - type-fest "^5.6.0" + type-fest "^5.5.0"