ソースを参照

fix(inventory): "#123" in Assign Spool finds that spool whatever the slot holds (#3301)

- The search ran on what was left after the slot's tray-match filter,
  so a spool of another material came up empty for its own number
  unless "Show all spools" was ticked. A "#<number>" search now skips
  the tray match; the material-mismatch confirm still asks on assign,
  and any other search is filtered as before.
- A spool that stays hidden says why: assigned to another slot,
  archived, or no such spool (15 locales).
maziggy 16 時間 前
親
コミット
f652d3e0aa
53 ファイル変更、214 行追加、13 行削除
  1. 3 0
      CHANGELOG.md
  2. 125 0
      frontend/src/__tests__/components/AssignSpoolModalIdSearch.test.tsx
  3. 23 2
      frontend/src/components/AssignSpoolModal.tsx
  4. 3 0
      frontend/src/i18n/locales/de.ts
  5. 3 0
      frontend/src/i18n/locales/en.ts
  6. 3 0
      frontend/src/i18n/locales/es.ts
  7. 3 0
      frontend/src/i18n/locales/fr.ts
  8. 3 0
      frontend/src/i18n/locales/it.ts
  9. 3 0
      frontend/src/i18n/locales/ja.ts
  10. 3 0
      frontend/src/i18n/locales/ko.ts
  11. 3 0
      frontend/src/i18n/locales/nl.ts
  12. 3 0
      frontend/src/i18n/locales/pt-BR.ts
  13. 3 0
      frontend/src/i18n/locales/ru.ts
  14. 3 0
      frontend/src/i18n/locales/sv.ts
  15. 3 0
      frontend/src/i18n/locales/tr.ts
  16. 3 0
      frontend/src/i18n/locales/uk.ts
  17. 3 0
      frontend/src/i18n/locales/zh-CN.ts
  18. 3 0
      frontend/src/i18n/locales/zh-TW.ts
  19. 14 7
      frontend/src/utils/inventorySearch.ts
  20. 0 0
      static/assets/ArchivesPage-T44r8_GZ.js
  21. 0 0
      static/assets/CameraPage-D1r1BgQs.js
  22. 0 0
      static/assets/ConfigureAmsSlotModal-BbvuPAz9.js
  23. 0 0
      static/assets/ConfigureAmsSlotModal-DDCiNSu4.js
  24. 0 0
      static/assets/FileManagerPage-CBrrmcFF.js
  25. 0 0
      static/assets/InventoryPage-9azZne1t.js
  26. 0 0
      static/assets/Layout-5XmJ6sFu.js
  27. 0 0
      static/assets/ModelSourcesPage-CRxbcp7J.js
  28. 0 0
      static/assets/PhotoGalleryModal-C9AOj-rv.js
  29. 0 0
      static/assets/PrintersPage-DHZj6rAO.js
  30. 0 0
      static/assets/SettingsPage-mGwxufx4.js
  31. 0 0
      static/assets/SliceModal-oRCHtk8r.js
  32. 0 0
      static/assets/SpoolBuddyAmsPage-DsKnHRZf.js
  33. 0 0
      static/assets/SpoolBuddyInventoryPage-g4NutWEy.js
  34. 1 1
      static/assets/SpoolBuddySettingsPage-CWw4bF1H.js
  35. 0 0
      static/assets/de-CFWeH8Ul.js
  36. 0 0
      static/assets/es-Cchn5VB9.js
  37. 0 0
      static/assets/fr-BT_J_zVM.js
  38. 0 0
      static/assets/index-zDGNl2cU.js
  39. 1 0
      static/assets/inventorySearch-CV2XH-nr.js
  40. 0 1
      static/assets/inventorySearch-pLpY8YP6.js
  41. 0 0
      static/assets/it-DutDdwLw.js
  42. 0 0
      static/assets/ja-Pj7dKdot.js
  43. 0 0
      static/assets/ko-BmMRH101.js
  44. 0 0
      static/assets/locationSensorDefaults-4o7AEXRw.js
  45. 0 0
      static/assets/nl-BzpgyxY8.js
  46. 0 0
      static/assets/pt-BR-CuOC_z96.js
  47. 0 0
      static/assets/ru-rRER1tNn.js
  48. 0 0
      static/assets/sv-PmtW_Cal.js
  49. 0 0
      static/assets/tr-D_5ihDnp.js
  50. 0 0
      static/assets/uk-DfR9bzZC.js
  51. 0 0
      static/assets/zh-CN-CNUqJ-NL.js
  52. 0 0
      static/assets/zh-TW-PFcoiePX.js
  53. 2 2
      static/index.html

+ 3 - 0
CHANGELOG.md

@@ -174,6 +174,9 @@ All notable changes to Bambuddy will be documented in this file.
 - **The frontend build no longer warns about `path` and `crypto` being externalized for the STEP previewer (#2976)** — `occt-import-js`, the Emscripten build behind STEP previews, requires both modules, but only inside its `ENVIRONMENT_IS_NODE` branches; in the browser it loads its `.wasm` from the URL the preview worker passes and draws randomness from `crypto.getRandomValues`. Vite still externalized both and printed two warnings on every build. `vite.config.ts` now drops exactly those two warnings for that one package through `build.rolldownOptions.onLog`, so an externalization anywhere else, or of any other module, still shows.
 
 ### Fixed
+- **Searching "#123" in Assign Spool finds that spool whatever the slot holds (#3301)** — Searching for a spool's number with a `#` in front, as in `#123`, only looked among the spools matching the slot's material. A spool of another material came up empty unless **Show all spools** was ticked, as if it had been deleted.
+  - **Now:** a `#` search shows exactly that spool, whatever its material. Assigning a spool of another material still asks first, as before. Any other search still lists only spools matching the slot.
+  - **When the spool still can't be offered**, the list says why: it is already assigned to another slot, it is archived, or there is no spool with that number.
 - **The File Manager's buttons fit on a phone (#3295, reported by @apizz)** — The buttons at the top of the File Manager (view mode, Generate Thumbnails, Link External, New Folder, Tags, Purge old, Trash, Upload) sat in one row that couldn't wrap. On a phone they ran far past the right edge and pushed the whole page sideways. They now wrap onto more lines when the screen is too narrow, on a phone or a narrow desktop window.
 - **The print dialog's slot picker can be read on a phone held upright (#3294, reported by @apizz)** — In the Print and Schedule dialogs, the picker that chooses a printer slot for each filament was squeezed to a color dot and an arrow on a portrait phone. On printers with a nozzle rack (H2C) it had about 55 pixels. Its open list was only as wide as the picker, so the slot names couldn't be read until the phone was turned sideways.
   - **Now:** on screens narrower than 640 pixels the picker gets a line of its own under the filament the file asks for. The open list is as wide as the slot names need, up to the width of the screen, and moves left instead of running off the right edge. The same applies to the filament override picker used when printing to any printer of a model, and to the custom slot mapping shown for each printer when printing to several. On wider screens the rows are laid out as before, and a long slot name in the open list is shown in full instead of cut off.

+ 125 - 0
frontend/src/__tests__/components/AssignSpoolModalIdSearch.test.tsx

@@ -0,0 +1,125 @@
+/**
+ * Searching "#<number>" in Assign Spool finds that spool whatever the slot
+ * holds (#3301).
+ *
+ * The search ran on what was left after the tray-match filter, so a spool of
+ * another material never appeared for its exact number unless "Show all
+ * spools" was ticked -- and the empty list read as if the spool were gone.
+ * Typing a spool's number is choosing it: "#123" now skips the tray match
+ * (the material-mismatch warning still asks on assign). A spool that stays
+ * hidden says why: in another slot, archived, or no such spool.
+ */
+
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { render } from '../utils';
+import { AssignSpoolModal } from '../../components/AssignSpoolModal';
+import { api } from '../../api/client';
+
+vi.mock('../../api/client', () => ({
+  api: {
+    getSpools: vi.fn(),
+    getAssignments: vi.fn(),
+    assignSpool: vi.fn(),
+    assignSpoolmanSlot: vi.fn(),
+    getSpoolmanInventorySpools: vi.fn(),
+    getSpoolmanSlotAssignments: vi.fn().mockResolvedValue([]),
+    getSettings: vi.fn().mockResolvedValue({}),
+    getAuthStatus: vi.fn().mockResolvedValue({ auth_enabled: false }),
+    refreshPrinterStatus: vi.fn().mockResolvedValue({ status: 'ok' }),
+  },
+}));
+
+const spool = (id: number, material: string, brand: string, extra: Record<string, unknown> = {}) => ({
+  id,
+  material,
+  subtype: 'Basic',
+  brand,
+  color_name: 'Black',
+  rgba: '000000FF',
+  label_weight: 1000,
+  weight_used: 0,
+  tag_uid: null,
+  tray_uuid: null,
+  slicer_filament_name: material,
+  ...extra,
+});
+
+const SPOOLS = [
+  spool(1, 'PLA', 'Polymaker'),
+  spool(7, 'PETG', 'Sunlu'),
+  spool(8, 'PLA', 'Elegoo', { archived_at: '2026-10-01T00:00:00Z' }),
+  spool(9, 'PLA', 'Overture'),
+];
+
+const props = {
+  isOpen: true,
+  onClose: vi.fn(),
+  printerId: 1,
+  amsId: 0,
+  trayId: 0,
+  trayInfo: { type: 'PLA', material: 'PLA', profile: 'PLA', color: '000000', location: 'AMS 1 - Slot 1' },
+};
+
+async function search(query: string) {
+  const user = userEvent.setup();
+  render(<AssignSpoolModal {...props} />);
+  await screen.findByText(/Polymaker/);
+  await user.type(screen.getByPlaceholderText(/search/i), query);
+  return user;
+}
+
+describe('AssignSpoolModal "#<number>" search', () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+    (api.getSpools as ReturnType<typeof vi.fn>).mockResolvedValue(SPOOLS);
+    // Spool 9 sits in another slot of the same printer.
+    (api.getAssignments as ReturnType<typeof vi.fn>).mockResolvedValue([
+      { id: 1, spool_id: 9, printer_id: 1, ams_id: 0, tray_id: 2 },
+    ]);
+  });
+
+  it('finds a spool of another material than the slot, and only that one', async () => {
+    await search('#7');
+    expect(await screen.findByText(/Sunlu/)).toBeInTheDocument();
+    expect(screen.queryByText(/Polymaker/)).not.toBeInTheDocument();
+  });
+
+  it('still asks before assigning it', async () => {
+    const user = await search('#7');
+    await user.click(await screen.findByText(/Sunlu/));
+    await user.click(screen.getByRole('button', { name: /assign spool/i }));
+    expect(await screen.findByText('Material mismatch')).toBeInTheDocument();
+    expect(api.assignSpool).not.toHaveBeenCalled();
+  });
+
+  it('leaves the tray match on for any other search', async () => {
+    await search('Sunlu');
+    await waitFor(() => expect(screen.queryByText(/Polymaker/)).not.toBeInTheDocument());
+    expect(screen.queryByText(/Sunlu/)).not.toBeInTheDocument();
+  });
+
+  it('says so when the spool is in another slot, and still does not offer it', async () => {
+    await search('#9');
+    expect(await screen.findByText('Spool #9 is already assigned to another slot. Unassign it there first.')).toBeInTheDocument();
+    expect(screen.queryByText(/Overture/)).not.toBeInTheDocument();
+  });
+
+  it('says so when the spool is archived', async () => {
+    await search('#8');
+    expect(await screen.findByText('Spool #8 is archived. Restore it to assign it.')).toBeInTheDocument();
+    expect(screen.queryByText(/Elegoo/)).not.toBeInTheDocument();
+  });
+
+  it('says so when there is no such spool', async () => {
+    await search('#99');
+    expect(await screen.findByText('There is no spool #99.')).toBeInTheDocument();
+  });
+
+  it('with "Show all spools" ticked, a spool in another slot is offered as before', async () => {
+    const user = await search('#9');
+    await user.click(screen.getByLabelText(/show all spools/i));
+    expect(await screen.findByText(/Overture/)).toBeInTheDocument();
+  });
+});

