mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-18 21:47:39 +02:00
fix(slicer): preset visibility + lookup precedence + signed-out banner + AMS slot badges (#1712)
Four #1712 issues from the 2026-06-04 Orca Cloud integration: (1) Tier order put Orca Cloud above everything across SliceModal, auto-pick scoring, dropdown groups, the AMS slot picker, and the backend precedence. Bambu-Cloud-only users saw their profiles deprioritised behind an empty Orca tier. (2) Cross-tier dedup hid a same-named preset in all but the highest- priority tier. A user with both a local-imported and an Orca-synced "Bambu PLA Basic" couldn't see the Orca copy as a picker option. (3) CloudStatusBanner nagged signed-out users with a permanent "Sign in to Orca Cloud" line at the top of every slice -- even after explicit logout. Bambu Cloud had the symmetric problem. (4) ConfigureAmsSlotModal source badges were inconsistent: Orca rows showed only "Custom" (no source identity), Bambu Cloud built-in rows had no badge at all, and the orthogonal isUser-driven "Custom" badge collided with the source badge for cloud user presets. Order is local > orca_cloud > cloud > standard everywhere it lives (SliceModal SLICE_MODAL_TIER_ORDER + TIER_BONUS + dropdown tier list, ConfigureAmsSlotModal sourceOrder, and backend precedence). The order drives auto-pick + visual group rendering; it does NOT hide profiles. _dedupe_by_name replaced with _enrich_cloud_metadata: every tier returns its full list across all three slots (printer / process / filament). The function still backfills Bambu Cloud filament metadata from same-named local / orca_cloud / standard entries so cloud filaments score in pickFilamentForSlot. CloudStatusBanner silently no-ops on not_authenticated for both clouds; expired / unreachable still surface. The not_authenticated i18n keys stay in the locale files dormant. ConfigureAmsSlotModal: one source badge per row, one colour per source (green Local / purple Orca Cloud / bambu-blue Bambu Cloud / amber Built-in). The legacy isUser-driven "Custom" badge is gone; every row identifies its tier consistently.
This commit is contained in:
@@ -392,10 +392,15 @@ describe('SliceModal', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('renders a "sign in" banner when cloud_status is not_authenticated', async () => {
|
||||
it('omits the cloud banner when status is not_authenticated (#1712)', async () => {
|
||||
// A signed-out user (Bambu or Orca) shouldn't get a permanent "sign in"
|
||||
// nag at the top of every slice. Sign-in lives on the Profiles page; the
|
||||
// modal stays silent unless a previously-signed-in session actually broke
|
||||
// (expired / unreachable).
|
||||
mockApi.getSlicerPresets.mockResolvedValue(
|
||||
makeUnified({
|
||||
cloud_status: 'not_authenticated',
|
||||
orca_cloud_status: 'not_authenticated',
|
||||
local: fullThreeTier.local,
|
||||
standard: fullThreeTier.standard,
|
||||
}),
|
||||
@@ -405,9 +410,8 @@ describe('SliceModal', () => {
|
||||
onClose: vi.fn(),
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('status')).toHaveTextContent(/Sign in to Bambu Cloud/i);
|
||||
});
|
||||
await waitFor(() => expect(screen.getByText('Imported X1C 0.4')).toBeDefined());
|
||||
expect(screen.queryByRole('status')).toBeNull();
|
||||
});
|
||||
|
||||
it('renders an "expired" banner when cloud_status is expired', async () => {
|
||||
|
||||
@@ -1464,8 +1464,9 @@ export interface UnifiedPresetsBySlot {
|
||||
filament: UnifiedPreset[];
|
||||
}
|
||||
export interface UnifiedPresetsResponse {
|
||||
// Priority order: orca_cloud > cloud > local > standard. Dedup is applied
|
||||
// backend-side so each name appears in only one tier.
|
||||
// Priority order: local > orca_cloud > cloud > standard. No cross-tier
|
||||
// dedup — every tier surfaces its full list so the user can pick from
|
||||
// any source. The order drives auto-pick + visual group rendering only.
|
||||
orca_cloud: UnifiedPresetsBySlot;
|
||||
cloud: UnifiedPresetsBySlot;
|
||||
local: UnifiedPresetsBySlot;
|
||||
|
||||
@@ -639,10 +639,11 @@ export function ConfigureAmsSlotModal({
|
||||
}
|
||||
}
|
||||
|
||||
// Sort: orca_cloud first (user-curated), then cloud user presets, then
|
||||
// cloud built-in, then local, then builtin fallback
|
||||
// Sort: local first (user explicitly imported them), then orca_cloud,
|
||||
// then bambu cloud, then builtin fallback. Matches the SliceModal
|
||||
// tier priority.
|
||||
return items.sort((a, b) => {
|
||||
const sourceOrder = { orca_cloud: 0, cloud: 1, local: 2, builtin: 3 };
|
||||
const sourceOrder = { local: 0, orca_cloud: 1, cloud: 2, builtin: 3 };
|
||||
if (a.source !== b.source) return sourceOrder[a.source] - sourceOrder[b.source];
|
||||
if (a.isUser && !b.isUser) return -1;
|
||||
if (!a.isUser && b.isUser) return 1;
|
||||
@@ -1099,16 +1100,21 @@ export function ConfigureAmsSlotModal({
|
||||
{t('profiles.localProfiles.badge')}
|
||||
</span>
|
||||
)}
|
||||
{preset.source === 'orca_cloud' && (
|
||||
<span className="text-xs px-1.5 py-0.5 rounded bg-purple-500/20 text-purple-400">
|
||||
{t('configureAmsSlot.orcaCloud')}
|
||||
</span>
|
||||
)}
|
||||
{preset.source === 'cloud' && (
|
||||
<span className="text-xs px-1.5 py-0.5 rounded bg-bambu-blue/20 text-bambu-blue">
|
||||
{t('configureAmsSlot.bambuCloud')}
|
||||
</span>
|
||||
)}
|
||||
{preset.source === 'builtin' && (
|
||||
<span className="text-xs px-1.5 py-0.5 rounded bg-amber-500/20 text-amber-400">
|
||||
{t('configureAmsSlot.builtin')}
|
||||
</span>
|
||||
)}
|
||||
{preset.isUser && (
|
||||
<span className="text-xs px-1.5 py-0.5 rounded bg-bambu-blue/20 text-bambu-blue">
|
||||
{t('configureAmsSlot.custom')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
@@ -1334,16 +1340,21 @@ export function ConfigureAmsSlotModal({
|
||||
{t('profiles.localProfiles.badge')}
|
||||
</span>
|
||||
)}
|
||||
{preset.source === 'orca_cloud' && (
|
||||
<span className="text-xs px-1.5 py-0.5 rounded bg-purple-500/20 text-purple-400">
|
||||
{t('configureAmsSlot.orcaCloud')}
|
||||
</span>
|
||||
)}
|
||||
{preset.source === 'cloud' && (
|
||||
<span className="text-xs px-1.5 py-0.5 rounded bg-bambu-blue/20 text-bambu-blue">
|
||||
{t('configureAmsSlot.bambuCloud')}
|
||||
</span>
|
||||
)}
|
||||
{preset.source === 'builtin' && (
|
||||
<span className="text-xs px-1.5 py-0.5 rounded bg-amber-500/20 text-amber-400">
|
||||
{t('configureAmsSlot.builtin')}
|
||||
</span>
|
||||
)}
|
||||
{preset.isUser && (
|
||||
<span className="text-xs px-1.5 py-0.5 rounded bg-bambu-blue/20 text-bambu-blue">
|
||||
{t('configureAmsSlot.custom')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -38,15 +38,14 @@ interface SliceModalProps {
|
||||
|
||||
type Slot = 'printer' | 'process' | 'filament';
|
||||
|
||||
// SliceModal-specific tier priority: orca_cloud → local → cloud → standard.
|
||||
// Imported (local) profiles are surfaced before Bambu Cloud because they're
|
||||
// metadata-tagged (Bambu Cloud isn't, by design — see
|
||||
// `_fetch_cloud_presets`'s rate-limit note). Orca Cloud comes first because
|
||||
// its sync_pull response inlines metadata too AND represents the user's
|
||||
// most-recently-curated source. Standard is the bundled fallback. This is
|
||||
// distinct from the listing endpoint's dedup order and only affects what
|
||||
// the SliceModal renders / pre-picks.
|
||||
const SLICE_MODAL_TIER_ORDER = ['orca_cloud', 'local', 'cloud', 'standard'] as const;
|
||||
// Lookup priority: local → orca_cloud → cloud → standard. Local imports
|
||||
// outrank everything else because the user explicitly imported them for
|
||||
// this install; Orca Cloud comes next; Bambu Cloud after that; standard
|
||||
// (bundled) is the final fallback. The backend does NOT dedup tiers —
|
||||
// every group renders its full set so the user can pick a same-named
|
||||
// preset from a lower-priority source if they want to override the
|
||||
// auto-pick.
|
||||
const SLICE_MODAL_TIER_ORDER = ['local', 'orca_cloud', 'cloud', 'standard'] as const;
|
||||
|
||||
function pickDefault(by: UnifiedPresetsResponse, slot: Slot): PresetRef | null {
|
||||
for (const tier of SLICE_MODAL_TIER_ORDER) {
|
||||
@@ -117,8 +116,8 @@ function pickProcessDefault(
|
||||
}
|
||||
|
||||
const TIER_BONUS: Record<PresetSource, number> = {
|
||||
orca_cloud: 1.75,
|
||||
local: 1.5,
|
||||
local: 1.75,
|
||||
orca_cloud: 1.5,
|
||||
cloud: 1.0,
|
||||
standard: 0.5,
|
||||
};
|
||||
@@ -999,7 +998,12 @@ function CloudStatusBanner({
|
||||
cloudName?: 'bambu' | 'orca';
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
if (status === 'ok') return null;
|
||||
// `ok` is the happy path. `not_authenticated` is silenced too: a user who
|
||||
// hasn't signed in (or has explicitly logged out — #1712) doesn't need a
|
||||
// permanent nag at the top of the modal; sign-in lives on the Profiles
|
||||
// page if they want it. Only `expired` and `unreachable` surface — those
|
||||
// are real breakage states a previously-signed-in user needs to see.
|
||||
if (status === 'ok' || status === 'not_authenticated') return null;
|
||||
|
||||
// Same status vocabulary for both Bambu and Orca Cloud — only the
|
||||
// user-facing text varies. The fallbacks below name each cloud explicitly
|
||||
@@ -1008,10 +1012,6 @@ function CloudStatusBanner({
|
||||
const messages =
|
||||
cloudName === 'orca'
|
||||
? {
|
||||
not_authenticated: {
|
||||
key: 'slice.orcaCloud.notAuthenticated',
|
||||
fallback: 'Sign in to Orca Cloud (Profiles → Orca Cloud) to see your Orca presets.',
|
||||
},
|
||||
expired: {
|
||||
key: 'slice.orcaCloud.expired',
|
||||
fallback: 'Orca Cloud session expired — sign in again to refresh your Orca presets.',
|
||||
@@ -1022,10 +1022,6 @@ function CloudStatusBanner({
|
||||
},
|
||||
}
|
||||
: {
|
||||
not_authenticated: {
|
||||
key: 'slice.cloud.notAuthenticated',
|
||||
fallback: 'Sign in to Bambu Cloud (Settings → Profiles → Cloud) to see your cloud presets.',
|
||||
},
|
||||
expired: {
|
||||
key: 'slice.cloud.expired',
|
||||
fallback: 'Bambu Cloud session expired — sign in again to refresh your cloud presets.',
|
||||
@@ -1036,11 +1032,7 @@ function CloudStatusBanner({
|
||||
},
|
||||
};
|
||||
|
||||
const tones: Record<Exclude<SlicerCloudStatus, 'ok'>, { tone: string; icon: typeof Cloud }> = {
|
||||
not_authenticated: {
|
||||
tone: 'border-bambu-dark-tertiary/40 bg-bambu-dark text-bambu-gray',
|
||||
icon: Cloud,
|
||||
},
|
||||
const tones: Record<'expired' | 'unreachable', { tone: string; icon: typeof Cloud }> = {
|
||||
expired: {
|
||||
tone: 'border-amber-700/40 bg-amber-900/20 text-amber-200',
|
||||
icon: CloudOff,
|
||||
@@ -1149,8 +1141,8 @@ function PresetDropdown({
|
||||
// empty sections collapse out.
|
||||
const { sections, otherEntries } = useMemo(() => {
|
||||
const tiers: { key: keyof UnifiedPresetsResponse; label: string; fallback: string }[] = [
|
||||
{ key: 'orca_cloud', label: 'slice.tier.orcaCloud', fallback: 'Orca Cloud' },
|
||||
{ key: 'local', label: 'slice.tier.local', fallback: 'Imported' },
|
||||
{ key: 'orca_cloud', label: 'slice.tier.orcaCloud', fallback: 'Orca Cloud' },
|
||||
{ key: 'cloud', label: 'slice.tier.cloud', fallback: 'Bambu Cloud' },
|
||||
{ key: 'standard', label: 'slice.tier.standard', fallback: 'Standard' },
|
||||
];
|
||||
|
||||
@@ -5246,6 +5246,8 @@ export default {
|
||||
noMatchingPresets: 'Keine passenden Voreinstellungen gefunden.',
|
||||
custom: 'Benutzerdefiniert',
|
||||
builtin: 'Integriert',
|
||||
orcaCloud: 'Orca Cloud',
|
||||
bambuCloud: 'Bambu Cloud',
|
||||
settingsSentToPrinter: 'Einstellungen an Drucker gesendet',
|
||||
filamentProfile: 'Filamentprofil',
|
||||
kProfileLabel: 'K-Profil (Pressure Advance)',
|
||||
|
||||
@@ -5259,6 +5259,8 @@ export default {
|
||||
noMatchingPresets: 'No matching presets found.',
|
||||
custom: 'Custom',
|
||||
builtin: 'Built-in',
|
||||
orcaCloud: 'Orca Cloud',
|
||||
bambuCloud: 'Bambu Cloud',
|
||||
settingsSentToPrinter: 'Settings sent to printer',
|
||||
filamentProfile: 'Filament Profile',
|
||||
kProfileLabel: 'K Profile (Pressure Advance)',
|
||||
|
||||
@@ -5255,6 +5255,8 @@ export default {
|
||||
noMatchingPresets: 'No se encontraron preajustes coincidentes.',
|
||||
custom: 'Personalizado',
|
||||
builtin: 'Integrado',
|
||||
orcaCloud: 'Orca Cloud',
|
||||
bambuCloud: 'Bambu Cloud',
|
||||
settingsSentToPrinter: 'Ajustes enviados a la impresora',
|
||||
filamentProfile: 'Perfil de filamento',
|
||||
kProfileLabel: 'Perfil K (avance de presión)',
|
||||
|
||||
@@ -5236,6 +5236,8 @@ export default {
|
||||
noMatchingPresets: 'Aucun profil trouvé.',
|
||||
custom: 'Perso',
|
||||
builtin: 'Inclus',
|
||||
orcaCloud: 'Orca Cloud',
|
||||
bambuCloud: 'Bambu Cloud',
|
||||
settingsSentToPrinter: 'Réglages envoyés',
|
||||
filamentProfile: 'Profil Filament',
|
||||
kProfileLabel: 'Profil K (Pressure Advance)',
|
||||
|
||||
@@ -5235,6 +5235,8 @@ export default {
|
||||
noMatchingPresets: 'Nessun preset corrispondente trovato.',
|
||||
custom: 'Personalizzato',
|
||||
builtin: 'Integrato',
|
||||
orcaCloud: 'Orca Cloud',
|
||||
bambuCloud: 'Bambu Cloud',
|
||||
settingsSentToPrinter: 'Impostazioni inviate alla stampante',
|
||||
filamentProfile: 'Profilo filamento',
|
||||
kProfileLabel: 'Profilo K (Pressure Advance)',
|
||||
|
||||
@@ -5247,6 +5247,8 @@ export default {
|
||||
noMatchingPresets: '一致するプリセットが見つかりません。',
|
||||
custom: 'カスタム',
|
||||
builtin: '内蔵',
|
||||
orcaCloud: 'Orca Cloud',
|
||||
bambuCloud: 'Bambu Cloud',
|
||||
settingsSentToPrinter: '設定をプリンターに送信しました',
|
||||
filamentProfile: 'フィラメントプロファイル',
|
||||
kProfileLabel: 'Kプロファイル(Pressure Advance)',
|
||||
|
||||
@@ -4939,6 +4939,8 @@ export default {
|
||||
noMatchingPresets: '일치하는 프리셋을 찾을 수 없습니다.',
|
||||
custom: '사용자 지정',
|
||||
builtin: '기본 제공',
|
||||
orcaCloud: 'Orca Cloud',
|
||||
bambuCloud: 'Bambu Cloud',
|
||||
settingsSentToPrinter: '설정이 프린터로 전송됨',
|
||||
filamentProfile: '필라멘트 프로필',
|
||||
kProfileLabel: 'K 프로필 (압력 전진)',
|
||||
|
||||
@@ -5235,6 +5235,8 @@ export default {
|
||||
noMatchingPresets: 'Nenhuma predefinição correspondente encontrada.',
|
||||
custom: 'Personalizado',
|
||||
builtin: 'Integrado',
|
||||
orcaCloud: 'Orca Cloud',
|
||||
bambuCloud: 'Bambu Cloud',
|
||||
settingsSentToPrinter: 'Configurações enviadas para a impressora',
|
||||
filamentProfile: 'Perfil de Filamento',
|
||||
kProfileLabel: 'Perfil K (Avanço de Pressão)',
|
||||
|
||||
@@ -5190,6 +5190,8 @@ export default {
|
||||
noMatchingPresets: 'Eşleşen ön ayar bulunamadı.',
|
||||
custom: 'Özel',
|
||||
builtin: 'Yerleşik',
|
||||
orcaCloud: 'Orca Cloud',
|
||||
bambuCloud: 'Bambu Cloud',
|
||||
settingsSentToPrinter: 'Ayarlar yazıcıya gönderildi',
|
||||
filamentProfile: 'Filament Profili',
|
||||
kProfileLabel: 'K Profili (Basınç İlerlemesi)',
|
||||
|
||||
@@ -5234,6 +5234,8 @@ export default {
|
||||
noMatchingPresets: '未找到匹配的预设。',
|
||||
custom: '自定义',
|
||||
builtin: '内置',
|
||||
orcaCloud: 'Orca Cloud',
|
||||
bambuCloud: 'Bambu Cloud',
|
||||
settingsSentToPrinter: '设置已发送到打印机',
|
||||
filamentProfile: '耗材配置',
|
||||
kProfileLabel: 'K 值配置(压力推进)',
|
||||
|
||||
@@ -5234,6 +5234,8 @@ export default {
|
||||
noMatchingPresets: '未找到匹配的預設。',
|
||||
custom: '自訂',
|
||||
builtin: '內建',
|
||||
orcaCloud: 'Orca Cloud',
|
||||
bambuCloud: 'Bambu Cloud',
|
||||
settingsSentToPrinter: '設定已傳送到印表機',
|
||||
filamentProfile: '耗材設定',
|
||||
kProfileLabel: 'K 值設定(壓力推進)',
|
||||
|
||||
Reference in New Issue
Block a user