mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 03:01:21 +02:00
Two related bugs in K-profile matching, same root cause. #1688 — spool form's PA-profile suggester (PAProfileSection via isMatchingCalibration in spool-form/utils.ts) matched K-profiles by parsing the profile NAME for material/brand/variant. Spools already store slicer_filament (the slicer preset id) and K-profiles already carry filament_id, but both were ignored — so a user's custom K-profile whose name doesn't agree with the slicer preset got silently dropped from suggestions even when the underlying filament_id was identical. #1689 — ConfigureAmsSlotModal's matchingKProfiles ran the same name-only logic on the slot's selected preset. A spool assigned under "Generic PLA" with a custom K-profile actively bound on the printer landed in the modal as "K profile not assigned, default 0.020 will be used", while the printer-card hover-card correctly showed the active profile. Two paths, only one was filtering by name. Shared root: spool preset ids and K-profile filament_ids look different but are equivalent after normalising. Spools store slicer_filament as the cloud setting_id form ("GFSG98_09" — _09 is the variant suffix, the S infix marks setting_id form); K-profiles store filament_id as the bare form ("GFG98"). Plain === doesn't work; both need normalising. This conversion already existed in the other direction at buildFilamentOptions (filament_id → "GFS" + filament_id.slice(2)), so the inverse toFilamentId helper is just the matching reverse, not new ground. Fix — one shared helper, two surfaces: - spool-form/utils.ts: new exports toFilamentId(id) (drops "_NN" variant suffix and strips the "S" in "GFS", so GFSG98_09 → GFG98) and isGenericFilamentId(id) (flags Bambu's generic GFx99 ids which are shared across many filaments and must NOT id-match — the name fallback handles those correctly). - isMatchingCalibration: gains slicer_filament?: string in formData, tries id-match (with generic exclusion) before the existing name parse. PAProfileSection already passes the full formData so no caller edit needed. Strictly additive precedence. - ConfigureAmsSlotModal.selectedPresetInfo: resolves a filamentId field (toFilamentId(cp.setting_id) for cloud presets, toFilamentId(builtinFilamentId) for builtin; empty for local / orca paths which fall through to name match). - ConfigureAmsSlotModal.matchingKProfiles: id-match check at the top of the per-profile predicate (preferred when both sides agree after normalisation), then the existing name-parse logic, then ALWAYS unshifts the slot's currently-active K-profile by slot_id === slotInfo.caliIdx — gated on activeIdx > 0 (so caliIdx 0/null doesn't leak unrelated profiles in), extruder-matched when slotInfo.extruderId is known. This is Spionkiller01's #1689 patch verbatim with the activeIdx > 0 guard added. SpoolBuddy: both kiosk K-profile surfaces reuse the shared components. SpoolBuddyWriteTagPage renders PAProfileSection; SpoolBuddyAmsPage renders ConfigureAmsSlotModal. Verified — fixes propagate automatically, no kiosk-specific edits. What this does NOT change: spools without slicer_filament, K-profiles without filament_id, and generic GFx99 ids all fall through to the existing name-based matching path. Strictly additive precedence; no input shape that matched under the old logic fails to match under the new. The #1053 cloud-preset PFUS* path is preserved because the toFilamentId regex /^GFS/ doesn't match a "PFU" prefix.
This commit is contained in:
@@ -424,4 +424,111 @@ describe('ConfigureAmsSlotModal', () => {
|
||||
expect(screen.getByRole('button', { name: /Cancel/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /Reset Slot/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('surfaces a K-profile whose name does not match the preset when filament_id agrees (#1688)', async () => {
|
||||
// Spool was edited with slicer_filament = "GFSL05_09" (the setting_id form
|
||||
// for Bambu PLA Basic). The printer has a *custom* K-profile saved on the
|
||||
// same filament_id, but the user named it something that doesn't include
|
||||
// "PLA". Pre-fix, the name-only filter dropped it; the id-match path now
|
||||
// surfaces it because both sides normalise to "GFL05".
|
||||
(api.getKProfiles as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
profiles: [
|
||||
{
|
||||
slot_id: 3,
|
||||
extruder_id: 0,
|
||||
nozzle_id: 'HH00-0.4',
|
||||
nozzle_diameter: '0.4',
|
||||
filament_id: 'GFL05',
|
||||
name: 'my-custom-tune',
|
||||
k_value: '0.025',
|
||||
n_coef: '0',
|
||||
ams_id: 0,
|
||||
tray_id: 0,
|
||||
setting_id: '',
|
||||
},
|
||||
],
|
||||
});
|
||||
const slotInfo = {
|
||||
...defaultProps.slotInfo,
|
||||
savedPresetId: 'GFSL05_09', // setting_id form for Bambu PLA Basic
|
||||
};
|
||||
render(<ConfigureAmsSlotModal {...defaultProps} slotInfo={slotInfo} />);
|
||||
|
||||
await waitFor(() => {
|
||||
// Renders as an <option> on the K-profile select even though
|
||||
// "my-custom-tune" doesn't contain "PLA" anywhere.
|
||||
expect(screen.getByRole('option', { name: /my-custom-tune/ })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("always includes the slot's currently-active K-profile when name and id don't match (#1689)", async () => {
|
||||
// Reporter scenario: spool assigned under "Generic PLA" but the slot has
|
||||
// a custom K-profile (filament_id "GFG98" = PETG-something) actively
|
||||
// selected via cali_idx. Pre-fix the modal showed "default 0.020"; the
|
||||
// safety net now surfaces the active profile so Configure Slot reflects
|
||||
// what the printer is actually using.
|
||||
(api.getKProfiles as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
profiles: [
|
||||
{
|
||||
slot_id: 7, // matches caliIdx below
|
||||
extruder_id: 0,
|
||||
nozzle_id: 'HH00-0.4',
|
||||
nozzle_diameter: '0.4',
|
||||
filament_id: 'GFG98', // unrelated to "Generic PLA"
|
||||
name: 'unrelated-petg-tune',
|
||||
k_value: '0.030',
|
||||
n_coef: '0',
|
||||
ams_id: 0,
|
||||
tray_id: 0,
|
||||
setting_id: '',
|
||||
},
|
||||
],
|
||||
});
|
||||
const slotInfo = {
|
||||
...defaultProps.slotInfo,
|
||||
savedPresetId: 'GFSL05_09', // Generic PLA preset
|
||||
caliIdx: 7,
|
||||
extruderId: 0,
|
||||
};
|
||||
render(<ConfigureAmsSlotModal {...defaultProps} slotInfo={slotInfo} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('option', { name: /unrelated-petg-tune/ })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('does not include the active K-profile when caliIdx is 0 or null (#1689 guard)', async () => {
|
||||
// cali_idx == 0 / null means no profile is active (printer default 0.020).
|
||||
// The safety net only triggers for activeIdx > 0 — otherwise unrelated
|
||||
// profiles whose slot_id happens to equal 0 would leak in.
|
||||
(api.getKProfiles as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
profiles: [
|
||||
{
|
||||
slot_id: 0,
|
||||
extruder_id: 0,
|
||||
nozzle_id: 'HH00-0.4',
|
||||
nozzle_diameter: '0.4',
|
||||
filament_id: 'GFG98',
|
||||
name: 'should-not-appear',
|
||||
k_value: '0.030',
|
||||
n_coef: '0',
|
||||
ams_id: 0,
|
||||
tray_id: 0,
|
||||
setting_id: '',
|
||||
},
|
||||
],
|
||||
});
|
||||
const slotInfo = {
|
||||
...defaultProps.slotInfo,
|
||||
savedPresetId: 'GFSL05_09',
|
||||
caliIdx: 0,
|
||||
extruderId: 0,
|
||||
};
|
||||
render(<ConfigureAmsSlotModal {...defaultProps} slotInfo={slotInfo} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Configure AMS/)).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.queryByRole('option', { name: /should-not-appear/ })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* Tests for K-profile matching helpers (#1688 + #1689).
|
||||
*
|
||||
* `isMatchingCalibration` is the predicate used by both the spool form's
|
||||
* PA-profile suggester and ConfigureAmsSlotModal's K-profile filter. The
|
||||
* #1688 enhancement adds a filament_id-first match path with id
|
||||
* normalisation; the old name-parsing logic stays as the fallback.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
isMatchingCalibration,
|
||||
toFilamentId,
|
||||
isGenericFilamentId,
|
||||
} from '../../../components/spool-form/utils';
|
||||
|
||||
describe('toFilamentId', () => {
|
||||
it('strips the variant suffix', () => {
|
||||
expect(toFilamentId('GFG98_09')).toBe('GFG98');
|
||||
});
|
||||
|
||||
it('strips the "S" infix from a setting_id', () => {
|
||||
// Spool stores setting_id "GFSG98", K-profile stores filament_id "GFG98".
|
||||
// Normalisation has to drop the "S" so both sides agree.
|
||||
expect(toFilamentId('GFSG98')).toBe('GFG98');
|
||||
});
|
||||
|
||||
it('strips both the "S" infix and the variant suffix', () => {
|
||||
expect(toFilamentId('GFSG98_09')).toBe('GFG98');
|
||||
});
|
||||
|
||||
it('returns bare filament_id unchanged', () => {
|
||||
expect(toFilamentId('GFL05')).toBe('GFL05');
|
||||
});
|
||||
|
||||
it('uppercases the result', () => {
|
||||
expect(toFilamentId('gfsg98_09')).toBe('GFG98');
|
||||
});
|
||||
|
||||
it('returns empty string for null/undefined/empty', () => {
|
||||
expect(toFilamentId(null)).toBe('');
|
||||
expect(toFilamentId(undefined)).toBe('');
|
||||
expect(toFilamentId('')).toBe('');
|
||||
});
|
||||
|
||||
it('passes through non-Bambu IDs (numeric local-preset, Orca UUID) without crashing', () => {
|
||||
// Numeric local-preset ID — caller falls through to name match, no crash.
|
||||
expect(toFilamentId('42')).toBe('42');
|
||||
// Orca UUID — same.
|
||||
expect(toFilamentId('orca-uuid-abc')).toBe('ORCA-UUID-ABC');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isGenericFilamentId', () => {
|
||||
it('flags GFx99 patterns as generic', () => {
|
||||
expect(isGenericFilamentId('GFL99')).toBe(true);
|
||||
expect(isGenericFilamentId('GFG99')).toBe(true);
|
||||
expect(isGenericFilamentId('GFB99')).toBe(true);
|
||||
});
|
||||
|
||||
it('does not flag non-generic IDs', () => {
|
||||
expect(isGenericFilamentId('GFL05')).toBe(false);
|
||||
expect(isGenericFilamentId('GFG98')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for null/undefined/empty', () => {
|
||||
expect(isGenericFilamentId(null)).toBe(false);
|
||||
expect(isGenericFilamentId(undefined)).toBe(false);
|
||||
expect(isGenericFilamentId('')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isMatchingCalibration (#1688)', () => {
|
||||
const formData = {
|
||||
material: 'PETG',
|
||||
brand: 'Generic',
|
||||
subtype: '',
|
||||
slicer_filament: 'GFSG98_09', // spool's setting_id form
|
||||
};
|
||||
|
||||
it('matches by filament_id when ids agree after normalisation', () => {
|
||||
// K-profile stores bare filament_id; spool stores setting_id. Both
|
||||
// normalise to "GFG98" — match without any name parsing.
|
||||
expect(
|
||||
isMatchingCalibration(
|
||||
{ name: 'My Custom K Profile', filament_id: 'GFG98' },
|
||||
formData,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('id-match wins even when the K-profile name would not parse to anything sensible', () => {
|
||||
expect(
|
||||
isMatchingCalibration(
|
||||
{ name: 'literal-garbage-no-material', filament_id: 'GFG98' },
|
||||
formData,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('skips id-match for generic GFx99 ids and falls through to name match', () => {
|
||||
// GFL99 = generic PLA, shared across many real filaments. Even if the
|
||||
// spool stored GFL99, name parsing must drive the decision.
|
||||
const result = isMatchingCalibration(
|
||||
{ name: 'Random thing with no PETG in it', filament_id: 'GFL99' },
|
||||
{ ...formData, slicer_filament: 'GFL99' },
|
||||
);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('falls through to name match when spool has no slicer_filament', () => {
|
||||
expect(
|
||||
isMatchingCalibration(
|
||||
{ name: 'Generic PETG', filament_id: 'GFG98' },
|
||||
{ material: 'PETG', brand: 'Generic', subtype: '' },
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('falls through to name match when K-profile has no filament_id', () => {
|
||||
expect(
|
||||
isMatchingCalibration(
|
||||
{ name: 'Generic PETG' },
|
||||
formData,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('falls through to name match when normalised ids differ', () => {
|
||||
// Spool says GFG98, K-profile says GFL05 — id-match fails, fall to name.
|
||||
expect(
|
||||
isMatchingCalibration(
|
||||
{ name: 'Generic PETG', filament_id: 'GFL05' },
|
||||
formData,
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isMatchingCalibration(
|
||||
{ name: 'Bambu PLA Basic', filament_id: 'GFL05' },
|
||||
formData,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects calibrations whose material does not match (name fallback path)', () => {
|
||||
expect(
|
||||
isMatchingCalibration(
|
||||
{ name: 'Bambu PLA Basic', filament_id: 'GFL05' },
|
||||
{ material: 'PETG', brand: '', subtype: '' },
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when formData has no material', () => {
|
||||
expect(
|
||||
isMatchingCalibration(
|
||||
{ name: 'PETG', filament_id: 'GFG98' },
|
||||
{ material: '', brand: '', subtype: '' },
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -5,6 +5,7 @@ import { X, Loader2, Settings2, ChevronDown, CheckCircle2, RotateCcw } from 'luc
|
||||
import { api } from '../api/client';
|
||||
import type { KProfile } from '../api/client';
|
||||
import { matchesPrinterModelSuffix } from '../utils/slicerPrinterMatch';
|
||||
import { toFilamentId, isGenericFilamentId } from './spool-form/utils';
|
||||
import { Button } from './Button';
|
||||
|
||||
interface SlotInfo {
|
||||
@@ -656,15 +657,21 @@ export function ConfigureAmsSlotModal({
|
||||
// Resolve the name from orca, cloud, local, or builtin presets. The
|
||||
// Orca branch tolerates both ``orca_<UUID>`` and a bare UUID — see the
|
||||
// configure-mutation comment for why the raw UUID also reaches us.
|
||||
// ``filamentId`` is the bare Bambu filament_id (e.g. "GFG98") when the
|
||||
// preset has one — used to id-match K-profiles directly without name
|
||||
// parsing (#1688). Empty string for paths with no usable filament_id
|
||||
// (orca presets, local presets), which makes the id-match branch skip.
|
||||
let presetName: string | null = null;
|
||||
let filamentId = '';
|
||||
if (selectedPresetId.startsWith('local_')) {
|
||||
const localId = parseInt(selectedPresetId.replace('local_', ''), 10);
|
||||
const lp = localPresets?.filament.find(p => p.id === localId);
|
||||
presetName = lp?.name || null;
|
||||
} else if (selectedPresetId.startsWith('builtin_')) {
|
||||
const filamentId = selectedPresetId.replace('builtin_', '');
|
||||
const bf = builtinFilaments?.find(b => b.filament_id === filamentId);
|
||||
const builtinFilamentId = selectedPresetId.replace('builtin_', '');
|
||||
const bf = builtinFilaments?.find(b => b.filament_id === builtinFilamentId);
|
||||
presetName = bf?.name || null;
|
||||
filamentId = toFilamentId(builtinFilamentId);
|
||||
} else {
|
||||
const orcaCandidateId = selectedPresetId.startsWith('orca_')
|
||||
? selectedPresetId.replace('orca_', '')
|
||||
@@ -675,6 +682,11 @@ export function ConfigureAmsSlotModal({
|
||||
} else if (cloudSettings?.filament) {
|
||||
const cp = cloudSettings.filament.find(p => p.setting_id === selectedPresetId);
|
||||
presetName = cp?.name || null;
|
||||
if (cp) {
|
||||
// SlicerSetting only carries setting_id ("GFSG98_09"); toFilamentId
|
||||
// drops the variant suffix and the "S" infix to yield "GFG98".
|
||||
filamentId = toFilamentId(cp.setting_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!presetName) {
|
||||
@@ -693,6 +705,7 @@ export function ConfigureAmsSlotModal({
|
||||
fullName: nameWithoutSuffix,
|
||||
material: parsed.material,
|
||||
brand: parsed.brand,
|
||||
filamentId,
|
||||
};
|
||||
}, [selectedPresetId, cloudSettings?.filament, localPresets?.filament, builtinFilaments, orcaCloudList?.filament]);
|
||||
|
||||
@@ -737,16 +750,28 @@ export function ConfigureAmsSlotModal({
|
||||
const matchingKProfiles = useMemo(() => {
|
||||
if (!kprofilesData?.profiles || !selectedPresetInfo) return [];
|
||||
|
||||
const { fullName, material, brand } = selectedPresetInfo;
|
||||
const { fullName, material, brand, filamentId } = selectedPresetInfo;
|
||||
const upperFullName = fullName.toUpperCase();
|
||||
const upperMaterial = material.toUpperCase();
|
||||
const upperBrand = brand.toUpperCase();
|
||||
const presetFid = filamentId; // already normalised via toFilamentId
|
||||
|
||||
// Material must be at least 2 chars to avoid false positives
|
||||
if (!upperMaterial || upperMaterial.length < 2) return [];
|
||||
|
||||
// Filter profiles - require brand match if brand is present in selected preset
|
||||
const filtered = kprofilesData.profiles.filter(p => {
|
||||
// Preferred: exact filament_id match (#1688). A user's custom K-profile
|
||||
// whose name doesn't agree with the slicer preset still surfaces when
|
||||
// both sides agree on filament_id. Generic GFx99 IDs are excluded —
|
||||
// they're shared across many filaments and over-match if id-compared.
|
||||
if (presetFid) {
|
||||
const calFid = toFilamentId(p.filament_id);
|
||||
if (calFid && calFid === presetFid && !isGenericFilamentId(calFid)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const profileName = p.name.toUpperCase();
|
||||
|
||||
// If the selected preset has a brand (e.g., "Azurefilm PLA Wood"),
|
||||
@@ -803,8 +828,26 @@ export function ConfigureAmsSlotModal({
|
||||
seen.set(key, profile);
|
||||
}
|
||||
}
|
||||
return Array.from(seen.values());
|
||||
}, [kprofilesData?.profiles, selectedPresetInfo, slotInfo.extruderId]);
|
||||
|
||||
const result = Array.from(seen.values());
|
||||
|
||||
// Always include the slot's currently-active K-profile (by cali_idx / slot_id),
|
||||
// even if its name and filament_id didn't match the selected preset (#1689).
|
||||
// A spool assigned under "Generic PLA" can have a K-profile actively bound on
|
||||
// the printer whose filament_id differs from "Generic PLA"; without this
|
||||
// safety net the modal shows "not assigned, default 0.020" while the printer
|
||||
// card's hover-card correctly shows the active profile.
|
||||
const activeIdx = slotInfo.caliIdx;
|
||||
if (activeIdx != null && activeIdx > 0 && !result.some(p => p.slot_id === activeIdx)) {
|
||||
const active = kprofilesData.profiles.find(
|
||||
p => p.slot_id === activeIdx
|
||||
&& (slotInfo.extruderId === undefined || p.extruder_id === slotInfo.extruderId),
|
||||
);
|
||||
if (active) result.unshift(active);
|
||||
}
|
||||
|
||||
return result;
|
||||
}, [kprofilesData?.profiles, selectedPresetInfo, slotInfo.extruderId, slotInfo.caliIdx]);
|
||||
|
||||
// Pre-select current profile when modal opens, reset when closes
|
||||
useEffect(() => {
|
||||
|
||||
@@ -291,13 +291,52 @@ export function saveRecentColor(color: ColorPreset, currentRecent: ColorPreset[]
|
||||
return updated;
|
||||
}
|
||||
|
||||
// Normalise a Bambu filament identifier to its bare filament_id form (#1688).
|
||||
// Spools store ``slicer_filament`` as a setting_id like "GFSG98_09" (the "_NN"
|
||||
// suffix is the variant, the "S" infix marks it as a setting_id); printer
|
||||
// K-profiles store ``filament_id`` as "GFG98" (bare). Both shapes need
|
||||
// normalising before comparison.
|
||||
//
|
||||
// This is the inverse of the filament_id→setting_id mapping at
|
||||
// ``buildFilamentOptions`` ("GFS" + filament_id.slice(2)), so a round-trip
|
||||
// stays consistent. Non-Bambu IDs (numeric local-preset IDs, Orca UUIDs)
|
||||
// are returned unchanged uppercase — they won't match any K-profile's
|
||||
// filament_id and the caller falls through to name-based matching.
|
||||
export function toFilamentId(id: string | null | undefined): string {
|
||||
if (!id) return '';
|
||||
// Drop "_NN" variant suffix.
|
||||
let s = id.split('_')[0];
|
||||
// Strip the "S" infix in "GFS..." so "GFSG98" → "GFG98".
|
||||
if (/^GFS/i.test(s)) s = s.slice(0, 2) + s.slice(3);
|
||||
return s.toUpperCase();
|
||||
}
|
||||
|
||||
// "GFx99" identifiers (GFL99, GFG99, GFB99, ...) are Bambu's *generic* filament
|
||||
// IDs — shared across many different physical filaments. Matching K-profiles
|
||||
// by an exact generic ID would over-match, so the id-match path skips them and
|
||||
// the caller falls through to name-based matching.
|
||||
export function isGenericFilamentId(id: string | null | undefined): boolean {
|
||||
return !!id && /^GF[A-Z]99$/i.test(id);
|
||||
}
|
||||
|
||||
// Check if a calibration matches based on brand, material, and variant
|
||||
export function isMatchingCalibration(
|
||||
cal: { name?: string; filament_id?: string },
|
||||
formData: { material: string; brand: string; subtype: string },
|
||||
formData: { material: string; brand: string; subtype: string; slicer_filament?: string },
|
||||
): boolean {
|
||||
if (!formData.material) return false;
|
||||
|
||||
// Preferred path: exact filament_id match after normalising both sides
|
||||
// (#1688). When the spool has a non-generic preset assigned and it agrees
|
||||
// with the K-profile's filament_id, this is unambiguous — no name parsing
|
||||
// needed. A spool storing "GFSG98_09" matches a K-profile with filament_id
|
||||
// "GFG98" without going anywhere near parsePresetName.
|
||||
const spoolFid = toFilamentId(formData.slicer_filament);
|
||||
const calFid = toFilamentId(cal.filament_id);
|
||||
if (spoolFid && calFid && spoolFid === calFid && !isGenericFilamentId(calFid)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const profileName = cal.name || '';
|
||||
|
||||
// Remove flow type prefixes
|
||||
|
||||
Reference in New Issue
Block a user