@open-mercato/core 0.10.1-develop.7879.1.80d3740285 → 0.10.1-develop.7883.1.44ac2125f5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/attachments/lib/transferClient.js +72 -0
- package/dist/modules/attachments/lib/transferClient.js.map +7 -0
- package/dist/modules/attachments/lib/transferLimits.js +5 -0
- package/dist/modules/attachments/lib/transferLimits.js.map +7 -0
- package/dist/modules/catalog/backend/catalog/products/[productId]/variants/create/page.js +23 -26
- package/dist/modules/catalog/backend/catalog/products/[productId]/variants/create/page.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/create/page.js +31 -23
- package/dist/modules/catalog/backend/catalog/products/create/page.js.map +2 -2
- package/dist/modules/catalog/components/products/mediaTransfer.js +27 -0
- package/dist/modules/catalog/components/products/mediaTransfer.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/Lane.js +5 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/components/Lane.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js +2 -1
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/attachments/lib/transferClient.ts +87 -0
- package/src/modules/attachments/lib/transferLimits.ts +1 -0
- package/src/modules/catalog/backend/catalog/products/[productId]/variants/create/page.tsx +23 -32
- package/src/modules/catalog/backend/catalog/products/create/page.tsx +31 -28
- package/src/modules/catalog/components/products/mediaTransfer.ts +40 -0
- package/src/modules/catalog/i18n/de.json +2 -0
- package/src/modules/catalog/i18n/en.json +2 -0
- package/src/modules/catalog/i18n/es.json +2 -0
- package/src/modules/catalog/i18n/ko.json +2 -0
- package/src/modules/catalog/i18n/pl.json +2 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/Lane.tsx +6 -2
- package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +3 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.10.1-develop.
|
|
3
|
+
"version": "0.10.1-develop.7883.1.44ac2125f5",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -266,16 +266,16 @@
|
|
|
266
266
|
"zod": "^4.6.6"
|
|
267
267
|
},
|
|
268
268
|
"peerDependencies": {
|
|
269
|
-
"@open-mercato/ai-assistant": "0.10.1-develop.
|
|
270
|
-
"@open-mercato/shared": "0.10.1-develop.
|
|
271
|
-
"@open-mercato/ui": "0.10.1-develop.
|
|
269
|
+
"@open-mercato/ai-assistant": "0.10.1-develop.7883.1.44ac2125f5",
|
|
270
|
+
"@open-mercato/shared": "0.10.1-develop.7883.1.44ac2125f5",
|
|
271
|
+
"@open-mercato/ui": "0.10.1-develop.7883.1.44ac2125f5",
|
|
272
272
|
"react": "^19.0.0",
|
|
273
273
|
"react-dom": "^19.0.0"
|
|
274
274
|
},
|
|
275
275
|
"devDependencies": {
|
|
276
|
-
"@open-mercato/ai-assistant": "0.10.1-develop.
|
|
277
|
-
"@open-mercato/shared": "0.10.1-develop.
|
|
278
|
-
"@open-mercato/ui": "0.10.1-develop.
|
|
276
|
+
"@open-mercato/ai-assistant": "0.10.1-develop.7883.1.44ac2125f5",
|
|
277
|
+
"@open-mercato/shared": "0.10.1-develop.7883.1.44ac2125f5",
|
|
278
|
+
"@open-mercato/ui": "0.10.1-develop.7883.1.44ac2125f5",
|
|
279
279
|
"@testing-library/dom": "^10.4.1",
|
|
280
280
|
"@testing-library/jest-dom": "^7.0.1",
|
|
281
281
|
"@testing-library/react": "^16.3.3",
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'
|
|
2
|
+
import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
|
|
3
|
+
import { ATTACHMENT_TRANSFER_MAX_IDS } from './transferLimits'
|
|
4
|
+
|
|
5
|
+
export type TransferAttachmentsInput = {
|
|
6
|
+
entityId: string
|
|
7
|
+
attachmentIds: string[]
|
|
8
|
+
fromRecordId?: string | null
|
|
9
|
+
toRecordId: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type TransferAttachmentsResult = {
|
|
13
|
+
transferredCount: number
|
|
14
|
+
failedIds: string[]
|
|
15
|
+
unconfirmedCount: number
|
|
16
|
+
errors: string[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function chunkAttachmentIds(attachmentIds: string[], size = ATTACHMENT_TRANSFER_MAX_IDS): string[][] {
|
|
20
|
+
const chunks: string[][] = []
|
|
21
|
+
for (let index = 0; index < attachmentIds.length; index += size) {
|
|
22
|
+
chunks.push(attachmentIds.slice(index, index + size))
|
|
23
|
+
}
|
|
24
|
+
return chunks
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function stopsRemainingChunks(error: unknown): boolean {
|
|
28
|
+
const name = typeof error === 'object' && error !== null ? (error as { name?: unknown }).name : undefined
|
|
29
|
+
return name === 'AbortError' || name === 'UnauthorizedError'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function describeError(error: unknown): string {
|
|
33
|
+
return error instanceof Error && error.message ? error.message : String(error)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function transferAttachmentsInChunks({
|
|
37
|
+
entityId,
|
|
38
|
+
attachmentIds,
|
|
39
|
+
fromRecordId,
|
|
40
|
+
toRecordId,
|
|
41
|
+
}: TransferAttachmentsInput): Promise<TransferAttachmentsResult> {
|
|
42
|
+
const result: TransferAttachmentsResult = { transferredCount: 0, failedIds: [], unconfirmedCount: 0, errors: [] }
|
|
43
|
+
const chunks = chunkAttachmentIds(attachmentIds)
|
|
44
|
+
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex += 1) {
|
|
45
|
+
const chunk = chunks[chunkIndex]
|
|
46
|
+
try {
|
|
47
|
+
const call = await apiCall<{ ok?: boolean; updated?: number; error?: string }>(
|
|
48
|
+
'/api/attachments/transfer',
|
|
49
|
+
{
|
|
50
|
+
method: 'POST',
|
|
51
|
+
headers: { 'content-type': 'application/json' },
|
|
52
|
+
body: JSON.stringify({
|
|
53
|
+
entityId,
|
|
54
|
+
attachmentIds: chunk,
|
|
55
|
+
...(fromRecordId ? { fromRecordId } : {}),
|
|
56
|
+
toRecordId,
|
|
57
|
+
}),
|
|
58
|
+
},
|
|
59
|
+
{ fallback: null },
|
|
60
|
+
)
|
|
61
|
+
if (!call.ok) {
|
|
62
|
+
result.failedIds.push(...chunk)
|
|
63
|
+
if (call.result?.error) result.errors.push(call.result.error)
|
|
64
|
+
continue
|
|
65
|
+
}
|
|
66
|
+
const updated = call.result?.updated
|
|
67
|
+
const confirmed = typeof updated === 'number' && Number.isFinite(updated)
|
|
68
|
+
? Math.min(Math.max(Math.trunc(updated), 0), chunk.length)
|
|
69
|
+
: 0
|
|
70
|
+
result.transferredCount += confirmed
|
|
71
|
+
result.unconfirmedCount += chunk.length - confirmed
|
|
72
|
+
} catch (error) {
|
|
73
|
+
getTelemetryRuntime()?.reportError(error, {
|
|
74
|
+
module: 'attachments',
|
|
75
|
+
code: 'attachments.transfer_chunk_failed',
|
|
76
|
+
attributes: { chunkSize: chunk.length },
|
|
77
|
+
})
|
|
78
|
+
result.errors.push(describeError(error))
|
|
79
|
+
if (stopsRemainingChunks(error)) {
|
|
80
|
+
result.failedIds.push(...chunks.slice(chunkIndex).flat())
|
|
81
|
+
break
|
|
82
|
+
}
|
|
83
|
+
result.failedIds.push(...chunk)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return result
|
|
87
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const ATTACHMENT_TRANSFER_MAX_IDS = 100
|
|
@@ -35,10 +35,10 @@ import {
|
|
|
35
35
|
VariantPricesSection,
|
|
36
36
|
VariantMediaSection,
|
|
37
37
|
} from '@open-mercato/core/modules/catalog/components/products/VariantBuilder'
|
|
38
|
-
import type { ProductMediaItem } from '@open-mercato/core/modules/catalog/components/products/ProductMediaManager'
|
|
39
38
|
import { buildAttachmentImageUrl, slugifyAttachmentFileName } from '@open-mercato/core/modules/attachments/lib/imageUrls'
|
|
40
39
|
import { fetchOptionSchemaTemplate } from '../../../optionSchemaClient'
|
|
41
40
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
41
|
+
import { transferDraftMedia } from '@open-mercato/core/modules/catalog/components/products/mediaTransfer'
|
|
42
42
|
|
|
43
43
|
const logger = createLogger('catalog')
|
|
44
44
|
|
|
@@ -393,11 +393,19 @@ export default function CreateVariantPage({ params }: { params?: { productId?: s
|
|
|
393
393
|
if (!variantId) {
|
|
394
394
|
throw createCrudFormError(t('catalog.variants.form.errors.idMissing', 'Variant id missing after create.'))
|
|
395
395
|
}
|
|
396
|
-
await
|
|
396
|
+
const mediaTransfer = await transferDraftMedia({
|
|
397
|
+
entityId: E.catalog.catalog_product_variant,
|
|
397
398
|
draftId: values.mediaDraftId,
|
|
398
|
-
variantId,
|
|
399
|
+
recordId: variantId,
|
|
399
400
|
mediaItems: Array.isArray(values.mediaItems) ? values.mediaItems : [],
|
|
400
401
|
})
|
|
402
|
+
if (mediaTransfer.failedCount > 0) {
|
|
403
|
+
logger.error('attachments.transfer.failed', {
|
|
404
|
+
failed: mediaTransfer.failedCount,
|
|
405
|
+
requested: mediaTransfer.requestedCount,
|
|
406
|
+
errors: mediaTransfer.errors,
|
|
407
|
+
})
|
|
408
|
+
}
|
|
401
409
|
await syncVariantPrices({
|
|
402
410
|
priceKinds,
|
|
403
411
|
priceDrafts: values.prices ?? {},
|
|
@@ -408,7 +416,18 @@ export default function CreateVariantPage({ params }: { params?: { productId?: s
|
|
|
408
416
|
productTaxRateId,
|
|
409
417
|
productTaxRate,
|
|
410
418
|
})
|
|
411
|
-
|
|
419
|
+
if (mediaTransfer.failedCount > 0) {
|
|
420
|
+
flash(
|
|
421
|
+
t(
|
|
422
|
+
'catalog.variants.form.mediaTransferIncomplete',
|
|
423
|
+
'Variant created, but {failed} of {total} media files could not be attached.',
|
|
424
|
+
{ failed: mediaTransfer.failedCount, total: mediaTransfer.requestedCount },
|
|
425
|
+
),
|
|
426
|
+
'warning',
|
|
427
|
+
)
|
|
428
|
+
} else {
|
|
429
|
+
flash(t('catalog.variants.form.createSuccess', 'Variant created.'), 'success')
|
|
430
|
+
}
|
|
412
431
|
router.push(`/backend/catalog/products/${productId}#variants`)
|
|
413
432
|
}}
|
|
414
433
|
/>
|
|
@@ -417,34 +436,6 @@ export default function CreateVariantPage({ params }: { params?: { productId?: s
|
|
|
417
436
|
)
|
|
418
437
|
}
|
|
419
438
|
|
|
420
|
-
async function transferVariantMedia({
|
|
421
|
-
draftId,
|
|
422
|
-
variantId,
|
|
423
|
-
mediaItems,
|
|
424
|
-
}: {
|
|
425
|
-
draftId?: string | null
|
|
426
|
-
variantId: string
|
|
427
|
-
mediaItems: ProductMediaItem[]
|
|
428
|
-
}): Promise<void> {
|
|
429
|
-
if (!draftId || !variantId) return
|
|
430
|
-
const attachmentIds = mediaItems.map((item) => item.id).filter((id): id is string => typeof id === 'string' && id.length > 0)
|
|
431
|
-
if (!attachmentIds.length) return
|
|
432
|
-
await apiCall(
|
|
433
|
-
'/api/attachments/transfer',
|
|
434
|
-
{
|
|
435
|
-
method: 'POST',
|
|
436
|
-
headers: { 'content-type': 'application/json' },
|
|
437
|
-
body: JSON.stringify({
|
|
438
|
-
entityId: E.catalog.catalog_product_variant,
|
|
439
|
-
attachmentIds,
|
|
440
|
-
fromRecordId: draftId,
|
|
441
|
-
toRecordId: variantId,
|
|
442
|
-
}),
|
|
443
|
-
},
|
|
444
|
-
{ fallback: null },
|
|
445
|
-
)
|
|
446
|
-
}
|
|
447
|
-
|
|
448
439
|
async function syncVariantPrices({
|
|
449
440
|
priceKinds,
|
|
450
441
|
priceDrafts,
|
|
@@ -98,6 +98,7 @@ import {
|
|
|
98
98
|
type ProductUnitConversionInput,
|
|
99
99
|
} from "@open-mercato/core/modules/catalog/components/products/productFormUtils";
|
|
100
100
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
101
|
+
import { transferDraftMedia } from "@open-mercato/core/modules/catalog/components/products/mediaTransfer";
|
|
101
102
|
|
|
102
103
|
const logger = createLogger('catalog')
|
|
103
104
|
|
|
@@ -419,9 +420,6 @@ export default function CreateCatalogProductPage() {
|
|
|
419
420
|
const mediaItems = Array.isArray(formValues.mediaItems)
|
|
420
421
|
? formValues.mediaItems
|
|
421
422
|
: [];
|
|
422
|
-
const attachmentIds = mediaItems
|
|
423
|
-
.map((item) => (typeof item.id === "string" ? item.id : null))
|
|
424
|
-
.filter((value): value is string => !!value);
|
|
425
423
|
const mediaDraftId =
|
|
426
424
|
typeof formValues.mediaDraftId === "string"
|
|
427
425
|
? formValues.mediaDraftId
|
|
@@ -768,27 +766,18 @@ export default function CreateCatalogProductPage() {
|
|
|
768
766
|
await createCrud("catalog/prices", pricePayload);
|
|
769
767
|
}
|
|
770
768
|
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
fromRecordId: mediaDraftId,
|
|
784
|
-
toRecordId: productId,
|
|
785
|
-
}),
|
|
786
|
-
},
|
|
787
|
-
{ fallback: null },
|
|
788
|
-
);
|
|
789
|
-
if (!transfer.ok) {
|
|
790
|
-
logger.error("attachments.transfer.failed", { err: transfer.result?.error });
|
|
791
|
-
}
|
|
769
|
+
const mediaTransfer = await transferDraftMedia({
|
|
770
|
+
entityId: E.catalog.catalog_product,
|
|
771
|
+
draftId: mediaDraftId,
|
|
772
|
+
recordId: productId,
|
|
773
|
+
mediaItems,
|
|
774
|
+
});
|
|
775
|
+
if (mediaTransfer.failedCount > 0) {
|
|
776
|
+
logger.error("attachments.transfer.failed", {
|
|
777
|
+
failed: mediaTransfer.failedCount,
|
|
778
|
+
requested: mediaTransfer.requestedCount,
|
|
779
|
+
errors: mediaTransfer.errors,
|
|
780
|
+
});
|
|
792
781
|
}
|
|
793
782
|
|
|
794
783
|
if (inboxDraft) {
|
|
@@ -817,10 +806,24 @@ export default function CreateCatalogProductPage() {
|
|
|
817
806
|
}
|
|
818
807
|
}
|
|
819
808
|
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
809
|
+
if (mediaTransfer.failedCount > 0) {
|
|
810
|
+
flash(
|
|
811
|
+
t(
|
|
812
|
+
"catalog.products.create.mediaTransferIncomplete",
|
|
813
|
+
"Product created, but {failed} of {total} media files could not be attached.",
|
|
814
|
+
{
|
|
815
|
+
failed: mediaTransfer.failedCount,
|
|
816
|
+
total: mediaTransfer.requestedCount,
|
|
817
|
+
},
|
|
818
|
+
),
|
|
819
|
+
"warning",
|
|
820
|
+
);
|
|
821
|
+
} else {
|
|
822
|
+
flash(
|
|
823
|
+
t("catalog.products.create.success", "Product created."),
|
|
824
|
+
"success",
|
|
825
|
+
);
|
|
826
|
+
}
|
|
824
827
|
if (inboxDraft) {
|
|
825
828
|
router.push(
|
|
826
829
|
`/backend/inbox-ops/proposals/${encodeURIComponent(inboxDraft.proposalId)}`,
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { transferAttachmentsInChunks } from '@open-mercato/core/modules/attachments/lib/transferClient'
|
|
2
|
+
import type { ProductMediaItem } from './ProductMediaManager'
|
|
3
|
+
|
|
4
|
+
export type DraftMediaTransferInput = {
|
|
5
|
+
entityId: string
|
|
6
|
+
draftId?: string | null
|
|
7
|
+
recordId: string
|
|
8
|
+
mediaItems: ProductMediaItem[]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type DraftMediaTransferResult = {
|
|
12
|
+
requestedCount: number
|
|
13
|
+
failedCount: number
|
|
14
|
+
errors: string[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function transferDraftMedia({
|
|
18
|
+
entityId,
|
|
19
|
+
draftId,
|
|
20
|
+
recordId,
|
|
21
|
+
mediaItems,
|
|
22
|
+
}: DraftMediaTransferInput): Promise<DraftMediaTransferResult> {
|
|
23
|
+
const attachmentIds = mediaItems
|
|
24
|
+
.map((item) => item.id)
|
|
25
|
+
.filter((id): id is string => typeof id === 'string' && id.length > 0)
|
|
26
|
+
if (!draftId || !recordId || !attachmentIds.length) {
|
|
27
|
+
return { requestedCount: 0, failedCount: 0, errors: [] }
|
|
28
|
+
}
|
|
29
|
+
const result = await transferAttachmentsInChunks({
|
|
30
|
+
entityId,
|
|
31
|
+
attachmentIds,
|
|
32
|
+
fromRecordId: draftId,
|
|
33
|
+
toRecordId: recordId,
|
|
34
|
+
})
|
|
35
|
+
return {
|
|
36
|
+
requestedCount: attachmentIds.length,
|
|
37
|
+
failedCount: result.failedIds.length + result.unconfirmedCount,
|
|
38
|
+
errors: result.errors,
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -416,6 +416,7 @@
|
|
|
416
416
|
"catalog.products.create.errors.variant": "Variante konnte nicht erstellt werden.",
|
|
417
417
|
"catalog.products.create.fields.currency": "Hauptwährung",
|
|
418
418
|
"catalog.products.create.handleHelp": "Handle wird für URLs verwendet und muss eindeutig sein.",
|
|
419
|
+
"catalog.products.create.mediaTransferIncomplete": "Produkt erstellt, aber {failed} von {total} Mediendateien konnten nicht angehängt werden.",
|
|
419
420
|
"catalog.products.create.meta.description": "Verwalte Untertitel, Handle und Währung für deine Storefronts.",
|
|
420
421
|
"catalog.products.create.meta.handleLabel": "Handle",
|
|
421
422
|
"catalog.products.create.meta.title": "Produkt-Metadaten",
|
|
@@ -882,6 +883,7 @@
|
|
|
882
883
|
"catalog.variants.form.isDefaultLabel": "Standardvariante",
|
|
883
884
|
"catalog.variants.form.loading": "Formular wird geladen...",
|
|
884
885
|
"catalog.variants.form.media": "Medien",
|
|
886
|
+
"catalog.variants.form.mediaTransferIncomplete": "Variante erstellt, aber {failed} von {total} Mediendateien konnten nicht angehängt werden.",
|
|
885
887
|
"catalog.variants.form.nameLabel": "Name",
|
|
886
888
|
"catalog.variants.form.namePlaceholder": "z. B. Blau / Klein",
|
|
887
889
|
"catalog.variants.form.nameRequired": "Geben Sie den Variantennamen an.",
|
|
@@ -416,6 +416,7 @@
|
|
|
416
416
|
"catalog.products.create.errors.variant": "Failed to create variant.",
|
|
417
417
|
"catalog.products.create.fields.currency": "Primary currency",
|
|
418
418
|
"catalog.products.create.handleHelp": "Handle is used for URLs and must be unique.",
|
|
419
|
+
"catalog.products.create.mediaTransferIncomplete": "Product created, but {failed} of {total} media files could not be attached.",
|
|
419
420
|
"catalog.products.create.meta.description": "Manage subtitle, handle, and currency for storefronts.",
|
|
420
421
|
"catalog.products.create.meta.handleLabel": "Handle",
|
|
421
422
|
"catalog.products.create.meta.title": "Product meta",
|
|
@@ -882,6 +883,7 @@
|
|
|
882
883
|
"catalog.variants.form.isDefaultLabel": "Default variant",
|
|
883
884
|
"catalog.variants.form.loading": "Loading form...",
|
|
884
885
|
"catalog.variants.form.media": "Media",
|
|
886
|
+
"catalog.variants.form.mediaTransferIncomplete": "Variant created, but {failed} of {total} media files could not be attached.",
|
|
885
887
|
"catalog.variants.form.nameLabel": "Name",
|
|
886
888
|
"catalog.variants.form.namePlaceholder": "e.g., Blue / Small",
|
|
887
889
|
"catalog.variants.form.nameRequired": "Provide the variant name.",
|
|
@@ -416,6 +416,7 @@
|
|
|
416
416
|
"catalog.products.create.errors.variant": "No se pudo crear la variante.",
|
|
417
417
|
"catalog.products.create.fields.currency": "Moneda principal",
|
|
418
418
|
"catalog.products.create.handleHelp": "El identificador se usa en URLs y debe ser único.",
|
|
419
|
+
"catalog.products.create.mediaTransferIncomplete": "Producto creado, pero no se pudieron adjuntar {failed} de {total} archivos multimedia.",
|
|
419
420
|
"catalog.products.create.meta.description": "Administra el subtítulo, el identificador y la moneda para las vitrinas.",
|
|
420
421
|
"catalog.products.create.meta.handleLabel": "Identificador",
|
|
421
422
|
"catalog.products.create.meta.title": "Metadatos del producto",
|
|
@@ -882,6 +883,7 @@
|
|
|
882
883
|
"catalog.variants.form.isDefaultLabel": "Variante predeterminada",
|
|
883
884
|
"catalog.variants.form.loading": "Cargando formulario...",
|
|
884
885
|
"catalog.variants.form.media": "Medios",
|
|
886
|
+
"catalog.variants.form.mediaTransferIncomplete": "Variante creada, pero no se pudieron adjuntar {failed} de {total} archivos multimedia.",
|
|
885
887
|
"catalog.variants.form.nameLabel": "Nombre",
|
|
886
888
|
"catalog.variants.form.namePlaceholder": "ej., Azul / Pequeño",
|
|
887
889
|
"catalog.variants.form.nameRequired": "Ingrese el nombre de la variante.",
|
|
@@ -416,6 +416,7 @@
|
|
|
416
416
|
"catalog.products.create.errors.variant": "변형 생성에 실패했습니다.",
|
|
417
417
|
"catalog.products.create.fields.currency": "기본 통화",
|
|
418
418
|
"catalog.products.create.handleHelp": "핸들은 URL에 사용되며 고유해야 합니다.",
|
|
419
|
+
"catalog.products.create.mediaTransferIncomplete": "상품이 생성되었지만 미디어 파일 {total}개 중 {failed}개를 첨부하지 못했습니다.",
|
|
419
420
|
"catalog.products.create.meta.description": "스토어프론트에 사용할 부제목, 핸들, 통화를 관리합니다.",
|
|
420
421
|
"catalog.products.create.meta.handleLabel": "핸들",
|
|
421
422
|
"catalog.products.create.meta.title": "상품 메타 정보",
|
|
@@ -882,6 +883,7 @@
|
|
|
882
883
|
"catalog.variants.form.isDefaultLabel": "기본 변형",
|
|
883
884
|
"catalog.variants.form.loading": "폼을 불러오는 중...",
|
|
884
885
|
"catalog.variants.form.media": "미디어",
|
|
886
|
+
"catalog.variants.form.mediaTransferIncomplete": "변형이 생성되었지만 미디어 파일 {total}개 중 {failed}개를 첨부하지 못했습니다.",
|
|
885
887
|
"catalog.variants.form.nameLabel": "이름",
|
|
886
888
|
"catalog.variants.form.namePlaceholder": "예: 블루 / 스몰",
|
|
887
889
|
"catalog.variants.form.nameRequired": "변형 이름을 입력하세요.",
|
|
@@ -416,6 +416,7 @@
|
|
|
416
416
|
"catalog.products.create.errors.variant": "Nie udało się utworzyć wariantu.",
|
|
417
417
|
"catalog.products.create.fields.currency": "Główna waluta",
|
|
418
418
|
"catalog.products.create.handleHelp": "Identyfikator jest używany w adresach URL i musi być unikalny.",
|
|
419
|
+
"catalog.products.create.mediaTransferIncomplete": "Produkt utworzony, ale nie udało się dołączyć {failed} z {total} plików multimedialnych.",
|
|
419
420
|
"catalog.products.create.meta.description": "Zarządzaj podtytułem, identyfikatorem i walutą dla witryn.",
|
|
420
421
|
"catalog.products.create.meta.handleLabel": "Identyfikator",
|
|
421
422
|
"catalog.products.create.meta.title": "Metadane produktu",
|
|
@@ -882,6 +883,7 @@
|
|
|
882
883
|
"catalog.variants.form.isDefaultLabel": "Domyślny wariant",
|
|
883
884
|
"catalog.variants.form.loading": "Ładowanie formularza...",
|
|
884
885
|
"catalog.variants.form.media": "Multimedia",
|
|
886
|
+
"catalog.variants.form.mediaTransferIncomplete": "Wariant utworzony, ale nie udało się dołączyć {failed} z {total} plików multimedialnych.",
|
|
885
887
|
"catalog.variants.form.nameLabel": "Nazwa",
|
|
886
888
|
"catalog.variants.form.namePlaceholder": "np. Niebieski / Mały",
|
|
887
889
|
"catalog.variants.form.nameRequired": "Podaj nazwę wariantu.",
|
|
@@ -36,6 +36,7 @@ type LaneProps = {
|
|
|
36
36
|
deals: DealCardData[]
|
|
37
37
|
/** Server-side aggregate for accurate counts/totals across all deals in this stage, not just loaded ones */
|
|
38
38
|
aggregate?: LaneAggregateProp | null
|
|
39
|
+
paginationTotal?: number
|
|
39
40
|
selectedDealIds: Set<string>
|
|
40
41
|
buildMenuItems: (deal: DealCardData) => RowActionItem[]
|
|
41
42
|
activeDragDealId?: string | null
|
|
@@ -176,6 +177,7 @@ function LaneImpl({
|
|
|
176
177
|
stage,
|
|
177
178
|
deals,
|
|
178
179
|
aggregate,
|
|
180
|
+
paginationTotal,
|
|
179
181
|
selectedDealIds,
|
|
180
182
|
buildMenuItems,
|
|
181
183
|
activeDragDealId,
|
|
@@ -212,7 +214,8 @@ function LaneImpl({
|
|
|
212
214
|
: null
|
|
213
215
|
const visibleCount = deals.length
|
|
214
216
|
const totalCount = stats.count
|
|
215
|
-
const
|
|
217
|
+
const remainingCount = (paginationTotal ?? totalCount) - visibleCount
|
|
218
|
+
const hasMoreDeals = remainingCount > 0
|
|
216
219
|
const accentClass = getAccentClass(stage.tone)
|
|
217
220
|
const countBadgeClass = getCountBadgeClass(stage.tone)
|
|
218
221
|
|
|
@@ -336,7 +339,7 @@ function LaneImpl({
|
|
|
336
339
|
t,
|
|
337
340
|
'customers.deals.kanban.lane.loadMore',
|
|
338
341
|
'Show more ({remaining})',
|
|
339
|
-
{ remaining:
|
|
342
|
+
{ remaining: remainingCount },
|
|
340
343
|
)}
|
|
341
344
|
</DashedTileButton>
|
|
342
345
|
) : null}
|
|
@@ -358,6 +361,7 @@ export const Lane = React.memo(LaneImpl, (prev, next) => {
|
|
|
358
361
|
if (prev.stage !== next.stage) return false
|
|
359
362
|
if (prev.deals !== next.deals) return false
|
|
360
363
|
if (prev.aggregate !== next.aggregate) return false
|
|
364
|
+
if (prev.paginationTotal !== next.paginationTotal) return false
|
|
361
365
|
if (prev.selectedDealIds !== next.selectedDealIds) return false
|
|
362
366
|
if (prev.buildMenuItems !== next.buildMenuItems) return false
|
|
363
367
|
if (prev.activeDragDealId !== next.activeDragDealId) return false
|
|
@@ -631,7 +631,7 @@ export default function DealsKanbanPage(): React.ReactElement {
|
|
|
631
631
|
)
|
|
632
632
|
|
|
633
633
|
// Aggregate query — gives accurate per-stage counts + totals (in base currency) across the entire pipeline,
|
|
634
|
-
// not just the loaded slice. Drives
|
|
634
|
+
// not just the loaded slice. Drives lane headers and currency breakdowns.
|
|
635
635
|
const aggregateQuery = useQuery<StageAggregateResponse | null>({
|
|
636
636
|
queryKey: [
|
|
637
637
|
'customers',
|
|
@@ -2853,7 +2853,7 @@ export default function DealsKanbanPage(): React.ReactElement {
|
|
|
2853
2853
|
/>
|
|
2854
2854
|
) : (
|
|
2855
2855
|
<>
|
|
2856
|
-
{lanes.map((stage) => {
|
|
2856
|
+
{lanes.map((stage, stageIndex) => {
|
|
2857
2857
|
const laneDeals = sortedDealsByStage.get(stage.id) ?? EMPTY_DEAL_ARRAY
|
|
2858
2858
|
return (
|
|
2859
2859
|
<Lane
|
|
@@ -2861,6 +2861,7 @@ export default function DealsKanbanPage(): React.ReactElement {
|
|
|
2861
2861
|
stage={stage}
|
|
2862
2862
|
deals={laneDeals}
|
|
2863
2863
|
aggregate={aggregateByStage.get(stage.id) ?? null}
|
|
2864
|
+
paginationTotal={laneQueries[stageIndex]?.data?.total ?? 0}
|
|
2864
2865
|
selectedDealIds={selectedDealIds}
|
|
2865
2866
|
buildMenuItems={buildMenuItems}
|
|
2866
2867
|
activeDragDealId={activeDragDealId}
|