mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-05-10 15:12:12 +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
|
|
@ -3,14 +3,16 @@ import { generateCsrfHeaders } from "./csrf_protection_controller"
|
|||
|
||||
export default class extends Controller {
|
||||
static targets = ["progressBar", "progressText"]
|
||||
static values = {
|
||||
static values = {
|
||||
jobId: Number,
|
||||
partId: Number,
|
||||
researchUrl: String,
|
||||
researchAllUrl: String,
|
||||
markCompletedUrl: String,
|
||||
markSkippedUrl: String,
|
||||
markPendingUrl: String
|
||||
markPendingUrl: String,
|
||||
quickApplyUrl: String,
|
||||
quickApplyAllUrl: String
|
||||
}
|
||||
|
||||
connect() {
|
||||
|
|
@ -321,6 +323,94 @@ export default class extends Controller {
|
|||
}
|
||||
}
|
||||
|
||||
async quickApply(event) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
const partId = event.currentTarget.dataset.partId
|
||||
const providerKey = event.currentTarget.dataset.providerKey
|
||||
const providerId = event.currentTarget.dataset.providerId
|
||||
const button = event.currentTarget
|
||||
const originalHtml = button.innerHTML
|
||||
|
||||
button.disabled = true
|
||||
button.innerHTML = '<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) {
|
||||
this.showToast('success', message)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,10 +29,12 @@ use App\Entity\Parts\Part;
|
|||
use App\Entity\Parts\Supplier;
|
||||
use App\Entity\UserSystem\User;
|
||||
use App\Form\InfoProviderSystem\GlobalFieldMappingType;
|
||||
use App\Services\EntityMergers\Mergers\PartMerger;
|
||||
use App\Services\InfoProviderSystem\BulkInfoProviderService;
|
||||
use App\Services\InfoProviderSystem\DTOs\BulkSearchFieldMappingDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\BulkSearchPartResultsDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\BulkSearchResponseDTO;
|
||||
use App\Services\InfoProviderSystem\PartInfoRetriever;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
|
|
@ -515,6 +517,171 @@ class BulkInfoProviderImportController extends AbstractController
|
|||
}
|
||||
}
|
||||
|
||||
#[Route('/job/{jobId}/part/{partId}/quick-apply', name: 'bulk_info_provider_quick_apply', methods: ['POST'])]
|
||||
public function quickApply(
|
||||
int $jobId,
|
||||
int $partId,
|
||||
Request $request,
|
||||
PartInfoRetriever $infoRetriever,
|
||||
PartMerger $partMerger
|
||||
): JsonResponse {
|
||||
$job = $this->validateJobAccess($jobId);
|
||||
if (!$job) {
|
||||
return $this->createErrorResponse('Job not found or access denied', 404, ['job_id' => $jobId]);
|
||||
}
|
||||
|
||||
$part = $this->entityManager->getRepository(Part::class)->find($partId);
|
||||
if (!$part) {
|
||||
return $this->createErrorResponse('Part not found', 404, ['part_id' => $partId]);
|
||||
}
|
||||
|
||||
$this->denyAccessUnlessGranted('edit', $part);
|
||||
|
||||
// Get provider key/id from request body, or fall back to top search result
|
||||
$body = json_decode($request->getContent(), true) ?? [];
|
||||
$providerKey = $body['providerKey'] ?? null;
|
||||
$providerId = $body['providerId'] ?? null;
|
||||
|
||||
if (!$providerKey || !$providerId) {
|
||||
$searchResults = $job->getSearchResults($this->entityManager);
|
||||
foreach ($searchResults->partResults as $partResult) {
|
||||
if ($partResult->part->getId() === $partId) {
|
||||
$sorted = $partResult->getResultsSortedByPriority();
|
||||
if (!empty($sorted)) {
|
||||
$providerKey = $sorted[0]->searchResult->provider_key;
|
||||
$providerId = $sorted[0]->searchResult->provider_id;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$providerKey || !$providerId) {
|
||||
return $this->createErrorResponse('No search result available for this part', 400, ['part_id' => $partId]);
|
||||
}
|
||||
|
||||
try {
|
||||
$dto = $infoRetriever->getDetails($providerKey, $providerId);
|
||||
$providerPart = $infoRetriever->dtoToPart($dto);
|
||||
$partMerger->merge($part, $providerPart);
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
$job->markPartAsCompleted($partId);
|
||||
if ($job->isAllPartsCompleted() && !$job->isCompleted()) {
|
||||
$job->markAsCompleted();
|
||||
}
|
||||
$this->entityManager->flush();
|
||||
|
||||
return $this->json([
|
||||
'success' => true,
|
||||
'message' => sprintf('Applied provider data to "%s"', $part->getName()),
|
||||
'part_id' => $partId,
|
||||
'provider_key' => $providerKey,
|
||||
'provider_id' => $providerId,
|
||||
'progress' => $job->getProgressPercentage(),
|
||||
'completed_count' => $job->getCompletedPartsCount(),
|
||||
'total_count' => $job->getPartCount(),
|
||||
'job_completed' => $job->isCompleted(),
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return $this->createErrorResponse(
|
||||
'Quick apply failed: ' . $e->getMessage(),
|
||||
500,
|
||||
['job_id' => $jobId, 'part_id' => $partId, 'exception' => $e->getMessage()]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[Route('/job/{jobId}/quick-apply-all', name: 'bulk_info_provider_quick_apply_all', methods: ['POST'])]
|
||||
public function quickApplyAll(
|
||||
int $jobId,
|
||||
PartInfoRetriever $infoRetriever,
|
||||
PartMerger $partMerger
|
||||
): JsonResponse {
|
||||
set_time_limit(600);
|
||||
|
||||
$job = $this->validateJobAccess($jobId);
|
||||
if (!$job) {
|
||||
return $this->createErrorResponse('Job not found or access denied', 404, ['job_id' => $jobId]);
|
||||
}
|
||||
|
||||
$searchResults = $job->getSearchResults($this->entityManager);
|
||||
$applied = 0;
|
||||
$failed = 0;
|
||||
$noResults = 0;
|
||||
$errors = [];
|
||||
|
||||
foreach ($job->getJobParts() as $jobPart) {
|
||||
if ($jobPart->isCompleted() || $jobPart->isSkipped()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$part = $jobPart->getPart();
|
||||
|
||||
if (!$this->isGranted('edit', $part)) {
|
||||
$errors[] = sprintf('No edit permission for "%s"', $part->getName());
|
||||
$failed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find top search result for this part
|
||||
$providerKey = null;
|
||||
$providerId = null;
|
||||
foreach ($searchResults->partResults as $partResult) {
|
||||
if ($partResult->part->getId() === $part->getId()) {
|
||||
$sorted = $partResult->getResultsSortedByPriority();
|
||||
if (!empty($sorted)) {
|
||||
$providerKey = $sorted[0]->searchResult->provider_key;
|
||||
$providerId = $sorted[0]->searchResult->provider_id;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$providerKey || !$providerId) {
|
||||
$noResults++;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$dto = $infoRetriever->getDetails($providerKey, $providerId);
|
||||
$providerPart = $infoRetriever->dtoToPart($dto);
|
||||
$partMerger->merge($part, $providerPart);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$job->markPartAsCompleted($part->getId());
|
||||
$applied++;
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->error('Quick apply failed for part', [
|
||||
'part_id' => $part->getId(),
|
||||
'part_name' => $part->getName(),
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
$errors[] = sprintf('Failed for "%s": %s', $part->getName(), $e->getMessage());
|
||||
$failed++;
|
||||
}
|
||||
}
|
||||
|
||||
if ($job->isAllPartsCompleted() && !$job->isCompleted()) {
|
||||
$job->markAsCompleted();
|
||||
}
|
||||
$this->entityManager->flush();
|
||||
|
||||
return $this->json([
|
||||
'success' => true,
|
||||
'applied' => $applied,
|
||||
'failed' => $failed,
|
||||
'no_results' => $noResults,
|
||||
'errors' => $errors,
|
||||
'message' => sprintf('Applied to %d parts, %d failed, %d had no results', $applied, $failed, $noResults),
|
||||
'progress' => $job->getProgressPercentage(),
|
||||
'completed_count' => $job->getCompletedPartsCount(),
|
||||
'total_count' => $job->getPartCount(),
|
||||
'job_completed' => $job->isCompleted(),
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/job/{jobId}/research-all', name: 'bulk_info_provider_research_all', methods: ['POST'])]
|
||||
public function researchAllParts(int $jobId): JsonResponse
|
||||
{
|
||||
|
|
|
|||
|
|
@ -20,7 +20,9 @@
|
|||
'researchAllUrl': path('bulk_info_provider_research_all', {'jobId': job.id}),
|
||||
'markCompletedUrl': path('bulk_info_provider_mark_completed', {'jobId': job.id, 'partId': '__PART_ID__'}),
|
||||
'markSkippedUrl': path('bulk_info_provider_mark_skipped', {'jobId': job.id, 'partId': '__PART_ID__'}),
|
||||
'markPendingUrl': path('bulk_info_provider_mark_pending', {'jobId': job.id, 'partId': '__PART_ID__'})
|
||||
'markPendingUrl': path('bulk_info_provider_mark_pending', {'jobId': job.id, 'partId': '__PART_ID__'}),
|
||||
'quickApplyUrl': path('bulk_info_provider_quick_apply', {'jobId': job.id, 'partId': '__PART_ID__'}),
|
||||
'quickApplyAllUrl': path('bulk_info_provider_quick_apply_all', {'jobId': job.id})
|
||||
}) }}>
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
|
|
@ -95,6 +97,13 @@
|
|||
<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 %}
|
||||
</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>
|
||||
|
|
@ -214,6 +223,16 @@
|
|||
</td>
|
||||
<td>
|
||||
<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',
|
||||
{'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 %}>
|
||||
|
|
|
|||
|
|
@ -589,6 +589,296 @@ final class BulkInfoProviderImportControllerTest extends WebTestCase
|
|||
return $parts;
|
||||
}
|
||||
|
||||
public function testQuickApplyWithNonExistentJob(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$this->loginAsUser($client, 'admin');
|
||||
|
||||
$client->request('POST', '/en/tools/bulk_info_provider_import/job/999999/part/1/quick-apply');
|
||||
|
||||
$this->assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND);
|
||||
$response = json_decode($client->getResponse()->getContent(), true);
|
||||
$this->assertArrayHasKey('error', $response);
|
||||
}
|
||||
|
||||
public function testQuickApplyWithNonExistentPart(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$this->loginAsUser($client, 'admin');
|
||||
|
||||
$entityManager = $client->getContainer()->get('doctrine')->getManager();
|
||||
$userRepository = $entityManager->getRepository(User::class);
|
||||
$user = $userRepository->findOneBy(['name' => 'admin']);
|
||||
|
||||
if (!$user) {
|
||||
$this->markTestSkipped('Admin user not found in fixtures');
|
||||
}
|
||||
|
||||
$parts = $this->getTestParts($entityManager, [1]);
|
||||
|
||||
$job = new BulkInfoProviderImportJob();
|
||||
$job->setCreatedBy($user);
|
||||
foreach ($parts as $part) {
|
||||
$job->addPart($part);
|
||||
}
|
||||
$job->setStatus(BulkImportJobStatus::IN_PROGRESS);
|
||||
$job->setSearchResults(new BulkSearchResponseDTO([]));
|
||||
|
||||
$entityManager->persist($job);
|
||||
$entityManager->flush();
|
||||
|
||||
$client->request('POST', '/en/tools/bulk_info_provider_import/job/' . $job->getId() . '/part/999999/quick-apply');
|
||||
|
||||
$this->assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND);
|
||||
|
||||
// Clean up
|
||||
$entityManager->remove($job);
|
||||
$entityManager->flush();
|
||||
}
|
||||
|
||||
public function testQuickApplyWithNoSearchResults(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$this->loginAsUser($client, 'admin');
|
||||
|
||||
$entityManager = $client->getContainer()->get('doctrine')->getManager();
|
||||
$userRepository = $entityManager->getRepository(User::class);
|
||||
$user = $userRepository->findOneBy(['name' => 'admin']);
|
||||
|
||||
if (!$user) {
|
||||
$this->markTestSkipped('Admin user not found in fixtures');
|
||||
}
|
||||
|
||||
$parts = $this->getTestParts($entityManager, [1]);
|
||||
|
||||
$job = new BulkInfoProviderImportJob();
|
||||
$job->setCreatedBy($user);
|
||||
foreach ($parts as $part) {
|
||||
$job->addPart($part);
|
||||
}
|
||||
$job->setStatus(BulkImportJobStatus::IN_PROGRESS);
|
||||
// Empty search results - no provider results for any parts
|
||||
$job->setSearchResults(new BulkSearchResponseDTO([
|
||||
new BulkSearchPartResultsDTO(part: $parts[0], searchResults: [], errors: [])
|
||||
]));
|
||||
|
||||
$entityManager->persist($job);
|
||||
$entityManager->flush();
|
||||
|
||||
// Quick apply without providing providerKey/providerId and no search results available
|
||||
$client->request('POST', '/en/tools/bulk_info_provider_import/job/' . $job->getId() . '/part/1/quick-apply', [], [], [
|
||||
'CONTENT_TYPE' => 'application/json',
|
||||
], json_encode([]));
|
||||
|
||||
$this->assertResponseStatusCodeSame(Response::HTTP_BAD_REQUEST);
|
||||
$response = json_decode($client->getResponse()->getContent(), true);
|
||||
$this->assertFalse($response['success']);
|
||||
|
||||
// Clean up
|
||||
$entityManager->remove($job);
|
||||
$entityManager->flush();
|
||||
}
|
||||
|
||||
public function testQuickApplyAccessControl(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$this->loginAsUser($client, 'admin');
|
||||
|
||||
$entityManager = $client->getContainer()->get('doctrine')->getManager();
|
||||
$userRepository = $entityManager->getRepository(User::class);
|
||||
$admin = $userRepository->findOneBy(['name' => 'admin']);
|
||||
$readonly = $userRepository->findOneBy(['name' => 'noread']);
|
||||
|
||||
if (!$admin || !$readonly) {
|
||||
$this->markTestSkipped('Required test users not found in fixtures');
|
||||
}
|
||||
|
||||
$parts = $this->getTestParts($entityManager, [1]);
|
||||
|
||||
// Create job owned by readonly user
|
||||
$job = new BulkInfoProviderImportJob();
|
||||
$job->setCreatedBy($readonly);
|
||||
foreach ($parts as $part) {
|
||||
$job->addPart($part);
|
||||
}
|
||||
$job->setStatus(BulkImportJobStatus::IN_PROGRESS);
|
||||
$job->setSearchResults(new BulkSearchResponseDTO([]));
|
||||
|
||||
$entityManager->persist($job);
|
||||
$entityManager->flush();
|
||||
|
||||
// Admin tries to quick apply on readonly user's job - should fail
|
||||
$client->request('POST', '/en/tools/bulk_info_provider_import/job/' . $job->getId() . '/part/1/quick-apply');
|
||||
$this->assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND);
|
||||
|
||||
// Clean up
|
||||
$jobId = $job->getId();
|
||||
$entityManager->clear();
|
||||
$persistedJob = $entityManager->find(BulkInfoProviderImportJob::class, $jobId);
|
||||
if ($persistedJob) {
|
||||
$entityManager->remove($persistedJob);
|
||||
$entityManager->flush();
|
||||
}
|
||||
}
|
||||
|
||||
public function testQuickApplyAllWithNonExistentJob(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$this->loginAsUser($client, 'admin');
|
||||
|
||||
$client->request('POST', '/en/tools/bulk_info_provider_import/job/999999/quick-apply-all');
|
||||
|
||||
$this->assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND);
|
||||
$response = json_decode($client->getResponse()->getContent(), true);
|
||||
$this->assertArrayHasKey('error', $response);
|
||||
}
|
||||
|
||||
public function testQuickApplyAllWithNoResults(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$this->loginAsUser($client, 'admin');
|
||||
|
||||
$entityManager = $client->getContainer()->get('doctrine')->getManager();
|
||||
$userRepository = $entityManager->getRepository(User::class);
|
||||
$user = $userRepository->findOneBy(['name' => 'admin']);
|
||||
|
||||
if (!$user) {
|
||||
$this->markTestSkipped('Admin user not found in fixtures');
|
||||
}
|
||||
|
||||
$parts = $this->getTestParts($entityManager, [1, 2]);
|
||||
|
||||
$job = new BulkInfoProviderImportJob();
|
||||
$job->setCreatedBy($user);
|
||||
foreach ($parts as $part) {
|
||||
$job->addPart($part);
|
||||
}
|
||||
$job->setStatus(BulkImportJobStatus::IN_PROGRESS);
|
||||
// Empty search results for all parts
|
||||
$job->setSearchResults(new BulkSearchResponseDTO([
|
||||
new BulkSearchPartResultsDTO(part: $parts[0], searchResults: [], errors: []),
|
||||
new BulkSearchPartResultsDTO(part: $parts[1], searchResults: [], errors: []),
|
||||
]));
|
||||
|
||||
$entityManager->persist($job);
|
||||
$entityManager->flush();
|
||||
|
||||
$client->request('POST', '/en/tools/bulk_info_provider_import/job/' . $job->getId() . '/quick-apply-all');
|
||||
|
||||
$this->assertResponseStatusCodeSame(Response::HTTP_OK);
|
||||
$response = json_decode($client->getResponse()->getContent(), true);
|
||||
$this->assertTrue($response['success']);
|
||||
$this->assertEquals(0, $response['applied']);
|
||||
$this->assertEquals(2, $response['no_results']);
|
||||
|
||||
// Clean up
|
||||
$entityManager->remove($job);
|
||||
$entityManager->flush();
|
||||
}
|
||||
|
||||
public function testQuickApplyAllAccessControl(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$this->loginAsUser($client, 'admin');
|
||||
|
||||
$entityManager = $client->getContainer()->get('doctrine')->getManager();
|
||||
$userRepository = $entityManager->getRepository(User::class);
|
||||
$readonly = $userRepository->findOneBy(['name' => 'noread']);
|
||||
|
||||
if (!$readonly) {
|
||||
$this->markTestSkipped('Required test users not found in fixtures');
|
||||
}
|
||||
|
||||
$parts = $this->getTestParts($entityManager, [1]);
|
||||
|
||||
$job = new BulkInfoProviderImportJob();
|
||||
$job->setCreatedBy($readonly);
|
||||
foreach ($parts as $part) {
|
||||
$job->addPart($part);
|
||||
}
|
||||
$job->setStatus(BulkImportJobStatus::IN_PROGRESS);
|
||||
$job->setSearchResults(new BulkSearchResponseDTO([]));
|
||||
|
||||
$entityManager->persist($job);
|
||||
$entityManager->flush();
|
||||
|
||||
// Admin tries quick apply all on readonly user's job
|
||||
$client->request('POST', '/en/tools/bulk_info_provider_import/job/' . $job->getId() . '/quick-apply-all');
|
||||
$this->assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND);
|
||||
|
||||
// Clean up
|
||||
$jobId = $job->getId();
|
||||
$entityManager->clear();
|
||||
$persistedJob = $entityManager->find(BulkInfoProviderImportJob::class, $jobId);
|
||||
if ($persistedJob) {
|
||||
$entityManager->remove($persistedJob);
|
||||
$entityManager->flush();
|
||||
}
|
||||
}
|
||||
|
||||
public function testStep2TemplateRenderingWithQuickApplyButtons(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$this->loginAsUser($client, 'admin');
|
||||
|
||||
$entityManager = static::getContainer()->get('doctrine')->getManager();
|
||||
$partRepository = $entityManager->getRepository(Part::class);
|
||||
$part = $partRepository->find(1);
|
||||
|
||||
if (!$part) {
|
||||
$this->markTestSkipped('Test part with ID 1 not found in fixtures');
|
||||
}
|
||||
|
||||
$userRepository = $entityManager->getRepository(User::class);
|
||||
$user = $userRepository->findOneBy(['name' => 'admin']);
|
||||
|
||||
if (!$user) {
|
||||
$this->markTestSkipped('Admin user not found in fixtures');
|
||||
}
|
||||
|
||||
$job = new BulkInfoProviderImportJob();
|
||||
$job->setCreatedBy($user);
|
||||
$job->addPart($part);
|
||||
$job->setStatus(BulkImportJobStatus::IN_PROGRESS);
|
||||
|
||||
$searchResults = new BulkSearchResponseDTO(partResults: [
|
||||
new BulkSearchPartResultsDTO(part: $part,
|
||||
searchResults: [new BulkSearchPartResultDTO(
|
||||
searchResult: new SearchResultDTO(provider_key: 'test_provider', provider_id: 'TEST123', name: 'Test Component', description: 'Test description', manufacturer: 'Test Mfg', mpn: 'TEST-MPN', provider_url: 'https://example.com/test', preview_image_url: null),
|
||||
sourceField: 'mpn',
|
||||
sourceKeyword: 'TEST-MPN',
|
||||
)]
|
||||
)
|
||||
]);
|
||||
|
||||
$job->setSearchResults($searchResults);
|
||||
|
||||
$entityManager->persist($job);
|
||||
$entityManager->flush();
|
||||
|
||||
$client->request('GET', '/tools/bulk_info_provider_import/step2/' . $job->getId());
|
||||
|
||||
if ($client->getResponse()->isRedirect()) {
|
||||
$client->followRedirect();
|
||||
}
|
||||
|
||||
self::assertResponseStatusCodeSame(Response::HTTP_OK);
|
||||
|
||||
$content = (string) $client->getResponse()->getContent();
|
||||
// Verify quick apply buttons are rendered (Stimulus renders camelCase as kebab-case data attributes)
|
||||
$this->assertStringContainsString('quick-apply-url-value', $content);
|
||||
$this->assertStringContainsString('quick-apply-all-url-value', $content);
|
||||
|
||||
// Clean up
|
||||
$jobId = $job->getId();
|
||||
$entityManager->clear();
|
||||
$jobToRemove = $entityManager->find(BulkInfoProviderImportJob::class, $jobId);
|
||||
if ($jobToRemove) {
|
||||
$entityManager->remove($jobToRemove);
|
||||
$entityManager->flush();
|
||||
}
|
||||
}
|
||||
|
||||
public function testStep1Form(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
|
|
|
|||
|
|
@ -11103,6 +11103,30 @@ Please note, that you can not impersonate a disabled user. If you try you will g
|
|||
<target>Update Part</target>
|
||||
</segment>
|
||||
</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">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.prefetch_details</source>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue