mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-06-28 05:21:36 +00:00
Compare commits
5 commits
673d5b5e83
...
19d138632a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19d138632a | ||
|
|
83074a2403 | ||
|
|
1d1e3008aa | ||
|
|
ce2b7d11a9 | ||
|
|
0ddf4f903e |
17 changed files with 1779 additions and 225 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() {
|
||||
|
|
@ -119,13 +121,11 @@ 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',
|
||||
body: JSON.stringify({ reason })
|
||||
method: 'POST'
|
||||
})
|
||||
|
||||
if (data.success) {
|
||||
|
|
@ -321,6 +321,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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,6 +70,13 @@ 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()
|
||||
}
|
||||
|
|
@ -119,6 +126,18 @@ 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
|
||||
|
|
|
|||
58
composer.lock
generated
58
composer.lock
generated
|
|
@ -318,16 +318,16 @@
|
|||
},
|
||||
{
|
||||
"name": "amphp/hpack",
|
||||
"version": "v3.2.1",
|
||||
"version": "v3.2.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/amphp/hpack.git",
|
||||
"reference": "4f293064b15682a2b178b1367ddf0b8b5feb0239"
|
||||
"reference": "291da27078e7e149a9bad4d08ff05bf7d81c89f4"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/amphp/hpack/zipball/4f293064b15682a2b178b1367ddf0b8b5feb0239",
|
||||
"reference": "4f293064b15682a2b178b1367ddf0b8b5feb0239",
|
||||
"url": "https://api.github.com/repos/amphp/hpack/zipball/291da27078e7e149a9bad4d08ff05bf7d81c89f4",
|
||||
"reference": "291da27078e7e149a9bad4d08ff05bf7d81c89f4",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -336,7 +336,7 @@
|
|||
"require-dev": {
|
||||
"amphp/php-cs-fixer-config": "^2",
|
||||
"http2jp/hpack-test-case": "^1",
|
||||
"nikic/php-fuzzer": "^0.0.10",
|
||||
"nikic/php-fuzzer": "^0.0.11",
|
||||
"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.1"
|
||||
"source": "https://github.com/amphp/hpack/tree/v3.2.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -388,7 +388,7 @@
|
|||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2024-03-21T19:00:16+00:00"
|
||||
"time": "2026-05-03T19:28:59+00:00"
|
||||
},
|
||||
{
|
||||
"name": "amphp/http",
|
||||
|
|
@ -17374,16 +17374,16 @@
|
|||
},
|
||||
{
|
||||
"name": "symplify/easy-coding-standard",
|
||||
"version": "13.0.4",
|
||||
"version": "13.1.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/easy-coding-standard/easy-coding-standard.git",
|
||||
"reference": "5c7e7a07e5d6a98b9dd2e6fc0a9155efb7c166c8"
|
||||
"url": "https://github.com/easy-coding-standard/ecs.git",
|
||||
"reference": "6d22473d1f36945884d8cb291777166020a47770"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/easy-coding-standard/easy-coding-standard/zipball/5c7e7a07e5d6a98b9dd2e6fc0a9155efb7c166c8",
|
||||
"reference": "5c7e7a07e5d6a98b9dd2e6fc0a9155efb7c166c8",
|
||||
"url": "https://api.github.com/repos/easy-coding-standard/ecs/zipball/6d22473d1f36945884d8cb291777166020a47770",
|
||||
"reference": "6d22473d1f36945884d8cb291777166020a47770",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -17418,8 +17418,8 @@
|
|||
"static analysis"
|
||||
],
|
||||
"support": {
|
||||
"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"
|
||||
"issues": "https://github.com/easy-coding-standard/ecs/issues",
|
||||
"source": "https://github.com/easy-coding-standard/ecs/tree/13.1.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -17431,7 +17431,7 @@
|
|||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-01-05T09:10:04+00:00"
|
||||
"time": "2026-05-03T22:05:09+00:00"
|
||||
},
|
||||
{
|
||||
"name": "tecnickcom/tc-lib-barcode",
|
||||
|
|
@ -18479,16 +18479,16 @@
|
|||
},
|
||||
{
|
||||
"name": "web-auth/cose-lib",
|
||||
"version": "4.5.1",
|
||||
"version": "4.5.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/web-auth/cose-lib.git",
|
||||
"reference": "3185af4df10dc537b65c140c315b88d15ae15b80"
|
||||
"reference": "5b38660f90070a8e45f3dbc9528ade3b608dd77d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/web-auth/cose-lib/zipball/3185af4df10dc537b65c140c315b88d15ae15b80",
|
||||
"reference": "3185af4df10dc537b65c140c315b88d15ae15b80",
|
||||
"url": "https://api.github.com/repos/web-auth/cose-lib/zipball/5b38660f90070a8e45f3dbc9528ade3b608dd77d",
|
||||
"reference": "5b38660f90070a8e45f3dbc9528ade3b608dd77d",
|
||||
"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.1"
|
||||
"source": "https://github.com/web-auth/cose-lib/tree/4.5.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -18546,11 +18546,11 @@
|
|||
"type": "patreon"
|
||||
}
|
||||
],
|
||||
"time": "2026-04-01T12:47:39+00:00"
|
||||
"time": "2026-05-03T09:49:50+00:00"
|
||||
},
|
||||
{
|
||||
"name": "web-auth/webauthn-lib",
|
||||
"version": "5.3.1",
|
||||
"version": "5.3.2",
|
||||
"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.1"
|
||||
"source": "https://github.com/web-auth/webauthn-lib/tree/5.3.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -18636,16 +18636,16 @@
|
|||
},
|
||||
{
|
||||
"name": "web-auth/webauthn-symfony-bundle",
|
||||
"version": "5.3.1",
|
||||
"version": "5.3.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/web-auth/webauthn-symfony-bundle.git",
|
||||
"reference": "40f5ae033d4bea090aaa395b906ebfa7d7fe6055"
|
||||
"reference": "1d20af98b50810e8776c52b671201b6bb73ea981"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/web-auth/webauthn-symfony-bundle/zipball/40f5ae033d4bea090aaa395b906ebfa7d7fe6055",
|
||||
"reference": "40f5ae033d4bea090aaa395b906ebfa7d7fe6055",
|
||||
"url": "https://api.github.com/repos/web-auth/webauthn-symfony-bundle/zipball/1d20af98b50810e8776c52b671201b6bb73ea981",
|
||||
"reference": "1d20af98b50810e8776c52b671201b6bb73ea981",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -18703,7 +18703,7 @@
|
|||
"webauthn"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/web-auth/webauthn-symfony-bundle/tree/5.3.1"
|
||||
"source": "https://github.com/web-auth/webauthn-symfony-bundle/tree/5.3.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -18715,7 +18715,7 @@
|
|||
"type": "patreon"
|
||||
}
|
||||
],
|
||||
"time": "2026-05-01T12:14:37+00:00"
|
||||
"time": "2026-05-04T08:08:16+00:00"
|
||||
},
|
||||
{
|
||||
"name": "webmozart/assert",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Generated on Mon Apr 13 05:19:27 UTC 2026
|
||||
# Generated on Mon May 4 05:40:05 UTC 2026
|
||||
# This file contains all footprints available in the offical KiCAD library
|
||||
Audio_Module:Reverb_BTDR-1H
|
||||
Audio_Module:Reverb_BTDR-1V
|
||||
|
|
@ -8366,6 +8366,7 @@ 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
|
||||
|
|
@ -11978,6 +11979,8 @@ 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
|
||||
|
|
@ -12028,6 +12031,8 @@ 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
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Generated on Mon Apr 13 05:20:06 UTC 2026
|
||||
# Generated on Mon May 4 05:40:43 UTC 2026
|
||||
# This file contains all symbols available in the offical KiCAD library
|
||||
4xxx:14528
|
||||
4xxx:14529
|
||||
|
|
@ -8845,6 +8845,7 @@ 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
|
||||
|
|
|
|||
|
|
@ -229,24 +229,37 @@ class DBPlatformConvertCommand extends Command
|
|||
|
||||
if ($platform instanceof PostgreSQLPlatform) {
|
||||
$connection->executeStatement(
|
||||
//From: https://wiki.postgresql.org/wiki/Fixing_Sequences
|
||||
//See https://github.com/Part-DB/Part-DB-server/issues/1362
|
||||
<<<SQL
|
||||
SELECT 'SELECT SETVAL(' ||
|
||||
quote_literal(quote_ident(PGT.schemaname) || '.' || quote_ident(S.relname)) ||
|
||||
', COALESCE(MAX(' ||quote_ident(C.attname)|| '), 1) ) FROM ' ||
|
||||
quote_ident(PGT.schemaname)|| '.'||quote_ident(T.relname)|| ';'
|
||||
FROM pg_class AS S,
|
||||
pg_depend AS D,
|
||||
pg_class AS T,
|
||||
pg_attribute AS C,
|
||||
pg_tables AS PGT
|
||||
WHERE S.relkind = 'S'
|
||||
AND S.oid = D.objid
|
||||
AND D.refobjid = T.oid
|
||||
AND D.refobjid = C.attrelid
|
||||
AND D.refobjsubid = C.attnum
|
||||
AND T.relname = PGT.tablename
|
||||
ORDER BY S.relname;
|
||||
DO $$
|
||||
DECLARE
|
||||
rec RECORD;
|
||||
max_id BIGINT;
|
||||
seq TEXT;
|
||||
BEGIN
|
||||
FOR rec IN
|
||||
SELECT c.table_name
|
||||
FROM information_schema.columns c
|
||||
JOIN pg_tables t
|
||||
ON t.tablename = c.table_name AND t.schemaname = 'public'
|
||||
WHERE c.column_name = 'id'
|
||||
AND c.table_schema = 'public'
|
||||
LOOP
|
||||
BEGIN
|
||||
seq := pg_get_serial_sequence(rec.table_name, 'id');
|
||||
IF seq IS NOT NULL THEN
|
||||
EXECUTE format('SELECT MAX(id) FROM %I', rec.table_name) INTO max_id;
|
||||
IF max_id IS NOT NULL THEN
|
||||
PERFORM setval(seq, max_id);
|
||||
RAISE NOTICE 'Reset: %.id → %', rec.table_name, max_id;
|
||||
END IF;
|
||||
END IF;
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
RAISE NOTICE 'Skipped %: %', rec.table_name, SQLERRM;
|
||||
END;
|
||||
END LOOP;
|
||||
END;
|
||||
$$;
|
||||
SQL);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,11 +29,14 @@ 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 Doctrine\ORM\ORMInvalidArgumentException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
|
|
@ -66,6 +69,10 @@ class BulkInfoProviderImportController extends AbstractController
|
|||
{
|
||||
$dtos = [];
|
||||
foreach ($fieldMappings as $mapping) {
|
||||
// Skip entries where field is null/empty (e.g. user added a row but didn't select a field)
|
||||
if (empty($mapping['field'])) {
|
||||
continue;
|
||||
}
|
||||
$dtos[] = new BulkSearchFieldMappingDTO(field: $mapping['field'], providers: $mapping['providers'], priority: $mapping['priority'] ?? 1);
|
||||
}
|
||||
return $dtos;
|
||||
|
|
@ -276,8 +283,8 @@ class BulkInfoProviderImportController extends AbstractController
|
|||
$updatedJobs = true;
|
||||
}
|
||||
|
||||
// Mark jobs with no results for deletion (failed searches)
|
||||
if ($job->getResultCount() === 0 && $job->isInProgress()) {
|
||||
// Mark jobs with no results for deletion (failed searches or stale pending)
|
||||
if ($job->getResultCount() === 0 && ($job->isInProgress() || $job->isPending())) {
|
||||
$jobsToDelete[] = $job;
|
||||
}
|
||||
}
|
||||
|
|
@ -297,9 +304,23 @@ 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', [
|
||||
'jobs' => $this->entityManager->getRepository(BulkInfoProviderImportJob::class)
|
||||
->findBy([], ['createdAt' => 'DESC']) // Refetch after cleanup
|
||||
'active_jobs' => $activeJobs,
|
||||
'finished_jobs' => $finishedJobs,
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -470,22 +491,13 @@ class BulkInfoProviderImportController extends AbstractController
|
|||
$fieldMappingDtos = $job->getFieldMappings();
|
||||
$prefetchDetails = $job->isPrefetchDetails();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
$searchResultsDto = $this->bulkService->performBulkSearch([$part], $fieldMappingDtos, $prefetchDetails);
|
||||
|
||||
// Update the job's search results for this specific part efficiently
|
||||
$this->updatePartSearchResults($job, $searchResultsDto[0] ?? null);
|
||||
|
||||
// Prefetch details if requested
|
||||
if ($prefetchDetails && $searchResultsDto !== null) {
|
||||
if ($prefetchDetails) {
|
||||
$this->bulkService->prefetchDetailsForResults($searchResultsDto);
|
||||
}
|
||||
|
||||
|
|
@ -515,6 +527,191 @@ 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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ 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;
|
||||
|
|
@ -128,6 +129,17 @@ 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',
|
||||
[
|
||||
|
|
@ -140,10 +152,39 @@ 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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -46,7 +46,6 @@ final class BulkInfoProviderService
|
|||
}
|
||||
|
||||
$partResults = [];
|
||||
$hasAnyResults = false;
|
||||
|
||||
// Group providers by batch capability
|
||||
$batchProviders = [];
|
||||
|
|
@ -88,7 +87,6 @@ final class BulkInfoProviderService
|
|||
);
|
||||
|
||||
if (!empty($allResults)) {
|
||||
$hasAnyResults = true;
|
||||
$searchResults = $this->formatSearchResults($allResults);
|
||||
}
|
||||
|
||||
|
|
@ -99,10 +97,6 @@ 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
|
||||
|
|
|
|||
|
|
@ -397,6 +397,7 @@ 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) {
|
||||
|
|
|
|||
|
|
@ -22,103 +22,130 @@
|
|||
</p>
|
||||
</div>
|
||||
|
||||
{% if jobs is not empty %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans %}info_providers.bulk_import.job_name{% endtrans %}</th>
|
||||
<th>{% trans %}info_providers.bulk_import.parts_count{% endtrans %}</th>
|
||||
<th>{% trans %}info_providers.bulk_import.results_count{% endtrans %}</th>
|
||||
<th>{% trans %}info_providers.bulk_import.progress{% endtrans %}</th>
|
||||
<th>{% trans %}info_providers.bulk_import.status{% endtrans %}</th>
|
||||
<th>{% trans %}info_providers.bulk_import.created_by{% endtrans %}</th>
|
||||
<th>{% trans %}info_providers.bulk_import.created_at{% endtrans %}</th>
|
||||
<th>{% trans %}info_providers.bulk_import.completed_at{% endtrans %}</th>
|
||||
<th>{% trans %}info_providers.bulk_import.action.label{% endtrans %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for job in jobs %}
|
||||
<tr>
|
||||
<td>
|
||||
<strong>{{ job.displayNameKey|trans(job.displayNameParams) }} - {{ job.formattedTimestamp }}</strong>
|
||||
{% if job.isInProgress %}
|
||||
<span class="badge bg-info ms-2">Active</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ job.partCount }}</td>
|
||||
<td>{{ job.resultCount }}</td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="progress me-2" style="width: 80px; height: 12px;">
|
||||
<div class="progress-bar {% if job.isCompleted %}bg-success{% elseif job.isFailed %}bg-danger{% else %}bg-info{% endif %}"
|
||||
role="progressbar"
|
||||
style="width: {{ job.progressPercentage }}%"
|
||||
aria-valuenow="{{ job.progressPercentage }}"
|
||||
aria-valuemin="0" aria-valuemax="100">
|
||||
</div>
|
||||
</div>
|
||||
<small class="text-muted">{{ job.progressPercentage }}%</small>
|
||||
</div>
|
||||
<small class="text-muted">
|
||||
{% trans with {'%current%': job.completedPartsCount + job.skippedPartsCount, '%total%': job.partCount} %}info_providers.bulk_import.progress_label{% endtrans %}
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
{% if job.isPending %}
|
||||
<span class="badge bg-warning">{% trans %}info_providers.bulk_import.status.pending{% endtrans %}</span>
|
||||
{% elseif job.isInProgress %}
|
||||
<span class="badge bg-info">{% trans %}info_providers.bulk_import.status.in_progress{% endtrans %}</span>
|
||||
{% elseif job.isCompleted %}
|
||||
<span class="badge bg-success">{% trans %}info_providers.bulk_import.status.completed{% endtrans %}</span>
|
||||
{% elseif job.isStopped %}
|
||||
<span class="badge bg-secondary">{% trans %}info_providers.bulk_import.status.stopped{% endtrans %}</span>
|
||||
{% elseif job.isFailed %}
|
||||
<span class="badge bg-danger">{% trans %}info_providers.bulk_import.status.failed{% endtrans %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ job.createdBy.fullName(true) }}</td>
|
||||
<td>{{ job.createdAt|format_datetime('short') }}</td>
|
||||
<td>
|
||||
{% if job.completedAt %}
|
||||
{{ job.completedAt|format_datetime('short') }}
|
||||
{% else %}
|
||||
<span class="text-muted">-</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
{% if job.isInProgress or job.isCompleted or job.isStopped %}
|
||||
<a href="{{ path('bulk_info_provider_step2', {'jobId': job.id}) }}" class="btn btn-primary">
|
||||
<i class="fas fa-eye"></i> {% trans %}info_providers.bulk_import.view_results{% endtrans %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if job.canBeStopped %}
|
||||
<button type="button" class="btn btn-warning" data-action="click->bulk-job-manage#stopJob" data-job-id="{{ job.id }}">
|
||||
<i class="fas fa-stop"></i> {% trans %}info_providers.bulk_import.action.stop{% endtrans %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if job.isCompleted or job.isFailed or job.isStopped %}
|
||||
<button type="button" class="btn btn-danger" data-action="click->bulk-job-manage#deleteJob" data-job-id="{{ job.id }}">
|
||||
<i class="fas fa-trash"></i> {% trans %}info_providers.bulk_import.action.delete{% endtrans %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
{% if active_jobs is empty and finished_jobs is empty %}
|
||||
<div class="alert alert-info" role="alert">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
{% trans %}info_providers.bulk_import.no_jobs_found{% endtrans %}<br>
|
||||
{% trans %}info_providers.bulk_import.create_first_job{% endtrans %}
|
||||
</div>
|
||||
{% else %}
|
||||
{# Active Jobs #}
|
||||
{% if active_jobs is not empty %}
|
||||
<h5 class="mb-3">
|
||||
<i class="fas fa-tasks me-1"></i> {% trans %}info_providers.bulk_import.active_jobs{% endtrans %}
|
||||
<span class="badge bg-primary">{{ active_jobs|length }}</span>
|
||||
</h5>
|
||||
{{ _self.job_table(active_jobs, false) }}
|
||||
{% endif %}
|
||||
|
||||
{# Finished Jobs (History) #}
|
||||
{% if finished_jobs is not empty %}
|
||||
<h5 class="mb-3">
|
||||
<i class="fas fa-history me-1"></i> {% trans %}info_providers.bulk_import.finished_jobs{% endtrans %}
|
||||
<span class="badge bg-secondary">{{ finished_jobs|length }}</span>
|
||||
</h5>
|
||||
{{ _self.job_table(finished_jobs, true) }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% macro job_table(jobs, showCompletedAt) %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans %}info_providers.bulk_import.job_name{% endtrans %}</th>
|
||||
<th>{% trans %}info_providers.bulk_import.parts_count{% endtrans %}</th>
|
||||
<th>{% trans %}info_providers.bulk_import.results_count{% endtrans %}</th>
|
||||
<th>{% trans %}info_providers.bulk_import.progress{% endtrans %}</th>
|
||||
<th>{% trans %}info_providers.bulk_import.status{% endtrans %}</th>
|
||||
<th>{% trans %}info_providers.bulk_import.created_by{% endtrans %}</th>
|
||||
<th>{% trans %}info_providers.bulk_import.created_at{% endtrans %}</th>
|
||||
{% if showCompletedAt %}
|
||||
<th>{% trans %}info_providers.bulk_import.completed_at{% endtrans %}</th>
|
||||
{% endif %}
|
||||
<th>{% trans %}info_providers.bulk_import.action.label{% endtrans %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for job in jobs %}
|
||||
{{ _self.job_row(job, showCompletedAt) }}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro job_row(job, showCompletedAt) %}
|
||||
{% set showCompletedAt = showCompletedAt|default(false) %}
|
||||
<tr>
|
||||
<td>
|
||||
<strong>#{{ job.id }} - {{ job.displayNameKey|trans(job.displayNameParams) }}</strong>
|
||||
<br><small class="text-muted">{{ job.formattedTimestamp }}</small>
|
||||
</td>
|
||||
<td>{{ job.partCount }}</td>
|
||||
<td>{{ job.resultCount }}</td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="progress me-2" style="width: 80px; height: 12px;">
|
||||
<div class="progress-bar {% if job.isCompleted %}bg-success{% elseif job.isFailed %}bg-danger{% else %}bg-info{% endif %}"
|
||||
role="progressbar"
|
||||
style="width: {{ job.progressPercentage }}%"
|
||||
aria-valuenow="{{ job.progressPercentage }}"
|
||||
aria-valuemin="0" aria-valuemax="100">
|
||||
</div>
|
||||
</div>
|
||||
<small class="text-muted">{{ job.progressPercentage }}%</small>
|
||||
</div>
|
||||
<small class="text-muted">
|
||||
{% trans with {'%current%': job.completedPartsCount + job.skippedPartsCount, '%total%': job.partCount} %}info_providers.bulk_import.progress_label{% endtrans %}
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
{% if job.isPending %}
|
||||
<span class="badge bg-warning">{% trans %}info_providers.bulk_import.status.pending{% endtrans %}</span>
|
||||
{% elseif job.isInProgress %}
|
||||
<span class="badge bg-info">{% trans %}info_providers.bulk_import.status.in_progress{% endtrans %}</span>
|
||||
{% elseif job.isCompleted %}
|
||||
<span class="badge bg-success">{% trans %}info_providers.bulk_import.status.completed{% endtrans %}</span>
|
||||
{% elseif job.isStopped %}
|
||||
<span class="badge bg-secondary">{% trans %}info_providers.bulk_import.status.stopped{% endtrans %}</span>
|
||||
{% elseif job.isFailed %}
|
||||
<span class="badge bg-danger">{% trans %}info_providers.bulk_import.status.failed{% endtrans %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ job.createdBy.fullName(true) }}</td>
|
||||
<td>{{ job.createdAt|format_datetime('short') }}</td>
|
||||
{% if showCompletedAt %}
|
||||
<td>
|
||||
{% if job.completedAt %}
|
||||
{{ job.completedAt|format_datetime('short') }}
|
||||
{% else %}
|
||||
<span class="text-muted">-</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endif %}
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
{% if job.isInProgress or job.isCompleted or job.isStopped %}
|
||||
<a href="{{ path('bulk_info_provider_step2', {'jobId': job.id}) }}" class="btn btn-primary">
|
||||
<i class="fas fa-eye"></i> {% trans %}info_providers.bulk_import.view_results{% endtrans %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if job.canBeStopped %}
|
||||
<button type="button" class="btn btn-warning" data-action="click->bulk-job-manage#stopJob" data-job-id="{{ job.id }}">
|
||||
<i class="fas fa-stop"></i> {% trans %}info_providers.bulk_import.action.stop{% endtrans %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if job.isCompleted or job.isFailed or job.isStopped %}
|
||||
<button type="button" class="btn btn-danger" data-action="click->bulk-job-manage#deleteJob" data-job-id="{{ job.id }}">
|
||||
<i class="fas fa-trash"></i> {% trans %}info_providers.bulk_import.action.delete{% endtrans %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endmacro %}
|
||||
|
|
|
|||
|
|
@ -9,22 +9,42 @@
|
|||
|
||||
{% block card_title %}
|
||||
<i class="fas fa-search"></i> {% trans %}info_providers.bulk_import.step2.title{% endtrans %}
|
||||
<span class="badge bg-secondary">{{ job.displayNameKey|trans(job.displayNameParams) }} - {{ job.formattedTimestamp }}</span>
|
||||
<span class="badge bg-secondary">#{{ job.id }} - {{ job.displayNameKey|trans(job.displayNameParams) }}</span>
|
||||
{% endblock %}
|
||||
|
||||
{% block card_content %}
|
||||
|
||||
<!-- Navigation -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<a href="{{ path('bulk_info_provider_manage') }}" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="fas fa-arrow-left"></i> {% trans %}info_providers.bulk_import.back_to_jobs{% endtrans %}
|
||||
</a>
|
||||
<a href="{{ path('parts_show_all') }}" class="btn btn-outline-primary btn-sm">
|
||||
<i class="fas fa-list"></i> {% trans %}info_providers.bulk_import.back_to_parts{% endtrans %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% if job.isCompleted %}
|
||||
<div class="alert alert-success mb-3" role="alert">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
<strong>{% trans %}info_providers.bulk_import.job_completed{% endtrans %}</strong>
|
||||
{% trans %}info_providers.bulk_import.job_completed.description{% endtrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div {{ stimulus_controller('bulk-import', {
|
||||
'jobId': job.id,
|
||||
'researchUrl': path('bulk_info_provider_research_part', {'jobId': job.id, 'partId': '__PART_ID__'}),
|
||||
'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>
|
||||
<h5 class="mb-1">{{ job.displayNameKey|trans(job.displayNameParams) }} - {{ job.formattedTimestamp }}</h5>
|
||||
<h5 class="mb-1">#{{ job.id }} - {{ job.displayNameKey|trans(job.displayNameParams) }}</h5>
|
||||
<small class="text-muted">
|
||||
{{ job.partCount }} {% trans %}info_providers.bulk_import.parts{% endtrans %} •
|
||||
{{ job.resultCount }} {% trans %}info_providers.bulk_import.results{% endtrans %} •
|
||||
|
|
@ -95,6 +115,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>
|
||||
|
|
@ -181,39 +208,74 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for result in part_result.searchResults %}
|
||||
{% set sortedResults = part_result.resultsSortedByPriority %}
|
||||
{% for result in sortedResults %}
|
||||
{# @var result \App\Services\InfoProviderSystem\DTOs\BulkSearchPartResultDTO #}
|
||||
{% set dto = result.searchResult %}
|
||||
{% set localPart = result.localPart %}
|
||||
<tr>
|
||||
{% set isTopResult = loop.first %}
|
||||
<tr{% if isTopResult and not isCompleted %} class="table-success"{% endif %}>
|
||||
<td>
|
||||
<img src="{{ dto.preview_image_url }}" data-thumbnail="{{ dto.preview_image_url }}"
|
||||
class="hoverpic" style="max-width: 35px;" {{ stimulus_controller('elements/hoverpic') }}>
|
||||
{% if dto.preview_image_url %}
|
||||
<img src="{{ dto.preview_image_url }}" data-thumbnail="{{ dto.preview_image_url }}"
|
||||
class="hoverpic" style="max-width: 35px;" {{ stimulus_controller('elements/hoverpic') }}
|
||||
onerror="this.style.display='none'">
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{# 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 %}
|
||||
<a href="{{ dto.provider_url }}" target="_blank" rel="noopener">{{ dto.name }}</a>
|
||||
<a href="{{ dto.provider_url }}" target="_blank" rel="noopener"{% if nameMatch %} class="fw-bold"{% endif %}>{{ dto.name }}</a>
|
||||
{% else %}
|
||||
{{ dto.name }}
|
||||
<span{% if nameMatch %} class="fw-bold"{% endif %}>{{ dto.name }}</span>
|
||||
{% endif %}
|
||||
{% if nameMatch %}
|
||||
<span class="badge bg-success ms-1" title="{% trans %}info_providers.bulk_import.exact_match{% endtrans %}"><i class="fas fa-check-circle"></i></span>
|
||||
{% endif %}
|
||||
{% if dto.mpn is not null %}
|
||||
<br><small class="text-muted">{{ dto.mpn }}</small>
|
||||
<br><small{% if mpnMatch %} class="fw-bold text-success"{% endif %}>{{ dto.mpn }}</small>
|
||||
{% if mpnMatch %}
|
||||
<span class="badge bg-success ms-1" style="font-size: 0.65em;" title="{% trans %}info_providers.bulk_import.mpn_match{% endtrans %}">MPN <i class="fas fa-check-circle"></i></span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ dto.description }}</td>
|
||||
<td>{{ dto.manufacturer ?? '' }}</td>
|
||||
<td>
|
||||
{{ info_provider_label(dto.provider_key)|default(dto.provider_key) }}
|
||||
<br><small class="text-muted">{{ dto.provider_id }}</small>
|
||||
<br><small{% if spnMatch %} class="fw-bold text-success"{% endif %}>{{ dto.provider_id }}</small>
|
||||
{% if spnMatch %}
|
||||
<span class="badge bg-success ms-1" style="font-size: 0.65em;" title="{% trans %}info_providers.bulk_import.spn_match{% endtrans %}">SPN <i class="fas fa-check-circle"></i></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-info">{{ result.sourceField ?? 'unknown' }}</span>
|
||||
{% if anyMatch %}
|
||||
<span class="badge bg-success">{% trans %}info_providers.bulk_import.match{% endtrans %}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-info">{{ result.sourceField ?? 'unknown' }}</span>
|
||||
{% endif %}
|
||||
{% if result.sourceKeyword %}
|
||||
<br><small class="text-muted">{{ result.sourceKeyword }}</small>
|
||||
{% endif %}
|
||||
<br><small{% if anyMatch %} class="fw-bold text-success"{% endif %}>{{ result.sourceKeyword }}</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group-vertical btn-group-sm" role="group">
|
||||
{% if not isCompleted %}
|
||||
<button type="button" class="btn {% if not isTopResult %} btn-outline-success{% else %}btn-success{% endif %}"
|
||||
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 %}
|
||||
{% if isTopResult %}<span class="badge bg-light text-success ms-1">{% trans %}info_providers.bulk_import.recommended{% endtrans %}</span>{% endif %}
|
||||
</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 %}>
|
||||
|
|
|
|||
46
templates/parts/info/_add_lot_modal.html.twig
Normal file
46
templates/parts/info/_add_lot_modal.html.twig
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
{% if add_lot_form is not null %}
|
||||
{% form_theme add_lot_form 'form/extended_bootstrap_layout.html.twig' %}
|
||||
|
||||
<div class="modal fade" id="add-lot-modal" tabindex="-1" aria-labelledby="add-lot-modal-title" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
{{ form_start(add_lot_form) }}
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="add-lot-modal-title">
|
||||
<i class="fas fa-plus-square fa-fw"></i>
|
||||
{% trans %}part_lot.create{% endtrans %}
|
||||
</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{{ form_row(add_lot_form.description) }}
|
||||
{{ form_row(add_lot_form.storage_location) }}
|
||||
{{ form_row(add_lot_form.amount) }}
|
||||
{{ form_row(add_lot_form.instock_unknown) }}
|
||||
{{ form_row(add_lot_form.needs_refill) }}
|
||||
{{ form_row(add_lot_form.expiration_date) }}
|
||||
|
||||
<div>
|
||||
<a class="btn btn-link btn-sm {{ offset_label }}" data-bs-toggle="collapse" href="#add-lot-advanced" role="button" aria-expanded="false" aria-controls="add-lot-advanced">
|
||||
{% trans %}part_lot.edit.advanced{% endtrans %}
|
||||
</a>
|
||||
<div class="collapse" id="add-lot-advanced">
|
||||
{{ form_row(add_lot_form.comment) }}
|
||||
{{ form_row(add_lot_form.owner) }}
|
||||
{{ form_row(add_lot_form.user_barcode) }}
|
||||
{{ form_row(add_lot_form.last_stocktake_at) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans %}modal.close{% endtrans %}</button>
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="fas fa-plus-square fa-fw"></i>
|
||||
{% trans %}part_lot.create{% endtrans %}
|
||||
</button>
|
||||
</div>
|
||||
{{ form_end(add_lot_form) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
{% include "parts/info/_withdraw_modal.html.twig" %}
|
||||
{% include "parts/info/_stocktake_modal.html.twig" %}
|
||||
{% include "parts/info/_add_lot_modal.html.twig" %}
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
|
|
@ -126,3 +127,10 @@
|
|||
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% if add_lot_form is not null %}
|
||||
<button type="button" class="btn btn-outline-success" data-bs-toggle="modal" data-bs-target="#add-lot-modal">
|
||||
<i class="fas fa-plus-square fa-fw"></i>
|
||||
{% trans %}part_lot.create{% endtrans %}
|
||||
</button>
|
||||
{% endif %}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -11211,6 +11211,96 @@ Please note, that you can not impersonate a disabled user. If you try you will g
|
|||
<target>Update Part</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="_sWGLGs" name="info_providers.bulk_import.back_to_jobs">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.back_to_jobs</source>
|
||||
<target>Back to Jobs</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="2DCRx_T" name="info_providers.bulk_import.back_to_parts">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.back_to_parts</source>
|
||||
<target>Back to Parts</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="9OAohXg" name="info_providers.bulk_import.job_completed">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.job_completed</source>
|
||||
<target>Job completed!</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="hwkbU38" name="info_providers.bulk_import.job_completed.description">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.job_completed.description</source>
|
||||
<target>All parts have been processed. You can review the results below or navigate back to the parts list.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="ahbWfwA" name="info_providers.bulk_import.recommended">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.recommended</source>
|
||||
<target>Top</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="tFJOMYX" name="info_providers.bulk_import.exact_match">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.exact_match</source>
|
||||
<target>Exact name match</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="mBAxdTx" name="info_providers.bulk_import.mpn_match">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.mpn_match</source>
|
||||
<target>MPN matches</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="W1HbYWX" name="info_providers.bulk_import.active_jobs">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.active_jobs</source>
|
||||
<target>Active Jobs</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="tZSOzU1" name="info_providers.bulk_import.finished_jobs">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.finished_jobs</source>
|
||||
<target>History</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="noEU4s7" name="info_providers.bulk_import.spn_match">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.spn_match</source>
|
||||
<target>SPN matches</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="RiHOuLh" name="info_providers.bulk_import.match">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.match</source>
|
||||
<target>Match</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="UCKGkQ3" 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="4uMgGbn" 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="a8kwuvb" 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=".iZc63I" 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>
|
||||
|
|
|
|||
64
yarn.lock
64
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.2"
|
||||
resolved "https://registry.yarnpkg.com/barcode-detector/-/barcode-detector-3.1.2.tgz#8032a211ebb6cb5cc25724c5c56322c77ed02503"
|
||||
integrity sha512-Q5kjXpVH5I3ItykNzbWmfWnNryFN1ZTWp10k9/PKJuS0RnoKR7jTrHEJODR4fn04bRomq7TJwie/Dr9fj/GoGQ==
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/barcode-detector/-/barcode-detector-3.1.3.tgz#ea3224c8cf106b91e4f05a25ff0d798cb2b380a9"
|
||||
integrity sha512-omL3/x26oU9jlR0gUQcGdXIjQtMlrUGKF7xRFO1RwrQkRkRU7WLz0mgQEsdUtYBm2uX3JH+HQLrKlyTS/BxZRw==
|
||||
dependencies:
|
||||
zxing-wasm "3.0.2"
|
||||
zxing-wasm "3.0.3"
|
||||
|
||||
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.25"
|
||||
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.25.tgz#183b45c0d3bdd12addb352426555fb3627eb022a"
|
||||
integrity sha512-QO/VHsXCQdnzADMfmkeOPvHdIAkoB7i0/rGjINPJEetLx75hNttVWGQ/jycHUDP9zZ9rupbm60WRxcwViB0MiA==
|
||||
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==
|
||||
|
||||
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.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==
|
||||
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==
|
||||
dependencies:
|
||||
browserslist "^4.28.2"
|
||||
css-declaration-sorter "^7.2.0"
|
||||
|
|
@ -2670,7 +2670,7 @@ cssnano-preset-default@^7.0.16:
|
|||
postcss-minify-font-values "^7.0.3"
|
||||
postcss-minify-gradients "^7.0.5"
|
||||
postcss-minify-params "^7.0.9"
|
||||
postcss-minify-selectors "^7.1.1"
|
||||
postcss-minify-selectors "^7.1.2"
|
||||
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.8"
|
||||
resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-7.1.8.tgz#a6508bd13f3b206676c6594fa19ef45a89acc9dc"
|
||||
integrity sha512-OGXtXqXmwEoIGfXM2QoD35vweUAtx+J8ZvLSZHOEV0Jv9Hs9ScTdGGjRzZXun5J4PEZhEoytKig2O2NR8NXxKw==
|
||||
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==
|
||||
dependencies:
|
||||
cssnano-preset-default "^7.0.16"
|
||||
cssnano-preset-default "^7.0.17"
|
||||
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.0"
|
||||
resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.0.tgz#66eecff6c764c0df9b762e62ca7edcfb53b4edfa"
|
||||
integrity sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.1.tgz#dd085fec2494a2a33bac6e61277374669e1dd774"
|
||||
integrity sha512-h2r7rcm6Ee/J8o0LD5djLuFVcfbZxhvho4vvsbeV0aMvXjUgqv4YpxpkEx0d68l6+IleVfLAdVEfhR7QNMkGHQ==
|
||||
|
||||
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.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==
|
||||
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==
|
||||
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.13"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.13.tgz#6cfaf647f2e7ef69850208eccd849e0d3f65d420"
|
||||
integrity sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==
|
||||
version "8.5.14"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.14.tgz#a66c2d7808fadf69ebb5b84a03f8bafd76c4919c"
|
||||
integrity sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==
|
||||
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.5.0:
|
||||
type-fest@^5.6.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.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==
|
||||
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==
|
||||
dependencies:
|
||||
"@types/emscripten" "^1.41.5"
|
||||
type-fest "^5.5.0"
|
||||
type-fest "^5.6.0"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue