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

Commit 9578b82

Browse files
authored
feat: LSDV-4654: Allow per-image classification (#1381)
* feat: LSDV-4654: Allow per-image classification * test: LSDV-4654: Add tests for perItem functionality * Check footnotes support * Add footnotes normalization into docs generation * Update JSDocs * Fix ControlBase mixing order in compose args * Force adding new lines before footnote definitions * Fix textarea perregion area getting * Fix eslint problems * Try to fix combineCoverage bug * Update @heartexlabs/ls-test * Update @heartexlabs/ls-test * Fix FF_LSDV_4583 value in tests and documents * Fix FF_DEV_2100 to work with MIG * Update @heartexlabs/ls-test * Update @heartexlabs/ls-test * Update @heartexlabs/ls-test * Update @heartexlabs/ls-test * Add perItem with perRegion validation * Update @heartexlabs/ls-test * Update @heartexlabs/ls-test * Update @heartexlabs/ls-test
1 parent 8c63614 commit 9578b82

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+3493
-280
lines changed

scripts/create-docs.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,21 @@ fetch(currentTagsUrl)
9595
// move comments from examples to description
9696
.replace(/```html[\n\s]*<!--[\n\s]*([\w\W]*?)[\n\s]*-->[\n\s]*/g, '\n$1\n\n```html\n')
9797
// change example language if it looks like JSON
98-
.replace(/```html[\n\s]*([[{])/g, '```json\n$1');
98+
.replace(/```html[\n\s]*([[{])/g, '```json\n$1')
99+
// normalize footnotes to be numbers (e.g. `[^FF_LSDV_0000]` => `[^1]`)
100+
.replace(/\[\^([^\]]+)\]/g, (()=>{
101+
let footnoteLastIndex = 0;
102+
const footnoteIdToIdxMap = {};
103+
104+
return (match, footnoteId) => {
105+
const footnoteIdx = footnoteIdToIdxMap[footnoteId] || ++footnoteLastIndex;
106+
107+
footnoteIdToIdxMap[footnoteId] = footnoteIdx;
108+
return `[^${footnoteIdx}]`;
109+
};
110+
})())
111+
// force adding new lines before footnote definitions
112+
.replace(/(?<![\r\n])([\r\n])(\[\^[^\[]+\]:)/gm, '$1$1$2');
99113

100114
if (supertags.includes(t.name)) {
101115
console.log(`Fetching subtags of ${t.name}`);

src/components/ImageView/ImageView.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import ResizeObserver from '../../utils/resize-observer';
2020
import { debounce } from '../../utils/debounce';
2121
import Constants from '../../core/Constants';
2222
import { fixRectToFit } from '../../utils/image';
23-
import { FF_DEV_1285, FF_DEV_1442, FF_DEV_3077, FF_DEV_3793, FF_DEV_4081, FF_LSDV_4583, FF_LSDV_4583_6, FF_LSDV_4711, isFF } from '../../utils/feature-flags';
23+
import { FF_DEV_1285, FF_DEV_1442, FF_DEV_3077, FF_DEV_3793, FF_DEV_4081, FF_LSDV_4583_6, FF_LSDV_4711, isFF } from '../../utils/feature-flags';
2424
import { Pagination } from '../../common/Pagination/Pagination';
2525
import { Image } from './Image';
2626

@@ -842,7 +842,7 @@ export default observer(
842842

843843
const containerClassName = styles.container;
844844

845-
const paginationEnabled = isFF(FF_LSDV_4583) && item.valuelist;
845+
const paginationEnabled = !!item.isMultiItem;
846846

847847
if (getRoot(item).settings.fullscreen === false) {
848848
containerStyle['maxWidth'] = item.maxwidth;

src/core/DataValidator/ConfigValidator.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,22 @@ const validateAttributes = (child, model, fieldsToSkip) => {
247247
return result;
248248
};
249249

250+
/**
251+
* Validate perRegion restrictions
252+
* @param {Object} child
253+
*/
254+
const validatePerRegion = (child) => {
255+
const validationResult = [];
256+
257+
// PerItem and PerRegion are incompatible but PerRegion is more prioritized mode
258+
if (child.perregion && child.peritem) {
259+
validationResult.push(errorBuilder.generalError('Attribute <b>perItem</b> is incompatible with attribute <b>perRegion</b>. ' +
260+
'They define two different modes. However <b>perRegion</b> works fine even with multi-item mode of object tags.'));
261+
}
262+
263+
return validationResult;
264+
};
265+
250266
/**
251267
* Convert MST type to a human-readable string
252268
* @param {import("mobx-state-tree").IType} type
@@ -285,6 +301,8 @@ export class ConfigValidator {
285301

286302
if (parentValidation !== null) validationResult.push(parentValidation);
287303

304+
validationResult.push(...validatePerRegion(child));
305+
288306
validationResult.push(...validateAttributes(child, model, propertiesToSkip));
289307
} catch (e) {
290308
validationResult.push(errorBuilder.unknownTag(child.type, child.name, child.type));

src/mixins/PerItem.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { types } from 'mobx-state-tree';
2+
3+
/**
4+
* This mixing defines perItem control-tag's parameter and related basic functionality
5+
* It should be used right after ClassificationBase mixin
6+
* @see ClassificationBase
7+
*/
8+
const PerItemMixin = types
9+
.model({
10+
peritem: types.optional(types.boolean, false),
11+
}).extend(self => {
12+
/* Validation */
13+
if (self.isClassificationTag !== true) {
14+
throw new Error('The PerItemMixin mixin should be used only for classification control-tags');
15+
}
16+
return {};
17+
}).views(self => ({
18+
get _perItemResult() {
19+
return self.annotation.results.find(r => {
20+
return r.from_name === self && r.area.item_index === self.toNameTag.currentItemIndex;
21+
});
22+
},
23+
}))
24+
.actions(self => ({
25+
createPerItemResult() {
26+
self.createPerObjectResult({
27+
item_index: self.toNameTag.currentItemIndex,
28+
});
29+
},
30+
}));
31+
32+
export default PerItemMixin;

src/mixins/PerRegion.js

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,43 @@ import { types } from 'mobx-state-tree';
22
import { PER_REGION_MODES } from './PerRegionModes';
33

44

5-
/*
6-
* Per Region Mixin
5+
/**
6+
* This mixing defines perRegion control tag's parameter and related basic functionality
7+
* It should be used right after ClassificationBase mixin
8+
* @see ClassificationBase
79
*/
810
const PerRegionMixin = types
911
.model({
1012
perregion: types.optional(types.boolean, false),
1113
whenlabelvalue: types.maybeNull(types.string),
1214
displaymode: types.optional(types.enumeration(Object.values(PER_REGION_MODES)), PER_REGION_MODES.TAG),
15+
}).extend(self => {
16+
/* Validation */
17+
if (self.isClassificationTag !== true) {
18+
throw new Error('The PerRegionMixin mixin should be used only for classification control-tags');
19+
}
20+
return {};
1321
}).volatile(() => {
1422
return {
1523
focusable: false,
1624
};
1725
},
1826
).views(self => ({
27+
get perRegionArea() {
28+
if (!self.perregion) return null;
29+
return self.annotation.highlightedNode;
30+
},
31+
get _perRegionResult() {
32+
const area = self.perRegionArea;
33+
34+
if (!area) return null;
35+
36+
return self.annotation.results.find(r => r.from_name === self && r.area === area);
37+
},
1938
perRegionVisible() {
2039
if (!self.perregion) return true;
2140

22-
const region = self.annotation.highlightedNode;
41+
const region = self.perRegionArea;
2342

2443
if (!region) {
2544
// no region is selected return hidden
@@ -35,7 +54,11 @@ const PerRegionMixin = types
3554
return true;
3655
},
3756
}))
38-
.actions(() => ({}));
57+
.actions(self => ({
58+
createPerRegionResult() {
59+
self.perRegionArea?.setValue(self);
60+
},
61+
}));
3962

4063
export default PerRegionMixin;
4164
export { PER_REGION_MODES } from './PerRegionModes';

src/mixins/Required.js

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { getParent, types } from 'mobx-state-tree';
2+
import { FF_LSDV_4583, isFF } from '../utils/feature-flags';
23

34
const RequiredMixin = types
45
.model({
@@ -13,10 +14,10 @@ const RequiredMixin = types
1314
// validating when choices labeling is done per region,
1415
// for example choice may be required to be selected for
1516
// every bbox
16-
const objectTag = self.annotation.names.get(self.toname);
17+
const objectTag = self.toNameTag;
1718

1819
// if regions don't meet visibility conditions skip validation
19-
for (const reg of objectTag.regs) {
20+
for (const reg of objectTag.allRegs) {
2021
const s = reg.results.find(s => s.from_name === self);
2122

2223
if (self.visiblewhen === 'region-selected') {
@@ -35,6 +36,27 @@ const RequiredMixin = types
3536
self.annotation.selectArea(reg);
3637
self.requiredModal();
3738

39+
return false;
40+
}
41+
}
42+
} else if (isFF(FF_LSDV_4583) && self.peritem) {
43+
// validating when choices labeling is done per item,
44+
const objectTag = self.toNameTag;
45+
const maxItemIndex = objectTag.maxItemIndex;
46+
const existingResultsIndexes = self.annotation.regions
47+
.reduce((existingResultsIndexes, reg)=>{
48+
const result = reg.results.find(s => s.from_name === self);
49+
50+
if (result?.hasValue) {
51+
existingResultsIndexes.add(reg.item_index);
52+
}
53+
return existingResultsIndexes;
54+
}, new Set());
55+
56+
for (let idx = 0; idx <= maxItemIndex; idx++) {
57+
if (!existingResultsIndexes.has(idx)) {
58+
objectTag.setCurrentItem(idx);
59+
self.requiredModal();
3860
return false;
3961
}
4062
}

src/stores/Annotation/Annotation.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -862,7 +862,7 @@ export const Annotation = types
862862
// happening locally. So to reproduce you have to test in production or environment
863863
const area = self?.areas?.put(areaRaw);
864864

865-
object?.afterResultCreated?.(area);
865+
objectTag?.afterResultCreated?.(area);
866866

867867
if (!area) return;
868868

@@ -1064,12 +1064,15 @@ export const Annotation = types
10641064

10651065
self.areas.forEach(a => {
10661066
const controlName = a.results[0].from_name.name;
1067+
// May be null but null is also valid key in this case
1068+
const itemIndex = a.item_index;
10671069

10681070
if (a.classification) {
1069-
if (classificationAreasByControlName[controlName]) {
1070-
duplicateAreaIds.push(classificationAreasByControlName[controlName]);
1071+
if (classificationAreasByControlName[controlName]?.[itemIndex]) {
1072+
duplicateAreaIds.push(classificationAreasByControlName[controlName][itemIndex]);
10711073
}
1072-
classificationAreasByControlName[controlName] = a.id;
1074+
classificationAreasByControlName[controlName] = classificationAreasByControlName[controlName] || {};
1075+
classificationAreasByControlName[controlName][itemIndex] = a.id;
10731076
}
10741077
});
10751078
duplicateAreaIds.forEach(id => self.areas.delete(id));

src/stores/Annotation/store.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,7 @@ const AnnotationStoreModel = types
420420
const updatedItem = item ?? self.selected;
421421

422422
Array.from(updatedItem.names.values())
423-
.filter(t => t.isClassification)
423+
.filter(t => t.isClassificationTag)
424424
.forEach(t => t.updateFromResult([]));
425425

426426
updatedItem?.results

src/tags/TagBase.js

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,6 @@
11
import { types } from 'mobx-state-tree';
2-
import { isDefined } from '../utils/utilities';
32

43
const BaseTag = types
5-
.model('BaseTag')
6-
.views((self) => ({
7-
/**
8-
* Identify classification type tags
9-
*
10-
* `perRegionVisible` is only available for classification type tags
11-
*/
12-
get isClassification() {
13-
return isDefined(self.perRegionVisible);
14-
},
15-
}));
4+
.model('BaseTag');
165

176
export { BaseTag };

src/tags/control/Base.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,18 @@ const ControlBase = types.model({
3131
get valueType() {
3232
return self.type;
3333
},
34+
35+
get toNameTag() {
36+
return self.annotation.names.get(self.toname);
37+
},
38+
39+
selectedValues() {
40+
throw new Error('Control tag needs to implement selectedValues method in views');
41+
},
42+
43+
get result() {
44+
return self.annotation.results.find(r => r.from_name === self);
45+
},
3446
}));
3547

3648
export default types.compose(ControlBase, BaseTag);

0 commit comments

Comments
 (0)