Procházet zdrojové kódy

Approving a review upload needs library:upload

- Approving pending VP uploads (single and all) adds File Manager
  files, so it needs library:upload as well as queue:create
- The review panel disables Add to File Manager and Add all without
  both, with a tooltip naming the permissions (16 locales)
- Tests: backend 403 per missing permission, frontend disabled buttons
maziggy před 3 dny
rodič
revize
02c0131c04
53 změnil soubory, kde provedl 120 přidání a 11 odebrání
  1. 1 1
      CHANGELOG.md
  2. 10 4
      backend/app/api/routes/pending_uploads.py
  3. 35 0
      backend/tests/integration/test_pending_upload_archive_project.py
  4. 44 1
      frontend/src/__tests__/components/PendingUploadsPanelWiring.test.tsx
  5. 11 2
      frontend/src/components/PendingUploadsPanel.tsx
  6. 1 0
      frontend/src/i18n/locales/de.ts
  7. 1 0
      frontend/src/i18n/locales/en.ts
  8. 1 0
      frontend/src/i18n/locales/es.ts
  9. 1 0
      frontend/src/i18n/locales/fr.ts
  10. 1 0
      frontend/src/i18n/locales/it.ts
  11. 1 0
      frontend/src/i18n/locales/ja.ts
  12. 1 0
      frontend/src/i18n/locales/ko.ts
  13. 1 0
      frontend/src/i18n/locales/nl.ts
  14. 1 0
      frontend/src/i18n/locales/pl.ts
  15. 1 0
      frontend/src/i18n/locales/pt-BR.ts
  16. 1 0
      frontend/src/i18n/locales/ru.ts
  17. 1 0
      frontend/src/i18n/locales/sv.ts
  18. 1 0
      frontend/src/i18n/locales/tr.ts
  19. 1 0
      frontend/src/i18n/locales/uk.ts
  20. 1 0
      frontend/src/i18n/locales/zh-CN.ts
  21. 1 0
      frontend/src/i18n/locales/zh-TW.ts
  22. 0 0
      static/assets/ArchivesPage-B_rJqS4I.js
  23. 0 0
      static/assets/CamWallPage-BX6GuqAu.js
  24. 0 0
      static/assets/CameraPage-CRAGdiq2.js
  25. 0 0
      static/assets/FileManagerPage-C8TWAsBn.js
  26. 0 0
      static/assets/InventoryPage-DmGboQua.js
  27. 0 0
      static/assets/Layout-CeU3kIEZ.js
  28. 0 0
      static/assets/ModelSourcesPage-DSbbbP-S.js
  29. 0 0
      static/assets/PrintersPage-B0mpqybt.js
  30. 0 0
      static/assets/SettingsPage-CKLH6JLh.js
  31. 0 0
      static/assets/SliceModal-BZGr23BN.js
  32. 0 0
      static/assets/SpoolBuddyInventoryPage-DyGPUD0i.js
  33. 1 1
      static/assets/SpoolBuddySettingsPage-Dtd9Qs9V.js
  34. 0 0
      static/assets/de-EqruBgE8.js
  35. 0 0
      static/assets/es-BLk2fQHR.js
  36. 0 0
      static/assets/fr-DcGLjOB9.js
  37. 0 0
      static/assets/index-xRy4ceQQ.js
  38. 0 0
      static/assets/it-CRvoD9SH.js
  39. 0 0
      static/assets/ja-B7EvEMFf.js
  40. 0 0
      static/assets/ko-vPn7dkOw.js
  41. 0 0
      static/assets/locationSensorDefaults-DLVLw5wf.js
  42. 0 0
      static/assets/nl-CAbPOvvZ.js
  43. 0 0
      static/assets/pl-DjtlmnRG.js
  44. 0 0
      static/assets/pt-BR-JNJqTFi3.js
  45. 0 0
      static/assets/ru-CVPUX1AG.js
  46. 0 0
      static/assets/spoolCost-C3FynVBJ.js
  47. 0 0
      static/assets/sv-BPyr4Xjz.js
  48. 0 0
      static/assets/tr-BdhYwrzF.js
  49. 0 0
      static/assets/uk-DsWsBmoQ.js
  50. 0 0
      static/assets/usePageFileDrop-4TJxpJO8.js
  51. 0 0
      static/assets/zh-CN-6hIB_S5e.js
  52. 0 0
      static/assets/zh-TW-B9XOFrQk.js
  53. 2 2
      static/index.html