+ 23 - 2
frontend/src/components/AssignSpoolModal.tsx

@@ -7,7 +7,7 @@ import type { InventorySpool, SpoolAssignment } from '../api/client';
 import { Button } from './Button';
 import { ConfirmModal } from './ConfirmModal';
 import { useToast } from '../contexts/ToastContext';
-import { filterSpoolsByQuery } from '../utils/inventorySearch';
+import { exactSpoolIdQuery, filterSpoolsByQuery } from '../utils/inventorySearch';
 import { getSwatchStyle, resolveSpoolColorName } from '../utils/colors';
 import { useUiSettings } from '../hooks/useUiSettings';
 
@@ -267,8 +267,14 @@ export function AssignSpoolModal({ isOpen, onClose, printerId, amsId, trayId, tr
   // tray's material (partial-match both directions — "PLA" spool accepts a "PLA Basic" slot and
   // vice versa). Manually-added inventory spools typically have no slicer_filament_name; gating
   // on strict profile equality alone hid them even when the material matched (#1047).
+  //
+  // A "#123" search skips the tray match: typing a spool's number is choosing
+  // that spool, and the material-mismatch warning still asks on assign
+  // (#3301). Before, a spool of another material came up empty for its own
+  // number, as if it had been deleted.
+  const exactSpoolId = exactSpoolIdQuery(searchFilter);
   let filteredSpools = availableSpools;
-  if (!disableFiltering) {
+  if (!disableFiltering && exactSpoolId === null) {
     const trayProfile = stripProfileQualifier(normalizeValue(trayInfo?.profile));
     const trayMaterial = normalizeValue(trayInfo?.material || trayInfo?.type);
     if (trayProfile || trayMaterial) {
@@ -291,6 +297,17 @@ export function AssignSpoolModal({ isOpen, onClose, printerId, amsId, trayId, tr
   if (searchFilter && filteredSpools) {
     filteredSpools = filterSpoolsByQuery(filteredSpools, searchFilter);
   }
+  // Why a "#123" search found nothing, when it did -- the spool is not gone
+  // just because it is not offered here.
+  let exactSpoolMissReason: string | null = null;
+  if (exactSpoolId !== null && spools && filteredSpools?.length === 0) {
+    const wanted = spools.find((spool: InventorySpool) => spool.id === exactSpoolId);
+    exactSpoolMissReason = !wanted
+      ? t('inventory.spoolIdNotFound', { id: exactSpoolId })
+      : wanted.archived_at
+        ? t('inventory.spoolIdArchived', { id: exactSpoolId })
+        : t('inventory.spoolIdInOtherSlot', { id: exactSpoolId });
+  }
 
   const handleAssign = () => {
     if (selectedSpoolmanSpoolId !== null) {
@@ -451,6 +468,10 @@ export function AssignSpoolModal({ isOpen, onClose, printerId, amsId, trayId, tr
                   </button>
                 ))}
               </div>
+            ) : exactSpoolMissReason ? (
+              <div className="text-center py-8 text-bambu-gray">
+                <p>{exactSpoolMissReason}</p>
+              </div>
             ) : availableSpools && availableSpools.length === 0 ? (
               <div className="text-center py-8 text-bambu-gray">
                 <p>{t('inventory.noAvailableSpools')}</p>

+ 3 - 0
frontend/src/i18n/locales/de.ts

@@ -5403,6 +5403,9 @@ export default {
     page: 'Seite',
     noSpoolsMatch: 'Keine Ergebnisse',
     noSpoolsMatchDesc: 'Versuchen Sie, Ihre Suche oder Filter anzupassen.',
+    spoolIdNotFound: 'Es gibt keine Spule #{{id}}.',
+    spoolIdArchived: 'Spule #{{id}} ist archiviert. Stellen Sie sie wieder her, um sie zuzuweisen.',
+    spoolIdInOtherSlot: 'Spule #{{id}} ist bereits einem anderen Slot zugewiesen. Heben Sie die Zuweisung dort zuerst auf.',
     active: 'Aktiv',
     archived: 'Archiviert',
     all: 'Alle',

+ 3 - 0
frontend/src/i18n/locales/en.ts

@@ -5450,6 +5450,9 @@ export default {
     page: 'Page',
     noSpoolsMatch: 'No results found',
     noSpoolsMatchDesc: 'Try adjusting your search or filters to find what you\'re looking for.',
+    spoolIdNotFound: 'There is no spool #{{id}}.',
+    spoolIdArchived: 'Spool #{{id}} is archived. Restore it to assign it.',
+    spoolIdInOtherSlot: 'Spool #{{id}} is already assigned to another slot. Unassign it there first.',
     active: 'Active',
     archived: 'Archived',
     all: 'All',

+ 3 - 0
frontend/src/i18n/locales/es.ts

@@ -5410,6 +5410,9 @@ export default {
     page: 'Página',
     noSpoolsMatch: 'No se encontraron resultados',
     noSpoolsMatchDesc: 'Pruebe a ajustar su búsqueda o sus filtros para encontrar lo que busca.',
+    spoolIdNotFound: 'No existe la bobina #{{id}}.',
+    spoolIdArchived: 'La bobina #{{id}} está archivada. Restáurela para asignarla.',
+    spoolIdInOtherSlot: 'La bobina #{{id}} ya está asignada a otra ranura. Desasígnela allí primero.',
     active: 'Activas',
     archived: 'Archivadas',
     all: 'Todas',

+ 3 - 0
frontend/src/i18n/locales/fr.ts

@@ -5390,6 +5390,9 @@ export default {
     page: 'Page',
     noSpoolsMatch: 'Aucun résultat trouvé',
     noSpoolsMatchDesc: 'Ajustez votre recherche ou vos filtres.',
+    spoolIdNotFound: 'Il n\'existe pas de bobine #{{id}}.',
+    spoolIdArchived: 'La bobine #{{id}} est archivée. Restaurez-la pour l\'assigner.',
+    spoolIdInOtherSlot: 'La bobine #{{id}} est déjà assignée à un autre emplacement. Désassignez-la d\'abord de celui-ci.',
     active: 'Actif',
     archived: 'Archivé',
     all: 'Tous',

+ 3 - 0
frontend/src/i18n/locales/it.ts

@@ -5389,6 +5389,9 @@ export default {
     page: 'Pagina',
     noSpoolsMatch: 'Nessun risultato trovato',
     noSpoolsMatchDesc: 'Prova a modificare la ricerca o i filtri per trovare quello che cerchi.',
+    spoolIdNotFound: 'Non esiste la bobina #{{id}}.',
+    spoolIdArchived: 'La bobina #{{id}} è archiviata. Ripristinala per assegnarla.',
+    spoolIdInOtherSlot: 'La bobina #{{id}} è già assegnata a un altro slot. Rimuovi prima l\'assegnazione lì.',
     active: 'Attive',
     archived: 'Archiviate',
     all: 'Tutte',

+ 3 - 0
frontend/src/i18n/locales/ja.ts

@@ -5403,6 +5403,9 @@ export default {
     page: 'ページ',
     noSpoolsMatch: '結果なし',
     noSpoolsMatchDesc: '検索やフィルターを調整してみてください。',
+    spoolIdNotFound: 'スプール #{{id}} は存在しません。',
+    spoolIdArchived: 'スプール #{{id}} はアーカイブされています。割り当てるには復元してください。',
+    spoolIdInOtherSlot: 'スプール #{{id}} は既に別のスロットに割り当てられています。先にそちらの割り当てを解除してください。',
     active: 'アクティブ',
     archived: 'アーカイブ済み',
     all: 'すべて',

+ 3 - 0
frontend/src/i18n/locales/ko.ts

@@ -5173,6 +5173,9 @@ export default {
     page: '페이지',
     noSpoolsMatch: '결과를 찾을 수 없음',
     noSpoolsMatchDesc: '검색 또는 필터를 조정하여 원하는 항목을 찾아보세요.',
+    spoolIdNotFound: '스풀 #{{id}}이(가) 없습니다.',
+    spoolIdArchived: '스풀 #{{id}}은(는) 보관되었습니다. 할당하려면 먼저 복원하세요.',
+    spoolIdInOtherSlot: '스풀 #{{id}}은(는) 이미 다른 슬롯에 할당되어 있습니다. 먼저 그곳에서 해제하세요.',
     active: '활성',
     archived: '보관됨',
     all: '전체',

+ 3 - 0
frontend/src/i18n/locales/nl.ts

@@ -5448,6 +5448,9 @@ export default {
     page: 'Pagina',
     noSpoolsMatch: 'Geen resultaten gevonden',
     noSpoolsMatchDesc: 'Pas je zoekopdracht of filters aan om te vinden wat je zoekt.',
+    spoolIdNotFound: 'Er is geen spoel #{{id}}.',
+    spoolIdArchived: 'Spoel #{{id}} is gearchiveerd. Herstel hem om hem toe te wijzen.',
+    spoolIdInOtherSlot: 'Spoel #{{id}} is al aan een andere sleuf toegewezen. Maak de toewijzing daar eerst ongedaan.',
     active: 'Actief',
     archived: 'Gearchiveerd',
     all: 'Alle',

+ 3 - 0
frontend/src/i18n/locales/pt-BR.ts

@@ -5389,6 +5389,9 @@ export default {
     page: 'Página',
     noSpoolsMatch: 'Nenhum resultado encontrado',
     noSpoolsMatchDesc: 'Tente ajustar sua pesquisa ou filtros para encontrar o que você está procurando.',
+    spoolIdNotFound: 'Não existe o carretel #{{id}}.',
+    spoolIdArchived: 'O carretel #{{id}} está arquivado. Restaure-o para atribuí-lo.',
+    spoolIdInOtherSlot: 'O carretel #{{id}} já está atribuído a outro slot. Remova a atribuição lá primeiro.',
     active: 'Ativo',
     archived: 'Arquivado',
     all: 'Todos',

+ 3 - 0
frontend/src/i18n/locales/ru.ts

@@ -5173,6 +5173,9 @@ export default {
     page: "Страница",
     noSpoolsMatch: "Ничего не найдено",
     noSpoolsMatchDesc: "Измените поисковый запрос или фильтры.",
+    spoolIdNotFound: "Катушки #{{id}} не существует.",
+    spoolIdArchived: "Катушка #{{id}} в архиве. Восстановите её, чтобы назначить.",
+    spoolIdInOtherSlot: "Катушка #{{id}} уже назначена другому слоту. Сначала отмените назначение там.",
     active: "Активные",
     archived: "Архивные",
     all: "Все",

+ 3 - 0
frontend/src/i18n/locales/sv.ts

@@ -5446,6 +5446,9 @@ errors: {
     page: 'Sida',
     noSpoolsMatch: 'Inga resultat hittade',
     noSpoolsMatchDesc: 'Försök justera din sökning eller filter för att hitta det du letar efter.',
+    spoolIdNotFound: 'Det finns ingen rulle #{{id}}.',
+    spoolIdArchived: 'Rulle #{{id}} är arkiverad. Återställ den för att tilldela den.',
+    spoolIdInOtherSlot: 'Rulle #{{id}} är redan tilldelad ett annat fack. Avlänka den där först.',
     active: 'Aktiv',
     archived: 'Arkiverad',
     all: 'Alla',

+ 3 - 0
frontend/src/i18n/locales/tr.ts

@@ -5384,6 +5384,9 @@ export default {
     page: 'Sayfa',
     noSpoolsMatch: 'Sonuç bulunamadı',
     noSpoolsMatchDesc: 'Aradığınızı bulmak için aramanızı veya filtrelerinizi ayarlamayı deneyin.',
+    spoolIdNotFound: '#{{id}} numaralı makara yok.',
+    spoolIdArchived: '#{{id}} numaralı makara arşivlendi. Atamak için önce geri yükleyin.',
+    spoolIdInOtherSlot: '#{{id}} numaralı makara zaten başka bir yuvaya atanmış. Önce oradaki atamayı kaldırın.',
     active: 'Aktif',
     archived: 'Arşivlendi',
     all: 'Tümü',

+ 3 - 0
frontend/src/i18n/locales/uk.ts

@@ -5443,6 +5443,9 @@ export default {
     page: "Сторінка",
     noSpoolsMatch: "Результатів не знайдено",
     noSpoolsMatchDesc: "Спробуйте налаштувати пошук або фільтри, щоб знайти те, що ви шукаєте.",
+    spoolIdNotFound: "Котушки #{{id}} не існує.",
+    spoolIdArchived: "Котушку #{{id}} заархівовано. Відновіть її, щоб призначити.",
+    spoolIdInOtherSlot: "Котушку #{{id}} вже призначено іншому слоту. Спочатку скасуйте призначення там.",
     active: "Активний",
     archived: "Архівовано",
     all: "все",

+ 3 - 0
frontend/src/i18n/locales/zh-CN.ts

@@ -5395,6 +5395,9 @@ export default {
     page: '页',
     noSpoolsMatch: '未找到结果',
     noSpoolsMatchDesc: '尝试调整您的搜索或筛选条件。',
+    spoolIdNotFound: '不存在耗材 #{{id}}。',
+    spoolIdArchived: '耗材 #{{id}} 已归档。请先恢复后再分配。',
+    spoolIdInOtherSlot: '耗材 #{{id}} 已分配给其他槽位。请先在那里取消分配。',
     active: '活跃',
     archived: '已归档',
     all: '全部',

+ 3 - 0
frontend/src/i18n/locales/zh-TW.ts

@@ -5395,6 +5395,9 @@ export default {
     page: '頁',
     noSpoolsMatch: '未找到結果',
     noSpoolsMatchDesc: '嘗試調整您的搜尋或篩選條件。',
+    spoolIdNotFound: '不存在耗材 #{{id}}。',
+    spoolIdArchived: '耗材 #{{id}} 已歸檔。請先還原後再指派。',
+    spoolIdInOtherSlot: '耗材 #{{id}} 已指派給其他槽位。請先在那裡取消指派。',
     active: '活躍',
     archived: '已歸檔',
     all: '全部',

+ 14 - 7
frontend/src/utils/inventorySearch.ts

@@ -1,6 +1,18 @@
 import type { InventorySpool } from '../api/client';
 import { resolveSpoolColorName } from './colors';
 
+/**
+ * The spool ID a "#<number>" query asks for, or null for any other query.
+ *
+ * A hash-prefixed number is an explicit spool ID lookup. Plain numeric
+ * searches stay backwards-compatible ("3" can still match #3, #13, #30, ...),
+ * while "#3" selects only the physical spool whose label says #3.
+ */
+export function exactSpoolIdQuery(query: string): number | null {
+  const match = query.trim().match(/^#(\d+)$/);
+  return match ? Number(match[1]) : null;
+}
+
 /**
  * Return true when spool matches the search query across all searchable text fields.
  * Case-insensitive. Empty query always returns true.
@@ -9,13 +21,8 @@ export function spoolMatchesQuery(spool: InventorySpool, query: string): boolean
   const q = query.trim().toLowerCase();
   if (!q) return true;
 
-  // A hash-prefixed number is an explicit spool ID lookup. Keep plain numeric
-  // searches backwards-compatible ("3" can still match #3, #13, #30, ...),
-  // while "#3" selects only the physical spool whose label says #3.
-  const exactIdQuery = q.match(/^#(\d+)$/);
-  if (exactIdQuery) {
-    return spool.id === Number(exactIdQuery[1]);
-  }
+  const exactId = exactSpoolIdQuery(q);
+  if (exactId !== null) return spool.id === exactId;
 
   return (
     String(spool.id).includes(q) ||

ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/ArchivesPage-T44r8_GZ.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/CameraPage-D1r1BgQs.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/ConfigureAmsSlotModal-BbvuPAz9.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/ConfigureAmsSlotModal-DDCiNSu4.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/FileManagerPage-CBrrmcFF.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/InventoryPage-9azZne1t.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/Layout-5XmJ6sFu.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/ModelSourcesPage-CRxbcp7J.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/PhotoGalleryModal-C9AOj-rv.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/PrintersPage-DHZj6rAO.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/SettingsPage-mGwxufx4.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/SliceModal-oRCHtk8r.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/SpoolBuddyAmsPage-DsKnHRZf.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/SpoolBuddyInventoryPage-g4NutWEy.js


ファイルの差分が大きいため隠しています
+ 1 - 1
static/assets/SpoolBuddySettingsPage-CWw4bF1H.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/de-CFWeH8Ul.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/es-Cchn5VB9.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/fr-BT_J_zVM.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/index-zDGNl2cU.js


+ 1 - 0
static/assets/inventorySearch-CV2XH-nr.js

@@ -0,0 +1 @@
+import{c as e}from"./colors-BF4_h-XA.js";function t(e){let t=e.trim().match(/^#(\d+)$/);return t?Number(t[1]):null}function n(n,r){let i=r.trim().toLowerCase();if(!i)return!0;let a=t(i);return a===null?String(n.id).includes(i)||n.material.toLowerCase().includes(i)||(n.brand?.toLowerCase().includes(i)??!1)||(n.color_name?.toLowerCase().includes(i)??!1)||(e(n.color_name,n.rgba,n.color_name_is_synthesized)?.toLowerCase().includes(i)??!1)||(n.subtype?.toLowerCase().includes(i)??!1)||(n.note?.toLowerCase().includes(i)??!1)||(n.slicer_filament_name?.toLowerCase().includes(i)??!1)||(n.storage_location?.toLowerCase().includes(i)??!1)||(n.material_number?.toLowerCase().includes(i)??!1)||(n.suppliers?.some(e=>e.supplier_name.toLowerCase().includes(i)||(e.supplier_article_number?.toLowerCase().includes(i)??!1))??!1):n.id===a}function r(e,t){return t?e.filter(e=>n(e,t)):e}export{r as n,t};

+ 0 - 1
static/assets/inventorySearch-pLpY8YP6.js

@@ -1 +0,0 @@
-import{c as e}from"./colors-BF4_h-XA.js";function t(t,n){let r=n.trim().toLowerCase();if(!r)return!0;let i=r.match(/^#(\d+)$/);return i?t.id===Number(i[1]):String(t.id).includes(r)||t.material.toLowerCase().includes(r)||(t.brand?.toLowerCase().includes(r)??!1)||(t.color_name?.toLowerCase().includes(r)??!1)||(e(t.color_name,t.rgba,t.color_name_is_synthesized)?.toLowerCase().includes(r)??!1)||(t.subtype?.toLowerCase().includes(r)??!1)||(t.note?.toLowerCase().includes(r)??!1)||(t.slicer_filament_name?.toLowerCase().includes(r)??!1)||(t.storage_location?.toLowerCase().includes(r)??!1)||(t.material_number?.toLowerCase().includes(r)??!1)||(t.suppliers?.some(e=>e.supplier_name.toLowerCase().includes(r)||(e.supplier_article_number?.toLowerCase().includes(r)??!1))??!1)}function n(e,n){return n?e.filter(e=>t(e,n)):e}export{n as t};

ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/it-DutDdwLw.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/ja-Pj7dKdot.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/ko-BmMRH101.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/locationSensorDefaults-4o7AEXRw.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/nl-BzpgyxY8.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/pt-BR-CuOC_z96.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/ru-rRER1tNn.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/sv-PmtW_Cal.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/tr-D_5ihDnp.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/uk-DfR9bzZC.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/zh-CN-CNUqJ-NL.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/zh-TW-PFcoiePX.js


+ 2 - 2
static/index.html

@@ -26,7 +26,7 @@
 
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-DI9-9ji2.js"></script>
+    <script type="module" crossorigin src="/assets/index-zDGNl2cU.js"></script>
     <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-aKtaBQYM.js">
     <link rel="modulepreload" crossorigin href="/assets/client-DSw2TdH4.js">
     <link rel="modulepreload" crossorigin href="/assets/react-dom-PmRWqOqI.js">
@@ -120,7 +120,7 @@
     <link rel="modulepreload" crossorigin href="/assets/circle-minus-CpiTIx_j.js">
     <link rel="modulepreload" crossorigin href="/assets/ConnectionDiagnostic-jD6ObGRm.js">
     <link rel="modulepreload" crossorigin href="/assets/plateCheck-fqh7_usj.js">
-    <link rel="modulepreload" crossorigin href="/assets/Layout-C9f49H5A.js">
+    <link rel="modulepreload" crossorigin href="/assets/Layout-5XmJ6sFu.js">
     <link rel="stylesheet" crossorigin href="/assets/index-evpNfdOF.css">
   </head>
   <body>

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません