mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-18 21:47:39 +02:00
feat: dual external spool support, AMS slot model filtering & pre-population
Backend: - Add dual external spool support for H2D (vt_tray as list: Ext-L/Ext-R) - Add cloud filament ID map endpoint (/cloud/filament-id-map) - Fix RFID spool data erased by periodic AMS updates (skip tag matcher for RFID-tagged trays) - Fix AMS slot config overwrites RFID spool state - Fix K-profile selection corrupts existing profiles on X1C/P1S - Resolve K-profiles filament name via cloud filament ID map - Update print scheduler and usage tracker for dual external spools Frontend: - Add printer model filtering to ConfigureAmsSlotModal (cloud/local/builtin presets filtered by @BBL model suffix and compatible_printers) - Add pre-population for configured slots (preset, color, K-profile) - Add K-Profiles view with accurate filament name resolution - Internationalize all ConfigureAmsSlotModal strings (en/de/fr/it/ja — 21 keys) - Add 5 new ConfigureAmsSlotModal tests (model filtering, pre-selection, color pre-population, i18n) - Update PrintersPage for dual external spool rendering Docs: - Update CHANGELOG, README, website features, and wiki AMS docs
This commit is contained in:
@@ -14,6 +14,9 @@ interface SlotInfo {
|
||||
trayColor?: string;
|
||||
traySubBrands?: string;
|
||||
trayInfoIdx?: string;
|
||||
extruderId?: number;
|
||||
caliIdx?: number | null;
|
||||
savedPresetId?: string;
|
||||
}
|
||||
|
||||
// Get proper AMS label (handles HT AMS with ID 128+)
|
||||
@@ -72,6 +75,7 @@ interface ConfigureAmsSlotModalProps {
|
||||
printerId: number;
|
||||
slotInfo: SlotInfo;
|
||||
nozzleDiameter?: string;
|
||||
printerModel?: string;
|
||||
onSuccess?: () => void;
|
||||
}
|
||||
|
||||
@@ -209,12 +213,23 @@ function colorNameToHex(name: string): string | null {
|
||||
return COLOR_NAME_MAP[normalized] || null;
|
||||
}
|
||||
|
||||
// Extract printer model from preset name suffix "@BBL X1C 0.4 nozzle" → "X1C"
|
||||
function extractPresetModel(name: string): string | null {
|
||||
const atIdx = name.indexOf('@');
|
||||
if (atIdx < 0) return null;
|
||||
const suffix = name.slice(atIdx + 1).trim();
|
||||
const bblMatch = suffix.match(/^BBL\s+(.+?)(?:\s+[\d.]+\s*nozzle)?$/i);
|
||||
if (bblMatch) return bblMatch[1].trim();
|
||||
return null;
|
||||
}
|
||||
|
||||
export function ConfigureAmsSlotModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
printerId,
|
||||
slotInfo,
|
||||
nozzleDiameter = '0.4',
|
||||
printerModel,
|
||||
onSuccess,
|
||||
}: ConfigureAmsSlotModalProps) {
|
||||
const { t } = useTranslation();
|
||||
@@ -256,6 +271,14 @@ export function ConfigureAmsSlotModal({
|
||||
enabled: isOpen && !!printerId,
|
||||
});
|
||||
|
||||
// Fetch color catalog
|
||||
const { data: colorCatalog } = useQuery({
|
||||
queryKey: ['colorCatalog'],
|
||||
queryFn: () => api.getColorCatalog(),
|
||||
enabled: isOpen,
|
||||
staleTime: Infinity,
|
||||
});
|
||||
|
||||
// Configure slot mutation
|
||||
const configureMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
@@ -435,22 +458,44 @@ export function ConfigureAmsSlotModal({
|
||||
// Collect IDs already covered by cloud and local to avoid duplicates in fallback
|
||||
const coveredIds = new Set<string>();
|
||||
|
||||
// Currently-configured preset should always be shown (bypass model filter)
|
||||
const savedId = slotInfo.savedPresetId;
|
||||
const trayIdx = slotInfo.trayInfoIdx;
|
||||
|
||||
// 1. Cloud presets
|
||||
if (cloudSettings?.filament) {
|
||||
for (const cp of cloudSettings.filament) {
|
||||
coveredIds.add(cp.setting_id);
|
||||
if (!query || cp.name.toLowerCase().includes(query)) {
|
||||
items.push({ id: cp.setting_id, name: cp.name, source: 'cloud', isUser: isUserPreset(cp.setting_id) });
|
||||
// Keep preset if it matches the slot's saved mapping or current tray_info_idx
|
||||
const isCurrentPreset = savedId === cp.setting_id
|
||||
|| (trayIdx && (cp.setting_id === trayIdx || convertToTrayInfoIdx(cp.setting_id) === trayIdx));
|
||||
if (!isCurrentPreset && query && !cp.name.toLowerCase().includes(query)) continue;
|
||||
// Filter by printer model if set (skip for current preset)
|
||||
if (!isCurrentPreset && printerModel) {
|
||||
const presetModel = extractPresetModel(cp.name);
|
||||
if (presetModel && presetModel.toUpperCase() !== printerModel.toUpperCase()) continue;
|
||||
}
|
||||
items.push({ id: cp.setting_id, name: cp.name, source: 'cloud', isUser: isUserPreset(cp.setting_id) });
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Local presets
|
||||
if (localPresets?.filament) {
|
||||
for (const lp of localPresets.filament) {
|
||||
if (!query || lp.name.toLowerCase().includes(query)) {
|
||||
items.push({ id: `local_${lp.id}`, name: lp.name, source: 'local', isUser: false });
|
||||
const localId = `local_${lp.id}`;
|
||||
const isSaved = savedId === localId;
|
||||
if (!isSaved && query && !lp.name.toLowerCase().includes(query)) continue;
|
||||
// Filter by compatible_printers if set (skip for saved preset)
|
||||
if (!isSaved && printerModel && lp.compatible_printers) {
|
||||
const compatModels = lp.compatible_printers.split(';').map(p => {
|
||||
// Extract model from "BBL X1C" → "X1C"
|
||||
const trimmed = p.trim();
|
||||
const bblMatch = trimmed.match(/^BBL\s+(.+)/i);
|
||||
return bblMatch ? bblMatch[1].trim().toUpperCase() : trimmed.toUpperCase();
|
||||
}).filter(Boolean);
|
||||
if (compatModels.length > 0 && !compatModels.includes(printerModel.toUpperCase())) continue;
|
||||
}
|
||||
items.push({ id: localId, name: lp.name, source: 'local', isUser: false });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -478,7 +523,7 @@ export function ConfigureAmsSlotModal({
|
||||
if (!a.isUser && b.isUser) return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
}, [cloudSettings?.filament, localPresets?.filament, builtinFilaments, searchQuery]);
|
||||
}, [cloudSettings?.filament, localPresets?.filament, builtinFilaments, searchQuery, printerModel, slotInfo.savedPresetId, slotInfo.trayInfoIdx]);
|
||||
|
||||
// Get full preset name for K profile filtering (brand + material, without printer suffix)
|
||||
const selectedPresetInfo = useMemo(() => {
|
||||
@@ -518,6 +563,41 @@ export function ConfigureAmsSlotModal({
|
||||
// For backwards compatibility with the label
|
||||
const selectedMaterial = selectedPresetInfo?.fullName || '';
|
||||
|
||||
// Filter color catalog entries matching the selected preset's brand + material
|
||||
const catalogColors = useMemo(() => {
|
||||
if (!colorCatalog || !selectedPresetInfo) return [];
|
||||
|
||||
const { fullName, brand } = selectedPresetInfo;
|
||||
|
||||
// Try to find colors matching the full preset name (e.g., "PLA Metal")
|
||||
// The catalog uses the variant as part of the material field (e.g., material="PLA Metal")
|
||||
// Extract the full material+variant from the preset name
|
||||
const materialVariant = fullName.replace(/^(Bambu\s*(Lab)?|eSUN|Polymaker|Overture|Sunlu|Hatchbox)\s*/i, '').trim();
|
||||
|
||||
return colorCatalog.filter(entry => {
|
||||
const entryMaterial = (entry.material || '').toUpperCase();
|
||||
const entryManufacturer = entry.manufacturer.toUpperCase();
|
||||
|
||||
// Match material: try full material+variant first, then just material type
|
||||
const materialMatch = entryMaterial === materialVariant.toUpperCase()
|
||||
|| entryMaterial.includes(materialVariant.toUpperCase())
|
||||
|| materialVariant.toUpperCase().includes(entryMaterial);
|
||||
|
||||
if (!materialMatch) return false;
|
||||
|
||||
// If brand is present, also match manufacturer
|
||||
if (brand) {
|
||||
const upperBrand = brand.toUpperCase();
|
||||
// Fuzzy match: "Bambu" matches "Bambu Lab", etc.
|
||||
if (!entryManufacturer.includes(upperBrand) && !upperBrand.includes(entryManufacturer)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}, [colorCatalog, selectedPresetInfo]);
|
||||
|
||||
const matchingKProfiles = useMemo(() => {
|
||||
if (!kprofilesData?.profiles || !selectedPresetInfo) return [];
|
||||
|
||||
@@ -575,34 +655,52 @@ export function ConfigureAmsSlotModal({
|
||||
});
|
||||
|
||||
// Deduplicate profiles with same name and k_value (multi-nozzle printers have duplicates)
|
||||
// Prefer extruder_id=1 (High Flow) profiles as they're more commonly used on H2D
|
||||
// Prefer the profile matching the slot's extruder (e.g. ext-R uses extruder 0, ext-L uses extruder 1)
|
||||
const seen = new Map<string, KProfile>();
|
||||
for (const profile of filtered) {
|
||||
const key = `${profile.name}|${profile.k_value}`;
|
||||
const existing = seen.get(key);
|
||||
if (!existing) {
|
||||
seen.set(key, profile);
|
||||
} else if (profile.extruder_id === 1 && existing.extruder_id === 0) {
|
||||
// Replace extruder_id=0 profile with extruder_id=1 (High Flow) profile
|
||||
} else if (slotInfo.extruderId !== undefined && profile.extruder_id === slotInfo.extruderId && existing.extruder_id !== slotInfo.extruderId) {
|
||||
// Replace with profile matching slot's extruder
|
||||
seen.set(key, profile);
|
||||
}
|
||||
}
|
||||
return Array.from(seen.values());
|
||||
}, [kprofilesData?.profiles, selectedPresetInfo]);
|
||||
}, [kprofilesData?.profiles, selectedPresetInfo, slotInfo.extruderId]);
|
||||
|
||||
// Pre-select current profile when modal opens, reset when closes
|
||||
useEffect(() => {
|
||||
if (isOpen && cloudSettings?.filament) {
|
||||
// Try to pre-select current profile based on trayInfoIdx
|
||||
if (slotInfo.trayInfoIdx) {
|
||||
const currentPreset = cloudSettings.filament.find(
|
||||
if (isOpen) {
|
||||
// Pre-populate from saved preset mapping (most reliable)
|
||||
if (slotInfo.savedPresetId) {
|
||||
setSelectedPresetId(slotInfo.savedPresetId);
|
||||
} else if (slotInfo.trayInfoIdx && cloudSettings?.filament) {
|
||||
// Fallback: try to match by tray_info_idx in cloud presets
|
||||
// First try exact match on setting_id
|
||||
let currentPreset = cloudSettings.filament.find(
|
||||
p => p.setting_id === slotInfo.trayInfoIdx
|
||||
);
|
||||
// Then try matching by converting setting_id → filament_id format
|
||||
if (!currentPreset) {
|
||||
currentPreset = cloudSettings.filament.find(
|
||||
p => convertToTrayInfoIdx(p.setting_id) === slotInfo.trayInfoIdx
|
||||
);
|
||||
}
|
||||
if (currentPreset) {
|
||||
setSelectedPresetId(currentPreset.setting_id);
|
||||
}
|
||||
}
|
||||
} else if (!isOpen) {
|
||||
|
||||
// Pre-populate color from current slot
|
||||
if (slotInfo.trayColor) {
|
||||
const hex = slotInfo.trayColor.slice(0, 6);
|
||||
if (hex && hex !== '000000') {
|
||||
setColorHex(hex);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Reset when modal closes
|
||||
setSelectedPresetId('');
|
||||
setSelectedKProfile(null);
|
||||
@@ -611,17 +709,25 @@ export function ConfigureAmsSlotModal({
|
||||
setSearchQuery('');
|
||||
setShowSuccess(false);
|
||||
}
|
||||
}, [isOpen, cloudSettings?.filament, slotInfo.trayInfoIdx]);
|
||||
}, [isOpen, slotInfo.savedPresetId, slotInfo.trayInfoIdx, slotInfo.trayColor, cloudSettings?.filament]);
|
||||
|
||||
// Auto-select best matching K profile when preset changes
|
||||
useEffect(() => {
|
||||
if (matchingKProfiles.length > 0) {
|
||||
// Auto-select first matching profile
|
||||
// Prefer the currently-active K-profile (by cali_idx) if available
|
||||
if (slotInfo.caliIdx != null && slotInfo.caliIdx > 0) {
|
||||
const active = matchingKProfiles.find(p => p.slot_id === slotInfo.caliIdx);
|
||||
if (active) {
|
||||
setSelectedKProfile(active);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Fallback: first matching profile
|
||||
setSelectedKProfile(matchingKProfiles[0]);
|
||||
} else {
|
||||
setSelectedKProfile(null);
|
||||
}
|
||||
}, [selectedPresetId, matchingKProfiles]);
|
||||
}, [selectedPresetId, matchingKProfiles, slotInfo.caliIdx]);
|
||||
|
||||
// Escape key handler
|
||||
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
||||
@@ -659,7 +765,7 @@ export function ConfigureAmsSlotModal({
|
||||
<div className="flex items-center justify-between p-4 border-b border-bambu-dark-tertiary">
|
||||
<div className="flex items-center gap-2">
|
||||
<Settings2 className="w-5 h-5 text-bambu-blue" />
|
||||
<h2 className="text-lg font-semibold text-white">Configure AMS Slot</h2>
|
||||
<h2 className="text-lg font-semibold text-white">{t('configureAmsSlot.title')}</h2>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
@@ -676,7 +782,7 @@ export function ConfigureAmsSlotModal({
|
||||
<div className="absolute inset-0 bg-bambu-dark-secondary/95 z-10 flex items-center justify-center rounded-xl">
|
||||
<div className="text-center space-y-3">
|
||||
<CheckCircle2 className="w-16 h-16 text-bambu-green mx-auto" />
|
||||
<p className="text-lg font-semibold text-white">Slot Configured!</p>
|
||||
<p className="text-lg font-semibold text-white">{t('configureAmsSlot.slotConfigured')}</p>
|
||||
<p className="text-sm text-bambu-gray">{t('configureAmsSlot.settingsSentToPrinter')}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -684,7 +790,7 @@ export function ConfigureAmsSlotModal({
|
||||
|
||||
{/* Slot info */}
|
||||
<div className="p-3 bg-bambu-dark rounded-lg border border-bambu-dark-tertiary">
|
||||
<p className="text-xs text-bambu-gray mb-1">Configuring slot:</p>
|
||||
<p className="text-xs text-bambu-gray mb-1">{t('configureAmsSlot.configuringSlot')}</p>
|
||||
<div className="flex items-center gap-2">
|
||||
{slotInfo.trayColor && (
|
||||
<span
|
||||
@@ -693,7 +799,7 @@ export function ConfigureAmsSlotModal({
|
||||
/>
|
||||
)}
|
||||
<span className="text-white font-medium">
|
||||
{getAmsLabel(slotInfo.amsId, slotInfo.trayCount)} Slot {slotInfo.trayId + 1}
|
||||
{t('configureAmsSlot.slotLabel', { ams: getAmsLabel(slotInfo.amsId, slotInfo.trayCount), slot: slotInfo.trayId + 1 })}
|
||||
</span>
|
||||
{slotInfo.traySubBrands && (
|
||||
<span className="text-bambu-gray">({slotInfo.traySubBrands})</span>
|
||||
@@ -710,7 +816,7 @@ export function ConfigureAmsSlotModal({
|
||||
{/* Filament Profile Select */}
|
||||
<div>
|
||||
<label className="block text-sm text-bambu-gray mb-2">
|
||||
Filament Profile <span className="text-red-400">*</span>
|
||||
{t('configureAmsSlot.filamentProfile')} <span className="text-red-400">*</span>
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
@@ -724,13 +830,16 @@ export function ConfigureAmsSlotModal({
|
||||
{filteredPresets.length === 0 ? (
|
||||
<p className="text-center py-4 text-bambu-gray">
|
||||
{(cloudSettings?.filament?.length === 0 && !localPresets?.filament?.length)
|
||||
? 'No presets available. Login to Bambu Cloud or import local profiles.'
|
||||
: 'No matching presets found.'}
|
||||
? t('configureAmsSlot.noPresetsAvailable')
|
||||
: t('configureAmsSlot.noMatchingPresets')}
|
||||
</p>
|
||||
) : (
|
||||
filteredPresets.map((preset) => (
|
||||
<button
|
||||
key={preset.id}
|
||||
ref={selectedPresetId === preset.id ? (el) => {
|
||||
el?.scrollIntoView({ block: 'nearest' });
|
||||
} : undefined}
|
||||
onClick={() => setSelectedPresetId(preset.id)}
|
||||
className={`w-full p-2 rounded-lg border text-left transition-colors ${
|
||||
selectedPresetId === preset.id
|
||||
@@ -768,10 +877,10 @@ export function ConfigureAmsSlotModal({
|
||||
{/* K Profile Select */}
|
||||
<div>
|
||||
<label className="block text-sm text-bambu-gray mb-2">
|
||||
K Profile (Pressure Advance)
|
||||
{t('configureAmsSlot.kProfileLabel')}
|
||||
{selectedMaterial && (
|
||||
<span className="ml-2 text-xs text-bambu-blue">
|
||||
Filtering for: {selectedMaterial}
|
||||
{t('configureAmsSlot.filteringFor', { material: selectedMaterial })}
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
@@ -785,7 +894,7 @@ export function ConfigureAmsSlotModal({
|
||||
}}
|
||||
className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none appearance-none pr-10"
|
||||
>
|
||||
<option value="">No K profile (use default 0.020)</option>
|
||||
<option value="">{t('configureAmsSlot.noKProfile')}</option>
|
||||
{matchingKProfiles.map((profile) => (
|
||||
<option key={`${profile.name}-${profile.extruder_id}`} value={profile.name}>
|
||||
{profile.name} (K={profile.k_value})
|
||||
@@ -796,16 +905,16 @@ export function ConfigureAmsSlotModal({
|
||||
</div>
|
||||
) : selectedPresetId ? (
|
||||
<p className="text-sm text-bambu-gray italic py-2">
|
||||
No matching K profiles found. Default K=0.020 will be used.
|
||||
{t('configureAmsSlot.noMatchingKProfiles')}
|
||||
</p>
|
||||
) : (
|
||||
<span className="inline-block text-xs px-2 py-1 rounded bg-amber-500/20 text-amber-400 border border-amber-500/30">
|
||||
Select a filament profile first
|
||||
{t('configureAmsSlot.selectFilamentFirst')}
|
||||
</span>
|
||||
)}
|
||||
{selectedKProfile && (
|
||||
<p className="text-xs text-bambu-green mt-1">
|
||||
K={selectedKProfile.k_value} from printer calibration
|
||||
{t('configureAmsSlot.kFromCalibration', { value: selectedKProfile.k_value })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -813,8 +922,40 @@ export function ConfigureAmsSlotModal({
|
||||
{/* Optional: Custom color */}
|
||||
<div>
|
||||
<label className="block text-sm text-bambu-gray mb-2">
|
||||
Custom Color (optional)
|
||||
{t('configureAmsSlot.customColorLabel')}
|
||||
</label>
|
||||
{/* Catalog colors matching selected preset */}
|
||||
{catalogColors.length > 0 && (
|
||||
<div className="mb-3">
|
||||
<p className="text-xs text-bambu-gray mb-1.5">
|
||||
{t('configureAmsSlot.presetColors', { name: selectedPresetInfo?.fullName })}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{catalogColors.map((entry) => (
|
||||
<button
|
||||
key={entry.id}
|
||||
onClick={() => {
|
||||
const hex = entry.hex_color.replace('#', '').toUpperCase();
|
||||
setColorHex(hex);
|
||||
setColorInput(entry.color_name);
|
||||
}}
|
||||
className={`h-7 px-2 rounded-md border-2 transition-all flex items-center gap-1.5 ${
|
||||
colorHex === entry.hex_color.replace('#', '').toUpperCase()
|
||||
? 'border-bambu-green scale-105'
|
||||
: 'border-white/20 hover:border-white/40'
|
||||
}`}
|
||||
title={entry.color_name}
|
||||
>
|
||||
<span
|
||||
className="w-4 h-4 rounded-full border border-white/30 flex-shrink-0"
|
||||
style={{ backgroundColor: entry.hex_color }}
|
||||
/>
|
||||
<span className="text-xs text-white/80 whitespace-nowrap">{entry.color_name}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Quick color buttons */}
|
||||
<div className="flex flex-wrap gap-1.5 mb-2">
|
||||
{QUICK_COLORS_BASIC.map((color) => (
|
||||
@@ -836,7 +977,7 @@ export function ConfigureAmsSlotModal({
|
||||
<button
|
||||
onClick={() => setShowExtendedColors(!showExtendedColors)}
|
||||
className="w-7 h-7 rounded-md border-2 border-white/20 hover:border-white/40 flex items-center justify-center text-white/60 hover:text-white/80 transition-all text-xs"
|
||||
title={showExtendedColors ? 'Show less colors' : 'Show more colors'}
|
||||
title={showExtendedColors ? t('configureAmsSlot.showLessColors') : t('configureAmsSlot.showMoreColors')}
|
||||
>
|
||||
{showExtendedColors ? '−' : '+'}
|
||||
</button>
|
||||
@@ -902,13 +1043,13 @@ export function ConfigureAmsSlotModal({
|
||||
className="px-2 py-1 text-xs text-bambu-gray hover:text-white bg-bambu-dark-tertiary rounded"
|
||||
title={t('configureAmsSlot.clearCustomColor')}
|
||||
>
|
||||
Clear
|
||||
{t('configureAmsSlot.clear')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{colorHex && (
|
||||
<p className="text-xs text-bambu-gray mt-1.5">
|
||||
Hex: #{colorHex}
|
||||
{t('configureAmsSlot.hexLabel', { hex: colorHex })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -928,19 +1069,19 @@ export function ConfigureAmsSlotModal({
|
||||
{resetMutation.isPending ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
Resetting...
|
||||
{t('configureAmsSlot.resetting')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RotateCcw className="w-4 h-4" />
|
||||
Reset Slot
|
||||
{t('configureAmsSlot.resetSlot')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
{/* Cancel and Configure buttons on the right */}
|
||||
<div className="flex gap-2">
|
||||
<Button variant="secondary" onClick={onClose}>
|
||||
Cancel
|
||||
{t('configureAmsSlot.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => configureMutation.mutate()}
|
||||
@@ -949,12 +1090,12 @@ export function ConfigureAmsSlotModal({
|
||||
{configureMutation.isPending ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
Configuring...
|
||||
{t('configureAmsSlot.configuring')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Settings2 className="w-4 h-4" />
|
||||
Configure Slot
|
||||
{t('configureAmsSlot.configureSlot')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user