From 8630055fccd75942e59d68a863abeb1e45649e3d Mon Sep 17 00:00:00 2001 From: Martin Ledvinka Date: Mon, 26 Aug 2024 10:30:26 +0200 Subject: [PATCH 1/3] [Enhancement #449] Split new vocabulary import into separate tabs for SKOS and Excel. --- src/component/misc/Tabs.tsx | 2 +- src/component/resource/file/UploadFile.tsx | 9 +- .../vocabulary/CreateVocabularyForm.tsx | 6 + .../importing/CreateVocabularyFromExcel.tsx | 138 ++++++++++++++++++ .../importing/CreateVocabularyFromSkos.tsx | 48 ++++++ .../importing/ImportVocabularyPage.tsx | 57 +++----- src/i18n/cs.ts | 7 +- src/i18n/en.ts | 5 +- 8 files changed, 225 insertions(+), 47 deletions(-) create mode 100644 src/component/vocabulary/importing/CreateVocabularyFromExcel.tsx create mode 100644 src/component/vocabulary/importing/CreateVocabularyFromSkos.tsx diff --git a/src/component/misc/Tabs.tsx b/src/component/misc/Tabs.tsx index 58fc4110b..d057345c2 100644 --- a/src/component/misc/Tabs.tsx +++ b/src/component/misc/Tabs.tsx @@ -11,7 +11,7 @@ interface TabsProps { /** * Map of IDs to the actual components */ - tabs: { [activeTabLabelKey: string]: JSX.Element }; + tabs: { [activeTabLabelKey: string]: React.JSX.Element }; /** * Map of IDs to the tab badge (no badge shown if the key is missing) */ diff --git a/src/component/resource/file/UploadFile.tsx b/src/component/resource/file/UploadFile.tsx index 42b448bf1..e13a2571e 100644 --- a/src/component/resource/file/UploadFile.tsx +++ b/src/component/resource/file/UploadFile.tsx @@ -45,10 +45,13 @@ function limitStringToBytes(limitStr: string) { interface UploadFileProps { setFile: (file: File) => void; + labelKey?: string; } -export const UploadFile: React.FC = (props) => { - const { setFile } = props; +export const UploadFile: React.FC = ({ + setFile, + labelKey = "resource.create.file.select.label", +}) => { const [currentFile, setCurrentFile] = React.useState(); const [dragActive, setDragActive] = React.useState(false); const { i18n, formatMessage } = useI18n(); @@ -82,7 +85,7 @@ export const UploadFile: React.FC = (props) => {
diff --git a/src/component/vocabulary/CreateVocabularyForm.tsx b/src/component/vocabulary/CreateVocabularyForm.tsx index 3c7852a60..f704f0248 100644 --- a/src/component/vocabulary/CreateVocabularyForm.tsx +++ b/src/component/vocabulary/CreateVocabularyForm.tsx @@ -31,6 +31,8 @@ interface CreateVocabularyFormProps { onCancel: () => void; language: string; selectLanguage: (lang: string) => void; + childrenBefore?: React.ReactNode; + childrenAfter?: React.ReactNode; } function generateIri( @@ -51,6 +53,8 @@ const CreateVocabularyForm: React.FC = ({ onCancel, language, selectLanguage, + childrenBefore, + childrenAfter, }) => { const { i18n, formatMessage } = useI18n(); const [iri, setIri] = useState(""); @@ -141,6 +145,7 @@ const CreateVocabularyForm: React.FC = ({ /> + {childrenBefore} @@ -203,6 +208,7 @@ const CreateVocabularyForm: React.FC = ({ />, ]} /> + {childrenAfter} diff --git a/src/component/vocabulary/importing/CreateVocabularyFromExcel.tsx b/src/component/vocabulary/importing/CreateVocabularyFromExcel.tsx new file mode 100644 index 000000000..4bdcdc0c2 --- /dev/null +++ b/src/component/vocabulary/importing/CreateVocabularyFromExcel.tsx @@ -0,0 +1,138 @@ +import React, { useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import TermItState from "../../../model/TermItState"; +import PromiseTrackingMask from "../../misc/PromiseTrackingMask"; +import CreateVocabularyForm from "../CreateVocabularyForm"; +import Routes from "../../../util/Routes"; +import Routing from "../../../util/Routing"; +import { ThunkDispatch } from "../../../util/Types"; +import Vocabulary from "../../../model/Vocabulary"; +import TermItFile from "../../../model/File"; +import { trackPromise } from "react-promise-tracker"; +import { + createFileInDocument, + createVocabulary, + uploadFileContent, +} from "../../../action/AsyncActions"; +import Utils from "../../../util/Utils"; +import VocabularyUtils from "../../../util/VocabularyUtils"; +import { publishNotification } from "../../../action/SyncActions"; +import NotificationType from "../../../model/NotificationType"; +import IdentifierResolver from "../../../util/IdentifierResolver"; +import { Col, Label, Row } from "reactstrap"; +import UploadFile from "../../resource/file/UploadFile"; +import { + downloadExcelTemplate, + importIntoExistingVocabulary, +} from "../../../action/AsyncImportActions"; +import { FormattedMessage } from "react-intl"; +import { useI18n } from "../../hook/useI18n"; + +const CreateVocabularyFromExcel: React.FC = () => { + const { i18n } = useI18n(); + const configuredLanguage = useSelector( + (state: TermItState) => state.configuration.language + ); + const [language, setLanguage] = React.useState(configuredLanguage); + const [file, setFile] = useState(); + const dispatch: ThunkDispatch = useDispatch(); + const downloadTemplate = () => { + dispatch(downloadExcelTemplate()); + }; + const onCreate = ( + vocabulary: Vocabulary, + files: TermItFile[], + fileContents: File[] + ) => { + trackPromise( + dispatch(createVocabulary(vocabulary)).then((location) => { + if (!location) { + return; + } + return Promise.all( + Utils.sanitizeArray(files).map((f, fIndex) => + dispatch( + createFileInDocument( + f, + VocabularyUtils.create(vocabulary.document!.iri) + ) + ) + .then(() => + dispatch( + uploadFileContent( + VocabularyUtils.create(f.iri), + fileContents[fIndex] + ) + ) + ) + .then(() => + dispatch( + publishNotification({ + source: { type: NotificationType.FILE_CONTENT_UPLOADED }, + }) + ) + ) + ) + ) + .then(() => { + if (file) { + return dispatch( + importIntoExistingVocabulary( + VocabularyUtils.create(vocabulary.iri), + file + ) + ); + } + return Promise.resolve({}); + }) + .then(() => + Routing.transitionTo( + Routes.vocabularySummary, + IdentifierResolver.routingOptionsFromLocation(location) + ) + ); + }), + "import-excel-vocabulary" + ); + }; + + return ( + <> + + Routing.transitionTo(Routes.vocabularies)} + language={language} + selectLanguage={setLanguage} + childrenBefore={ + + + + setFile(file)} /> + + + } + /> + + ); +}; + +export default CreateVocabularyFromExcel; diff --git a/src/component/vocabulary/importing/CreateVocabularyFromSkos.tsx b/src/component/vocabulary/importing/CreateVocabularyFromSkos.tsx new file mode 100644 index 000000000..c4956108c --- /dev/null +++ b/src/component/vocabulary/importing/CreateVocabularyFromSkos.tsx @@ -0,0 +1,48 @@ +import React from "react"; +import { Card, CardBody, Label } from "reactstrap"; +import { useI18n } from "src/component/hook/useI18n"; +import PromiseTrackingMask from "../../misc/PromiseTrackingMask"; +import ImportVocabularyDialog from "./ImportVocabularyDialog"; +import Routing from "../../../util/Routing"; +import Routes from "../../../util/Routes"; +import { ThunkDispatch } from "../../../util/Types"; +import { useDispatch } from "react-redux"; +import { trackPromise } from "react-promise-tracker"; +import { importSkosAsNewVocabulary } from "../../../action/AsyncImportActions"; +import IdentifierResolver from "../../../util/IdentifierResolver"; + +const CreateVocabularyFromSkos: React.FC = () => { + const { i18n } = useI18n(); + const dispatch: ThunkDispatch = useDispatch(); + const importSkos = (file: File, rename: Boolean) => + trackPromise( + dispatch(importSkosAsNewVocabulary(file, rename)), + "import-vocabulary" + ).then((location?: string) => { + if (location) { + Routing.transitionTo( + Routes.vocabularySummary, + IdentifierResolver.routingOptionsFromLocation(location) + ); + } + }); + + return ( + + + + + Routing.transitionTo(Routes.vocabularies)} + allowRename={true} + /> + + + ); +}; + +export default CreateVocabularyFromSkos; diff --git a/src/component/vocabulary/importing/ImportVocabularyPage.tsx b/src/component/vocabulary/importing/ImportVocabularyPage.tsx index 5484c5f23..9804e9f1d 100644 --- a/src/component/vocabulary/importing/ImportVocabularyPage.tsx +++ b/src/component/vocabulary/importing/ImportVocabularyPage.tsx @@ -1,51 +1,32 @@ +import React from "react"; import IfUserIsEditor from "../../authorization/IfUserIsEditor"; -import Routing from "../../../util/Routing"; -import Routes from "../../../util/Routes"; -import { useDispatch } from "react-redux"; -import { ThunkDispatch } from "../../../util/Types"; -import { importSkosAsNewVocabulary } from "../../../action/AsyncImportActions"; import { useI18n } from "../../hook/useI18n"; import HeaderWithActions from "../../misc/HeaderWithActions"; -import { Card, CardBody, Label } from "reactstrap"; -import IdentifierResolver from "../../../util/IdentifierResolver"; -import PromiseTrackingMask from "../../misc/PromiseTrackingMask"; -import { trackPromise } from "react-promise-tracker"; -import ImportVocabularyDialog from "./ImportVocabularyDialog"; +import Tabs from "../../misc/Tabs"; +import CreateVocabularyFromExcel from "./CreateVocabularyFromExcel"; +import CreateVocabularyFromSkos from "./CreateVocabularyFromSkos"; + +declare type ImportType = + | "vocabulary.import.type.skos" + | "vocabulary.import.type.excel"; const ImportVocabularyPage = () => { const { i18n } = useI18n(); - const dispatch: ThunkDispatch = useDispatch(); - const createFile = (file: File, rename: Boolean) => - trackPromise( - dispatch(importSkosAsNewVocabulary(file, rename)), - "import-vocabulary" - ).then((location?: string) => { - if (location) { - Routing.transitionTo( - Routes.vocabularySummary, - IdentifierResolver.routingOptionsFromLocation(location) - ); - } - }); - const onCancel = () => Routing.transitionTo(Routes.vocabularies); + const [activeTab, setActiveTab] = React.useState( + "vocabulary.import.type.skos" + ); return ( - - - - - - - + , + "vocabulary.import.type.excel": , + }} + changeTab={(k) => setActiveTab(k as ImportType)} + /> ); }; diff --git a/src/i18n/cs.ts b/src/i18n/cs.ts index 71f252b32..ce02195ce 100644 --- a/src/i18n/cs.ts +++ b/src/i18n/cs.ts @@ -320,9 +320,10 @@ const cs = { "Stáhnout šablonu pro MS Excel", "vocabulary.summary.import.nonEmpty.warning": "Slovník není prázdný, stávající data budou přepsána importovanými.", + "vocabulary.import.type.skos": "SKOS", + "vocabulary.import.type.excel": "MS Excel", "vocabulary.import.action": "Importovat", - "vocabulary.import.action.tooltip": "Import SKOS slovníku.", - "vocabulary.import.dialog.title": "Importovat SKOS slovník", + "vocabulary.import.dialog.title": "Importovat slovník", "vocabulary.import.dialog.message": "Importovaný soubor musí být formátu SKOS. " + "Soubor musí obsahovat jediný skos:ConceptScheme.", @@ -436,7 +437,7 @@ const cs = { "Přidat nový soubor do tohoto dokumentu", "resource.metadata.document.files.actions.add.dialog.title": "Nový soubor", "resource.metadata.document.files.empty": - "Žádné soubory nenalezeny. Vytvořte nějaký...", + "Žádné soubory nenalezeny. Přidejte nějaký...", "resource.file.vocabulary.create": "Přidat soubor", "term.language.selector.item": diff --git a/src/i18n/en.ts b/src/i18n/en.ts index d9124ad1b..7ef4f0ffe 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -312,9 +312,10 @@ const en = { "Download a MS Excel template", "vocabulary.summary.import.nonEmpty.warning": "Vocabulary is not empty, existing data will be overwritten by the imported.", + "vocabulary.import.type.skos": "SKOS", + "vocabulary.import.type.excel": "MS Excel", "vocabulary.import.action": "Import", - "vocabulary.import.action.tooltip": "SKOS vocabulary import.", - "vocabulary.import.dialog.title": "Import SKOS vocabulary", + "vocabulary.import.dialog.title": "Import vocabulary", "vocabulary.import.dialog.message": "Imported file must be in the SKOS format. " + "The file must contain exactly one instance of skos:ConceptScheme.", From b7d3faa165906ff6db0de1a399dd252f945a9fec Mon Sep 17 00:00:00 2001 From: Martin Ledvinka Date: Mon, 26 Aug 2024 10:46:55 +0200 Subject: [PATCH 2/3] [Enhancement #449] Adjust import vocabulary page style to better separate tabs. --- src/component/misc/Tabs.tsx | 19 +++++++++++++++++-- .../importing/ImportVocabularyPage.tsx | 1 + 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/component/misc/Tabs.tsx b/src/component/misc/Tabs.tsx index d057345c2..3e0aa2eb5 100644 --- a/src/component/misc/Tabs.tsx +++ b/src/component/misc/Tabs.tsx @@ -24,6 +24,14 @@ interface TabsProps { * Navigation link style. */ navLinkStyle?: string; + /** + * Classname for the Nav component + */ + navClassName?: string; + /** + * Classname for the TabContent component + */ + contentClassName?: string; } const Tabs: React.FC = (props) => { @@ -72,8 +80,15 @@ const Tabs: React.FC = (props) => { return (
- - {tabs} + + + {tabs} +
); }; diff --git a/src/component/vocabulary/importing/ImportVocabularyPage.tsx b/src/component/vocabulary/importing/ImportVocabularyPage.tsx index 9804e9f1d..cd698ac55 100644 --- a/src/component/vocabulary/importing/ImportVocabularyPage.tsx +++ b/src/component/vocabulary/importing/ImportVocabularyPage.tsx @@ -26,6 +26,7 @@ const ImportVocabularyPage = () => { "vocabulary.import.type.excel": , }} changeTab={(k) => setActiveTab(k as ImportType)} + contentClassName="pt-3" /> ); From 26d4dee7dc03354aec9f061e319a5a6cdd7506d3 Mon Sep 17 00:00:00 2001 From: Martin Ledvinka Date: Mon, 26 Aug 2024 10:58:44 +0200 Subject: [PATCH 3/3] [Enhancement #449] Add error messages for Excel import errors involving duplicate data. --- src/i18n/cs.ts | 4 ++++ src/i18n/en.ts | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/i18n/cs.ts b/src/i18n/cs.ts index ce02195ce..2846e46a4 100644 --- a/src/i18n/cs.ts +++ b/src/i18n/cs.ts @@ -792,6 +792,10 @@ const cs = { "Soubor nemohl být nahrán, protože jeho velikost přesahuje nastavený limit.", "error.term.state.terminal.liveChildren": "Pojmu nelze nastavit koncový stav, dokud má alespoň jednoho potomka v jiném než koncovém stavu.", + "error.vocabulary.import.excel.duplicateIdentifier": + "Excel obsahuje více pojmů se stejným identifikátorem.", + "error.vocabulary.import.excel.duplicateLabel": + "Excel obsahuje více pojmů se stejným názvem.", "history.label": "Historie změn", "history.loading": "Načítám historii...", diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 7ef4f0ffe..9c886173f 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -784,6 +784,10 @@ const en = { "The file could not be uploaded because it exceeds the configured maximum file size limit.", "error.term.state.terminal.liveChildren": "Cannot set term state to a terminal when it has at least one sub term in non-terminal state.", + "error.vocabulary.import.excel.duplicateIdentifier": + "The Excel file contains multiple terms with the same identifier.", + "error.vocabulary.import.excel.duplicateLabel": + "The Excel file contains multiple terms with the same label.", "history.label": "Change history", "history.loading": "Loading history...",