From ea842ee46ce5256e58223794fe8294609c5869c5 Mon Sep 17 00:00:00 2001 From: Guillermo Arrieta Medina Date: Wed, 11 Mar 2026 16:01:09 -0600 Subject: [PATCH] =?UTF-8?q?close=20#170:=20se=20a=C3=B1adieron=20validacio?= =?UTF-8?q?nes=20y=20mejoras=20en=20el=20modal=20de=20nueva=20bibliograf?= =?UTF-8?q?=C3=ADa?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -Se implementaron restricciones en SugerenciasStep: el campo de búsqueda se limitó a 200 caracteres y la generación quedó bloqueada si hay 20 o más referencias seleccionadas; se añadió tooltip en el botón de generar cuando la query tiene menos de 3 caracteres. -Se reforzaron validaciones en FormatoYCitasStep y DatosBasicosManualStep: el título se trim-eó y se forzó a no quedar vacío (max 500 caracteres); si un título queda vacío se hace scroll al input/card, se muestra mensaje de error junto al label y se resalta el input; autores se limitó a 2000 caracteres; editorial a 300 caracteres; ISBN a 20 caracteres; el año se convirtió en input numérico permitiendo vacío o un año de 4 dígitos entre 1450 y el año actual +1. -Se añadieron checkboxes "Año aproximado" y "En prensa" (mutuamente excluyentes): "En prensa" deshabilita el input de año y se marca el estado para citeproc; "Año aproximado" se envía como circa en issued. -Al generar CSL se incluyeron las propiedades issued.circa y status ('in press') según los flags del ref. -En ResumenStep se añadieron advertencias por referencia cuando falte autor(es), año (si no está "en prensa"), editorial o ISBN. -Se corrigieron detalles de UX en edición de autores para preservar saltos de línea y se añadieron handlers para evitar errores de validación al mover entre pasos. --- .../nueva/NuevaBibliografiaModalContainer.tsx | 495 ++++++++++++++---- 1 file changed, 403 insertions(+), 92 deletions(-) diff --git a/src/features/bibliografia/nueva/NuevaBibliografiaModalContainer.tsx b/src/features/bibliografia/nueva/NuevaBibliografiaModalContainer.tsx index c1cd980..a389f92 100644 --- a/src/features/bibliografia/nueva/NuevaBibliografiaModalContainer.tsx +++ b/src/features/bibliografia/nueva/NuevaBibliografiaModalContainer.tsx @@ -8,7 +8,14 @@ import { RefreshCw, X, } from 'lucide-react' -import { useEffect, useMemo, useRef, useState } from 'react' +import { + forwardRef, + useEffect, + useImperativeHandle, + useMemo, + useRef, + useState, +} from 'react' import type { BuscarBibliografiaRequest } from '@/data' import type { @@ -101,6 +108,9 @@ const IDIOMA_TO_OPEN_LIBRARY: Record = { RU: 'rus', } +const MIN_YEAR = 1450 +const MAX_YEAR = new Date().getFullYear() + 1 + type CSLAuthor = { family: string given: string @@ -112,7 +122,8 @@ type CSLItem = { title: string author: Array publisher?: string - issued?: { 'date-parts': Array> } + issued?: { 'date-parts': Array>; circa?: boolean } + status?: string ISBN?: string } @@ -131,6 +142,8 @@ type BibliografiaRef = { authors: Array publisher?: string year?: number + yearIsApproximate?: boolean + isInPress?: boolean isbn?: string tipo: BibliografiaTipo @@ -209,6 +222,19 @@ function tryParseYear(publishedDate?: string): number | undefined { return Number.isFinite(year) ? year : undefined } +function sanitizeYearInput(value: string): string { + return value.replace(/[^\d]/g, '').slice(0, 4) +} + +function tryParseStrictYear(value: string): number | undefined { + const cleaned = sanitizeYearInput(value) + if (!/^\d{4}$/.test(cleaned)) return undefined + const year = Number.parseInt(cleaned, 10) + if (!Number.isFinite(year)) return undefined + if (year < MIN_YEAR || year > MAX_YEAR) return undefined + return year +} + function randomUUID(): string { try { const c = (globalThis as any).crypto @@ -425,6 +451,8 @@ export function NuevaBibliografiaModalContainer({ const navigate = useNavigate() const createBibliografia = useCreateBibliografia() + const formatoStepRef = useRef(null) + const [wizard, setWizard] = useState({ metodo: null, ia: { @@ -513,6 +541,8 @@ export function NuevaBibliografiaModalContainer({ async function handleBuscarSugerencias() { const hadNoSugerenciasBefore = wizard.ia.sugerencias.length === 0 + if (wizard.ia.sugerencias.filter((s) => s.selected).length >= 20) return + const q = wizard.ia.q.trim() if (!q) return @@ -633,13 +663,21 @@ export function NuevaBibliografiaModalContainer({ const cslItems: Record = {} for (const r of refs) { + const trimmedTitle = r.title.trim() cslItems[r.id] = { id: r.id, type: 'book', - title: r.title || 'Sin título', + title: trimmedTitle || 'Sin título', author: r.authors.map(parsearAutor), publisher: r.publisher, - issued: r.year ? { 'date-parts': [[r.year]] } : undefined, + issued: + r.isInPress || !r.year + ? undefined + : { + 'date-parts': [[r.year]], + circa: r.yearIsApproximate ? true : undefined, + }, + status: r.isInPress ? 'in press' : undefined, ISBN: r.isbn, } } @@ -797,7 +835,14 @@ export function NuevaBibliografiaModalContainer({ ) : (