|
@@ -1,5 +1,5 @@
|
|
|
import { useMutation, useQueries, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
import { useMutation, useQueries, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
|
-import { AlertCircle, AlertTriangle, Loader2, Pencil, Printer, ThumbsUp, X } from 'lucide-react';
|
|
|
|
|
|
|
+import { AlertCircle, AlertTriangle, Layers, Loader2, Pencil, Printer, ThumbsUp, X } from 'lucide-react';
|
|
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
|
import { useTranslation } from 'react-i18next';
|
|
import { useTranslation } from 'react-i18next';
|
|
|
import type { CostCenterSummary, PrintQueueItemCreate, PrintQueueItemUpdate, SlotMaterial } from '../../api/client';
|
|
import type { CostCenterSummary, PrintQueueItemCreate, PrintQueueItemUpdate, SlotMaterial } from '../../api/client';
|
|
@@ -92,6 +92,7 @@ export function PrintModal({
|
|
|
archiveName,
|
|
archiveName,
|
|
|
queueItem,
|
|
queueItem,
|
|
|
initialSelectedPrinterIds,
|
|
initialSelectedPrinterIds,
|
|
|
|
|
+ preselectPlateId,
|
|
|
onClose,
|
|
onClose,
|
|
|
onSuccess,
|
|
onSuccess,
|
|
|
projectId,
|
|
projectId,
|
|
@@ -159,6 +160,9 @@ export function PrintModal({
|
|
|
if (mode === 'edit-queue-item' && queueItem?.plate_id != null) {
|
|
if (mode === 'edit-queue-item' && queueItem?.plate_id != null) {
|
|
|
return new Set([queueItem.plate_id]);
|
|
return new Set([queueItem.plate_id]);
|
|
|
}
|
|
}
|
|
|
|
|
+ if (mode === 'create' && !isCrossModel && preselectPlateId != null) {
|
|
|
|
|
+ return new Set([preselectPlateId]);
|
|
|
|
|
+ }
|
|
|
return new Set();
|
|
return new Set();
|
|
|
});
|
|
});
|
|
|
|
|
|
|
@@ -740,11 +744,19 @@ export function PrintModal({
|
|
|
sharedMappingPrinterId,
|
|
sharedMappingPrinterId,
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
- // Auto-select first plate when plates load (single or multi-plate)
|
|
|
|
|
|
|
+ // Auto-select first plate when plates load (single or multi-plate). A plate
|
|
|
|
|
+ // chosen before the plates were known (a reprint's or a queue item's) that
|
|
|
|
|
+ // the file doesn't have, because the file changed since, is dropped: left
|
|
|
|
|
+ // selected, it would be printed or keep the selection from ever falling
|
|
|
|
|
+ // back to a plate the file has (#3395).
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
|
- if (platesData?.plates && platesData.plates.length >= 1 && selectedPlates.size === 0) {
|
|
|
|
|
- setSelectedPlates(new Set([platesData.plates[0].index]));
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ const list = platesData?.plates;
|
|
|
|
|
+ if (!list?.length) return;
|
|
|
|
|
+ setSelectedPlates((prev) => {
|
|
|
|
|
+ const kept = [...prev].filter((index) => list.some((p) => p.index === index));
|
|
|
|
|
+ if (kept.length > 0 && kept.length === prev.size) return prev;
|
|
|
|
|
+ return new Set(kept.length > 0 ? kept : [list[0].index]);
|
|
|
|
|
+ });
|
|
|
}, [platesData, selectedPlates.size]);
|
|
}, [platesData, selectedPlates.size]);
|
|
|
|
|
|
|
|
// Cross-model: the candidate list owns plate choice, and `platesData` is the
|
|
// Cross-model: the candidate list owns plate choice, and `platesData` is the
|
|
@@ -891,6 +903,19 @@ export function PrintModal({
|
|
|
|
|
|
|
|
const isMultiPlate = platesData?.is_multi_plate ?? false;
|
|
const isMultiPlate = platesData?.is_multi_plate ?? false;
|
|
|
const plates = platesData?.plates ?? [];
|
|
const plates = platesData?.plates ?? [];
|
|
|
|
|
+ // The plates that will print, beside the buttons: in a long plate list the
|
|
|
|
|
+ // selected card can be far out of view (#3395).
|
|
|
|
|
+ const selectedPlatesSummary =
|
|
|
|
|
+ !isCrossModel && isMultiPlate && plates.length > 1
|
|
|
|
|
+ ? plates
|
|
|
|
|
+ .filter((p) => selectedPlates.has(p.index))
|
|
|
|
|
+ .map((p) => {
|
|
|
|
|
+ const label = t('printModal.plateN', 'Plate {{n}}', { n: p.index });
|
|
|
|
|
+ const name = p.name?.trim();
|
|
|
|
|
+ return name && name !== label ? `${label} – ${name}` : label;
|
|
|
|
|
+ })
|
|
|
|
|
+ .join(', ')
|
|
|
|
|
+ : '';
|
|
|
|
|
|
|
|
const filamentWarningMessage = useMemo(() => {
|
|
const filamentWarningMessage = useMemo(() => {
|
|
|
if (!filamentWarningItems || filamentWarningItems.length === 0) return '';
|
|
if (!filamentWarningItems || filamentWarningItems.length === 0) return '';
|
|
@@ -2147,6 +2172,17 @@ export function PrintModal({
|
|
|
</div>
|
|
</div>
|
|
|
)}
|
|
)}
|
|
|
|
|
|
|
|
|
|
+ {selectedPlatesSummary && (
|
|
|
|
|
+ <p
|
|
|
|
|
+ className="flex items-center gap-1.5 text-sm text-bambu-gray min-w-0"
|
|
|
|
|
+ title={selectedPlatesSummary}
|
|
|
|
|
+ data-testid="selected-plates-summary"
|
|
|
|
|
+ >
|
|
|
|
|
+ <Layers className="w-4 h-4 flex-shrink-0" />
|
|
|
|
|
+ <span className="truncate">{selectedPlatesSummary}</span>
|
|
|
|
|
+ </p>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
{/* Actions */}
|
|
{/* Actions */}
|
|
|
<div className="flex gap-3 pt-2">
|
|
<div className="flex gap-3 pt-2">
|
|
|
<Button type="button" variant="secondary" onClick={onClose} className="flex-1" disabled={isSubmitting}>
|
|
<Button type="button" variant="secondary" onClick={onClose} className="flex-1" disabled={isSubmitting}>
|