From be6d2f8937d2cdf97661bbeb8ec069074d221f50 Mon Sep 17 00:00:00 2001 From: Martin Ledvinka Date: Tue, 8 Oct 2024 14:50:57 +0200 Subject: [PATCH 1/3] [Enhancement #528] Support opening a modeling tool from vocabulary detail. All relevant vocabularies are used together with the current vocabulary. --- .../vocabulary/VocabularyActions.tsx | 36 ++++++++++- .../modeling/OpenModelingToolDialog.tsx | 60 +++++++++++++++++++ src/i18n/cs.ts | 6 ++ src/i18n/en.ts | 6 ++ src/model/Configuration.ts | 2 + src/util/Utils.ts | 4 ++ 6 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 src/component/vocabulary/modeling/OpenModelingToolDialog.tsx diff --git a/src/component/vocabulary/VocabularyActions.tsx b/src/component/vocabulary/VocabularyActions.tsx index 368f4cc33..e4916d800 100644 --- a/src/component/vocabulary/VocabularyActions.tsx +++ b/src/component/vocabulary/VocabularyActions.tsx @@ -6,13 +6,23 @@ import { DropdownToggle, UncontrolledButtonDropdown, } from "reactstrap"; -import { GoClippy, GoCloudDownload, GoCloudUpload } from "react-icons/go"; +import { + GoClippy, + GoCloudDownload, + GoCloudUpload, + GoRepoForked, +} from "react-icons/go"; import ImportBackupOfVocabulary from "./importing/ImportBackupOfVocabulary"; import { FaCamera } from "react-icons/fa"; import Vocabulary from "../../model/Vocabulary"; import IfVocabularyActionAuthorized from "./authorization/IfVocabularyActionAuthorized"; import AccessLevel from "../../model/acl/AccessLevel"; import IfUserIsAdmin from "../authorization/IfUserIsAdmin"; +import If from "../misc/If"; +import { useSelector } from "react-redux"; +import TermItState from "../../model/TermItState"; +import Utils from "../../util/Utils"; +import OpenModelingToolDialog from "./modeling/OpenModelingToolDialog"; interface VocabularyActionsProps { vocabulary: Vocabulary; @@ -31,6 +41,8 @@ const VocabularyActions: React.FC = ({ }) => { const { i18n } = useI18n(); const [showImportDialog, setShowImportDialog] = React.useState(false); + const [showModelingDialog, setShowModelingDialog] = React.useState(false); + const config = useSelector((state: TermItState) => state.configuration); return ( <> @@ -39,6 +51,11 @@ const VocabularyActions: React.FC = ({ showDialog={showImportDialog} closeDialog={() => setShowImportDialog(false)} /> + setShowModelingDialog(false)} + vocabulary={vocabulary} + /> = ({ {i18n("vocabulary.snapshot.create.label")} + + + setShowModelingDialog(true)} + > + + {i18n("vocabulary.summary.model.label")} + + + diff --git a/src/component/vocabulary/modeling/OpenModelingToolDialog.tsx b/src/component/vocabulary/modeling/OpenModelingToolDialog.tsx new file mode 100644 index 000000000..74b1dc14e --- /dev/null +++ b/src/component/vocabulary/modeling/OpenModelingToolDialog.tsx @@ -0,0 +1,60 @@ +import React from "react"; +import Vocabulary from "../../../model/Vocabulary"; +import { useI18n } from "../../hook/useI18n"; +import ConfirmCancelDialog from "../../misc/ConfirmCancelDialog"; +import { getLocalized } from "../../../model/MultilingualString"; +import { ThunkDispatch } from "../../../util/Types"; +import { useDispatch, useSelector } from "react-redux"; +import { loadRelatedVocabularies } from "../../../action/AsyncVocabularyActions"; +import VocabularyUtils from "../../../util/VocabularyUtils"; +import TermItState from "../../../model/TermItState"; + +interface OpenModelingToolDialogProps { + open: boolean; + onClose: () => void; + vocabulary: Vocabulary; +} + +const OpenModelingToolDialog: React.FC = ({ + open, + onClose, + vocabulary, +}) => { + const { formatMessage } = useI18n(); + const dispatch: ThunkDispatch = useDispatch(); + const [relatedVocabularies, setRelatedVocabularies] = React.useState< + string[] + >([]); + const modelingToolUrl = useSelector( + (state: TermItState) => state.configuration + ).modelingToolUrl; + React.useEffect(() => { + if (open) { + dispatch( + loadRelatedVocabularies(VocabularyUtils.create(vocabulary.iri)) + ).then((data) => setRelatedVocabularies(data)); + } + }, [open, vocabulary.iri, dispatch]); + const onOpen = () => { + const vocabularyList = [vocabulary.iri, ...relatedVocabularies]; + let params = vocabularyList + .map((v) => encodeURIComponent(v)) + .join("&vocabulary="); + window.location.href = modelingToolUrl + "?vocabulary=" + params; + }; + + return ( + + ); +}; + +export default OpenModelingToolDialog; diff --git a/src/i18n/cs.ts b/src/i18n/cs.ts index a99a857a4..21db8a5ec 100644 --- a/src/i18n/cs.ts +++ b/src/i18n/cs.ts @@ -335,6 +335,12 @@ const cs = { "Při zaškrtnutí tohoto políčka budou při importu identifikátory nahrazeny novými, pokud by kolidovaly s existujícími identifikátory.", "vocabulary.summary.startTextAnalysis.title": "Spustit textovou analýzu definic všech pojmů v tomto slovníku", + "vocabulary.summary.model.label": "Modelovat vztahy", + "vocabulary.summary.model.title": + "Modelovat vztahy mezi pojmy ve slovníku pomocí externího nástroje", + "vocabulary.summary.model.open": "Otevřít", + "vocabulary.summary.model.dialog.title": + "Modelovat vztahy pojmů slovníku {vocabulary}", "vocabulary.updated.message": "Slovník úspěšně uložen.", "vocabulary.created.message": "Slovník úspěšně vytvořen.", "vocabulary.detail.subtitle": "Vytvořen autorem {author} ", diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 00c1b1a3b..a4c8dd643 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -327,6 +327,12 @@ const en = { "When ticked, identifiers colliding with existing ones will be replaced by new ones.", "vocabulary.summary.startTextAnalysis.title": "Start text analysis on definitions of all terms in this vocabulary", + "vocabulary.summary.model.label": "Model relationships", + "vocabulary.summary.model.title": + "Model relationships between terms in this vocabulary using an external tool", + "vocabulary.summary.model.open": "Open", + "vocabulary.summary.model.dialog.title": + "Model relationships of terms in {vocabulary}", "vocabulary.updated.message": "Vocabulary successfully updated.", "vocabulary.created.message": "Vocabulary successfully created.", "vocabulary.detail.subtitle": "Created by {author} on ", diff --git a/src/model/Configuration.ts b/src/model/Configuration.ts index 771690468..72459dd01 100644 --- a/src/model/Configuration.ts +++ b/src/model/Configuration.ts @@ -8,6 +8,7 @@ const ctx = { roles: `${VocabularyUtils.NS_TERMIT}má-uživatelskou-roli`, maxFileUploadSize: `${VocabularyUtils.NS_TERMIT}má-maximální-velikost-souboru`, versionSeparator: `${VocabularyUtils.NS_TERMIT}má-oddělovač-verze`, + modelingToolUrl: `${VocabularyUtils.NS_TERMIT}má-adresu-modelovacího-nástroje`, }; export const CONTEXT = Object.assign({}, USERROLE_CONTEXT, ctx); @@ -21,6 +22,7 @@ export interface Configuration { roles: UserRole[]; maxFileUploadSize: string; versionSeparator: string; + modelingToolUrl?: string; } export const DEFAULT_CONFIGURATION: Configuration = { diff --git a/src/util/Utils.ts b/src/util/Utils.ts index 4d89ec4cc..45b54bebe 100644 --- a/src/util/Utils.ts +++ b/src/util/Utils.ts @@ -363,6 +363,10 @@ const Utils = { } return true; }, + + notBlank(str?: string | null) { + return !!(str && str.trim().length > 0); + }, }; export default Utils; From 734079a25b57c4018a04d25f72d1c19678578b41 Mon Sep 17 00:00:00 2001 From: Martin Ledvinka Date: Tue, 8 Oct 2024 17:45:40 +0200 Subject: [PATCH 2/3] [Enhancement #528] Allow user to select additional vocabularies to use to open for modeling --- src/component/misc/ConfirmCancelDialog.tsx | 4 +- .../modeling/OpenModelingToolDialog.scss | 9 +++ .../modeling/OpenModelingToolDialog.tsx | 74 +++++++++++++++++-- src/i18n/cs.ts | 2 + src/i18n/en.ts | 2 + 5 files changed, 84 insertions(+), 7 deletions(-) create mode 100644 src/component/vocabulary/modeling/OpenModelingToolDialog.scss diff --git a/src/component/misc/ConfirmCancelDialog.tsx b/src/component/misc/ConfirmCancelDialog.tsx index 64923aa9a..547323196 100644 --- a/src/component/misc/ConfirmCancelDialog.tsx +++ b/src/component/misc/ConfirmCancelDialog.tsx @@ -22,7 +22,8 @@ interface ConfirmCancelDialogProps { confirmDisabled?: boolean; cancelKey?: string; cancelColor?: string; - size?: string; + size?: "lg" | "sm"; + className?: string; } /** @@ -37,6 +38,7 @@ const ConfirmCancelDialog: React.FC = (props) => { isOpen={props.show} toggle={props.onClose} size={props.size} + className={props.className} > {props.title} {props.children} diff --git a/src/component/vocabulary/modeling/OpenModelingToolDialog.scss b/src/component/vocabulary/modeling/OpenModelingToolDialog.scss new file mode 100644 index 000000000..4735ad6f2 --- /dev/null +++ b/src/component/vocabulary/modeling/OpenModelingToolDialog.scss @@ -0,0 +1,9 @@ +.modeling-vocabulary-select-dialog { + max-width: 80%; +} + +input.vocabulary-checkbox { + position: relative; + margin-left: 0; + margin-right: 0.5rem; +} diff --git a/src/component/vocabulary/modeling/OpenModelingToolDialog.tsx b/src/component/vocabulary/modeling/OpenModelingToolDialog.tsx index 74b1dc14e..cd7ffee45 100644 --- a/src/component/vocabulary/modeling/OpenModelingToolDialog.tsx +++ b/src/component/vocabulary/modeling/OpenModelingToolDialog.tsx @@ -8,6 +8,12 @@ import { useDispatch, useSelector } from "react-redux"; import { loadRelatedVocabularies } from "../../../action/AsyncVocabularyActions"; import VocabularyUtils from "../../../util/VocabularyUtils"; import TermItState from "../../../model/TermItState"; +import { loadVocabularies } from "../../../action/AsyncActions"; +import { Col, Input, Row } from "reactstrap"; +import { getShortLocale } from "../../../util/IntlUtil"; +import "./OpenModelingToolDialog.scss"; + +const COLUMN_COUNT = 3; interface OpenModelingToolDialogProps { open: boolean; @@ -20,29 +26,80 @@ const OpenModelingToolDialog: React.FC = ({ onClose, vocabulary, }) => { - const { formatMessage } = useI18n(); + const { formatMessage, i18n, locale } = useI18n(); const dispatch: ThunkDispatch = useDispatch(); const [relatedVocabularies, setRelatedVocabularies] = React.useState< string[] >([]); + const [selectedVocabularies, setSelectedVocabularies] = React.useState< + string[] + >([]); const modelingToolUrl = useSelector( (state: TermItState) => state.configuration ).modelingToolUrl; + const vocabularies = useSelector((state: TermItState) => state.vocabularies); + const vocabularyIris = Object.keys(vocabularies); React.useEffect(() => { if (open) { dispatch( loadRelatedVocabularies(VocabularyUtils.create(vocabulary.iri)) - ).then((data) => setRelatedVocabularies(data)); + ).then((data) => { + setRelatedVocabularies(data); + // The vocabulary is also among the related vocabularies loaded from the server + setSelectedVocabularies(data); + }); + if (Object.keys(vocabularies).length === 0) { + dispatch(loadVocabularies()); + } + } + }, [open, vocabulary.iri, vocabularies, dispatch]); + + const onSelect = (vIri: string) => { + if (selectedVocabularies.includes(vIri)) { + setSelectedVocabularies(selectedVocabularies.filter((v) => v !== vIri)); + } else { + setSelectedVocabularies([...selectedVocabularies, vIri]); } - }, [open, vocabulary.iri, dispatch]); + }; const onOpen = () => { - const vocabularyList = [vocabulary.iri, ...relatedVocabularies]; - let params = vocabularyList + let params = selectedVocabularies .map((v) => encodeURIComponent(v)) .join("&vocabulary="); window.location.href = modelingToolUrl + "?vocabulary=" + params; }; + const rowCount = Math.ceil(vocabularyIris.length / COLUMN_COUNT); + const rows = []; + for (let i = 0; i < rowCount; i++) { + const cols = []; + for (let j = 0; j < COLUMN_COUNT; j++) { + const index = i * COLUMN_COUNT + j; + if (index >= vocabularyIris.length) { + break; + } + cols.push( + + onSelect(vocabularyIris[index])} + className="vocabulary-checkbox" + /> + {getLocalized( + vocabularies[vocabularyIris[index]].label, + getShortLocale(locale) + )} + + ); + } + rows.push( + + {cols} + + ); + } + return ( = ({ title={formatMessage("vocabulary.summary.model.dialog.title", { vocabulary: getLocalized(vocabulary.label), })} - > + size="lg" + className="modeling-vocabulary-select-dialog" + > +

{i18n("vocabulary.summary.model.dialog.text")}

+ {rows} + ); }; diff --git a/src/i18n/cs.ts b/src/i18n/cs.ts index 21db8a5ec..8211dd440 100644 --- a/src/i18n/cs.ts +++ b/src/i18n/cs.ts @@ -341,6 +341,8 @@ const cs = { "vocabulary.summary.model.open": "Otevřít", "vocabulary.summary.model.dialog.title": "Modelovat vztahy pojmů slovníku {vocabulary}", + "vocabulary.summary.model.dialog.text": + "Vyberte slovníky, které chcete pro modelování otevřít. Předvybrány jsou slovníky, které s otevíraným slovníkem souvisí a musí být v seznamy zahrnuty.", "vocabulary.updated.message": "Slovník úspěšně uložen.", "vocabulary.created.message": "Slovník úspěšně vytvořen.", "vocabulary.detail.subtitle": "Vytvořen autorem {author} ", diff --git a/src/i18n/en.ts b/src/i18n/en.ts index a4c8dd643..2ea8a26d1 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -333,6 +333,8 @@ const en = { "vocabulary.summary.model.open": "Open", "vocabulary.summary.model.dialog.title": "Model relationships of terms in {vocabulary}", + "vocabulary.summary.model.dialog.text": + "Select vocabularies you want to open for modeling. Vocabularies related to the one being open are pre-selected as they are required by the modeling tool.", "vocabulary.updated.message": "Vocabulary successfully updated.", "vocabulary.created.message": "Vocabulary successfully created.", "vocabulary.detail.subtitle": "Created by {author} on ", From b3b4561bdc4c9d90056af736e4be3b9a56fdeb82 Mon Sep 17 00:00:00 2001 From: Martin Ledvinka Date: Wed, 9 Oct 2024 08:41:26 +0200 Subject: [PATCH 3/3] [Enhancement #528] Load related vocabularies when vocabulary IRI changes. --- .../modeling/OpenModelingToolDialog.tsx | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/component/vocabulary/modeling/OpenModelingToolDialog.tsx b/src/component/vocabulary/modeling/OpenModelingToolDialog.tsx index cd7ffee45..2b198f738 100644 --- a/src/component/vocabulary/modeling/OpenModelingToolDialog.tsx +++ b/src/component/vocabulary/modeling/OpenModelingToolDialog.tsx @@ -40,19 +40,17 @@ const OpenModelingToolDialog: React.FC = ({ const vocabularies = useSelector((state: TermItState) => state.vocabularies); const vocabularyIris = Object.keys(vocabularies); React.useEffect(() => { - if (open) { - dispatch( - loadRelatedVocabularies(VocabularyUtils.create(vocabulary.iri)) - ).then((data) => { - setRelatedVocabularies(data); - // The vocabulary is also among the related vocabularies loaded from the server - setSelectedVocabularies(data); - }); - if (Object.keys(vocabularies).length === 0) { - dispatch(loadVocabularies()); - } + dispatch( + loadRelatedVocabularies(VocabularyUtils.create(vocabulary.iri)) + ).then((data) => { + setRelatedVocabularies(data); + // The vocabulary is also among the related vocabularies loaded from the server + setSelectedVocabularies(data); + }); + if (Object.keys(vocabularies).length === 0) { + dispatch(loadVocabularies()); } - }, [open, vocabulary.iri, vocabularies, dispatch]); + }, [vocabulary.iri, vocabularies, dispatch]); const onSelect = (vIri: string) => { if (selectedVocabularies.includes(vIri)) {