From 99d76cc056c4595b085628aa2d612edf45ef10b2 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Fri, 9 Jan 2026 15:04:54 -0500 Subject: [PATCH 01/34] feature: edit template and table --- public/locales/en/collection.json | 1 + public/locales/en/datasetTemplates.json | 23 ++ public/locales/es/collection.json | 1 + public/locales/es/datasetTemplates.json | 23 ++ src/router/routes.tsx | 17 ++ src/sections/Route.enum.ts | 2 + .../EditCollectionDropdown.tsx | 3 + .../DatasetTemplates.module.scss | 120 +++++++++ .../dataset-templates/DatasetTemplates.tsx | 255 ++++++++++++++++++ .../DatasetTemplatesFactory.tsx | 31 +++ 10 files changed, 476 insertions(+) create mode 100644 public/locales/en/datasetTemplates.json create mode 100644 public/locales/es/datasetTemplates.json create mode 100644 src/sections/dataset-templates/DatasetTemplates.module.scss create mode 100644 src/sections/dataset-templates/DatasetTemplates.tsx create mode 100644 src/sections/dataset-templates/DatasetTemplatesFactory.tsx diff --git a/public/locales/en/collection.json b/public/locales/en/collection.json index c6f1a3a88..db2c27b47 100644 --- a/public/locales/en/collection.json +++ b/public/locales/en/collection.json @@ -52,6 +52,7 @@ "editCollection": { "edit": "Edit", "generalInfo": "General Information", + "datasetTemplates": "Dataset Templates", "deleteCollection": "Delete Collection" }, "featuredItems": { diff --git a/public/locales/en/datasetTemplates.json b/public/locales/en/datasetTemplates.json new file mode 100644 index 000000000..849679468 --- /dev/null +++ b/public/locales/en/datasetTemplates.json @@ -0,0 +1,23 @@ +{ + "pageTitle": "Dataset Templates", + "infoAlert": { + "title": "Manage Dataset Templates", + "text": "Create a template prefilled with metadata fields standard values, such as Author Affiliation, or add instructions in the metadata fields to give depositors more information on what metadata is expected." + }, + "actions": { + "create": "Create Dataset Template", + "makeDefault": "Make Default", + "default": "Default", + "view": "View", + "copy": "Copy", + "edit": "Edit Template", + "delete": "Delete" + }, + "table": { + "name": "Template Name", + "created": "Date Created", + "usage": "Usage", + "action": "Action" + }, + "emptyState": "This collection has no dataset templates yet." +} diff --git a/public/locales/es/collection.json b/public/locales/es/collection.json index a7b718fa1..e0a963c09 100644 --- a/public/locales/es/collection.json +++ b/public/locales/es/collection.json @@ -52,6 +52,7 @@ "editCollection": { "edit": "Editar", "generalInfo": "Información general", + "datasetTemplates": "Plantillas de conjuntos de datos", "deleteCollection": "Eliminar colección" }, "featuredItems": { diff --git a/public/locales/es/datasetTemplates.json b/public/locales/es/datasetTemplates.json new file mode 100644 index 000000000..4a4ce32bd --- /dev/null +++ b/public/locales/es/datasetTemplates.json @@ -0,0 +1,23 @@ +{ + "pageTitle": "Plantillas de conjuntos de datos", + "infoAlert": { + "title": "Gestionar plantillas de conjuntos de datos", + "text": "Cree una plantilla precargada con valores estándar de campos de metadatos, como Afiliación del autor, o agregue instrucciones en los campos de metadatos para dar a los depositantes más información sobre qué metadatos se esperan." + }, + "actions": { + "create": "Crear plantilla de conjunto de datos", + "makeDefault": "Hacer predeterminada", + "default": "Predeterminada", + "view": "Ver", + "copy": "Copiar", + "edit": "Editar plantilla", + "delete": "Eliminar" + }, + "table": { + "name": "Nombre de la plantilla", + "created": "Fecha de creación", + "usage": "Uso", + "action": "Acción" + }, + "emptyState": "Esta colección aún no tiene plantillas de conjuntos de datos." +} diff --git a/src/router/routes.tsx b/src/router/routes.tsx index 7331ef96f..b4da12c0f 100644 --- a/src/router/routes.tsx +++ b/src/router/routes.tsx @@ -85,6 +85,14 @@ const EditFeaturedItems = lazy(() => ) ) +const DatasetTemplatesPage = lazy(() => + import('../sections/dataset-templates/DatasetTemplatesFactory').then( + ({ DatasetTemplatesFactory }) => ({ + default: () => DatasetTemplatesFactory.create() + }) + ) +) + const ReplaceFile = lazy(() => import('../sections/replace-file/ReplaceFileFactory').then(({ ReplaceFileFactory }) => ({ default: () => ReplaceFileFactory.create() @@ -275,6 +283,15 @@ export const routes: RouteObject[] = [ ), errorElement: }, + { + path: Route.COLLECTION_TEMPLATES, + element: ( + }> + + + ), + errorElement: + }, { path: Route.EDIT_FILE_METADATA, element: ( diff --git a/src/sections/Route.enum.ts b/src/sections/Route.enum.ts index d7d64fb7a..68570f9ba 100644 --- a/src/sections/Route.enum.ts +++ b/src/sections/Route.enum.ts @@ -19,6 +19,7 @@ export enum Route { ACCOUNT = '/account', EDIT_COLLECTION = '/collections/:collectionId/edit', EDIT_FEATURED_ITEMS = '/collections/:collectionId/edit-featured-items', + COLLECTION_TEMPLATES = '/:collectionId/templates', FEATURED_ITEM = '/featured-item/:parentCollectionId/:featuredItemId', NOT_FOUND_PAGE = '/404', AUTH_CALLBACK = '/auth-callback', @@ -33,6 +34,7 @@ export const RouteWithParams = { CREATE_DATASET: (collectionId: string) => `/datasets/${collectionId}/create`, EDIT_COLLECTION: (collectionId: string) => `/collections/${collectionId}/edit`, EDIT_FEATURED_ITEMS: (collectionId: string) => `/collections/${collectionId}/edit-featured-items`, + COLLECTION_TEMPLATES: (collectionId: string) => `/${collectionId}/templates`, EDIT_FILE_METADATA: ( datasetPersistentId: string, datasetVersion: string, diff --git a/src/sections/collection/edit-collection-dropdown/EditCollectionDropdown.tsx b/src/sections/collection/edit-collection-dropdown/EditCollectionDropdown.tsx index eace09e12..d185bd29d 100644 --- a/src/sections/collection/edit-collection-dropdown/EditCollectionDropdown.tsx +++ b/src/sections/collection/edit-collection-dropdown/EditCollectionDropdown.tsx @@ -60,6 +60,9 @@ export const EditCollectionDropdown = ({ {t('featuredItems.title')} + + {t('editCollection.datasetTemplates')} + {canCollectionBeDeleted && ( <> diff --git a/src/sections/dataset-templates/DatasetTemplates.module.scss b/src/sections/dataset-templates/DatasetTemplates.module.scss new file mode 100644 index 000000000..535b3cfce --- /dev/null +++ b/src/sections/dataset-templates/DatasetTemplates.module.scss @@ -0,0 +1,120 @@ +@import 'node_modules/bootstrap/scss/functions'; +@import 'node_modules/bootstrap/scss/variables'; +@import 'node_modules/@iqss/dataverse-design-system/src/lib/assets/styles/design-tokens/colors.module'; + +.header { + margin-bottom: $spacer; +} + +.header-title { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 0.5rem; +} + +.subtext { + color: $dv-subtext-color; +} + +.table-actions { + display: flex; + justify-content: flex-end; + margin: $spacer 0; +} + +.th, + td { + text-align: center; + } + +.create-button { + display: inline-flex; + align-items: center; + gap: 0.5rem; +} + +.button-icon { + font-size: 1.1rem; +} + +.action-group { + flex-wrap: wrap; + gap: 0; +} + +.action-icon { + display: inline-flex; +} + +.action-label { + margin-left: 0.4rem; +} + +.action-cell { + text-align: center; +} + +.make-default-button { + margin-right: 0.75rem; +} + +.name-column { + width: 32%; +} + +.action-column { + width: 32%; +} + +.sort-button { + padding: 0; + text-decoration: none; + color: inherit; + background: none; + border: none; + display: inline-flex; + align-items: center; + gap: 0.35rem; + font-size: inherit; + font-weight: inherit; +} + +.sort-button:focus, +.sort-button:active, +.sort-button:focus-visible { + color: inherit; + box-shadow: none; + opacity: 1; +} + +.sort-button:hover { + color: inherit; +} + +.sort-button-active { + color: $dv-subtext-color; +} + +.sort-header-active { + background-color: rgba($dv-subtext-color, 0.08); + color: $dv-subtext-color; +} + +.sort-indicator { + font-size: 0.75rem; +} + +.sort-icon { + display: inline-flex; + vertical-align: middle; +} + +.default-badge { + margin-left: 0.5rem; +} + +.empty-state { + text-align: center; + color: $dv-subtext-color; +} diff --git a/src/sections/dataset-templates/DatasetTemplates.tsx b/src/sections/dataset-templates/DatasetTemplates.tsx new file mode 100644 index 000000000..86a238af7 --- /dev/null +++ b/src/sections/dataset-templates/DatasetTemplates.tsx @@ -0,0 +1,255 @@ +import { useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { Alert, Button, ButtonGroup, Table, Tooltip } from '@iqss/dataverse-design-system' +import { + CaretDown, + CaretUp, + ChevronExpand, + Eye, + Files, + Pencil, + PlusLg, + Trash +} from 'react-bootstrap-icons' +import { CollectionRepository } from '@/collection/domain/repositories/CollectionRepository' +import { TemplateRepository } from '@/templates/domain/repositories/TemplateRepository' +import { useCollection } from '../collection/useCollection' +import { useGetDatasetTemplates } from '@/dataset/domain/hooks/useGetDatasetTemplates' +import { BreadcrumbsGenerator } from '../shared/hierarchy/BreadcrumbsGenerator' +import { NotFoundPage } from '../not-found-page/NotFoundPage' +import { AppLoader } from '../shared/layout/app-loader/AppLoader' +import { NotImplementedModal } from '../not-implemented/NotImplementedModal' +import { useNotImplementedModal } from '../not-implemented/NotImplementedModalContext' +import { Template } from '@/dataset/domain/models/DatasetTemplate' +import styles from './DatasetTemplates.module.scss' + +interface DatasetTemplatesProps { + collectionRepository: CollectionRepository + templateRepository: TemplateRepository + collectionIdFromParams: string | undefined +} + +export const DatasetTemplates = ({ + collectionRepository, + templateRepository, + collectionIdFromParams +}: DatasetTemplatesProps) => { + const { t } = useTranslation('datasetTemplates') + const { isModalOpen, hideModal, showModal } = useNotImplementedModal() + const [sortBy, setSortBy] = useState<'name' | 'created' | 'usage' | null>(null) + const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc') + const { collection, isLoading: isLoadingCollection } = useCollection( + collectionRepository, + collectionIdFromParams + ) + const { datasetTemplates, isLoadingDatasetTemplates, errorGetDatasetTemplates } = + useGetDatasetTemplates({ + templateRepository, + collectionIdOrAlias: collectionIdFromParams ?? '', + autoFetch: Boolean(collectionIdFromParams) + }) + + const isLoadingData = isLoadingCollection || isLoadingDatasetTemplates + const formatCreateDate = (template: Template) => template.createDate || template.createTime || '' + const resolveCreateDate = (template: Template) => { + const value = formatCreateDate(template) + const time = Date.parse(value) + return Number.isNaN(time) ? 0 : time + } + + const sortedTemplates = useMemo(() => { + if (!sortBy) { + return datasetTemplates + } + const sorted = [...datasetTemplates] + sorted.sort((first, second) => { + if (sortBy === 'name') { + return first.name.localeCompare(second.name, undefined, { sensitivity: 'base' }) + } + if (sortBy === 'created') { + return resolveCreateDate(first) - resolveCreateDate(second) + } + return first.usageCount - second.usageCount + }) + return sortDirection === 'asc' ? sorted : sorted.reverse() + }, [datasetTemplates, sortBy, sortDirection]) + + const handleSort = (column: 'name' | 'created' | 'usage') => { + if (sortBy === column) { + setSortDirection((current) => (current === 'asc' ? 'desc' : 'asc')) + return + } + setSortBy(column) + setSortDirection('asc') + } + + const sortIndicator = (column: 'name' | 'created' | 'usage') => { + if (sortBy === column) { + return sortDirection === 'asc' ? ( + + ) : ( + + ) + } + return + } + const sortButtonClass = (column: 'name' | 'created' | 'usage') => + `${styles['sort-button']}${sortBy === column ? ` ${styles['sort-button-active']}` : ''}` + const sortHeaderClass = (column: 'name' | 'created' | 'usage') => + sortBy === column ? styles['sort-header-active'] : '' + + if (!isLoadingCollection && !collection) { + return + } + + if (isLoadingData || !collection) { + return + } + + if (errorGetDatasetTemplates) { + return {errorGetDatasetTemplates} + } + + return ( + <> + +
+ +
+
+

{collection.name}

+ {collection.affiliation ? ( + ({collection.affiliation}) + ) : null} +
+
+ + + {t('infoAlert.text')} + + +
+ +
+ + + + + + + + + + + + {datasetTemplates.length === 0 ? ( + + + + ) : ( + sortedTemplates.map((template) => ( + + + + + + + )) + )} + +
+ + + + + + + {t('table.action')} +
+ {t('emptyState')} +
{template.name}{formatCreateDate(template)}{template.usageCount} + + {template.isDefault ? ( + + ) : ( + + )} + + + + + + + + + + + + + +
+
+ + ) +} diff --git a/src/sections/dataset-templates/DatasetTemplatesFactory.tsx b/src/sections/dataset-templates/DatasetTemplatesFactory.tsx new file mode 100644 index 000000000..e089f9cac --- /dev/null +++ b/src/sections/dataset-templates/DatasetTemplatesFactory.tsx @@ -0,0 +1,31 @@ +import { ReactElement } from 'react' +import { useParams } from 'react-router-dom' +import { CollectionJSDataverseRepository } from '@/collection/infrastructure/repositories/CollectionJSDataverseRepository' +import { TemplateJSDataverseRepository } from '@/templates/infrastructure/repositories/TemplateJSDataverseRepository' +import { NotImplementedModalProvider } from '../not-implemented/NotImplementedModalProvider' +import { DatasetTemplates } from './DatasetTemplates' + +const collectionRepository = new CollectionJSDataverseRepository() +const templateRepository = new TemplateJSDataverseRepository() + +export class DatasetTemplatesFactory { + static create(): ReactElement { + return ( + + + + ) + } +} + +function DatasetTemplatesWithParams() { + const { collectionId } = useParams<{ collectionId: string }>() + + return ( + + ) +} From 4e9cfbf568f5c8a94727bcde2a518ee95bcfa8ac Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Fri, 9 Jan 2026 16:49:56 -0500 Subject: [PATCH 02/34] feature: delete and view a template --- public/locales/en/datasetTemplates.json | 21 ++- public/locales/es/datasetTemplates.json | 21 ++- .../DatasetTemplates.module.scss | 6 +- .../dataset-templates/DatasetTemplates.tsx | 83 ++++++++- .../DatasetTemplatesFactory.tsx | 3 + .../ConfirmDeleteTemplateModal.module.scss | 7 + .../ConfirmDeleteTemplateModal.tsx | 53 ++++++ .../DatasetTemplatePreviewModal.module.scss | 30 ++++ .../DatasetTemplatePreviewModal.tsx | 170 ++++++++++++++++++ 9 files changed, 381 insertions(+), 13 deletions(-) create mode 100644 src/sections/dataset-templates/confirm-delete-template-modal/ConfirmDeleteTemplateModal.module.scss create mode 100644 src/sections/dataset-templates/confirm-delete-template-modal/ConfirmDeleteTemplateModal.tsx create mode 100644 src/sections/dataset-templates/dataset-template-preview-modal/DatasetTemplatePreviewModal.module.scss create mode 100644 src/sections/dataset-templates/dataset-template-preview-modal/DatasetTemplatePreviewModal.tsx diff --git a/public/locales/en/datasetTemplates.json b/public/locales/en/datasetTemplates.json index 849679468..0dc77a4da 100644 --- a/public/locales/en/datasetTemplates.json +++ b/public/locales/en/datasetTemplates.json @@ -19,5 +19,24 @@ "usage": "Usage", "action": "Action" }, - "emptyState": "This collection has no dataset templates yet." + "emptyState": "This collection has no dataset templates yet.", + "deleteModal": { + "title": "Delete Template", + "message": "Are you sure you want to delete this template? A new dataset will not be able to use this template." + }, + "alerts": { + "deleteSuccess": "Template deleted.", + "deleteError": "Something went wrong deleting the template. Try again later." + }, + "preview": { + "title": "Dataset Template Preview", + "templateLabel": "Dataset Template", + "sections": { + "metadata": "Citation Metadata", + "terms": "Dataset Terms", + "access": "Restricted Files + Terms of Access" + }, + "noMetadata": "No citation metadata is available for this template.", + "error": "Unable to load the template preview. Try again later." + } } diff --git a/public/locales/es/datasetTemplates.json b/public/locales/es/datasetTemplates.json index 4a4ce32bd..b013d9a7f 100644 --- a/public/locales/es/datasetTemplates.json +++ b/public/locales/es/datasetTemplates.json @@ -19,5 +19,24 @@ "usage": "Uso", "action": "Acción" }, - "emptyState": "Esta colección aún no tiene plantillas de conjuntos de datos." + "emptyState": "Esta colección aún no tiene plantillas de conjuntos de datos.", + "deleteModal": { + "title": "Eliminar plantilla", + "message": "¿Está seguro de que desea eliminar esta plantilla? Un nuevo conjunto de datos no podrá usar esta plantilla." + }, + "alerts": { + "deleteSuccess": "Plantilla eliminada.", + "deleteError": "Algo salió mal al eliminar la plantilla. Inténtelo de nuevo más tarde." + }, + "preview": { + "title": "Vista previa de plantilla de conjunto de datos", + "templateLabel": "Plantilla de conjunto de datos", + "sections": { + "metadata": "Metadatos de citación", + "terms": "Términos del conjunto de datos", + "access": "Archivos restringidos + términos de acceso" + }, + "noMetadata": "No hay metadatos de citación disponibles para esta plantilla.", + "error": "No se pudo cargar la vista previa de la plantilla. Inténtelo de nuevo más tarde." + } } diff --git a/src/sections/dataset-templates/DatasetTemplates.module.scss b/src/sections/dataset-templates/DatasetTemplates.module.scss index 535b3cfce..f67a3a34d 100644 --- a/src/sections/dataset-templates/DatasetTemplates.module.scss +++ b/src/sections/dataset-templates/DatasetTemplates.module.scss @@ -24,9 +24,9 @@ } .th, - td { - text-align: center; - } +td { + text-align: center; +} .create-button { display: inline-flex; diff --git a/src/sections/dataset-templates/DatasetTemplates.tsx b/src/sections/dataset-templates/DatasetTemplates.tsx index 86a238af7..c9ec55b12 100644 --- a/src/sections/dataset-templates/DatasetTemplates.tsx +++ b/src/sections/dataset-templates/DatasetTemplates.tsx @@ -5,13 +5,16 @@ import { CaretDown, CaretUp, ChevronExpand, + CheckLg, Eye, Files, Pencil, PlusLg, Trash } from 'react-bootstrap-icons' +import { toast } from 'react-toastify' import { CollectionRepository } from '@/collection/domain/repositories/CollectionRepository' +import { MetadataBlockInfoRepository } from '@/metadata-block-info/domain/repositories/MetadataBlockInfoRepository' import { TemplateRepository } from '@/templates/domain/repositories/TemplateRepository' import { useCollection } from '../collection/useCollection' import { useGetDatasetTemplates } from '@/dataset/domain/hooks/useGetDatasetTemplates' @@ -21,33 +24,45 @@ import { AppLoader } from '../shared/layout/app-loader/AppLoader' import { NotImplementedModal } from '../not-implemented/NotImplementedModal' import { useNotImplementedModal } from '../not-implemented/NotImplementedModalContext' import { Template } from '@/dataset/domain/models/DatasetTemplate' +import { ConfirmDeleteTemplateModal } from './confirm-delete-template-modal/ConfirmDeleteTemplateModal' +import { DatasetTemplatePreviewModal } from './dataset-template-preview-modal/DatasetTemplatePreviewModal' import styles from './DatasetTemplates.module.scss' interface DatasetTemplatesProps { collectionRepository: CollectionRepository templateRepository: TemplateRepository + metadataBlockInfoRepository: MetadataBlockInfoRepository collectionIdFromParams: string | undefined } export const DatasetTemplates = ({ collectionRepository, templateRepository, + metadataBlockInfoRepository, collectionIdFromParams }: DatasetTemplatesProps) => { const { t } = useTranslation('datasetTemplates') const { isModalOpen, hideModal, showModal } = useNotImplementedModal() const [sortBy, setSortBy] = useState<'name' | 'created' | 'usage' | null>(null) const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc') + const [templateToDelete, setTemplateToDelete] = useState