Skip to content
This repository was archived by the owner on Apr 18, 2024. It is now read-only.

Commit 791b713

Browse files
bmartelhlomzik
andauthored
fix: LSDV-4836: Conditional annotation with visibleWhen choice-selected works with Taxonomy (#1273)
* fix: LSDV-4836: Conditional annotation with visibleWhen choice-selected works with Taxonomy * make a more robust fix for Taxonomy choice comparisons for visibleWhen * removing unused function * fix serialize of conditional taxonomy choice results * fix serialization of dependent taxonomy choice(s) with aliases * fix choices inclusion in serialization * use a safer lookup for multi dimensional array choice results * ensure the check for choiceValue when in the choice-unselected case handles null or empty * fix classification unselected choices case --------- Co-authored-by: hlomzik <hlomzik@gmail.com>
1 parent ffd3f4c commit 791b713

File tree

6 files changed

+342
-22
lines changed

6 files changed

+342
-22
lines changed

e2e/tests/taxonomy.test.js

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -722,3 +722,240 @@ Scenario('Taxonomy maxUsages', async ({ I, LabelStudio, AtTaxonomy }) => {
722722
AtTaxonomy.dontSeeCheckedItemByText('One');
723723
AtTaxonomy.dontSeeSelectedValues('One');
724724
});
725+
726+
Scenario('Taxonomy visibleWhen', async ({ I, LabelStudio, AtTaxonomy }) => {
727+
const createConfig = ({ showFullPath = false, visibleWhen = 'choice-selected', whenChoiceValue = 'Four' } = {}) => `
728+
<View>
729+
<Text name="text" value="$text"/>
730+
<Taxonomy required="true" name="taxonomy" toName="text" leafsOnly="true" placeholder="Select something..." showFullPath="${showFullPath}">
731+
<Choice value="One to three">
732+
<Choice value="One" />
733+
<Choice value="Two" />
734+
<Choice value="Three" />
735+
</Choice>
736+
<Choice value="Four to seven">
737+
<Choice value="Four" />
738+
<Choice value="Five" />
739+
<Choice value="Six" />
740+
<Choice value="Seven" />
741+
</Choice>
742+
</Taxonomy>
743+
<Choices name="other" toName="text"
744+
showInline="true"
745+
visibleWhen="${visibleWhen}"
746+
whenTagName="taxonomy"
747+
whenChoiceValue="${whenChoiceValue}"
748+
>
749+
<Choice value="Eight" />
750+
<Choice value="Nine" />
751+
</Choices>
752+
</View>
753+
`;
754+
755+
I.amOnPage('/');
756+
LabelStudio.init({
757+
config: createConfig(),
758+
data: {
759+
text: 'A text',
760+
},
761+
});
762+
I.say('Should see values of choices and work with them');
763+
AtTaxonomy.clickTaxonomy();
764+
AtTaxonomy.toggleGroupWithText('One to three');
765+
AtTaxonomy.toggleGroupWithText('Four to seven');
766+
AtTaxonomy.seeItemByText('Two');
767+
AtTaxonomy.seeItemByText('Five');
768+
AtTaxonomy.clickItemByText('Three');
769+
AtTaxonomy.clickItemByText('Four');
770+
AtTaxonomy.seeSelectedValues(['Three', 'Four']);
771+
AtTaxonomy.clickTaxonomy();
772+
I.click('Eight'); // click on the choice
773+
I.seeElement('.ant-checkbox-checked [name=\'Eight\']');
774+
I.say('Should get results for taxonomy and choices');
775+
776+
let result = await LabelStudio.serialize();
777+
778+
assert.deepStrictEqual(result[0].value.taxonomy, [['One to three', 'Three'], ['Four to seven', 'Four']]);
779+
assert.deepStrictEqual(result[1].value.choices, ['Eight']);
780+
781+
I.say('Should get results for only taxonomy when visibleWhen is not met');
782+
AtTaxonomy.clickTaxonomy();
783+
AtTaxonomy.clickItemByText('Four');
784+
AtTaxonomy.clickTaxonomy();
785+
I.dontSeeElement('.ant-checkbox-checked [name=\'Eight\']');
786+
787+
result = await LabelStudio.serialize();
788+
789+
assert.deepStrictEqual(result[0].value.taxonomy, [['One to three', 'Three']]);
790+
assert.deepStrictEqual(result?.[1]?.value?.choices, undefined);
791+
792+
I.say('Should get results for taxonomy and choices when visibleWhen is met');
793+
AtTaxonomy.clickTaxonomy();
794+
AtTaxonomy.clickItemByText('Four');
795+
AtTaxonomy.clickTaxonomy();
796+
I.seeElement('.ant-checkbox-checked [name=\'Eight\']');
797+
798+
result = await LabelStudio.serialize();
799+
800+
assert.deepStrictEqual(result[0].value.taxonomy, [['One to three', 'Three'], ['Four to seven', 'Four']]);
801+
assert.deepStrictEqual(result[1].value.choices, ['Eight']);
802+
803+
await session('Deserialization', async () => {
804+
I.amOnPage('/');
805+
LabelStudio.init({
806+
config: createConfig(),
807+
data: {
808+
text: 'A text',
809+
},
810+
annotations: [{
811+
id: 'test',
812+
result,
813+
}],
814+
});
815+
I.say('Should see the same result');
816+
AtTaxonomy.clickTaxonomy();
817+
AtTaxonomy.toggleGroupWithText('One to three');
818+
AtTaxonomy.toggleGroupWithText('Four to seven');
819+
AtTaxonomy.seeCheckedItemByText('Three');
820+
AtTaxonomy.seeCheckedItemByText('Four');
821+
AtTaxonomy.seeSelectedValues(['Three', 'Four']);
822+
I.seeElement('.ant-checkbox-checked [name=\'Eight\']');
823+
});
824+
825+
await session('ShowFullPath', async () => {
826+
//showFullPath
827+
I.amOnPage('/');
828+
LabelStudio.init({
829+
config: createConfig({ showFullPath: true }),
830+
data: {
831+
text: 'A text',
832+
},
833+
annotations: [{
834+
id: 'test',
835+
result,
836+
}],
837+
});
838+
I.say('Should see the full paths');
839+
AtTaxonomy.clickTaxonomy();
840+
AtTaxonomy.seeSelectedValues(['One to three / Three', 'Four to seven / Four']);
841+
I.seeElement('.ant-checkbox-checked [name=\'Eight\']');
842+
});
843+
});
844+
845+
Scenario('Taxonomy visibleWhen with aliases', async ({ I, LabelStudio, AtTaxonomy }) => {
846+
const createConfig = ({ showFullPath = false, visibleWhen = 'choice-selected', whenChoiceValue = 'Four' } = {}) => `
847+
<View>
848+
<Text name="text" value="$text"/>
849+
<Taxonomy required="true" name="taxonomy" toName="text" leafsOnly="true" placeholder="Select something..." showFullPath="${showFullPath}">
850+
<Choice alias="1-3" value="One to three">
851+
<Choice alias="1" value="One" />
852+
<Choice alias="2" value="Two" />
853+
<Choice alias="3" value="Three" />
854+
</Choice>
855+
<Choice alias="4-7" value="Four to seven">
856+
<Choice alias="4" value="Four" />
857+
<Choice alias="5" value="Five" />
858+
<Choice alias="6" value="Six" />
859+
<Choice alias="7" value="Seven" />
860+
</Choice>
861+
</Taxonomy>
862+
<Choices name="choices" toName="text"
863+
visibleWhen="${visibleWhen}"
864+
whenTagName="taxonomy"
865+
whenChoiceValue="${whenChoiceValue}"
866+
>
867+
<Choice alias="8" value="Eight"></Choice>
868+
<Choice alias="9" value="Nine"></Choice>
869+
</Choices>
870+
</View>
871+
`;
872+
873+
I.amOnPage('/');
874+
LabelStudio.init({
875+
config: createConfig(),
876+
data: {
877+
text: 'A text',
878+
},
879+
});
880+
I.say('Should see values of choices and work with them');
881+
AtTaxonomy.clickTaxonomy();
882+
AtTaxonomy.toggleGroupWithText('One to three');
883+
AtTaxonomy.toggleGroupWithText('Four to seven');
884+
AtTaxonomy.seeItemByText('Two');
885+
AtTaxonomy.seeItemByText('Five');
886+
AtTaxonomy.clickItemByText('Three');
887+
AtTaxonomy.clickItemByText('Four');
888+
AtTaxonomy.seeSelectedValues(['Three', 'Four']);
889+
AtTaxonomy.clickTaxonomy();
890+
I.click('Eight'); // click on the choice
891+
I.seeElement('.ant-checkbox-checked [name=\'Eight\']');
892+
I.say('Should get aliases as results');
893+
894+
let result = await LabelStudio.serialize();
895+
896+
assert.deepStrictEqual(result[0].value.taxonomy, [['1-3', '3'], ['4-7', '4']]);
897+
assert.deepStrictEqual(result[1].value.choices, ['8']);
898+
899+
I.say('Should get alias results for only taxonomy when visibleWhen is not met');
900+
AtTaxonomy.clickTaxonomy();
901+
AtTaxonomy.clickItemByText('Four');
902+
AtTaxonomy.clickTaxonomy();
903+
I.dontSeeElement('.ant-checkbox-checked [name=\'Eight\']');
904+
905+
result = await LabelStudio.serialize();
906+
907+
assert.deepStrictEqual(result[0].value.taxonomy, [['1-3', '3']]);
908+
assert.deepStrictEqual(result?.[1]?.value?.choices, undefined);
909+
910+
I.say('Should get alias results for taxonomy and choices when visibleWhen is met');
911+
AtTaxonomy.clickTaxonomy();
912+
AtTaxonomy.clickItemByText('Four');
913+
AtTaxonomy.clickTaxonomy();
914+
I.seeElement('.ant-checkbox-checked [name=\'Eight\']');
915+
916+
result = await LabelStudio.serialize();
917+
918+
assert.deepStrictEqual(result[0].value.taxonomy, [['1-3', '3'], ['4-7', '4']]);
919+
assert.deepStrictEqual(result[1].value.choices, ['8']);
920+
921+
await session('Deserialization', async () => {
922+
I.amOnPage('/');
923+
LabelStudio.init({
924+
config: createConfig(),
925+
data: {
926+
text: 'A text',
927+
},
928+
annotations: [{
929+
id: 'test',
930+
result,
931+
}],
932+
});
933+
I.say('Should see the same result');
934+
AtTaxonomy.clickTaxonomy();
935+
AtTaxonomy.toggleGroupWithText('One to three');
936+
AtTaxonomy.toggleGroupWithText('Four to seven');
937+
AtTaxonomy.seeCheckedItemByText('Three');
938+
AtTaxonomy.seeCheckedItemByText('Four');
939+
AtTaxonomy.seeSelectedValues(['Three', 'Four']);
940+
I.seeElement('.ant-checkbox-checked [name=\'Eight\']');
941+
});
942+
943+
await session('ShowFullPath', async () => {
944+
//showFullPath
945+
I.amOnPage('/');
946+
LabelStudio.init({
947+
config: createConfig({ showFullPath: true }),
948+
data: {
949+
text: 'A text',
950+
},
951+
annotations: [{
952+
id: 'test',
953+
result,
954+
}],
955+
});
956+
I.say('Should see the full paths');
957+
AtTaxonomy.clickTaxonomy();
958+
AtTaxonomy.seeSelectedValues(['One to three / Three', 'Four to seven / Four']);
959+
I.seeElement('.ant-checkbox-checked [name=\'Eight\']');
960+
});
961+
});

src/mixins/SelectedChoiceMixin.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { types } from 'mobx-state-tree';
2+
import { isDefined } from '../utils/utilities';
3+
4+
const SelectedChoiceMixin = types
5+
.model()
6+
.views(self => ({
7+
findSelectedChoice(aliasOrValue) {
8+
let item;
9+
10+
if (self.findLabel) {
11+
item = self.findLabel(aliasOrValue);
12+
} else if (self.findItemByValueOrAlias) {
13+
item = self.findItemByValueOrAlias(aliasOrValue);
14+
}
15+
16+
return item?.alias || item?.value;
17+
},
18+
selectedChoicesMatch(aliasOrValue1, aliasOrValue2) {
19+
const choice1 = self.findSelectedChoice(aliasOrValue1);
20+
const choice2 = self.findSelectedChoice(aliasOrValue2);
21+
22+
return isDefined(choice1) && isDefined(choice2) && choice1 === choice2;
23+
},
24+
hasChoiceSelection(choiceValue, selectedValues = []) {
25+
if (choiceValue?.length) {
26+
// @todo Revisit this and make it more consistent, and refactor this
27+
// behaviour out of the SelectedModel mixin and use a singular approach.
28+
// This is the original behaviour of other SelectedModel mixin usages
29+
// as they are using alias lookups for choices. For now we will keep it as is since it works for all the
30+
// other cases currently.
31+
if (self.findLabel) {
32+
return choiceValue
33+
.map(v => self.findLabel(v))
34+
.some(c => c && c.sel);
35+
}
36+
37+
// Check the selected values of the choices for existence of the choiceValue(s)
38+
if (selectedValues.length) {
39+
const includesValue = v => {
40+
if (self.findItemByValueOrAlias) {
41+
const item = self.findItemByValueOrAlias(v);
42+
43+
v = item?.alias || item?.value || v;
44+
}
45+
46+
return selectedValues.map(s => Array.isArray(s) ? s.at(-1) : s).includes(v);
47+
};
48+
49+
return choiceValue.some(includesValue);
50+
}
51+
52+
return false;
53+
}
54+
55+
return self.isSelected;
56+
},
57+
}));
58+
59+
export default SelectedChoiceMixin;

src/mixins/Visibility.js

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -43,18 +43,9 @@ const VisibilityMixin = types
4343

4444
const tag = self.annotation.names.get(tagName);
4545

46-
if (!tag) return false;
46+
if (!tag?.hasChoiceSelection && !choiceValue?.length) return false;
4747

48-
if (choiceValue) {
49-
const choicesSelected = choiceValue
50-
.split(',')
51-
.map(v => tag.findLabel(v))
52-
.some(c => c && c.sel);
53-
54-
return choicesSelected;
55-
}
56-
57-
return tag.isSelected;
48+
return tag.hasChoiceSelection(choiceValue?.split(','), tag.selectedValues());
5849
},
5950

