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:
@@ -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
|
||||
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"
|
||||
>
|
||||
{EXPORT_PROFILE_OPTIONS.map(({ value: optionValue, label }) => (
|
||||
<option key={optionValue} value={optionValue}>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<div className="relative">
|
||||
<button
|
||||
type="button"
|
||||
id={id}
|
||||
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"
|
||||
>
|
||||
<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}
|
||||
</div>
|
||||
<div className="text-xs text-ink-tertiary">{desc}</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</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";
|
||||
Reference in New Issue
Block a user