Merge pull request 'fix: openEdit fetch ProductTypeDetail antes de abrir dialog (closes #37)' (#42) from fix/issue-37-openedit-fetch-detail into main
This commit was merged in pull request #42.
This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
import { useState } from 'react'
|
||||
import { AlertCircle, Plus } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { CanPerform } from '@/components/auth/CanPerform'
|
||||
import { useProductTypes } from '../hooks/useProductTypes'
|
||||
import { useDeactivateProductType } from '../hooks/useDeactivateProductType'
|
||||
import { getProductTypeById } from '../api/getProductTypeById'
|
||||
import { ProductTypeFormDialog } from '../components/ProductTypeFormDialog'
|
||||
import { DeactivateProductTypeDialog } from '../components/DeactivateProductTypeDialog'
|
||||
import type { ProductTypeListItem, ProductTypeDetail } from '../types'
|
||||
@@ -18,11 +20,13 @@ export function ProductTypesPage() {
|
||||
// ── Edit dialog state ────────────────────────────────────────────────────
|
||||
const [editOpen, setEditOpen] = useState(false)
|
||||
const [editingProductType, setEditingProductType] = useState<ProductTypeDetail | null>(null)
|
||||
const [editLoadingId, setEditLoadingId] = useState<number | null>(null)
|
||||
|
||||
// ── Deactivate dialog state ──────────────────────────────────────────────
|
||||
const [deactivateOpen, setDeactivateOpen] = useState(false)
|
||||
const [deactivatingProductType, setDeactivatingProductType] = useState<ProductTypeListItem | null>(null)
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const { data: paged, isLoading, isError } = useProductTypes({ activo: true })
|
||||
const { mutateAsync: deactivateProductType } = useDeactivateProductType()
|
||||
|
||||
@@ -32,19 +36,20 @@ export function ProductTypesPage() {
|
||||
setCreateOpen(true)
|
||||
}
|
||||
|
||||
function openEdit(pt: ProductTypeListItem) {
|
||||
// Map list item to detail shape for pre-filling (multimedia nulls are fine for list item)
|
||||
const detail: ProductTypeDetail = {
|
||||
...pt,
|
||||
maxImages: null,
|
||||
maxImageSizeMB: null,
|
||||
maxImageWidth: null,
|
||||
maxImageHeight: null,
|
||||
fechaCreacion: '',
|
||||
fechaModificacion: null,
|
||||
async function openEdit(pt: ProductTypeListItem) {
|
||||
setEditLoadingId(pt.id)
|
||||
try {
|
||||
const detail = await queryClient.fetchQuery({
|
||||
queryKey: ['product-type', pt.id],
|
||||
queryFn: () => getProductTypeById(pt.id),
|
||||
})
|
||||
setEditingProductType(detail)
|
||||
setEditOpen(true)
|
||||
} catch {
|
||||
toast.error('Error al cargar el tipo de producto')
|
||||
} finally {
|
||||
setEditLoadingId(null)
|
||||
}
|
||||
setEditingProductType(detail)
|
||||
setEditOpen(true)
|
||||
}
|
||||
|
||||
function openDeactivate(pt: ProductTypeListItem) {
|
||||
@@ -136,6 +141,7 @@ export function ProductTypesPage() {
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => openEdit(pt)}
|
||||
disabled={editLoadingId === pt.id}
|
||||
>
|
||||
Editar
|
||||
</Button>
|
||||
|
||||
@@ -8,7 +8,7 @@ import { MemoryRouter, Routes, Route } from 'react-router-dom'
|
||||
import React from 'react'
|
||||
import { ProductTypesPage } from '../../../features/product-types/pages/ProductTypesPage'
|
||||
import { useAuthStore } from '../../../stores/authStore'
|
||||
import type { ProductTypeListItem, PagedResult } from '../../../features/product-types/types'
|
||||
import type { ProductTypeListItem, ProductTypeDetail, PagedResult } from '../../../features/product-types/types'
|
||||
|
||||
const API_URL = 'http://localhost:5000'
|
||||
|
||||
@@ -45,6 +45,23 @@ const mockItem: ProductTypeListItem = {
|
||||
isActive: true,
|
||||
}
|
||||
|
||||
const mockDetail: ProductTypeDetail = {
|
||||
id: 1,
|
||||
nombre: 'Clasificados',
|
||||
hasDuration: true,
|
||||
requiresText: true,
|
||||
requiresCategory: false,
|
||||
isBundle: false,
|
||||
allowImages: true,
|
||||
maxImages: 7,
|
||||
maxImageSizeMB: 5,
|
||||
maxImageWidth: 800,
|
||||
maxImageHeight: 600,
|
||||
isActive: true,
|
||||
fechaCreacion: '2024-01-01T00:00:00Z',
|
||||
fechaModificacion: null,
|
||||
}
|
||||
|
||||
const mockPaged: PagedResult<ProductTypeListItem> = {
|
||||
items: [mockItem],
|
||||
page: 1,
|
||||
@@ -175,6 +192,7 @@ describe('ProductTypesPage — edit dialog', () => {
|
||||
it('opens edit dialog pre-filled when Edit button is clicked', async () => {
|
||||
server.use(
|
||||
http.get(`${API_URL}/api/v1/product-types`, () => HttpResponse.json(mockPaged)),
|
||||
http.get(`${API_URL}/api/v1/product-types/1`, () => HttpResponse.json(mockDetail)),
|
||||
)
|
||||
renderPage(adminUser)
|
||||
await waitFor(() => expect(screen.getByText('Clasificados')).toBeInTheDocument())
|
||||
@@ -185,6 +203,66 @@ describe('ProductTypesPage — edit dialog', () => {
|
||||
const input = screen.getByLabelText(/nombre/i) as HTMLInputElement
|
||||
expect(input.value).toBe('Clasificados')
|
||||
})
|
||||
|
||||
it('fetches full detail and populates all multimedia fields in edit dialog', async () => {
|
||||
server.use(
|
||||
http.get(`${API_URL}/api/v1/product-types`, () => HttpResponse.json(mockPaged)),
|
||||
http.get(`${API_URL}/api/v1/product-types/1`, () => HttpResponse.json(mockDetail)),
|
||||
)
|
||||
renderPage(adminUser)
|
||||
await waitFor(() => expect(screen.getByText('Clasificados')).toBeInTheDocument())
|
||||
await userEvent.click(screen.getByRole('button', { name: /editar/i }))
|
||||
await waitFor(() =>
|
||||
expect(screen.getByRole('heading', { name: /editar tipo/i })).toBeInTheDocument(),
|
||||
)
|
||||
// Multimedia fields must be populated from the fetched detail (not null)
|
||||
const maxImages = screen.getByLabelText(/máx\. imágenes/i) as HTMLInputElement
|
||||
const maxSizeMB = screen.getByLabelText(/máx\. tamaño/i) as HTMLInputElement
|
||||
const maxWidth = screen.getByLabelText(/ancho máx/i) as HTMLInputElement
|
||||
const maxHeight = screen.getByLabelText(/alto máx/i) as HTMLInputElement
|
||||
expect(maxImages.value).toBe('7')
|
||||
expect(maxSizeMB.value).toBe('5')
|
||||
expect(maxWidth.value).toBe('800')
|
||||
expect(maxHeight.value).toBe('600')
|
||||
})
|
||||
|
||||
it('disables edit button while fetch is in flight', async () => {
|
||||
let resolveDetail!: (value: unknown) => void
|
||||
server.use(
|
||||
http.get(`${API_URL}/api/v1/product-types`, () => HttpResponse.json(mockPaged)),
|
||||
http.get(`${API_URL}/api/v1/product-types/1`, () =>
|
||||
new Promise((resolve) => {
|
||||
resolveDetail = () => resolve(HttpResponse.json(mockDetail))
|
||||
}),
|
||||
),
|
||||
)
|
||||
renderPage(adminUser)
|
||||
await waitFor(() => expect(screen.getByText('Clasificados')).toBeInTheDocument())
|
||||
const editBtn = screen.getByRole('button', { name: /editar/i })
|
||||
await userEvent.click(editBtn)
|
||||
// Button should be disabled while fetch is pending
|
||||
await waitFor(() => expect(editBtn).toBeDisabled())
|
||||
// Resolve the fetch so the dialog opens and no unhandled promise remains
|
||||
resolveDetail(undefined)
|
||||
await waitFor(() =>
|
||||
expect(screen.getByRole('heading', { name: /editar tipo/i })).toBeInTheDocument(),
|
||||
)
|
||||
})
|
||||
|
||||
it('shows toast error and does not open dialog when fetch fails', async () => {
|
||||
const { toast } = await import('sonner')
|
||||
server.use(
|
||||
http.get(`${API_URL}/api/v1/product-types`, () => HttpResponse.json(mockPaged)),
|
||||
http.get(`${API_URL}/api/v1/product-types/1`, () =>
|
||||
HttpResponse.json({ error: 'not_found' }, { status: 404 }),
|
||||
),
|
||||
)
|
||||
renderPage(adminUser)
|
||||
await waitFor(() => expect(screen.getByText('Clasificados')).toBeInTheDocument())
|
||||
await userEvent.click(screen.getByRole('button', { name: /editar/i }))
|
||||
await waitFor(() => expect(toast.error).toHaveBeenCalled())
|
||||
expect(screen.queryByRole('heading', { name: /editar tipo/i })).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ─── Deactivate dialog ───────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user