Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: OPTIC-1553: Add URL-based region visibility, hiding all but the specified region on load #6880

Merged
merged 16 commits into from
Feb 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 54 additions & 13 deletions web/libs/datamanager/src/stores/AppStore.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { destroy, flow, types } from "mobx-state-tree";
import { Modal } from "../components/Common/Modal/Modal";
import { FF_DEV_2887, FF_LOPS_E_3, isFF } from "../utils/feature-flags";
import { FF_DEV_2887, FF_LOPS_E_3, FF_REGION_VISIBILITY_FROM_URL, isFF } from "../utils/feature-flags";
import { History } from "../utils/history";
import { isDefined } from "../utils/utils";
import { Action } from "./Action";
Expand Down Expand Up @@ -199,7 +199,22 @@ export const AppStore = types

setTask: flow(function* ({ taskID, annotationID, pushState }) {
if (pushState !== false) {
History.navigate({ task: taskID, annotation: annotationID ?? null, interaction: null });
History.navigate({
task: taskID,
annotation: annotationID ?? null,
interaction: null,
region: null,
});
} else if (isFF(FF_REGION_VISIBILITY_FROM_URL)) {
const { task, region, annotation } = History.getParams();
History.navigate(
{
task,
region,
annotation,
},
true,
);
}

if (!isDefined(taskID)) return;
Expand All @@ -216,20 +231,41 @@ export const AppStore = types
self.annotationStore.setSelected(annotationID);
} else {
self.taskStore.setSelected(taskID);
}
bmartel marked this conversation as resolved.
Show resolved Hide resolved

const taskPromise = self.taskStore.loadTask(taskID, {
select: !!taskID && !!annotationID,
});
const taskPromise = self.taskStore.loadTask(taskID, {
select: !!taskID && !!annotationID,
});

taskPromise.then(() => {
const annotation = self.LSF?.currentAnnotation;
const id = annotation?.pk ?? annotation?.id;
taskPromise.then(() => {
const annotation = self.LSF?.currentAnnotation;
const id = annotation?.pk ?? annotation?.id;

self.LSF?.setLSFTask(self.taskStore.selected, id);
self.LSF?.setLSFTask(self.taskStore.selected, id);

self.setLoadingData(false);
});
}
if (isFF(FF_REGION_VISIBILITY_FROM_URL)) {
const { annotation: annIDFromUrl, region: regionIDFromUrl } = History.getParams();
if (annIDFromUrl) {
const lsfAnnotation = self.LSF.lsf.annotationStore.annotations.find((a) => {
return a.pk === annIDFromUrl || a.id === annIDFromUrl;
});

if (lsfAnnotation) {
const annID = lsfAnnotation.pk ?? lsfAnnotation.id;
self.LSF?.setLSFTask(self.taskStore.selected, annID);
}
}
if (regionIDFromUrl) {
const currentAnn = self.LSF?.currentAnnotation;
// Focus on the region by hiding all other regions
currentAnn?.regionStore?.setRegionVisible(regionIDFromUrl);
ddishi marked this conversation as resolved.
Show resolved Hide resolved
// Select the region so outliner details are visible
currentAnn?.regionStore?.selectRegionByID(regionIDFromUrl);
}
}

self.setLoadingData(false);
});
}),

setLoadingData(value) {
Expand Down Expand Up @@ -379,7 +415,7 @@ export const AppStore = types
},

