From 3fb8f5bf833421697d442d59ef731727146838d5 Mon Sep 17 00:00:00 2001 From: Martin Ledvinka Date: Tue, 30 Mar 2021 12:29:56 +0200 Subject: [PATCH 1/8] [Feature #1544] Show vocabulary which was used to annotate file content in annotator. --- src/component/annotator/Annotator.scss | 3 +- src/component/annotator/Annotator.tsx | 89 ++++++++++++------- .../annotator/__tests__/Annotator.test.tsx | 78 +++++++++------- src/i18n/cs.ts | 1 + src/i18n/en.ts | 1 + 5 files changed, 109 insertions(+), 63 deletions(-) diff --git a/src/component/annotator/Annotator.scss b/src/component/annotator/Annotator.scss index 0726a4b2..6eb77964 100644 --- a/src/component/annotator/Annotator.scss +++ b/src/component/annotator/Annotator.scss @@ -3,8 +3,9 @@ display: inline-block; position: fixed !important; bottom: 120px; - right:15px; + right: 15px; width:230px; + margin-right: 0.5rem; background-color: whitesmoke; } diff --git a/src/component/annotator/Annotator.tsx b/src/component/annotator/Annotator.tsx index b57bc7a9..63261efb 100644 --- a/src/component/annotator/Annotator.tsx +++ b/src/component/annotator/Annotator.tsx @@ -6,7 +6,7 @@ import Term from "../../model/Term"; import HtmlDomUtils from "./HtmlDomUtils"; import LegendToggle from "./LegendToggle"; import {DomUtils} from "htmlparser2"; -import VocabularyUtils, {IRI} from "../../util/VocabularyUtils"; +import VocabularyUtils, {IRI, IRIImpl} from "../../util/VocabularyUtils"; import CreateTermFromAnnotation, {CreateTermFromAnnotation as CT} from "./CreateTermFromAnnotation"; import SelectionPurposeDialog from "./SelectionPurposeDialog"; import {connect} from "react-redux"; @@ -29,6 +29,10 @@ import IfUserAuthorized from "../authorization/IfUserAuthorized"; import TermItState from "../../model/TermItState"; import User from "../../model/User"; import "./Annotator.scss"; +import HeaderWithActions from "../misc/HeaderWithActions"; +import {Card, CardBody, CardHeader, Col, Label, Row} from "reactstrap"; +import VocabularyIriLink from "../vocabulary/VocabularyIriLink"; +import File from "../../model/File"; interface AnnotatorProps extends HasI18n { fileIri: IRI; @@ -36,6 +40,7 @@ interface AnnotatorProps extends HasI18n { initialHtml: string; scrollTo?: TextQuoteSelector; // Selector of an annotation to scroll to (and highlight) after rendering user: User; + file: File; onUpdate(newHtml: string): void; @@ -387,35 +392,56 @@ export class Annotator extends React.Component { }; public render() { - return
+ + return <> - - - - - - - -
- -
-
+ + + + {this.renderMetadata()} + + + + + + + + + +
+ +
+
+
+ + } + + private renderMetadata() { + return + + + + + + + ; } private generateVirtualPopperAnchor(): HTMLElement { @@ -469,7 +495,10 @@ export class Annotator extends React.Component { } } -export default connect((state: TermItState) => ({user: state.user}), (dispatch: ThunkDispatch) => { +export default connect((state: TermItState) => ({ + user: state.user, + file: state.selectedFile +}), (dispatch: ThunkDispatch) => { return { publishMessage: (message: Message) => dispatch(publishMessage(message)), setTermDefinitionSource: (src: TermOccurrence, term: Term) => dispatch(setTermDefinitionSource(src, term)), diff --git a/src/component/annotator/__tests__/Annotator.test.tsx b/src/component/annotator/__tests__/Annotator.test.tsx index 3eeb6bd5..49107f3b 100644 --- a/src/component/annotator/__tests__/Annotator.test.tsx +++ b/src/component/annotator/__tests__/Annotator.test.tsx @@ -17,8 +17,10 @@ import TermOccurrence, {TextQuoteSelector} from "../../../model/TermOccurrence"; import AnnotatorContent from "../AnnotatorContent"; import {intlFunctions} from "../../../__tests__/environment/IntlUtil"; import User from "../../../model/User"; +import File from "../../../model/File"; jest.mock("../HtmlDomUtils"); +jest.mock("../../misc/AssetIriLink"); describe("Annotator", () => { @@ -38,6 +40,12 @@ describe("Annotator", () => { updateTerm(term: Term): Promise; }; let user: User; + let file: File; + let stateProps: { + user: User; + file: File; + }; + beforeEach(() => { mockedCallbackProps = { onUpdate: jest.fn(), @@ -46,11 +54,17 @@ describe("Annotator", () => { updateTerm: jest.fn().mockResolvedValue({}) }; user = Generator.generateUser(); + file = new File({ + iri: Generator.generateUri(), + label: "test.html", + types: [VocabularyUtils.FILE] + }); + stateProps = {user, file}; }); it("renders body of provided html content", () => { const wrapper = mountWithIntl(); @@ -62,7 +76,7 @@ describe("Annotator", () => { const htmlContent = surroundWithHtml("This is a link"); const wrapper = mountWithIntl(); @@ -78,7 +92,7 @@ describe("Annotator", () => { ) ); const wrapper = mountWithIntlAttached(); @@ -103,7 +117,7 @@ describe("Annotator", () => { HtmlDomUtils.addClassToElement = jest.fn(); HtmlDomUtils.removeClassFromElement = jest.fn(); element.scrollIntoView = jest.fn(); - shallow((); expect(HtmlDomUtils.findAnnotationElementBySelector).toHaveBeenCalledWith(document, selector); @@ -118,7 +132,7 @@ describe("Annotator", () => { HtmlDomUtils.removeClassFromElement = jest.fn(); element.scrollIntoView = jest.fn(); jest.useFakeTimers(); - shallow((); jest.runAllTimers(); @@ -130,7 +144,7 @@ describe("Annotator", () => { throw new Error("Unable to find annotation.") }); jest.useFakeTimers(); - shallow((); jest.runAllTimers(); @@ -145,7 +159,7 @@ describe("Annotator", () => { HtmlDomUtils.addClassToElement = jest.fn(); HtmlDomUtils.removeClassFromElement = jest.fn(); element.scrollIntoView = jest.fn(); - const wrapper = shallow((); wrapper.update(); @@ -158,7 +172,7 @@ describe("Annotator", () => { const div = document.createElement("div"); document.body.appendChild(div); const wrapper = mountWithIntl(, {attachTo: div}); const newSpan = div.querySelector("span"); @@ -188,7 +202,7 @@ describe("Annotator", () => { it("stores annotation from which the new term is being created for later reference", () => { const wrapper = shallow(); wrapper.instance().onCreateTerm("label", annotation); @@ -198,7 +212,7 @@ describe("Annotator", () => { // Bug #1245 it("removes created label annotation when new term creation is cancelled", () => { const wrapper = shallow(); wrapper.instance().setState({newTermLabelAnnotation: annotation}); @@ -213,7 +227,7 @@ describe("Annotator", () => { // Bug #1443 it("does not remove suggested label occurrence when new term creation is cancelled", () => { const wrapper = shallow(); annotation.score = "1.0"; @@ -228,7 +242,7 @@ describe("Annotator", () => { it("does not confirmed term label occurrence when new term creation is cancelled", () => { const wrapper = shallow(); annotation.resource = Generator.generateUri(); @@ -244,7 +258,7 @@ describe("Annotator", () => { // Bug #1245 it("removes created definition annotation when new term creation is cancelled", () => { const wrapper = shallow(); const labelAnnotation = annotation; @@ -270,7 +284,7 @@ describe("Annotator", () => { it("makes a shallow copy of parsed content to force its re-render when new term is created", () => { const wrapper = shallow(); const annotationNode = { @@ -318,7 +332,7 @@ describe("Annotator", () => { getPropertyValue: () => "16px" }); const wrapper = mountWithIntl(); wrapper.find("#annotator").simulate("mouseUp"); @@ -331,7 +345,7 @@ describe("Annotator", () => { getPropertyValue: () => "16px" }); const wrapper = mountWithIntl(); wrapper.find("#annotator").simulate("mouseUp"); @@ -348,7 +362,7 @@ describe("Annotator", () => { getPropertyValue: () => "16px" }); const wrapper = mountWithIntl(); const originalState = Object.assign({}, wrapper.find(Annotator).state()); @@ -378,7 +392,7 @@ describe("Annotator", () => { // Bug #1230 it("does not mark term occurrence sticky when it is being used to create new term", () => { const wrapper = shallow(); HtmlDomUtils.getSelectionRange = jest.fn().mockReturnValue(range); @@ -420,7 +434,7 @@ describe("Annotator", () => { it("sets content from the created annotation as definition of the term being currently created", () => { const wrapper = shallow(); wrapper.setState({newTermLabelAnnotation: annotation}); @@ -439,7 +453,7 @@ describe("Annotator", () => { // Bug #1230 it("does not mark term definition sticky when it is being used as new term's definition", () => { const wrapper = shallow(); wrapper.setState({newTermLabelAnnotation: annotation}); @@ -490,7 +504,7 @@ describe("Annotator", () => { it("assigns new term to the annotation used to define new term label", () => { const wrapper = shallow(); wrapper.setState({newTermLabelAnnotation: labelAnnotation}); @@ -502,7 +516,7 @@ describe("Annotator", () => { it("assigns new term to the annotation used to define new term definition", () => { const wrapper = shallow(); wrapper.setState({ @@ -518,7 +532,7 @@ describe("Annotator", () => { it("sets definition source of the new term", () => { const wrapper = shallow(); wrapper.setState({ @@ -558,7 +572,7 @@ describe("Annotator", () => { it("creates term definition source when annotation is definition", () => { const wrapper = shallow(); AnnotationDomHelper.findAnnotation = jest.fn().mockReturnValue(annotationNode); @@ -576,7 +590,7 @@ describe("Annotator", () => { it("makes a shallow copy of parsed content to force its re-render", () => { const wrapper = shallow(); const originalContent = wrapper.find(AnnotatorContent).prop("content"); @@ -596,7 +610,7 @@ describe("Annotator", () => { AnnotationDomHelper.findAnnotation = jest.fn().mockReturnValue(annotationNode); AnnotationDomHelper.removeAnnotation = jest.fn(); const wrapper = shallow(); wrapper.setState({existingTermDefinitionAnnotationElement: annotationNode as Element}); @@ -611,7 +625,7 @@ describe("Annotator", () => { it("updates term with the specified definition content", async () => { const wrapper = shallow(); AnnotationDomHelper.findAnnotation = jest.fn().mockReturnValue(annotationNode); @@ -626,7 +640,7 @@ describe("Annotator", () => { describe("onRemove", () => { it("makes a shallow copy of parsed content to force its re-render", () => { const wrapper = shallow(); const originalContent = wrapper.find(AnnotatorContent).prop("content"); @@ -675,7 +689,7 @@ describe("Annotator", () => { it("sets annotation resource attribute to provided value when a term was indeed selected", () => { const wrapper = shallow(); const term = Generator.generateTerm(); @@ -688,7 +702,7 @@ describe("Annotator", () => { // Bug #1399 it("deletes annotation resource attribute when null term is selected", () => { const wrapper = shallow(); annotationNode.resource = Generator.generateUri(); @@ -698,7 +712,7 @@ describe("Annotator", () => { it("updates content when annotation is term occurrence", () => { const wrapper = shallow(); const term = Generator.generateTerm(); @@ -712,7 +726,7 @@ describe("Annotator", () => { annotation.property = VocabularyUtils.IS_DEFINITION_OF_TERM; annotation.typeof = AnnotationType.DEFINITION; const wrapper = shallow(); const term = Generator.generateTerm(); diff --git a/src/i18n/cs.ts b/src/i18n/cs.ts index 5212412b..797c1bee 100644 --- a/src/i18n/cs.ts +++ b/src/i18n/cs.ts @@ -461,6 +461,7 @@ const cs = { "annotator": "Anotátor", "annotator.content.loading": "Načítám obsah souboru...", + "annotator.vocabulary": "Používá pojmy ze slovníku", "annotator.selectionPurpose.dialog.title": "K čemu bude sloužit vybraný text?", "annotator.selectionPurpose.create": "Nový pojem", "annotator.selectionPurpose.occurrence": "Označení výskytu pojmu", diff --git a/src/i18n/en.ts b/src/i18n/en.ts index f1e5e02c..c1efbb57 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -456,6 +456,7 @@ const en = { "annotator": "Annotator", "annotator.content.loading": "Loading file content...", + "annotator.vocabulary": "Uses terms from vocabulary", "annotator.selectionPurpose.dialog.title": "What is the purpose of the selected text?", "annotator.selectionPurpose.create": "Create term", "annotator.selectionPurpose.occurrence": "Mark term occurrence", From b771a8d6d40c53d10c36d56fd4524a1bbe74dfd0 Mon Sep 17 00:00:00 2001 From: Martin Ledvinka Date: Tue, 30 Mar 2021 14:33:46 +0200 Subject: [PATCH 2/8] [Feature #1545] Let user select text analysis vocabulary in the annotator. --- src/component/annotator/Annotator.tsx | 6 +- .../TextAnalysisButtonAnnotatorWrapper.tsx | 16 --- .../TextAnalysisInvocationButton.tsx | 26 ++-- .../TextAnalysisInvocationButton.test.tsx | 32 ++--- .../resource/ResourceSelectVocabulary.scss | 4 + .../resource/ResourceSelectVocabulary.tsx | 93 +++++--------- src/component/vocabulary/VocabularySelect.tsx | 121 +++++++----------- .../__tests__/VocabularySelect.test.tsx | 40 ++++-- src/i18n/cs.ts | 2 +- 9 files changed, 150 insertions(+), 190 deletions(-) delete mode 100644 src/component/annotator/TextAnalysisButtonAnnotatorWrapper.tsx rename src/component/{resource/file => annotator}/TextAnalysisInvocationButton.tsx (74%) rename src/component/{resource/file => annotator}/__tests__/TextAnalysisInvocationButton.test.tsx (88%) create mode 100644 src/component/resource/ResourceSelectVocabulary.scss diff --git a/src/component/annotator/Annotator.tsx b/src/component/annotator/Annotator.tsx index 63261efb..538cc982 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, Col, Label, Row} from "reactstrap"; import VocabularyIriLink from "../vocabulary/VocabularyIriLink"; import File from "../../model/File"; +import TextAnalysisInvocationButton from "./TextAnalysisInvocationButton"; interface AnnotatorProps extends HasI18n { fileIri: IRI; @@ -403,8 +403,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 74% rename from src/component/resource/file/TextAnalysisInvocationButton.tsx rename to src/component/annotator/TextAnalysisInvocationButton.tsx index 432ec388..9adcccda 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) { @@ -73,7 +69,7 @@ export class TextAnalysisInvocationButton extends React.Component {i18n("file.metadata.startTextAnalysis.text")} + onClick={this.onClick}>{i18n("file.metadata.startTextAnalysis.text")} ; } 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( void; onCancel: () => void; } -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} = props; + const [selectedVocabulary, setSelectedVocabulary] = React.useState(null); + const vocabularies = useSelector((state: TermItState) => state.vocabularies); + const submit = () => onSubmit(getVocabulary(selectedVocabulary, vocabularies, defaultVocabularyIri)); + const cancel = () => { + setSelectedVocabulary(null); + onCancel(); } -} - -export default connect((state: TermItState) => { - return { - vocabularies: Object.keys(state.vocabularies).map(value => state.vocabularies[value]), - }; -}) (injectIntl(withI18n(ResourceSelectVocabulary))); + const {i18n} = useI18n(); + + return + {i18n("vocabulary.select-vocabulary")} + + + + + + + + + + ; +}; + +export default ResourceSelectVocabulary; diff --git a/src/component/vocabulary/VocabularySelect.tsx b/src/component/vocabulary/VocabularySelect.tsx index dc5d1f19..93d45777 100644 --- a/src/component/vocabulary/VocabularySelect.tsx +++ b/src/component/vocabulary/VocabularySelect.tsx @@ -2,86 +2,59 @@ import * as React from "react"; import {DropdownItem, DropdownMenu, DropdownToggle, UncontrolledDropdown} from "reactstrap"; import Vocabulary from "../../model/Vocabulary"; import TermItState from "../../model/TermItState"; -import {connect} from "react-redux"; -import {ThunkDispatch} from "../../util/Types"; +import {useDispatch, useSelector} from "react-redux"; import {loadVocabularies} from "../../action/AsyncActions"; -import withI18n, {HasI18n} from "../hoc/withI18n"; -import {injectIntl} from "react-intl"; import Utils from "../../util/Utils"; +import {useI18n} from "../hook/useI18n"; -interface PropsExt { - vocabulary: Vocabulary | null; +interface VocabularySelectProps { id?: string; + vocabulary: Vocabulary | null; + onVocabularySet: (vocabulary: Vocabulary) => 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..2ea5a639 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", From c5003cee44844e8987f031da07e8880c6b10a018 Mon Sep 17 00:00:00 2001 From: Martin Ledvinka Date: Tue, 30 Mar 2021 16:04:14 +0200 Subject: [PATCH 3/8] [Feature #1545] Simplify the text analysis invocation button. --- .../TextAnalysisInvocationButton.tsx | 86 ++++-------- .../TextAnalysisInvocationButton.test.tsx | 125 +++++------------- .../resource/ResourceSelectVocabulary.tsx | 2 +- 3 files changed, 56 insertions(+), 157 deletions(-) diff --git a/src/component/annotator/TextAnalysisInvocationButton.tsx b/src/component/annotator/TextAnalysisInvocationButton.tsx index 9adcccda..b96ce994 100644 --- a/src/component/annotator/TextAnalysisInvocationButton.tsx +++ b/src/component/annotator/TextAnalysisInvocationButton.tsx @@ -1,10 +1,7 @@ import * as React from "react"; -import {injectIntl} from "react-intl"; -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 {useDispatch} from "react-redux"; import {ThunkDispatch} from "../../util/Types"; import {executeFileTextAnalysis} from "../../action/AsyncActions"; import {publishNotification} from "../../action/SyncActions"; @@ -12,72 +9,39 @@ import NotificationType from "../../model/NotificationType"; import ResourceSelectVocabulary from "../resource/ResourceSelectVocabulary"; import Vocabulary from "../../model/Vocabulary"; import {IRI} from "../../util/VocabularyUtils"; +import {useI18n} from "../hook/useI18n"; -interface TextAnalysisInvocationButtonProps extends HasI18n, InjectsLoading { - id?: string; +interface TextAnalysisInvocationButtonProps { fileIri: IRI; defaultVocabularyIri?: string; - executeTextAnalysis: (fileIri: IRI, vocabularyIri: string) => Promise; - notifyAnalysisFinish: () => void; className?: string } -interface TextAnalysisInvocationButtonState { - showVocabularySelector: boolean; - -} - -export class TextAnalysisInvocationButton extends React.Component { - - constructor(props: InjectsLoading & TextAnalysisInvocationButtonProps) { - super(props); - this.state = {showVocabularySelector: false}; - } - - public onClick = () => { - this.setState({showVocabularySelector: true}); - }; - - private invokeTextAnalysis(fileIri: IRI, vocabularyIri: string) { - this.props.loadingOn(); - this.props.executeTextAnalysis(fileIri, vocabularyIri).then(() => { - this.props.loadingOff(); - this.props.notifyAnalysisFinish(); - }); - } - - public onVocabularySelect = (vocabulary: Vocabulary | null) => { - this.closeVocabularySelect(); - if (!vocabulary) { +const TextAnalysisInvocationButton: React.FC = props => { + const {fileIri, defaultVocabularyIri, className} = props; + const [showSelector, setShowSelector] = React.useState(false); + const dispatch: ThunkDispatch = useDispatch(); + const {i18n} = useI18n(); + const close = () => setShowSelector(false); + const submit = (voc: Vocabulary | null) => { + close(); + if (!voc) { return; } - this.invokeTextAnalysis(this.props.fileIri, vocabulary.iri); + dispatch(executeFileTextAnalysis(fileIri, voc.iri)).then(() => dispatch(publishNotification({source: {type: NotificationType.TEXT_ANALYSIS_FINISHED}}))); }; - private closeVocabularySelect = () => { - this.setState({showVocabularySelector: false}); - }; - - public render() { - const i18n = this.props.i18n; - return <> - - - ; - } + return <> + + + ; } -export default connect(undefined, (dispatch: ThunkDispatch) => { - return { - executeTextAnalysis: (fileIri: IRI, vocabularyIri: string) => dispatch(executeFileTextAnalysis(fileIri, vocabularyIri)), - notifyAnalysisFinish: () => dispatch(publishNotification({source: {type: NotificationType.TEXT_ANALYSIS_FINISHED}})) - }; -})(injectIntl(withI18n(withInjectableLoading(TextAnalysisInvocationButton)))); +export default TextAnalysisInvocationButton; diff --git a/src/component/annotator/__tests__/TextAnalysisInvocationButton.test.tsx b/src/component/annotator/__tests__/TextAnalysisInvocationButton.test.tsx index 5372b072..1df03175 100644 --- a/src/component/annotator/__tests__/TextAnalysisInvocationButton.test.tsx +++ b/src/component/annotator/__tests__/TextAnalysisInvocationButton.test.tsx @@ -1,125 +1,60 @@ import * as React from "react"; -import File from "../../../model/File"; -import VocabularyUtils, {IRI} from "../../../util/VocabularyUtils"; +import VocabularyUtils 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 TextAnalysisInvocationButton from "../TextAnalysisInvocationButton"; +import {intlFunctions, mockUseI18n} from "../../../__tests__/environment/IntlUtil"; import ResourceSelectVocabulary from "../../resource/ResourceSelectVocabulary"; import Vocabulary from "../../../model/Vocabulary"; +import {ThunkDispatch} from "../../../util/Types"; +import * as redux from "react-redux"; +import * as AsyncActions from "../../../action/AsyncActions"; +import * as SyncActions from "../../../action/SyncActions"; +import {Button} from "reactstrap"; describe("TextAnalysisInvocationButton", () => { - const namespace = "http://onto.fel.cvut.cz/ontologies/termit/resources/"; - const fileName = "test.html"; - - let file: File; - - let executeTextAnalysis: (fileIri: IRI, vocabularyIri?: string) => Promise; - let notifyAnalysisFinish: () => void; - let loadingOn: () => void; - let loadingOff: () => void; - let renderMask: () => JSX.Element | null; - - let loadingProps: InjectsLoading; let vocabulary: Vocabulary; + let dispatch: ThunkDispatch; + beforeEach(() => { - file = new File({ - iri: namespace + fileName, - label: fileName, - types: [VocabularyUtils.FILE, VocabularyUtils.RESOURCE] - }); - executeTextAnalysis = jest.fn().mockImplementation(() => Promise.resolve()); - notifyAnalysisFinish = jest.fn(); - loadingOn = jest.fn(); - 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().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().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().onVocabularySelect(vocabulary); - return Promise.resolve().then(() => { - expect(loadingOff).toHaveBeenCalled(); - }); + dispatch = jest.fn().mockResolvedValue({}); + jest.spyOn(redux, "useDispatch").mockReturnValue(dispatch); + jest.spyOn(AsyncActions, "executeFileTextAnalysis"); + jest.spyOn(SyncActions, "publishNotification"); + mockUseI18n(); }); 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().onVocabularySelect(vocabulary); + const wrapper = shallow(); + wrapper.find(ResourceSelectVocabulary).prop("onSubmit")(vocabulary); return Promise.resolve().then(() => { - expect(notifyAnalysisFinish).toHaveBeenCalled(); + expect(SyncActions.publishNotification).toHaveBeenCalled(); }); }); - it("shows vocabulary selector when no default vocabulary was specified", () => { + it("shows vocabulary selector on button click", () => { const fileIri = VocabularyUtils.create(Generator.generateUri()); - const wrapper = shallow(); - wrapper.instance().onClick(); + const wrapper = shallow(); + wrapper.find(Button).simulate("click"); wrapper.update(); - expect(wrapper.instance().state.showVocabularySelector).toBeTruthy(); expect(wrapper.exists(ResourceSelectVocabulary)); - expect(executeTextAnalysis).not.toHaveBeenCalled(); + expect(wrapper.find(ResourceSelectVocabulary).prop("show")).toBeTruthy(); }); it("invokes text analysis with selected Vocabulary when Vocabulary selector is submitted", () => { - const wrapper = shallow(); - wrapper.instance().onClick(); - wrapper.update(); - expect(wrapper.instance().state.showVocabularySelector).toBeTruthy(); - wrapper.instance().onVocabularySelect(vocabulary); - expect(executeTextAnalysis).toHaveBeenLastCalledWith(VocabularyUtils.create(file.iri), vocabulary.iri); - wrapper.update(); - expect(wrapper.instance().state.showVocabularySelector).toBeFalsy(); + const fileIri = VocabularyUtils.create(Generator.generateUri()); + const vocabularyIri = Generator.generateUri(); + vocabulary.iri = vocabularyIri; + const wrapper = shallow(); + wrapper.find(ResourceSelectVocabulary).prop("onSubmit")(vocabulary); + expect(AsyncActions.executeFileTextAnalysis).toHaveBeenCalledWith(fileIri, vocabularyIri) }); }); diff --git a/src/component/resource/ResourceSelectVocabulary.tsx b/src/component/resource/ResourceSelectVocabulary.tsx index e7b2a1fb..2d2856bf 100644 --- a/src/component/resource/ResourceSelectVocabulary.tsx +++ b/src/component/resource/ResourceSelectVocabulary.tsx @@ -24,8 +24,8 @@ const ResourceSelectVocabulary: React.FC = props const vocabularies = useSelector((state: TermItState) => state.vocabularies); const submit = () => onSubmit(getVocabulary(selectedVocabulary, vocabularies, defaultVocabularyIri)); const cancel = () => { - setSelectedVocabulary(null); onCancel(); + setSelectedVocabulary(null); } const {i18n} = useI18n(); From 5810b45c2426aa187e2473005640d54b0ee332d4 Mon Sep 17 00:00:00 2001 From: Martin Ledvinka Date: Tue, 30 Mar 2021 16:09:36 +0200 Subject: [PATCH 4/8] [Fix] Fix mask styling (conflicting with argon default styles). --- src/component/misc/Mask.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/component/misc/Mask.scss b/src/component/misc/Mask.scss index d12a6c17..d168f79f 100644 --- a/src/component/misc/Mask.scss +++ b/src/component/misc/Mask.scss @@ -1,7 +1,7 @@ @import "../../styles/custom/colors"; .mask { - position: fixed; + position: fixed !important; padding: 0; margin: 0; top: 0; From 7ae3e7670aa6e212106675ca646abeda66ab66d2 Mon Sep 17 00:00:00 2001 From: Martin Ledvinka Date: Tue, 30 Mar 2021 17:51:28 +0200 Subject: [PATCH 5/8] [Feature #1545] Allow selecting vocabulary when opening standalone file in annotator. --- src/component/resource/ResourceFileDetail.tsx | 25 +++++++++++++------ .../__tests__/ResourceFileDetail.test.tsx | 11 ++++++-- src/i18n/cs.ts | 2 +- src/i18n/en.ts | 2 +- 4 files changed, 29 insertions(+), 11 deletions(-) diff --git a/src/component/resource/ResourceFileDetail.tsx b/src/component/resource/ResourceFileDetail.tsx index 6013f062..70f371ad 100644 --- a/src/component/resource/ResourceFileDetail.tsx +++ b/src/component/resource/ResourceFileDetail.tsx @@ -11,11 +11,13 @@ import Resource, {EMPTY_RESOURCE} from "../../model/Resource"; import File from "../../model/File"; import TermItState from "../../model/TermItState"; import {TextAnalysisRecord} from "../../model/TextAnalysisRecord"; -import {Label} from "reactstrap"; import withI18n, {HasI18n} from "../hoc/withI18n"; import {popRoutingPayload} from "../../action/SyncActions"; import Routes from "../../util/Routes"; import {TextQuoteSelector} from "../../model/TermOccurrence"; +import VocabularySelect from "../vocabulary/VocabularySelect"; +import Vocabulary from "../../model/Vocabulary"; +import {Card, CardBody, CardHeader} from "reactstrap"; interface StoreStateProps { resource: Resource; @@ -85,12 +87,12 @@ export class ResourceFileDetail extends React.Component { + 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/__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( Date: Tue, 30 Mar 2021 16:51:55 +0200 Subject: [PATCH 6/8] [Feature #1544] Remove attribute label from vocabulary info, remove "Annotator" from page title. --- src/component/annotator/Annotator.tsx | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/src/component/annotator/Annotator.tsx b/src/component/annotator/Annotator.tsx index 538cc982..d3ba8e08 100644 --- a/src/component/annotator/Annotator.tsx +++ b/src/component/annotator/Annotator.tsx @@ -29,7 +29,7 @@ import TermItState from "../../model/TermItState"; import User from "../../model/User"; import "./Annotator.scss"; import HeaderWithActions from "../misc/HeaderWithActions"; -import {Card, CardBody, CardHeader, Col, Label, Row} from "reactstrap"; +import {Card, CardBody, CardHeader} from "reactstrap"; import VocabularyIriLink from "../vocabulary/VocabularyIriLink"; import File from "../../model/File"; import TextAnalysisInvocationButton from "./TextAnalysisInvocationButton"; @@ -392,13 +392,12 @@ export class Annotator extends React.Component { }; public render() { - return <> - + - - {this.renderMetadata()} + + @@ -433,17 +432,6 @@ export class Annotator extends React.Component { } - private renderMetadata() { - return - - - - - - - ; - } - private generateVirtualPopperAnchor(): HTMLElement { // Based on https://popper.js.org/docs/v2/virtual-elements/ return HtmlDomUtils.generateVirtualElement(this.state.selectionPurposeDialogAnchorPosition.x, this.state.selectionPurposeDialogAnchorPosition.y); From a8ce4d7f99d625374484e36982a17858cfb1a279 Mon Sep 17 00:00:00 2001 From: Martin Ledvinka Date: Tue, 30 Mar 2021 14:33:46 +0200 Subject: [PATCH 7/8] [Feature #1545] Let user select text analysis vocabulary in the annotator. --- .../TextAnalysisInvocationButton.tsx | 86 ++++++++---- .../TextAnalysisInvocationButton.test.tsx | 125 +++++++++++++----- 2 files changed, 156 insertions(+), 55 deletions(-) diff --git a/src/component/annotator/TextAnalysisInvocationButton.tsx b/src/component/annotator/TextAnalysisInvocationButton.tsx index b96ce994..9adcccda 100644 --- a/src/component/annotator/TextAnalysisInvocationButton.tsx +++ b/src/component/annotator/TextAnalysisInvocationButton.tsx @@ -1,7 +1,10 @@ import * as React from "react"; +import {injectIntl} from "react-intl"; +import withI18n, {HasI18n} from "../hoc/withI18n"; +import withInjectableLoading, {InjectsLoading} from "../hoc/withInjectableLoading"; import {GoClippy} from "react-icons/go"; import {Button} from "reactstrap"; -import {useDispatch} from "react-redux"; +import {connect} from "react-redux"; import {ThunkDispatch} from "../../util/Types"; import {executeFileTextAnalysis} from "../../action/AsyncActions"; import {publishNotification} from "../../action/SyncActions"; @@ -9,39 +12,72 @@ import NotificationType from "../../model/NotificationType"; import ResourceSelectVocabulary from "../resource/ResourceSelectVocabulary"; import Vocabulary from "../../model/Vocabulary"; import {IRI} from "../../util/VocabularyUtils"; -import {useI18n} from "../hook/useI18n"; -interface TextAnalysisInvocationButtonProps { +interface TextAnalysisInvocationButtonProps extends HasI18n, InjectsLoading { + id?: string; fileIri: IRI; defaultVocabularyIri?: string; + executeTextAnalysis: (fileIri: IRI, vocabularyIri: string) => Promise; + notifyAnalysisFinish: () => void; className?: string } -const TextAnalysisInvocationButton: React.FC = props => { - const {fileIri, defaultVocabularyIri, className} = props; - const [showSelector, setShowSelector] = React.useState(false); - const dispatch: ThunkDispatch = useDispatch(); - const {i18n} = useI18n(); - const close = () => setShowSelector(false); - const submit = (voc: Vocabulary | null) => { - close(); - if (!voc) { +interface TextAnalysisInvocationButtonState { + showVocabularySelector: boolean; + +} + +export class TextAnalysisInvocationButton extends React.Component { + + constructor(props: InjectsLoading & TextAnalysisInvocationButtonProps) { + super(props); + this.state = {showVocabularySelector: false}; + } + + public onClick = () => { + this.setState({showVocabularySelector: true}); + }; + + private invokeTextAnalysis(fileIri: IRI, vocabularyIri: string) { + this.props.loadingOn(); + this.props.executeTextAnalysis(fileIri, vocabularyIri).then(() => { + this.props.loadingOff(); + this.props.notifyAnalysisFinish(); + }); + } + + public onVocabularySelect = (vocabulary: Vocabulary | null) => { + this.closeVocabularySelect(); + if (!vocabulary) { return; } - dispatch(executeFileTextAnalysis(fileIri, voc.iri)).then(() => dispatch(publishNotification({source: {type: NotificationType.TEXT_ANALYSIS_FINISHED}}))); + this.invokeTextAnalysis(this.props.fileIri, vocabulary.iri); }; - return <> - - - ; + private closeVocabularySelect = () => { + this.setState({showVocabularySelector: false}); + }; + + public render() { + const i18n = this.props.i18n; + return <> + + + ; + } } -export default TextAnalysisInvocationButton; +export default connect(undefined, (dispatch: ThunkDispatch) => { + return { + executeTextAnalysis: (fileIri: IRI, vocabularyIri: string) => dispatch(executeFileTextAnalysis(fileIri, vocabularyIri)), + notifyAnalysisFinish: () => dispatch(publishNotification({source: {type: NotificationType.TEXT_ANALYSIS_FINISHED}})) + }; +})(injectIntl(withI18n(withInjectableLoading(TextAnalysisInvocationButton)))); diff --git a/src/component/annotator/__tests__/TextAnalysisInvocationButton.test.tsx b/src/component/annotator/__tests__/TextAnalysisInvocationButton.test.tsx index 1df03175..5372b072 100644 --- a/src/component/annotator/__tests__/TextAnalysisInvocationButton.test.tsx +++ b/src/component/annotator/__tests__/TextAnalysisInvocationButton.test.tsx @@ -1,60 +1,125 @@ import * as React from "react"; -import VocabularyUtils from "../../../util/VocabularyUtils"; +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, mockUseI18n} from "../../../__tests__/environment/IntlUtil"; +import {TextAnalysisInvocationButton} from "../TextAnalysisInvocationButton"; +import {intlFunctions} from "../../../__tests__/environment/IntlUtil"; +import {InjectsLoading} from "../../hoc/withInjectableLoading"; import ResourceSelectVocabulary from "../../resource/ResourceSelectVocabulary"; import Vocabulary from "../../../model/Vocabulary"; -import {ThunkDispatch} from "../../../util/Types"; -import * as redux from "react-redux"; -import * as AsyncActions from "../../../action/AsyncActions"; -import * as SyncActions from "../../../action/SyncActions"; -import {Button} from "reactstrap"; describe("TextAnalysisInvocationButton", () => { - let vocabulary: Vocabulary; + const namespace = "http://onto.fel.cvut.cz/ontologies/termit/resources/"; + const fileName = "test.html"; + + let file: File; - let dispatch: ThunkDispatch; + let executeTextAnalysis: (fileIri: IRI, vocabularyIri?: string) => Promise; + let notifyAnalysisFinish: () => void; + let loadingOn: () => void; + let loadingOff: () => void; + let renderMask: () => JSX.Element | null; + + let loadingProps: InjectsLoading; + let vocabulary: Vocabulary; beforeEach(() => { + file = new File({ + iri: namespace + fileName, + label: fileName, + types: [VocabularyUtils.FILE, VocabularyUtils.RESOURCE] + }); + executeTextAnalysis = jest.fn().mockImplementation(() => Promise.resolve()); + notifyAnalysisFinish = jest.fn(); + loadingOn = jest.fn(); + loadingOff = jest.fn(); + renderMask = jest.fn(); + loadingProps = {loadingOff, loadingOn, renderMask, loading: false}; vocabulary = Generator.generateVocabulary(); - dispatch = jest.fn().mockResolvedValue({}); - jest.spyOn(redux, "useDispatch").mockReturnValue(dispatch); - jest.spyOn(AsyncActions, "executeFileTextAnalysis"); - jest.spyOn(SyncActions, "publishNotification"); - mockUseI18n(); + }); + + 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().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().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().onVocabularySelect(vocabulary); + return Promise.resolve().then(() => { + expect(loadingOff).toHaveBeenCalled(); + }); }); 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.find(ResourceSelectVocabulary).prop("onSubmit")(vocabulary); + const wrapper = shallow(); + wrapper.instance().onVocabularySelect(vocabulary); return Promise.resolve().then(() => { - expect(SyncActions.publishNotification).toHaveBeenCalled(); + expect(notifyAnalysisFinish).toHaveBeenCalled(); }); }); - it("shows vocabulary selector on button click", () => { + it("shows vocabulary selector when no default vocabulary was specified", () => { const fileIri = VocabularyUtils.create(Generator.generateUri()); - const wrapper = shallow(); - wrapper.find(Button).simulate("click"); + const wrapper = shallow(); + wrapper.instance().onClick(); wrapper.update(); + expect(wrapper.instance().state.showVocabularySelector).toBeTruthy(); expect(wrapper.exists(ResourceSelectVocabulary)); - expect(wrapper.find(ResourceSelectVocabulary).prop("show")).toBeTruthy(); + expect(executeTextAnalysis).not.toHaveBeenCalled(); }); it("invokes text analysis with selected Vocabulary when Vocabulary selector is submitted", () => { - const fileIri = VocabularyUtils.create(Generator.generateUri()); - const vocabularyIri = Generator.generateUri(); - vocabulary.iri = vocabularyIri; - const wrapper = shallow(); - wrapper.find(ResourceSelectVocabulary).prop("onSubmit")(vocabulary); - expect(AsyncActions.executeFileTextAnalysis).toHaveBeenCalledWith(fileIri, vocabularyIri) + const wrapper = shallow(); + wrapper.instance().onClick(); + wrapper.update(); + expect(wrapper.instance().state.showVocabularySelector).toBeTruthy(); + wrapper.instance().onVocabularySelect(vocabulary); + expect(executeTextAnalysis).toHaveBeenLastCalledWith(VocabularyUtils.create(file.iri), vocabulary.iri); + wrapper.update(); + expect(wrapper.instance().state.showVocabularySelector).toBeFalsy(); }); }); From f703bb0244b63b5ca27aa817a9fb092a619ea975 Mon Sep 17 00:00:00 2001 From: Martin Ledvinka Date: Thu, 1 Apr 2021 09:56:20 +0200 Subject: [PATCH 8/8] [Feature #1545] Improve vocabulary select dialog title to better communicate its purpose. --- src/component/annotator/TextAnalysisInvocationButton.tsx | 3 ++- src/component/resource/ResourceSelectVocabulary.tsx | 5 +++-- src/i18n/cs.ts | 1 + src/i18n/en.ts | 1 + 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/component/annotator/TextAnalysisInvocationButton.tsx b/src/component/annotator/TextAnalysisInvocationButton.tsx index 9adcccda..2c73b8e5 100644 --- a/src/component/annotator/TextAnalysisInvocationButton.tsx +++ b/src/component/annotator/TextAnalysisInvocationButton.tsx @@ -63,7 +63,8 @@ export class TextAnalysisInvocationButton extends React.Component + onCancel={this.closeVocabularySelect} onSubmit={this.onVocabularySelect} + title={i18n("file.metadata.startTextAnalysis.vocabularySelect.title")}/>