+ 1 - 1
CHANGELOG.md

@@ -10,7 +10,7 @@ All notable changes to Bambuddy will be documented in this file.
 - **Cam Wall tiles show temperatures and finish time, and can pause, stop and switch the light (#3479, sponsor vote Q4 2026)** — With the **Full** status overlay, every connected tile shows its nozzle (both on a dual-nozzle printer), bed and chamber temperatures, and a printing tile adds its finish time beside the time left. Hovering over a tile shows **Pause/Resume** and **Stop**, with the same confirmations as the printer card, and a **chamber light** switch; **Tile controls: Always** in the wall settings keeps them on screen, and a touch screen always shows them. The controls need the `printers:control` permission; a user without it, an offline printer and a Cam Wall token (kiosk) wall get none.
 
 ### Changed
-- **Virtual printers put uploads in the File Manager, not the archive** — An archive is a print that happened, but a virtual printer archived every file the slicer sent, in Archive, Review and Queue mode, before it ever printed. Uploads now go to the File Manager, and the archive gets the file when it prints, like any other File Manager print. **Archive** mode is now **File Manager** mode; virtual printers set to it switch over on upgrade, and archives they created before stay as they are. Each virtual printer card has a **File Manager folder** (by default a shared top-level folder named **Virtual Printer**; external folders can't be picked). The same file sent again under the same name into that folder doesn't make a second copy. **Review** mode stays: pending uploads now wait at the top of the File Manager page (the sidebar badge moved there too), and **Add to File Manager** stores them in the virtual printer's folder with their tags, notes and project; the panel is translated now too. **Queue** mode stores the file and queues it from there, and a new switch, **Remove from File Manager once it prints** (off by default), takes it out of the File Manager when its print starts. With **Save AMS mapping** on, the slicer's tray pick still reaches the archive for reprints: the queue job keeps it until the print creates the archive, shows the saved-mapping badge meanwhile, and offers it under **Mapping** when edited. The virtual printer's archive-name setting (file name or 3MF title) applies to the archive made when the print starts. Wiki updated.
+- **Virtual printers put uploads in the File Manager, not the archive** — An archive is a print that happened, but a virtual printer archived every file the slicer sent, in Archive, Review and Queue mode, before it ever printed. Uploads now go to the File Manager, and the archive gets the file when it prints, like any other File Manager print. **Archive** mode is now **File Manager** mode; virtual printers set to it switch over on upgrade, and archives they created before stay as they are. Each virtual printer card has a **File Manager folder** (by default a shared top-level folder named **Virtual Printer**; external folders can't be picked). The same file sent again under the same name into that folder doesn't make a second copy. **Review** mode stays: pending uploads now wait at the top of the File Manager page (the sidebar badge moved there too), and **Add to File Manager** stores them in the virtual printer's folder with their tags, notes and project (approving now needs the `library:upload` permission as well as `queue:create`, since it adds a file); the panel is translated now too. **Queue** mode stores the file and queues it from there, and a new switch, **Remove from File Manager once it prints** (off by default), takes it out of the File Manager when its print starts. With **Save AMS mapping** on, the slicer's tray pick still reaches the archive for reprints: the queue job keeps it until the print creates the archive, shows the saved-mapping badge meanwhile, and offers it under **Mapping** when edited. The virtual printer's archive-name setting (file name or 3MF title) applies to the archive made when the print starts. Wiki updated.
 - **A queue virtual printer shows where its jobs go, and its target can be cleared** — In Print Queue mode a virtual printer without a target printer sends each job to the next free printer of its model, but the card's target list only said "Select a printer...", and once a printer was picked there was no way back. The first entry now reads **Any available [model]** and switches the virtual printer back to that, and a line under the list says where the jobs go. The access code stays the same, so the slicer stays bound. In the other server modes the entry reads **None**; proxy mode still needs a printer. Wiki updated.
 - **Profile defaults is compact and has Apply all** — Settings → Filament → Profile defaults showed every default as a large box with all its presets and K profiles, so a few products already filled the page. Each default is now one line (product, colour, slicer preset, and how many printer presets and K profiles it has), sorted by product, with a search box; click a line to see its presets and K profiles. The list scrolls on its own. **Apply all** applies every default to the spools in stock in one go, and like Apply to existing spools it only fills in what a spool doesn't have yet. Wiki updated.
 - **Slicer sidecars on Bambu Studio 02.09.01.70 and OrcaSlicer 2.5.0 (#3442)** — The Bambu Studio sidecar now ships the H3C printer presets (0.2, 0.4, 0.6 and 0.8 mm nozzle). OrcaSlicer has no H3C presets yet. OrcaSlicer 2.5.0 ships its bundled presets in a new packed format, so the sidecar image now fetches the same release's presets as JSON for its preset handling. Slicing with a printer preset saved under a name of its own keeps working on 2.5.0; the sliced file then names the Bambu preset it was saved from as its printer preset. Sidecar images built for older slicer versions work as before.

+ 10 - 4
backend/app/api/routes/pending_uploads.py

@@ -171,9 +171,12 @@ async def _store_pending(db: AsyncSession, pending: PendingUpload, request: "Arc
 @router.post("/archive-all")
 async def archive_all_pending(
     db: AsyncSession = Depends(get_db, scope="function"),
-    _: User | None = RequirePermissionIfAuthEnabled(Permission.QUEUE_CREATE),
+    _: User | None = RequirePermissionIfAuthEnabled(Permission.QUEUE_CREATE, Permission.LIBRARY_UPLOAD),
 ):
-    """Approve every pending upload: each goes to the File Manager."""
+    """Approve every pending upload: each goes to the File Manager.
+
+    Needs library:upload as well as queue:create: approving adds files.
+    """
     # By id, each loaded fresh: a failed one is rolled back, and a rollback
     # expires every row the session holds.
     pending_ids = (await db.execute(select(PendingUpload.id).where(PendingUpload.status == "pending"))).scalars().all()
@@ -255,9 +258,12 @@ async def archive_pending_upload(
     upload_id: int,
     request: ArchiveRequest = None,
     db: AsyncSession = Depends(get_db, scope="function"),
-    _: User | None = RequirePermissionIfAuthEnabled(Permission.QUEUE_CREATE),
+    _: User | None = RequirePermissionIfAuthEnabled(Permission.QUEUE_CREATE, Permission.LIBRARY_UPLOAD),
 ):
-    """Approve a pending upload: it goes to the File Manager."""
+    """Approve a pending upload: it goes to the File Manager.
+
+    Needs library:upload as well as queue:create: approving adds a file.
+    """
     result = await db.execute(select(PendingUpload).where(PendingUpload.id == upload_id))
     pending = result.scalar_one_or_none()
 

+ 35 - 0
backend/tests/integration/test_pending_upload_archive_project.py

@@ -182,3 +182,38 @@ async def test_approve_all_goes_on_after_one_upload_fails(async_client: AsyncCli
 
     assert response.status_code == 200, response.text
     assert response.json() == {"stored": 2, "failed": 1}
+
+
+@pytest.mark.parametrize(
+    ("permissions", "status"),
+    [(["queue:create"], 403), (["library:upload"], 403), (["queue:create", "library:upload"], 200)],
+    ids=["queue-only", "library-only", "both"],
+)
+async def test_approving_needs_library_upload_as_well(
+    async_client: AsyncClient, db_session, tmp_path, permissions, status
+):
+    # Approving adds a File Manager file, so it needs what an upload needs.
+    from backend.app.core.auth import get_password_hash
+    from backend.app.models.group import Group
+    from backend.app.models.settings import Settings
+    from backend.app.models.user import User
+
+    db_session.add_all(
+        [Settings(key="auth_enabled", value="true"), Settings(key="advanced_auth_enabled", value="false")]
+    )
+    group = Group(name="reviewers", description="t", permissions=permissions, is_system=False)
+    db_session.add(group)
+    await db_session.flush()
+    db_session.add(
+        User(username="teacher", password_hash=get_password_hash("Pass1word!"), is_active=True, groups=[group])
+    )
+    await db_session.commit()
+    upload_id = (await _pending(db_session, tmp_path)).id
+    login = await async_client.post("/api/v1/auth/login", json={"username": "teacher", "password": "Pass1word!"})
+    headers = {"Authorization": f"Bearer {login.json()['access_token']}"}
+
+    single = await async_client.post(f"/api/v1/pending-uploads/{upload_id}/archive", headers=headers)
+    every = await async_client.post("/api/v1/pending-uploads/archive-all", headers=headers)
+
+    assert single.status_code == status, single.text
+    assert every.status_code == status, every.text

+ 44 - 1
frontend/src/__tests__/components/PendingUploadsPanelWiring.test.tsx

@@ -5,13 +5,14 @@
  * cancelling a destructive confirm sends nothing.
  */
 
-import { describe, it, expect, beforeEach } from 'vitest';
+import { describe, it, expect, beforeEach, afterEach } from 'vitest';
 import { screen, waitFor, within } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import { http, HttpResponse } from 'msw';
 import { render } from '../utils';
 import { server } from '../mocks/server';
 import { PendingUploadsPanel } from '../../components/PendingUploadsPanel';
+import { setAuthToken } from '../../api/client';
 
 const uploads = [
   {
@@ -199,3 +200,45 @@ describe('PendingUploadsPanel wiring', () => {
     expect(calls).toHaveLength(0);
   });
 });
+
+
+// Approving adds a File Manager file, so it needs library:upload as well as
+// queue:create (the backend checks both); without them the buttons are off.
+describe('PendingUploadsPanel permissions', () => {
+  function signInWith(permissions: string[]) {
+    setAuthToken('test-token-1', 'session');
+    server.use(
+      http.get('*/api/v1/auth/status', () => HttpResponse.json({ auth_enabled: true, requires_setup: false })),
+      http.get('*/api/v1/auth/me', () =>
+        HttpResponse.json({
+          id: 9,
+          username: 'teacher',
+          is_active: true,
+          is_admin: false,
+          groups: [],
+          permissions,
+          created_at: '2026-01-01T00:00:00Z',
+        }),
+      ),
+    );
+  }
+
+  afterEach(() => {
+    setAuthToken(null);
+  });
+
+  it.each([
+    [['queue:create'], true],
+    [['queue:create', 'library:upload'], false],
+  ])('with %j the approve buttons are disabled: %s', async (permissions, disabled) => {
+    signInWith(permissions);
+    render(<PendingUploadsPanel />);
+    const row = await rowFor('First Upload');
+
+    await waitFor(() => {
+      const button = within(row).getByRole('button', { name: /Add to File Manager/ }) as HTMLButtonElement;
+      expect(button.disabled).toBe(disabled);
+    });
+    expect((screen.getByRole('button', { name: 'Add all' }) as HTMLButtonElement).disabled).toBe(disabled);
+  });
+});

+ 11 - 2
frontend/src/components/PendingUploadsPanel.tsx

@@ -8,6 +8,7 @@ import { api } from '../api/client';
 import { Card, CardContent, CardHeader } from './Card';
 import { Button } from './Button';
 import { useToast } from '../contexts/ToastContext';
+import { useAuth } from '../contexts/AuthContext';
 import { ConfirmModal } from './ConfirmModal';
 import { formatFileSize } from '../utils/file';
 import { assignableProjects } from '../utils/projectTree';
@@ -20,6 +21,7 @@ interface PendingUploadItemProps {
   onDiscard: (id: number) => void;
   isArchiving: boolean;
   isDiscarding: boolean;
+  canApprove: boolean;
 }
 
 function PendingUploadItem({
@@ -29,6 +31,7 @@ function PendingUploadItem({
   onDiscard,
   isArchiving,
   isDiscarding,
+  canApprove,
 }: PendingUploadItemProps) {
   const { t } = useTranslation();
   const [expanded, setExpanded] = useState(false);
@@ -73,7 +76,8 @@ function PendingUploadItem({
               variant="primary"
               size="sm"
               onClick={() => onArchive(upload.id, { tags, notes, project_id: projectId || undefined })}
-              disabled={isArchiving}
+              disabled={isArchiving || !canApprove}
+              title={canApprove ? undefined : t('pendingUploads.noApprovePermission')}
             >
               {isArchiving ? (
                 <Loader2 className="w-4 h-4 animate-spin" />
@@ -163,6 +167,9 @@ function PendingUploadItem({
 export function PendingUploadsPanel() {
   const { t } = useTranslation();
   const queryClient = useQueryClient();
+  // Approving adds a File Manager file, so it needs both (the backend checks).
+  const { hasPermission } = useAuth();
+  const canApprove = hasPermission('queue:create') && hasPermission('library:upload');
   const { showToast } = useToast();
   const [showArchiveAllConfirm, setShowArchiveAllConfirm] = useState(false);
   const [showDiscardAllConfirm, setShowDiscardAllConfirm] = useState(false);
@@ -292,7 +299,8 @@ export function PendingUploadsPanel() {
                 variant="primary"
                 size="sm"
                 onClick={() => setShowArchiveAllConfirm(true)}
-                disabled={archiveAllMutation.isPending}
+                disabled={archiveAllMutation.isPending || !canApprove}
+                title={canApprove ? undefined : t('pendingUploads.noApprovePermission')}
               >
                 {archiveAllMutation.isPending ? (
                   <Loader2 className="w-4 h-4 animate-spin" />
@@ -335,6 +343,7 @@ export function PendingUploadsPanel() {
                 onDiscard={(id) => discardMutation.mutate(id)}
                 isArchiving={archivingIds.has(upload.id)}
                 isDiscarding={discardingIds.has(upload.id)}
+                canApprove={canApprove}
               />
             ))}
           </div>

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

@@ -7324,6 +7324,7 @@ export default {
   // Pending Uploads Panel
   pendingUploads: {
     title: 'Ausstehende Uploads ({{count}})',
+    noApprovePermission: 'Zum Ablegen im Dateimanager brauchst du die Berechtigungen queue:create und library:upload',
     description: 'Diese Dateien kamen über einen virtuellen Drucker im Prüfmodus. Lege sie im Dateimanager ab oder verwirf sie.',
     from: 'von {{ip}}',
     addToFileManager: 'In den Dateimanager',

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

@@ -7377,6 +7377,7 @@ export default {
   // Pending Uploads Panel
   pendingUploads: {
     title: 'Pending uploads ({{count}})',
+    noApprovePermission: 'Adding to the File Manager needs the queue:create and library:upload permissions',
     description: 'These files came in on a virtual printer in review mode. Add them to the File Manager or discard them.',
     from: 'from {{ip}}',
     addToFileManager: 'Add to File Manager',

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

@@ -7332,6 +7332,7 @@ export default {
   // Pending Uploads Panel
   pendingUploads: {
     title: 'Subidas pendientes ({{count}})',
+    noApprovePermission: 'Añadir al gestor de archivos requiere los permisos queue:create y library:upload',
     description: 'Estos archivos llegaron por una impresora virtual en modo revisión. Añádelos al gestor de archivos o descártalos.',
     from: 'desde {{ip}}',
     addToFileManager: 'Añadir al gestor de archivos',

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

@@ -7312,6 +7312,7 @@ export default {
   // Pending Uploads Panel
   pendingUploads: {
     title: 'Envois en attente ({{count}})',
+    noApprovePermission: 'L\'ajout au gestionnaire de fichiers nécessite les permissions queue:create et library:upload',
     description: 'Ces fichiers sont arrivés par une imprimante virtuelle en mode vérification. Ajoutez-les au gestionnaire de fichiers ou supprimez-les.',
     from: 'depuis {{ip}}',
     addToFileManager: 'Ajouter au gestionnaire',

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

@@ -7311,6 +7311,7 @@ export default {
   // Pending Uploads Panel
   pendingUploads: {
     title: 'Caricamenti in attesa ({{count}})',
+    noApprovePermission: 'Per aggiungere al gestore file servono i permessi queue:create e library:upload',
     description: 'Questi file sono arrivati da una stampante virtuale in modalità revisione. Aggiungili al gestore file o scartali.',
     from: 'da {{ip}}',
     addToFileManager: 'Aggiungi al gestore file',

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

@@ -7325,6 +7325,7 @@ export default {
   // Pending Uploads Panel
   pendingUploads: {
     title: '保留中のアップロード ({{count}})',
+    noApprovePermission: 'ファイル管理への追加には queue:create と library:upload の権限が必要です',
     description: 'これらのファイルはレビューモードの仮想プリンターから届きました。ファイル管理に追加するか破棄してください。',
     from: '{{ip}} から',
     addToFileManager: 'ファイル管理に追加',

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

@@ -7018,6 +7018,7 @@ export default {
   },
   pendingUploads: {
     title: '대기 중인 업로드 ({{count}})',
+    noApprovePermission: '파일 관리자에 추가하려면 queue:create 및 library:upload 권한이 필요합니다',
     description: '이 파일들은 검토 모드의 가상 프린터로 들어왔습니다. 파일 관리자에 추가하거나 버리세요.',
     from: '{{ip}}에서',
     addToFileManager: '파일 관리자에 추가',

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

@@ -7375,6 +7375,7 @@ export default {
   // Pending Uploads Panel
   pendingUploads: {
     title: 'Wachtende uploads ({{count}})',
+    noApprovePermission: 'Toevoegen aan bestandsbeheer vereist de rechten queue:create en library:upload',
     description: 'Deze bestanden kwamen binnen via een virtuele printer in controlemodus. Voeg ze toe aan bestandsbeheer of verwijder ze.',
     from: 'van {{ip}}',
     addToFileManager: 'Toevoegen aan bestandsbeheer',

+ 1 - 0
frontend/src/i18n/locales/pl.ts

@@ -7377,6 +7377,7 @@ export default {
   // Pending Uploads Panel
   pendingUploads: {
     title: 'Oczekujące przesłania ({{count}})',
+    noApprovePermission: 'Dodawanie do menedżera plików wymaga uprawnień queue:create i library:upload',
     description: 'Te pliki przyszły przez wirtualną drukarkę w trybie przeglądu. Dodaj je do menedżera plików albo odrzuć.',
     from: 'z {{ip}}',
     addToFileManager: 'Dodaj do menedżera plików',

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

@@ -7311,6 +7311,7 @@ export default {
   // Pending Uploads Panel
   pendingUploads: {
     title: 'Envios pendentes ({{count}})',
+    noApprovePermission: 'Adicionar ao Gerenciador de Arquivos exige as permissões queue:create e library:upload',
     description: 'Estes arquivos chegaram por uma impressora virtual no modo revisão. Adicione-os ao Gerenciador de Arquivos ou descarte-os.',
     from: 'de {{ip}}',
     addToFileManager: 'Adicionar ao Gerenciador de Arquivos',

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

@@ -7005,6 +7005,7 @@ export default {
   },
   pendingUploads: {
     title: "Ожидающие загрузки ({{count}})",
+    noApprovePermission: "Для добавления в файловый менеджер нужны права queue:create и library:upload",
     description: "Эти файлы пришли через виртуальный принтер в режиме проверки. Добавьте их в файловый менеджер или отклоните.",
     from: "с {{ip}}",
     addToFileManager: "В файловый менеджер",

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

@@ -7406,6 +7406,7 @@ errors: {
   // Pending Uploads Panel
   pendingUploads: {
     title: 'Väntande uppladdningar ({{count}})',
+    noApprovePermission: 'Att lägga i filhanteraren kräver behörigheterna queue:create och library:upload',
     description: 'De här filerna kom in via en virtuell skrivare i granskningsläge. Lägg dem i filhanteraren eller släng dem.',
     from: 'från {{ip}}',
     addToFileManager: 'Lägg i filhanteraren',

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

@@ -7268,6 +7268,7 @@ export default {
   // Bekleyen Yüklemeler Paneli
   pendingUploads: {
     title: 'Bekleyen yüklemeler ({{count}})',
+    noApprovePermission: 'Dosya Yöneticisine eklemek için queue:create ve library:upload izinleri gerekir',
     description: 'Bu dosyalar inceleme modundaki bir sanal yazıcıdan geldi. Onları Dosya Yöneticisine ekleyin ya da atın.',
     from: '{{ip}} adresinden',
     addToFileManager: 'Dosya Yöneticisine ekle',

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

@@ -7365,6 +7365,7 @@ export default {
   // Pending Uploads Panel
   pendingUploads: {
     title: "Очікувані завантаження ({{count}})",
+    noApprovePermission: "Для додавання до менеджера файлів потрібні дозволи queue:create і library:upload",
     description: "Ці файли надійшли через віртуальний принтер у режимі перевірки. Додайте їх до менеджера файлів або відхиліть.",
     from: "з {{ip}}",
     addToFileManager: "До менеджера файлів",

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

@@ -7309,6 +7309,7 @@ export default {
   // Pending Uploads Panel
   pendingUploads: {
     title: '待处理上传({{count}})',
+    noApprovePermission: '加入文件管理器需要 queue:create 和 library:upload 权限',
     description: '这些文件来自审核模式下的虚拟打印机。将它们加入文件管理器或丢弃。',
     from: '来自 {{ip}}',
     addToFileManager: '加入文件管理器',

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

@@ -7309,6 +7309,7 @@ export default {
   // Pending Uploads Panel
   pendingUploads: {
     title: '待處理上傳({{count}})',
+    noApprovePermission: '加入檔案管理器需要 queue:create 與 library:upload 權限',
     description: '這些檔案來自審核模式下的虛擬印表機。將它們加入檔案管理器或捨棄。',
     from: '來自 {{ip}}',
     addToFileManager: '加入檔案管理器',

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
static/assets/ArchivesPage-B_rJqS4I.js


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
static/assets/CamWallPage-BX6GuqAu.js


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
static/assets/CameraPage-CRAGdiq2.js


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
static/assets/FileManagerPage-C8TWAsBn.js


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
static/assets/InventoryPage-DmGboQua.js


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
static/assets/Layout-CeU3kIEZ.js


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
static/assets/ModelSourcesPage-DSbbbP-S.js


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
static/assets/PrintersPage-B0mpqybt.js


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
static/assets/SettingsPage-CKLH6JLh.js


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
static/assets/SliceModal-BZGr23BN.js


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
static/assets/SpoolBuddyInventoryPage-DyGPUD0i.js


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 1
static/assets/SpoolBuddySettingsPage-Dtd9Qs9V.js


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
static/assets/de-EqruBgE8.js


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
static/assets/es-BLk2fQHR.js


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
static/assets/fr-DcGLjOB9.js


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
static/assets/index-xRy4ceQQ.js


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
static/assets/it-CRvoD9SH.js


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
static/assets/ja-B7EvEMFf.js


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
static/assets/ko-vPn7dkOw.js


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
static/assets/locationSensorDefaults-DLVLw5wf.js


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
static/assets/nl-CAbPOvvZ.js


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
static/assets/pl-DjtlmnRG.js


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
static/assets/pt-BR-JNJqTFi3.js


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
static/assets/ru-CVPUX1AG.js


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
static/assets/spoolCost-C3FynVBJ.js


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
static/assets/sv-BPyr4Xjz.js


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
static/assets/tr-BdhYwrzF.js


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
static/assets/uk-DsWsBmoQ.js


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
static/assets/usePageFileDrop-4TJxpJO8.js


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
static/assets/zh-CN-6hIB_S5e.js


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
static/assets/zh-TW-B9XOFrQk.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-BfS_79_Q.js"></script>
+    <script type="module" crossorigin src="/assets/index-xRy4ceQQ.js"></script>
     <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-aKtaBQYM.js">
     <link rel="modulepreload" crossorigin href="/assets/client-B6bKL_Bn.js">
     <link rel="modulepreload" crossorigin href="/assets/react-dom-Dlxsc6eh.js">
@@ -122,7 +122,7 @@
     <link rel="modulepreload" crossorigin href="/assets/useColorCatalogVersion-CzRz1CGh.js">
     <link rel="modulepreload" crossorigin href="/assets/circle-minus-CsLeBcC0.js">
     <link rel="modulepreload" crossorigin href="/assets/ConnectionDiagnostic-X2u1rQlE.js">
-    <link rel="modulepreload" crossorigin href="/assets/Layout-heUoL0ca.js">
+    <link rel="modulepreload" crossorigin href="/assets/Layout-CeU3kIEZ.js">
     <link rel="stylesheet" crossorigin href="/assets/index-C7v2Gy3m.css">
   </head>
   <body>

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů