diff --git a/src/component/annotator/Annotator.tsx b/src/component/annotator/Annotator.tsx index 4f090544..d3ba8e08 100644 --- a/src/component/annotator/Annotator.tsx +++ b/src/component/annotator/Annotator.tsx @@ -18,7 +18,6 @@ import TermOccurrence, {TextQuoteSelector} from "../../model/TermOccurrence"; import {setTermDefinitionSource} from "../../action/AsyncTermActions"; import JsonLdUtils from "../../util/JsonLdUtils"; import Utils from "../../util/Utils"; -import TextAnalysisButtonAnnotatorWrapper from "./TextAnalysisButtonAnnotatorWrapper"; import AnnotatorContent from "./AnnotatorContent"; import withI18n, {HasI18n} from "../hoc/withI18n"; import {injectIntl} from "react-intl"; @@ -33,6 +32,7 @@ import HeaderWithActions from "../misc/HeaderWithActions"; import {Card, CardBody, CardHeader} from "reactstrap"; import VocabularyIriLink from "../vocabulary/VocabularyIriLink"; import File from "../../model/File"; +import TextAnalysisInvocationButton from "./TextAnalysisInvocationButton"; interface AnnotatorProps extends HasI18n { fileIri: IRI; @@ -402,8 +402,8 @@ export class Annotator extends React.Component { - + = props => - ; - -export default ShowTextAnalysisInvocationButton; diff --git a/src/component/resource/file/TextAnalysisInvocationButton.tsx b/src/component/annotator/TextAnalysisInvocationButton.tsx similarity index 72% rename from src/component/resource/file/TextAnalysisInvocationButton.tsx rename to src/component/annotator/TextAnalysisInvocationButton.tsx index 432ec388..2c73b8e5 100644 --- a/src/component/resource/file/TextAnalysisInvocationButton.tsx +++ b/src/component/annotator/TextAnalysisInvocationButton.tsx @@ -1,17 +1,17 @@ import * as React from "react"; import {injectIntl} from "react-intl"; -import withI18n, {HasI18n} from "../../hoc/withI18n"; -import withInjectableLoading, {InjectsLoading} from "../../hoc/withInjectableLoading"; +import withI18n, {HasI18n} from "../hoc/withI18n"; +import withInjectableLoading, {InjectsLoading} from "../hoc/withInjectableLoading"; import {GoClippy} from "react-icons/go"; import {Button} from "reactstrap"; import {connect} from "react-redux"; -import {ThunkDispatch} from "../../../util/Types"; -import {executeFileTextAnalysis} from "../../../action/AsyncActions"; -import {publishNotification} from "../../../action/SyncActions"; -import NotificationType from "../../../model/NotificationType"; -import ResourceSelectVocabulary from "../ResourceSelectVocabulary"; -import Vocabulary from "../../../model/Vocabulary"; -import {IRI} from "../../../util/VocabularyUtils"; +import {ThunkDispatch} from "../../util/Types"; +import {executeFileTextAnalysis} from "../../action/AsyncActions"; +import {publishNotification} from "../../action/SyncActions"; +import NotificationType from "../../model/NotificationType"; +import ResourceSelectVocabulary from "../resource/ResourceSelectVocabulary"; +import Vocabulary from "../../model/Vocabulary"; +import {IRI} from "../../util/VocabularyUtils"; interface TextAnalysisInvocationButtonProps extends HasI18n, InjectsLoading { id?: string; @@ -35,11 +35,7 @@ export class TextAnalysisInvocationButton extends React.Component { - if (this.props.defaultVocabularyIri) { - this.invokeTextAnalysis(this.props.fileIri,this.props.defaultVocabularyIri); - } else { - this.setState({showVocabularySelector: true}); - } + this.setState({showVocabularySelector: true}); }; private invokeTextAnalysis(fileIri: IRI, vocabularyIri: string) { @@ -67,13 +63,14 @@ export class TextAnalysisInvocationButton extends React.Component + onCancel={this.closeVocabularySelect} onSubmit={this.onVocabularySelect} + title={i18n("file.metadata.startTextAnalysis.vocabularySelect.title")}/> ; } diff --git a/src/component/resource/file/__tests__/TextAnalysisInvocationButton.test.tsx b/src/component/annotator/__tests__/TextAnalysisInvocationButton.test.tsx similarity index 88% rename from src/component/resource/file/__tests__/TextAnalysisInvocationButton.test.tsx rename to src/component/annotator/__tests__/TextAnalysisInvocationButton.test.tsx index 65471b18..5372b072 100644 --- a/src/component/resource/file/__tests__/TextAnalysisInvocationButton.test.tsx +++ b/src/component/annotator/__tests__/TextAnalysisInvocationButton.test.tsx @@ -1,13 +1,13 @@ import * as React from "react"; -import File from "../../../../model/File"; -import VocabularyUtils, {IRI} from "../../../../util/VocabularyUtils"; -import Generator from "../../../../__tests__/environment/Generator"; +import File from "../../../model/File"; +import VocabularyUtils, {IRI} from "../../../util/VocabularyUtils"; +import Generator from "../../../__tests__/environment/Generator"; import {shallow} from "enzyme"; import {TextAnalysisInvocationButton} from "../TextAnalysisInvocationButton"; -import {intlFunctions} from "../../../../__tests__/environment/IntlUtil"; -import {InjectsLoading} from "../../../hoc/withInjectableLoading"; -import ResourceSelectVocabulary from "../../ResourceSelectVocabulary"; -import Vocabulary from "../../../../model/Vocabulary"; +import {intlFunctions} from "../../../__tests__/environment/IntlUtil"; +import {InjectsLoading} from "../../hoc/withInjectableLoading"; +import ResourceSelectVocabulary from "../../resource/ResourceSelectVocabulary"; +import Vocabulary from "../../../model/Vocabulary"; describe("TextAnalysisInvocationButton", () => { @@ -23,6 +23,7 @@ describe("TextAnalysisInvocationButton", () => { let renderMask: () => JSX.Element | null; let loadingProps: InjectsLoading; + let vocabulary: Vocabulary; beforeEach(() => { file = new File({ @@ -36,41 +37,45 @@ describe("TextAnalysisInvocationButton", () => { loadingOff = jest.fn(); renderMask = jest.fn(); loadingProps = {loadingOff, loadingOn, renderMask, loading: false}; + vocabulary = Generator.generateVocabulary(); }); it("runs text analysis immediately when defaultVocabulary was specified.", () => { const vocabularyIri = Generator.generateUri(); + vocabulary.iri = vocabularyIri; const fileIri = VocabularyUtils.create(file.iri); const wrapper = shallow(); - wrapper.instance().onClick(); + wrapper.instance().onVocabularySelect(vocabulary); expect(executeTextAnalysis).toHaveBeenCalledWith(fileIri, vocabularyIri); }); it("starts loading when text analysis is invoked", () => { const fileIri = VocabularyUtils.create(Generator.generateUri()); const vocabularyIri = Generator.generateUri(); + vocabulary.iri = vocabularyIri; const wrapper = shallow(); - wrapper.instance().onClick(); + wrapper.instance().onVocabularySelect(vocabulary); expect(loadingOn).toHaveBeenCalled(); }); it("stops loading after text analysis invocation finishes", () => { const fileIri = VocabularyUtils.create(Generator.generateUri()); const vocabularyIri = Generator.generateUri(); + vocabulary.iri = vocabularyIri; const wrapper = shallow(); - wrapper.instance().onClick(); + wrapper.instance().onVocabularySelect(vocabulary); return Promise.resolve().then(() => { expect(loadingOff).toHaveBeenCalled(); }); @@ -79,12 +84,13 @@ describe("TextAnalysisInvocationButton", () => { it("publishes notification after text analysis invocation finishes", () => { const fileIri = VocabularyUtils.create(Generator.generateUri()); const vocabularyIri = Generator.generateUri(); + vocabulary.iri = vocabularyIri; const wrapper = shallow(); - wrapper.instance().onClick(); + wrapper.instance().onVocabularySelect(vocabulary); return Promise.resolve().then(() => { expect(notifyAnalysisFinish).toHaveBeenCalled(); }); @@ -104,10 +110,6 @@ describe("TextAnalysisInvocationButton", () => { }); it("invokes text analysis with selected Vocabulary when Vocabulary selector is submitted", () => { - const vocabulary = new Vocabulary({ - iri: Generator.generateUri(), - label: "Test vocabulary" - }); const wrapper = shallow( { + if (voc) { + this.setState({vocabularyIri: VocabularyUtils.create(voc.iri)}); + } + }; + public render() { const resource = this.props.resource; const vocabularyIri = this.state.vocabularyIri; @@ -111,9 +119,12 @@ export class ResourceFileDetail extends React.Component{this.props.i18n("file.annotate.unknown-vocabulary")} + return + {this.props.i18n("file.annotate.selectVocabulary")} + + + + ; } return diff --git a/src/component/resource/ResourceSelectVocabulary.scss b/src/component/resource/ResourceSelectVocabulary.scss new file mode 100644 index 00000000..fdafae91 --- /dev/null +++ b/src/component/resource/ResourceSelectVocabulary.scss @@ -0,0 +1,4 @@ +.resource-select-vocabulary-modal { + width: fit-content !important; + min-width: 300px; +} \ No newline at end of file diff --git a/src/component/resource/ResourceSelectVocabulary.tsx b/src/component/resource/ResourceSelectVocabulary.tsx index 605a27b4..e504a015 100644 --- a/src/component/resource/ResourceSelectVocabulary.tsx +++ b/src/component/resource/ResourceSelectVocabulary.tsx @@ -1,73 +1,51 @@ import * as React from "react"; -import {injectIntl} from "react-intl"; import {Button, ButtonToolbar, Modal, ModalBody, ModalFooter, ModalHeader} from "reactstrap"; -import withI18n, {HasI18n} from "../hoc/withI18n"; import Vocabulary from "../../model/Vocabulary"; import VocabularySelect from "../vocabulary/VocabularySelect"; -import {connect} from "react-redux"; +import {useSelector} from "react-redux"; import TermItState from "../../model/TermItState"; +import {useI18n} from "../hook/useI18n"; +import "./ResourceSelectVocabulary.scss"; -interface PropsConnected { - vocabularies: Vocabulary[] -} - -interface ResourceSelectVocabularyOwnProps { +interface ResourceSelectVocabularyProps { show: boolean; defaultVocabularyIri?: string; onSubmit: (voc: Vocabulary | null) => void; onCancel: () => void; + title?: string; } -type ResourceSelectVocabularyProps = PropsConnected & ResourceSelectVocabularyOwnProps & HasI18n; - -interface ResourceSelectVocabularyState { - vocabularySelect: Vocabulary | null; +function getVocabulary(selectedVocabulary: Vocabulary | null, vocabularies: { [key: string]: Vocabulary }, defaultVocabularyIri?: string) { + return selectedVocabulary ? selectedVocabulary : defaultVocabularyIri ? vocabularies[defaultVocabularyIri] || null : null; } -class ResourceSelectVocabulary extends React.Component { - public constructor(props: ResourceSelectVocabularyProps) { - super(props); - this.state = { - vocabularySelect: null, - }; - } - - private onVocabularySet(voc: Vocabulary): void { - this.setState({vocabularySelect: voc}); - } - - private onSubmit = () => { - this.props.onSubmit(this.getVocabulary()); - }; - - private getVocabulary() { - return this.state.vocabularySelect ? this.state.vocabularySelect : this.props.vocabularies - .find( v => v.iri === this.props.defaultVocabularyIri) || null; - } - - public render() { - const onVocabularySet = this.onVocabularySet.bind(this); - const onSubmit = this.onSubmit.bind(this); - const vocabulary = this.getVocabulary(); - return - {this.props.i18n("vocabulary.select-vocabulary")} - - - - - - - - - - ; +const ResourceSelectVocabulary: React.FC = props => { + const {show, defaultVocabularyIri, onSubmit, onCancel, title} = props; + const [selectedVocabulary, setSelectedVocabulary] = React.useState(null); + const vocabularies = useSelector((state: TermItState) => state.vocabularies); + const submit = () => onSubmit(getVocabulary(selectedVocabulary, vocabularies, defaultVocabularyIri)); + const cancel = () => { + onCancel(); + setSelectedVocabulary(null); } -} - -export default connect((state: TermItState) => { - return { - vocabularies: Object.keys(state.vocabularies).map(value => state.vocabularies[value]), - }; -}) (injectIntl(withI18n(ResourceSelectVocabulary))); + const {i18n} = useI18n(); + + return + {title ? title : i18n("vocabulary.select-vocabulary")} + + + + + + + + + + ; +}; + +export default ResourceSelectVocabulary; diff --git a/src/component/resource/__tests__/ResourceFileDetail.test.tsx b/src/component/resource/__tests__/ResourceFileDetail.test.tsx index aba0ac5c..a6399af2 100644 --- a/src/component/resource/__tests__/ResourceFileDetail.test.tsx +++ b/src/component/resource/__tests__/ResourceFileDetail.test.tsx @@ -11,6 +11,7 @@ import Generator from "../../../__tests__/environment/Generator"; import Resource, {EMPTY_RESOURCE} from "../../../model/Resource"; import FileDetail from "../../file/FileContentDetail"; import Routes from "../../../util/Routes"; +import VocabularySelect from "../../vocabulary/VocabularySelect"; describe("ResourceFileDetail", () => { @@ -146,7 +147,7 @@ describe("ResourceFileDetail", () => { }); }); - it("renders info when no text analysis record exists for a standalone file", () => { + it("renders vocabulary selector when no text analysis record exists for a standalone file", () => { const wrapper = shallow( { return Promise.resolve().then(() => { expect(loadLatestTextAnalysisRecord).toHaveBeenCalledWith(VocabularyUtils.create(resourceNamespace + resourceName)); expect(wrapper.find(FileDetail).exists()).toBeFalsy(); - expect(wrapper.exists("#file-detail-no-vocabulary")).toBeTruthy(); + expect(wrapper.exists(VocabularySelect)).toBeTruthy(); }); }); @@ -269,6 +270,12 @@ describe("ResourceFileDetail", () => { }); it("sets vocabulary in state to undefined to force its reload when namespace in URL changes", () => { + resource.owner = { + vocabulary: {iri: Generator.generateUri()}, + iri: Generator.generateUri(), + label: "Test document", + files: [resource] + }; const wrapper = shallow( void; } -interface DispatchExt { - onVocabularySet: (voc: Vocabulary) => void; -} - -interface PropsCon { - vocabularies: { [key: string]: Vocabulary }, -} - -interface DispatchCon { - loadVocabularies: () => void -} - -interface Props extends PropsExt, DispatchExt, - PropsCon, HasI18n, DispatchCon { -} - -export class VocabularySelect extends React.Component { - - public componentDidMount() { - this.props.loadVocabularies(); - } - - private changeValue(vIri: string) { - this.props.onVocabularySet(this.props.vocabularies[vIri]); - } - - public render() { - const that = this; - const items = Object.keys(this.props.vocabularies || []).map(vIri => { - const onClick = () => that.changeValue(vIri); - return - {this.props.vocabularies[vIri].label} - - } - ); - - return - - {this.props.vocabulary ? this.props.vocabulary.label : that.props.i18n("vocabulary.select-vocabulary")} - - { - return { - ...data, - styles: { - ...data.styles, - overflow: "auto", - maxHeight: Utils.calculateAssetListHeight() + "px", - }, - }; - }, +const VocabularySelect: React.FC = props => { + const {vocabulary, onVocabularySet} = props; + const vocabularies = useSelector((state: TermItState) => state.vocabularies); + const dispatch = useDispatch(); + React.useEffect(() => { + if (Object.getOwnPropertyNames(vocabularies).length === 0) { + dispatch(loadVocabularies()); + } + }); + const onChange = (vIri: string) => onVocabularySet(vocabularies[vIri]); + const {i18n} = useI18n(); + + const items = Object.keys(vocabularies || []).map(vIri => { + return onChange(vIri)}> + {vocabularies[vIri].label} + + } + ); + + return + + {vocabulary ? vocabulary.label : i18n("vocabulary.select-vocabulary")} + + { + return { + ...data, + styles: { + ...data.styles, + overflow: "auto", + maxHeight: Utils.calculateAssetListHeight() + "px", + }, + }; }, - }}> - {items} - - ; - } + }, + }}> + {items} + + ; } -export default connect((state: TermItState) => { - return { - vocabularies: state.vocabularies - }; -}, (dispatch: ThunkDispatch) => { - return { - loadVocabularies: () => dispatch(loadVocabularies()) - }; -})(injectIntl(withI18n(VocabularySelect))); +export default VocabularySelect; diff --git a/src/component/vocabulary/__tests__/VocabularySelect.test.tsx b/src/component/vocabulary/__tests__/VocabularySelect.test.tsx index 2f84b189..8714c9ec 100644 --- a/src/component/vocabulary/__tests__/VocabularySelect.test.tsx +++ b/src/component/vocabulary/__tests__/VocabularySelect.test.tsx @@ -1,37 +1,61 @@ import * as React from "react"; import {default as Vocabulary, EMPTY_VOCABULARY} from "../../../model/Vocabulary"; -import {VocabularySelect} from "../VocabularySelect"; +import VocabularySelect from "../VocabularySelect"; import {mountWithIntl} from "../../../__tests__/environment/Environment"; -import {intlFunctions} from "../../../__tests__/environment/IntlUtil"; +import {mockUseI18n} from "../../../__tests__/environment/IntlUtil"; import {DropdownItem, DropdownToggle} from "reactstrap"; +import * as redux from "react-redux"; +import {withHooks} from "jest-react-hooks-shallow"; +import {shallow} from "enzyme"; +import * as Actions from "../../../action/AsyncActions"; describe("VocabularySelect", () => { let voc: Vocabulary; let onVocabularySet: (voc: Vocabulary) => void; - let loadVocabularies: () => void; let vocabularies: { [key: string]: Vocabulary }; beforeEach(() => { onVocabularySet = jest.fn(); - loadVocabularies = jest.fn(); voc = EMPTY_VOCABULARY; vocabularies = {}; vocabularies[EMPTY_VOCABULARY.iri] = EMPTY_VOCABULARY; }); + it("loads vocabularies on mount", () => { + jest.spyOn(redux, "useSelector").mockReturnValue({}); + const fakeDispatch = jest.fn(); + jest.spyOn(redux, "useDispatch").mockReturnValue(fakeDispatch); + jest.spyOn(Actions, "loadVocabularies").mockReturnValue(jest.fn()); + withHooks(() => { + mockUseI18n(); + shallow(); + expect(fakeDispatch).toHaveBeenCalled(); + expect(Actions.loadVocabularies).toHaveBeenCalled(); + }); + }); + + it("does not load vocabularies when they are already loaded", () => { + jest.spyOn(redux, "useSelector").mockReturnValue(vocabularies); + const fakeDispatch = jest.fn(); + jest.spyOn(redux, "useDispatch").mockReturnValue(fakeDispatch); + jest.spyOn(Actions, "loadVocabularies").mockReturnValue(jest.fn()); + withHooks(() => { + mockUseI18n(); + shallow(); + expect(fakeDispatch).not.toHaveBeenCalled(); + }); + }); + it("VocabularySelect Selection calls the callback", () => { + jest.spyOn(redux, "useSelector").mockReturnValue(vocabularies); const wrapper = mountWithIntl(); wrapper.find(DropdownToggle).simulate("click"); wrapper.find(DropdownItem).simulate("click"); expect(onVocabularySet).toHaveBeenCalled(); }); }); - diff --git a/src/i18n/cs.ts b/src/i18n/cs.ts index 797c1bee..7c41c3f0 100644 --- a/src/i18n/cs.ts +++ b/src/i18n/cs.ts @@ -226,7 +226,7 @@ const cs = { "vocabulary.document.remove": "Odpojit dokument", "vocabulary.term.created.message": "Pojem úspěšně vytvořen.", - "vocabulary.select-vocabulary": "Vyber slovník", + "vocabulary.select-vocabulary": "Vyberte slovník", "resource.management": "Správa zdrojů", "resource.management.resources": "Zdroje", @@ -404,8 +404,9 @@ const cs = { "file.text-analysis.finished.message": "Textová analýza souboru úspěšně dokončena.", "file.metadata.startTextAnalysis": "Spustit textovou analýzu", "file.metadata.startTextAnalysis.text": "Analyzovat", + "file.metadata.startTextAnalysis.vocabularySelect.title": "Vyberte slovník pro automatickou analýzu textu", "file.content.upload.success": "Soubor \"{fileName}\" úspěšně nahrán na server.", - "file.annotate.unknown-vocabulary": "Nelze určit slovník pro anotování tohoto souboru. Soubor nepatří slovníkovému dokumentu ani nebyl zpracován službou textové analýzy.", + "file.annotate.selectVocabulary": "Nelze určit slovník pro anotování tohoto souboru. Vyberte ho, prosím...", "dataset.license": "Licence", "dataset.format": "Formát", diff --git a/src/i18n/en.ts b/src/i18n/en.ts index c1efbb57..09c7612b 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -399,8 +399,9 @@ const en = { "file.text-analysis.finished.message": "Text analysis successfully finished.", "file.metadata.startTextAnalysis": "Start text analysis", "file.metadata.startTextAnalysis.text": "Analyze", + "file.metadata.startTextAnalysis.vocabularySelect.title": "Select vocabulary for automatic text analysis", "file.content.upload.success": "Content of file \"{fileName}\" successfully uploaded.", - "file.annotate.unknown-vocabulary": "Unable to determine vocabulary for annotating this file. It neither belongs to a vocabulary document nor has been processed by text analysis.", + "file.annotate.selectVocabulary": "Unable to determine vocabulary for annotating this file. Please, select one...", "dataset.license": "License", "dataset.format": "Format",