feat(client): remove direct upload from product form, filter gallery to resized

This commit is contained in:
Kirill
2026-05-17 18:17:27 +05:00
parent 35dee985f7
commit f0365d0b98
@@ -1,4 +1,4 @@
import { useRef, useState } from 'react' import { useState } from 'react'
import Alert from '@mui/material/Alert' import Alert from '@mui/material/Alert'
import Box from '@mui/material/Box' import Box from '@mui/material/Box'
import Button from '@mui/material/Button' import Button from '@mui/material/Button'
@@ -31,10 +31,8 @@ import {
fetchAdminProducts, fetchAdminProducts,
fetchCategories, fetchCategories,
updateProduct, updateProduct,
uploadAdminProductImages,
} from '@/entities/product/api/product-api' } from '@/entities/product/api/product-api'
import type { Category, Product } from '@/entities/product/model/types' import type { Category, Product } from '@/entities/product/model/types'
import { formatAdminImageMaxSizeHint } from '@/shared/constants/upload-limits'
import { formatPriceRub } from '@/shared/lib/format-price' import { formatPriceRub } from '@/shared/lib/format-price'
import { getErrorMessage } from '@/shared/lib/get-error-message' import { getErrorMessage } from '@/shared/lib/get-error-message'
import { invalidateQueryKeys } from '@/shared/lib/invalidate-query-keys' import { invalidateQueryKeys } from '@/shared/lib/invalidate-query-keys'
@@ -203,20 +201,7 @@ export function AdminProductsPage() {
else createMut.mutate() else createMut.mutate()
} }
const productImagesInputRef = useRef<HTMLInputElement>(null) const mutationError = createMut.error ?? updateMut.error ?? deleteMut.error
const uploadImagesMut = useMutation({
mutationFn: (picked: File[]) => uploadAdminProductImages(picked),
onSuccess: (urls) => {
const current = productForm.getValues('imageUrls')
productForm.setValue('imageUrls', [...current, ...urls], { shouldDirty: true })
if (productImagesInputRef.current) {
productImagesInputRef.current.value = ''
}
},
})
const mutationError = createMut.error ?? updateMut.error ?? deleteMut.error ?? uploadImagesMut.error
const removeImage = (url: string) => { const removeImage = (url: string) => {
const current = productForm.getValues('imageUrls') const current = productForm.getValues('imageUrls')
@@ -401,11 +386,11 @@ export function AdminProductsPage() {
/> />
<Box> <Box>
<Typography variant="subtitle2" sx={{ mb: 0.5 }}> <Typography variant="subtitle2" sx={{ mb: 0.5 }}>
Фото (загрузка) Фото (из галереи)
</Typography> </Typography>
<FormHelperText sx={{ mt: 0, mb: 1 }}> <FormHelperText sx={{ mt: 0, mb: 1 }}>
PNG, JPEG или WebP, до {formatAdminImageMaxSizeHint()} на файл. Крестик на превью убирает фото только из Выберите обработанные изображения из галереи. Крестик на превью убирает фото только из карточки; файл
карточки; файл остаётся на сервере и в галерее. остаётся на сервере и в галерее.
</FormHelperText> </FormHelperText>
<Box <Box
sx={{ sx={{
@@ -416,21 +401,6 @@ export function AdminProductsPage() {
flexWrap: 'wrap', flexWrap: 'wrap',
}} }}
> >
<Button component="label" variant="outlined" disabled={uploadImagesMut.isPending}>
Выбрать файлы
<input
ref={productImagesInputRef}
hidden
type="file"
accept="image/png,image/jpeg,image/webp"
multiple
onChange={(e) => {
const files = e.target.files
if (!files || files.length === 0) return
uploadImagesMut.mutate(Array.from(files))
}}
/>
</Button>
<Button <Button
variant="outlined" variant="outlined"
onClick={() => { onClick={() => {
@@ -440,8 +410,6 @@ export function AdminProductsPage() {
> >
Из галереи Из галереи
</Button> </Button>
{uploadImagesMut.isPending && <Typography color="text.secondary">Загрузка</Typography>}
{uploadImagesMut.isError && <Typography color="error">Не удалось загрузить фото</Typography>}
</Box> </Box>
{productForm.watch('imageUrls').length > 0 && ( {productForm.watch('imageUrls').length > 0 && (
@@ -558,6 +526,14 @@ export function AdminProductsPage() {
{galleryForPickQuery.data?.items.length === 0 && !galleryForPickQuery.isLoading && ( {galleryForPickQuery.data?.items.length === 0 && !galleryForPickQuery.isLoading && (
<Typography color="text.secondary">В галерее пока нет файлов. Загрузите их в разделе «Галерея».</Typography> <Typography color="text.secondary">В галерее пока нет файлов. Загрузите их в разделе «Галерея».</Typography>
)} )}
{galleryForPickQuery.data &&
galleryForPickQuery.data.items.length > 0 &&
galleryForPickQuery.data.items.filter((i) => i.isResized).length === 0 &&
!galleryForPickQuery.isLoading && (
<Typography color="text.secondary">
В галерее нет обработанных изображений. Сначала обработайте их в разделе «Галерея».
</Typography>
)}
<Box <Box
sx={{ sx={{
display: 'grid', display: 'grid',
@@ -566,33 +542,35 @@ export function AdminProductsPage() {
pt: 1, pt: 1,
}} }}
> >
{(galleryForPickQuery.data?.items ?? []).map((item) => { {(galleryForPickQuery.data?.items ?? [])
const alreadyInCard = productForm.watch('imageUrls').includes(item.url) .filter((item) => item.isResized)
return ( .map((item) => {
<FormControlLabel const alreadyInCard = productForm.watch('imageUrls').includes(item.url)
key={item.id} return (
sx={{ m: 0, alignItems: 'flex-start' }} <FormControlLabel
control={ key={item.id}
<Checkbox sx={{ m: 0, alignItems: 'flex-start' }}
checked={alreadyInCard || gallerySelectedUrls.has(item.url)} control={
disabled={alreadyInCard} <Checkbox
onChange={() => toggleGalleryPickUrl(item.url)} checked={alreadyInCard || gallerySelectedUrls.has(item.url)}
/> disabled={alreadyInCard}
} onChange={() => toggleGalleryPickUrl(item.url)}
label={
<Box sx={{ width: '100%', maxHeight: 100, borderRadius: 1, overflow: 'hidden' }}>
<OptimizedImage
src={item.url}
alt=""
widths={[320, 640]}
sizes="120px"
sx={{ width: '100%', height: '100%', objectFit: 'cover' }}
/> />
</Box> }
} label={
/> <Box sx={{ width: '100%', maxHeight: 100, borderRadius: 1, overflow: 'hidden' }}>
) <OptimizedImage
})} src={item.url}
alt=""
widths={[320, 640]}
sizes="120px"
sx={{ width: '100%', height: '100%', objectFit: 'cover' }}
/>
</Box>
}
/>
)
})}
</Box> </Box>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>