Skip to content

Commit f4553f7

Browse files
Mingzemergify[bot]
andauthored
feat(region): Retain annotation message between pages (#498)
* feat(region): Retain annotation message between pages * feat(region): Address comments Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
1 parent 92588cb commit f4553f7

File tree

14 files changed

+90
-81
lines changed

14 files changed

+90
-81
lines changed

src/region/RegionAnnotations.tsx

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,24 @@
1-
import merge from 'lodash/merge';
21
import * as React from 'react';
32
import PopupReply from '../components/Popups/PopupReply';
43
import RegionCreator from './RegionCreator';
54
import RegionList from './RegionList';
65
import RegionRect, { RegionRectRef } from './RegionRect';
76
import { AnnotationRegion, Rect } from '../@types';
7+
import { CreateArg } from './actions';
88
import { CreatorItem, CreatorStatus } from '../store/creator';
99
import { scaleShape } from './regionUtil';
1010
import './RegionAnnotations.scss';
1111

1212
type Props = {
1313
activeAnnotationId: string | null;
1414
annotations: AnnotationRegion[];
15-
createRegion: (arg: { location: number; message: string; shape: Rect }) => void;
15+
createRegion: (arg: CreateArg) => void;
1616
isCreating: boolean;
17+
message: string;
1718
page: number;
1819
scale: number;
1920
setActiveAnnotationId: (annotationId: string | null) => void;
21+
setMessage: (message: string) => void;
2022
setStaged: (staged: CreatorItem | null) => void;
2123
setStatus: (status: CreatorStatus) => void;
2224
staged?: CreatorItem | null;
@@ -36,11 +38,6 @@ export default class RegionAnnotations extends React.PureComponent<Props, State>
3638

3739
state: State = {};
3840

39-
setStaged(data: Partial<CreatorItem> | null): void {
40-
const { page, setStaged, staged } = this.props;
41-
setStaged(data ? merge({ location: page, message: '' }, staged, data) : data);
42-
}
43-
4441
setStatus(status: CreatorStatus): void {
4542
const { setStatus } = this.props;
4643
setStatus(status);
@@ -53,44 +50,46 @@ export default class RegionAnnotations extends React.PureComponent<Props, State>
5350
};
5451

5552
handleCancel = (): void => {
56-
this.setStaged(null);
53+
const { setMessage, setStaged } = this.props;
54+
setMessage('');
55+
setStaged(null);
5756
this.setStatus(CreatorStatus.init);
5857
};
5958

6059
handleChange = (text?: string): void => {
61-
this.setStaged({ message: text });
60+
const { setMessage } = this.props;
61+
setMessage(text || '');
6262
};
6363

6464
handleStart = (): void => {
65-
this.setStaged(null);
65+
const { setStaged } = this.props;
66+
setStaged(null);
6667
this.setStatus(CreatorStatus.init);
6768
};
6869

6970
handleStop = (shape: Rect): void => {
70-
const { scale } = this.props;
71-
72-
this.setStaged({ shape: scaleShape(shape, scale, true) });
71+
const { page, scale, setStaged } = this.props;
72+
setStaged({ location: page, shape: scaleShape(shape, scale, true) });
7373
this.setStatus(CreatorStatus.staged);
7474
};
7575

7676
handleSubmit = (): void => {
77-
const { createRegion, staged } = this.props;
77+
const { createRegion, message, staged } = this.props;
7878

7979
if (!staged) {
8080
return;
8181
}
8282

83-
createRegion(staged);
83+
createRegion({ ...staged, message });
8484
};
8585

8686
setRectRef = (rectRef: RegionRectRef): void => {
8787
this.setState({ rectRef });
8888
};
8989

9090
render(): JSX.Element {
91-
const { activeAnnotationId, annotations, isCreating, scale, staged, status } = this.props;
91+
const { activeAnnotationId, annotations, isCreating, message, scale, staged, status } = this.props;
9292
const { rectRef } = this.state;
93-
const canDraw = !staged || !staged.message;
9493
const canReply = status !== CreatorStatus.init;
9594
const isPending = status === CreatorStatus.pending;
9695

@@ -108,7 +107,6 @@ export default class RegionAnnotations extends React.PureComponent<Props, State>
108107
{/* Layer 2: Drawn (unsaved) incomplete annotation target, if any */}
109108
{isCreating && (
110109
<RegionCreator
111-
canDraw={canDraw}
112110
className="ba-RegionAnnotations-creator"
113111
onStart={this.handleStart}
114112
onStop={this.handleStop}
@@ -131,7 +129,7 @@ export default class RegionAnnotations extends React.PureComponent<Props, State>
131129
onChange={this.handleChange}
132130
onSubmit={this.handleSubmit}
133131
reference={rectRef}
134-
value={staged.message}
132+
value={message}
135133
/>
136134
</div>
137135
)}

src/region/RegionContainer.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ import {
77
getActiveAnnotationId,
88
getAnnotationMode,
99
getAnnotationsForLocation,
10+
getCreatorMessage,
1011
getCreatorStagedForLocation,
1112
getCreatorStatus,
1213
setActiveAnnotationIdAction,
14+
setMessageAction,
1315
setStagedAction,
1416
setStatusAction,
1517
} from '../store';
@@ -22,6 +24,7 @@ export type Props = {
2224
activeAnnotationId: string | null;
2325
annotations: AnnotationRegion[];
2426
isCreating: boolean;
27+
message: string;
2528
staged: CreatorItem | null;
2629
status: CreatorStatus;
2730
};
@@ -30,13 +33,15 @@ export const mapStateToProps = (state: AppState, { page }: { page: number }): Pr
3033
activeAnnotationId: getActiveAnnotationId(state),
3134
annotations: getAnnotationsForLocation(state, page).filter(isRegion),
3235
isCreating: getAnnotationMode(state) === 'region',
36+
message: getCreatorMessage(state),
3337
staged: getCreatorStagedForLocation(state, page),
3438
status: getCreatorStatus(state),
3539
});
3640

3741
export const mapDispatchToProps = {
3842
createRegion: createRegionAction,
3943
setActiveAnnotationId: setActiveAnnotationIdAction,
44+
setMessage: setMessageAction,
4045
setStaged: setStagedAction,
4146
setStatus: setStatusAction,
4247
};

src/region/RegionCreator.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ $region_cursor_32: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAA
22
$region_cursor_32_2x: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAABGdBTUEAALGPC/xhBQAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAQKADAAQAAAABAAAAQAAAAABlmWCKAAABWUlEQVR4Ae2YOxKDIBCGNaM5R4pYWOT+B0lhYQrPEZ1JGGcoZIy6Lj4IH40DLAt8+8srSUgQgAAEIAABCEAAAnESSM867Y9JmrGlJi1pf1li9M822dknd7/dREN8NY3IHgWIcB1oPBdZqVLsVFCAJRHK113dtbsFCtg78iZib9Nn7vTbmshenbJdsihgF8wjnZRF0ZdWdT1S+7tI+8+7nlGAS+Ss+bX7/Nx8UMAcoaPr506A2vGhAC3Brdq7J76t+oleAQDYSlqh+FW/CHk6meVSP77WiOh/gUUvp1NytpF7lGXSdd2U6aCubds+n+fuxXBgNshkWZY8q6ovQwEDNOsz3hQgHcLa26DtBwVYEsqveheQRsKsGbwIKYPmtXn02yAAvOopQGcoIMCgeR0yCvCKM0Bn0StAfReQBp2ToJQY9hCAAAQgAAEIQAACEIAABCAAAQhAwDOBLxaXQ2IYNsXoAAAAAElFTkSuQmCC';
33
$region_cursor_32_3x: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAABGdBTUEAALGPC/xhBQAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAYKADAAQAAAABAAAAYAAAAACK+310AAACaUlEQVR4Ae3bwUrDQBDG8VTE+Bg9CD0IBd//IYSCh4KHPobxopuDYQgGOtNvdhvy39O2zc5uf9Odpol2HQ0BBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAgXSBXfoMdzDBT2mZy9iVFo3/EB3IOI0ACdA4hqM8hkeudODLfi9Z+eflIonDDpAwxoOQgLidZOTmSpBV85YRVfmya2AHWI0GfRLQAN1OuekSZCGWfkxl/4hjB9gsNOiTgAbodsrVlKBSCr7Kwnu7+Fl/KGXkefbc3T9kBzROEQlonIDVlCDr9Ho4dMMwdH3fdx/ns30p3M8+21laGDtgSabS8ySgEvTSNKssQUtvxvt8xrUd7xrYAV4x8fEkQAzqDbe5EuS9BO0F9R7PDvCKiY8nAWJQb7hNlKClS81erIzj2QEZqo6YJMCBlXEoCchQdcQkAQ6sjENJQIaqI2b6WVDFy7y9aq6aZ03sAMenNeNQEpCh6ogZ/seCa+ewZeHteOy+y52sW9t4N+yvjXfFbm1PJcb76TSFqVmC0r8DpndVOiO+xbOvRfvqeNF1RMdVTcD4SVM0i67aAYp1RWJULUGRBf43JuOmvJ2nZgniS9jKN+iTgAbodsr07wDVdi5nU/xpos0cfY0AJUjjGI5CAsJ0moEkQOMYjkICwnSagSRA4xiOQgLCdJqBJEDjGI5CAsJ0moEkQOMYjkICwnSagSRA4xiOQgLCdJqBJEDjGI6SfkcsvLLZQC5Hz0B4qBGgBGkciYIAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIITAK/BlNVeN572e8AAAAASUVORK5CYII=';
44

5-
.ba-RegionCreator.is-active {
5+
.ba-RegionCreator {
66
cursor: url($region_cursor_32) 16 16, crosshair; /* Legacy */
77
cursor: image-set(url($region_cursor_32) 1x, url($region_cursor_32_2x) 2x, url($region_cursor_32_3x) 3x) 16 16, crosshair; /* Webkit */ /* stylelint-disable-line */
88
}

src/region/RegionCreator.tsx

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { styleShape } from './regionUtil';
88
import './RegionCreator.scss';
99

1010
type Props = {
11-
canDraw: boolean;
1211
className?: string;
1312
onStart: () => void;
1413
onStop: (shape: Rect) => void;
@@ -19,7 +18,7 @@ const MIN_Y = 0; // Minimum region y position must be positive
1918
const MIN_SIZE = 10; // Minimum region size must be large enough to be clickable
2019
const MOUSE_PRIMARY = 1; // Primary mouse button
2120

22-
export default function RegionCreator({ canDraw, className, onStart, onStop }: Props): JSX.Element {
21+
export default function RegionCreator({ className, onStart, onStop }: Props): JSX.Element {
2322
const [isDrawing, setIsDrawing] = React.useState<boolean>(false);
2423
const [isHovering, setIsHovering] = React.useState<boolean>(false);
2524
const creatorElRef = React.useRef<HTMLDivElement>(null);
@@ -154,18 +153,6 @@ export default function RegionCreator({ canDraw, className, onStart, onStop }: P
154153
const handleTouchStart = ({ targetTouches }: React.TouchEvent): void => {
155154
startDraw(targetTouches[0].clientX, targetTouches[0].clientY);
156155
};
157-
const eventHandlers = canDraw
158-
? {
159-
onClick: handleClick,
160-
onMouseDown: handleMouseDown,
161-
onMouseOut: handleMouseOut,
162-
onMouseOver: handleMouseOver,
163-
onTouchCancel: handleTouchCancel,
164-
onTouchEnd: handleTouchEnd,
165-
onTouchMove: handleTouchMove,
166-
onTouchStart: handleTouchStart,
167-
}
168-
: {};
169156

170157
const renderStep = (callback: () => void): void => {
171158
renderHandleRef.current = window.requestAnimationFrame(callback);
@@ -216,14 +203,23 @@ export default function RegionCreator({ canDraw, className, onStart, onStop }: P
216203
});
217204

218205
return (
206+
// eslint-disable-next-line jsx-a11y/mouse-events-have-key-events
219207
<div
220208
ref={creatorElRef}
221-
className={classNames(className, 'ba-RegionCreator', { 'is-active': canDraw })}
209+
className={classNames(className, 'ba-RegionCreator')}
222210
data-testid="ba-RegionCreator"
223-
{...eventHandlers}
211+
onClick={handleClick}
212+
onMouseDown={handleMouseDown}
213+
onMouseOut={handleMouseOut}
214+
onMouseOver={handleMouseOver}
215+
onTouchCancel={handleTouchCancel}
216+
onTouchEnd={handleTouchEnd}
217+
onTouchMove={handleTouchMove}
218+
onTouchStart={handleTouchStart}
219+
role="presentation"
224220
>
225221
{isDrawing && <RegionRect ref={regionRectRef} className="ba-RegionCreator-rect" isActive />}
226-
{!isDrawing && isHovering && canDraw && <PopupCursor />}
222+
{!isDrawing && isHovering && <PopupCursor />}
227223
</div>
228224
);
229225
}

src/region/__tests__/RegionAnnotations-test.tsx

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@ jest.mock('../regionUtil', () => ({
1818
scaleShape: jest.fn(value => value),
1919
}));
2020

21-
describe('RegionAnnotations', () => {
21+
describe('components/region/RegionAnnotations', () => {
2222
const defaults = {
2323
activeAnnotationId: null,
2424
createRegion: jest.fn(),
25+
message: 'test',
2526
page: 1,
2627
scale: 1,
2728
setActiveAnnotationId: jest.fn(),
29+
setMessage: jest.fn(),
2830
setStaged: jest.fn(),
2931
setStatus: jest.fn(),
3032
staged: null,
@@ -40,7 +42,6 @@ describe('RegionAnnotations', () => {
4042
const getRectRef = (): HTMLDivElement => document.createElement('div');
4143
const getStaged = (): CreatorItem => ({
4244
location: 1,
43-
message: 'test',
4445
shape: getRect(),
4546
});
4647
const getWrapper = (props = {}): ShallowWrapper => shallow(<RegionAnnotations {...defaults} {...props} />);
@@ -60,6 +61,7 @@ describe('RegionAnnotations', () => {
6061
test('should reset the staged state and status', () => {
6162
instance.handleCancel();
6263

64+
expect(defaults.setMessage).toHaveBeenCalledWith('');
6365
expect(defaults.setStaged).toHaveBeenCalledWith(null);
6466
expect(defaults.setStatus).toHaveBeenCalledWith(CreatorStatus.init);
6567
});
@@ -69,18 +71,15 @@ describe('RegionAnnotations', () => {
6971
test('should set the staged state with the new message', () => {
7072
instance.handleChange('test');
7173

72-
expect(defaults.setStaged).toHaveBeenCalledWith(
73-
expect.objectContaining({
74-
message: 'test',
75-
}),
76-
);
74+
expect(defaults.setMessage).toHaveBeenCalledWith('test');
7775
});
7876
});
7977

8078
describe('handleStart', () => {
8179
test('should reset the creator status', () => {
8280
instance.handleStart();
8381

82+
expect(defaults.setStaged).toHaveBeenCalledWith(null);
8483
expect(defaults.setStatus).toHaveBeenCalledWith(CreatorStatus.init);
8584
});
8685
});
@@ -98,7 +97,10 @@ describe('RegionAnnotations', () => {
9897
instance.handleStop(shape);
9998

10099
expect(scaleShape).toHaveBeenCalledWith(shape, defaults.scale, true); // Inverse scale
101-
expect(defaults.setStaged).toHaveBeenCalledWith(expect.objectContaining({ shape }));
100+
expect(defaults.setStaged).toHaveBeenCalledWith({
101+
location: defaults.page,
102+
shape,
103+
});
102104
expect(defaults.setStatus).toHaveBeenCalledWith(CreatorStatus.staged);
103105
});
104106
});
@@ -107,7 +109,10 @@ describe('RegionAnnotations', () => {
107109
test('should save the staged annotation', () => {
108110
instance.handleSubmit();
109111

110-
expect(defaults.createRegion).toHaveBeenCalledWith(getStaged());
112+
expect(defaults.createRegion).toHaveBeenCalledWith({
113+
...getStaged(),
114+
message: defaults.message,
115+
});
111116
});
112117
});
113118

@@ -134,7 +139,6 @@ describe('RegionAnnotations', () => {
134139
const creator = wrapper.find(RegionCreator);
135140

136141
expect(creator.hasClass('ba-RegionAnnotations-creator')).toBe(true);
137-
expect(creator.prop('canDraw')).toBe(true);
138142
});
139143

140144
test('should scale and render a RegionAnnotation if one has been drawn', () => {

src/region/__tests__/RegionContainer-test.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,13 @@ describe('RegionContainer', () => {
2727
annotations: [],
2828
createRegion: expect.any(Function),
2929
isCreating: false,
30+
message: '',
3031
page: defaults.page,
3132
scale: defaults.scale,
3233
staged: null,
3334
status: CreatorStatus.init,
3435
setActiveAnnotationId: expect.any(Function),
36+
setMessage: expect.any(Function),
3537
setStaged: expect.any(Function),
3638
setStatus: expect.any(Function),
3739
store: defaults.store,

src/region/__tests__/RegionCreator-test.tsx

Lines changed: 6 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ jest.mock('../regionUtil');
1313

1414
describe('RegionCreator', () => {
1515
const defaults = {
16-
canDraw: true,
1716
onDraw: jest.fn(),
1817
onStart: jest.fn(),
1918
onStop: jest.fn(),
@@ -278,36 +277,21 @@ describe('RegionCreator', () => {
278277
});
279278
});
280279

281-
test('should add event listeners if canDraw is true', () => {
280+
test('should add class and event listeners', () => {
282281
const wrapper = getWrapper();
283282
const rootEl = getWrapperRoot(wrapper);
284283

284+
expect(rootEl.hasClass('ba-RegionCreator')).toBe(true);
285+
285286
expect(rootEl.prop('onClick')).toBeDefined();
286287
expect(rootEl.prop('onMouseDown')).toBeDefined();
288+
expect(rootEl.prop('onMouseOut')).toBeDefined();
289+
expect(rootEl.prop('onMouseOver')).toBeDefined();
290+
expect(rootEl.prop('onTouchCancel')).toBeDefined();
287291
expect(rootEl.prop('onTouchCancel')).toBeDefined();
288292
expect(rootEl.prop('onTouchEnd')).toBeDefined();
289293
expect(rootEl.prop('onTouchMove')).toBeDefined();
290294
expect(rootEl.prop('onTouchStart')).toBeDefined();
291295
});
292-
293-
test('should not add event listeners if canDraw is false', () => {
294-
const wrapper = getWrapper({ canDraw: false });
295-
const rootEl = getWrapperRoot(wrapper);
296-
297-
expect(rootEl.prop('onClick')).not.toBeDefined();
298-
expect(rootEl.prop('onMouseDown')).not.toBeDefined();
299-
expect(rootEl.prop('onTouchCancel')).not.toBeDefined();
300-
expect(rootEl.prop('onTouchEnd')).not.toBeDefined();
301-
expect(rootEl.prop('onTouchMove')).not.toBeDefined();
302-
expect(rootEl.prop('onTouchStart')).not.toBeDefined();
303-
});
304-
305-
test.each([true, false])('should add the is-active class based on canDraw: %s', canDraw => {
306-
const wrapper = getWrapper({ canDraw });
307-
const rootEl = getWrapperRoot(wrapper);
308-
309-
expect(rootEl.hasClass('ba-RegionCreator')).toBe(true);
310-
expect(rootEl.hasClass('is-active')).toBe(canDraw);
311-
});
312296
});
313297
});

src/store/creator/__mocks__/creatorState.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import { CreatorStatus } from '../types';
33
export default {
44
cursor: 0,
55
error: null,
6+
message: 'test',
67
staged: {
78
location: 1,
8-
message: 'test',
99
shape: {
1010
height: 100,
1111
width: 100,

0 commit comments

Comments
 (0)