Files
bambuddy/frontend/src/components/PrintModal/PlateSelector.tsx
T
maziggy eaf0a1c281 Add queue timeline view, visual refresh, and fix plate thumbnail auth
Queue page redesign: compact stats bar replaces summary cards, color-
  coded left borders for status scanning, collapsible history with
  condensed rows, and a new production schedule timeline view (#823)
  showing estimated completion times grouped by hour with filter tabs
  and day navigation. i18n keys added for all 7 languages.

  Fix plate thumbnails returning 401 in print modal when auth is enabled
  (missing stream token). Fix schedule calendar picker opening off-screen
  (hidden input positioned with sr-only instead of near the date field).
2026-03-28 13:45:37 +01:00

110 lines
4.3 KiB
TypeScript

import { Layers, Check, AlertTriangle, Square, CheckSquare } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import type { PlateSelectorProps } from './types';
import { formatDuration } from '../../utils/date';
import { withStreamToken } from '../../api/client';
/**
* Plate selection grid for multi-plate 3MF files.
* Shows thumbnails, names, objects, and print times for each plate.
* In multi-select mode (add-to-queue), plates have checkboxes for selecting a subset.
* In single-select mode (reprint/edit), only one plate can be selected at a time.
*/
export function PlateSelector({
plates,
isMultiPlate,
selectedPlates,
onToggle,
onSelectAll,
onDeselectAll,
multiSelect,
}: PlateSelectorProps) {
const { t } = useTranslation();
// Only show for multi-plate files with multiple plates
if (!isMultiPlate || plates.length <= 1) {
return null;
}
const allSelected = selectedPlates.size === plates.length;
return (
<div className="mb-4">
<div className="flex items-center gap-2 mb-2">
<Layers className="w-4 h-4 text-bambu-gray" />
<span className="text-sm text-bambu-gray">Select Plate{multiSelect ? 's' : ''} to Print</span>
{selectedPlates.size === 0 && (
<span className="text-xs text-orange-400 flex items-center gap-1">
<AlertTriangle className="w-3 h-3" />
Selection required
</span>
)}
{multiSelect && onSelectAll && onDeselectAll && (
<button
type="button"
onClick={allSelected ? onDeselectAll : onSelectAll}
className={`ml-auto text-xs px-2 py-0.5 rounded-full border transition-colors ${
allSelected
? 'border-bambu-green bg-bambu-green/10 text-bambu-green'
: 'border-bambu-dark-tertiary text-bambu-gray hover:border-bambu-gray'
}`}
>
{allSelected
? t('queue.deselectAll')
: t('queue.selectAllPlates', { count: plates.length })}
</button>
)}
</div>
<div className="grid grid-cols-2 gap-2">
{plates.map((plate) => {
const isSelected = selectedPlates.has(plate.index);
return (
<button
key={plate.index}
type="button"
onClick={() => onToggle(plate.index)}
className={`flex items-center gap-2 p-2 rounded-lg border transition-colors text-left ${
isSelected
? 'border-bambu-green bg-bambu-green/10'
: 'border-bambu-dark-tertiary bg-bambu-dark hover:border-bambu-gray'
}`}
>
{multiSelect && (
isSelected
? <CheckSquare className="w-4 h-4 text-bambu-green flex-shrink-0" />
: <Square className="w-4 h-4 text-bambu-gray flex-shrink-0" />
)}
{plate.has_thumbnail && plate.thumbnail_url != null ? (
<img
src={withStreamToken(plate.thumbnail_url)}
alt={`Plate ${plate.index}`}
className="w-10 h-10 rounded object-cover bg-bambu-dark-tertiary"
/>
) : (
<div className="w-10 h-10 rounded bg-bambu-dark-tertiary flex items-center justify-center">
<Layers className="w-5 h-5 text-bambu-gray" />
</div>
)}
<div className="min-w-0 flex-1">
<p className="text-sm text-white font-medium truncate">
{plate.name || `Plate ${plate.index}`}
</p>
<p className="text-xs text-bambu-gray truncate">
{plate.objects.length > 0
? plate.objects.slice(0, 3).join(', ') +
(plate.objects.length > 3 ? '...' : '')
: `${plate.filaments.length} filament${plate.filaments.length !== 1 ? 's' : ''}`}
{plate.print_time_seconds != null ? ` • ${formatDuration(plate.print_time_seconds)}` : ''}
</p>
</div>
{!multiSelect && isSelected && (
<Check className="w-4 h-4 text-bambu-green flex-shrink-0" />
)}
</button>
);
})}
</div>
</div>
);
}