handlePopState: (({ state }) => {
const { tab, task, annotation, labeling } = state ?? {};
const { tab, task, annotation, labeling, region } = state ?? {};

if (tab) {
const tabId = Number.parseInt(tab);
Expand All @@ -399,6 +435,11 @@ export const AppStore = types
} else {
params.id = Number.parseInt(task);
}
if (region) {
params.region = region;
} else {
delete params.region;
}

self.startLabeling(params, { pushState: false });
} else if (labeling) {
Expand Down
5 changes: 5 additions & 0 deletions web/libs/datamanager/src/utils/feature-flags.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ export const FF_GRID_PREVIEW = "fflag_feat_front_leap_1424_grid_preview_short";

export const FF_MEMORY_LEAK_FIX = "fflag_feat_all_optic_1178_reduce_memory_leak_short";

/**
* Add ability to show specific region from URL params (by hiding all other regions).
*/
export const FF_REGION_VISIBILITY_FROM_URL = "fflag_feat_front_optic_1553_url_based_region_visibility_short";

// Customize flags
const flags = {};

Expand Down
35 changes: 34 additions & 1 deletion web/libs/editor/src/stores/RegionStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -507,17 +507,26 @@ export default types
},

findRegionID(id) {
if (!id) return null;
return self.regions.find((r) => r.id === id);
},

findRegion(id) {
return self.regions.find((r) => r.id === id);
return self.findRegionID(id);
},

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

normalizeRegionID(regionId) {
if (!regionId) return "";
if (!regionId.includes("#")) {
regionId = `${regionId}#${self.annotation.id}`;
}
return regionId;
},

afterCreate() {
onPatch(self, (patch) => {
if ((patch.op === "add" || patch.op === "delete") && patch.path.indexOf("/regions/") !== -1) {
Expand Down Expand Up @@ -582,13 +591,36 @@ export default types
}
});
},

selectRegionByID(regionId) {
const normalizedRegionId = self.normalizeRegionID(regionId);
const targetRegion = self.findRegionID(normalizedRegionId);
if (!targetRegion) return;
self.toggleSelection(targetRegion, true);
},

setRegionVisible(regionId) {
const normalizedRegionId = self.normalizeRegionID(regionId);
const targetRegion = self.findRegionID(normalizedRegionId);
if (!targetRegion) return;

self.regions.forEach((area) => {
if (!area.hidden) {
area.toggleHidden();
}
});

targetRegion.toggleHidden();
},

setHiddenByTool(shouldBeHidden, label) {
self.regions.forEach((area) => {
if (area.hidden !== shouldBeHidden && area.type === label.type) {
area.toggleHidden();
}
});
},

setHiddenByLabel(shouldBeHidden, label) {
self.regions.forEach((area) => {
if (area.hidden !== shouldBeHidden) {
Expand All @@ -604,6 +636,7 @@ export default types
}
});
},

highlight(area) {
self.selection.highlight(area);
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ export const simpleRegionsConfig = `<View>
<Labels name="label" toName="text">
<Label value="Label 1"/>
<Label value="Label 2"/>
<Label value="Label 2"/>
<Label value="Label 3"/>
</Labels>
</View>`;

Expand Down
92 changes: 92 additions & 0 deletions web/libs/editor/tests/integration/e2e/outliner/hide-all.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,97 @@ describe("Outliner - Hide all regions", () => {
Sidebar.hasHiddenRegion(3);
});

it("should hide all regions except the target region by ID from param", () => {
LabelStudio.params()
.config(simpleRegionsConfig)
.data(simpleRegionsData)
.withResult(simpleRegionsResult)
.withParam("region", "label_2")
.init();

cy.window().then((window: any | unknown) => {
window.Htx.annotationStore.annotations[0].regionStore.setRegionVisible(window.LSF_CONFIG.region);
});

Sidebar.hasRegions(3);
Sidebar.hasHiddenRegion(2);

Sidebar.assertRegionHidden(0, "Label 1", true);
Sidebar.assertRegionHidden(1, "Label 2", false);
Sidebar.assertRegionHidden(2, "Label 3", true);
});

it("should hide all regions except the target region by ID within the targeted annotation tab specified by param", () => {
LabelStudio.params()
.config(simpleRegionsConfig)
.data(simpleRegionsData)
.withAnnotation({ id: "10", result: simpleRegionsResult })
.withAnnotation({ id: "20", result: simpleRegionsResult })
.withParam("annotation", "10")
.withParam("region", "label_2")
.init();

cy.window().then((window: any | unknown) => {
const annIdFromParam = window.LSF_CONFIG.annotation;
const annotations = window.Htx.annotationStore.annotations;
const lsfAnnotation = annotations.find((ann: any) => ann.pk === annIdFromParam || ann.id === annIdFromParam);
const annID = lsfAnnotation.pk ?? lsfAnnotation.id;

expect(annID).to.equal("10");

// Move to the annotation tab specified by param
cy.get('[class="lsf-annotations-list__toggle"]').click();
cy.get('[class="lsf-annotations-list__entity-id"]').contains("10").click();

annotations[1].regionStore.setRegionVisible(window.LSF_CONFIG.region);
});

Sidebar.hasRegions(3);
Sidebar.hasHiddenRegion(2);

Sidebar.assertRegionHidden(0, "Label 1", true);
Sidebar.assertRegionHidden(1, "Label 2", false);
Sidebar.assertRegionHidden(2, "Label 3", true);
});

it("should not hide regions in the non-targeted annotaion tab", () => {
LabelStudio.params()
.config(simpleRegionsConfig)
.data(simpleRegionsData)
.withAnnotation({ id: "10", result: simpleRegionsResult })
.withAnnotation({ id: "20", result: simpleRegionsResult })
.withParam("annotation", "10")
.withParam("region", "label_2")
.init();

cy.window().then((window: any | unknown) => {
window.Htx.annotationStore.annotations[1].regionStore.setRegionVisible(window.LSF_CONFIG.region);
});

// Validate the annotation tab
cy.get('[class="lsf-annotations-list__entity-id"]').should("contain.text", "20");

Sidebar.hasRegions(3);
Sidebar.hasHiddenRegion(0);
});

it("should select the target region by ID from param", () => {
LabelStudio.params()
.config(simpleRegionsConfig)
.data(simpleRegionsData)
.withResult(simpleRegionsResult)
.withParam("region", "label_2")
.init();

cy.window().then((window: any | unknown) => {
window.Htx.annotationStore.annotations[0].regionStore.selectRegionByID(window.LSF_CONFIG.region);
});

Sidebar.hasRegions(3);
Sidebar.hasSelectedRegions(1);
Sidebar.hasSelectedRegion(1);
});

it("should have tooltip for hide action", () => {
LabelStudio.params().config(simpleRegionsConfig).data(simpleRegionsData).withResult(simpleRegionsResult).init();

Expand All @@ -61,6 +152,7 @@ describe("Outliner - Hide all regions", () => {
Sidebar.showAllRegionsButton.trigger("mouseenter");
Tooltip.hasText("Show all regions");
});

it("should react to changes in regions' visibility", () => {
LabelStudio.params().config(simpleRegionsConfig).data(simpleRegionsData).withResult(simpleRegionsResult).init();

Expand Down
4 changes: 4 additions & 0 deletions web/libs/frontend-test/src/helpers/LSF/Sidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,8 @@ export const Sidebar = {
expandDetailsRightPanel() {
cy.get(".lsf-sidepanels__wrapper_align_right .lsf-panel__header").should("be.visible").click();
},
assertRegionHidden(idx: number, id: string, shouldBeHidden: boolean) {
const expectation = shouldBeHidden ? "have.class" : "not.have.class";
this.findRegionByIndex(idx).should("contain.text", id).parent().should(expectation, "lsf-tree__node_hidden");
},
};
19 changes: 11 additions & 8 deletions web/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -264,15 +264,18 @@ module.exports = composePlugins(
publicPath: `${FRONTEND_HOSTNAME}/react-app/`,
},
allowedHosts: "all", // Allow access from Django's server
proxy: [
{
context: ["/api"],
target: DJANGO_HOSTNAME,
proxy: {
"/api": {
target: `${DJANGO_HOSTNAME}/api`,
changeOrigin: true,
pathRewrite: { "^/api": "" },
secure: false,
},
"/": {
target: `${DJANGO_HOSTNAME}`,
changeOrigin: true,
secure: false,
},
],
historyApiFallback: {
index: "/index.html",
disableDotRule: true,
},
},
});
Expand Down
Loading