diff --git a/src/components/NewTaxonomy/NewTaxonomy.styl b/src/components/NewTaxonomy/NewTaxonomy.styl
new file mode 100644
index 0000000000..f034aa4b34
--- /dev/null
+++ b/src/components/NewTaxonomy/NewTaxonomy.styl
@@ -0,0 +1,3 @@
+:global(.htx-taxonomy-item-color)
+ padding 4px 4px
+ border-radius 2px
diff --git a/src/components/NewTaxonomy/NewTaxonomy.tsx b/src/components/NewTaxonomy/NewTaxonomy.tsx
index 7f97a99e7e..3b91ba4dab 100644
--- a/src/components/NewTaxonomy/NewTaxonomy.tsx
+++ b/src/components/NewTaxonomy/NewTaxonomy.tsx
@@ -3,6 +3,8 @@ import React, { useCallback, useEffect, useState } from 'react';
import { Tooltip } from '../../common/Tooltip/Tooltip';
+import './NewTaxonomy.styl';
+
type TaxonomyPath = string[];
type onAddLabelCallback = (path: string[]) => any;
type onDeleteLabelCallback = (path: string[]) => any;
@@ -15,6 +17,7 @@ type TaxonomyItem = {
children?: TaxonomyItem[],
origin?: 'config' | 'user' | 'session',
hint?: string,
+ color?: string,
};
type AntTaxonomyItem = {
@@ -57,17 +60,32 @@ const convert = (
options: TaxonomyExtendedOptions,
selectedPaths: string[],
): AntTaxonomyItem[] => {
+ // generate string or component to be the `title` of the item
+ const enrich = (item: TaxonomyItem) => {
+ const color = (item: TaxonomyItem) => (
+ // no BEM here to make it more lightweight
+ // global classname to allow to change it in Style tag
+
+ {item.label}
+
+ );
+
+ if (!item.hint) return item.color ? color(item) : item.label;
+
+ return (
+
+ {item.color ? color(item) : {item.label}}
+
+ );
+ };
+
const convertItem = (item: TaxonomyItem): AntTaxonomyItem => {
const value = item.path.join(options.pathSeparator);
const disabledNode = options.leafsOnly && (item.isLeaf === false || !!item.children);
const maxUsagesReached = options.maxUsagesReached && !selectedPaths.includes(value);
return {
- title: item.hint ? (
-
- {item.label}
-
- ) : item.label,
+ title: enrich(item),
value,
key: value,
isLeaf: item.isLeaf !== false && !item.children,
diff --git a/src/components/Node/Node.tsx b/src/components/Node/Node.tsx
index d48cef6709..0785c979af 100644
--- a/src/components/Node/Node.tsx
+++ b/src/components/Node/Node.tsx
@@ -188,6 +188,9 @@ const NodeMinimal: FC = observer(({ node }) => {
});
const useNodeName = (node: any) => {
+ // @todo sometimes node is control tag, not a region
+ // @todo and for new taxonomy it can be plain object
+ if (!node.$treenode) return null;
return getType(node).name as keyof typeof NodeViews;
};
diff --git a/src/components/Taxonomy/Taxonomy.tsx b/src/components/Taxonomy/Taxonomy.tsx
index 4607417d4b..90f7f26503 100644
--- a/src/components/Taxonomy/Taxonomy.tsx
+++ b/src/components/Taxonomy/Taxonomy.tsx
@@ -9,15 +9,15 @@ import React, {
} from 'react';
import { Dropdown, Menu } from 'antd';
+import { LsChevron } from '../../assets/icons';
+import { Tooltip } from '../../common/Tooltip/Tooltip';
import { useToggle } from '../../hooks/useToggle';
+import { CNTagName } from '../../utils/bem';
+import { FF_DEV_4075, FF_PROD_309, isFF } from '../../utils/feature-flags';
import { isArraysEqual } from '../../utils/utilities';
-import { LsChevron } from '../../assets/icons';
import TreeStructure from '../TreeStructure/TreeStructure';
import styles from './Taxonomy.module.scss';
-import { FF_DEV_4075, FF_PROD_309, isFF } from '../../utils/feature-flags';
-import { Tooltip } from '../../common/Tooltip/Tooltip';
-import { CNTagName } from '../../utils/bem';
type TaxonomyPath = string[];
type onAddLabelCallback = (path: string[]) => any;
@@ -33,6 +33,7 @@ type TaxonomyItem = {
};
type TaxonomyOptions = {
+ canRemoveItems?: boolean,
leafsOnly?: boolean,
showFullPath?: boolean,
pathSeparator?: string,
@@ -504,6 +505,10 @@ const Taxonomy = ({
const setSelected = (path: TaxonomyPath, value: boolean) => {
const newSelected = value ? [...selected, path] : selected.filter(current => !isArraysEqual(current, path));
+ // don't remove last item when taxonomy is used as labeling tool
+ // canRemoveItems is undefined when FF is off; false only when region is active
+ if (options.canRemoveItems === false && !newSelected.length) return;
+
setInternalSelected(newSelected);
onChange && onChange(null, newSelected);
};
diff --git a/src/mixins/HighlightMixin.js b/src/mixins/HighlightMixin.js
index 7d604be1bf..2223861209 100644
--- a/src/mixins/HighlightMixin.js
+++ b/src/mixins/HighlightMixin.js
@@ -153,14 +153,8 @@ export const HighlightMixin = types
updateSpans() {
if (self._hasSpans || (isFF(FF_LSDV_4620_3) && self._spans?.length)) {
const lastSpan = self._spans[self._spans.length - 1];
- const label = self.getLabels();
- // label is array, string or null, so check for length
- if (!label?.length) {
- lastSpan.removeAttribute('data-label');
- } else {
- lastSpan.setAttribute('data-label', label);
- }
+ Utils.Selection.applySpanStyles(lastSpan, { label: self.getLabels() });
}
},
@@ -274,7 +268,7 @@ export const HighlightMixin = types
},
getLabels() {
- return self.labeling?.mainValue ?? [];
+ return (self.labeling?.selectedLabels ?? []).map(label => label.value).join(',');
},
getLabelColor() {
diff --git a/src/mixins/SelectedChoiceMixin.js b/src/mixins/SelectedChoiceMixin.js
index 99f5fb84b7..5ef40a7b95 100644
--- a/src/mixins/SelectedChoiceMixin.js
+++ b/src/mixins/SelectedChoiceMixin.js
@@ -21,6 +21,19 @@ const SelectedChoiceMixin = types
return isDefined(choice1) && isDefined(choice2) && choice1 === choice2;
},
+ // @todo it's better to only take final values into account
+ // @todo (meaning alias only, not alias + value when alias is present)
+ // @todo so this should be the final and simpliest method
+ hasChoiceSelectionSimple(choiceValue) {
+ if (choiceValue?.length) {
+ // grab the string value; for taxonomy, it's the last value in the array
+ const selectedValues = self.selectedValues().map(s => Array.isArray(s) ? s.at(-1) : s);
+
+ return choiceValue.some(value => selectedValues.includes(value));
+ }
+
+ return self.isSelected;
+ },
hasChoiceSelection(choiceValue, selectedValues = []) {
if (choiceValue?.length) {
// @todo Revisit this and make it more consistent, and refactor this
diff --git a/src/regions/Result.js b/src/regions/Result.js
index 4be89f4a53..d1756fa6fd 100644
--- a/src/regions/Result.js
+++ b/src/regions/Result.js
@@ -140,19 +140,14 @@ const Result = types
return self.mainValue?.join(joinstr) || '';
},
+ // @todo check all usages of selectedLabels:
+ // — check usages of non-array values (like `if selectedValues ...`)
+ // - check empty labels, they should be returned as an array
get selectedLabels() {
- if (self.type === 'taxonomy') {
- const sep = self.from_name.pathseparator;
- const join = self.from_name.showfullpath;
-
- return (self.mainValue || [])
- .map(v => join ? v.join(sep) : v.at(-1))
- .map(v => ({ value: v, id: v }));
- }
if (self.mainValue?.length === 0 && self.from_name.allowempty) {
return self.from_name.findLabel(null);
}
- return self.mainValue?.map(value => self.from_name.findLabel(value)).filter(Boolean);
+ return self.mainValue?.map(value => self.from_name.findLabel(value)).filter(Boolean) ?? [];
},
/**
@@ -212,7 +207,7 @@ const Result = types
get style() {
if (!self.tag) return null;
- const fillcolor = self.tag.background || self.tag.parent.fillcolor;
+ const fillcolor = self.tag.background || self.tag.parent?.fillcolor;
if (!fillcolor) return null;
const strokecolor = self.tag.background || self.tag.parent.strokecolor;
diff --git a/src/tags/control/Choice.js b/src/tags/control/Choice.js
index 570ba80849..bfe6143aaf 100644
--- a/src/tags/control/Choice.js
+++ b/src/tags/control/Choice.js
@@ -44,8 +44,9 @@ import { HintTooltip } from '../../components/Taxonomy/Taxonomy';
* @param {string} [alias] - Alias for the choice. If used, the alias replaces the choice value in the annotation results. Alias does not display in the interface.
* @param {style} [style] - CSS style of the checkbox element
* @param {string} [hotkey] - Hotkey for the selection
- * @param {string} [html] - can be used to show enriched content[^FF_DEV_2007], it has higher priority than `value`, however `value` will be used in the exported result (should be properly escaped)
+ * @param {string} [html] - Can be used to show enriched content[^FF_DEV_2007], it has higher priority than `value`, however `value` will be used in the exported result (should be properly escaped)
* @param {string} [hint] - Hint for choice on hover[^FF_PROD_309]
+ * @param {string} [color] - Color for Taxonomy item
*/
const TagAttrs = types.model({
...(isFF(FF_DEV_3391) ? { id: types.identifier } : {}),
@@ -54,6 +55,7 @@ const TagAttrs = types.model({
value: types.maybeNull(types.string),
hotkey: types.maybeNull(types.string),
style: types.maybeNull(types.string),
+ color: types.maybeNull(types.string),
...(isFF(FF_DEV_2007) ? { html: types.maybeNull(types.string) } : {}),
...(isFF(FF_PROD_309) ? { hint: types.maybeNull(types.string) } : {}),
});
diff --git a/src/tags/control/Taxonomy/Taxonomy.js b/src/tags/control/Taxonomy/Taxonomy.js
index 3a70556091..8e6542183a 100644
--- a/src/tags/control/Taxonomy/Taxonomy.js
+++ b/src/tags/control/Taxonomy/Taxonomy.js
@@ -112,6 +112,7 @@ function traverse(root) {
const depth = parents.length;
const obj = { label, path, depth, hint };
+ if (node.color) obj.color = node.color;
if (node.children) {
obj.children = visitUnique(node.children, path);
}
@@ -150,6 +151,10 @@ const TaxonomyLabelingResult = types
return self.annotation.results.find(r => r.from_name === self && r.area === area);
},
+ get canRemoveItems() {
+ if (!self.isLabeling) return true;
+ return !self.result;
+ },
}))
.actions(self => {
const Super = {
@@ -163,6 +168,35 @@ const TaxonomyLabelingResult = types
self.result.area.setValue(self);
}
},
+
+ /**
+ * @param {string[]} path saved value from Taxonomy
+ * @returns quazi-label object to act as Label in most places
+ */
+ findLabel(path) {
+ let title = '';
+ let items = self.items;
+ let item;
+
+ for (const value of path) {
+ item = items?.find(item => item.path.at(-1) === value);
+
+ if (!item) return null;
+
+ items = item.children;
+ title = self.showfullpath && title ? title + self.pathseparator + item.label : item.label;
+ }
+
+ const label = { value: title, id: path.join(self.pathseparator) };
+
+ if (item.color) {
+ // to conform the current format of our Result#style (and it requires parent)
+ label.background = item.color;
+ label.parent = {};
+ }
+
+ return label;
+ },
};
});
@@ -411,6 +445,10 @@ const Model = types
},
onChange(_node, checked) {
+ // don't remove last label from region if region is selected (so canRemoveItems is false)
+ // should be checked only for Taxonomy as labbeling tool
+ if (self.canRemoveItems === false && !checked.length) return;
+
self.selected = checked.map(s => s.path ?? s);
self.maxUsagesReached = self.selected.length >= self.maxusages;
self.updateResult();
@@ -507,6 +545,7 @@ const HtxTaxonomy = observer(({ item }) => {
minWidth: item.minwidth,
dropdownWidth: item.dropdownwidth,
placeholder: item.placeholder,
+ canRemoveItems: item.canRemoveItems,
};
return (