Skip to content
Open
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 web/libs/editor/src/common/TextArea/TextArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ export type TextAreaProps = {
placeholder?: string;
name?: string;
id?: string;
"aria-label"?: string;
"data-testid"?: string;
};

export const TextArea: FC<TextAreaProps> = ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,16 +85,43 @@

.relation-meta {
display: flex;
flex-direction: column;
gap: 8px;
padding-left: 64px;
padding-right: 8px;
padding-bottom: 5px;
align-items: center;
align-items: stretch;

&__title {
flex: none;
padding-right: 8px;
}

&__field {
display: flex;
flex-direction: column;
gap: 4px;
}

&__label {
color: var(--color-neutral-content-subtle);
font-size: 12px;
line-height: 16px;
}

&__select {
flex: 1;
}

&__note {
width: 100%;
min-height: 48px !important;
padding: 6px 8px !important;
border: 1px solid var(--color-neutral-border) !important;
border-radius: 4px !important;
background-color: var(--color-neutral-background) !important;
color: var(--color-neutral-content) !important;
font-size: 12px !important;
line-height: 18px !important;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import { Button, Select } from "@humansignal/ui";
import { observer } from "mobx-react";
import { type FC, useCallback, useMemo, useState } from "react";
import { TextArea } from "../../../common/TextArea/TextArea";
import { cn } from "../../../utils/bem";
import { wrapArray } from "../../../utils/utilities";
import { RegionItem } from "./RegionItem";
Expand Down Expand Up @@ -91,10 +92,10 @@ const RelationItem: FC<{ relation: any }> = observer(({ relation }) => {
</div>
<div className={cn("relations").elem("actions").toClassName()}>
<div className={cn("relations").elem("action").toClassName()}>
{(hovered || relation.showMeta) && relation.hasRelations && (
{(hovered || relation.showMeta) && (
<Button
primary={relation.showMeta}
aria-label={`${relation.showMeta ? "Hide" : "Show"} Relation Labels`}
aria-label={`${relation.showMeta ? "Hide" : "Show"} Relation Details`}
type={relation.showMeta ? undefined : "text"}
onClick={relation.toggleMeta}
style={{ padding: 0 }}
Expand Down Expand Up @@ -148,7 +149,7 @@ const RelationItem: FC<{ relation: any }> = observer(({ relation }) => {

const RelationMeta: FC<any> = observer(({ relation }) => {
const { selectedValues, control } = relation;
const { children, choice } = control;
const { children = [], choice } = control ?? {};

const selectionMode = useMemo(() => {
return choice === "multiple";
Expand All @@ -171,16 +172,43 @@ const RelationMeta: FC<any> = observer(({ relation }) => {
[children],
);

const onNoteInput = useCallback(
(value: string) => {
relation.setNote(value);
},
[relation],
);

return (
<div className={cn("relation-meta").toClassName()}>
<Select
multiple={selectionMode}
style={{ width: "100%" }}
placeholder="Select labels"
value={selectedValues}
onChange={onChange}
options={options}
/>
{relation.hasRelations && (
<div className={cn("relation-meta").elem("field").toClassName()}>
<Select
multiple={selectionMode}
style={{ width: "100%" }}
placeholder="Select labels"
value={selectedValues}
onChange={onChange}
options={options}
/>
</div>
)}
<div className={cn("relation-meta").elem("field").toClassName()}>
<label className={cn("relation-meta").elem("label").toClassName()} htmlFor={`relation-note-${relation.id}`}>
Note
</label>
<TextArea
id={`relation-note-${relation.id}`}
className={cn("relation-meta").elem("note").toClassName()}
placeholder="Add a note"
value={relation.note}
rows={2}
maxRows={6}
aria-label="Relation note"
data-testid="relation-note"
onInput={onNoteInput}
/>
</div>
</div>
);
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { fireEvent, render, screen } from "@testing-library/react";
import "@testing-library/jest-dom";
import { Relations } from "../Relations";

jest.mock("mobx-react", () => ({
observer: (component: any) => component,
}));

jest.mock("@humansignal/icons", () => ({
IconEyeClosed: () => <span data-testid="icon-eye-closed" />,
IconEyeOpened: () => <span data-testid="icon-eye-opened" />,
IconMenu: () => <span data-testid="icon-menu" />,
IconRelationBi: () => <span data-testid="icon-relation-bi" />,
IconRelationLeft: () => <span data-testid="icon-relation-left" />,
IconRelationRight: () => <span data-testid="icon-relation-right" />,
IconTrash: () => <span data-testid="icon-trash" />,
}));

jest.mock("@humansignal/ui", () => ({
Button: ({ children, onClick, primary, type, ...props }: any) => (
<button type="button" data-primary={primary ? "true" : "false"} onClick={onClick} {...props}>
{children}
</button>
),
Select: ({ onChange, options = [], value, multiple, placeholder }: any) => (
<select
aria-label={placeholder}
multiple={multiple}
value={multiple ? value : (value?.[0] ?? "")}
onChange={(event) => onChange(event.target.value)}
>
<option value="" />
{options.map((option: any) => (
<option key={option.value} value={option.value}>
{option.value}
</option>
))}
</select>
),
}));

jest.mock("../RegionItem", () => ({
RegionItem: ({ region }: any) => <div data-testid="region-item">{region.id}</div>,
}));

const makeRelation = (overrides: any = {}) => ({
id: "relation-1",
direction: "right",
visible: true,
showMeta: true,
hasRelations: true,
note: "Existing note",
selectedValues: ["supports"],
control: {
choice: "single",
children: [{ value: "supports", background: "#fff" }],
},
node1: { id: "region-1", setHighlight: jest.fn(), toggleHighlight: jest.fn() },
node2: { id: "region-2", setHighlight: jest.fn(), toggleHighlight: jest.fn() },
parent: { deleteRelation: jest.fn() },
rotateDirection: jest.fn(),
toggleMeta: jest.fn(),
toggleVisibility: jest.fn(),
toggleHighlight: jest.fn(),
setSelfHighlight: jest.fn(),
setRelations: jest.fn(),
setNote: jest.fn(),
...overrides,
});

describe("Relations", () => {
beforeAll(() => {
Object.defineProperty(global, "ResizeObserver", {
writable: true,
value: jest.fn().mockImplementation(() => ({
observe: jest.fn(),
unobserve: jest.fn(),
disconnect: jest.fn(),
})),
});
});

it("renders and updates a relation note", () => {
const relation = makeRelation();

render(<Relations relationStore={{ orderedRelations: [relation] }} />);

const note = screen.getByLabelText("Relation note");

expect(note).toHaveValue("Existing note");

fireEvent.input(note, { target: { value: "Updated relationship note" } });

expect(relation.setNote).toHaveBeenCalledWith("Updated relationship note");
});

it("shows relation details even without configured relation labels", () => {
const relation = makeRelation({
hasRelations: false,
selectedValues: [],
control: { children: [] },
});

render(<Relations relationStore={{ orderedRelations: [relation] }} />);

expect(screen.queryByLabelText("Select labels")).not.toBeInTheDocument();
expect(screen.getByLabelText("Relation note")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Hide Relation Details" })).toBeInTheDocument();
});
});
1 change: 1 addition & 0 deletions web/libs/editor/src/stores/Annotation/Annotation.js
Original file line number Diff line number Diff line change
Expand Up @@ -1266,6 +1266,7 @@ const _Annotation = types
`${obj.to_id}#${self.id}`,
obj.direction,
obj.labels,
obj.note,
);
}
}
Expand Down
10 changes: 9 additions & 1 deletion web/libs/editor/src/stores/RelationStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ const Relation = types

// labels
labels: types.maybeNull(types.array(types.string)),

note: types.optional(types.string, ""),
})
.volatile(() => ({
showMeta: false,
Expand Down Expand Up @@ -101,6 +103,10 @@ const Relation = types
setRelations(values) {
self.labels = values;
},

setNote(value) {
self.note = value;
},
}));

const RelationStore = types
Expand Down Expand Up @@ -212,18 +218,20 @@ const RelationStore = types
};

if (r.selectedValues) s.labels = r.selectedValues;
if (r.note) s.note = r.note;

return s;
});
},

deserializeRelation(node1, node2, direction, labels) {
deserializeRelation(node1, node2, direction, labels, note) {
const rl = self.addRelation(node1, node2);

if (!rl) return; // duplicated relation

rl.direction = direction;
rl.labels = labels;
rl.note = note ?? "";
},

toggleConnections() {
Expand Down
25 changes: 25 additions & 0 deletions web/libs/editor/src/stores/__tests__/RelationStore.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,15 @@ describe("RelationStore", () => {
expect(ser[0].labels).toEqual(["parent"]);
});

it("serialize includes note when relation has a note", () => {
const { relationStore, regions } = createStoreWithTwoRectRegionsAndRelations();
const [r1, r2] = regions;
const rl = relationStore.addRelation(r1, r2);
rl.setNote("Review this relationship");
const ser = relationStore.serialize();
expect(ser[0].note).toBe("Review this relationship");
});

it("deserializeRelation adds relation with direction and labels", () => {
const { relationStore, regions } = createStoreWithTwoRectRegionsAndRelations();
const [r1, r2] = regions;
Expand All @@ -237,6 +246,14 @@ describe("RelationStore", () => {
expect(rl.labels).toEqual(["child"]);
});

it("deserializeRelation restores note", () => {
const { relationStore, regions } = createStoreWithTwoRectRegionsAndRelations();
const [r1, r2] = regions;
relationStore.deserializeRelation(r1, r2, "left", ["child"], "Needs reviewer context");
const rl = relationStore.relations[0];
expect(rl.note).toBe("Needs reviewer context");
});

it("deserializeRelation does nothing when relation already exists", () => {
const { relationStore, regions } = createStoreWithTwoRectRegionsAndRelations();
const [r1, r2] = regions;
Expand Down Expand Up @@ -438,6 +455,14 @@ describe("Relation (model)", () => {
expect(rl.labels).toEqual(["parent", "child"]);
});

it("setNote updates note", () => {
const { relationStore, regions } = createStoreWithTwoRectRegionsAndRelations();
const [r1, r2] = regions;
const rl = relationStore.addRelation(r1, r2);
rl.setNote("This relationship is uncertain");
expect(rl.note).toBe("This relationship is uncertain");
});

it("toggleVisibility toggles visible", () => {
const { relationStore, regions } = createStoreWithTwoRectRegionsAndRelations();
const [r1, r2] = regions;
Expand Down
Loading