box-annotations 5.2.1-beta.8 → 5.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/annotations.css +1 -1
- package/dist/annotations.js +1 -1
- package/dist/annotations.js.LICENSE.txt +1 -1
- package/index.js +1 -1
- package/package.json +33 -22
- package/src/@types/api.ts +3 -2
- package/src/@types/events.ts +27 -10
- package/src/@types/global.ts +0 -9
- package/src/@types/model.ts +9 -0
- package/src/BoxAnnotations.ts +5 -1
- package/src/adapters/threadedAnnotationsAdapters.ts +39 -26
- package/src/api/APIFactory.ts +6 -1
- package/src/api/types.ts +23 -1
- package/src/common/AnnotationCallbacksContext.ts +7 -0
- package/src/common/BaseAnnotator.scss +12 -0
- package/src/common/BaseAnnotator.ts +32 -2
- package/src/common/BaseManager.ts +2 -0
- package/src/common/DeselectListener.tsx +1 -1
- package/src/common/withProviders.tsx +6 -2
- package/src/components/Popups/PopupDrawingToolbar.tsx +0 -7
- package/src/components/Popups/PopupV2.tsx +111 -42
- package/src/document/DocumentAnnotator.ts +3 -2
- package/src/document/docUtil.ts +2 -1
- package/src/drawing/DrawingAnnotations.tsx +23 -14
- package/src/drawing/DrawingAnnotationsContainer.tsx +3 -0
- package/src/drawing/DrawingCreator.tsx +9 -16
- package/src/drawing/DrawingManager.tsx +13 -2
- package/src/highlight/HighlightAnnotations.tsx +27 -14
- package/src/highlight/HighlightContainer.tsx +3 -0
- package/src/highlight/HighlightCreatorManager.ts +8 -5
- package/src/highlight/HighlightListener.ts +4 -2
- package/src/highlight/HighlightManager.tsx +13 -2
- package/src/image/ImageAnnotator.ts +2 -1
- package/src/media/MediaAnnotator.ts +1 -0
- package/src/popup/PopupLayer.tsx +37 -6
- package/src/popup/PopupManager.tsx +4 -2
- package/src/region/RegionCreation.tsx +11 -8
- package/src/region/RegionCreationContainer.tsx +2 -2
- package/src/region/RegionCreator.tsx +9 -17
- package/src/store/annotations/actions.ts +89 -0
- package/src/store/annotations/reducer.ts +48 -0
- package/src/store/eventing/create.ts +3 -3
- package/src/store/eventing/delete.ts +27 -0
- package/src/store/eventing/middleware.ts +40 -4
- package/src/store/eventing/replyCreate.ts +34 -0
- package/src/store/eventing/replyDelete.ts +35 -0
- package/src/store/eventing/replyUpdate.ts +39 -0
- package/src/store/eventing/types.ts +4 -4
- package/src/store/eventing/update.ts +29 -0
- package/src/store/highlight/actions.ts +32 -3
- package/src/store/highlight/types.ts +1 -0
- package/src/store/options/selectors.ts +2 -2
- package/src/store/options/types.ts +2 -2
- package/src/utils/rotate.ts +43 -0
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import * as ReactDOM from 'react-dom/client';
|
|
3
|
-
import BaseManager, { Props } from '../common/BaseManager';
|
|
3
|
+
import BaseManager, { Options as BaseOptions, Props } from '../common/BaseManager';
|
|
4
4
|
import HighlightContainer from './HighlightContainer';
|
|
5
5
|
|
|
6
|
+
export type Options = BaseOptions & {
|
|
7
|
+
popupPortalEl?: HTMLElement | null;
|
|
8
|
+
};
|
|
9
|
+
|
|
6
10
|
export default class HighlightManager extends BaseManager {
|
|
11
|
+
popupPortalEl?: HTMLElement | null;
|
|
12
|
+
|
|
13
|
+
constructor({ popupPortalEl, ...options }: Options) {
|
|
14
|
+
super(options);
|
|
15
|
+
this.popupPortalEl = popupPortalEl;
|
|
16
|
+
}
|
|
17
|
+
|
|
7
18
|
decorate(): void {
|
|
8
19
|
this.reactEl.classList.add('ba-Layer--highlight');
|
|
9
20
|
this.reactEl.dataset.testid = 'ba-Layer--highlight';
|
|
@@ -14,6 +25,6 @@ export default class HighlightManager extends BaseManager {
|
|
|
14
25
|
this.root = ReactDOM.createRoot(this.reactEl);
|
|
15
26
|
}
|
|
16
27
|
|
|
17
|
-
this.root.render(<HighlightContainer location={this.location} {...props} />);
|
|
28
|
+
this.root.render(<HighlightContainer location={this.location} popupPortalEl={this.popupPortalEl} {...props} />);
|
|
18
29
|
}
|
|
19
30
|
}
|
|
@@ -74,7 +74,7 @@ export default class ImageAnnotator extends BaseAnnotator {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
this.managers.add(new PopupManager({ popupPortalEl: this.popupPortalEl, referenceEl, resinTags }));
|
|
77
|
-
this.managers.add(new DrawingManager({ referenceEl, resinTags }));
|
|
77
|
+
this.managers.add(new DrawingManager({ popupPortalEl: this.popupPortalEl, referenceEl, resinTags }));
|
|
78
78
|
this.managers.add(new RegionManager({ referenceEl, resinTags }));
|
|
79
79
|
this.managers.add(new RegionCreationManager({ referenceEl, resinTags }));
|
|
80
80
|
}
|
|
@@ -124,6 +124,7 @@ export default class ImageAnnotator extends BaseAnnotator {
|
|
|
124
124
|
});
|
|
125
125
|
|
|
126
126
|
manager.render({
|
|
127
|
+
callbacks: { onCopyLink: this.onCopyLink },
|
|
127
128
|
intl: this.intl,
|
|
128
129
|
store: this.store,
|
|
129
130
|
});
|
package/src/popup/PopupLayer.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
+
import * as ReactDOM from 'react-dom';
|
|
2
3
|
import noop from 'lodash/noop';
|
|
3
4
|
import PopupReply from '../components/Popups/PopupReply';
|
|
4
5
|
import PopupV2 from '../components/Popups/PopupV2';
|
|
@@ -44,6 +45,8 @@ const modeStagedMap: { [M in Mode]?: (staged: CreatorItem | null) => boolean } =
|
|
|
44
45
|
[Mode.REGION]: isCreatorStagedRegion,
|
|
45
46
|
};
|
|
46
47
|
|
|
48
|
+
const ACTIVE_TARGET_OBSERVER_TIMEOUT_MS = 10000;
|
|
49
|
+
|
|
47
50
|
const PopupLayer = (props: Props): JSX.Element | null => {
|
|
48
51
|
const {
|
|
49
52
|
activeAnnotationId,
|
|
@@ -97,13 +100,39 @@ const PopupLayer = (props: Props): JSX.Element | null => {
|
|
|
97
100
|
setReference(referenceId ? document.querySelector(`[data-ba-reference-id="${referenceId}"]`) : null);
|
|
98
101
|
}, [referenceId]);
|
|
99
102
|
|
|
103
|
+
// activeAnnotationId can be set (e.g. via deep link) before the target DOM node mounts;
|
|
104
|
+
// observe until it appears, or give up after ACTIVE_TARGET_OBSERVER_TIMEOUT_MS.
|
|
100
105
|
React.useEffect(() => {
|
|
101
|
-
if (activeAnnotationId
|
|
102
|
-
const el = document.querySelector(`[data-ba-annotation-id="${CSS.escape(activeAnnotationId)}"]`);
|
|
103
|
-
setActiveReference(el);
|
|
104
|
-
} else {
|
|
106
|
+
if (!activeAnnotationId || !isThreadedAnnotation) {
|
|
105
107
|
setActiveReference(null);
|
|
108
|
+
return noop;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const selector = `[data-ba-annotation-id="${CSS.escape(activeAnnotationId)}"]`;
|
|
112
|
+
const existing = document.querySelector(selector);
|
|
113
|
+
if (existing) {
|
|
114
|
+
setActiveReference(existing);
|
|
115
|
+
return noop;
|
|
106
116
|
}
|
|
117
|
+
|
|
118
|
+
setActiveReference(null);
|
|
119
|
+
const observer = new MutationObserver(() => {
|
|
120
|
+
const el = document.querySelector(selector);
|
|
121
|
+
if (el) {
|
|
122
|
+
setActiveReference(el);
|
|
123
|
+
observer.disconnect();
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
127
|
+
|
|
128
|
+
const timeoutId = window.setTimeout(() => {
|
|
129
|
+
observer.disconnect();
|
|
130
|
+
}, ACTIVE_TARGET_OBSERVER_TIMEOUT_MS);
|
|
131
|
+
|
|
132
|
+
return () => {
|
|
133
|
+
observer.disconnect();
|
|
134
|
+
window.clearTimeout(timeoutId);
|
|
135
|
+
};
|
|
107
136
|
}, [activeAnnotationId, isThreadedAnnotation]);
|
|
108
137
|
|
|
109
138
|
const showCreator = canCreate && canReply && reference && staged;
|
|
@@ -121,7 +150,8 @@ const PopupLayer = (props: Props): JSX.Element | null => {
|
|
|
121
150
|
}
|
|
122
151
|
|
|
123
152
|
if (showCreator) {
|
|
124
|
-
return
|
|
153
|
+
if (!popupPortalEl) return null;
|
|
154
|
+
return ReactDOM.createPortal(
|
|
125
155
|
<div className="ba-PopupLayer-popup">
|
|
126
156
|
<PopupReply
|
|
127
157
|
isPending={isPending}
|
|
@@ -131,7 +161,8 @@ const PopupLayer = (props: Props): JSX.Element | null => {
|
|
|
131
161
|
reference={reference}
|
|
132
162
|
value={message}
|
|
133
163
|
/>
|
|
134
|
-
</div
|
|
164
|
+
</div>,
|
|
165
|
+
popupPortalEl,
|
|
135
166
|
);
|
|
136
167
|
}
|
|
137
168
|
|
|
@@ -20,16 +20,18 @@ export default class PopupManager extends BaseManager {
|
|
|
20
20
|
this.reactEl.dataset.testid = 'ba-Layer--popup';
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
render(
|
|
23
|
+
render({ callbacks, intl, store }: Props): void {
|
|
24
24
|
if (!this.root) {
|
|
25
25
|
this.root = ReactDOM.createRoot(this.reactEl);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
this.root.render(
|
|
29
29
|
<PopupContainer
|
|
30
|
+
callbacks={callbacks}
|
|
31
|
+
intl={intl}
|
|
30
32
|
location={this.location}
|
|
31
|
-
{...props}
|
|
32
33
|
popupPortalEl={this.popupPortalEl}
|
|
34
|
+
store={store}
|
|
33
35
|
targetType={this.targetType}
|
|
34
36
|
/>,
|
|
35
37
|
);
|
|
@@ -9,9 +9,9 @@ import { getVideoCurrentTimeInMilliseconds } from '../utils/useVideoTiming';
|
|
|
9
9
|
|
|
10
10
|
type Props = {
|
|
11
11
|
isCreating: boolean;
|
|
12
|
-
isRotated: boolean;
|
|
13
12
|
location: number;
|
|
14
13
|
resetCreator: () => void;
|
|
14
|
+
rotation: number;
|
|
15
15
|
setReferenceId: (uuid: string) => void;
|
|
16
16
|
setStaged: (staged: CreatorItemRegion | null) => void;
|
|
17
17
|
setStatus: (status: CreatorStatus) => void;
|
|
@@ -27,13 +27,19 @@ type State = {
|
|
|
27
27
|
export default class RegionCreation extends React.PureComponent<Props, State> {
|
|
28
28
|
static defaultProps = {
|
|
29
29
|
isCreating: false,
|
|
30
|
-
|
|
30
|
+
rotation: 0,
|
|
31
31
|
};
|
|
32
32
|
|
|
33
33
|
state: State = {};
|
|
34
34
|
|
|
35
35
|
handleAbort = (): void => {
|
|
36
|
-
const { resetCreator } = this.props;
|
|
36
|
+
const { resetCreator, staged } = this.props;
|
|
37
|
+
|
|
38
|
+
// Click-away (a click too small to start a new region) must not discard an in-progress comment/annotation
|
|
39
|
+
if (staged) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
37
43
|
resetCreator();
|
|
38
44
|
};
|
|
39
45
|
|
|
@@ -57,11 +63,7 @@ export default class RegionCreation extends React.PureComponent<Props, State> {
|
|
|
57
63
|
};
|
|
58
64
|
|
|
59
65
|
render(): JSX.Element | null {
|
|
60
|
-
const { isCreating,
|
|
61
|
-
|
|
62
|
-
if (isRotated) {
|
|
63
|
-
return null;
|
|
64
|
-
}
|
|
66
|
+
const { isCreating, rotation, staged } = this.props;
|
|
65
67
|
|
|
66
68
|
return (
|
|
67
69
|
<>
|
|
@@ -71,6 +73,7 @@ export default class RegionCreation extends React.PureComponent<Props, State> {
|
|
|
71
73
|
onAbort={this.handleAbort}
|
|
72
74
|
onStart={this.handleStart}
|
|
73
75
|
onStop={this.handleStop}
|
|
76
|
+
rotation={rotation}
|
|
74
77
|
/>
|
|
75
78
|
)}
|
|
76
79
|
|
|
@@ -19,7 +19,7 @@ import withProviders from '../common/withProviders';
|
|
|
19
19
|
|
|
20
20
|
export type Props = {
|
|
21
21
|
isCreating: boolean;
|
|
22
|
-
|
|
22
|
+
rotation: number;
|
|
23
23
|
staged: CreatorItemRegion | null;
|
|
24
24
|
};
|
|
25
25
|
|
|
@@ -28,7 +28,7 @@ export const mapStateToProps = (state: AppState, { location }: { location: numbe
|
|
|
28
28
|
|
|
29
29
|
return {
|
|
30
30
|
isCreating: getAnnotationMode(state) === Mode.REGION && getCreatorStatus(state) !== CreatorStatus.pending,
|
|
31
|
-
|
|
31
|
+
rotation: getRotation(state),
|
|
32
32
|
staged: isCreatorStagedRegion(staged) ? staged : null,
|
|
33
33
|
};
|
|
34
34
|
};
|
|
@@ -4,6 +4,7 @@ import PointerCapture, { Status as DrawingStatus } from '../components/PointerCa
|
|
|
4
4
|
import PopupCursor from '../components/Popups/PopupCursor';
|
|
5
5
|
import RegionRect, { RegionRectRef } from './RegionRect';
|
|
6
6
|
import useAutoScroll from '../common/useAutoScroll';
|
|
7
|
+
import { getElementLocalPosition } from '../utils/rotate';
|
|
7
8
|
import { Rect } from '../@types';
|
|
8
9
|
import { styleShape } from './regionUtil';
|
|
9
10
|
import './RegionCreator.scss';
|
|
@@ -13,6 +14,7 @@ type Props = {
|
|
|
13
14
|
onAbort: () => void;
|
|
14
15
|
onStart: () => void;
|
|
15
16
|
onStop: (shape: Rect) => void;
|
|
17
|
+
rotation?: number;
|
|
16
18
|
};
|
|
17
19
|
|
|
18
20
|
const MIN_X = 0; // Minimum region x position must be positive
|
|
@@ -22,7 +24,7 @@ const MIN_SIZE = 10; // Minimum region size must be large enough to be clickable
|
|
|
22
24
|
const isValid = (x1: number, y1: number, x2: number, y2: number): boolean =>
|
|
23
25
|
Math.abs(x2 - x1) >= MIN_SIZE || Math.abs(y2 - y1) >= MIN_SIZE;
|
|
24
26
|
|
|
25
|
-
export default function RegionCreator({ className, onAbort, onStart, onStop }: Props): JSX.Element {
|
|
27
|
+
export default function RegionCreator({ className, onAbort, onStart, onStop, rotation = 0 }: Props): JSX.Element {
|
|
26
28
|
const [drawingStatus, setDrawingStatus] = React.useState<DrawingStatus>(DrawingStatus.init);
|
|
27
29
|
const [isHovering, setIsHovering] = React.useState<boolean>(false);
|
|
28
30
|
const creatorElRef = React.useRef<HTMLDivElement>(null);
|
|
@@ -34,18 +36,6 @@ export default function RegionCreator({ className, onAbort, onStart, onStop }: P
|
|
|
34
36
|
const regionRectRef = React.useRef<RegionRectRef>(null);
|
|
35
37
|
const renderHandleRef = React.useRef<number | null>(null);
|
|
36
38
|
|
|
37
|
-
// Drawing Helpers
|
|
38
|
-
const getPosition = (x: number, y: number): [number, number] => {
|
|
39
|
-
const { current: creatorEl } = creatorElRef;
|
|
40
|
-
|
|
41
|
-
if (!creatorEl) {
|
|
42
|
-
return [x, y];
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Calculate the new position based on the mouse position less the page offset
|
|
46
|
-
const { left, top } = creatorEl.getBoundingClientRect();
|
|
47
|
-
return [x - left, y - top];
|
|
48
|
-
};
|
|
49
39
|
const getShape = (): Rect | null => {
|
|
50
40
|
const { current: creatorEl } = creatorElRef;
|
|
51
41
|
const { current: x1 } = positionX1Ref;
|
|
@@ -57,7 +47,9 @@ export default function RegionCreator({ className, onAbort, onStart, onStop }: P
|
|
|
57
47
|
return null;
|
|
58
48
|
}
|
|
59
49
|
|
|
60
|
-
|
|
50
|
+
// Get the element's dimensions (before any rotation is applied)
|
|
51
|
+
const width = creatorEl.offsetWidth;
|
|
52
|
+
const height = creatorEl.offsetHeight;
|
|
61
53
|
const MAX_HEIGHT = height - MIN_SIZE;
|
|
62
54
|
const MAX_WIDTH = width - MIN_SIZE;
|
|
63
55
|
const MAX_X = Math.max(0, width - MIN_X);
|
|
@@ -88,7 +80,7 @@ export default function RegionCreator({ className, onAbort, onStart, onStop }: P
|
|
|
88
80
|
|
|
89
81
|
// Drawing Lifecycle Callbacks
|
|
90
82
|
const startDraw = (x: number, y: number): void => {
|
|
91
|
-
const [x1, y1] =
|
|
83
|
+
const [x1, y1] = getElementLocalPosition(x, y, creatorElRef.current, rotation);
|
|
92
84
|
|
|
93
85
|
setDrawingStatus(DrawingStatus.dragging);
|
|
94
86
|
|
|
@@ -119,7 +111,7 @@ export default function RegionCreator({ className, onAbort, onStart, onStop }: P
|
|
|
119
111
|
|
|
120
112
|
const updateDraw = React.useCallback(
|
|
121
113
|
(x: number, y: number): void => {
|
|
122
|
-
const [x2, y2] =
|
|
114
|
+
const [x2, y2] = getElementLocalPosition(x, y, creatorElRef.current, rotation);
|
|
123
115
|
const { current: x1 } = positionX1Ref;
|
|
124
116
|
const { current: y1 } = positionY1Ref;
|
|
125
117
|
const { current: prevX2 } = positionX2Ref;
|
|
@@ -138,7 +130,7 @@ export default function RegionCreator({ className, onAbort, onStart, onStop }: P
|
|
|
138
130
|
onStart();
|
|
139
131
|
}
|
|
140
132
|
},
|
|
141
|
-
[drawingStatus, onStart, setDrawingStatus],
|
|
133
|
+
[drawingStatus, onStart, rotation, setDrawingStatus],
|
|
142
134
|
);
|
|
143
135
|
|
|
144
136
|
// Event Handlers
|
|
@@ -121,6 +121,95 @@ export const updateAnnotationAction = createAsyncThunk<
|
|
|
121
121
|
},
|
|
122
122
|
);
|
|
123
123
|
|
|
124
|
+
export const updateReplyAction = createAsyncThunk<
|
|
125
|
+
{ annotationId: string; reply: Reply },
|
|
126
|
+
{ annotationId: string; replyId: string; payload: { message?: string; status?: string } },
|
|
127
|
+
AppThunkAPI
|
|
128
|
+
>(
|
|
129
|
+
'UPDATE_REPLY',
|
|
130
|
+
async ({ annotationId, replyId, payload }, { extra, getState, signal }) => {
|
|
131
|
+
const client = extra.api.getThreadedCommentsAPI();
|
|
132
|
+
const state = getState();
|
|
133
|
+
const fileId = getFileId(state);
|
|
134
|
+
const annotation = getAnnotation(state, annotationId);
|
|
135
|
+
const reply = annotation?.replies?.find(r => r.id === replyId);
|
|
136
|
+
|
|
137
|
+
if (!reply) {
|
|
138
|
+
throw new Error(`updateReplyAction: reply ${replyId} not found on annotation ${annotationId}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
signal.addEventListener('abort', () => {
|
|
142
|
+
client.destroy();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const updated = await new Promise<Reply>((resolve, reject) => {
|
|
146
|
+
client.updateComment({
|
|
147
|
+
commentId: replyId,
|
|
148
|
+
errorCallback: reject,
|
|
149
|
+
fileId,
|
|
150
|
+
message: payload.message,
|
|
151
|
+
permissions: reply.permissions ?? {},
|
|
152
|
+
status: payload.status,
|
|
153
|
+
successCallback: resolve,
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
return { annotationId, reply: updated };
|
|
158
|
+
},
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
export const deleteReplyAction = createAsyncThunk<
|
|
162
|
+
{ annotationId: string; replyId: string },
|
|
163
|
+
{ annotationId: string; replyId: string },
|
|
164
|
+
AppThunkAPI
|
|
165
|
+
>(
|
|
166
|
+
'DELETE_REPLY',
|
|
167
|
+
async ({ annotationId, replyId }, { extra, getState, signal }) => {
|
|
168
|
+
const client = extra.api.getThreadedCommentsAPI();
|
|
169
|
+
const state = getState();
|
|
170
|
+
const fileId = getFileId(state);
|
|
171
|
+
const annotation = getAnnotation(state, annotationId);
|
|
172
|
+
const reply = annotation?.replies?.find(r => r.id === replyId);
|
|
173
|
+
|
|
174
|
+
if (!reply) {
|
|
175
|
+
throw new Error(`deleteReplyAction: reply ${replyId} not found on annotation ${annotationId}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
signal.addEventListener('abort', () => {
|
|
179
|
+
client.destroy();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
await new Promise<void>((resolve, reject) => {
|
|
183
|
+
client.deleteComment({
|
|
184
|
+
commentId: replyId,
|
|
185
|
+
errorCallback: reject,
|
|
186
|
+
fileId,
|
|
187
|
+
permissions: reply.permissions ?? {},
|
|
188
|
+
successCallback: resolve,
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
return { annotationId, replyId };
|
|
193
|
+
},
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
export type SidebarAnnotationUpdatePayload = Partial<Annotation> & { id: string };
|
|
197
|
+
export type SidebarReplyMutationPayload = { annotationId: string; reply: Reply };
|
|
198
|
+
export type SidebarReplyDeletePayload = { annotationId: string; replyId: string };
|
|
199
|
+
|
|
200
|
+
export const applySidebarAnnotationUpdateAction = createAction<SidebarAnnotationUpdatePayload>(
|
|
201
|
+
'APPLY_SIDEBAR_ANNOTATION_UPDATE',
|
|
202
|
+
);
|
|
203
|
+
export const applySidebarReplyCreateAction = createAction<SidebarReplyMutationPayload>(
|
|
204
|
+
'APPLY_SIDEBAR_REPLY_CREATE',
|
|
205
|
+
);
|
|
206
|
+
export const applySidebarReplyDeleteAction = createAction<SidebarReplyDeletePayload>(
|
|
207
|
+
'APPLY_SIDEBAR_REPLY_DELETE',
|
|
208
|
+
);
|
|
209
|
+
export const applySidebarReplyUpdateAction = createAction<SidebarReplyMutationPayload>(
|
|
210
|
+
'APPLY_SIDEBAR_REPLY_UPDATE',
|
|
211
|
+
);
|
|
212
|
+
|
|
124
213
|
export const removeAnnotationAction = createAction<string>('REMOVE_ANNOTATION');
|
|
125
214
|
export const setActiveAnnotationIdAction = createAction<string | null>('SET_ACTIVE_ANNOTATION_ID');
|
|
126
215
|
export const setIsInitialized = createAction('SET_IS_INITIALIZED');
|
|
@@ -2,14 +2,20 @@ import { createReducer, combineReducers } from '@reduxjs/toolkit';
|
|
|
2
2
|
import { formatDrawing, isDrawing } from '../../drawing/drawingUtil';
|
|
3
3
|
import { AnnotationsState } from './types';
|
|
4
4
|
import {
|
|
5
|
+
applySidebarAnnotationUpdateAction,
|
|
6
|
+
applySidebarReplyCreateAction,
|
|
7
|
+
applySidebarReplyDeleteAction,
|
|
8
|
+
applySidebarReplyUpdateAction,
|
|
5
9
|
createAnnotationAction,
|
|
6
10
|
createReplyAction,
|
|
7
11
|
deleteAnnotationAction,
|
|
12
|
+
deleteReplyAction,
|
|
8
13
|
fetchAnnotationsAction,
|
|
9
14
|
removeAnnotationAction,
|
|
10
15
|
setActiveAnnotationIdAction,
|
|
11
16
|
setIsInitialized,
|
|
12
17
|
updateAnnotationAction,
|
|
18
|
+
updateReplyAction,
|
|
13
19
|
} from './actions';
|
|
14
20
|
import { setViewModeAction } from '../options/actions';
|
|
15
21
|
|
|
@@ -58,6 +64,48 @@ const annotationsById = createReducer<AnnotationsState['byId']>({}, builder =>
|
|
|
58
64
|
.addCase(updateAnnotationAction.fulfilled, (state, { payload }) => {
|
|
59
65
|
state[payload.id] = isDrawing(payload) ? formatDrawing(payload) : payload;
|
|
60
66
|
})
|
|
67
|
+
.addCase(updateReplyAction.fulfilled, (state, { payload: { annotationId, reply } }) => {
|
|
68
|
+
const annotation = state[annotationId];
|
|
69
|
+
if (annotation && annotation.replies) {
|
|
70
|
+
annotation.replies = annotation.replies.map(existing =>
|
|
71
|
+
existing.id === reply.id ? reply : existing,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
.addCase(deleteReplyAction.fulfilled, (state, { payload: { annotationId, replyId } }) => {
|
|
76
|
+
const annotation = state[annotationId];
|
|
77
|
+
if (annotation && annotation.replies) {
|
|
78
|
+
annotation.replies = annotation.replies.filter(existing => existing.id !== replyId);
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
.addCase(applySidebarAnnotationUpdateAction, (state, { payload }) => {
|
|
82
|
+
const existing = state[payload.id];
|
|
83
|
+
if (!existing) return;
|
|
84
|
+
Object.entries(payload).forEach(([key, value]) => {
|
|
85
|
+
if (value !== undefined) {
|
|
86
|
+
(existing as Record<string, unknown>)[key] = value;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
})
|
|
90
|
+
.addCase(applySidebarReplyCreateAction, (state, { payload: { annotationId, reply } }) => {
|
|
91
|
+
const annotation = state[annotationId];
|
|
92
|
+
if (!annotation) return;
|
|
93
|
+
const replies = annotation.replies ?? [];
|
|
94
|
+
if (replies.some(existing => existing.id === reply.id)) return;
|
|
95
|
+
annotation.replies = [...replies, reply];
|
|
96
|
+
})
|
|
97
|
+
.addCase(applySidebarReplyUpdateAction, (state, { payload: { annotationId, reply } }) => {
|
|
98
|
+
const annotation = state[annotationId];
|
|
99
|
+
if (!annotation || !annotation.replies) return;
|
|
100
|
+
annotation.replies = annotation.replies.map(existing =>
|
|
101
|
+
existing.id === reply.id ? { ...existing, ...reply } : existing,
|
|
102
|
+
);
|
|
103
|
+
})
|
|
104
|
+
.addCase(applySidebarReplyDeleteAction, (state, { payload: { annotationId, replyId } }) => {
|
|
105
|
+
const annotation = state[annotationId];
|
|
106
|
+
if (!annotation || !annotation.replies) return;
|
|
107
|
+
annotation.replies = annotation.replies.filter(existing => existing.id !== replyId);
|
|
108
|
+
})
|
|
61
109
|
.addCase(fetchAnnotationsAction.fulfilled, (state, { payload }) => {
|
|
62
110
|
payload.entries.forEach(annotation => {
|
|
63
111
|
state[annotation.id] = isDrawing(annotation) ? formatDrawing(annotation) : annotation;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import eventManager from '../../common/EventManager';
|
|
2
2
|
import { AsyncAction, Status } from './types';
|
|
3
3
|
import { AppState } from '../types';
|
|
4
|
-
import { Event } from '../../@types';
|
|
4
|
+
import { Annotation, Event, NewAnnotation } from '../../@types';
|
|
5
5
|
|
|
6
|
-
const emitCreateEvent = (action: AsyncAction, status: Status): void => {
|
|
6
|
+
const emitCreateEvent = (action: AsyncAction<NewAnnotation, Annotation>, status: Status): void => {
|
|
7
7
|
const { error, meta: { arg, requestId } = {}, payload } = action;
|
|
8
8
|
eventManager.emit(Event.ANNOTATION_CREATE, {
|
|
9
9
|
annotation: payload || arg,
|
|
@@ -16,7 +16,7 @@ const emitCreateEvent = (action: AsyncAction, status: Status): void => {
|
|
|
16
16
|
};
|
|
17
17
|
|
|
18
18
|
const createHandler = (status: Status) => (prevState: AppState, nextState: AppState, action: AsyncAction): void =>
|
|
19
|
-
emitCreateEvent(action, status);
|
|
19
|
+
emitCreateEvent(action as AsyncAction<NewAnnotation, Annotation>, status);
|
|
20
20
|
|
|
21
21
|
const handleCreateErrorEvents = createHandler(Status.ERROR);
|
|
22
22
|
const handleCreatePendingEvents = createHandler(Status.PENDING);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import eventManager from '../../common/EventManager';
|
|
2
|
+
import { AppState } from '../types';
|
|
3
|
+
import { AsyncAction, Status } from './types';
|
|
4
|
+
import { Event } from '../../@types';
|
|
5
|
+
|
|
6
|
+
const emitDeleteEvent = (action: AsyncAction<string, string>, status: Status): void => {
|
|
7
|
+
const { error, meta: { arg, requestId } = {}, payload } = action;
|
|
8
|
+
if (status === Status.SUCCESS && !payload) return;
|
|
9
|
+
const id = payload ?? arg;
|
|
10
|
+
eventManager.emit(Event.ANNOTATION_DELETE, {
|
|
11
|
+
annotation: id ? { id } : undefined,
|
|
12
|
+
error,
|
|
13
|
+
meta: { requestId, status },
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const deleteHandler = (status: Status) => (
|
|
18
|
+
_prev: AppState,
|
|
19
|
+
_next: AppState,
|
|
20
|
+
action: AsyncAction,
|
|
21
|
+
): void => emitDeleteEvent(action as AsyncAction<string, string>, status);
|
|
22
|
+
|
|
23
|
+
const handleDeleteErrorEvents = deleteHandler(Status.ERROR);
|
|
24
|
+
const handleDeletePendingEvents = deleteHandler(Status.PENDING);
|
|
25
|
+
const handleDeleteSuccessEvents = deleteHandler(Status.SUCCESS);
|
|
26
|
+
|
|
27
|
+
export { handleDeleteErrorEvents, handleDeletePendingEvents, handleDeleteSuccessEvents };
|
|
@@ -2,28 +2,58 @@ import noop from 'lodash/noop';
|
|
|
2
2
|
import { Action, Dispatch, Middleware, MiddlewareAPI } from '@reduxjs/toolkit';
|
|
3
3
|
import {
|
|
4
4
|
createAnnotationAction,
|
|
5
|
+
createReplyAction,
|
|
6
|
+
deleteAnnotationAction,
|
|
7
|
+
deleteReplyAction,
|
|
5
8
|
fetchAnnotationsAction,
|
|
6
9
|
setActiveAnnotationIdAction,
|
|
7
10
|
setIsInitialized,
|
|
11
|
+
updateAnnotationAction,
|
|
12
|
+
updateReplyAction,
|
|
8
13
|
} from '../annotations/actions';
|
|
9
14
|
import { navigateBoundingBoxHighlightAction } from '../boundingBoxHighlights/actions';
|
|
10
15
|
import { EventHandlerMap } from './types';
|
|
11
16
|
import { handleActiveAnnotationEvents } from './active';
|
|
12
17
|
import { handleAnnotationsInitialized } from './init';
|
|
18
|
+
import { handleNavigateBoundingBoxHighlight } from './boundingBoxHighlightNav';
|
|
13
19
|
import { handleCreateErrorEvents, handleCreatePendingEvents, handleCreateSuccessEvents } from './create';
|
|
20
|
+
import { handleDeleteErrorEvents, handleDeletePendingEvents, handleDeleteSuccessEvents } from './delete';
|
|
14
21
|
import { handleFetchErrorEvents } from './fetch';
|
|
15
|
-
import {
|
|
22
|
+
import { handleToggleAnnotationModeAction } from './mode';
|
|
23
|
+
import {
|
|
24
|
+
handleReplyCreateErrorEvents,
|
|
25
|
+
handleReplyCreatePendingEvents,
|
|
26
|
+
handleReplyCreateSuccessEvents,
|
|
27
|
+
} from './replyCreate';
|
|
28
|
+
import {
|
|
29
|
+
handleReplyDeleteErrorEvents,
|
|
30
|
+
handleReplyDeletePendingEvents,
|
|
31
|
+
handleReplyDeleteSuccessEvents,
|
|
32
|
+
} from './replyDelete';
|
|
33
|
+
import {
|
|
34
|
+
handleReplyUpdateErrorEvents,
|
|
35
|
+
handleReplyUpdatePendingEvents,
|
|
36
|
+
handleReplyUpdateSuccessEvents,
|
|
37
|
+
} from './replyUpdate';
|
|
16
38
|
import { handleResetCreatorAction, handleSetStagedAction } from './staged';
|
|
17
39
|
import { handleSetStatusAction } from './status';
|
|
18
|
-
import {
|
|
40
|
+
import { handleUpdateErrorEvents, handleUpdatePendingEvents, handleUpdateSuccessEvents } from './update';
|
|
19
41
|
import { resetCreatorAction, setStagedAction, setStatusAction } from '../creator';
|
|
20
42
|
import { toggleAnnotationModeAction } from '../common/actions';
|
|
21
43
|
|
|
22
|
-
|
|
23
|
-
const eventHandlers: EventHandlerMap = {
|
|
44
|
+
export const eventHandlers: EventHandlerMap = {
|
|
24
45
|
[createAnnotationAction.fulfilled.toString()]: handleCreateSuccessEvents,
|
|
25
46
|
[createAnnotationAction.pending.toString()]: handleCreatePendingEvents,
|
|
26
47
|
[createAnnotationAction.rejected.toString()]: handleCreateErrorEvents,
|
|
48
|
+
[createReplyAction.fulfilled.toString()]: handleReplyCreateSuccessEvents,
|
|
49
|
+
[createReplyAction.pending.toString()]: handleReplyCreatePendingEvents,
|
|
50
|
+
[createReplyAction.rejected.toString()]: handleReplyCreateErrorEvents,
|
|
51
|
+
[deleteAnnotationAction.fulfilled.toString()]: handleDeleteSuccessEvents,
|
|
52
|
+
[deleteAnnotationAction.pending.toString()]: handleDeletePendingEvents,
|
|
53
|
+
[deleteAnnotationAction.rejected.toString()]: handleDeleteErrorEvents,
|
|
54
|
+
[deleteReplyAction.fulfilled.toString()]: handleReplyDeleteSuccessEvents,
|
|
55
|
+
[deleteReplyAction.pending.toString()]: handleReplyDeletePendingEvents,
|
|
56
|
+
[deleteReplyAction.rejected.toString()]: handleReplyDeleteErrorEvents,
|
|
27
57
|
[fetchAnnotationsAction.rejected.toString()]: handleFetchErrorEvents,
|
|
28
58
|
[navigateBoundingBoxHighlightAction.toString()]: handleNavigateBoundingBoxHighlight,
|
|
29
59
|
[resetCreatorAction.toString()]: handleResetCreatorAction,
|
|
@@ -32,6 +62,12 @@ const eventHandlers: EventHandlerMap = {
|
|
|
32
62
|
[setStagedAction.toString()]: handleSetStagedAction,
|
|
33
63
|
[setStatusAction.toString()]: handleSetStatusAction,
|
|
34
64
|
[toggleAnnotationModeAction.toString()]: handleToggleAnnotationModeAction,
|
|
65
|
+
[updateAnnotationAction.fulfilled.toString()]: handleUpdateSuccessEvents,
|
|
66
|
+
[updateAnnotationAction.pending.toString()]: handleUpdatePendingEvents,
|
|
67
|
+
[updateAnnotationAction.rejected.toString()]: handleUpdateErrorEvents,
|
|
68
|
+
[updateReplyAction.fulfilled.toString()]: handleReplyUpdateSuccessEvents,
|
|
69
|
+
[updateReplyAction.pending.toString()]: handleReplyUpdatePendingEvents,
|
|
70
|
+
[updateReplyAction.rejected.toString()]: handleReplyUpdateErrorEvents,
|
|
35
71
|
};
|
|
36
72
|
|
|
37
73
|
function getEventingMiddleware(handlers: EventHandlerMap = eventHandlers): Middleware {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import eventManager from '../../common/EventManager';
|
|
2
|
+
import { Reply , Event } from '../../@types';
|
|
3
|
+
import { AppState } from '../types';
|
|
4
|
+
import { AsyncAction, Status } from './types';
|
|
5
|
+
|
|
6
|
+
type ReplyCreateArg = { annotationId: string; message: string };
|
|
7
|
+
type ReplyCreatePayload = { annotationId: string; reply: Reply };
|
|
8
|
+
|
|
9
|
+
const emitReplyCreateEvent = (
|
|
10
|
+
action: AsyncAction<ReplyCreateArg, ReplyCreatePayload>,
|
|
11
|
+
status: Status,
|
|
12
|
+
): void => {
|
|
13
|
+
const { error, meta: { arg, requestId } = {}, payload } = action;
|
|
14
|
+
if (status === Status.SUCCESS && !payload?.reply) return;
|
|
15
|
+
const annotationId = payload?.annotationId ?? arg?.annotationId;
|
|
16
|
+
eventManager.emit(Event.ANNOTATION_REPLY_CREATE, {
|
|
17
|
+
annotation: annotationId ? { id: annotationId } : undefined,
|
|
18
|
+
annotationReply: payload?.reply,
|
|
19
|
+
error,
|
|
20
|
+
meta: { requestId, status },
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const replyCreateHandler = (status: Status) => (
|
|
25
|
+
_prev: AppState,
|
|
26
|
+
_next: AppState,
|
|
27
|
+
action: AsyncAction,
|
|
28
|
+
): void => emitReplyCreateEvent(action as AsyncAction<ReplyCreateArg, ReplyCreatePayload>, status);
|
|
29
|
+
|
|
30
|
+
const handleReplyCreateErrorEvents = replyCreateHandler(Status.ERROR);
|
|
31
|
+
const handleReplyCreatePendingEvents = replyCreateHandler(Status.PENDING);
|
|
32
|
+
const handleReplyCreateSuccessEvents = replyCreateHandler(Status.SUCCESS);
|
|
33
|
+
|
|
34
|
+
export { handleReplyCreateErrorEvents, handleReplyCreatePendingEvents, handleReplyCreateSuccessEvents };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import eventManager from '../../common/EventManager';
|
|
2
|
+
import { AppState } from '../types';
|
|
3
|
+
import { AsyncAction, Status } from './types';
|
|
4
|
+
import { Event } from '../../@types';
|
|
5
|
+
|
|
6
|
+
type ReplyDeleteArg = { annotationId: string; replyId: string };
|
|
7
|
+
type ReplyDeletePayload = { annotationId: string; replyId: string };
|
|
8
|
+
|
|
9
|
+
const emitReplyDeleteEvent = (
|
|
10
|
+
action: AsyncAction<ReplyDeleteArg, ReplyDeletePayload>,
|
|
11
|
+
status: Status,
|
|
12
|
+
): void => {
|
|
13
|
+
const { error, meta: { arg, requestId } = {}, payload } = action;
|
|
14
|
+
if (status === Status.SUCCESS && !payload) return;
|
|
15
|
+
const annotationId = payload?.annotationId ?? arg?.annotationId;
|
|
16
|
+
const replyId = payload?.replyId ?? arg?.replyId;
|
|
17
|
+
eventManager.emit(Event.ANNOTATION_REPLY_DELETE, {
|
|
18
|
+
annotation: annotationId ? { id: annotationId } : undefined,
|
|
19
|
+
annotationReply: replyId ? { id: replyId } : undefined,
|
|
20
|
+
error,
|
|
21
|
+
meta: { requestId, status },
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const replyDeleteHandler = (status: Status) => (
|
|
26
|
+
_prev: AppState,
|
|
27
|
+
_next: AppState,
|
|
28
|
+
action: AsyncAction,
|
|
29
|
+
): void => emitReplyDeleteEvent(action as AsyncAction<ReplyDeleteArg, ReplyDeletePayload>, status);
|
|
30
|
+
|
|
31
|
+
const handleReplyDeleteErrorEvents = replyDeleteHandler(Status.ERROR);
|
|
32
|
+
const handleReplyDeletePendingEvents = replyDeleteHandler(Status.PENDING);
|
|
33
|
+
const handleReplyDeleteSuccessEvents = replyDeleteHandler(Status.SUCCESS);
|
|
34
|
+
|
|
35
|
+
export { handleReplyDeleteErrorEvents, handleReplyDeletePendingEvents, handleReplyDeleteSuccessEvents };
|