Merge branch 'dev' into feature/oidc-env-config

This commit is contained in:
MartinNYHC
2026-08-01 11:22:58 +02:00
committed by GitHub
35 changed files with 2197 additions and 539 deletions
@@ -8,6 +8,7 @@ import { matchesPrinterModelSuffix, presetCompatibility, buildCompatibilityIndex
import { toFilamentId } from './spool-form/utils';
import { Button } from './Button';
import { getAmsLabel } from '../utils/amsHelpers';
import { useCancellableTimeout } from '../hooks/useCancellableTimeout';
interface SlotInfo {
amsId: number;
@@ -305,6 +306,9 @@ export function ConfigureAmsSlotModal({
const [showSuccess, setShowSuccess] = useState(false);
const [showExtendedColors, setShowExtendedColors] = useState(false);
const scrolledToRef = useRef<string>('');
// The success state is held briefly before the modal closes itself; that
// timer must not outlive the modal.
const { schedule: scheduleClose } = useCancellableTimeout();
// Fetch cloud settings (gracefully handle 401 when logged out)
const { data: cloudSettings, isLoading: settingsLoading, isError: cloudError } = useQuery({
@@ -614,7 +618,7 @@ export function ConfigureAmsSlotModal({
setShowSuccess(true);
onSuccess?.();
// Close after showing success briefly
setTimeout(() => {
scheduleClose(() => {
setShowSuccess(false);
onClose();
}, 1500);
@@ -629,7 +633,7 @@ export function ConfigureAmsSlotModal({
onSuccess: () => {
setShowSuccess(true);
onSuccess?.();
setTimeout(() => {
scheduleClose(() => {
setShowSuccess(false);
onClose();
}, 1500);
+269 -113
View File
@@ -21,10 +21,17 @@ import {
} from 'lucide-react';
import { api } from '../api/client';
import type { KProfile, KProfileCreate, KProfileDelete, Permission } from '../api/client';
import {
buildFilamentPresetOptions,
resolveFilamentId,
type FilamentPresetOption,
type FilamentPresetSource,
} from '../utils/filamentPresets';
import { Card, CardContent } from './Card';
import { Button } from './Button';
import { useToast } from '../contexts/ToastContext';
import { useAuth } from '../contexts/AuthContext';
import { useCancellableTimeout } from '../hooks/useCancellableTimeout';
interface KProfileCardProps {
profile: KProfile;
@@ -42,18 +49,26 @@ const truncateK = (value: string) => {
return (Math.trunc(num * 1000) / 1000).toFixed(3);
};
// Get flow type label from nozzle_id (e.g., "HH00-0.4" -> "HF", "HS00-0.4" -> "S")
const getFlowTypeLabel = (nozzleId: string) => {
if (nozzleId.startsWith('HH')) return 'HF'; // High Flow
return 'S'; // Standard Flow (default)
};
// nozzle_id encodes the flow type, per the slicer's own generator:
// "H" + (Standard ? "S" : "H") + "00" + "-" + diameter
// so "HS00-0.4" is Standard and "HH00-0.4" is High Flow. The "00" is a literal,
// not a material code.
const STANDARD_FLOW = 'HS00';
const HIGH_FLOW = 'HH00';
// Extract nozzle type prefix from nozzle_id (e.g., "HH00-0.4" -> "HH00")
// Many printers omit nozzle_id from their extrusion_cali_get response entirely
// (#1748) — the field simply isn't in the payload. BambuStudio treats that as
// Standard (its parser falls back to nvtStandard when the key is absent), and
// so do we: the flow type stays a real, editable value rather than a blank.
const getNozzleTypePrefix = (nozzleId: string) => {
const match = nozzleId.match(/^([A-Z]{2}\d{2})/);
return match ? match[1] : 'HH00';
return match ? match[1] : STANDARD_FLOW;
};
// Short label for the profile list.
const getFlowTypeLabel = (nozzleId: string) =>
getNozzleTypePrefix(nozzleId) === HIGH_FLOW ? 'HF' : 'S';
// Extract filament name from profile name (e.g., "High Flow_Devil Design PLA Basic" -> "Devil Design PLA Basic")
const extractFilamentName = (profileName: string) => {
// Profile names are formatted as "{Flow Type}_{Filament Name}" or "{Flow Type} {Filament Name}"
@@ -149,9 +164,11 @@ interface KProfileModalProps {
profile?: KProfile;
printerId: number;
nozzleDiameter: string;
existingProfiles?: KProfile[]; // Existing profiles for filament selection
existingProfiles?: KProfile[]; // Existing profiles, used for name resolution
builtinFilaments?: { filament_id: string; name: string }[]; // Filament ID → name lookup
filamentPresets?: FilamentPresetOption[]; // Every filament this install knows, tiered
isDualNozzle?: boolean; // Whether this is a dual-nozzle printer
supportsFlowType?: boolean; // Model sells both Standard and High Flow nozzles
initialNote?: string; // Initial note value for the profile
initialNoteKey?: string | null; // Key the note was stored under (for clearing)
onClose: () => void;
@@ -166,7 +183,9 @@ function KProfileModal({
nozzleDiameter,
existingProfiles = [],
builtinFilaments = [],
filamentPresets = [],
isDualNozzle = false,
supportsFlowType = true,
initialNote = '',
initialNoteKey = null,
onClose,
@@ -181,10 +200,19 @@ function KProfileModal({
const [kValue, setKValue] = useState(
profile?.k_value ? truncateK(profile.k_value) : '0.020'
);
const [filamentId, setFilamentId] = useState(profile?.filament_id || '');
// What the Filament select is bound to. When editing, the printer's own
// filament_id (the select is read-only). For a new profile, the *preset
// handle* from the tiered list — a local row id, an Orca UUID, a Bambu Cloud
// setting_id or a builtin filament id — which is resolved to a real
// filament_id on submit, since only some tiers carry one directly.
const [filamentChoice, setFilamentChoice] = useState(profile?.filament_id || '');
// Split nozzle into type and diameter
// Both selects are read-only while editing: they report what the printer
// holds, they don't set it. '' means the printer reported no nozzle_id, which
// single-nozzle models never do (#1748) — showing "High Flow" there was the
// UI inventing a value the printer never sent.
const [nozzleType, setNozzleType] = useState(
profile?.nozzle_id ? getNozzleTypePrefix(profile.nozzle_id) : 'HH00'
profile ? getNozzleTypePrefix(profile.nozzle_id) : STANDARD_FLOW
);
const [modalDiameter, setModalDiameter] = useState(
profile?.nozzle_diameter || nozzleDiameter
@@ -197,40 +225,49 @@ function KProfileModal({
const [isSyncing, setIsSyncing] = useState(false);
const [savingProgress, setSavingProgress] = useState({ current: 0, total: 0 });
const [note, setNote] = useState(initialNote);
const [filamentQuery, setFilamentQuery] = useState('');
// The modal defers its own close so the printer has time to process the
// command; that timer must not outlive the modal.
const { schedule: scheduleClose } = useCancellableTimeout();
// Extract unique filaments from existing K-profiles on the printer
// Use builtin filament table for accurate name resolution (filament_id → name)
// Falls back to extracting from profile name for custom/unknown presets
const knownFilaments = React.useMemo(() => {
// Build lookup map from builtin filament names (includes cloud presets from parent)
const builtinMap = new Map<string, string>();
for (const bf of builtinFilaments) {
builtinMap.set(bf.filament_id, bf.name);
}
// Name for the filament an existing profile is bound to. The builtin table
// (which the parent has already merged with the user's cloud presets) is
// authoritative; a profile whose filament_id is in neither falls back to the
// name the printer stored for it.
const editedFilamentName = React.useMemo(() => {
if (!profile?.filament_id) return '';
const builtinName = builtinFilaments.find(bf => bf.filament_id === profile.filament_id)?.name;
if (builtinName) return builtinName;
const fromProfile = existingProfiles.find(p => p.filament_id === profile.filament_id);
return extractFilamentName(fromProfile?.name || profile.name || '') || profile.filament_id;
}, [profile, existingProfiles, builtinFilaments]);
const filamentMap = new Map<string, { id: string; name: string }>();
for (const p of existingProfiles) {
if (p.filament_id && !filamentMap.has(p.filament_id)) {
// Prefer builtin name (accurate), fall back to extracting from profile name
const builtinName = builtinMap.get(p.filament_id);
const filamentName = builtinName || extractFilamentName(p.name || '');
filamentMap.set(p.filament_id, {
id: p.filament_id,
name: filamentName || p.filament_id,
});
}
}
return Array.from(filamentMap.values()).sort((a, b) =>
a.name.localeCompare(b.name)
);
}, [existingProfiles, builtinFilaments]);
// The tiered list, grouped for rendering. Order is fixed app-wide —
// imported, then Orca Cloud, then Bambu Cloud, then the hardcoded table —
// and buildFilamentPresetOptions has already sorted by it, so grouping is
// just a partition that preserves that order.
const presetGroups = React.useMemo(() => {
const labels: [FilamentPresetSource, string][] = [
['local', t('kProfiles.modal.source.local')],
['orca_cloud', t('kProfiles.modal.source.orcaCloud')],
['cloud', t('kProfiles.modal.source.bambuCloud')],
['builtin', t('kProfiles.modal.source.builtin')],
];
const query = filamentQuery.trim().toLowerCase();
const matches = query
? filamentPresets.filter(p => p.name.toLowerCase().includes(query))
: filamentPresets;
return labels
.map(([source, label]) => ({ source, label, items: matches.filter(p => p.source === source) }))
.filter(g => g.items.length > 0);
}, [filamentPresets, filamentQuery, t]);
const saveMutation = useMutation({
mutationFn: (data: KProfileCreate) => {
console.log('[KProfile] Calling API...');
return api.setKProfile(printerId, data);
},
onSuccess: (result) => {
onSuccess: (result, variables) => {
console.log('[KProfile] Save success:', result);
showToast(t('kProfiles.toast.profileSaved'));
// Save note if it changed (including clearing it)
@@ -243,8 +280,10 @@ function KProfileModal({
// Editing: use setting_id if available, or composite key with slot_id
profileKey = profile.setting_id || `slot_${profile.slot_id}_${profile.filament_id}_${profile.extruder_id}`;
} else {
// New profile: use name as key (will be matched when profile is loaded)
profileKey = `name_${name}_${filamentId}`;
// New profile: use name as key (matched against the reloaded profile,
// so it has to be the resolved filament_id that was sent — not the
// preset handle the user picked).
profileKey = `name_${name}_${variables.filament_id}`;
}
onSaveNote(profileKey, note);
}
@@ -252,7 +291,7 @@ function KProfileModal({
setIsSyncing(true);
// Add delay before closing to give printer time to process the save
// onSave will trigger refetch in the parent component
setTimeout(() => {
scheduleClose(() => {
setIsSyncing(false);
onSave();
}, 2500);
@@ -276,7 +315,7 @@ function KProfileModal({
setIsSyncing(true);
// Add longer delay for delete - printer needs more time to process
// before it can return the updated profile list
setTimeout(() => {
scheduleClose(() => {
setIsSyncing(false);
onClose();
}, 4000);
@@ -316,14 +355,46 @@ function KProfileModal({
// Combine nozzle type and diameter into nozzle_id (e.g., "HH00-0.4")
const nozzleId = `${nozzleType}-${modalDiameter}`;
// An edit is delete + re-add on single-nozzle printers, so the nozzle
// fields have to survive the round trip — both selects are disabled while
// editing. Rebuilding them blindly from the selects is what let a 0.6mm
// profile come back as "HH00-0.4" once the parse defaults had stamped it
// 0.4 (#1748), so prefer whatever the printer reported. Where it reported
// no nozzle_id at all, send the rebuilt one rather than an empty string —
// the field is part of the profile's identity on the wire and the slicer
// always populates it.
const editNozzleId = profile ? profile.nozzle_id || nozzleId : nozzleId;
const editDiameter = profile ? profile.nozzle_diameter : modalDiameter;
// The printer indexes its calibration table by filament_id, so the preset
// the user picked has to be reduced to one before anything is sent. Only
// the builtin tier and Bambu's official cloud presets carry one outright;
// a cloud *user* preset needs its detail fetched, and imported / Orca
// presets have no Bambu id at all and map to the generic for their
// material. Refuse rather than guess when nothing resolves — a profile
// filed under the wrong filament is invisible to the slot that needs it.
let resolvedFilamentId = profile?.filament_id || '';
if (!profile) {
const picked = filamentPresets.find(p => p.id === filamentChoice);
if (!picked) {
showToast(t('kProfiles.toast.selectFilament'), 'error');
return;
}
resolvedFilamentId = await resolveFilamentId(picked, api.getCloudSettingDetail);
if (!resolvedFilamentId) {
showToast(t('kProfiles.toast.filamentNotResolvable', { name: picked.name }), 'error');
return;
}
}
// For editing or single extruder: just save one profile
if (profile || selectedExtruders.length === 1) {
const payload = {
name: name,
k_value: formattedKValue,
filament_id: filamentId,
nozzle_id: nozzleId,
nozzle_diameter: modalDiameter,
filament_id: resolvedFilamentId,
nozzle_id: editNozzleId,
nozzle_diameter: editDiameter,
extruder_id: profile ? profile.extruder_id : selectedExtruders[0],
setting_id: profile?.setting_id,
slot_id: profile?.slot_id ?? 0,
@@ -341,7 +412,7 @@ function KProfileModal({
const batchPayload = selectedExtruders.map(extruderId => ({
name: name,
k_value: formattedKValue,
filament_id: filamentId,
filament_id: resolvedFilamentId,
nozzle_id: nozzleId,
nozzle_diameter: modalDiameter,
extruder_id: extruderId,
@@ -356,7 +427,7 @@ function KProfileModal({
showToast(t('kProfiles.toast.profilesSaved', { count: selectedExtruders.length }));
// Save note for new batch profiles
if (onSaveNote && note) {
const profileKey = `name_${name}_${filamentId}`;
const profileKey = `name_${name}_${resolvedFilamentId}`;
onSaveNote(profileKey, note);
}
} catch (error) {
@@ -370,7 +441,7 @@ function KProfileModal({
setSavingProgress({ current: selectedExtruders.length, total: selectedExtruders.length });
// Wait for final sync before closing
// onSave will trigger refetch in the parent component
setTimeout(() => {
scheduleClose(() => {
setIsSyncing(false);
setSavingProgress({ current: 0, total: 0 });
onSave();
@@ -454,49 +525,77 @@ function KProfileModal({
{/* Filament - read-only when editing */}
<div>
<label className="block text-sm text-bambu-gray mb-1">{t('kProfiles.modal.filament')}</label>
<select
value={filamentId}
onChange={(e) => {
const newFilamentId = e.target.value;
setFilamentId(newFilamentId);
// Auto-generate profile name when filament is selected (for new profiles)
// Only auto-generate if name is empty - don't overwrite user input
if (!profile && newFilamentId && !name) {
const selectedFilament = knownFilaments.find(f => f.id === newFilamentId);
if (selectedFilament) {
const flowLabel = nozzleType === 'HH00' ? 'HF' : 'S';
setName(`${flowLabel} ${selectedFilament.name}`);
}
}
}}
disabled={!!profile}
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 ${profile ? 'opacity-60 cursor-not-allowed' : ''}`}
required={!profile}
>
<option value="">{t('kProfiles.modal.selectFilament')}</option>
{/* Show current filament when editing - look up from knownFilaments */}
{profile?.filament_id && (
<option key={profile.filament_id} value={profile.filament_id}>
{knownFilaments.find(f => f.id === profile.filament_id)?.name || profile.filament_id}
</option>
)}
{/* Show known filaments from existing K-profiles (for new profiles) */}
{!profile && knownFilaments.map((f) => (
<option key={f.id} value={f.id}>
{f.name}
</option>
))}
</select>
{!profile && knownFilaments.length === 0 && (
<p className="text-xs text-bambu-gray mt-1">
{t('kProfiles.modal.noFilamentsHelp')}
</p>
{profile ? (
// Editing or copying: the filament is fixed, so this is a
// readout rather than a control.
<div className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white opacity-60">
{editedFilamentName || profile.filament_id}
</div>
) : (
// A real list rather than a <select>: Chrome ignores almost
// every CSS property on <optgroup>, so a source heading inside
// a native dropdown can't be made to stand out.
<div className="border border-bambu-dark-tertiary rounded-lg overflow-hidden">
<div className="relative border-b border-bambu-dark-tertiary">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-bambu-gray" />
<input
type="text"
value={filamentQuery}
onChange={(e) => setFilamentQuery(e.target.value)}
placeholder={t('kProfiles.modal.searchFilaments')}
className="w-full pl-10 pr-3 py-2 bg-bambu-dark text-white placeholder-bambu-gray focus:outline-none"
/>
</div>
<div className="max-h-56 overflow-y-auto bg-bambu-dark">
{presetGroups.length === 0 ? (
<p className="px-3 py-3 text-xs text-bambu-gray">
{filamentPresets.length === 0
? t('kProfiles.modal.noFilamentsHelp')
: t('kProfiles.modal.noFilamentMatches')}
</p>
) : presetGroups.map((group) => (
<div key={group.source}>
<div className="sticky top-0 z-10 flex items-center gap-2 px-3 py-1.5 bg-bambu-dark-secondary border-y border-bambu-dark-tertiary">
<span className="text-xs font-bold uppercase tracking-wider text-bambu-green">
{group.label}
</span>
<span className="text-[10px] text-bambu-gray">{group.items.length}</span>
</div>
{group.items.map((f) => (
<button
key={f.id}
type="button"
onClick={() => {
setFilamentChoice(f.id);
// Auto-generate the profile name, but never over
// an entry the user typed.
if (!name) {
const flowLabel = nozzleType === HIGH_FLOW ? 'HF' : 'S';
setName(`${flowLabel} ${f.name}`);
}
}}
className={`w-full text-left px-3 py-1.5 text-sm transition-colors ${
filamentChoice === f.id
? 'bg-bambu-green/20 text-white'
: 'text-white hover:bg-bambu-dark-tertiary'
}`}
>
{f.name}
</button>
))}
</div>
))}
</div>
</div>
)}
</div>
{/* Flow Type and Nozzle Size - read-only when editing */}
<div className="grid grid-cols-2 gap-4">
<div>
{/* Flow Type and Nozzle Size - read-only when editing. Flow type
is hidden on models sold with a single nozzle variant (the
A-series), where the choice would be meaningless — same gate
the slicer applies via support_nozzle_volume(). */}
<div className={supportsFlowType ? 'grid grid-cols-2 gap-4' : ''}>
<div className={supportsFlowType ? '' : 'hidden'}>
<label className="block text-sm text-bambu-gray mb-1">{t('kProfiles.modal.flowType')}</label>
<select
value={nozzleType}
@@ -505,10 +604,10 @@ function KProfileModal({
setNozzleType(newNozzleType);
// Update profile name when flow type changes (for new profiles)
// Only auto-generate if name is empty - don't overwrite user input
if (!profile && filamentId && !name) {
const selectedFilament = knownFilaments.find(f => f.id === filamentId);
if (!profile && filamentChoice && !name) {
const selectedFilament = filamentPresets.find(f => f.id === filamentChoice);
if (selectedFilament) {
const flowLabel = newNozzleType === 'HS00' ? 'HF' : 'S';
const flowLabel = newNozzleType === HIGH_FLOW ? 'HF' : 'S';
setName(`${flowLabel} ${selectedFilament.name}`);
}
}
@@ -516,8 +615,8 @@ function KProfileModal({
disabled={!!profile}
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 ${profile ? 'opacity-60 cursor-not-allowed' : ''}`}
>
<option value="HH00">{t('kProfiles.modal.highFlow')}</option>
<option value="HS00">{t('kProfiles.modal.standard')}</option>
<option value={HIGH_FLOW}>{t('kProfiles.modal.highFlow')}</option>
<option value={STANDARD_FLOW}>{t('kProfiles.modal.standard')}</option>
</select>
</div>
<div>
@@ -772,13 +871,12 @@ export function KProfilesView() {
refetchOnMount: 'always', // Always refetch when component mounts
});
// Also fetch 0.4mm profiles for the filament dropdown (most filaments are calibrated for 0.4mm)
const { data: allProfiles } = useQuery({
queryKey: ['kprofiles', selectedPrinter, '0.4'],
queryFn: () => api.getKProfiles(selectedPrinter!, '0.4'),
enabled: !!selectedPrinter,
staleTime: 60000, // Cache for 1 minute
});
// A second fetch for 0.4mm profiles used to seed the Add-Profile filament
// dropdown. The dropdown is built from the filament preset tiers now
// (#2719), so the round trip bought nothing — and it fired concurrently
// with the fetch above whenever a different nozzle was selected, which is
// exactly the two-requests-in-flight case that made K-profile fetches time
// out (#1748).
// Fetch builtin filament names for accurate filament_id → name resolution
const { data: builtinFilaments } = useQuery({
@@ -794,6 +892,28 @@ export function KProfilesView() {
staleTime: 300000, // Cache for 5 minutes
});
// The other three filament tiers, so a printer with no K-profiles yet can
// still be given its first one (#2719). Each query stands alone and fails
// quietly: not being signed in to a cloud should thin the list, not break
// the page, and the builtin tier above guarantees it is never empty.
const { data: localPresets } = useQuery({
queryKey: ['localPresets'],
queryFn: () => api.getLocalPresets(),
retry: false,
});
const { data: orcaCloudList } = useQuery({
queryKey: ['orcaCloudProfilesForKProfiles'],
queryFn: () => api.orcaCloudListProfiles(),
retry: false,
});
const { data: cloudSettings } = useQuery({
queryKey: ['cloudSettings'],
queryFn: () => api.getCloudSettings(),
retry: false,
});
// Fetch K-profile notes (stored locally)
const {
data: notesData,
@@ -860,6 +980,19 @@ export function KProfilesView() {
}));
}, [builtinFilamentMap]);
// Every filament this install knows about, ranked in the app-wide order:
// imported presets, then Orca Cloud, then Bambu Cloud, then the hardcoded
// built-in table as the floor.
const filamentPresets = React.useMemo(
() => buildFilamentPresetOptions({
localPresets: localPresets?.filament,
orcaProfiles: orcaCloudList?.filament,
cloudSettings: cloudSettings?.filament,
builtinFilaments,
}),
[localPresets?.filament, orcaCloudList?.filament, cloudSettings?.filament, builtinFilaments]
);
// Resolve filament name: builtin table first, then extract from profile name
const resolveFilamentName = React.useCallback((profile: KProfile) => {
return builtinFilamentMap.get(profile.filament_id) || extractFilamentName(profile.name);
@@ -911,6 +1044,18 @@ export function KProfilesView() {
const selectedPrinterData = printers?.find((p) => p.id === selectedPrinter);
const isDualNozzle = selectedPrinterData?.nozzle_count === 2;
// Whether this printer model is sold with both Standard and High Flow
// nozzles. Comes from the model, not from whether the payload happened to
// carry a nozzle_id — most printers omit that field entirely (#1748) while
// still offering both flows. Only the A-series has a single variant.
const supportsFlowType = selectedPrinterData?.supports_nozzle_flow_type ?? true;
// Don't strand the list behind a filter whose control just disappeared.
useEffect(() => {
if (!supportsFlowType) setFlowTypeFilter('all');
}, [supportsFlowType]);
// Keyboard shortcuts
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
@@ -1002,7 +1147,10 @@ export function KProfilesView() {
name: p.name,
k_value: parseFloat(p.k_value).toFixed(6),
filament_id: p.filament_id,
nozzle_id: p.nozzle_id || `HH00-${nozzleDiameter}`,
// An export from a printer that reports no nozzle_id carries
// none; fall back to Standard, the same default the slicer's
// parser uses for a missing field.
nozzle_id: p.nozzle_id || `${STANDARD_FLOW}-${nozzleDiameter}`,
nozzle_diameter: p.nozzle_diameter || nozzleDiameter,
extruder_id: p.extruder_id ?? 0,
slot_id: 0, // Always create new
@@ -1248,17 +1396,19 @@ export function KProfilesView() {
</select>
</div>
)}
<div className="w-32">
<select
value={flowTypeFilter}
onChange={(e) => setFlowTypeFilter(e.target.value as FlowTypeFilter)}
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"
>
<option value="all">{t('kProfiles.allFlow')}</option>
<option value="hf">{t('kProfiles.hfOnly')}</option>
<option value="s">{t('kProfiles.sOnly')}</option>
</select>
</div>
{supportsFlowType && (
<div className="w-32">
<select
value={flowTypeFilter}
onChange={(e) => setFlowTypeFilter(e.target.value as FlowTypeFilter)}
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"
>
<option value="all">{t('kProfiles.allFlow')}</option>
<option value="hf">{t('kProfiles.hfOnly')}</option>
<option value="s">{t('kProfiles.sOnly')}</option>
</select>
</div>
)}
<div className="w-32">
<select
value={sortOption}
@@ -1451,9 +1601,11 @@ export function KProfilesView() {
profile={editingProfile}
printerId={selectedPrinter}
nozzleDiameter={nozzleDiameter}
existingProfiles={allProfiles?.profiles || kprofiles?.profiles}
existingProfiles={kprofiles?.profiles}
builtinFilaments={enrichedBuiltinFilaments}
filamentPresets={filamentPresets}
isDualNozzle={isDualNozzle}
supportsFlowType={supportsFlowType}
initialNote={note}
initialNoteKey={key}
onSaveNote={handleSaveNote}
@@ -1476,9 +1628,11 @@ export function KProfilesView() {
<KProfileModal
printerId={selectedPrinter}
nozzleDiameter={nozzleDiameter}
existingProfiles={allProfiles?.profiles || kprofiles?.profiles}
existingProfiles={kprofiles?.profiles}
builtinFilaments={enrichedBuiltinFilaments}
filamentPresets={filamentPresets}
isDualNozzle={isDualNozzle}
supportsFlowType={supportsFlowType}
onSaveNote={handleSaveNote}
hasPermission={hasPermission}
onClose={() => {
@@ -1497,9 +1651,11 @@ export function KProfilesView() {
<KProfileModal
printerId={selectedPrinter}
nozzleDiameter={nozzleDiameter}
existingProfiles={allProfiles?.profiles || kprofiles?.profiles}
existingProfiles={kprofiles?.profiles}
builtinFilaments={enrichedBuiltinFilaments}
filamentPresets={filamentPresets}
isDualNozzle={isDualNozzle}
supportsFlowType={supportsFlowType}
onSaveNote={handleSaveNote}
hasPermission={hasPermission}
// Pass profile data but without slot_id to create a new profile