feat(export): mejora en el selector de destino y manejo de exportaciones
Se actualiza el componente ExportDropdown para incluir un selector de destino que permite elegir entre diferentes perfiles de exportación, incluyendo la opción de ZIP. Se mejora la lógica de descarga y se ajusta el componente SwordProfileSelect para manejar la selección de perfiles de exportación. Además, se realizan cambios en la página Dashboard para integrar el nuevo sistema de exportación.
This commit is contained in:
@@ -129,10 +129,30 @@ def get_optional_current_researcher(
|
||||
db: Session = Depends(get_db),
|
||||
) -> Researcher | None:
|
||||
"""
|
||||
Devuelve el investigador autenticado si hay Bearer válido.
|
||||
Si no hay Bearer, devuelve None.
|
||||
Si hay Bearer inválido, lanza 401 (no se acepta como anónimo).
|
||||
Devuelve el investigador autenticado si hay Bearer válido y la sesión sigue activa.
|
||||
|
||||
Sin Bearer, token inválido/expirado o investigador no autenticado → None.
|
||||
Las rutas públicas (p. ej. búsqueda) deben seguir funcionando aunque el navegador
|
||||
conserve un JWT caducado en localStorage.
|
||||
"""
|
||||
if not creds or not creds.credentials:
|
||||
return None
|
||||
return get_current_researcher(request=request, creds=creds, db=db)
|
||||
|
||||
try:
|
||||
payload = _decode_token(creds.credentials)
|
||||
except HTTPException:
|
||||
return None
|
||||
|
||||
if payload.get("typ") != "access":
|
||||
return None
|
||||
|
||||
orcid_id = payload.get("sub")
|
||||
if not isinstance(orcid_id, str) or not is_valid_orcid(orcid_id):
|
||||
return None
|
||||
|
||||
researcher = db.query(Researcher).filter(Researcher.orcid_id == orcid_id).first()
|
||||
if not researcher or not researcher.authenticated:
|
||||
return None
|
||||
|
||||
request.state.researcher = researcher
|
||||
return researcher
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg id="Capa_3" data-name="Capa 3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 360 497"><defs><style>.cls-1{fill:#1b4a73;}.cls-2{fill:#f47d21;}</style></defs><title>eprints-logo</title><polygon class="cls-1" points="214 0 0 0 0 497 360 497 360 131 326 131 326 465 34 465 34 33 214 33 214 0"/><polygon class="cls-2" points="245 0 245 106 360 106 245 0"/><path class="cls-1" d="M198,267.55c.93,58,35.71,81.84,77,81.84,29.21,0,47.29-5.36,62.13-12.18l7.42,30.69c-14.37,6.82-39.41,15.1-75.12,15.1C200.27,383,159,334.77,159,263.65S198.88,137,264.26,137C338,137,357,204.22,357,247.58c0,8.77-.46,15.1-1.39,20Zm119.64-30.69c.46-26.79-10.67-69.17-56.58-69.17-41.73,0-59.35,39.46-62.6,69.17Z" transform="translate(-81.5 -7.5)"/></svg>
|
||||
|
After Width: | Height: | Size: 728 B |
@@ -1,40 +1,17 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
DocumentIcon,
|
||||
DownloadIcon,
|
||||
PackageIcon,
|
||||
SparkleIcon,
|
||||
} from "../ui/Icons";
|
||||
import { Spinner } from "../ui/Spinner";
|
||||
import { SwordProfileSelect } from "./SwordProfileSelect";
|
||||
import { DEFAULT_EXPORT_PROFILE } from "../../utils/exportProfiles";
|
||||
|
||||
const FORMATS = [
|
||||
{
|
||||
format: "xml",
|
||||
icon: <DocumentIcon size={20} className="shrink-0 text-ink-secondary" />,
|
||||
label: "SWORD XML",
|
||||
desc: "Según destino seleccionado",
|
||||
},
|
||||
{
|
||||
format: "zip",
|
||||
icon: <PackageIcon size={20} className="shrink-0 text-ink-secondary" />,
|
||||
label: "Paquete ZIP",
|
||||
desc: "XML + ficheros adjuntos",
|
||||
},
|
||||
];
|
||||
import {
|
||||
DEFAULT_EXPORT_DESTINATION,
|
||||
resolveExportFromDestination,
|
||||
} from "../../utils/exportProfiles";
|
||||
|
||||
/**
|
||||
* SWORD export dropdown. Delegatea the actual download to `onExport(format)`.
|
||||
*
|
||||
* Props:
|
||||
* - `isAuthenticated` → cambia el texto del botón principal.
|
||||
* - `newPublicationsCount` → cuántas publicaciones tiene downloaded_by_me=false.
|
||||
* - `selectedCount` → publicaciones seleccionadas manualmente.
|
||||
* - `exportingFormat` → formato en curso (pone el botón en loading).
|
||||
* - `swordProfile` → perfil SWORD (dublin_core, dspace, eprints…).
|
||||
* - `onSwordProfileChange` → callback al cambiar destino.
|
||||
* Controles de exportación: selector de destino + botón único de descarga.
|
||||
* Delega la descarga en `onExport(format, profile)`.
|
||||
*/
|
||||
export function ExportDropdown({
|
||||
onExport,
|
||||
@@ -42,38 +19,24 @@ export function ExportDropdown({
|
||||
selectedCount = 0,
|
||||
isAuthenticated = false,
|
||||
newPublicationsCount = 0,
|
||||
swordProfile = DEFAULT_EXPORT_PROFILE,
|
||||
onSwordProfileChange,
|
||||
exportDestination = DEFAULT_EXPORT_DESTINATION,
|
||||
onExportDestinationChange,
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const rootRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
function handleClick(event) {
|
||||
if (rootRef.current && !rootRef.current.contains(event.target)) {
|
||||
setOpen(false);
|
||||
}
|
||||
}
|
||||
document.addEventListener("mousedown", handleClick);
|
||||
return () => document.removeEventListener("mousedown", handleClick);
|
||||
}, []);
|
||||
|
||||
const isBusy = Boolean(exportingFormat);
|
||||
const hasSelection = selectedCount > 0;
|
||||
|
||||
function handlePick(format) {
|
||||
setOpen(false);
|
||||
onExport(format, format === "xml" ? swordProfile : undefined);
|
||||
const nothingToDownload =
|
||||
isAuthenticated && !hasSelection && newPublicationsCount === 0;
|
||||
|
||||
function handleDownload() {
|
||||
const { format, profile } = resolveExportFromDestination(exportDestination);
|
||||
onExport(format, profile);
|
||||
}
|
||||
|
||||
// Label logic:
|
||||
// manual selection → always "Exportar seleccionadas (N)"
|
||||
// logged in, no selection → "Descargar lo nuevo (N)" or "Todo descargado"
|
||||
// not logged in, no selection → "Descargar todo"
|
||||
let idleLabel;
|
||||
let showSparkle = false;
|
||||
if (hasSelection) {
|
||||
idleLabel = `Exportar seleccionadas (${selectedCount})`;
|
||||
idleLabel = `Descargar selección (${selectedCount})`;
|
||||
} else if (isAuthenticated) {
|
||||
if (newPublicationsCount > 0) {
|
||||
idleLabel = `Descargar lo nuevo (${newPublicationsCount})`;
|
||||
@@ -86,18 +49,18 @@ export function ExportDropdown({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center justify-end gap-2">
|
||||
<div className="flex flex-wrap items-center justify-end gap-2 sm:flex-nowrap">
|
||||
<SwordProfileSelect
|
||||
id="dashboard-sword-profile"
|
||||
value={swordProfile}
|
||||
onChange={onSwordProfileChange}
|
||||
id="dashboard-export-destination"
|
||||
value={exportDestination}
|
||||
onChange={onExportDestinationChange}
|
||||
includeZip
|
||||
/>
|
||||
|
||||
<div className="relative" ref={rootRef}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen((o) => !o)}
|
||||
disabled={isBusy || (isAuthenticated && !hasSelection && newPublicationsCount === 0)}
|
||||
onClick={handleDownload}
|
||||
disabled={isBusy || nothingToDownload}
|
||||
className="inline-flex items-center gap-2 rounded-lg border border-surface-border-strong bg-surface-primary px-[18px] py-2.5 text-sm font-medium text-ink-primary transition-colors enabled:hover:bg-surface-secondary disabled:cursor-not-allowed disabled:opacity-70"
|
||||
>
|
||||
{isBusy ? (
|
||||
@@ -107,37 +70,8 @@ export function ExportDropdown({
|
||||
) : (
|
||||
<DownloadIcon />
|
||||
)}
|
||||
{isBusy
|
||||
? `Exportando ${exportingFormat.toUpperCase()}...`
|
||||
: idleLabel}
|
||||
{!isBusy && <ChevronDownIcon />}
|
||||
{isBusy ? `Descargando ${exportingFormat.toUpperCase()}...` : idleLabel}
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div className="absolute right-0 top-[calc(100%+6px)] z-50 min-w-[210px] overflow-hidden rounded-xl border border-surface-border-strong bg-surface-primary shadow-lg">
|
||||
{FORMATS.map(({ format, icon, label, desc }, idx) => (
|
||||
<button
|
||||
key={format}
|
||||
type="button"
|
||||
onClick={() => handlePick(format)}
|
||||
className={`flex w-full items-center gap-3 px-4 py-3 text-left transition-colors hover:bg-surface-secondary ${
|
||||
idx < FORMATS.length - 1
|
||||
? "border-b border-surface-border/60"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{icon}
|
||||
<div>
|
||||
<div className="text-sm font-medium text-ink-primary">
|
||||
{label}
|
||||
</div>
|
||||
<div className="text-xs text-ink-tertiary">{desc}</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ export function ResearcherCard({ researcher, actions = null }) {
|
||||
</div>
|
||||
|
||||
{actions && (
|
||||
<div className="flex shrink-0 flex-wrap items-center gap-2.5">
|
||||
<div className="ml-auto flex shrink-0 flex-col items-end gap-2.5">
|
||||
{actions}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,35 +1,103 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { ChevronDownIcon } from "../ui/Icons";
|
||||
import { ExportProfileIcon } from "../ui/destination-logos/ExportProfileIcon";
|
||||
import {
|
||||
DEFAULT_EXPORT_PROFILE,
|
||||
EXPORT_PROFILE_OPTIONS,
|
||||
ZIP_DESTINATION_OPTION,
|
||||
} from "../../utils/exportProfiles";
|
||||
|
||||
/**
|
||||
* Selector de destino para exportación SWORD XML (DSpace, EPrints, Dublin Core…).
|
||||
* Selector de destino para exportación (perfiles SWORD XML y, opcionalmente, ZIP).
|
||||
*/
|
||||
export function SwordProfileSelect({
|
||||
value = DEFAULT_EXPORT_PROFILE,
|
||||
onChange,
|
||||
id = "sword-export-profile",
|
||||
className = "",
|
||||
includeZip = false,
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const rootRef = useRef(null);
|
||||
|
||||
const options = useMemo(() => {
|
||||
const items = [...EXPORT_PROFILE_OPTIONS];
|
||||
if (includeZip) {
|
||||
items.push(ZIP_DESTINATION_OPTION);
|
||||
}
|
||||
return items;
|
||||
}, [includeZip]);
|
||||
|
||||
const selected = options.find((opt) => opt.value === value) ?? options[0];
|
||||
|
||||
useEffect(() => {
|
||||
function handleClick(event) {
|
||||
if (rootRef.current && !rootRef.current.contains(event.target)) {
|
||||
setOpen(false);
|
||||
}
|
||||
}
|
||||
document.addEventListener("mousedown", handleClick);
|
||||
return () => document.removeEventListener("mousedown", handleClick);
|
||||
}, []);
|
||||
|
||||
function handlePick(optionValue) {
|
||||
onChange(optionValue);
|
||||
setOpen(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<label
|
||||
htmlFor={id}
|
||||
<div
|
||||
ref={rootRef}
|
||||
className={`flex items-center gap-2 text-sm ${className}`.trim()}
|
||||
>
|
||||
<span className="whitespace-nowrap text-ink-tertiary">Destino:</span>
|
||||
<select
|
||||
<div className="relative">
|
||||
<button
|
||||
type="button"
|
||||
id={id}
|
||||
value={value}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
className="min-w-[9.5rem] rounded-lg border border-surface-border-strong bg-surface-primary px-2.5 py-2 text-sm font-medium text-ink-primary transition-colors hover:bg-surface-secondary focus:border-brand-primary focus:outline-none focus:ring-2 focus:ring-brand-primary/20"
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={open}
|
||||
onClick={() => setOpen((o) => !o)}
|
||||
className="inline-flex min-w-44 items-center gap-2 rounded-lg border border-surface-border-strong bg-surface-primary px-3 py-2.5 text-sm font-medium text-ink-primary transition-colors hover:bg-surface-secondary"
|
||||
>
|
||||
{EXPORT_PROFILE_OPTIONS.map(({ value: optionValue, label }) => (
|
||||
<option key={optionValue} value={optionValue}>
|
||||
<ExportProfileIcon profile={selected.value} size={20} />
|
||||
<span className="truncate">{selected.label}</span>
|
||||
<ChevronDownIcon className="ml-auto shrink-0" />
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div
|
||||
role="listbox"
|
||||
aria-labelledby={id}
|
||||
className="absolute left-0 top-[calc(100%+6px)] z-50 min-w-[260px] overflow-hidden rounded-xl border border-surface-border-strong bg-surface-primary shadow-lg"
|
||||
>
|
||||
{options.map(({ value: optionValue, label, desc }, idx) => (
|
||||
<button
|
||||
key={optionValue}
|
||||
type="button"
|
||||
role="option"
|
||||
aria-selected={optionValue === value}
|
||||
onClick={() => handlePick(optionValue)}
|
||||
className={`flex w-full items-center gap-3 px-4 py-3 text-left transition-colors hover:bg-surface-secondary ${
|
||||
optionValue === value ? "bg-surface-secondary/70" : ""
|
||||
} ${
|
||||
idx < options.length - 1
|
||||
? "border-b border-surface-border/60"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<ExportProfileIcon profile={optionValue} size={20} />
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm font-medium text-ink-primary">
|
||||
{label}
|
||||
</option>
|
||||
</div>
|
||||
<div className="text-xs text-ink-tertiary">{desc}</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { DSPACE_LOGO_PATH, DSPACE_VIEWBOX } from "./dspace-path";
|
||||
|
||||
/** Logotipo oficial DSpace (#92C642). */
|
||||
export function DSpaceLogo({ size = 20, className = "" }) {
|
||||
return (
|
||||
<svg
|
||||
viewBox={DSPACE_VIEWBOX}
|
||||
width={size}
|
||||
height={size}
|
||||
className={className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
role="img"
|
||||
aria-label="DSpace"
|
||||
>
|
||||
<path fill="#92C642" d={DSPACE_LOGO_PATH} />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Logotipo DCMI Dublin Core: círculo central y anillos de puntos sobre fondo naranja.
|
||||
* Réplica del icono oficial (sunburst).
|
||||
*/
|
||||
|
||||
const ORANGE = "#FF6600";
|
||||
const CENTER = 50;
|
||||
const INNER = { count: 12, radius: 22, dotR: 4.8 };
|
||||
const OUTER = { count: 16, radius: 36, dotR: 3.6 };
|
||||
const CORE_R = 11.5;
|
||||
|
||||
function ringDots({ count, radius, dotR }) {
|
||||
return Array.from({ length: count }, (_, i) => {
|
||||
const angle = (i / count) * Math.PI * 2 - Math.PI / 2;
|
||||
return {
|
||||
key: `${radius}-${i}`,
|
||||
cx: CENTER + radius * Math.cos(angle),
|
||||
cy: CENTER + radius * Math.sin(angle),
|
||||
r: dotR,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function DublinCoreLogo({ size = 20, className = "" }) {
|
||||
const innerDots = ringDots(INNER);
|
||||
const outerDots = ringDots(OUTER);
|
||||
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 100 100"
|
||||
width={size}
|
||||
height={size}
|
||||
className={className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
role="img"
|
||||
aria-label="Dublin Core"
|
||||
>
|
||||
<rect width="100" height="100" fill={ORANGE} />
|
||||
<circle cx={CENTER} cy={CENTER} r={CORE_R} fill="#fff" />
|
||||
{innerDots.map(({ key, cx, cy, r }) => (
|
||||
<circle key={key} cx={cx} cy={cy} r={r} fill="#fff" />
|
||||
))}
|
||||
{outerDots.map(({ key, cx, cy, r }) => (
|
||||
<circle key={key} cx={cx} cy={cy} r={r} fill="#fff" />
|
||||
))}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Logotipo EPrints (vectorizado en `public/eprints-logo.svg`).
|
||||
*/
|
||||
const EPRINTS_LOGO_SRC = `${import.meta.env.BASE_URL}eprints-logo.svg`;
|
||||
|
||||
export function EPrintsLogo({ size = 20, className = "" }) {
|
||||
return (
|
||||
<img
|
||||
src={EPRINTS_LOGO_SRC}
|
||||
width={size}
|
||||
height={size}
|
||||
className={className}
|
||||
alt=""
|
||||
aria-label="EPrints"
|
||||
decoding="async"
|
||||
draggable={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { DocumentIcon, PackageIcon } from "../Icons";
|
||||
import { OrcidLogo } from "../OrcidLogo";
|
||||
import { DublinCoreLogo } from "./DublinCoreLogo";
|
||||
import { DSpaceLogo } from "./DSpaceLogo";
|
||||
import { EPrintsLogo } from "./EPrintsLogo";
|
||||
import { EXPORT_ZIP_DESTINATION } from "../../../utils/exportProfiles";
|
||||
|
||||
/**
|
||||
* Icono del destino de exportación (logos de repositorio o genérico).
|
||||
*/
|
||||
export function ExportProfileIcon({ profile, size = 20, className = "shrink-0" }) {
|
||||
switch (profile) {
|
||||
case "generic":
|
||||
return <OrcidLogo size={size} className={className} />;
|
||||
case "dublin_core":
|
||||
return <DublinCoreLogo size={size} className={className} />;
|
||||
case "dspace":
|
||||
return <DSpaceLogo size={size} className={className} />;
|
||||
case "eprints":
|
||||
return <EPrintsLogo size={15} className={className} />;
|
||||
case EXPORT_ZIP_DESTINATION:
|
||||
return <PackageIcon size={size} className={`text-ink-secondary ${className}`} />;
|
||||
default:
|
||||
return <DocumentIcon size={size} className={`text-ink-secondary ${className}`} />;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
/** Path oficial del logotipo DSpace (ver logos_preview_dropdown.html). */
|
||||
export const DSPACE_LOGO_PATH =
|
||||
"M120.726,58.569l0.11-0.006l0.116-0.01l0.106-0.013l0.111-0.01l0.11-0.023l0.109-0.019l0.107-0.023l0.106-0.029l0.106-0.023l0.106-0.033l0.103-0.034l0.096-0.035l0.104-0.04l0.101-0.042l0.099-0.042v-0.001l0.096-0.045v0l0.095-0.044l0.096-0.049l0.091-0.056v-0.001l0.094-0.05v-0.002l0.09-0.056v-0.001l0.092-0.06l0.083-0.056v-0.001l0.085-0.063l0.088-0.065v-0.002l0.087-0.063v-0.001c0.817-0.683,1.393-1.646,1.561-2.738l0.012-0.104v-0.009l0.014-0.101v-0.011l0.009-0.098v-0.012l0.009-0.101V54.38l0.005-0.095v-0.016l0.002-0.105v-16.46l-0.002-0.105v-0.016l-0.005-0.095v-0.013l-0.009-0.101v-0.012l-0.009-0.098v-0.011l-0.014-0.1v-0.01l-0.012-0.104c-0.167-1.092-0.744-2.057-1.561-2.738v-0.001l-0.087-0.063v-0.002l-0.088-0.065l-0.085-0.063v-0.001l-0.083-0.056l-0.092-0.061v0l-0.09-0.056v-0.003l-0.094-0.05v-0.001l-0.091-0.056l-0.096-0.049l-0.095-0.043v-0.001l-0.096-0.045v-0.001l-0.099-0.043l-0.101-0.042l-0.104-0.04l-0.096-0.035l-0.103-0.031l-0.106-0.036l-0.106-0.023l-0.106-0.028l-0.107-0.024l-0.109-0.019l-0.11-0.023l-0.111-0.009l-0.106-0.014l-0.116-0.01l-0.11-0.006l-0.114-0.005h-7.89c-9.715,0-15.858-7.838-15.858-17.15V6.92c0-3.812-3.102-6.915-6.914-6.915H74.085c-3.814,0-6.92,3.106-6.92,6.915v16.682c0,3.806,3.104,6.909,6.92,6.909h8.414c9.169,0,16.906,5.95,17.146,15.403v0.04c-0.24,9.453-7.977,15.402-17.146,15.402h-8.414c-3.816,0-6.92,3.103-6.92,6.909v16.682c0,3.809,3.106,6.915,6.92,6.915H89.95c3.812,0,6.914-3.104,6.914-6.915v-9.223c0-9.312,6.144-17.149,15.858-17.149h7.89L120.726,58.569z M154.772,9.956C148.631,3.814,140.15,0,130.816,0h-15.024v17.424h15.024c4.527,0,8.648,1.858,11.64,4.849c2.99,2.99,4.848,7.112,4.848,11.639v24.042c0,4.538-1.852,8.665-4.832,11.655l-0.016-0.016c-2.991,2.991-7.113,4.849-11.64,4.849h-15.024v17.424h15.024c9.333,0,17.815-3.814,23.956-9.956v-0.033c6.142-6.143,9.955-14.614,9.955-23.923V33.912C164.727,24.578,160.914,16.097,154.772,9.956z";
|
||||
|
||||
export const DSPACE_VIEWBOX = "67 0 98 93";
|
||||
@@ -16,7 +16,10 @@ import {
|
||||
syncResearcher,
|
||||
} from "../services/api";
|
||||
import { isValidOrcid } from "../utils/orcid";
|
||||
import { DEFAULT_EXPORT_PROFILE, swordXmlFilename } from "../utils/exportProfiles";
|
||||
import {
|
||||
DEFAULT_EXPORT_DESTINATION,
|
||||
swordXmlFilename,
|
||||
} from "../utils/exportProfiles";
|
||||
import { useAuth } from "../contexts/AuthContext";
|
||||
|
||||
const SUCCESS_FLASH_MS = 3000;
|
||||
@@ -50,7 +53,9 @@ export function DashboardPage() {
|
||||
|
||||
const [syncStatus, setSyncStatus] = useState("idle"); // idle | loading | success
|
||||
const [exportingFormat, setExportingFormat] = useState(null);
|
||||
const [swordProfile, setSwordProfile] = useState(DEFAULT_EXPORT_PROFILE);
|
||||
const [exportDestination, setExportDestination] = useState(
|
||||
DEFAULT_EXPORT_DESTINATION,
|
||||
);
|
||||
|
||||
const [selectedIds, setSelectedIds] = useState(() => new Set());
|
||||
|
||||
@@ -140,7 +145,7 @@ export function DashboardPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleExport(format, profile = DEFAULT_EXPORT_PROFILE) {
|
||||
async function handleExport(format, profile = DEFAULT_EXPORT_DESTINATION) {
|
||||
setExportingFormat(format);
|
||||
try {
|
||||
let ids;
|
||||
@@ -226,8 +231,8 @@ export function DashboardPage() {
|
||||
selectedCount={selectedIds.size}
|
||||
isAuthenticated={isAuthenticated}
|
||||
newPublicationsCount={newPublicationIds.length}
|
||||
swordProfile={swordProfile}
|
||||
onSwordProfileChange={setSwordProfile}
|
||||
exportDestination={exportDestination}
|
||||
onExportDestinationChange={setExportDestination}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -129,7 +129,19 @@ async function request(path, { method = "GET", body, signal, headers } = {}) {
|
||||
|
||||
const detail =
|
||||
payload?.detail ?? payload?.message ?? response.statusText ?? "Error";
|
||||
throw new ApiError(typeof detail === "string" ? detail : "Error de API", {
|
||||
const detailText = typeof detail === "string" ? detail : "Error de API";
|
||||
|
||||
// Sesión caducada: no bloquear rutas públicas; el backend ya ignora Bearer inválido
|
||||
// en búsqueda, pero otras rutas pueden seguir devolviendo 401.
|
||||
if (
|
||||
response.status === 401 &&
|
||||
/invalid|expired|token/i.test(detailText) &&
|
||||
localStorage.getItem("orcid_auth_token")
|
||||
) {
|
||||
localStorage.removeItem("orcid_auth_token");
|
||||
}
|
||||
|
||||
throw new ApiError(detailText, {
|
||||
status: response.status,
|
||||
payload,
|
||||
});
|
||||
|
||||
@@ -1,12 +1,45 @@
|
||||
/** Perfiles de exportación SWORD XML (query `profile` en el backend). */
|
||||
export const EXPORT_PROFILE_OPTIONS = [
|
||||
{ value: "generic", label: "Genérico (ORCID)" },
|
||||
{ value: "dublin_core", label: "Dublin Core" },
|
||||
{ value: "dspace", label: "DSpace" },
|
||||
{ value: "eprints", label: "EPrints" },
|
||||
{
|
||||
value: "generic",
|
||||
label: "Genérico (ORCID)",
|
||||
desc: "Metadatos ORCID en SWORD XML",
|
||||
},
|
||||
{
|
||||
value: "dublin_core",
|
||||
label: "Dublin Core",
|
||||
desc: "Esquema Dublin Core",
|
||||
},
|
||||
{
|
||||
value: "dspace",
|
||||
label: "DSpace",
|
||||
desc: "Compatible con repositorio DSpace",
|
||||
},
|
||||
{
|
||||
value: "eprints",
|
||||
label: "EPrints",
|
||||
desc: "Compatible con repositorio EPrints",
|
||||
},
|
||||
];
|
||||
|
||||
export const EXPORT_ZIP_DESTINATION = "zip";
|
||||
|
||||
export const ZIP_DESTINATION_OPTION = {
|
||||
value: EXPORT_ZIP_DESTINATION,
|
||||
label: "Paquete ZIP",
|
||||
desc: "Todos los formatos en un único paquete",
|
||||
};
|
||||
|
||||
export const DEFAULT_EXPORT_PROFILE = "generic";
|
||||
export const DEFAULT_EXPORT_DESTINATION = DEFAULT_EXPORT_PROFILE;
|
||||
|
||||
/** Convierte el valor del selector de destino en formato + perfil SWORD. */
|
||||
export function resolveExportFromDestination(destination) {
|
||||
if (destination === EXPORT_ZIP_DESTINATION) {
|
||||
return { format: "zip", profile: undefined };
|
||||
}
|
||||
return { format: "xml", profile: destination };
|
||||
}
|
||||
|
||||
export function swordXmlFilename(baseName, profile = DEFAULT_EXPORT_PROFILE) {
|
||||
const suffix =
|
||||
|
||||
Reference in New Issue
Block a user