6051
'no-region-selected': () => !self.annotation.highlightedNode,

src/regions/Result.js

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ const Result = types
150150
/**
151151
* Checks perRegion and Visibility params
152152
*/
153-
get isSubmitable() {
153+
get canBeSubmitted() {
154154
const control = self.from_name;
155155

156156
if (control.perregion) {
@@ -159,10 +159,13 @@ const Result = types
159159
if (label && !self.area.hasLabel(label)) return false;
160160
}
161161

162+
const innerResults = (r) =>
163+
r.map(s => Array.isArray(s) ? s.at(-1) : s);
164+
162165
const isChoiceSelected = () => {
163166
const tagName = control.whentagname;
164167
const choiceValues = control.whenchoicevalue ? control.whenchoicevalue.split(',') : null;
165-
const results = self.annotation.results.filter(r => r.type === 'choices' && r !== self);
168+
const results = self.annotation.results.filter(r => ['choices', 'taxonomy'].includes(r.type) && r !== self);
166169

167170
if (tagName) {
168171
const result = results.find(r => {
@@ -172,11 +175,11 @@ const Result = types
172175
});
173176

174177
if (!result) return false;
175-
if (choiceValues && !choiceValues.some(v => result.mainValue.includes(v))) return false;
178+
if (choiceValues && !choiceValues.some(v => innerResults(result.mainValue).some(vv => result.from_name.selectedChoicesMatch(v, vv)))) return false;
176179
} else {
177180
if (!results.length) return false;
178181
// if no given choice value is selected in any choice result
179-
if (choiceValues && !choiceValues.some(v => results.some(r => r.mainValue.includes(v)))) return false;
182+
if (choiceValues && !results.some(r => choiceValues.some(v => innerResults(r.mainValue).some(vv => r.from_name.selectedChoicesMatch(v, vv))))) return false;
180183
}
181184
return true;
182185
};
@@ -269,7 +272,7 @@ const Result = types
269272
const to_name = Tree.cleanUpId(sn.to_name);
270273

271274
if (!data) return null;
272-
if (!self.isSubmitable) return null;
275+
if (!self.canBeSubmitted) return null;
273276

274277
if (!isDefined(data.value)) data.value = {};
275278
// with `mergeLabelsAndResults` control uses only one result even with external `Labels`

src/tags/control/Choices.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import './Choice';
2121
import DynamicChildrenMixin from '../../mixins/DynamicChildrenMixin';
2222
import { FF_DEV_2007, FF_DEV_2007_DEV_2008, isFF } from '../../utils/feature-flags';
2323
import { ReadOnlyControlMixin } from '../../mixins/ReadOnlyMixin';
24+
import SelectedChoiceMixin from '../../mixins/SelectedChoiceMixin';
2425

2526
const { Option } = Select;
2627

@@ -246,6 +247,7 @@ const ChoicesModel = types.compose(
246247
RequiredMixin,
247248
PerRegionMixin,
248249
ReadOnlyControlMixin,
250+
SelectedChoiceMixin,
249251
VisibilityMixin,
250252
...(isFF(FF_DEV_2007_DEV_2008) ? [DynamicChildrenMixin] : []),
251253
Model,

0 commit comments

Comments
 (0)