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

Fix doubled text after deserialisation #85

Merged
merged 5 commits into from
May 27, 2020
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
2 changes: 2 additions & 0 deletions e2e/tests/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
* @param {function} done
*/
const initLabelStudio = async ({ config, data, completions = [{ result: [] }], predictions = [] }, done) => {
if (window.Konva && window.Konva.stages.length) window.Konva.stages.forEach(stage => stage.destroy());

const interfaces = [
"panel",
"update",
Expand Down
114 changes: 94 additions & 20 deletions e2e/tests/image.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/* global Feature, Scenario, locate */

const { initLabelStudio, clickRect } = require("./helpers");
const { initLabelStudio, clickRect, serialize } = require("./helpers");

const assert = require("assert");

Feature("Test Image object");

Expand All @@ -13,26 +15,43 @@ const config = `
</RectangleLabels>
</View>`;

const perRegionConfig = `
<View>
<Image name="img" value="$image"></Image>
<RectangleLabels name="tag" toName="img">
<Label value="Planet"></Label>
<Label value="Moonwalker" background="blue"></Label>
</RectangleLabels>
<TextArea name="answer" toName="img" perRegion="true" />
</View>`;

const createRegion = (from_name, type, values) => ({
id: "Dx_aB91ISN",
source: "$image",
from_name,
to_name: "img",
type,
value: {
height: 10.458911419423693,
rotation: 0,
width: 12.4,
x: 50.8,
y: 5.869797225186766,
...values,
},
});

const completionMoonwalker = {
id: "1001",
lead_time: 15.053,
result: [
{
from_name: "tag",
id: "Dx_aB91ISN",
source: "$image",
to_name: "img",
type: "rectanglelabels",
value: {
height: 10.458911419423693,
rectanglelabels: ["Moonwalker"],
rotation: 0,
width: 12.4,
x: 50.8,
y: 5.869797225186766,
},
},
],
result: [createRegion("tag", "rectanglelabels", { rectanglelabels: ["Moonwalker"] })],
};

// perregion regions have the same id as main region
// and their own data (`text` in this case)
const completionWithPerRegion = {
id: "1002",
result: [completionMoonwalker.result[0], createRegion("answer", "textarea", { text: ["blah"] })],
};

const image =
Expand All @@ -53,10 +72,65 @@ Scenario("Check Rect region for Image", async function(I) {
// select first and only region
I.click(locate("li").withText("Rectangle"));
I.see("Labels:");
I.wait(2);

// click on region's rect on the canvas
I.executeScript(clickRect);
I.dontSee("Labels:");
I.wait(2);
});

Scenario("Image with perRegion tags", async function(I) {
let result;
const params = {
config: perRegionConfig,
data: { image },
completions: [completionWithPerRegion],
};

I.amOnPage("/");
I.executeAsyncScript(initLabelStudio, params);

I.waitForVisible("canvas");
I.see("Regions (1)");
// select first and only region
I.click(locate("li").withText("Rectangle"));
I.see("Labels:");

// check that there is deserialized text for this region; and without doubles
I.seeNumberOfElements(locate("mark").withText("blah"), 1);

// add another note via textarea
I.fillField("[name=answer]", "another");
I.pressKey("Enter");
// texts are concatenated in the regions list (now with \n, so check separately)
I.seeNumberOfElements(locate("mark").withText("blah"), 1);
I.seeNumberOfElements(locate("mark").withText("another"), 1);
// and there is only one tag with all these texts
I.seeNumberOfElements("mark", 1);

// serialize with two textarea regions
result = await I.executeScript(serialize);
assert.equal(result.length, 2);
assert.equal(result[0].id, "Dx_aB91ISN");
assert.equal(result[1].id, "Dx_aB91ISN");
assert.deepEqual(result[0].value.rectanglelabels, ["Moonwalker"]);
assert.deepEqual(result[1].value.text, ["blah", "another"]);

// delete first deserialized text and check that only "another" left
I.click(locate("[aria-label=delete]").inside('[data-testid="textarea-region"]'));
I.dontSeeElement(locate("mark").withText("blah"));
I.seeElement(locate("mark").withText("another"));

result = await I.executeScript(serialize);
assert.equal(result.length, 2);
assert.deepEqual(result[0].value.rectanglelabels, ["Moonwalker"]);
assert.deepEqual(result[1].value.text, ["another"]);

// delete also "another" region
I.click(locate("[aria-label=delete]").inside('[data-testid="textarea-region"]'));
// there are should be no texts left at all
I.dontSeeElement(locate("mark"));

result = await I.executeScript(serialize);
assert.equal(result.length, 1);
assert.deepEqual(result[0].value.rectanglelabels, ["Moonwalker"]);
});
15 changes: 10 additions & 5 deletions src/components/Entity/Entity.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { NodeMinimal } from "../Node/Node";
import Hint from "../Hint/Hint";
import styles from "./Entity.module.scss";

const { Text } = Typography;
const { Paragraph, Text } = Typography;

const templateElement = element => {
return (
Expand All @@ -41,14 +41,19 @@ const RenderStates = observer(({ node }) => {
if (getType(s).name.indexOf("Labels") !== -1) {
return templateElement(s);
} else if (getType(s).name === "RatingModel") {
return <Text>Rating: {s.getSelectedString()}</Text>;
return <Paragraph>Rating: {s.getSelectedString()}</Paragraph>;
} else if (getType(s).name === "TextAreaModel") {
const text = s.regions.map(r => r._value).join("\n");
return (
<Text>
Text: <Text mark>{text.substring(0, 26)}...</Text>
</Text>
<Paragraph className={styles.row}>
<Text>Text: </Text>
<Text mark className={styles.long}>
{text}
</Text>
</Paragraph>
);
} else if (getType(s).name === "ChoicesModel") {
return <Paragraph>Choices: {s.getSelectedString(", ")}</Paragraph>;
}

return null;
Expand Down
15 changes: 15 additions & 0 deletions src/components/Entity/Entity.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,18 @@
.statesblk > span {
display: block;
}

.statesblk > div {
margin-bottom: 0;
}

.row {
display: flex;
white-space: pre-wrap;
}

.long {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
3 changes: 1 addition & 2 deletions src/mixins/SelectedModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@ const SelectedModelMixin = types
return self.selectedLabels.map(c => (c.alias ? c.alias : c.value));
},

getSelectedString(joinstr) {
joinstr = joinstr || " ";
getSelectedString(joinstr = " ") {
return self.selectedValues().join(joinstr);
},

Expand Down
4 changes: 4 additions & 0 deletions src/mixins/Tool.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ const ToolMixin = types
const fm = self.completion.names.get(obj.from_name);
fm.fromStateJSON(obj);

// workaround to prevent perregion textarea from duplicating
// during deserialisation
if (fm.perregion && fromModel.type === "textarea") return;

if (!fm.perregion && fromModel.type.indexOf("labels") === -1) return;

const { stateTypes, controlTagTypes } = self.tagTypes;
Expand Down
10 changes: 5 additions & 5 deletions src/regions/RectRegion.js
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,10 @@ const Model = types
},

serialize(control, object) {
let res = {
const value = control.serializableValue;
if (!value) return null;

return {
original_width: object.naturalWidth,
original_height: object.naturalHeight,
value: {
Expand All @@ -195,12 +198,9 @@ const Model = types
width: (self.width * (self.scaleX || 1) * 100) / object.stageWidth, // * (self.scaleX || 1)
height: (self.height * (self.scaleY || 1) * 100) / object.stageHeight,
rotation: self.rotation,
...value,
},
};

res.value = Object.assign(res.value, control.serializableValue);

return res;
},
}));

Expand Down
86 changes: 32 additions & 54 deletions src/regions/TextAreaRegion.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React from "react";
import { observer, inject } from "mobx-react";
import { types, getParent, getParentOfType, getRoot } from "mobx-state-tree";
import { Alert, Typography, Button } from "antd";

import { DeleteOutlined } from "@ant-design/icons";

Expand All @@ -13,7 +12,7 @@ import Registry from "../core/Registry";
import { TextAreaModel } from "../tags/control/TextArea";
import { guidGenerator } from "../core/Helpers";

const { Paragraph } = Typography;
import styles from "./TextAreaRegion/TextAreaRegion.module.scss";

const Model = types
.model("TextAreaRegionModel", {
Expand Down Expand Up @@ -44,42 +43,29 @@ const TextAreaRegionModel = types.compose(
);

const HtxTextAreaRegionView = ({ store, item }) => {
let markStyle = {
cursor: store.completionStore.selected.relationMode ? Constants.RELATION_MODE_CURSOR : Constants.POINTER_CURSOR,
display: "block",
marginBottom: "0.5em",

backgroundColor: "#f6ffed",
border: "1px solid #b7eb8f",
const classes = [styles.mark];
const params = {};
const { parent } = item;
const { relationMode } = item.completion;

borderRadius: "5px",
padding: "0.4em",
paddingLeft: "1em",
paddingRight: "1em",
};
if (relationMode) {
classes.push(styles.relation);
}

if (item.selected) {
markStyle = {
...markStyle,
border: "1px solid red",
};
classes.push(styles.selected);
} else if (item.highlighted) {
markStyle = {
...markStyle,
border: Constants.HIGHLIGHTED_CSS_BORDER,
};
classes.push(styles.highlighted);
}

const params = {};
const { parent } = item;
if (parent.editable) {
params["editable"] = {
onChange: str => {
item.setValue(str);

// here we update the parent object's state
if (parent.perregion) {
const reg = parent.completion.highlightedNode;
const reg = item.completion.highlightedNode;
reg && reg.updateSingleState(parent);

// self.regions = [];
Expand All @@ -89,50 +75,42 @@ const HtxTextAreaRegionView = ({ store, item }) => {
}

let divAttrs = {};
if (!item.parent.perregion) {
if (!parent.perregion) {
divAttrs = {
onClick: item.onClickRegion,
onMouseOver: () => {
if (store.completionStore.selected.relationMode) {
if (relationMode) {
item.setHighlight(true);
}
},
onMouseOut: () => {
/* range.setHighlight(false); */
if (store.completionStore.selected.relationMode) {
if (relationMode) {
item.setHighlight(false);
}
},
};
}

return (
<div {...divAttrs} style={{ display: "flex" }}>
<div>
<Paragraph style={markStyle} {...params}>
{item._value}
</Paragraph>
</div>
<div>
{item.parent.perregion && (
<div style={{ paddingTop: "0.5em", paddingLeft: "1em" }}>
<a
href=""
onClick={ev => {
const reg = item.completion.highlightedNode;
item.completion.deleteRegion(item);

reg && reg.updateSingleState(item.parent);

ev.preventDefault();
return false;
}}
>
<DeleteOutlined />
</a>
</div>
)}
</div>
<div {...divAttrs} className={styles.row} data-testid="textarea-region">
<p className={classes.join(" ")} {...params}>
{item._value}
</p>
{parent.perregion && (
<DeleteOutlined
className={styles.delete}
onClick={ev => {
const reg = item.completion.highlightedNode;
item.completion.deleteRegion(item);

reg && reg.updateSingleState(parent);

ev.preventDefault();
return false;
}}
/>
)}
</div>
);
};
Expand Down
Loading