Parcourir la source

Stop the File Manager card menu from clipping its own top entry (#2846)

A grid card drew its action menu inside itself and clipped its own
overflow, so a card shorter than its menu lost whichever entry sat at the
top. An STL card is the shortest in the library -- a square thumbnail
plus a name and a size, around 270px against a seven-entry menu needing
closer to 310px -- and the entry it lost was Slice, since Print is only
offered for an already-sliced file. A 3MF carries a target model and a
print count, two more rows, so its card was tall enough and the button
appeared, which made this read as a rule about file types. It was not:
the shortest card lost its first item, whatever that item was.

The menu is now the shared ContextMenu, anchored to the kebab in viewport
coordinates the way the archive card has always opened its own. Being
fixed, it escapes both clipping ancestors -- the card and the grid's
scroll container, which would have cropped the top row of cards even
without the card's own overflow. The card drops overflow-hidden anyway
and the thumbnail rounds its own corners instead, so nothing a child
positions outside the card can be cut off again.

While rewriting the block, 3D Preview and its permission tooltip stop
being hardcoded English; fileManager.preview3d and
fileManager.noPermissionPreview are added to all 13 locales.

List view was never affected: it has no menu, only inline buttons.
maziggy il y a 3 semaines
Parent
commit
a72f49be02

+ 5 - 0
CHANGELOG.md

@@ -2,6 +2,11 @@
 
 All notable changes to Bambuddy will be documented in this file.
 
+## [1.2.6b1] - Unreleased
+
+### Fixed
+- **The File Manager's card menu no longer loses its top entry (#2846)** — In grid view a file card's action menu was drawn inside the card, and the card clipped anything its children painted outside it. A card is as tall as its square thumbnail plus whatever metadata the file has, so an STL — which has none beyond a name and a size — produced the shortest card in the library, about 270px against a seven-entry menu that needs closer to 310px. The difference was one row, and the row it took was the top one: **Slice**, since **Print** is only offered for a file that is already sliced. A 3MF carries a target model and a print count, two more rows, and its card was tall enough, which is why the button appeared there and looked like a file-type rule rather than a layout accident. Nothing about STL was special; the shortest card simply lost the first item, whichever it happened to be. The menu now opens against the viewport, the way the archive card's menu already did, so no card can crop it, and the card no longer clips its own children. List view was never affected — it has no menu, only inline buttons. Covered by a frontend test.
+
 ## [1.2.5.3] - 2026-08-15
 
 ### Added

+ 25 - 0
frontend/src/__tests__/pages/FileManagerPage.test.tsx

@@ -1230,6 +1230,31 @@ describe('FileManagerPage', () => {
       expect(openInSlicer).not.toHaveBeenCalled();
     });
 
+    // #2846: the menu used to be an absolutely-positioned child of the card,
+    // and the card clipped its own overflow. A bare STL card is only about
+    // 270px tall -- thumbnail plus name and size -- which is shorter than the
+    // seven-entry menu, so the top entry was cut off. That entry is Slice,
+    // because Print is suppressed for an unsliced file. A 3MF card carries two
+    // more metadata rows and was tall enough, which is why the report said 3MF
+    // worked. Nothing about STL was special; the card was just the shortest.
+    it('keeps the first menu entry out of the card so it cannot be clipped (#2846)', async () => {
+      server.use(
+        http.get('/api/v1/settings/', () => HttpResponse.json({ use_slicer_api: true })),
+      );
+      const user = userEvent.setup();
+      render(<FileManagerPage />);
+
+      await waitFor(() => expect(screen.getByText('bracket.stl')).toBeInTheDocument());
+
+      const card = await openMenu(user, 'bracket.stl');
+      const menu = within(card).getByText('Slice').closest('.fixed');
+      // Viewport-positioned, so no ancestor's overflow can cut it down.
+      expect(menu).not.toBeNull();
+      expect(within(menu as HTMLElement).getAllByRole('button')[0]).toHaveTextContent('Slice');
+      // And the card itself no longer clips what its children draw.
+      expect(card.className).not.toContain('overflow-hidden');
+    });
+
     it('hides the slice item for already-sliced files', async () => {
       const user = userEvent.setup();
       render(<FileManagerPage />);

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

@@ -3941,6 +3941,8 @@ export default {
     noPermissionSlice: 'Sie haben keine Berechtigung, Dateien zu slicen',
     noPermissionAddToQueue: 'Sie haben keine Berechtigung, zur Warteschlange hinzuzufügen',
     noPermissionDownload: 'Sie haben keine Berechtigung, Dateien herunterzuladen',
+    noPermissionPreview: 'Sie haben keine Berechtigung, Dateien in der Vorschau anzuzeigen',
+    preview3d: '3D-Vorschau',
     noPermissionRenameFile: 'Sie haben keine Berechtigung, diese Datei umzubenennen',
     noPermissionGenerateThumbnail: 'Sie haben keine Berechtigung, Vorschaubilder zu generieren',
     noPermissionDeleteFile: 'Sie haben keine Berechtigung, diese Datei zu löschen',

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

@@ -3971,6 +3971,8 @@ export default {
     noPermissionAddToQueue: 'You do not have permission to add to queue',
     noPermissionSlice: 'You do not have permission to slice files',
     noPermissionDownload: 'You do not have permission to download files',
+    noPermissionPreview: 'You do not have permission to preview files',
+    preview3d: '3D Preview',
     noPermissionRenameFile: 'You do not have permission to rename this file',
     noPermissionGenerateThumbnail: 'You do not have permission to generate thumbnails',
     noPermissionDeleteFile: 'You do not have permission to delete this file',

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

@@ -3943,6 +3943,8 @@ export default {
     noPermissionAddToQueue: 'No tiene permiso para añadir a la cola',
     noPermissionSlice: 'No tiene permiso para laminar archivos',
     noPermissionDownload: 'No tiene permiso para descargar archivos',
+    noPermissionPreview: 'No tienes permiso para previsualizar archivos',
+    preview3d: 'Vista previa 3D',
     noPermissionRenameFile: 'No tiene permiso para renombrar este archivo',
     noPermissionGenerateThumbnail: 'No tiene permiso para generar miniaturas',
     noPermissionDeleteFile: 'No tiene permiso para eliminar este archivo',

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

@@ -3930,6 +3930,8 @@ export default {
     noPermissionSlice: 'Vous n\'avez pas la permission de découper des fichiers',
     noPermissionAddToQueue: 'Pas d\'autorisation file',
     noPermissionDownload: 'Pas d\'autorisation téléchargement',
+    noPermissionPreview: 'Vous n\'avez pas la permission de prévisualiser les fichiers',
+    preview3d: 'Aperçu 3D',
     noPermissionRenameFile: 'Pas d\'autorisation renommage fichier',
     noPermissionGenerateThumbnail: 'Pas d\'autorisation vignettes',
     noPermissionDeleteFile: 'Pas d\'autorisation suppression fichier',

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

@@ -3929,6 +3929,8 @@ export default {
     noPermissionSlice: 'Non hai il permesso di sezionare i file',
     noPermissionAddToQueue: 'Non hai il permesso di aggiungere alla coda',
     noPermissionDownload: 'Non hai il permesso di scaricare file',
+    noPermissionPreview: 'Non hai il permesso di visualizzare l\'anteprima dei file',
+    preview3d: 'Anteprima 3D',
     noPermissionRenameFile: 'Non hai il permesso di rinominare questo file',
     noPermissionGenerateThumbnail: 'Non hai il permesso di generare miniature',
     noPermissionDeleteFile: 'Non hai il permesso di eliminare questo file',

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

@@ -3941,6 +3941,8 @@ export default {
     noPermissionSlice: 'ファイルをスライスする権限がありません',
     noPermissionAddToQueue: 'キューに追加する権限がありません',
     noPermissionDownload: 'ファイルをダウンロードする権限がありません',
+    noPermissionPreview: 'ファイルをプレビューする権限がありません',
+    preview3d: '3Dプレビュー',
     noPermissionRenameFile: 'このファイル名を変更する権限がありません',
     noPermissionGenerateThumbnail: 'サムネイルを生成する権限がありません',
     noPermissionDeleteFile: 'このファイルを削除する権限がありません',

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

@@ -3751,6 +3751,8 @@ export default {
     noPermissionAddToQueue: '대기열 추가 권한이 없습니다',
     noPermissionSlice: '파일 슬라이싱 권한이 없습니다',
     noPermissionDownload: '파일 다운로드 권한이 없습니다',
+    noPermissionPreview: '파일을 미리 볼 권한이 없습니다',
+    preview3d: '3D 미리보기',
     noPermissionRenameFile: '파일 이름 변경 권한이 없습니다',
     noPermissionGenerateThumbnail: '썸네일 생성 권한이 없습니다',
     noPermissionDeleteFile: '파일 삭제 권한이 없습니다',

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

@@ -3929,6 +3929,8 @@ export default {
     noPermissionSlice: 'Você não tem permissão para fatiar arquivos',
     noPermissionAddToQueue: 'Você não tem permissão para adicionar à fila',
     noPermissionDownload: 'Você não tem permissão para baixar arquivos',
+    noPermissionPreview: 'Você não tem permissão para pré-visualizar arquivos',
+    preview3d: 'Pré-visualização 3D',
     noPermissionRenameFile: 'Você não tem permissão para renomear este arquivo',
     noPermissionGenerateThumbnail: 'Você não tem permissão para gerar miniaturas',
     noPermissionDeleteFile: 'Você não tem permissão para excluir este arquivo',

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

@@ -3743,6 +3743,8 @@ export default {
     noPermissionAddToQueue: "У вас нет прав на добавление в очередь",
     noPermissionSlice: "У вас нет прав на нарезку файлов",
     noPermissionDownload: "У вас нет прав на скачивание файлов",
+    noPermissionPreview: 'У вас нет прав на предварительный просмотр файлов',
+    preview3d: '3D-просмотр',
     noPermissionRenameFile: "У вас нет прав на переименование этого файла",
     noPermissionGenerateThumbnail: "У вас нет прав на создание миниатюр",
     noPermissionDeleteFile: "У вас нет прав на удаление этого файла",

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

@@ -3936,6 +3936,8 @@ export default {
     noPermissionAddToQueue: 'Kuyruğa ekleme izniniz yok',
     noPermissionSlice: 'Dosyaları dilimleme izniniz yok',
     noPermissionDownload: 'Dosyaları indirme izniniz yok',
+    noPermissionPreview: 'Dosyaları önizleme izniniz yok',
+    preview3d: '3B Önizleme',
     noPermissionRenameFile: 'Bu dosyayı yeniden adlandırma izniniz yok',
     noPermissionGenerateThumbnail: 'Küçük resim oluşturma izniniz yok',
     noPermissionDeleteFile: 'Bu dosyayı silme izniniz yok',

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

@@ -3969,6 +3969,8 @@ export default {
     noPermissionAddToQueue: "Ви не маєте дозволу на додавання в чергу",
     noPermissionSlice: "Ви не маєте дозволу нарізати файли",
     noPermissionDownload: "У вас немає дозволу на завантаження файлів",
+    noPermissionPreview: 'У вас немає дозволу на попередній перегляд файлів',
+    preview3d: '3D-перегляд',
     noPermissionRenameFile: "Ви не маєте дозволу на перейменування цього файлу",
     noPermissionGenerateThumbnail: "Ви не маєте дозволу створювати мініатюри",
     noPermissionDeleteFile: "Ви не маєте дозволу на видалення цього файлу",

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

@@ -3929,6 +3929,8 @@ export default {
     noPermissionSlice: '您没有切片文件的权限',
     noPermissionAddToQueue: '您没有添加到队列的权限',
     noPermissionDownload: '您没有下载文件的权限',
+    noPermissionPreview: '您没有预览文件的权限',
+    preview3d: '3D 预览',
     noPermissionRenameFile: '您没有重命名此文件的权限',
     noPermissionGenerateThumbnail: '您没有生成缩略图的权限',
     noPermissionDeleteFile: '您没有删除此文件的权限',

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

@@ -3929,6 +3929,8 @@ export default {
     noPermissionSlice: '您沒有切片檔案的權限',
     noPermissionAddToQueue: '您沒有新增到佇列的權限',
     noPermissionDownload: '您沒有下載檔案的權限',
+    noPermissionPreview: '您沒有預覽檔案的權限',
+    preview3d: '3D 預覽',
     noPermissionRenameFile: '您沒有重新命名此檔案的權限',
     noPermissionGenerateThumbnail: '您沒有產生縮圖的權限',
     noPermissionDeleteFile: '您沒有刪除此檔案的權限',

+ 95 - 116
frontend/src/pages/FileManagerPage.tsx

@@ -59,6 +59,7 @@ import type {
 } from '../api/client';
 import { Button } from '../components/Button';
 import { ConfirmModal } from '../components/ConfirmModal';
+import { ContextMenu, type ContextMenuItem } from '../components/ContextMenu';
 import { PrintModal } from '../components/PrintModal';
 import { ModelViewerModal } from '../components/ModelViewerModal';
 import { SliceModal } from '../components/SliceModal';
@@ -770,11 +771,94 @@ interface FileCardProps {
 }
 
 function FileCard({ file, isSelected, isMobile, onSelect, onDelete, onDownload, onPrint, onSlice, onOpenInSlicer, onRunPipeline, useSlicerApi, canSlice, onPreview3d, onRename, onGenerateThumbnail, onTagClick, thumbnailVersion, hasPermission, canModify, authEnabled, showModified, t }: FileCardProps) {
-  const [showActions, setShowActions] = useState(false);
+  // Viewport coordinates rather than a flag, because the menu is rendered by
+  // `ContextMenu` at `position: fixed` and anchored to the button (#2846). The
+  // card it belongs to is only ~270px tall for a bare STL, which is shorter
+  // than the seven-entry menu, so a menu positioned inside the card had its
+  // top entry -- Slice -- cut off. The archive card menu works the same way.
+  const [menuAnchor, setMenuAnchor] = useState<{ x: number; y: number } | null>(null);
+
+  const canPreview3d = hasPermission('library:read');
+  const canRename = canModify('library', 'update', file.created_by_id);
+  const canDelete = canModify('library', 'delete', file.created_by_id);
+
+  const menuItems: ContextMenuItem[] = [];
+  if (onPrint && isSlicedFilename(file.filename)) {
+    menuItems.push({
+      label: t('common.print'),
+      // The action stays visually distinct now that the menu component styles
+      // its own labels; only the icon carries the accent.
+      icon: <Printer className="w-4 h-4 text-bambu-green" />,
+      onClick: () => onPrint(file),
+      disabled: !hasPermission('queue:create'),
+      title: !hasPermission('queue:create') ? t('fileManager.noPermissionAddToQueue') : undefined,
+    });
+  }
+  if ((useSlicerApi ? isApiSliceableFilename(file.filename) : isSliceableFilename(file.filename))
+      && (useSlicerApi ? onSlice : onOpenInSlicer)) {
+    menuItems.push({
+      label: t('slice.action'),
+      icon: useSlicerApi ? <Cog className="w-4 h-4" /> : <ExternalLink className="w-4 h-4" />,
+      onClick: () => { if (useSlicerApi) onSlice?.(file); else onOpenInSlicer?.(file); },
+      disabled: !canSlice,
+      title: !canSlice ? (useSlicerApi ? t('fileManager.noPermissionSlice') : t('fileManager.noPermissionDownload')) : undefined,
+    });
+  }
+  if (onRunPipeline && useSlicerApi && isApiSliceableFilename(file.filename)) {
+    menuItems.push({
+      label: t('library.runWithPipeline.actionLabel'),
+      icon: <Play className="w-4 h-4" />,
+      onClick: () => onRunPipeline(file),
+      disabled: !hasPermission('pipelines:run'),
+      title: !hasPermission('pipelines:run') ? t('library.runWithPipeline.noPermission') : undefined,
+    });
+  }
+  if (onPreview3d && (file.file_type === '3mf' || file.file_type === 'gcode' || file.file_type === 'stl' || file.file_type === 'gcode.3mf')) {
+    menuItems.push({
+      label: t('fileManager.preview3d'),
+      icon: <Box className="w-4 h-4" />,
+      onClick: () => onPreview3d(file),
+      disabled: !canPreview3d,
+      title: !canPreview3d ? t('fileManager.noPermissionPreview') : undefined,
+    });
+  }
+  menuItems.push({
+    label: t('common.download'),
+    icon: <Download className="w-4 h-4" />,
+    onClick: () => onDownload(file.id),
+    disabled: !hasPermission('library:read'),
+    title: !hasPermission('library:read') ? t('fileManager.noPermissionDownload') : undefined,
+  });
+  if (onRename) {
+    menuItems.push({
+      label: t('common.rename'),
+      icon: <Pencil className="w-4 h-4" />,
+      onClick: () => onRename(file),
+      disabled: !canRename,
+      title: !canRename ? t('fileManager.noPermissionRenameFile') : undefined,
+    });
+  }
+  if (onGenerateThumbnail && file.file_type === 'stl') {
+    menuItems.push({
+      label: t('fileManager.generateThumbnail'),
+      icon: <Image className="w-4 h-4" />,
+      onClick: () => onGenerateThumbnail(file),
+      disabled: !canRename,
+      title: !canRename ? t('fileManager.noPermissionGenerateThumbnail') : undefined,
+    });
+  }
+  menuItems.push({
+    label: t('common.delete'),
+    icon: <Trash2 className="w-4 h-4" />,
+    onClick: () => onDelete(file.id),
+    danger: true,
+    disabled: !canDelete,
+    title: !canDelete ? t('fileManager.noPermissionDeleteFile') : undefined,
+  });
 
   return (
     <div
-      className={`group relative bg-bambu-dark-secondary rounded-lg border transition-all cursor-pointer overflow-hidden ${
+      className={`group relative bg-bambu-dark-secondary rounded-lg border transition-all cursor-pointer ${
         isSelected
           ? 'border-bambu-green ring-1 ring-bambu-green'
           : 'border-bambu-dark-tertiary hover:border-bambu-green/50'
@@ -782,7 +866,7 @@ function FileCard({ file, isSelected, isMobile, onSelect, onDelete, onDownload,
       onClick={() => onSelect(file.id)}
     >
       {/* Thumbnail */}
-      <div className="aspect-square bg-bambu-dark flex items-center justify-center overflow-hidden">
+      <div className="aspect-square bg-bambu-dark flex items-center justify-center overflow-hidden rounded-t-lg">
         {file.thumbnail_path ? (
           <img
             src={`${api.getLibraryFileThumbnailUrl(file.id)}${thumbnailVersion ? ((api.getLibraryFileThumbnailUrl(file.id).includes('?') ? '&' : '?') + `v=${thumbnailVersion}`) : ''}`}
@@ -873,123 +957,18 @@ function FileCard({ file, isSelected, isMobile, onSelect, onDelete, onDownload,
       {/* Actions - always visible on mobile, hover on desktop */}
       <div className={`absolute bottom-2 right-2 transition-opacity ${isMobile ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'}`} onClick={(e) => e.stopPropagation()}>
         <button
-          onClick={() => setShowActions(!showActions)}
+          onClick={(e) => {
+            // No open/close toggle: the menu's own outside-mousedown handler
+            // has already dismissed it by the time this click lands.
+            const rect = e.currentTarget.getBoundingClientRect();
+            setMenuAnchor({ x: rect.left, y: rect.bottom + 4 });
+          }}
           className="p-1.5 rounded bg-bambu-dark-secondary/90 hover:bg-bambu-dark-tertiary"
         >
           <MoreVertical className="w-4 h-4 text-bambu-gray" />
         </button>
-        {showActions && (
-          <>
-            <div className="fixed inset-0 z-10" onClick={() => setShowActions(false)} />
-            <div className="absolute right-0 bottom-8 z-20 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg shadow-xl py-1 min-w-[140px]">
-              {onPrint && isSlicedFilename(file.filename) && (
-                <button
-                  className={`w-full px-3 py-1.5 text-left text-sm flex items-center gap-2 ${
-                    hasPermission('queue:create') ? 'text-bambu-green hover:bg-bambu-dark' : 'text-bambu-gray cursor-not-allowed'
-                  }`}
-                  onClick={() => { if (hasPermission('queue:create')) { onPrint(file); setShowActions(false); } }}
-                  disabled={!hasPermission('queue:create')}
-                  title={!hasPermission('queue:create') ? t('fileManager.noPermissionAddToQueue') : undefined}
-                >
-                  <Printer className="w-3.5 h-3.5" />
-                  {t('common.print')}
-                </button>
-              )}
-              {(useSlicerApi ? isApiSliceableFilename(file.filename) : isSliceableFilename(file.filename)) &&
-                (useSlicerApi ? onSlice : onOpenInSlicer) && (
-                <button
-                  className={`w-full px-3 py-1.5 text-left text-sm flex items-center gap-2 ${
-                    canSlice ? 'text-white hover:bg-bambu-dark' : 'text-bambu-gray cursor-not-allowed'
-                  }`}
-                  onClick={() => {
-                    if (!canSlice) return;
-                    if (useSlicerApi) onSlice?.(file);
-                    else onOpenInSlicer?.(file);
-                    setShowActions(false);
-                  }}
-                  disabled={!canSlice}
-                  title={!canSlice ? (useSlicerApi ? t('fileManager.noPermissionSlice') : t('fileManager.noPermissionDownload')) : undefined}
-                >
-                  {useSlicerApi ? <Cog className="w-3.5 h-3.5" /> : <ExternalLink className="w-3.5 h-3.5" />}
-                  {t('slice.action')}
-                </button>
-              )}
-              {onRunPipeline && useSlicerApi && isApiSliceableFilename(file.filename) && (
-                <button
-                  className={`w-full px-3 py-1.5 text-left text-sm flex items-center gap-2 ${
-                    hasPermission('pipelines:run') ? 'text-white hover:bg-bambu-dark' : 'text-bambu-gray cursor-not-allowed'
-                  }`}
-                  onClick={() => { if (hasPermission('pipelines:run')) { onRunPipeline(file); setShowActions(false); } }}
-                  disabled={!hasPermission('pipelines:run')}
-                  title={!hasPermission('pipelines:run') ? t('library.runWithPipeline.noPermission') : undefined}
-                >
-                  <Play className="w-3.5 h-3.5" />
-                  {t('library.runWithPipeline.actionLabel')}
-                </button>
-              )}
-              {onPreview3d && (file.file_type === '3mf' || file.file_type === 'gcode' || file.file_type === 'stl' || file.file_type === 'gcode.3mf') && (
-                <button
-                  className={`w-full px-3 py-1.5 text-left text-sm flex items-center gap-2 ${
-                    hasPermission('library:read') ? 'text-white hover:bg-bambu-dark' : 'text-bambu-gray cursor-not-allowed'
-                  }`}
-                  onClick={() => { if (hasPermission('library:read')) { onPreview3d(file); setShowActions(false); } }}
-                  disabled={!hasPermission('library:read')}
-                  title={!hasPermission('library:read') ? 'You do not have permission to preview files' : undefined}
-                >
-                  <Box className="w-3.5 h-3.5" />
-                  3D Preview
-                </button>
-              )}
-              <button
-                className={`w-full px-3 py-1.5 text-left text-sm flex items-center gap-2 ${
-                  hasPermission('library:read') ? 'text-white hover:bg-bambu-dark' : 'text-bambu-gray cursor-not-allowed'
-                }`}
-                onClick={() => { if (hasPermission('library:read')) { onDownload(file.id); setShowActions(false); } }}
-                disabled={!hasPermission('library:read')}
-                title={!hasPermission('library:read') ? t('fileManager.noPermissionDownload') : undefined}
-              >
-                <Download className="w-3.5 h-3.5" />
-                {t('common.download')}
-              </button>
-              {onRename && (
-                <button
-                  className={`w-full px-3 py-1.5 text-left text-sm flex items-center gap-2 ${
-                    canModify('library', 'update', file.created_by_id) ? 'text-white hover:bg-bambu-dark' : 'text-bambu-gray cursor-not-allowed'
-                  }`}
-                  onClick={() => { if (canModify('library', 'update', file.created_by_id)) { onRename(file); setShowActions(false); } }}
-                  disabled={!canModify('library', 'update', file.created_by_id)}
-                  title={!canModify('library', 'update', file.created_by_id) ? t('fileManager.noPermissionRenameFile') : undefined}
-                >
-                  <Pencil className="w-3.5 h-3.5" />
-                  {t('common.rename')}
-                </button>
-              )}
-              {onGenerateThumbnail && file.file_type === 'stl' && (
-                <button
-                  className={`w-full px-3 py-1.5 text-left text-sm flex items-center gap-2 ${
-                    canModify('library', 'update', file.created_by_id) ? 'text-white hover:bg-bambu-dark' : 'text-bambu-gray cursor-not-allowed'
-                  }`}
-                  onClick={() => { if (canModify('library', 'update', file.created_by_id)) { onGenerateThumbnail(file); setShowActions(false); } }}
-                  disabled={!canModify('library', 'update', file.created_by_id)}
-                  title={!canModify('library', 'update', file.created_by_id) ? t('fileManager.noPermissionGenerateThumbnail') : undefined}
-                >
-                  <Image className="w-3.5 h-3.5" />
-                  {t('fileManager.generateThumbnail')}
-                </button>
-              )}
-              <button
-                className={`w-full px-3 py-1.5 text-left text-sm flex items-center gap-2 ${
-                  canModify('library', 'delete', file.created_by_id) ? 'text-red-700 dark:text-red-400 hover:bg-bambu-dark' : 'text-bambu-gray cursor-not-allowed'
-                }`}
-                onClick={() => { if (canModify('library', 'delete', file.created_by_id)) { onDelete(file.id); setShowActions(false); } }}
-                disabled={!canModify('library', 'delete', file.created_by_id)}
-                title={!canModify('library', 'delete', file.created_by_id) ? t('fileManager.noPermissionDeleteFile') : undefined}
-              >
-                <Trash2 className="w-3.5 h-3.5" />
-                {t('common.delete')}
-              </button>
-            </div>
-          </>
+        {menuAnchor && (
+          <ContextMenu x={menuAnchor.x} y={menuAnchor.y} items={menuItems} onClose={() => setMenuAnchor(null)} />
         )}
       </div>
 

Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/index-Dbwdo7E_.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 1
static/assets/index-IhvHtjfG.css


Fichier diff supprimé car celui-ci est trop grand
+ 1 - 0
static/assets/index-VSpFxsmE.css


+ 2 - 2
static/index.html

@@ -26,8 +26,8 @@
 
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-DS0_D3go.js"></script>
-    <link rel="stylesheet" crossorigin href="/assets/index-IhvHtjfG.css">
+    <script type="module" crossorigin src="/assets/index-Dbwdo7E_.js"></script>
+    <link rel="stylesheet" crossorigin href="/assets/index-VSpFxsmE.css">
   </head>
   <body>
     <div id="root"></div>

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff