Skip to content

Commit 40afcf3

Browse files
Gondragoshlomzik
andauthored
fix: BROS-291: Fix using Taxonomy as second labeling control (#8103)
Co-authored-by: Andrew <hlomzik@gmail.com> Co-authored-by: Gondragos <Gondragos@users.noreply.github.com>
1 parent a71e95c commit 40afcf3

File tree

10 files changed

+236
-10
lines changed

10 files changed

+236
-10
lines changed

web/libs/editor/src/core/Helpers.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,7 @@ export function cloneNode(node: IStateTreeNode) {
2424
id: guidGenerator(),
2525
});
2626

27+
snapshotRandomId.afterClone?.(node);
28+
2729
return snapshotRandomId;
2830
}

web/libs/editor/src/core/Types.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ function isType(node, types) {
6868
}
6969

7070
function getParentOfTypeString(node, str) {
71+
if (isRoot(node)) return null;
72+
7173
// same as getParentOfType but checks models .name instead of type
7274
let parent = getParent(node);
7375

web/libs/editor/src/mixins/SharedChoiceStore/mixin.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ export const SharedStoreMixin = types
7979
const store = Stores.get(self.storeId);
8080
const annotationStore = Types.getParentOfTypeString(self, "AnnotationStore");
8181

82+
// It means that an element is not connected to the store tree,
83+
// most probably as it is a temporal clone of the model
84+
if (!annotationStore) return;
8285
annotationStore.addSharedStore(store);
8386
StoreIds.add(self.storeId);
8487
self.store = self.storeId;

web/libs/editor/src/tags/control/Taxonomy/Taxonomy.jsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,10 @@ const Model = types
360360
}
361361
},
362362

