mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-05-16 00:11:35 +00:00
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.
This commit is contained in:
parent
343c078b7d
commit
319a455bf2
5 changed files with 593 additions and 3 deletions
|
|
@ -10,7 +10,9 @@ export default class extends Controller {
|
||||||
researchAllUrl: String,
|
researchAllUrl: String,
|
||||||
markCompletedUrl: String,
|
markCompletedUrl: String,
|
||||||
markSkippedUrl: String,
|
markSkippedUrl: String,
|
||||||
markPendingUrl: String
|
markPendingUrl: String,
|
||||||
|
quickApplyUrl: String,
|
||||||
|
quickApplyAllUrl: String
|
||||||
}
|
}
|
||||||
|
|
||||||
connect() {
|
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 = '<span class="spinner-border spinner-border-sm"></span> 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) {
|
showSuccessMessage(message) {
|
||||||
this.showToast('success', message)
|
this.showToast('success', message)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,10 +29,12 @@ use App\Entity\Parts\Part;
|
||||||
use App\Entity\Parts\Supplier;
|
use App\Entity\Parts\Supplier;
|
||||||
use App\Entity\UserSystem\User;
|
use App\Entity\UserSystem\User;
|
||||||
use App\Form\InfoProviderSystem\GlobalFieldMappingType;
|
use App\Form\InfoProviderSystem\GlobalFieldMappingType;
|
||||||
|
use App\Services\EntityMergers\Mergers\PartMerger;
|
||||||
use App\Services\InfoProviderSystem\BulkInfoProviderService;
|
use App\Services\InfoProviderSystem\BulkInfoProviderService;
|
||||||
use App\Services\InfoProviderSystem\DTOs\BulkSearchFieldMappingDTO;
|
use App\Services\InfoProviderSystem\DTOs\BulkSearchFieldMappingDTO;
|
||||||
use App\Services\InfoProviderSystem\DTOs\BulkSearchPartResultsDTO;
|
use App\Services\InfoProviderSystem\DTOs\BulkSearchPartResultsDTO;
|
||||||
use App\Services\InfoProviderSystem\DTOs\BulkSearchResponseDTO;
|
use App\Services\InfoProviderSystem\DTOs\BulkSearchResponseDTO;
|
||||||
|
use App\Services\InfoProviderSystem\PartInfoRetriever;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
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'])]
|
#[Route('/job/{jobId}/research-all', name: 'bulk_info_provider_research_all', methods: ['POST'])]
|
||||||
public function researchAllParts(int $jobId): JsonResponse
|
public function researchAllParts(int $jobId): JsonResponse
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,9 @@
|
||||||
'researchAllUrl': path('bulk_info_provider_research_all', {'jobId': job.id}),
|
'researchAllUrl': path('bulk_info_provider_research_all', {'jobId': job.id}),
|
||||||
'markCompletedUrl': path('bulk_info_provider_mark_completed', {'jobId': job.id, 'partId': '__PART_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__'}),
|
'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})
|
||||||
}) }}>
|
}) }}>
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -95,6 +97,13 @@
|
||||||
<span class="spinner-border spinner-border-sm me-1" style="display: none;" id="research-all-spinner"></span>
|
<span class="spinner-border spinner-border-sm me-1" style="display: none;" id="research-all-spinner"></span>
|
||||||
<i class="fas fa-search"></i> {% trans %}info_providers.bulk_import.research.all_pending{% endtrans %}
|
<i class="fas fa-search"></i> {% trans %}info_providers.bulk_import.research.all_pending{% endtrans %}
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" class="btn btn-success btn-sm"
|
||||||
|
data-action="click->bulk-import#quickApplyAll"
|
||||||
|
id="quick-apply-all-btn"
|
||||||
|
title="{% trans %}info_providers.bulk_import.quick_apply_all.tooltip{% endtrans %}">
|
||||||
|
<span class="spinner-border spinner-border-sm me-1" style="display: none;" id="quick-apply-all-spinner"></span>
|
||||||
|
<i class="fas fa-bolt"></i> {% trans %}info_providers.bulk_import.quick_apply_all{% endtrans %}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -214,6 +223,16 @@
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="btn-group-vertical btn-group-sm" role="group">
|
<div class="btn-group-vertical btn-group-sm" role="group">
|
||||||
|
{% if not isCompleted %}
|
||||||
|
<button type="button" class="btn btn-success"
|
||||||
|
data-action="click->bulk-import#quickApply"
|
||||||
|
data-part-id="{{ part.id }}"
|
||||||
|
data-provider-key="{{ dto.provider_key }}"
|
||||||
|
data-provider-id="{{ dto.provider_id }}"
|
||||||
|
title="{% trans %}info_providers.bulk_import.quick_apply.tooltip{% endtrans %}">
|
||||||
|
<i class="fas fa-bolt"></i> {% trans %}info_providers.bulk_import.quick_apply{% endtrans %}
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
{% set updateHref = path('info_providers_update_part',
|
{% set updateHref = path('info_providers_update_part',
|
||||||
{'id': part.id, 'providerKey': dto.provider_key, 'providerId': dto.provider_id}) ~ '?jobId=' ~ job.id %}
|
{'id': part.id, 'providerKey': dto.provider_key, 'providerId': dto.provider_id}) ~ '?jobId=' ~ job.id %}
|
||||||
<a class="btn btn-primary{% if isCompleted %} disabled{% endif %}" href="{% if not isCompleted %}{{ updateHref }}{% else %}#{% endif %}"{% if isCompleted %} aria-disabled="true"{% endif %}>
|
<a class="btn btn-primary{% if isCompleted %} disabled{% endif %}" href="{% if not isCompleted %}{{ updateHref }}{% else %}#{% endif %}"{% if isCompleted %} aria-disabled="true"{% endif %}>
|
||||||
|
|
|
||||||
|
|
@ -589,6 +589,296 @@ final class BulkInfoProviderImportControllerTest extends WebTestCase
|
||||||
return $parts;
|
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
|
public function testStep1Form(): void
|
||||||
{
|
{
|
||||||
$client = static::createClient();
|
$client = static::createClient();
|
||||||
|
|
|
||||||
|
|
@ -11103,6 +11103,30 @@ Please note, that you can not impersonate a disabled user. If you try you will g
|
||||||
<target>Update Part</target>
|
<target>Update Part</target>
|
||||||
</segment>
|
</segment>
|
||||||
</unit>
|
</unit>
|
||||||
|
<unit id="quick_apply_btn" name="info_providers.bulk_import.quick_apply">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>info_providers.bulk_import.quick_apply</source>
|
||||||
|
<target>Quick Apply</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
|
<unit id="quick_apply_tooltip" name="info_providers.bulk_import.quick_apply.tooltip">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>info_providers.bulk_import.quick_apply.tooltip</source>
|
||||||
|
<target>Apply this provider result to the part without opening the edit form</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
|
<unit id="quick_apply_all_btn" name="info_providers.bulk_import.quick_apply_all">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>info_providers.bulk_import.quick_apply_all</source>
|
||||||
|
<target>Apply All (Top Results)</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
|
<unit id="quick_apply_all_tooltip" name="info_providers.bulk_import.quick_apply_all.tooltip">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>info_providers.bulk_import.quick_apply_all.tooltip</source>
|
||||||
|
<target>Apply the top-ranked search result to all pending parts without individual review</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
<unit id="e_DDQ2u" name="info_providers.bulk_import.prefetch_details">
|
<unit id="e_DDQ2u" name="info_providers.bulk_import.prefetch_details">
|
||||||
<segment state="translated">
|
<segment state="translated">
|
||||||
<source>info_providers.bulk_import.prefetch_details</source>
|
<source>info_providers.bulk_import.prefetch_details</source>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue