Skip to content

Commit b02dfd4

Browse files
ddishibmartel
andauthored
feat: OPTIC-1553: Add URL-based region visibility, hiding all but the specified region on load (#6880)
Co-authored-by: Brandon Martel <[email protected]>
1 parent 78a8a67 commit b02dfd4

File tree

7 files changed

+201
-23
lines changed

7 files changed

+201
-23
lines changed

web/libs/datamanager/src/stores/AppStore.js

Lines changed: 54 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { destroy, flow, types } from "mobx-state-tree";
22
import { Modal } from "../components/Common/Modal/Modal";
3-
import { FF_DEV_2887, FF_LOPS_E_3, isFF } from "../utils/feature-flags";
3+
import { FF_DEV_2887, FF_LOPS_E_3, FF_REGION_VISIBILITY_FROM_URL, isFF } from "../utils/feature-flags";
44
import { History } from "../utils/history";
55
import { isDefined } from "../utils/utils";
66
import { Action } from "./Action";
@@ -199,7 +199,22 @@ export const AppStore = types
199199

200200
setTask: flow(function* ({ taskID, annotationID, pushState }) {
201201
if (pushState !== false) {
202-
History.navigate({ task: taskID, annotation: annotationID ?? null, interaction: null });
202+
History.navigate({
203+
task: taskID,
204+
annotation: annotationID ?? null,
205+
interaction: null,
206+
region: null,
207+
});
208+
} else if (isFF(FF_REGION_VISIBILITY_FROM_URL)) {
209+
const { task, region, annotation } = History.getParams();
210+
History.navigate(
211+
{
212+
task,
213+
region,
214+
annotation,
215+
},
216+
true,
217+
);
203218
}
204219

205220
if (!isDefined(taskID)) return;
@@ -216,20 +231,41 @@ export const AppStore = types
216231
self.annotationStore.setSelected(annotationID);
217232
} else {
218233
self.taskStore.setSelected(taskID);
234+
}
219235

220-
const taskPromise = self.taskStore.loadTask(taskID, {
221-
select: !!taskID && !!annotationID,
222-
});
236+
const taskPromise = self.taskStore.loadTask(taskID, {
237+
select: !!taskID && !!annotationID,
238+
});
223239

224-
taskPromise.then(() => {
225-
const annotation = self.LSF?.currentAnnotation;
226-
const id = annotation?.pk ?? annotation?.id;
240+
taskPromise.then(() => {
241+
const annotation = self.LSF?.currentAnnotation;
242+
const id = annotation?.pk ?? annotation?.id;
227243

228-
self.LSF?.setLSFTask(self.taskStore.selected, id);
244+
self.LSF?.setLSFTask(self.taskStore.selected, id);
229245

230-
self.setLoadingData(false);
231-
});
232-
}
246+
if (isFF(FF_REGION_VISIBILITY_FROM_URL)) {
247+
const { annotation: annIDFromUrl, region: regionIDFromUrl } = History.getParams();
248+
if (annIDFromUrl) {
249+
const lsfAnnotation = self.LSF.lsf.annotationStore.annotations.find((a) => {
250+
return a.pk === annIDFromUrl || a.id === annIDFromUrl;
251+
});
252+
253+
if (lsfAnnotation) {
254+
const annID = lsfAnnotation.pk ?? lsfAnnotation.id;
255+
self.LSF?.setLSFTask(self.taskStore.selected, annID);
256+
}
257+
}
258+
if (regionIDFromUrl) {
259+
const currentAnn = self.LSF?.currentAnnotation;
260+
// Focus on the region by hiding all other regions
261+
currentAnn?.regionStore?.setRegionVisible(regionIDFromUrl);
262+
// Select the region so outliner details are visible
263+
currentAnn?.regionStore?.selectRegionByID(regionIDFromUrl);
264+
}
265+
}
266+
267+
self.setLoadingData(false);
268+
});
233269
}),
234270

235271
setLoadingData(value) {
@@ -379,7 +415,7 @@ export const AppStore = types
379415
},
380416

381417
handlePopState: (({ state }) => {
382-
const { tab, task, annotation, labeling } = state ?? {};
418+
const { tab, task, annotation, labeling, region } = state ?? {};
383419

384420
if (tab) {
385421
const tabId = Number.parseInt(tab);
@@ -399,6 +435,11 @@ export const AppStore = types
399435
} else {
400436
params.id = Number.parseInt(task);
401437
}
438+
if (region) {
439+
params.region = region;
440+
} else {
441+
delete params.region;
442+
}
402443

403444
self.startLabeling(params, { pushState: false });
404445
} else if (labeling) {

web/libs/datamanager/src/utils/feature-flags.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@ export const FF_GRID_PREVIEW = "fflag_feat_front_leap_1424_grid_preview_short";
7373

7474
export const FF_MEMORY_LEAK_FIX = "fflag_feat_all_optic_1178_reduce_memory_leak_short";
7575

76+
/**
77+
* Add ability to show specific region from URL params (by hiding all other regions).
78+
*/
79+
export const FF_REGION_VISIBILITY_FROM_URL = "fflag_feat_front_optic_1553_url_based_region_visibility_short";
80+
7681
// Customize flags
7782
const flags = {};
7883

web/libs/editor/src/stores/RegionStore.js

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -507,17 +507,26 @@ export default types
507507
},
508508

509509
findRegionID(id) {
510+
if (!id) return null;
510511
return self.regions.find((r) => r.id === id);
511512
},
512513

513514
findRegion(id) {
514-
return self.regions.find((r) => r.id === id);
515+
return self.findRegionID(id);
515516
},
516517

517518
filterByParentID(id) {
518519
return self.regions.filter((r) => r.parentID === id);
519520
},
520521

522+
normalizeRegionID(regionId) {
523+
if (!regionId) return "";
524+
if (!regionId.includes("#")) {
525+
regionId = `${regionId}#${self.annotation.id}`;
526+
}
527+
return regionId;
528+
},
529+
521530
afterCreate() {
522531
onPatch(self, (patch) => {
523532
if ((patch.op === "add" || patch.op === "delete") && patch.path.indexOf("/regions/") !== -1) {
@@ -582,13 +591,36 @@ export default types
582591
}
583592
});
584593
},
594+
595+
selectRegionByID(regionId) {
596+
const normalizedRegionId = self.normalizeRegionID(regionId);
597+
const targetRegion = self.findRegionID(normalizedRegionId);
598+
if (!targetRegion) return;
599+
self.toggleSelection(targetRegion, true);
600+
},
601+
602+
setRegionVisible(regionId) {
603+
const normalizedRegionId = self.normalizeRegionID(regionId);
604+
const targetRegion = self.findRegionID(normalizedRegionId);
605+
if (!targetRegion) return;
606+
607+
self.regions.forEach((area) => {
608+
if (!area.hidden) {
609+
area.toggleHidden();
610+
}
611+
});
612+
613+
targetRegion.toggleHidden();
614+
},
615+
585616
setHiddenByTool(shouldBeHidden, label) {
586617
self.regions.forEach((area) => {
587618
if (area.hidden !== shouldBeHidden && area.type === label.type) {
588619
area.toggleHidden();
589620
}
590621
});
591622
},
623+
592624
setHiddenByLabel(shouldBeHidden, label) {
593625
self.regions.forEach((area) => {
594626
if (area.hidden !== shouldBeHidden) {
@@ -604,6 +636,7 @@ export default types
604636
}
605637
});
606638
},
639+
607640
highlight(area) {
608641
self.selection.highlight(area);
609642
},

web/libs/editor/tests/integration/data/outliner/hide-all.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ export const simpleRegionsConfig = `<View>
33
<Labels name="label" toName="text">
44
<Label value="Label 1"/>
55
<Label value="Label 2"/>
6-
<Label value="Label 2"/>
6+
<Label value="Label 3"/>
77
</Labels>
88
</View>`;
99

web/libs/editor/tests/integration/e2e/outliner/hide-all.cy.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,97 @@ describe("Outliner - Hide all regions", () => {
4545
Sidebar.hasHiddenRegion(3);
4646
});
4747

48+
it("should hide all regions except the target region by ID from param", () => {
49+
LabelStudio.params()
50+
.config(simpleRegionsConfig)
51+
.data(simpleRegionsData)
52+
.withResult(simpleRegionsResult)
53+
.withParam("region", "label_2")
54+
.init();
55+
56+
cy.window().then((window: any | unknown) => {
57+
window.Htx.annotationStore.annotations[0].regionStore.setRegionVisible(window.LSF_CONFIG.region);
58+
});
59+
60+
Sidebar.hasRegions(3);
61+
Sidebar.hasHiddenRegion(2);
62+
63+
Sidebar.assertRegionHidden(0, "Label 1", true);
64+
Sidebar.assertRegionHidden(1, "Label 2", false);
65+
Sidebar.assertRegionHidden(2, "Label 3", true);
66+
});
67+
68+
it("should hide all regions except the target region by ID within the targeted annotation tab specified by param", () => {
69+
LabelStudio.params()
70+
.config(simpleRegionsConfig)
71+
.data(simpleRegionsData)
72+
.withAnnotation({ id: "10", result: simpleRegionsResult })
73+
.withAnnotation({ id: "20", result: simpleRegionsResult })
74+
.withParam("annotation", "10")
75+
.withParam("region", "label_2")
76+
.init();
77+
78+
cy.window().then((window: any | unknown) => {
79+
const annIdFromParam = window.LSF_CONFIG.annotation;
80+
const annotations = window.Htx.annotationStore.annotations;
81+
const lsfAnnotation = annotations.find((ann: any) => ann.pk === annIdFromParam || ann.id === annIdFromParam);
82+
const annID = lsfAnnotation.pk ?? lsfAnnotation.id;
83+
84+
expect(annID).to.equal("10");
85+
86+
// Move to the annotation tab specified by param
87+
cy.get('[class="lsf-annotations-list__toggle"]').click();
88+
cy.get('[class="lsf-annotations-list__entity-id"]').contains("10").click();
89+
90+
annotations[1].regionStore.setRegionVisible(window.LSF_CONFIG.region);
91+
});
92+
93+
Sidebar.hasRegions(3);
94+
Sidebar.hasHiddenRegion(2);
95+
96+
Sidebar.assertRegionHidden(0, "Label 1", true);
97+
Sidebar.assertRegionHidden(1, "Label 2", false);
98+
Sidebar.assertRegionHidden(2, "Label 3", true);
99+
});
100+
101+
it("should not hide regions in the non-targeted annotaion tab", () => {
102+
LabelStudio.params()
103+
.config(simpleRegionsConfig)
104+
.data(simpleRegionsData)
105+
.withAnnotation({ id: "10", result: simpleRegionsResult })
106+
.withAnnotation({ id: "20", result: simpleRegionsResult })
107+
.withParam("annotation", "10")
108+
.withParam("region", "label_2")
109+
.init();
110+
111+
cy.window().then((window: any | unknown) => {
112+
window.Htx.annotationStore.annotations[1].regionStore.setRegionVisible(window.LSF_CONFIG.region);
113+
});
114+
115+
// Validate the annotation tab
116+
cy.get('[class="lsf-annotations-list__entity-id"]').should("contain.text", "20");
117+
118+
Sidebar.hasRegions(3);
119+
Sidebar.hasHiddenRegion(0);
120+
});
121+
122+
it("should select the target region by ID from param", () => {
123+
LabelStudio.params()
124+
.config(simpleRegionsConfig)
125+
.data(simpleRegionsData)
126+
.withResult(simpleRegionsResult)
127+
.withParam("region", "label_2")
128+
.init();
129+
130+
cy.window().then((window: any | unknown) => {
131+
window.Htx.annotationStore.annotations[0].regionStore.selectRegionByID(window.LSF_CONFIG.region);
132+
});
133+
134+
Sidebar.hasRegions(3);
135+
Sidebar.hasSelectedRegions(1);
136+
Sidebar.hasSelectedRegion(1);
137+
});
138+
48139
it("should have tooltip for hide action", () => {
49140
LabelStudio.params().config(simpleRegionsConfig).data(simpleRegionsData).withResult(simpleRegionsResult).init();
50141

@@ -61,6 +152,7 @@ describe("Outliner - Hide all regions", () => {
61152
Sidebar.showAllRegionsButton.trigger("mouseenter");
62153
Tooltip.hasText("Show all regions");
63154
});
155+
64156
it("should react to changes in regions' visibility", () => {
65157
LabelStudio.params().config(simpleRegionsConfig).data(simpleRegionsData).withResult(simpleRegionsResult).init();
66158

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,4 +93,8 @@ export const Sidebar = {
9393
expandDetailsRightPanel() {
9494
cy.get(".lsf-sidepanels__wrapper_align_right .lsf-panel__header").should("be.visible").click();
9595
},
96+
assertRegionHidden(idx: number, id: string, shouldBeHidden: boolean) {
97+
const expectation = shouldBeHidden ? "have.class" : "not.have.class";
98+
this.findRegionByIndex(idx).should("contain.text", id).parent().should(expectation, "lsf-tree__node_hidden");
99+
},
96100
};

web/webpack.config.js

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -264,15 +264,18 @@ module.exports = composePlugins(
264264
publicPath: `${FRONTEND_HOSTNAME}/react-app/`,
265265
},
266266
allowedHosts: "all", // Allow access from Django's server
267-
proxy: [
268-
{
269-
context: ["/api"],
270-
target: DJANGO_HOSTNAME,
267+
proxy: {
268+
"/api": {
269+
target: `${DJANGO_HOSTNAME}/api`,
270+
changeOrigin: true,
271+
pathRewrite: { "^/api": "" },
272+
secure: false,
273+
},
274+
"/": {
275+
target: `${DJANGO_HOSTNAME}`,
276+
changeOrigin: true,
277+
secure: false,
271278
},
272-
],
273-
historyApiFallback: {
274-
index: "/index.html",
275-
disableDotRule: true,
276279
},
277280
},
278281
});

0 commit comments

Comments
 (0)