363+
afterClone(node) {
364+
self.selected = [...node.selected];
365+
},
366+
363367
/**
364368
* Load items from `apiUrl` and set them indirectly to `items` (via `_items`)
365369
* @param {string[]} path to load nested items by this path

web/libs/editor/src/tags/object/Video/Rectangle.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { LabelOnVideoBbox } from "../../../components/ImageView/LabelOnRegion";
1010
type RectPropsExtend = typeof Rect;
1111

1212
interface RectProps extends RectPropsExtend {
13+
id: string;
1314
reg: any;
1415
frame: number;
1516
selected: boolean;
@@ -21,6 +22,7 @@ interface RectProps extends RectPropsExtend {
2122
}
2223

2324
const RectanglePure: FC<RectProps> = ({
25+
id,
2426
reg,
2527
box,
2628
frame,
@@ -59,7 +61,7 @@ const RectanglePure: FC<RectProps> = ({
5961
};
6062

6163
return (
62-
<Group>
64+
<Group id={id}>
6365
<LabelOnVideoBbox
6466
reg={reg}
6567
box={newBox}

web/libs/editor/src/tags/object/Video/VideoRegions.jsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,13 +258,14 @@ const RegionsLayer = observer(({ regions, item, locked, isDrawing, workinAreaCoo
258258
);
259259
});
260260

261-
const Shape = observer(({ reg, frame, stageRef, ...props }) => {
261+
const Shape = observer(({ id, reg, frame, stageRef, ...props }) => {
262262
const box = reg.getShape(frame);
263263

264264
return (
265265
reg.isInLifespan(frame) &&
266266
box && (
267267
<Rectangle
268+
id={id}
268269
reg={reg}
269270
box={box}
270271
frame={frame}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
export const textWithDualTaxonomyConfig = `<View>
2+
<Text name="text" value="$text"/>
3+
<Taxonomy name="category" toName="text" labeling="true">
4+
<Choice value="Person"/>
5+
<Choice value="Organization"/>
6+
<Choice value="Location"/>
7+
<Choice value="Event"/>
8+
</Taxonomy>
9+
<Taxonomy name="sentiment" toName="text" labeling="true">
10+
<Choice value="Positive"/>
11+
<Choice value="Negative"/>
12+
<Choice value="Neutral"/>
13+
</Taxonomy>
14+
</View>`;
15+
16+
export const simpleTextData = {
17+
text: "Apple Inc. is a technology company headquartered in Cupertino, California. The company was founded by Steve Jobs in 1976.",
18+
};
19+
20+
export const expectedResult = [
21+
{
22+
id: "taxonomy_category_1",
23+
type: "taxonomy",
24+
value: {
25+
taxonomy: [["Organization"]],
26+
},
27+
origin: "manual",
28+
to_name: "text",
29+
from_name: "category",
30+
},
31+
{
32+
id: "taxonomy_sentiment_1",
33+
type: "taxonomy",
34+
value: {
35+
taxonomy: [["Positive"]],
36+
},
37+
origin: "manual",
38+
to_name: "text",
39+
from_name: "sentiment",
40+
},
41+
];
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { LabelStudio, Sidebar, useTaxonomy } from "@humansignal/frontend-test/helpers/LSF";
2+
import { RichText } from "@humansignal/frontend-test/helpers/LSF/RichText";
3+
import { FF_TAXONOMY_LABELING } from "../../../../src/utils/feature-flags";
4+
import { textWithDualTaxonomyConfig, simpleTextData } from "../../data/control_tags/text-with-dual-taxonomy";
5+
6+
describe("Control Tags - Text with Dual Taxonomy", () => {
7+
it("should select options from two taxonomies and create text region", () => {
8+
LabelStudio.addFeatureFlagsOnPageLoad({
9+
[FF_TAXONOMY_LABELING]: true,
10+
});
11+
12+
cy.log("Initialize LabelStudio with text and dual taxonomy configuration");
13+
LabelStudio.params().config(textWithDualTaxonomyConfig).data(simpleTextData).withResult([]).init();
14+
15+
LabelStudio.waitForObjectsReady();
16+
Sidebar.hasNoRegions();
17+
18+
// Create taxonomy helpers for each taxonomy by index (first taxonomy, second taxonomy)
19+
const categoryTaxonomy = useTaxonomy("&:eq(0)");
20+
const sentimentTaxonomy = useTaxonomy("&:eq(1)");
21+
22+
cy.log("Select first taxonomy option - Organization from category taxonomy");
23+
categoryTaxonomy.open();
24+
categoryTaxonomy.findItem("Organization").click();
25+
categoryTaxonomy.close();
26+
27+
cy.log("Select second taxonomy option - Positive from sentiment taxonomy");
28+
sentimentTaxonomy.open();
29+
sentimentTaxonomy.findItem("Positive").click();
30+
sentimentTaxonomy.close();
31+
32+
cy.log("Create text region with selected taxonomies");
33+
RichText.selectText("Apple Inc.");
34+
35+
cy.log("Verify region was created");
36+
Sidebar.hasRegions(1);
37+
RichText.hasRegionWithText("Apple Inc.");
38+
39+
cy.log("Verify serialization contains taxonomy results");
40+
LabelStudio.serialize().then((results: any[]) => {
41+
expect(results).to.have.length(2);
42+
43+
// Check category taxonomy result
44+
const categoryResult = results.find((r: any) => r.from_name === "category");
45+
expect(categoryResult).to.exist;
46+
expect(categoryResult.type).to.eq("taxonomy");
47+
expect(categoryResult.value.taxonomy).to.deep.eq([["Organization"]]);
48+
49+
// Check sentiment taxonomy result
50+
const sentimentResult = results.find((r: any) => r.from_name === "sentiment");
51+
expect(sentimentResult).to.exist;
52+
expect(sentimentResult.type).to.eq("taxonomy");
53+
expect(sentimentResult.value.taxonomy).to.deep.eq([["Positive"]]);
54+
});
55+
});
56+
});

web/libs/editor/tests/integration/e2e/video/regions.cy.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,18 +77,19 @@ describe("Video segmentation", suiteConfig, () => {
7777

7878
cy.log("Remember an empty canvas state");
7979
VideoView.clickAtFrame(4);
80-
cy.wait(1000);
80+
VideoView.waitForFrame(4);
81+
VideoView.waitForRegionNotInKonvaByIndex(0);
82+
VideoView.waitForStableState();
8183
VideoView.captureCanvas("canvas");
8284

8385
VideoView.clickAtFrame(3);
84-
cy.wait(TWO_FRAMES_TIMEOUT);
86+
VideoView.waitForRegionInKonvaByIndex(0);
87+
VideoView.waitForStableState();
8588
cy.log("Select region");
8689
VideoView.clickAtRelative(0.5, 0.5);
87-
cy.wait(TWO_FRAMES_TIMEOUT);
8890
Sidebar.hasSelectedRegions(1);
89-
cy.wait(TWO_FRAMES_TIMEOUT);
9091
VideoView.clickAtFrame(4);
91-
cy.wait(TWO_FRAMES_TIMEOUT);
92+
VideoView.waitForFrame(4); // Wait for frame 4
9293
Sidebar.hasSelectedRegions(1);
9394

9495
cy.wait(1000);

web/libs/frontend-test/src/helpers/LSF/VideoView.ts

Lines changed: 117 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -262,9 +262,123 @@ export const VideoView = {
262262

263263
// Get current frame number from timeline controls
264264
getCurrentFrame() {
265-
return this.frameCounter.invoke("text").then((text) => {
266-
const match = text.match(/(\d+) of/);
267-
return match ? Number.parseInt(match[1]) : null;
265+
return this.frameCounter.invoke("text").then((text) => Number.parseInt(text.split(" ")[0]));
266+
},
267+
268+
// Wait a couple of animation frames based on requestAnimationFrame pattern
269+
waitForStableState() {
270+
// This ensures React has completed its render cycle
271+
cy.window().then((win) => {
272+
return new Cypress.Promise((resolve) => {
273+
win.requestAnimationFrame(() => {
274+
// Wait one more frame to be extra sure
275+
win.requestAnimationFrame(resolve);
276+
});
277+
});
278+
});
279+
},
280+
281+
// Wait for region by index to be passed to Konva Stage
282+
// Gets region ID from store and checks if it exists in Konva
283+
waitForRegionInKonvaByIndex(regionIndex: number) {
284+
cy.log(`Wait for region at index ${regionIndex} to be available in Konva Stage`);
285+
286+
// First get the region ID from store
287+
cy.window().then((win) => {
288+
const store = (win as any).Htx || (win as any).store;
289+
if (!store?.annotationStore?.selected?.regionStore?.regions) {
290+
cy.log("No regions found in store");
291+
return;
292+
}
293+
294+
const regions = store.annotationStore.selected.regionStore.regions;
295+
if (regionIndex >= regions.length) {
296+
cy.log(`Region index ${regionIndex} out of bounds (${regions.length} regions)`);
297+
return;
298+
}
299+
300+
const region = regions[regionIndex];
301+
const regionId = region.id;
302+
303+
cy.log(`Found region ID: ${regionId} for index ${regionIndex}`);
304+
305+
// Now wait for this region in Konva
306+
this.waitForRegionInKonva(regionId);
307+
});
308+
},
309+
310+
// Find specific Konva stage by DOM element and check for region
311+
waitForRegionInKonva(regionId: string) {
312+
cy.log(`Wait for region ${regionId} to be available in Konva Stage`);
313+
314+
cy.window().then((win) => {
315+
this.drawingArea.should(($drawingArea) => {
316+
// Get the specific Konva stage from this DOM element
317+
const drawingArea = $drawingArea[0];
318+
const stage = (win as any).Konva?.stages?.find((stage) => stage.content === drawingArea);
319+
320+
if (!stage) {
321+
throw new Error("Konva stage not found for this canvas");
322+
}
323+
324+
// Find region in this specific stage
325+
const elements = stage.find(`#${regionId}`);
326+
if (!elements || elements.length === 0) {
327+
throw new Error(`Region ${regionId} not found in Konva Stage`);
328+
}
329+
});
330+
});
331+
},
332+
333+
// Check that specific region is NOT in Konva Stage
334+
waitForRegionNotInKonva(regionId: string) {
335+
cy.log(`Wait for region ${regionId} to be NOT available in Konva Stage`);
336+
337+
cy.window().then((win) => {
338+
this.drawingArea.should(($drawingArea) => {
339+
// Get the specific Konva stage from this DOM element
340+
const drawingArea = $drawingArea[0];
341+
const stage = (win as any).Konva?.stages?.find((stage) => stage.content === drawingArea);
342+
343+
if (!stage) {
344+
// No stage means no regions - that's what we want
345+
return;
346+
}
347+
348+
// Find region in this specific stage
349+
const elements = stage.find(`#${regionId}`);
350+
if (elements && elements.length > 0) {
351+
throw new Error(`Region ${regionId} should NOT be in Konva Stage but it was found`);
352+
}
353+
});
354+
});
355+
},
356+
357+
// Check region by index is NOT in Konva Stage
358+
waitForRegionNotInKonvaByIndex(regionIndex: number) {
359+
cy.log(`Wait for region at index ${regionIndex} to be NOT available in Konva Stage`);
360+
361+
// First get the region ID from store
362+
cy.window().then((win) => {
363+
const store = (win as any).Htx || (win as any).store;
364+
if (!store?.annotationStore?.selected?.regionStore?.regions) {
365+
cy.log("No regions found in store - that's expected");
366+
return;
367+
}
368+
369+
const regions = store.annotationStore.selected.regionStore.regions;
370+
if (regionIndex >= regions.length) {
371+
cy.log(`Region index ${regionIndex} out of bounds (${regions.length} regions) - that's expected`);
372+
return;
373+
}
374+
375+
const region = regions[regionIndex];
376+
const regionId = region.id;
377+
378+
cy.log(`Checking that region ID: ${regionId} for index ${regionIndex} is NOT in Konva`);
379+
380+
// Now check this region is NOT in Konva
381+
this.waitForRegionNotInKonva(regionId);
268382
});
269383
},
270384

0 commit comments

Comments
 (0)