From d9f208cc3e32ffdb86a93d5ae11cb4091bf801a1 Mon Sep 17 00:00:00 2001 From: Martin Ledvinka Date: Mon, 2 Dec 2024 10:16:55 +0100 Subject: [PATCH] [Enhancement #581] Implement vocabulary translations import. UI is a part of the vocabulary content import dialog - using tabs to separate the two modes. --- src/action/AsyncImportActions.ts | 9 +- .../vocabulary/VocabularyActions.tsx | 6 +- .../vocabulary/VocabularySummary.tsx | 19 +-- .../importing/ImportBackupOfVocabulary.tsx | 86 ------------- .../importing/ImportTranslationsDialog.tsx | 64 ++++++++++ .../importing/ImportVocabularyDialog.tsx | 4 +- .../importing/LoadVocabularyFromFile.tsx | 116 ++++++++++++++++++ src/i18n/cs.ts | 14 ++- src/i18n/en.ts | 12 +- 9 files changed, 220 insertions(+), 110 deletions(-) delete mode 100644 src/component/vocabulary/importing/ImportBackupOfVocabulary.tsx create mode 100644 src/component/vocabulary/importing/ImportTranslationsDialog.tsx create mode 100644 src/component/vocabulary/importing/LoadVocabularyFromFile.tsx diff --git a/src/action/AsyncImportActions.ts b/src/action/AsyncImportActions.ts index 49f34831..ff798973 100644 --- a/src/action/AsyncImportActions.ts +++ b/src/action/AsyncImportActions.ts @@ -16,11 +16,18 @@ import { Action } from "redux"; import { loadVocabulary } from "./AsyncActions"; import Utils from "../util/Utils"; -export function importIntoExistingVocabulary(vocabularyIri: IRI, data: File) { +export function importIntoExistingVocabulary( + vocabularyIri: IRI, + data: File, + translationsOnly: boolean = false +) { const action = { type: ActionType.IMPORT_VOCABULARY }; const formData = new FormData(); formData.append("file", data, "thesaurus"); formData.append("namespace", vocabularyIri.namespace!); + if (translationsOnly) { + formData.append("translationsOnly", true.toString()); + } return (dispatch: ThunkDispatch) => { dispatch(asyncActionRequest(action, true)); return Ajax.post( diff --git a/src/component/vocabulary/VocabularyActions.tsx b/src/component/vocabulary/VocabularyActions.tsx index e4916d80..ae933930 100644 --- a/src/component/vocabulary/VocabularyActions.tsx +++ b/src/component/vocabulary/VocabularyActions.tsx @@ -12,7 +12,7 @@ import { GoCloudUpload, GoRepoForked, } from "react-icons/go"; -import ImportBackupOfVocabulary from "./importing/ImportBackupOfVocabulary"; +import LoadVocabularyFromFile from "./importing/LoadVocabularyFromFile"; import { FaCamera } from "react-icons/fa"; import Vocabulary from "../../model/Vocabulary"; import IfVocabularyActionAuthorized from "./authorization/IfVocabularyActionAuthorized"; @@ -28,7 +28,7 @@ interface VocabularyActionsProps { vocabulary: Vocabulary; onAnalyze: () => void; onExport: () => void; - onImport: (file: File, rename: Boolean) => Promise; + onImport: (file: File, translationsOnly: boolean) => Promise; onCreateSnapshot: () => void; } @@ -46,7 +46,7 @@ const VocabularyActions: React.FC = ({ return ( <> - setShowImportDialog(false)} diff --git a/src/component/vocabulary/VocabularySummary.tsx b/src/component/vocabulary/VocabularySummary.tsx index 25134161..7abeec36 100644 --- a/src/component/vocabulary/VocabularySummary.tsx +++ b/src/component/vocabulary/VocabularySummary.tsx @@ -71,7 +71,11 @@ interface VocabularySummaryProps ) => Promise; updateVocabulary: (vocabulary: Vocabulary) => Promise; removeVocabulary: (vocabulary: Vocabulary) => Promise; - importSkos: (iri: IRI, file: File) => Promise; + importSkos: ( + iri: IRI, + file: File, + translationsOnly?: boolean + ) => Promise; executeTextAnalysisOnAllTerms: (iri: IRI) => void; createSnapshot: (iri: IRI) => Promise; updateDocument: (document: Document) => Promise; @@ -210,16 +214,13 @@ export class VocabularySummary extends EditableComponent< this.setState({ showSnapshotDialog: !this.state.showSnapshotDialog }); }; - private onImport = (file: File) => + private onImport = (file: File, translationsOnly: boolean) => this.props.importSkos( VocabularyUtils.create(this.props.vocabulary.iri), - file + file, + translationsOnly ); - public onFileAdded = () => { - this.loadVocabulary(); - }; - private onExecuteTextAnalysisOnAllTerms = () => { this.props.executeTextAnalysisOnAllTerms( VocabularyUtils.create(this.props.vocabulary.iri) @@ -370,8 +371,8 @@ export default connect( dispatch(updateVocabulary(vocabulary)), removeVocabulary: (vocabulary: Vocabulary) => dispatch(removeVocabulary(vocabulary)), - importSkos: (iri: IRI, file: File) => - dispatch(importIntoExistingVocabulary(iri, file)), + importSkos: (iri: IRI, file: File, translationsOnly?: boolean) => + dispatch(importIntoExistingVocabulary(iri, file, translationsOnly)), executeTextAnalysisOnAllTerms: (iri: IRI) => dispatch(executeTextAnalysisOnAllTerms(iri)), createSnapshot: (iri: IRI) => dispatch(createVocabularySnapshot(iri)), diff --git a/src/component/vocabulary/importing/ImportBackupOfVocabulary.tsx b/src/component/vocabulary/importing/ImportBackupOfVocabulary.tsx deleted file mode 100644 index 93cc67c9..00000000 --- a/src/component/vocabulary/importing/ImportBackupOfVocabulary.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import React from "react"; -import { Alert, Label, Modal, ModalBody, ModalHeader } from "reactstrap"; -import { useI18n } from "../../hook/useI18n"; -import PromiseTrackingMask from "../../misc/PromiseTrackingMask"; -import { trackPromise } from "react-promise-tracker"; -import { FormattedMessage } from "react-intl"; -import ImportVocabularyDialog from "./ImportVocabularyDialog"; -import { useDispatch, useSelector } from "react-redux"; -import { ThunkDispatch } from "../../../util/Types"; -import { downloadExcelTemplate } from "../../../action/AsyncImportActions"; -import TermItState from "../../../model/TermItState"; - -interface ImportVocabularyProps { - showDialog: boolean; - onImport: (file: File, rename: Boolean) => Promise; - closeDialog: () => void; -} - -export const ImportVocabulary: React.FC = ({ - showDialog, - closeDialog, - onImport, -}) => { - const { i18n } = useI18n(); - const dispatch: ThunkDispatch = useDispatch(); - const onSubmit = (file: File, rename: Boolean) => - trackPromise(onImport(file, rename), "vocabulary-import").then(closeDialog); - const downloadTemplate = () => { - dispatch(downloadExcelTemplate()); - }; - const vocabularyNotEmpty = - (useSelector((state: TermItState) => state.vocabulary.termCount) || 0) > 0; - - return ( - <> - - - {i18n("vocabulary.summary.import.dialog.title")} - - - - -
    -
  • - -
  • -
  • - ( - - {chunks} - - ), - }} - /> -
  • -
