Skip to content

Commit d08545e

Browse files
feat(router): optional router in withAnnotatorContext and withAnnotations (#4136)
* feat: optional router in withAnnotatorContext and withAnnotations * feat: tests updated, some converted to RTL * feat: added some missing flow types * feat: flow types update * feat: updates after review * feat: Small updates after review * feat: Small updates after review --------- Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
1 parent 8b5eec6 commit d08545e

File tree

10 files changed

+372
-62
lines changed

10 files changed

+372
-62
lines changed

src/elements/common/annotator-context/__tests__/withAnnotations.test.tsx

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { shallow, ShallowWrapper } from 'enzyme';
33
import { createMemoryHistory, History, Location } from 'history';
44
import { Action, Annotator, AnnotatorContext, withAnnotations, Status } from '../index';
55
import { WithAnnotationsProps, ComponentWithAnnotations } from '../withAnnotations';
6+
import { FeedEntryType, ViewType } from '../../types/SidebarNavigation';
67

78
type ComponentProps = {
89
className?: string;
@@ -49,6 +50,37 @@ describe('elements/common/annotator-context/withAnnotations', () => {
4950
const wrapper = getWrapper({ history, location: history.location });
5051
expect(wrapper.state('activeAnnotationId')).toBe(null);
5152
});
53+
54+
test('should use sidebarNavigation prop to initialize state with activeAnnotationId if routerDisabled is true', () => {
55+
const wrapper = getWrapper({
56+
routerDisabled: true,
57+
sidebarNavigation: {
58+
activeFeedEntryType: FeedEntryType.ANNOTATIONS,
59+
activeFeedEntryId: '456',
60+
sidebar: ViewType.ACTIVITY,
61+
fileVersionId: '123',
62+
},
63+
});
64+
expect(wrapper.state('activeAnnotationId')).toBe('456');
65+
});
66+
67+
test('should use sidebarNavigation prop to initialize state with activeAnnotationId if routerDisabled is true in feature flags', () => {
68+
const wrapper = getWrapper({
69+
features: { routerDisabled: { value: true } },
70+
sidebarNavigation: {
71+
activeFeedEntryType: FeedEntryType.ANNOTATIONS,
72+
activeFeedEntryId: '456',
73+
sidebar: ViewType.ACTIVITY,
74+
fileVersionId: '123',
75+
},
76+
});
77+
expect(wrapper.state('activeAnnotationId')).toBe('456');
78+
});
79+
80+
test('should not initialize state with activeAnnotationId if routerDisabled is true and sidebarNavigation is not provided', () => {
81+
const wrapper = getWrapper({ routerDisabled: true });
82+
expect(wrapper.state('activeAnnotationId')).toBe(null);
83+
});
5284
});
5385

5486
test('should pass onAnnotator and onPreviewDestroy as props on the wrapped component', () => {
@@ -93,6 +125,20 @@ describe('elements/common/annotator-context/withAnnotations', () => {
93125
});
94126
});
95127

