Skip to content

Commit d960269

Browse files
author
Conrad Chan
authored
feat(discoverability): Reorder RegionCreator before RegionList (#597)
1 parent 516f772 commit d960269

File tree

10 files changed

+132
-19
lines changed

10 files changed

+132
-19
lines changed

src/common/BaseAnnotator.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export default class BaseAnnotator extends EventEmitter {
6060

6161
store: store.AppStore;
6262

63-
constructor({ apiHost, container, features, file, fileOptions, initialMode, intl, token }: Options) {
63+
constructor({ apiHost, container, features = {}, file, fileOptions, initialMode, intl, token }: Options) {
6464
super();
6565

6666
const fileOptionsValue = fileOptions?.[file.id];
@@ -74,6 +74,7 @@ export default class BaseAnnotator extends EventEmitter {
7474
},
7575
common: { mode: initialMode },
7676
options: {
77+
features,
7778
fileId: file.id,
7879
fileVersionId: fileOptionsVersionId ?? fileVersionId,
7980
isCurrentFileVersion: !fileOptionsVersionId || fileOptionsVersionId === currentFileVersionId,
@@ -82,7 +83,7 @@ export default class BaseAnnotator extends EventEmitter {
8283
};
8384

8485
this.container = container;
85-
this.features = features || {};
86+
this.features = features;
8687
this.intl = i18n.createIntlProvider(intl);
8788
this.store = store.createStore(initialState, {
8889
api: new API({ apiHost, token }),

src/common/__tests__/BaseAnnotator-test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ describe('BaseAnnotator', () => {
7070
expect.objectContaining({
7171
annotations: { activeId: expectedActiveId },
7272
options: {
73+
features: { enabledFeature: true },
7374
fileId: '12345',
7475
fileVersionId: '98765',
7576
isCurrentFileVersion: true,
@@ -97,6 +98,7 @@ describe('BaseAnnotator', () => {
9798
expect.objectContaining({
9899
annotations: { activeId: null },
99100
options: {
101+
features: { enabledFeature: true },
100102
fileId: '12345',
101103
fileVersionId: '456',
102104
isCurrentFileVersion: false,

src/region/RegionAnnotations.tsx

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ type Props = {
1313
annotations: AnnotationRegion[];
1414
createRegion: (arg: CreateArg) => void;
1515
isCreating: boolean;
16+
isDiscoverabilityEnabled: boolean;
1617
isRotated: boolean;
1718
location: number;
1819
message: string;
@@ -33,6 +34,7 @@ export default class RegionAnnotations extends React.PureComponent<Props, State>
3334
static defaultProps = {
3435
annotations: [],
3536
isCreating: false,
37+
isDiscoverabilityEnabled: false,
3638
isRotated: false,
3739
};
3840

@@ -76,34 +78,60 @@ export default class RegionAnnotations extends React.PureComponent<Props, State>
7678
createRegion({ ...staged, message });
7779
};
7880

81+
renderCreator = (): JSX.Element => {
82+
const { isCreating, isRotated } = this.props;
83+
const canCreate = isCreating && !isRotated;
84+
85+
return (
86+
<>
87+
{canCreate && (
88+
<RegionCreator
89+
className="ba-RegionAnnotations-creator"
90+
onStart={this.handleStart}
91+
onStop={this.handleStop}
92+
/>
93+
)}
94+
</>
95+
);
96+
};
97+
98+
renderList = (): JSX.Element => {
99+
const { activeAnnotationId, annotations } = this.props;
100+
101+
return (
102+
<RegionList
103+
activeId={activeAnnotationId}
104+
annotations={annotations}
105+
className="ba-RegionAnnotations-list"
106+
onSelect={this.handleAnnotationActive}
107+
/>
108+
);
109+
};
110+
79111
setRectRef = (rectRef: RegionRectRef): void => {
80112
this.setState({ rectRef });
81113
};
82114

83115
render(): JSX.Element {
84-
const { activeAnnotationId, annotations, isCreating, isRotated, message, staged, status } = this.props;
116+
const { isCreating, isDiscoverabilityEnabled, isRotated, message, staged, status } = this.props;
85117
const { rectRef } = this.state;
86118
const canCreate = isCreating && !isRotated;
87119
const canReply = status !== CreatorStatus.started && status !== CreatorStatus.init;
88120
const isPending = status === CreatorStatus.pending;
89121

90122
return (
91123
<>
92-
{/* Layer 1: Saved annotations */}
93-
<RegionList
94-
activeId={activeAnnotationId}
95-
annotations={annotations}
96-
className="ba-RegionAnnotations-list"
97-
onSelect={this.handleAnnotationActive}
98-
/>
99-
100-
{/* Layer 2: Drawn (unsaved) incomplete annotation target, if any */}
101-
{canCreate && (
102-
<RegionCreator
103-
className="ba-RegionAnnotations-creator"
104-
onStart={this.handleStart}
105-
onStop={this.handleStop}
106-
/>
124+
{isDiscoverabilityEnabled ? (
125+
<>
126+
{/* With discoverability, put the RegionCreator below the RegionList so that existing regions can be clicked */}
127+
{this.renderCreator()}
128+
{this.renderList()}
129+
</>
130+
) : (
131+
<>
132+
{this.renderList()}
133+
{this.renderCreator()}
134+
</>
107135
)}
108136

109137
{/* Layer 3a: Staged (unsaved) annotation target, if any */}

src/region/RegionContainer.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
getCreatorStatus,
1313
getRotation,
1414
isCreatorStagedRegion,
15+
isFeatureEnabled,
1516
Mode,
1617
resetCreatorAction,
1718
setActiveAnnotationIdAction,
@@ -28,6 +29,7 @@ export type Props = {
2829
activeAnnotationId: string | null;
2930
annotations: AnnotationRegion[];
3031
isCreating: boolean;
32+
isDiscoverabilityEnabled: boolean;
3133
isRotated: boolean;
3234
message: string;
3335
staged: CreatorItemRegion | null;
@@ -41,6 +43,7 @@ export const mapStateToProps = (state: AppState, { location }: { location: numbe
4143
activeAnnotationId: getActiveAnnotationId(state),
4244
annotations: getAnnotationsForLocation(state, location).filter(isRegion),
4345
isCreating: getAnnotationMode(state) === Mode.REGION,
46+
isDiscoverabilityEnabled: isFeatureEnabled(state, 'discoverability'),
4447
isRotated: !!getRotation(state),
4548
message: getCreatorMessage(state),
4649
staged: isCreatorStagedRegion(staged) ? staged : null,

src/region/__tests__/RegionAnnotations-test.tsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,5 +231,43 @@ describe('RegionAnnotations', () => {
231231
expect(wrapper.exists(RegionList)).toBe(true);
232232
expect(wrapper.exists(RegionRect)).toBe(false);
233233
});
234+
235+
test('should render RegionList before RegionCreator if discoverability is disabled', () => {
236+
const wrapper = getWrapper({ isCreating: true, isDiscoverabilityEnabled: false });
237+
expect(wrapper).toMatchInlineSnapshot(`
238+
<Fragment>
239+
<Memo(RegionList)
240+
activeId={null}
241+
annotations={Array []}
242+
className="ba-RegionAnnotations-list"
243+
onSelect={[Function]}
244+
/>
245+
<RegionCreator
246+
className="ba-RegionAnnotations-creator"
247+
onStart={[Function]}
248+
onStop={[Function]}
249+
/>
250+
</Fragment>
251+
`);
252+
});
253+
254+
test('should render RegionCreator before RegionList if discoverability is enabled', () => {
255+
const wrapper = getWrapper({ isCreating: true, isDiscoverabilityEnabled: true });
256+
expect(wrapper).toMatchInlineSnapshot(`
257+
<Fragment>
258+
<RegionCreator
259+
className="ba-RegionAnnotations-creator"
260+
onStart={[Function]}
261+
onStop={[Function]}
262+
/>
263+
<Memo(RegionList)
264+
activeId={null}
265+
annotations={Array []}
266+
className="ba-RegionAnnotations-list"
267+
onSelect={[Function]}
268+
/>
269+
</Fragment>
270+
`);
271+
});
234272
});
235273
});

src/region/__tests__/RegionContainer-test.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ describe('RegionContainer', () => {
2626
annotations: [],
2727
createRegion: expect.any(Function),
2828
isCreating: false,
29+
isDiscoverabilityEnabled: false,
2930
location: 1,
3031
message: '',
3132
staged: null,
@@ -54,5 +55,12 @@ describe('RegionContainer', () => {
5455

5556
expect(wrapper.find(RegionAnnotations).prop('isRotated')).toEqual(isRotated);
5657
});
58+
59+
test('should read the discoverability feature from the store', () => {
60+
const store = createStore({ options: { features: { discoverability: true } } });
61+
const wrapper = getWrapper({ store });
62+
63+
expect(wrapper.find(RegionAnnotations).prop('isDiscoverabilityEnabled')).toBe(true);
64+
});
5765
});
5866
});

src/store/options/__tests__/selectors-test.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
1-
import { getFileId, getFileVersionId, getPermissions, getRotation, getScale } from '../selectors';
1+
import {
2+
getFeatures,
3+
getFileId,
4+
getFileVersionId,
5+
getPermissions,
6+
getRotation,
7+
getScale,
8+
isFeatureEnabled,
9+
} from '../selectors';
210

311
describe('store/options/selectors', () => {
412
const optionsState = {
13+
features: { enabledFeature: true },
514
fileId: '12345',
615
fileVersionId: '67890',
716
isCurrentFileVersion: true,
@@ -13,6 +22,12 @@ describe('store/options/selectors', () => {
1322
scale: 1,
1423
};
1524

25+
describe('getFeatures', () => {
26+
test('should return the features', () => {
27+
expect(getFeatures({ options: optionsState })).toEqual({ enabledFeature: true });
28+
});
29+
});
30+
1631
describe('getFileVersionId', () => {
1732
test('should return the file id', () => {
1833
expect(getFileId({ options: optionsState })).toBe('12345');
@@ -45,4 +60,14 @@ describe('store/options/selectors', () => {
4560
expect(getScale({ options: optionsState })).toBe(1);
4661
});
4762
});
63+
64+
describe('isFeatureEnabled', () => {
65+
test('should return value for feature in the features object', () => {
66+
expect(isFeatureEnabled({ options: optionsState }, 'enabledFeature')).toBe(true);
67+
});
68+
69+
test('should return false for feature not in the features object', () => {
70+
expect(isFeatureEnabled({ options: optionsState }, 'nonExistentFeature')).toBe(false);
71+
});
72+
});
4873
});

src/store/options/reducer.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
} from './actions';
1010

1111
export const initialState = {
12+
features: {},
1213
fileId: null,
1314
fileVersionId: null,
1415
isCurrentFileVersion: true,

src/store/options/selectors.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
1+
import getProp from 'lodash/get';
12
import { AppState } from '../types';
23
import { Permissions } from '../../@types';
4+
import { Features } from '../../BoxAnnotations';
35

46
type State = Pick<AppState, 'options'>;
57

8+
export const getFeatures = (state: State): Features => state.options.features;
69
export const getFileId = (state: State): string | null => state.options.fileId;
710
export const getFileVersionId = (state: State): string | null => state.options.fileVersionId;
811
export const getIsCurrentFileVersion = (state: State): boolean => state.options.isCurrentFileVersion;
912
export const getPermissions = (state: State): Permissions => state.options.permissions;
1013
export const getRotation = (state: State): number => state.options.rotation;
1114
export const getScale = (state: State): number => state.options.scale;
15+
export const isFeatureEnabled = (state: State, featurename: string): boolean =>
16+
getProp(getFeatures(state), featurename, false);

src/store/options/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import { Features } from '../../BoxAnnotations';
12
import { Permissions } from '../../@types';
23

34
export type OptionsState = {
5+
features: Features;
46
fileId: string | null;
57
fileVersionId: string | null;
68
isCurrentFileVersion: boolean;

0 commit comments

Comments
 (0)