Refactor media upload handling in MediaPanel: remove direct upload logic and integrate useUploadStore for file enqueueing. Update translations for upload status and add UploadSnackbar component to display upload notifications in the UI.

This commit is contained in:
Leonid Pershin
2026-07-24 22:25:01 +03:00
parent 2523808e3b
commit 8f8ce5122a
7 changed files with 188 additions and 53 deletions
@@ -0,0 +1,99 @@
import { create } from 'zustand'
import { queryClient } from '@/shared/api/query-client'
import i18n from '@/shared/lib/i18n'
import { toast } from '@/shared/ui/toast-store'
import { listMedia, uploadMedia } from './api'
export type UploadItem = {
id: string
name: string
percent: number
status: 'queued' | 'uploading' | 'done' | 'error'
}
type UploadStore = {
items: UploadItem[]
active: boolean
minimized: boolean
enqueue: (files: File[]) => Promise<void>
toggleMinimize: () => void
dismiss: () => void
}
// Очередь и флаг живут вне React — загрузка продолжается при любой навигации.
let counter = 0
const queue: { id: string; file: File }[] = []
let running = false
const patch = (id: string, changes: Partial<UploadItem>) =>
useUploadStore.setState((s) => ({
items: s.items.map((i) => (i.id === id ? { ...i, ...changes } : i)),
}))
async function pump() {
if (running) return
running = true
useUploadStore.setState({ active: true, minimized: false })
let uploaded = 0
while (queue.length > 0) {
const job = queue.shift()!
patch(job.id, { status: 'uploading', percent: 0 })
try {
await uploadMedia(job.file, (percent) => patch(job.id, { percent }))
patch(job.id, { status: 'done', percent: 100 })
uploaded++
void queryClient.invalidateQueries({ queryKey: ['admin', 'media'] })
} catch {
patch(job.id, { status: 'error' })
}
}
running = false
useUploadStore.setState({ active: false })
if (uploaded > 0) toast.success(i18n.t('admin.media.uploadedCount', { count: uploaded }))
}
export const useUploadStore = create<UploadStore>((set) => ({
items: [],
active: false,
minimized: false,
enqueue: async (files) => {
// Пропускаем дубликаты: уже в библиотеке (не проваленные) и уже в текущей очереди.
let existing = new Set<string>()
try {
const all = await listMedia({
page: 1,
pageSize: 1000,
statuses: ['Pending', 'Processing', 'Ready'],
})
existing = new Set(all.items.map((a) => a.originalFileName))
} catch {
// положимся на серверную проверку
}
const inFlight = new Set(
useUploadStore
.getState()
.items.filter((i) => i.status !== 'error')
.map((i) => i.name),
)
const toAdd = files.filter((f) => !existing.has(f.name) && !inFlight.has(f.name))
const skipped = files.length - toAdd.length
if (toAdd.length > 0) {
const newItems: UploadItem[] = toAdd.map((file) => {
const id = `u${++counter}`
queue.push({ id, file })
return { id, name: file.name, percent: 0, status: 'queued' }
})
set((s) => ({ items: [...s.items, ...newItems], minimized: false }))
void pump()
}
if (skipped > 0) toast.message(i18n.t('admin.media.skippedDuplicates', { count: skipped }))
},
toggleMinimize: () => set((s) => ({ minimized: !s.minimized })),
dismiss: () => set({ items: [] }),
}))