{
about: string,
annotationType: string
): { container: HTMLElement; annotation: Element } | null {
- const range = HtmlDomUtils.getSelectionRange();
+ const range = HtmlDomUtils.getSelectionRange()?.cloneRange();
if (!range) {
return null;
}
- HtmlDomUtils.extendRangeToPreventNodeCrossing(range);
+ if (annotationType === AnnotationType.DEFINITION) {
+ HtmlDomUtils.extendRangeToPreventNodeCrossing(range);
+ }
const rangeContent = HtmlDomUtils.getRangeContent(range);
const newAnnotationNode = AnnotationDomHelper.createNewAnnotation(
about,
@@ -726,6 +781,14 @@ export default connect(
dispatch(saveOccurrence(occurrence)),
removeTermOccurrence: (occurrence: AssetData) =>
dispatch(removeOccurrence(occurrence, true)),
+ setAnnotatorLegendFilter: (
+ annotationClass: AnnotationClass,
+ annotationOrigin: AnnotationOrigin,
+ enabled: boolean
+ ) =>
+ dispatch(
+ setAnnotatorLegendFilter(annotationClass, annotationOrigin, enabled)
+ ),
};
}
)(injectIntl(withI18n(Annotator)));
diff --git a/src/component/annotator/HtmlDomUtils.ts b/src/component/annotator/HtmlDomUtils.ts
index 2bd6465f3..4deba3a28 100644
--- a/src/component/annotator/HtmlDomUtils.ts
+++ b/src/component/annotator/HtmlDomUtils.ts
@@ -2,10 +2,11 @@ import { Node as DomHandlerNode } from "domhandler";
import Utils from "../../util/Utils";
import { TextQuoteSelector } from "../../model/TermOccurrence";
import { AnnotationType } from "./AnnotationDomHelper";
-import { fromRange, toRange } from "xpath-range";
+import { fromNode, toNode } from "simple-xpath-position";
import * as React from "react";
+import TextSelection from "./TextSelection";
-const BLOCK_ELEMENTS = [
+export const BLOCK_ELEMENTS = [
"address",
"article",
"aside",
@@ -41,31 +42,6 @@ const BLOCK_ELEMENTS = [
"ul",
];
-const PUNCTUATION_CHARS = [".", ",", "!", "?", ":", ";"];
-
-function calculatePathLength(node: Node, ancestor: Node): number {
- let parent = node.parentNode;
- let length = 0;
- while (parent && parent !== ancestor) {
- length++;
- parent = parent.parentNode;
- }
- return length;
-}
-
-/**
- * Detects if the specified selection is backwards.
- * @param sel Selection to check
- */
-function isBackwards(sel: Selection): boolean {
- const range = document.createRange();
- range.setStart(sel.anchorNode!, sel.anchorOffset);
- range.setEnd(sel.focusNode!, sel.focusOffset);
- const backwards = range.collapsed;
- range.detach();
- return backwards;
-}
-
export interface HtmlSplit {
prefix: string;
body: string;
@@ -134,62 +110,22 @@ const HtmlDomUtils = {
};
},
- extendSelectionToWords() {
+ /**
+ * Extends and trims the selection so as not to contain leading/trailing spaces or punctuation characters.
+ * @param container
+ */
+ extendSelectionToWords(container: Node) {
const sel = window.getSelection();
- if (
- sel &&
- !sel.isCollapsed &&
- sel.anchorNode &&
- sel.focusNode &&
- // @ts-ignore
- sel.modify
- ) {
- const backwards = isBackwards(sel);
- // modify() works on the focus of the selection
- const endNode = sel.focusNode;
- const endOffset = Math.max(0, sel.focusOffset - 1);
- sel.collapse(
- sel.anchorNode,
- backwards ? sel.anchorOffset : sel.anchorOffset + 1
- );
- if (backwards) {
- // Note that we are not using sel.modify by word due to issues with Firefox messing up the modification/extension
- // in certain situations (Bug #1610)
- const anchorText = sel.anchorNode.textContent || "";
- while (
- anchorText.charAt(sel.anchorOffset).trim().length !== 0 &&
- sel.anchorOffset < anchorText.length
- ) {
- // @ts-ignore
- sel.modify("move", "forward", "character");
- }
- const text = endNode.textContent || "";
- let index = endOffset;
- while (text.charAt(index).trim().length !== 0 && index >= 0) {
- index--;
- }
- sel.extend(endNode, index + 1);
- } else {
- // @ts-ignore
- sel.modify("move", "backward", "word");
- const text = endNode.textContent || "";
- let index = endOffset;
- while (
- !this.isWhitespaceOrPunctuation(text.charAt(index)) &&
- index < text.length
- ) {
- index++;
- }
- sel.extend(endNode, index);
- }
- }
- },
+ if (sel && !sel.isCollapsed && sel.rangeCount > 0) {
+ const selectionModifier = new TextSelection(sel, container);
- isWhitespaceOrPunctuation(character: string) {
- return (
- character.trim().length === 0 ||
- PUNCTUATION_CHARS.indexOf(character) !== -1
- );
+ selectionModifier.adjustStart();
+ selectionModifier.adjustEnd();
+ selectionModifier.restoreSelection();
+
+ // Note that we are not using sel.modify by word due to issues with Firefox messing up the modification/extension
+ // in certain situations (Bug #1610)
+ }
},
/**
@@ -200,12 +136,15 @@ const HtmlDomUtils = {
*/
doesRangeSpanMultipleElements(range: Range): boolean {
return (
- range.startContainer !== range.endContainer &&
- calculatePathLength(
- range.startContainer,
- range.commonAncestorContainer
- ) !==
- calculatePathLength(range.endContainer, range.commonAncestorContainer)
+ // either they have the same parent node
+ // and they are different nodes
+ (range.startContainer.parentNode === range.endContainer.parentNode &&
+ range.startContainer !== range.endContainer &&
+ // and one of them is not a text node
+ (range.startContainer.nodeType !== Node.TEXT_NODE ||
+ range.endContainer.nodeType !== Node.TEXT_NODE)) ||
+ // or they have different parent
+ range.startContainer.parentNode !== range.endContainer.parentNode
);
},
@@ -214,12 +153,38 @@ const HtmlDomUtils = {
*
* This extension should handle situations when the range starts in one element and ends in another, which would
* prevent its replacement/annotation due to invalid element boundary crossing. This method attempts to fix this by
- * extending the range to be the contents of the closest common ancestor of the range's start and end containers.
+ * extending the range to contain both starting and ending elements.
* @param range Range to fix
*/
- extendRangeToPreventNodeCrossing(range: Range) {
+ extendRangeToPreventNodeCrossing(range: Range): void {
if (this.doesRangeSpanMultipleElements(range)) {
- range.selectNodeContents(range.commonAncestorContainer);
+ const startingChild = findLastParentBefore(
+ range.startContainer,
+ range.commonAncestorContainer
+ );
+ const endingChild = findLastParentBefore(
+ range.endContainer,
+ range.commonAncestorContainer
+ );
+
+ if (startingChild !== range.commonAncestorContainer) {
+ range.setStartBefore(startingChild);
+ } else {
+ range.setStart(range.commonAncestorContainer, 0);
+ }
+ if (endingChild !== range.commonAncestorContainer) {
+ range.setEndAfter(endingChild);
+ } else {
+ let offset = 0;
+ // just to be sure
+ if (range.commonAncestorContainer.hasChildNodes()) {
+ offset = range.commonAncestorContainer.childNodes.length - 1;
+ } else {
+ const text = range.commonAncestorContainer.textContent || "";
+ offset = text.length - 1;
+ }
+ range.setEnd(range.commonAncestorContainer, offset);
+ }
}
},
@@ -234,22 +199,21 @@ const HtmlDomUtils = {
range: Range,
surroundingElementHtml: string
): HTMLElement {
- const xpathRange = fromRange(range, rootElement);
+ const startXpath = fromNode(range.startContainer, rootElement) || ".";
+ const endXpath = fromNode(range.endContainer, rootElement) || ".";
const clonedElement = rootElement.cloneNode(true) as HTMLElement;
- const newRange = toRange(
- xpathRange.start,
- xpathRange.startOffset,
- xpathRange.end,
- range.endContainer.nodeType === Node.TEXT_NODE ? xpathRange.endOffset : 0,
- clonedElement
- );
- // This works around the issue that the toRange considers the offsets as textual characters, but if the end container is
- // not a text node, the offset represents the number of elements before it and thus the offset in the newRange would be incorrect
- // See https://developer.mozilla.org/en-US/docs/Web/API/Range/endOffset and in contrast the docs to toRange
- if (range.endContainer.nodeType !== Node.TEXT_NODE) {
- newRange.setEnd(newRange.endContainer, range.endOffset);
+
+ const startElement: Node | null = toNode(startXpath, clonedElement);
+ const endElement: Node | null = toNode(endXpath, clonedElement);
+
+ if (!startElement || !endElement) {
+ throw new Error("Unable to resolve selected range");
}
+ const newRange = new Range();
+ newRange.setStart(startElement, range.startOffset);
+ newRange.setEnd(endElement, range.endOffset);
+
const doc = clonedElement.ownerDocument;
const template = doc!.createElement("template");
template.innerHTML = surroundingElementHtml;
@@ -394,6 +358,14 @@ const HtmlDomUtils = {
},
};
+function findLastParentBefore(child: Node, parent: Node) {
+ let node = child;
+ while (node && node.parentNode && node.parentNode !== parent) {
+ node = node.parentNode;
+ }
+ return node;
+}
+
function prefixMatch(selector: TextQuoteSelector, element: Element) {
return (
selector.prefix &&
diff --git a/src/component/annotator/TextAnalysisInvocationButton.tsx b/src/component/annotator/TextAnalysisInvocationButton.tsx
index c2efa6d2b..606deaf9e 100644
--- a/src/component/annotator/TextAnalysisInvocationButton.tsx
+++ b/src/component/annotator/TextAnalysisInvocationButton.tsx
@@ -1,21 +1,22 @@
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 { 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";
+import { IRI, IRIImpl } from "../../util/VocabularyUtils";
+import { IMessage, withSubscription } from "react-stomp-hooks";
+import Constants from "../../util/Constants";
+import { publishMessage, publishNotification } from "../../action/SyncActions";
+import NotificationType from "../../model/NotificationType";
+import Message from "../../model/Message";
+import MessageType from "../../model/MessageType";
-interface TextAnalysisInvocationButtonProps extends HasI18n, InjectsLoading {
+interface TextAnalysisInvocationButtonProps extends HasI18n {
id?: string;
fileIri: IRI;
defaultVocabularyIri?: string;
@@ -32,7 +33,7 @@ export class TextAnalysisInvocationButton extends React.Component<
TextAnalysisInvocationButtonProps,
TextAnalysisInvocationButtonState
> {
- constructor(props: InjectsLoading & TextAnalysisInvocationButtonProps) {
+ constructor(props: TextAnalysisInvocationButtonProps) {
super(props);
this.state = { showVocabularySelector: false };
}
@@ -42,11 +43,7 @@ export class TextAnalysisInvocationButton extends React.Component<
};
private invokeTextAnalysis(fileIri: IRI, vocabularyIri: string) {
- this.props.loadingOn();
- this.props.executeTextAnalysis(fileIri, vocabularyIri).then(() => {
- this.props.loadingOff();
- this.props.notifyAnalysisFinish();
- });
+ this.props.executeTextAnalysis(fileIri, vocabularyIri);
}
public onVocabularySelect = (vocabulary: Vocabulary | null) => {
@@ -61,6 +58,18 @@ export class TextAnalysisInvocationButton extends React.Component<
this.setState({ showVocabularySelector: false });
};
+ public onMessage(message: IMessage) {
+ if (!message?.body) {
+ return;
+ }
+ if (
+ message.body.substring(1, message.body.length - 1) ===
+ IRIImpl.toString(this.props.fileIri)
+ ) {
+ this.props.notifyAnalysisFinish();
+ }
+ }
+
public render() {
const i18n = this.props.i18n;
return (
@@ -92,11 +101,31 @@ export default connect(undefined, (dispatch: ThunkDispatch) => {
return {
executeTextAnalysis: (fileIri: IRI, vocabularyIri: string) =>
dispatch(executeFileTextAnalysis(fileIri, vocabularyIri)),
- notifyAnalysisFinish: () =>
+ notifyAnalysisFinish: () => {
+ dispatch(
+ publishMessage(
+ new Message(
+ {
+ messageId: "file.text-analysis.finished.message",
+ },
+ MessageType.SUCCESS
+ )
+ )
+ );
dispatch(
publishNotification({
source: { type: NotificationType.TEXT_ANALYSIS_FINISHED },
})
- ),
+ );
+ },
};
-})(injectIntl(withI18n(withInjectableLoading(TextAnalysisInvocationButton))));
+})(
+ injectIntl(
+ withI18n(
+ withSubscription(
+ TextAnalysisInvocationButton,
+ Constants.WEBSOCKET_ENDPOINT.VOCABULARIES_TEXT_ANALYSIS_FINISHED_FILE
+ )
+ )
+ )
+);
diff --git a/src/component/annotator/TextSelection.ts b/src/component/annotator/TextSelection.ts
new file mode 100644
index 000000000..d9387d972
--- /dev/null
+++ b/src/component/annotator/TextSelection.ts
@@ -0,0 +1,366 @@
+export const PUNCTUATION_CHARS = [".", ",", "!", "?", ":", ";"];
+const PUNCTUATION_REGEX = PUNCTUATION_CHARS.map(escapeRegExp).join("");
+
+/**
+ * Allows manipulation with the first {@link Range} in the {@link Selection}.
+ *
+ * Every action manipulates the inner range, to apply the changes to the selection
+ * use {@link TextSelection#restoreSelection()} method.
+ *
+ * Using the Range has the advantage that there is no need to deal with the direction of selection,
+ * the beginning of the range is always before the end of the range.
+ * @see https://javascript.info/selection-range#selection
+ */
+export default class TextSelection {
+ private readonly selection: Selection;
+ private readonly range: Range;
+ private readonly container: Node;
+
+ /**
+ * @param selection The {@link Selection}
+ * @param container the selection never extends beyond it
+ * @see the Class {@link TextSelection}
+ * @throws Error when selection
is undefined, there is nothing selected (no range inside the selection) or the selection is collapsed
+ */
+ constructor(selection: Selection, container: Node) {
+ if (!selection || selection.rangeCount === 0 || selection.isCollapsed) {
+ throw new Error("Invalid selection");
+ }
+
+ this.selection = selection;
+ this.container = container;
+ // currently no browser other than firefox supports multiple range selection
+ // and in termit it does not make any sense
+ this.range = selection.getRangeAt(0).cloneRange();
+ }
+
+ /**
+ * Removes whitespace and {@link PUNCTUATION_CHARS} characters from the string start
+ */
+ private trimStringLeft(str: string) {
+ return str
+ .trimStart()
+ .replace(new RegExp(`^[${PUNCTUATION_REGEX}\\s)]+`, "g"), "");
+ }
+
+ /**
+ * Removes whitespace and {@link PUNCTUATION_CHARS} characters from the string end
+ */
+ private trimStringRight(str: string) {
+ return str
+ .trimEnd()
+ .replace(new RegExp(`[${PUNCTUATION_REGEX}\\s(]+$`, "g"), "");
+ }
+
+ /**
+ * Clears the selection and applies the range inside this object.
+ */
+ restoreSelection() {
+ this.selection.removeAllRanges();
+ this.selection.addRange(this.range.cloneRange());
+ }
+
+ /**
+ * Sets {@link #range#startContainer} to a text node when possible
+ */
+ private diveStartToTextNode() {
+ const it = createIteratorAtChild(this.container, this.range.startContainer);
+ let node;
+ do {
+ node = it.nextNode();
+ } while (node && node.nodeType !== Node.TEXT_NODE);
+ if (node?.nodeType === Node.TEXT_NODE) {
+ let offset = this.range.startOffset;
+ if (node !== this.range.startContainer) {
+ offset = 0;
+ }
+ this.range.setStart(node, offset);
+ }
+ }
+
+ /**
+ * Sets {@link #range#endContainer} to a text node when possible
+ */
+ private diveEndToTextNode() {
+ const it = createIteratorAtChild(this.container, this.range.endContainer);
+ let node;
+ it.nextNode(); // include current node
+ do {
+ node = it.previousNode();
+ } while (node && node.nodeType !== Node.TEXT_NODE);
+ if (node?.nodeType === Node.TEXT_NODE) {
+ let offset = this.range.endOffset;
+ if (node !== this.range.endContainer) {
+ offset = node.textContent?.length || 0;
+ }
+ this.range.setEnd(node, offset);
+ }
+ }
+
+ /**
+ * Moves {@link #range#startContainer} to the next text node,
+ * if there is no text node available, the node is unchanged
+ * @returns true if node was changed, false otherwise
+ */
+ private moveStartToNextTextNode() {
+ const it = createIteratorAtChild(this.container, this.range.startContainer);
+ it.nextNode(); // skip current child (startContainer)
+ let node;
+ do {
+ node = it.nextNode();
+ } while (node && node.nodeType !== Node.TEXT_NODE);
+ if (node?.nodeType === Node.TEXT_NODE) {
+ this.range.setStart(node, 0);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Moves {@link #range#endContainer} to the next text node,
+ * if there is no text node available, the node is unchanged
+ * @returns true if node was changed, false otherwise
+ */
+ private moveEndToNextTextNode() {
+ const it = createIteratorAtChild(this.container, this.range.endContainer);
+ it.nextNode(); // skip current child (endContainer)
+ let node;
+ do {
+ node = it.nextNode();
+ } while (node && node.nodeType !== Node.TEXT_NODE);
+ if (node?.nodeType === Node.TEXT_NODE) {
+ this.range.setEnd(node, 0);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Moves {@link #range#startContainer} to the prev text node,
+ * if there is no text node available, the node is unchanged
+ * @returns true if node was changed, false otherwise
+ */
+ private moveStartToPrevTextNode() {
+ const it = createIteratorAtChild(this.container, this.range.startContainer);
+ let node;
+ do {
+ node = it.previousNode();
+ } while (node && node.nodeType !== Node.TEXT_NODE);
+ if (node?.nodeType === Node.TEXT_NODE) {
+ const offset = node.textContent?.length || 0;
+ this.range.setStart(node, offset);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Moves {@link #range#endContainer} to the prev text node,
+ * if there is no text node available, the node is unchanged
+ * @returns true if node was changed, false otherwise
+ */
+ private moveEndToPrevTextNode() {
+ const it = createIteratorAtChild(this.container, this.range.endContainer);
+ let node;
+ do {
+ node = it.previousNode();
+ } while (node && node.nodeType !== Node.TEXT_NODE);
+ if (node?.nodeType === Node.TEXT_NODE) {
+ const offset = node.textContent?.length || 0;
+ this.range.setEnd(node, offset);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * @returns string A part of the text in {@link #range#startContainer} respecting the {@link #range#startOffset}
+ */
+ private getStartOffsetText() {
+ const { startContainer, startOffset } = this.range;
+ const text = startContainer.textContent || "";
+ return text.slice(startOffset);
+ }
+
+ /**
+ * @returns string A whole text in {@link #range#startContainer} or empty string
+ */
+ private getStartText() {
+ return this.range.startContainer.textContent || "";
+ }
+
+ /**
+ * @returns string A whole text in {@link #range#endContainer} or empty string
+ */
+ private getEndText() {
+ return this.range.endContainer.textContent || "";
+ }
+
+ /**
+ * @returns string A part of the text in {@link #range#endContainer} respecting the {@link #range#endOffset}
+ */
+ private getEndOffsetText() {
+ const { endContainer, endOffset } = this.range;
+ const text = endContainer.textContent || "";
+ return text.slice(0, endOffset);
+ }
+
+ /**
+ * Moves the start of the {@link #range} to the left as long as there is no whitespace/punctuation
+ * @see the trim method {@link #trimStringLeft}
+ */
+ private extendStart() {
+ this.diveStartToTextNode();
+ let oldContainer = this.range.startContainer;
+ let oldOffset = this.range.startOffset;
+ while (
+ this.getStartOffsetText().length ===
+ this.trimStringLeft(this.getStartOffsetText()).length
+ ) {
+ oldContainer = this.range.startContainer;
+ oldOffset = this.range.startOffset;
+ const { startContainer, startOffset } = this.range;
+ const newOffset = startOffset - 1;
+ if (newOffset < 0) {
+ if (this.moveStartToPrevTextNode()) {
+ continue;
+ } else {
+ break;
+ }
+ }
+ this.range.setStart(startContainer, newOffset);
+ }
+ // restore last action
+ this.range.setStart(oldContainer, oldOffset);
+ }
+
+ /**
+ * Moves the end of the {@link #range} to the right as long as there is no whitespace/punctuation
+ * @see the trim method {@link #trimStringRight}
+ */
+ private extendEnd() {
+ this.diveEndToTextNode();
+ let oldContainer = this.range.endContainer;
+ let oldOffset = this.range.endOffset;
+ while (
+ this.getEndOffsetText().length ===
+ this.trimStringRight(this.getEndOffsetText()).length
+ ) {
+ oldContainer = this.range.endContainer;
+ oldOffset = this.range.endOffset;
+ const { endContainer, endOffset } = this.range;
+ const newOffset = endOffset + 1;
+ if (newOffset >= this.getEndText().length) {
+ if (this.moveEndToNextTextNode()) {
+ continue;
+ } else {
+ break;
+ }
+ }
+ this.range.setEnd(endContainer, newOffset);
+ }
+ // restore last action
+ this.range.setEnd(oldContainer, oldOffset);
+ }
+
+ /**
+ * Moves the start of the {@link #range} to the right as long as there is a whitespace/punctuation
+ * @see the trim method {@link #trimStringLeft}
+ */
+ private trimStart() {
+ this.diveStartToTextNode();
+ while (
+ this.trimStringLeft(this.getStartOffsetText()).length !==
+ this.getStartOffsetText().length ||
+ this.getStartOffsetText().length === 0
+ ) {
+ const { startContainer, startOffset } = this.range;
+ const newOffset = startOffset + 1;
+ if (newOffset >= this.getStartText().length) {
+ if (this.moveStartToNextTextNode()) {
+ continue;
+ } else {
+ break;
+ }
+ }
+ this.range.setStart(startContainer, newOffset);
+ }
+ }
+
+ /**
+ * Moves the end of the {@link #range} to the right as long as there is a whitespace/punctuation
+ * @see the trim method {@link #trimStringRight}
+ */
+ private trimEnd() {
+ this.diveEndToTextNode();
+ while (
+ this.trimStringRight(this.getEndOffsetText()).length !==
+ this.getEndOffsetText().length ||
+ this.getEndOffsetText().length === 0
+ ) {
+ const { endContainer, endOffset } = this.range;
+ const newOffset = endOffset - 1;
+ if (newOffset < 0) {
+ if (this.moveEndToPrevTextNode()) {
+ continue;
+ } else {
+ break;
+ }
+ }
+ this.range.setEnd(endContainer, newOffset);
+ }
+ }
+
+ /**
+ * Trims or extends the start of the range to remove leading spaces/punctuation, or to contain whole word
+ * @see punctuation constant {@link PUNCTUATION_CHARS}
+ */
+ adjustStart() {
+ if (
+ this.trimStringLeft(this.getStartOffsetText()).length !==
+ this.getStartOffsetText().length ||
+ this.getStartOffsetText().length === 0
+ ) {
+ this.trimStart();
+ } else {
+ this.extendStart();
+ this.trimStart();
+ }
+ }
+
+ /**
+ * Trims or extends the end of the range to remove trailing spaces/punctuation, or to contain whole word
+ * @see punctuation constant {@link PUNCTUATION_CHARS}
+ */
+ adjustEnd() {
+ if (
+ this.trimStringRight(this.getEndOffsetText()).length !==
+ this.getEndOffsetText().length ||
+ this.getEndOffsetText().length === 0
+ ) {
+ this.trimEnd();
+ } else {
+ this.extendEnd();
+ this.trimEnd();
+ }
+ }
+}
+
+/**
+ * @returns an iterator {@link NodeIterator} at the child position (calling nextNode will return the child)
+ * @param parent parent node of the child
+ * @param child Node that is a child of the parent
+ */
+function createIteratorAtChild(parent: Node, child: Node) {
+ const it = document.createNodeIterator(parent);
+ let node;
+ do {
+ node = it.nextNode();
+ } while (node && node !== child);
+ it.previousNode();
+ return it;
+}
+
+function escapeRegExp(str: string): string {
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
+}
diff --git a/src/component/annotator/__tests__/Annotation.test.tsx b/src/component/annotator/__tests__/Annotation.test.tsx
index 2a9d163dd..e167793c0 100644
--- a/src/component/annotator/__tests__/Annotation.test.tsx
+++ b/src/component/annotator/__tests__/Annotation.test.tsx
@@ -12,6 +12,7 @@ import TermOccurrenceAnnotation from "../TermOccurrenceAnnotation";
import { MemoryRouter } from "react-router-dom";
import Generator from "../../../__tests__/environment/Generator";
import { langString } from "../../../model/MultilingualString";
+import AnnotatorLegendFilter from "../../../model/AnnotatorLegendFilter";
function assumeProps(
wrapper: ReactWrapper,
@@ -48,11 +49,15 @@ describe("Annotation", () => {
text,
};
let assignedOccProps: any;
+ let filter: AnnotatorLegendFilter;
// @ts-ignore
const popupComponentClass: ComponentClass = SimplePopupWithActions;
let mockedFunctions: {
- onFetchTerm: (termIri: string) => Promise;
+ onFetchTerm: (
+ termIri: string,
+ abortController: AbortController
+ ) => Promise;
onCreateTerm: (label: string, annotation: AnnotationSpanProps) => void;
onResetSticky: () => void;
onUpdate: (annotation: AnnotationSpanProps, term: Term | null) => void;
@@ -69,6 +74,7 @@ describe("Annotation", () => {
onResetSticky: jest.fn(),
onUpdate: jest.fn(),
};
+ filter = new AnnotatorLegendFilter();
});
/* --- recognizes occurrence --- */
@@ -78,6 +84,7 @@ describe("Annotation", () => {
{...mockedFunctions}
{...intlFunctions()}
{...suggestedOccProps}
+ filter={filter}
/>
);
@@ -107,7 +114,8 @@ describe("Annotation", () => {
/>
);
expect(mockedFunctions.onFetchTerm).toHaveBeenCalledWith(
- assignedOccProps.resource
+ assignedOccProps.resource,
+ expect.any(AbortController)
);
});
@@ -122,7 +130,10 @@ describe("Annotation", () => {
const newResource = Generator.generateUri();
wrapper.setProps({ resource: newResource });
wrapper.update();
- expect(mockedFunctions.onFetchTerm).toHaveBeenCalledWith(newResource);
+ expect(mockedFunctions.onFetchTerm).toHaveBeenCalledWith(
+ newResource,
+ expect.any(AbortController)
+ );
});
it("recognizes invalid occurrence", () => {
diff --git a/src/component/annotator/__tests__/Annotator.test.tsx b/src/component/annotator/__tests__/Annotator.test.tsx
index 9a20ffb8f..31af1e035 100644
--- a/src/component/annotator/__tests__/Annotator.test.tsx
+++ b/src/component/annotator/__tests__/Annotator.test.tsx
@@ -1,6 +1,7 @@
import {
mockWindowSelection,
mountWithIntl,
+ withWebSocket,
} from "../../../__tests__/environment/Environment";
import { Element } from "domhandler";
import { AnnotationSpanProps, Annotator } from "../Annotator";
@@ -30,6 +31,11 @@ import Vocabulary from "../../../model/Vocabulary";
import AccessLevel from "../../../model/acl/AccessLevel";
import { MemoryRouter } from "react-router";
import { AssetData } from "../../../model/Asset";
+import {
+ AnnotationClass,
+ AnnotationOrigin,
+} from "../../../model/AnnotatorLegendFilter";
+import { AnnotatorLegendFilterAction } from "../../../action/ActionType";
jest.mock("../../misc/AssetIriLink", () => () => AssetIriLink);
jest.mock("../HighlightTermOccurrencesButton", () => () => (
@@ -54,6 +60,11 @@ describe("Annotator", () => {
approveTermOccurrence: (occurrence: AssetData) => Promise;
removeTermOccurrence: (occurrence: AssetData) => Promise;
saveTermOccurrence: (occurrence: TermOccurrence) => Promise;
+ setAnnotatorLegendFilter: (
+ annotationClass: AnnotationClass,
+ annotationOrigin: AnnotationOrigin,
+ enabled: boolean
+ ) => AnnotatorLegendFilterAction;
};
let user: User;
let file: File;
@@ -73,6 +84,7 @@ describe("Annotator", () => {
approveTermOccurrence: jest.fn().mockResolvedValue({}),
removeTermOccurrence: jest.fn().mockResolvedValue({}),
saveTermOccurrence: jest.fn().mockResolvedValue({}),
+ setAnnotatorLegendFilter: jest.fn().mockResolvedValue({}),
};
user = Generator.generateUser();
file = new File({
@@ -89,16 +101,18 @@ describe("Annotator", () => {
it("renders body of provided html content", () => {
const wrapper = mountWithIntl(
-
-
-
+ withWebSocket(
+
+
+
+ )
);
expect(wrapper.html().includes(sampleContent)).toBe(true);
@@ -110,16 +124,18 @@ describe("Annotator", () => {
);
const wrapper = mountWithIntl(
-
-
-
+ withWebSocket(
+
+
+
+ )
);
const sampleOutput =
'This is a link';
@@ -131,16 +147,18 @@ describe("Annotator", () => {
createAnnotation(suggestedOccProps, "města")
);
const wrapper = mountWithIntlAttached(
-
-
-
+ withWebSocket(
+
+
+
+ )
);
const constructedAnnProps = wrapper.find(Annotation).props();
@@ -476,7 +494,19 @@ describe("Annotator", () => {
startContainer: container,
endContainer: container,
commonAncestorContainer: container,
+ cloneRange: function () {
+ return Object.assign({}, this);
+ },
+ setStart: function (node: Node, offset: number) {
+ this.startContainer = node;
+ this.startOffset = offset;
+ },
+ setEnd: function (node: Node, offset: number) {
+ this.endContainer = node;
+ this.endOffset = offset;
+ },
};
+
HtmlDomUtils.getSelectionRange = jest.fn().mockReturnValue(range);
});
@@ -485,22 +515,26 @@ describe("Annotator", () => {
isCollapsed: false,
rangeCount: 1,
getRangeAt: () => range,
+ removeAllRanges: () => null,
+ addRange: (r: Range) => (range = r),
});
window.getComputedStyle = jest.fn().mockReturnValue({
getPropertyValue: () => "16px",
});
HtmlDomUtils.isInPopup = jest.fn().mockReturnValue(false);
const wrapper = mountWithIntl(
-
-
-
+ withWebSocket(
+
+
+
+ )
);
wrapper.find("#annotator").simulate("mouseUp");
wrapper.update();
@@ -512,16 +546,18 @@ describe("Annotator", () => {
getPropertyValue: () => "16px",
});
const wrapper = mountWithIntl(
-
-
-
+ withWebSocket(
+
+
+
+ )
);
wrapper.find("#annotator").simulate("mouseUp");
expect(wrapper.find(SelectionPurposeDialog).props().show).toBeTruthy();
@@ -573,6 +609,9 @@ describe("Annotator", () => {
startContainer: container,
endContainer: container,
commonAncestorContainer: container,
+ cloneRange: function () {
+ return Object.assign({}, this);
+ },
};
});
@@ -621,6 +660,9 @@ describe("Annotator", () => {
startContainer: container,
endContainer: container,
commonAncestorContainer: container,
+ cloneRange: function () {
+ return Object.assign({}, this);
+ },
};
});
diff --git a/src/component/annotator/__tests__/HtmlDomUtils.test.ts b/src/component/annotator/__tests__/HtmlDomUtils.test.ts
index dc975d27b..7facc5cb5 100644
--- a/src/component/annotator/__tests__/HtmlDomUtils.test.ts
+++ b/src/component/annotator/__tests__/HtmlDomUtils.test.ts
@@ -1,5 +1,3 @@
-// @ts-ignore
-import { fromRange, toRange, XPathRange } from "xpath-range";
import HtmlDomUtils from "../HtmlDomUtils";
import { mockWindowSelection } from "../../../__tests__/environment/Environment";
import VocabularyUtils from "../../../util/VocabularyUtils";
@@ -7,10 +5,12 @@ import { TextQuoteSelector } from "../../../model/TermOccurrence";
import Generator from "../../../__tests__/environment/Generator";
import { NodeWithChildren, Text as DomHandlerText } from "domhandler";
import { ElementType } from "domelementtype";
+// @ts-ignore
+import { fromNode, toNode } from "simple-xpath-position";
-jest.mock("xpath-range", () => ({
- fromRange: jest.fn(),
- toRange: jest.fn(),
+jest.mock("simple-xpath-position", () => ({
+ toNode: jest.fn(),
+ fromNode: jest.fn(),
}));
describe("Html dom utils", () => {
@@ -19,7 +19,7 @@ describe("Html dom utils", () => {
const sampleDivContent =
"before divbefore spansample text pointer in spanafter span
after div";
const surroundingElementHtml = "text pointer";
- const xpathTextPointerRange: XPathRange = {
+ const xpathTextPointerRange = {
start: "/div[1]/span[1]/text()[1]",
end: "/div[1]/span[1]/text()[1]",
startOffset: 7,
@@ -32,6 +32,8 @@ describe("Html dom utils", () => {
let cloneContents: () => DocumentFragment;
let textPointerRange: any;
beforeEach(() => {
+ jest.resetAllMocks();
+
// // @ts-ignore
window.getSelection = jest.fn().mockImplementation(() => {
return {
@@ -119,58 +121,66 @@ describe("Html dom utils", () => {
describe("replace range", () => {
it("returns clone of input element", () => {
let ret: HTMLElement | null;
- (fromRange as jest.Mock).mockImplementation(() => {
- return xpathTextPointerRange;
- });
+ // start and end element is the same span node
+ (fromNode as jest.Mock).mockReturnValue(xpathTextPointerRange.start);
- (toRange as jest.Mock).mockImplementation(() => {
- return textPointerRange;
+ (toNode as jest.Mock).mockImplementation((path: string, root: Node) => {
+ return root.childNodes[1].childNodes[1].childNodes[0]; // span
});
- textPointerRange.endContainer = {
- nodeType: Node.TEXT_NODE,
- };
+
+ textPointerRange = Object.assign(
+ { startContainer: {}, endContainer: {} },
+ xpathTextPointerRange,
+ textPointerRange
+ );
+
ret = HtmlDomUtils.replaceRange(
sampleDiv,
textPointerRange,
surroundingElementHtml
);
- expect(fromRange).toHaveBeenCalledWith(expect.any(Object), sampleDiv);
- expect(toRange).toHaveBeenCalledWith(
- xpathTextPointerRange.start,
- xpathTextPointerRange.startOffset,
- xpathTextPointerRange.end,
- xpathTextPointerRange.endOffset,
- expect.any(Object)
+
+ expect(fromNode).toHaveBeenNthCalledWith(
+ 1,
+ textPointerRange.startContainer,
+ sampleDiv
+ );
+ expect(fromNode).toHaveBeenNthCalledWith(
+ 2,
+ textPointerRange.endContainer,
+ sampleDiv
);
+ expect(fromNode).toHaveBeenCalledTimes(2);
+
+ expect(toNode).toHaveBeenCalled();
expect(ret).not.toBe(sampleDiv);
- expect(ret.children[0].childNodes[0].nodeValue).toEqual(
- sampleDiv.children[0].childNodes[0].nodeValue
+ expect((ret.children[0].childNodes[1] as HTMLElement).innerHTML).toEqual(
+ "sample text pointer in span"
);
});
- // Bug #1564
- it("uses original range end offset to work around offsetting issues when range end container is not a text node", () => {
- (fromRange as jest.Mock).mockImplementation(() => {
- return xpathTextPointerRange;
+ it("detects when a node has childrens and uses the offset correctly", () => {
+ (fromNode as jest.Mock).mockReturnValue(xpathTextPointerRange.start);
+ (toNode as jest.Mock).mockImplementation((path: string, root: Node) => {
+ return root.childNodes[1]; // div
});
- (toRange as jest.Mock).mockImplementation(() => {
- return textPointerRange;
- });
- const originalRange: any = {
- endContainer: {
- nodeType: Node.ELEMENT_NODE,
- },
- endOffset: 10,
- };
- HtmlDomUtils.replaceRange(
+ const originalRange = new Range();
+ // a div element, range staring before second div child (span)
+ originalRange.setStart(sampleDiv.children[0], 1);
+ // a div element, range ending before third div child (text node after the span)
+ originalRange.setEnd(sampleDiv.children[0], 2);
+
+ const ret = HtmlDomUtils.replaceRange(
sampleDiv,
originalRange,
surroundingElementHtml
);
- expect(textPointerRange.setEnd).toHaveBeenCalledWith(
- textPointerRange.endContainer,
- originalRange.endOffset
+
+ expect(toNode).toHaveBeenCalledTimes(2);
+ expect(fromNode).toHaveBeenCalledTimes(2);
+ expect(ret.children[0].innerHTML).toEqual(
+ "before spantext pointerafter span"
);
});
});
@@ -202,6 +212,23 @@ describe("Html dom utils", () => {
).toBeFalsy();
});
+ it("returns true for range spanning between two nested siblings", () => {
+ const container = document.createElement("div");
+ container.innerHTML =
+ "before allbefore firstfirst spanafterFirst
middlebefore secondsecond spanafter second
after all";
+
+ const range: any = {
+ startContainer: container.children[0].childNodes[1],
+ startOffset: 0,
+ endContainer: container.children[1].childNodes[1],
+ endOffset: 5,
+ commonAncestorContainer: container,
+ };
+ expect(
+ HtmlDomUtils.doesRangeSpanMultipleElements(range as Range)
+ ).toBeTruthy();
+ });
+
it("returns true for range spanning two elements", () => {
const range: any = {
startContainer:
diff --git a/src/component/annotator/__tests__/TextAnalysisInvocationButton.test.tsx b/src/component/annotator/__tests__/TextAnalysisInvocationButton.test.tsx
index 6cfa7bc27..2a65bdd44 100644
--- a/src/component/annotator/__tests__/TextAnalysisInvocationButton.test.tsx
+++ b/src/component/annotator/__tests__/TextAnalysisInvocationButton.test.tsx
@@ -7,6 +7,7 @@ import { intlFunctions } from "../../../__tests__/environment/IntlUtil";
import { InjectsLoading } from "../../hoc/withInjectableLoading";
import ResourceSelectVocabulary from "../../resource/ResourceSelectVocabulary";
import Vocabulary from "../../../model/Vocabulary";
+import { webSocketProviderWrappingComponentOptions } from "../../../__tests__/environment/Environment";
describe("TextAnalysisInvocationButton", () => {
const namespace = "http://onto.fel.cvut.cz/ontologies/termit/resources/";
@@ -59,64 +60,6 @@ describe("TextAnalysisInvocationButton", () => {
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.instance().onVocabularySelect(vocabulary);
- return Promise.resolve().then(() => {
- expect(notifyAnalysisFinish).toHaveBeenCalled();
- });
- });
-
it("shows vocabulary selector when no default vocabulary was specified", () => {
const fileIri = VocabularyUtils.create(Generator.generateUri());
const wrapper = shallow(
@@ -126,7 +69,8 @@ describe("TextAnalysisInvocationButton", () => {
notifyAnalysisFinish={notifyAnalysisFinish}
{...loadingProps}
{...intlFunctions()}
- />
+ />,
+ webSocketProviderWrappingComponentOptions
);
wrapper.instance().onClick();
wrapper.update();
diff --git a/src/component/asset/RemoveAssetDialog.tsx b/src/component/asset/RemoveAssetDialog.tsx
index 1f75e5bd7..26f6e59c0 100644
--- a/src/component/asset/RemoveAssetDialog.tsx
+++ b/src/component/asset/RemoveAssetDialog.tsx
@@ -35,6 +35,7 @@ const RemoveAssetDialog: React.FC = (props) => {
label: props.asset.getLabel(),
})}
confirmKey="remove"
+ confirmColor="outline-danger"
>
)}
= (props) => {
@@ -72,8 +80,15 @@ const Tabs: React.FC = (props) => {
return (
-
- {tabs}
+
+
+ {tabs}
+
);
};
diff --git a/src/component/misc/UserDropdown.tsx b/src/component/misc/UserDropdown.tsx
index 7c1f412f3..68c9c9329 100644
--- a/src/component/misc/UserDropdown.tsx
+++ b/src/component/misc/UserDropdown.tsx
@@ -15,6 +15,7 @@ import { useI18n } from "../hook/useI18n";
import { ThunkDispatch } from "../../util/Types";
import { useContext } from "react";
import { AuthContext } from "./oidc/OidcAuthWrapper";
+import { useStompClient } from "react-stomp-hooks";
interface UserDropdownProps {
dark: boolean;
@@ -32,11 +33,12 @@ export const UserDropdown: React.FC = (props) => {
const user = useSelector((state: TermItState) => state.user);
const dispatch: ThunkDispatch = useDispatch();
const context = useContext(AuthContext);
+ const stompClient = useStompClient();
const onLogout = async () => {
if (context && context.logout) {
await context.logout();
}
- dispatch(logout());
+ dispatch(logout(stompClient));
};
return (
diff --git a/src/component/misc/__tests__/UserDropdown.test.tsx b/src/component/misc/__tests__/UserDropdown.test.tsx
index 46d9c73f7..3d9e30c4d 100644
--- a/src/component/misc/__tests__/UserDropdown.test.tsx
+++ b/src/component/misc/__tests__/UserDropdown.test.tsx
@@ -7,7 +7,10 @@ import {
UncontrolledDropdown,
} from "reactstrap";
import * as Redux from "react-redux";
-import { mountWithIntl } from "../../../__tests__/environment/Environment";
+import {
+ mountWithIntl,
+ withWebSocket,
+} from "../../../__tests__/environment/Environment";
import * as actions from "../../../action/ComplexActions";
jest.mock("react-redux", () => ({
@@ -29,7 +32,7 @@ describe("UserDropdown", () => {
});
it("renders correct structure of component", () => {
- const wrapper = mountWithIntl();
+ const wrapper = mountWithIntl(withWebSocket());
expect(
wrapper
diff --git a/src/component/misc/oidc/OidcAuthWrapper.tsx b/src/component/misc/oidc/OidcAuthWrapper.tsx
index 7ab8b168e..db8fa6229 100644
--- a/src/component/misc/oidc/OidcAuthWrapper.tsx
+++ b/src/component/misc/oidc/OidcAuthWrapper.tsx
@@ -6,6 +6,7 @@ import React, {
} from "react";
import { User, UserManager } from "oidc-client";
import { generateRedirectUri, getOidcConfig } from "../../../util/OidcUtils";
+import BrowserStorage from "../../../util/BrowserStorage";
// Taken from https://github.com/datagov-cz/assembly-line-shared but using a different config processing mechanism
@@ -74,6 +75,7 @@ const OidcAuthWrapper: React.FC = ({
} catch (error) {
throwError(error as Error);
}
+ BrowserStorage.dispatchTokenChangeEvent();
};
getUser();
}, [location, history, throwError, setUser, userManager]);
@@ -87,6 +89,7 @@ const OidcAuthWrapper: React.FC = ({
} catch (error) {
throwError(error as Error);
}
+ BrowserStorage.dispatchTokenChangeEvent();
};
userManager.events.addUserLoaded(updateUserData);
diff --git a/src/component/public/term/TermDetail.tsx b/src/component/public/term/TermDetail.tsx
index 0196ae2cd..5ec42b53b 100644
--- a/src/component/public/term/TermDetail.tsx
+++ b/src/component/public/term/TermDetail.tsx
@@ -88,7 +88,7 @@ export default connect(
(dispatch: ThunkDispatch) => {
return {
loadVocabulary: (iri: IRI, timestamp?: string) =>
- dispatch(loadVocabulary(iri, false, timestamp)),
+ dispatch(loadVocabulary(iri, timestamp)),
loadTerm: (termName: string, vocabularyIri: IRI, timestamp?: string) =>
dispatch(loadTerm(termName, vocabularyIri, timestamp)),
};
diff --git a/src/component/resource/file/UploadFile.tsx b/src/component/resource/file/UploadFile.tsx
index 42b448bf1..e13a2571e 100644
--- a/src/component/resource/file/UploadFile.tsx
+++ b/src/component/resource/file/UploadFile.tsx
@@ -45,10 +45,13 @@ function limitStringToBytes(limitStr: string) {
interface UploadFileProps {
setFile: (file: File) => void;
+ labelKey?: string;
}
-export const UploadFile: React.FC = (props) => {
- const { setFile } = props;
+export const UploadFile: React.FC = ({
+ setFile,
+ labelKey = "resource.create.file.select.label",
+}) => {
const [currentFile, setCurrentFile] = React.useState();
const [dragActive, setDragActive] = React.useState(false);
const { i18n, formatMessage } = useI18n();
@@ -82,7 +85,7 @@ export const UploadFile: React.FC = (props) => {
diff --git a/src/component/term/TermDetail.tsx b/src/component/term/TermDetail.tsx
index 0f8b3015f..7fd6cd858 100644
--- a/src/component/term/TermDetail.tsx
+++ b/src/component/term/TermDetail.tsx
@@ -23,7 +23,7 @@ import Utils from "../../util/Utils";
import AppNotification from "../../model/AppNotification";
import { publishNotification } from "../../action/SyncActions";
import NotificationType from "../../model/NotificationType";
-import { IRI } from "../../util/VocabularyUtils";
+import VocabularyUtils, { IRI } from "../../util/VocabularyUtils";
import * as _ from "lodash";
import Vocabulary from "../../model/Vocabulary";
import { FaTrashAlt } from "react-icons/fa";
@@ -39,6 +39,7 @@ import { DefinitionRelatedChanges } from "./DefinitionRelatedTermsEdit";
import TermOccurrence from "../../model/TermOccurrence";
import {
approveOccurrence,
+ loadDefinitionRelatedTermsTargeting,
loadTerm,
removeOccurrence,
} from "../../action/AsyncTermActions";
@@ -50,6 +51,14 @@ import IfVocabularyActionAuthorized from "../vocabulary/authorization/IfVocabula
import AccessLevel from "../../model/acl/AccessLevel";
import StoreBasedTerminalTermStateIcon from "./state/StoreBasedTerminalTermStateIcon";
import IfNotInTerminalState from "./state/IfNotInTerminalState";
+import { IMessage, withStompClient, withSubscription } from "react-stomp-hooks";
+import { HasStompClient, StompClient } from "../hoc/withStompClient";
+import Constants from "../../util/Constants";
+import { vocabularyValidation } from "../../reducer/WebSocketVocabularyDispatchers";
+import { requestVocabularyValidation } from "../../action/WebSocketVocabularyActions";
+
+const USER_VOCABULARIES_VALIDATION_ENDPOINT =
+ "/user" + Constants.WEBSOCKET_ENDPOINT.VOCABULARIES_VALIDATION;
export interface CommonTermDetailProps extends HasI18n {
configuredLanguage: string;
@@ -62,9 +71,16 @@ export interface CommonTermDetailProps extends HasI18n {
interface TermDetailProps
extends CommonTermDetailProps,
+ HasStompClient,
RouteComponentProps
{
updateTerm: (term: Term) => Promise;
removeTerm: (term: Term) => Promise;
+ requestVocabularyValidation: (
+ vocabularyIri: IRI,
+ stompClient: StompClient
+ ) => void;
+ loadDefinitionRelatedTermsTargeting: (termIri: IRI) => void;
+ vocabularyValidation: (message: IMessage, vocabularyIri: string) => void;
approveOccurrence: (occurrence: TermOccurrence) => Promise;
removeOccurrence: (occurrence: TermOccurrence) => Promise;
publishNotification: (notification: AppNotification) => void;
@@ -109,7 +125,9 @@ export class TermDetail extends EditableComponent<
this.props.location.search,
"namespace"
);
- this.props.loadVocabulary({ fragment: name, namespace }, timestamp);
+ const iri: IRI = { fragment: name, namespace };
+ this.props.loadVocabulary(iri, timestamp);
+ this.props.requestVocabularyValidation(iri, this.props.stompClient);
}
private loadTerm(): void {
@@ -131,6 +149,23 @@ export class TermDetail extends EditableComponent<
}
}
+ // used by withSubscription
+ public onMessage = (message: IMessage) => {
+ switch (message?.headers?.destination) {
+ case USER_VOCABULARIES_VALIDATION_ENDPOINT:
+ this.props.vocabularyValidation(message, this.props.vocabulary.iri);
+ break;
+ case Constants.WEBSOCKET_ENDPOINT
+ .VOCABULARIES_TEXT_ANALYSIS_FINISHED_TERM_DEFINITION:
+ if (this.props.term?.iri && this.props.term.iri === message.body) {
+ this.props.loadDefinitionRelatedTermsTargeting(
+ VocabularyUtils.create(this.props.term.iri)
+ );
+ }
+ break;
+ }
+ };
+
public setLanguage = (language: string) => {
this.setState({ language });
};
@@ -188,7 +223,10 @@ export class TermDetail extends EditableComponent<
const actions = [];
if (!this.state.edit) {
actions.push(
-
+