- {vocabularyNotEmpty && ( - - - - )} - -
-
- - ); -}; - -export default ImportVocabulary; diff --git a/src/component/vocabulary/importing/ImportTranslationsDialog.tsx b/src/component/vocabulary/importing/ImportTranslationsDialog.tsx new file mode 100644 index 00000000..6ae4ddbc --- /dev/null +++ b/src/component/vocabulary/importing/ImportTranslationsDialog.tsx @@ -0,0 +1,64 @@ +import React from "react"; +import UploadFile from "../../resource/file/UploadFile"; +import { Button, ButtonToolbar, Col, Form, Row } from "reactstrap"; +import { useI18n } from "../../hook/useI18n"; +import { FormattedMessage } from "react-intl"; + +export const ImportTranslationsDialog: React.FC<{ + onSubmit: (file: File) => void; + onCancel: () => void; + onDownloadTemplate: () => void; +}> = ({ onSubmit, onCancel, onDownloadTemplate }) => { + const { i18n } = useI18n(); + const [file, setFile] = React.useState(); + const cannotSubmit = () => !file; + return ( + <> +
+
+ ( + + {chunks} + + ), + }} + /> +
+ + + + + + + + + + + + ); +}; diff --git a/src/component/vocabulary/importing/ImportVocabularyDialog.tsx b/src/component/vocabulary/importing/ImportVocabularyDialog.tsx index 4a4bf019..d28d1a86 100644 --- a/src/component/vocabulary/importing/ImportVocabularyDialog.tsx +++ b/src/component/vocabulary/importing/ImportVocabularyDialog.tsx @@ -7,7 +7,7 @@ import "./ImportVocabularyDialog.scss"; interface ImportVocabularyDialogProps { propKeyPrefix: string; - onCreate: (file: File, rename: Boolean) => any; + onCreate: (file: File, rename: boolean) => any; onCancel: () => void; allowRename?: boolean; } @@ -15,7 +15,7 @@ interface ImportVocabularyDialogProps { const ImportVocabularyDialog = (props: ImportVocabularyDialogProps) => { const { i18n } = useI18n(); const [file, setFile] = useState(); - const [rename, setRename] = useState(false); + const [rename, setRename] = useState(false); const onCreate = () => { if (!file) { diff --git a/src/component/vocabulary/importing/LoadVocabularyFromFile.tsx b/src/component/vocabulary/importing/LoadVocabularyFromFile.tsx new file mode 100644 index 00000000..87266deb --- /dev/null +++ b/src/component/vocabulary/importing/LoadVocabularyFromFile.tsx @@ -0,0 +1,116 @@ +import React, { useState } from "react"; +import { Alert, Modal, ModalBody, ModalHeader } from "reactstrap"; +import { useI18n } from "../../hook/useI18n"; +import PromiseTrackingMask from "../../misc/PromiseTrackingMask"; +import { trackPromise } from "react-promise-tracker"; +import { FormattedMessage } from "react-intl"; +import ImportVocabularyDialog from "./ImportVocabularyDialog"; +import { useDispatch, useSelector } from "react-redux"; +import { ThunkDispatch } from "../../../util/Types"; +import { downloadExcelTemplate } from "../../../action/AsyncImportActions"; +import TermItState from "../../../model/TermItState"; +import Tabs from "../../misc/Tabs"; +import { ImportTranslationsDialog } from "./ImportTranslationsDialog"; + +interface LoadVocabularyFromFileProps { + showDialog: boolean; + onImport: (file: File, translationsOnly: boolean) => Promise; + closeDialog: () => void; +} + +export const LoadVocabularyFromFile: React.FC = ({ + showDialog, + closeDialog, + onImport, +}) => { + const { i18n } = useI18n(); + const dispatch: ThunkDispatch = useDispatch(); + const [selectedTab, setSelectedTab] = useState( + "vocabulary.summary.import.dialog.tab.replaceContent" + ); + const onClose = () => { + closeDialog(); + setSelectedTab("vocabulary.summary.import.dialog.tab.replaceContent"); + }; + const onImportContent = (file: File) => + trackPromise(onImport(file, false), "vocabulary-import").then(onClose); + const onImportTranslations = (file: File) => { + trackPromise(onImport(file, true), "vocabulary-import").then(onClose); + }; + const downloadTemplate = () => { + dispatch(downloadExcelTemplate()); + }; + const vocabularyNotEmpty = + (useSelector((state: TermItState) => state.vocabulary.termCount) || 0) > 0; + + return ( + <> + + + {i18n("vocabulary.summary.import.dialog.title")} + + + + +
+ +
+
    +
  • + +
  • +
  • + ( + + {chunks} + + ), + }} + /> +
  • +
+ {vocabularyNotEmpty && ( + + + + )} + + + ), + "vocabulary.summary.import.dialog.tab.translations": ( + + ), + }} + changeTab={setSelectedTab as (t: string) => void} + /> +
+
+ + ); +}; + +export default LoadVocabularyFromFile; diff --git a/src/i18n/cs.ts b/src/i18n/cs.ts index 86d8c610..cb8a284a 100644 --- a/src/i18n/cs.ts +++ b/src/i18n/cs.ts @@ -305,21 +305,25 @@ const cs = { "vocabulary.summary.export.rdfxml.title": "Export ve formátu RDF/XML.", "vocabulary.summary.export.error": "Nepodařilo se získat data z odpovědi serveru.", - "vocabulary.summary.import.action": "Obnovit ze zálohy", + "vocabulary.summary.import.action": "Nahrát ze souboru", "vocabulary.summary.import.action.tooltip": - "Obnovit slovník ze zálohy ve formátu SKOS či MS Excel", + "Nahrát obsah slovníku ze souboru obsahujícího data ve formátu SKOS či MS Excel", "vocabulary.summary.import.dialog.title": - "Obnova exportované verze slovníku", + "Import obsahu slovníku ze souboru", + "vocabulary.summary.import.dialog.tab.replaceContent": "Nahradit obsah", + "vocabulary.summary.import.dialog.tab.translations": "Importovat překlady", "vocabulary.summary.import.dialog.label": - "Nahrajte vyexportovanou verzi tohoto slovníku ", + "Nahrajte vyexportovanou verzi tohoto slovníku", "vocabulary.summary.import.dialog.skosImport": "Ve formátu SKOS a obsahující jediný skos:ConceptScheme s IRI ve tvaru '<'IRI-TOHOTO-SLOVNÍKU'>'/glosář.", "vocabulary.summary.import.dialog.excelImport": - "MS Excel odpovídající této šabloně", + "Ve formátu MS Excel odpovídající této šabloně", "vocabulary.summary.import.excel.template.tooltip": "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.summary.import.translations.label": + "Nahrajte soubor ve formátu MS Excel odpovídající této šabloně, ze kterého mají být naimportovány překlady existujících pojmů ve slovníku.", "vocabulary.import.type.skos": "SKOS", "vocabulary.import.type.excel": "MS Excel", "vocabulary.import.action": "Importovat", diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 81bce900..84119adc 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -297,21 +297,25 @@ const en = { "vocabulary.summary.export.rdfxml.title": "Export to RDF/XML (RDF).", "vocabulary.summary.export.error": "Unable to retrieve exported data from server response.", - "vocabulary.summary.import.action": "Restore from backup", + "vocabulary.summary.import.action": "Load from file", "vocabulary.summary.import.action.tooltip": - "Restore the vocabulary from its previously exported version", + "Load vocabulary data from a file containing data in SKOS or MS Excel format", "vocabulary.summary.import.dialog.title": - "Restore exported vocabulary version", + "Import vocabulary content from file", + "vocabulary.summary.import.dialog.tab.replaceContent": "Replace content", + "vocabulary.summary.import.dialog.tab.translations": "Import translations", "vocabulary.summary.import.dialog.label": "Upload an exported version of this vocabulary", "vocabulary.summary.import.dialog.skosImport": "In the SKOS format and containing a single skos:ConceptScheme with IRI '<'IRI-OF-THIS-VOCABULARY'>'/glosář", "vocabulary.summary.import.dialog.excelImport": - "MS Excel file corresponding to this template", + "As an MS Excel file corresponding to this template", "vocabulary.summary.import.excel.template.tooltip": "Download a MS Excel template", "vocabulary.summary.import.nonEmpty.warning": "Vocabulary is not empty, existing data will be overwritten by the imported.", + "vocabulary.summary.import.translations.label": + "Upload an MS Excel file corresponding to this template from which translations of existing terms in this vocabulary will be imported.", "vocabulary.import.type.skos": "SKOS", "vocabulary.import.type.excel": "MS Excel", "vocabulary.import.action": "Import",