128+
test('should not pass annotations router props if routerDisabled is true', () => {
129+
const wrapper = getWrapper({ routerDisabled: true });
130+
const contextProvider = getContextProvider(wrapper);
131+
expect(contextProvider.prop('value').getAnnotationsMatchPath).toBeUndefined();
132+
expect(contextProvider.prop('value').getAnnotationsPath).toBeUndefined();
133+
});
134+
135+
test('should not pass annotations router props if routerDisabled is true in feature flags', () => {
136+
const wrapper = getWrapper({ features: { routerDisabled: { value: true } } });
137+
const contextProvider = getContextProvider(wrapper);
138+
expect(contextProvider.prop('value').getAnnotationsMatchPath).toBeUndefined();
139+
expect(contextProvider.prop('value').getAnnotationsPath).toBeUndefined();
140+
});
141+
96142
describe('emitActiveAnnotationChangeEvent', () => {
97143
test('should call annotator emit on action', () => {
98144
const wrapper = getWrapper();

src/elements/common/annotator-context/__tests__/withAnnotatorContext.test.tsx

Lines changed: 116 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as React from 'react';
2-
import { shallow } from 'enzyme';
2+
import { render } from '../../../../test-utils/testing-library';
33
import withAnnotatorContext, { WithAnnotatorContextProps } from '../withAnnotatorContext';
44
import { Action } from '../types';
55

@@ -13,18 +13,59 @@ describe('elements/common/annotator-context/withAnnotatorContext', () => {
1313
className?: string | undefined;
1414
};
1515

16-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
17-
type WrappedComponentProps<T> = ComponentProps & WithAnnotatorContextProps; // T is supposed to be allowed component props
16+
type WrappedComponentProps = ComponentProps & WithAnnotatorContextProps;
1817

19-
const Component = (props: WrappedComponentProps<HTMLDivElement>) => <div {...props} />;
18+
beforeEach(() => jest.resetAllMocks());
2019

21-
const WrappedComponent = withAnnotatorContext(Component);
20+
test('should apply the annotator context to the wrapped component as a prop', () => {
21+
const annotatorState = {
22+
annotation: { foo: 'bar' },
23+
action: Action.CREATE_START,
24+
};
25+
const mockEmitActiveAnnotationChangeEvent = jest.fn();
26+
const mockEmitAnnotationRemoveEvent = jest.fn();
27+
const mockEmitAnnotationReplyCreateEvent = jest.fn();
28+
const mockEmitAnnotationReplyDeleteEvent = jest.fn();
29+
const mockMmitAnnotationReplyUpdateEvent = jest.fn();
30+
const mockEmitAnnotationUpdateEvent = jest.fn();
31+
const mockGetAnnotationsMatchPath = jest.fn();
32+
const mockGetAnnotationsPath = jest.fn();
2233

23-
const getWrapper = (props?: WrappedComponentProps<HTMLDivElement>) => shallow(<WrappedComponent {...props} />);
34+
mockContext.mockReturnValue({
35+
state: annotatorState,
36+
emitActiveAnnotationChangeEvent: mockEmitActiveAnnotationChangeEvent,
37+
emitAnnotationRemoveEvent: mockEmitAnnotationRemoveEvent,
38+
emitAnnotationReplyCreateEvent: mockEmitAnnotationReplyCreateEvent,
39+
emitAnnotationReplyDeleteEvent: mockEmitAnnotationReplyDeleteEvent,
40+
emitAnnotationReplyUpdateEvent: mockMmitAnnotationReplyUpdateEvent,
41+
emitAnnotationUpdateEvent: mockEmitAnnotationUpdateEvent,
42+
getAnnotationsMatchPath: mockGetAnnotationsMatchPath,
43+
getAnnotationsPath: mockGetAnnotationsPath,
44+
});
2445

25-
beforeEach(() => jest.resetAllMocks());
46+
const MockComponent = jest.fn<JSX.Element | null, [WrappedComponentProps]>(() => null);
47+
const WrappedWithMockComponent = withAnnotatorContext(MockComponent);
2648

27-
test('should apply the annotator context to the wrapped component as a prop', () => {
49+
render(<WrappedWithMockComponent />);
50+
51+
expect(MockComponent).toHaveBeenCalledTimes(1);
52+
const props = MockComponent.mock.calls[0][0];
53+
54+
expect(props.annotatorState).toEqual({
55+
annotation: { foo: 'bar' },
56+
action: Action.CREATE_START,
57+
});
58+
expect(props.emitActiveAnnotationChangeEvent).toBe(mockEmitActiveAnnotationChangeEvent);
59+
expect(props.emitAnnotationRemoveEvent).toBe(mockEmitAnnotationRemoveEvent);
60+
expect(props.emitAnnotationReplyCreateEvent).toBe(mockEmitAnnotationReplyCreateEvent);
61+
expect(props.emitAnnotationReplyDeleteEvent).toBe(mockEmitAnnotationReplyDeleteEvent);
62+
expect(props.emitAnnotationReplyUpdateEvent).toBe(mockMmitAnnotationReplyUpdateEvent);
63+
expect(props.emitAnnotationUpdateEvent).toBe(mockEmitAnnotationUpdateEvent);
64+
expect(props.getAnnotationsMatchPath).toBe(mockGetAnnotationsMatchPath);
65+
expect(props.getAnnotationsPath).toBe(mockGetAnnotationsPath);
66+
});
67+
68+
test('should apply the annotator context to the wrapped component without router props when routerDisabled is true', () => {
2869
const annotatorState = {
2970
annotation: { foo: 'bar' },
3071
action: Action.CREATE_START,
@@ -50,22 +91,77 @@ describe('elements/common/annotator-context/withAnnotatorContext', () => {
5091
getAnnotationsPath: mockGetAnnotationsPath,
5192
});
5293

53-
const wrapper = getWrapper();
54-
const wrappedComponent = wrapper.dive().find(Component);
55-
const props = wrappedComponent.props() as WrappedComponentProps<HTMLDivElement>;
94+
const MockComponent = jest.fn<JSX.Element | null, [WrappedComponentProps]>(() => null);
95+
const WrappedWithMockComponent = withAnnotatorContext(MockComponent);
96+
97+
render(<WrappedWithMockComponent routerDisabled={true} />);
98+
99+
expect(MockComponent).toHaveBeenCalledTimes(1);
100+
const props = MockComponent.mock.calls[0][0];
56101

57-
expect(wrappedComponent.exists()).toBeTruthy();
58102
expect(props.annotatorState).toEqual({
59103
annotation: { foo: 'bar' },
60104
action: Action.CREATE_START,
61105
});
62-
expect(props.emitActiveAnnotationChangeEvent).toEqual(mockEmitActiveAnnotationChangeEvent);
63-
expect(props.emitAnnotationRemoveEvent).toEqual(mockEmitAnnotationRemoveEvent);
64-
expect(props.emitAnnotationReplyCreateEvent).toEqual(mockEmitAnnotationReplyCreateEvent);
65-
expect(props.emitAnnotationReplyDeleteEvent).toEqual(mockEmitAnnotationReplyDeleteEvent);
66-
expect(props.emitAnnotationReplyUpdateEvent).toEqual(mockMmitAnnotationReplyUpdateEvent);
67-
expect(props.emitAnnotationUpdateEvent).toEqual(mockEmitAnnotationUpdateEvent);
68-
expect(props.getAnnotationsMatchPath).toEqual(mockGetAnnotationsMatchPath);
69-
expect(props.getAnnotationsPath).toEqual(mockGetAnnotationsPath);
106+
expect(props.emitActiveAnnotationChangeEvent).toBe(mockEmitActiveAnnotationChangeEvent);
107+
expect(props.emitAnnotationRemoveEvent).toBe(mockEmitAnnotationRemoveEvent);
108+
expect(props.emitAnnotationReplyCreateEvent).toBe(mockEmitAnnotationReplyCreateEvent);
109+
expect(props.emitAnnotationReplyDeleteEvent).toBe(mockEmitAnnotationReplyDeleteEvent);
110+
expect(props.emitAnnotationReplyUpdateEvent).toBe(mockMmitAnnotationReplyUpdateEvent);
111+
expect(props.emitAnnotationUpdateEvent).toBe(mockEmitAnnotationUpdateEvent);
112+
113+
// Router-related props should not be passed when routerDisabled is true
114+
expect(props.getAnnotationsMatchPath).toBeUndefined();
115+
expect(props.getAnnotationsPath).toBeUndefined();
116+
});
117+
118+
test('should apply the annotator context to the wrapped component without router props when routerDisabled in feature flags is true', () => {
119+
const annotatorState = {
120+
annotation: { foo: 'bar' },
121+
action: Action.CREATE_START,
122+
};
123+
const mockEmitActiveAnnotationChangeEvent = jest.fn();
124+
const mockEmitAnnotationRemoveEvent = jest.fn();
125+
const mockEmitAnnotationReplyCreateEvent = jest.fn();
126+
const mockEmitAnnotationReplyDeleteEvent = jest.fn();
127+
const mockMmitAnnotationReplyUpdateEvent = jest.fn();
128+
const mockEmitAnnotationUpdateEvent = jest.fn();
129+
const mockGetAnnotationsMatchPath = jest.fn();
130+
const mockGetAnnotationsPath = jest.fn();
131+
132+
mockContext.mockReturnValue({
133+
state: annotatorState,
134+
emitActiveAnnotationChangeEvent: mockEmitActiveAnnotationChangeEvent,
135+
emitAnnotationRemoveEvent: mockEmitAnnotationRemoveEvent,
136+
emitAnnotationReplyCreateEvent: mockEmitAnnotationReplyCreateEvent,
137+
emitAnnotationReplyDeleteEvent: mockEmitAnnotationReplyDeleteEvent,
138+
emitAnnotationReplyUpdateEvent: mockMmitAnnotationReplyUpdateEvent,
139+
emitAnnotationUpdateEvent: mockEmitAnnotationUpdateEvent,
140+
getAnnotationsMatchPath: mockGetAnnotationsMatchPath,
141+
getAnnotationsPath: mockGetAnnotationsPath,
142+
});
143+
144+
const MockComponent = jest.fn<JSX.Element | null, [WrappedComponentProps]>(() => null);
145+
const WrappedWithMockComponent = withAnnotatorContext(MockComponent);
146+
147+
render(<WrappedWithMockComponent features={{ routerDisabled: { value: true } }} />);
148+
149+
expect(MockComponent).toHaveBeenCalledTimes(1);
150+
const props = MockComponent.mock.calls[0][0];
151+
152+
expect(props.annotatorState).toEqual({
153+
annotation: { foo: 'bar' },
154+
action: Action.CREATE_START,
155+
});
156+
expect(props.emitActiveAnnotationChangeEvent).toBe(mockEmitActiveAnnotationChangeEvent);
157+
expect(props.emitAnnotationRemoveEvent).toBe(mockEmitAnnotationRemoveEvent);
158+
expect(props.emitAnnotationReplyCreateEvent).toBe(mockEmitAnnotationReplyCreateEvent);
159+
expect(props.emitAnnotationReplyDeleteEvent).toBe(mockEmitAnnotationReplyDeleteEvent);
160+
expect(props.emitAnnotationReplyUpdateEvent).toBe(mockMmitAnnotationReplyUpdateEvent);
161+
expect(props.emitAnnotationUpdateEvent).toBe(mockEmitAnnotationUpdateEvent);
162+
163+
// Router-related props should not be passed when routerDisabled is true
164+
expect(props.getAnnotationsMatchPath).toBeUndefined();
165+
expect(props.getAnnotationsPath).toBeUndefined();
70166
});
71167
});

src/elements/common/annotator-context/types.js.flow

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,8 @@ export interface AnnotatorContext {
7474
annotation: Object,
7575
isStartEvent?: boolean
7676
) => void;
77-
getAnnotationsMatchPath: GetMatchPath;
78-
getAnnotationsPath: (fileVersionId?: string, annotationId?: string) => string;
77+
getAnnotationsMatchPath?: GetMatchPath;
78+
getAnnotationsPath?: (fileVersionId?: string, annotationId?: string) => string;
7979
state: AnnotatorState;
8080
}
8181
declare export var Status: {|

src/elements/common/annotator-context/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ export interface AnnotatorContext {
5353
emitAnnotationReplyDeleteEvent?: (id: string, annotationId: string, isStartEvent?: boolean) => void;
5454
emitAnnotationReplyUpdateEvent?: (reply: Object, annotationId: string, isStartEvent?: boolean) => void;
5555
emitAnnotationUpdateEvent?: (annotation: Object, isStartEvent?: boolean) => void;
56-
getAnnotationsMatchPath: GetMatchPath;
57-
getAnnotationsPath: (fileVersionId?: string, annotationId?: string) => string;
56+
getAnnotationsMatchPath?: GetMatchPath;
57+
getAnnotationsPath?: (fileVersionId?: string, annotationId?: string) => string;
5858
state: AnnotatorState;
5959
}
6060

src/elements/common/annotator-context/withAnnotations.js.flow

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ import {
1818
MatchParams,
1919
Status
2020
} from "./types";
21+
import { SidebarNavigation } from '../types/SidebarNavigation';
22+
import { type FeatureConfig } from '../feature-checking';
23+
24+
2125
export type ActiveChangeEvent = {
2226
annotationId: string | null,
2327
fileVersionId: string,
@@ -48,11 +52,11 @@ export type ComponentWithAnnotations = {
4852
isStartEvent?: boolean
4953
) => void,
5054
getAction: (eventData: AnnotationActionEvent) => Action,
51-
getAnnotationsPath: (
55+
getAnnotationsPath?: (
5256
fileVersionId?: string,
5357
annotationId?: string | null
5458
) => string,
55-
getMatchPath: GetMatchPath,
59+
getMatchPath?: GetMatchPath,
5660
handleActiveChange: ActiveChangeEventHandler,
5761
handleAnnotationChangeEvent: (id: string | null) => void,
5862
handleAnnotationCreate: (eventData: AnnotationActionEvent) => void,
@@ -70,6 +74,7 @@ export type ComponentWithAnnotations = {
7074
...
7175
};
7276
export type WithAnnotationsProps = {
77+
features?: FeatureConfig;
7378
location?: Location,
7479
onAnnotator: (annotator: Annotator) => void,
7580
onError?: (
@@ -78,7 +83,9 @@ export type WithAnnotationsProps = {
7883
contextInfo?: { [key: string]: mixed, ... }
7984
) => void,
8085
onPreviewDestroy: (shouldReset?: boolean) => void,
81-
...
86+
routerDisabled?: boolean;
87+
sidebarNavigation?: SidebarNavigation;
88+
...
8289
};
8390
export type WithAnnotationsComponent<P> = React.ComponentClass<
8491
P & WithAnnotationsProps

src/elements/common/annotator-context/withAnnotations.tsx

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import getProp from 'lodash/get';
33
import { generatePath, match as matchType, matchPath } from 'react-router-dom';
44
import { Location } from 'history';
55
import AnnotatorContext from './AnnotatorContext';
6+
import { isFeatureEnabled, type FeatureConfig } from '../feature-checking';
67
import { Action, Annotator, AnnotationActionEvent, AnnotatorState, GetMatchPath, MatchParams, Status } from './types';
8+
import { FeedEntryType, SidebarNavigation } from '../types/SidebarNavigation';
79

810
export type ActiveChangeEvent = {
911
annotationId: string | null;
@@ -41,10 +43,13 @@ export type ComponentWithAnnotations = {
4143
};
4244

4345
export type WithAnnotationsProps = {
46+
features?: FeatureConfig;
4447
location?: Location;
4548
onAnnotator: (annotator: Annotator) => void;
4649
onError?: (error: Error, code: string, contextInfo?: Record<string, unknown>) => void;
4750
onPreviewDestroy: (shouldReset?: boolean) => void;
51+
routerDisabled?: boolean;
52+
sidebarNavigation?: SidebarNavigation;
4853
};
4954

5055
export type WithAnnotationsComponent<P> = React.ComponentClass<P & WithAnnotationsProps>;
@@ -71,10 +76,26 @@ export default function withAnnotations<P extends object>(
7176
constructor(props: P & WithAnnotationsProps) {
7277
super(props);
7378

74-
// Determine by url if there is already a deeply linked annotation
75-
const { location } = props;
76-
const match = this.getMatchPath(location);
77-
const activeAnnotationId = getProp(match, 'params.annotationId', null);
79+
const { routerDisabled, sidebarNavigation } = props;
80+
let activeAnnotationId = null;
81+
82+
const isRouterDisabled = routerDisabled || isFeatureEnabled(props?.features, 'routerDisabled.value');
83+
84+
if (isRouterDisabled) {
85+
if (
86+
sidebarNavigation &&
87+
'activeFeedEntryType' in sidebarNavigation &&
88+
sidebarNavigation.activeFeedEntryType === FeedEntryType.ANNOTATIONS &&
89+
'activeFeedEntryId' in sidebarNavigation
90+
) {
91+
activeAnnotationId = sidebarNavigation.activeFeedEntryId;
92+
}
93+
} else {
94+
// Determine by url if there is already a deeply linked annotation
95+
const { location } = props;
96+
const match = this.getMatchPath(location);
97+
activeAnnotationId = getProp(match, 'params.annotationId', null);
98+
}
7899

79100
// Seed the initial state with the activeAnnotationId if any from the location path
80101
this.state = { ...defaultState, activeAnnotationId };
@@ -192,6 +213,7 @@ export default function withAnnotations<P extends object>(
192213
});
193214
}
194215

216+
// remove this method with routerDisabled switch
195217
getMatchPath(location?: Location): matchType<MatchParams> | null {
196218
const pathname = getProp(location, 'pathname', '');
197219
return matchPath<MatchParams>(pathname, {
@@ -319,6 +341,14 @@ export default function withAnnotations<P extends object>(
319341
};
320342

321343
render(): JSX.Element {
344+
const isRouterDisabled =
345+
this.props?.routerDisabled || isFeatureEnabled(this.props?.features, 'routerDisabled.value');
346+
const annotationsRouterProps = isRouterDisabled
347+
? {}
348+
: {
349+
getAnnotationsMatchPath: this.getMatchPath,
350+
getAnnotationsPath: this.getAnnotationsPath,
351+
};
322352
return (
323353
<AnnotatorContext.Provider
324354
value={{
@@ -328,8 +358,7 @@ export default function withAnnotations<P extends object>(
328358
emitAnnotationReplyDeleteEvent: this.emitAnnotationReplyDeleteEvent,
329359
emitAnnotationReplyUpdateEvent: this.emitAnnotationReplyUpdateEvent,
330360
emitAnnotationUpdateEvent: this.emitAnnotationUpdateEvent,
331-
getAnnotationsMatchPath: this.getMatchPath,
332-
getAnnotationsPath: this.getAnnotationsPath,
361+
...annotationsRouterProps,
333362
state: this.state,
334363
}}
335364
>

src/elements/common/annotator-context/withAnnotatorContext.js.flow

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import * as React from "react";
88
import AnnotatorContext from "./AnnotatorContext";
99
import { AnnotatorState, GetMatchPath } from "./types";
10+
1011
export interface WithAnnotatorContextProps {
1112
annotatorState?: AnnotatorState;
1213
emitActiveAnnotationChangeEvent?: (id: string) => void;

0 commit comments

Comments
 (0)