Browse Source

feat(inventory): show spool ID in edit modal and AMS hover card (#1385) (#1402)

feat(inventory): show spool ID in edit modal and AMS hover card (#1385)
Chanakyan 1 week ago
parent
commit
ed5af66839

+ 15 - 0
frontend/src/__tests__/components/FilamentHoverCard.test.tsx

@@ -310,6 +310,21 @@ describe('FilamentHoverCard', () => {
       });
       expect(screen.queryAllByTitle('Open in Inventory')).toHaveLength(1);
     });
+
+    it('shows the spool ID in the assigned-spool block', async () => {
+      renderWithHover(
+        <FilamentHoverCard
+          data={baseFilamentData}
+          inventory={{ assignedSpool: inventorySpool }}
+        >
+          <div>trigger</div>
+        </FilamentHoverCard>
+      );
+      vi.advanceTimersByTime(100);
+      await waitFor(() => {
+        expect(screen.getByText('#42')).toBeInTheDocument();
+      });
+    });
   });
 });
 

+ 64 - 0
frontend/src/__tests__/components/SpoolFormModal.test.tsx

@@ -1117,3 +1117,67 @@ describe('SpoolFormModal copy mode', () => {
     expect((payload as Record<string, unknown>).weight_used).toBe(0);
   });
 });
+
+// The "#<id>" affordance in the modal header (#1385) is only meaningful when
+// editing an existing spool — there's no ID yet on create, and the copy path
+// is producing a new spool too. Guard all three cases so a future refactor
+// can't quietly start leaking the source spool's ID into the Copy modal.
+describe('SpoolFormModal header spool ID (#1385)', () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+  });
+
+  it('shows #<id> next to the title when editing an existing spool', async () => {
+    render(
+      <SpoolFormModal
+        isOpen={true}
+        onClose={vi.fn()}
+        spool={existingSpool}
+        mode="edit"
+        currencySymbol="$"
+      />
+    );
+
+    await waitFor(() => {
+      expect(screen.getByText('Edit Spool')).toBeInTheDocument();
+    });
+    // existingSpool.id is 1; render as "#1" in the modal header.
+    expect(screen.getByText('#1')).toBeInTheDocument();
+  });
+
+  it('does not show an ID when creating a new spool', async () => {
+    render(
+      <SpoolFormModal
+        isOpen={true}
+        onClose={vi.fn()}
+        currencySymbol="$"
+      />
+    );
+
+    await waitFor(() => {
+      expect(screen.getByRole('heading', { name: 'Add Spool' })).toBeInTheDocument();
+    });
+    // No spool exists yet → header carries no "#..." token.
+    expect(screen.queryByText(/^#\d+$/)).not.toBeInTheDocument();
+  });
+
+  it('does not leak the source spool ID when copying', async () => {
+    // Copying produces a fresh spool — surfacing the source ID in the
+    // "Copy Spool" header would mislead the user into thinking the new
+    // spool inherits it.
+    render(
+      <SpoolFormModal
+        isOpen={true}
+        onClose={vi.fn()}
+        spool={existingSpool}
+        mode="copy"
+        currencySymbol="$"
+      />
+    );
+
+    await waitFor(() => {
+      expect(screen.getByRole('heading', { name: 'Copy Spool' })).toBeInTheDocument();
+    });
+    expect(screen.queryByText(/^#\d+$/)).not.toBeInTheDocument();
+  });
+});

+ 8 - 5
frontend/src/components/FilamentHoverCard.tsx

@@ -366,11 +366,14 @@ export function FilamentHoverCard({ data, children, disabled, className = '', sp
                           {t('inventory.assigned')}
                         </span>
                       </div>
-                      <p className="text-xs text-white truncate">
-                        {inventory.assignedSpool.brand ? `${inventory.assignedSpool.brand} ` : ''}
-                        {inventory.assignedSpool.material}
-                        {inventory.assignedSpool.color_name ? ` - ${inventory.assignedSpool.color_name}` : ''}
-                      </p>
+                      <div className="flex items-baseline gap-1.5 min-w-0 mb-1">
+                        <p className="text-xs text-white truncate">
+                          {inventory.assignedSpool.brand ? `${inventory.assignedSpool.brand} ` : ''}
+                          {inventory.assignedSpool.material}
+                          {inventory.assignedSpool.color_name ? ` - ${inventory.assignedSpool.color_name}` : ''}
+                        </p>
+                        <span className="text-[10px] font-mono text-bambu-gray shrink-0">#{inventory.assignedSpool.id}</span>
+                      </div>
                       {(!spoolman?.linkedSpoolId || inventory.assignedSpool!.id !== spoolman.linkedSpoolId) && (
                         <button
                           onClick={(e) => {

+ 4 - 1
frontend/src/components/SpoolFormModal.tsx

@@ -745,8 +745,11 @@ export function SpoolFormModal({
       <div className="relative w-full max-w-xl mx-4 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-xl shadow-2xl max-h-[90vh] flex flex-col">
         {/* Header */}
         <div className="flex items-center justify-between p-4 border-b border-bambu-dark-tertiary flex-shrink-0">
-          <h2 className="text-lg font-semibold text-white">
+          <h2 className="text-lg font-semibold text-white flex items-baseline gap-2">
             {isEditing ? t('inventory.editSpool') : isCopying ? t('inventory.copySpool') : t('inventory.addSpool')}
+            {isEditing && spool && (
+              <span className="text-sm font-mono text-bambu-gray">#{spool.id}</span>
+            )}
           </h2>
           <button
             onClick={onClose}