box-annotations 5.2.1-beta.2 → 5.2.1-beta.21
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 +3 -4
- package/dist/annotations.js +1 -1
- package/dist/annotations.js.LICENSE.txt +3 -43
- package/i18n/en-US.js +4 -0
- package/i18n/en-US.properties +8 -0
- package/i18n/json/src/components/BoundingBoxHighlight/messages.json +1 -0
- package/i18n/json/src/components/Popups/messages.json +1 -1
- package/index.js +1 -1
- package/package.json +33 -15
- package/src/@types/api.ts +3 -2
- package/src/@types/events.ts +28 -7
- package/src/@types/global.ts +0 -9
- package/src/@types/model.ts +14 -0
- package/src/BoxAnnotations.ts +4 -1
- package/src/adapters/threadedAnnotationsAdapters.ts +164 -0
- package/src/api/APIFactory.ts +6 -1
- package/src/api/types.ts +50 -1
- package/src/boundingBoxHighlight/BoundingBoxHighlightContainer.tsx +33 -0
- package/src/boundingBoxHighlight/BoundingBoxHighlightManager.tsx +19 -0
- package/src/boundingBoxHighlight/index.ts +1 -0
- package/src/common/AnnotationCallbacksContext.ts +7 -0
- package/src/common/BaseAnnotator.scss +14 -1
- package/src/common/BaseAnnotator.ts +161 -14
- package/src/common/BaseManager.ts +2 -0
- package/src/common/DeselectListener.tsx +3 -1
- package/src/common/withProviders.tsx +6 -2
- package/src/components/BoundingBoxHighlight/BoundingBoxHighlightList.scss +8 -0
- package/src/components/BoundingBoxHighlight/BoundingBoxHighlightList.tsx +67 -0
- package/src/components/BoundingBoxHighlight/BoundingBoxHighlightNav.scss +73 -0
- package/src/components/BoundingBoxHighlight/BoundingBoxHighlightNav.tsx +68 -0
- package/src/components/BoundingBoxHighlight/BoundingBoxHighlightRect.scss +22 -0
- package/src/components/BoundingBoxHighlight/BoundingBoxHighlightRect.tsx +77 -0
- package/src/components/BoundingBoxHighlight/index.ts +4 -0
- package/src/components/BoundingBoxHighlight/messages.ts +14 -0
- package/src/components/Popups/PopupDrawingToolbar.tsx +0 -7
- package/src/components/Popups/PopupReply.tsx +8 -28
- package/src/components/Popups/PopupV2.scss +28 -0
- package/src/components/Popups/PopupV2.tsx +382 -0
- package/src/components/Popups/messages.ts +10 -0
- package/src/document/DocumentAnnotator.ts +79 -10
- 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/drawing/DrawingTarget.tsx +1 -0
- 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/highlight/HighlightTarget.tsx +1 -0
- package/src/icons/ChevronLeft.tsx +24 -0
- package/src/icons/ChevronRight.tsx +24 -0
- package/src/image/ImageAnnotator.ts +50 -3
- package/src/media/MediaAnnotator.ts +2 -1
- package/src/popup/PopupContainer.tsx +11 -1
- package/src/popup/PopupLayer.tsx +97 -22
- package/src/popup/PopupManager.tsx +24 -4
- package/src/region/RegionAnnotation.tsx +1 -0
- 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 +167 -3
- package/src/store/annotations/reducer.ts +71 -1
- package/src/store/boundingBoxHighlights/actions.ts +6 -0
- package/src/store/boundingBoxHighlights/index.ts +4 -0
- package/src/store/boundingBoxHighlights/reducer.ts +33 -0
- package/src/store/boundingBoxHighlights/selectors.ts +12 -0
- package/src/store/boundingBoxHighlights/types.ts +13 -0
- package/src/store/createRootReducer.ts +2 -0
- package/src/store/eventing/boundingBoxHighlightNav.ts +13 -0
- package/src/store/eventing/create.ts +3 -3
- package/src/store/eventing/delete.ts +27 -0
- package/src/store/eventing/middleware.ts +42 -3
- 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/index.ts +1 -0
- package/src/store/options/actions.ts +2 -0
- package/src/store/options/reducer.ts +9 -2
- package/src/store/options/selectors.ts +5 -1
- package/src/store/options/types.ts +7 -1
- package/src/store/types.ts +2 -0
- package/src/utils/rotate.ts +43 -0
- package/src/utils/scroll.ts +8 -3
- package/src/components/Popups/PopupReplyV2.tsx +0 -10
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { createAction, createAsyncThunk } from '@reduxjs/toolkit';
|
|
2
|
-
import { Annotation, NewAnnotation } from '../../@types';
|
|
2
|
+
import { Annotation, NewAnnotation, Reply } from '../../@types';
|
|
3
3
|
import { APICollection } from '../../api';
|
|
4
4
|
import { AppThunkAPI } from '../types';
|
|
5
|
-
import {
|
|
5
|
+
import { getAnnotation } from './selectors';
|
|
6
|
+
import { getFileId, getFileVersionId, getPermissions, isFeatureEnabled } from '../options';
|
|
6
7
|
|
|
7
8
|
export const createAnnotationAction = createAsyncThunk<Annotation, NewAnnotation, AppThunkAPI>(
|
|
8
9
|
'CREATE_ANNOTATION',
|
|
@@ -39,13 +40,176 @@ export const fetchAnnotationsAction = createAsyncThunk<APICollection<Annotation>
|
|
|
39
40
|
client.destroy();
|
|
40
41
|
});
|
|
41
42
|
|
|
43
|
+
const shouldFetchReplies = isFeatureEnabled({ options: getState().options }, 'isThreadedAnnotation');
|
|
44
|
+
|
|
42
45
|
// Wrap the client request in a promise to allow it to be returned and cancelled
|
|
43
46
|
return new Promise<APICollection<Annotation>>((resolve, reject) => {
|
|
44
|
-
client.getAnnotations(fileId, fileVersionId, permissions, resolve, reject, 1000, false);
|
|
47
|
+
client.getAnnotations(fileId, fileVersionId, permissions, resolve, reject, 1000, false, shouldFetchReplies);
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
export const createReplyAction = createAsyncThunk<
|
|
53
|
+
{ annotationId: string; reply: Reply },
|
|
54
|
+
{ annotationId: string; message: string },
|
|
55
|
+
AppThunkAPI
|
|
56
|
+
>(
|
|
57
|
+
'CREATE_REPLY',
|
|
58
|
+
async ({ annotationId, message }, { extra, getState, signal }) => {
|
|
59
|
+
const client = extra.api.getAnnotationsAPI();
|
|
60
|
+
const state = getState();
|
|
61
|
+
const fileId = getFileId(state);
|
|
62
|
+
const filePermissions = getPermissions(state);
|
|
63
|
+
const annotation = getAnnotation(state, annotationId);
|
|
64
|
+
const permissions = { ...filePermissions, ...annotation?.permissions };
|
|
65
|
+
|
|
66
|
+
signal.addEventListener('abort', () => {
|
|
67
|
+
client.destroy();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const reply = await new Promise<Reply>((resolve, reject) => {
|
|
71
|
+
client.createAnnotationReply(fileId, annotationId, permissions, message, resolve, reject);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return { annotationId, reply };
|
|
75
|
+
},
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
export const deleteAnnotationAction = createAsyncThunk<string, string, AppThunkAPI>(
|
|
79
|
+
'DELETE_ANNOTATION',
|
|
80
|
+
async (annotationId, { extra, getState, signal }) => {
|
|
81
|
+
const client = extra.api.getAnnotationsAPI();
|
|
82
|
+
const state = getState();
|
|
83
|
+
const fileId = getFileId(state);
|
|
84
|
+
const filePermissions = getPermissions(state);
|
|
85
|
+
const annotation = getAnnotation(state, annotationId);
|
|
86
|
+
const permissions = { ...filePermissions, ...annotation?.permissions };
|
|
87
|
+
|
|
88
|
+
signal.addEventListener('abort', () => {
|
|
89
|
+
client.destroy();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
await new Promise<void>((resolve, reject) => {
|
|
93
|
+
client.deleteAnnotation(fileId, annotationId, permissions, resolve, reject);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return annotationId;
|
|
97
|
+
},
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
export const updateAnnotationAction = createAsyncThunk<
|
|
101
|
+
Annotation,
|
|
102
|
+
{ annotationId: string; payload: { message?: string; status?: string } },
|
|
103
|
+
AppThunkAPI
|
|
104
|
+
>(
|
|
105
|
+
'UPDATE_ANNOTATION',
|
|
106
|
+
async ({ annotationId, payload }, { extra, getState, signal }) => {
|
|
107
|
+
const client = extra.api.getAnnotationsAPI();
|
|
108
|
+
const state = getState();
|
|
109
|
+
const fileId = getFileId(state);
|
|
110
|
+
const filePermissions = getPermissions(state);
|
|
111
|
+
const existingAnnotation = getAnnotation(state, annotationId);
|
|
112
|
+
const permissions = { ...filePermissions, ...existingAnnotation?.permissions };
|
|
113
|
+
|
|
114
|
+
signal.addEventListener('abort', () => {
|
|
115
|
+
client.destroy();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
return new Promise<Annotation>((resolve, reject) => {
|
|
119
|
+
client.updateAnnotation(fileId, annotationId, permissions, payload, resolve, reject);
|
|
120
|
+
});
|
|
121
|
+
},
|
|
122
|
+
);
|
|
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();
|
|
45
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 };
|
|
46
193
|
},
|
|
47
194
|
);
|
|
48
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
|
+
|
|
49
213
|
export const removeAnnotationAction = createAction<string>('REMOVE_ANNOTATION');
|
|
50
214
|
export const setActiveAnnotationIdAction = createAction<string | null>('SET_ACTIVE_ANNOTATION_ID');
|
|
51
215
|
export const setIsInitialized = createAction('SET_IS_INITIALIZED');
|
|
@@ -2,19 +2,34 @@ 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,
|
|
10
|
+
createReplyAction,
|
|
11
|
+
deleteAnnotationAction,
|
|
12
|
+
deleteReplyAction,
|
|
6
13
|
fetchAnnotationsAction,
|
|
7
14
|
removeAnnotationAction,
|
|
8
15
|
setActiveAnnotationIdAction,
|
|
9
16
|
setIsInitialized,
|
|
17
|
+
updateAnnotationAction,
|
|
18
|
+
updateReplyAction,
|
|
10
19
|
} from './actions';
|
|
20
|
+
import { setViewModeAction } from '../options/actions';
|
|
11
21
|
|
|
12
22
|
const activeAnnotationId = createReducer<AnnotationsState['activeId']>(null, builder =>
|
|
13
23
|
builder
|
|
14
24
|
/* Preview will set the active ID with a an event which will trigger the url change */
|
|
15
25
|
.addCase(createAnnotationAction.fulfilled, () => null)
|
|
26
|
+
.addCase(deleteAnnotationAction.fulfilled, (state, { payload: id }) => (state === id ? null : state))
|
|
16
27
|
.addCase(removeAnnotationAction, (state, { payload: id }) => (state === id ? null : state))
|
|
17
|
-
.addCase(setActiveAnnotationIdAction, (state, { payload: annotationId }) => annotationId)
|
|
28
|
+
.addCase(setActiveAnnotationIdAction, (state, { payload: annotationId }) => annotationId)
|
|
29
|
+
.addCase(setViewModeAction, (state, { payload }) => {
|
|
30
|
+
// Clear active annotation when switching to bounding boxes mode
|
|
31
|
+
return payload === 'boundingBoxes' ? null : state;
|
|
32
|
+
}),
|
|
18
33
|
);
|
|
19
34
|
|
|
20
35
|
const annotationsAllIds = createReducer<AnnotationsState['allIds']>([], builder =>
|
|
@@ -22,6 +37,7 @@ const annotationsAllIds = createReducer<AnnotationsState['allIds']>([], builder
|
|
|
22
37
|
.addCase(createAnnotationAction.fulfilled, (state, { payload: { id } }) => {
|
|
23
38
|
state.push(id);
|
|
24
39
|
})
|
|
40
|
+
.addCase(deleteAnnotationAction.fulfilled, (state, { payload: id }) => state.filter(annotationId => annotationId !== id))
|
|
25
41
|
.addCase(removeAnnotationAction, (state, { payload: id }) => state.filter(annotationId => annotationId !== id))
|
|
26
42
|
.addCase(fetchAnnotationsAction.fulfilled, (state, { payload }) => {
|
|
27
43
|
payload.entries.forEach(({ id }) => state.indexOf(id) === -1 && state.push(id));
|
|
@@ -33,9 +49,63 @@ const annotationsById = createReducer<AnnotationsState['byId']>({}, builder =>
|
|
|
33
49
|
.addCase(createAnnotationAction.fulfilled, (state, { payload }) => {
|
|
34
50
|
state[payload.id] = isDrawing(payload) ? formatDrawing(payload) : payload;
|
|
35
51
|
})
|
|
52
|
+
.addCase(createReplyAction.fulfilled, (state, { payload: { annotationId, reply } }) => {
|
|
53
|
+
const annotation = state[annotationId];
|
|
54
|
+
if (annotation) {
|
|
55
|
+
annotation.replies = [...(annotation.replies ?? []), reply];
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
.addCase(deleteAnnotationAction.fulfilled, (state, { payload: id }) => {
|
|
59
|
+
delete state[id];
|
|
60
|
+
})
|
|
36
61
|
.addCase(removeAnnotationAction, (state, { payload: id }) => {
|
|
37
62
|
delete state[id];
|
|
38
63
|
})
|
|
64
|
+
.addCase(updateAnnotationAction.fulfilled, (state, { payload }) => {
|
|
65
|
+
state[payload.id] = isDrawing(payload) ? formatDrawing(payload) : payload;
|
|
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
|
+
})
|
|
39
109
|
.addCase(fetchAnnotationsAction.fulfilled, (state, { payload }) => {
|
|
40
110
|
payload.entries.forEach(annotation => {
|
|
41
111
|
state[annotation.id] = isDrawing(annotation) ? formatDrawing(annotation) : annotation;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createAction } from '@reduxjs/toolkit';
|
|
2
|
+
import { BoundingBox } from './types';
|
|
3
|
+
|
|
4
|
+
export const setBoundingBoxHighlightsAction = createAction<BoundingBox[]>('SET_BOUNDING_BOX_HIGHLIGHTS');
|
|
5
|
+
export const setSelectedBoundingBoxHighlightAction = createAction<string | null>('SET_SELECTED_BOUNDING_BOX_HIGHLIGHT');
|
|
6
|
+
export const navigateBoundingBoxHighlightAction = createAction<string>('NAVIGATE_BOUNDING_BOX_HIGHLIGHT');
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { createReducer } from '@reduxjs/toolkit';
|
|
2
|
+
import { BoundingBoxHighlightsState } from './types';
|
|
3
|
+
import {
|
|
4
|
+
navigateBoundingBoxHighlightAction,
|
|
5
|
+
setBoundingBoxHighlightsAction,
|
|
6
|
+
setSelectedBoundingBoxHighlightAction,
|
|
7
|
+
} from './actions';
|
|
8
|
+
import { setViewModeAction } from '../options/actions';
|
|
9
|
+
|
|
10
|
+
const initialState: BoundingBoxHighlightsState = {
|
|
11
|
+
boundingBoxes: [],
|
|
12
|
+
selectedId: null,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default createReducer(initialState, builder => {
|
|
16
|
+
builder
|
|
17
|
+
.addCase(setBoundingBoxHighlightsAction, (state, { payload }) => {
|
|
18
|
+
state.boundingBoxes = payload;
|
|
19
|
+
state.selectedId = null;
|
|
20
|
+
})
|
|
21
|
+
.addCase(setSelectedBoundingBoxHighlightAction, (state, { payload }) => {
|
|
22
|
+
state.selectedId = payload;
|
|
23
|
+
})
|
|
24
|
+
.addCase(navigateBoundingBoxHighlightAction, (state, { payload }) => {
|
|
25
|
+
state.selectedId = payload;
|
|
26
|
+
})
|
|
27
|
+
.addCase(setViewModeAction, (state, { payload }) => {
|
|
28
|
+
// Clear selected bounding box when switching to annotations mode
|
|
29
|
+
if (payload === 'annotations') {
|
|
30
|
+
state.selectedId = null;
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { BoundingBox, BoundingBoxHighlightsState } from './types';
|
|
2
|
+
|
|
3
|
+
type State = { boundingBoxHighlights: BoundingBoxHighlightsState };
|
|
4
|
+
|
|
5
|
+
export const getBoundingBoxHighlights = ({ boundingBoxHighlights }: State): BoundingBox[] =>
|
|
6
|
+
boundingBoxHighlights.boundingBoxes;
|
|
7
|
+
|
|
8
|
+
export const getBoundingBoxHighlightsForPage = ({ boundingBoxHighlights }: State, pageNumber: number): BoundingBox[] =>
|
|
9
|
+
boundingBoxHighlights.boundingBoxes.filter(box => box.pageNumber === pageNumber);
|
|
10
|
+
|
|
11
|
+
export const getSelectedBoundingBoxHighlightId = ({ boundingBoxHighlights }: State): string | null =>
|
|
12
|
+
boundingBoxHighlights.selectedId;
|
|
@@ -4,6 +4,7 @@ import { commonReducer } from './common';
|
|
|
4
4
|
import { creatorReducer } from './creator';
|
|
5
5
|
import { drawingReducer } from './drawing';
|
|
6
6
|
import { highlightReducer } from './highlight';
|
|
7
|
+
import { boundingBoxHighlightsReducer } from './boundingBoxHighlights';
|
|
7
8
|
import { optionsReducer } from './options';
|
|
8
9
|
import { usersReducer } from './users';
|
|
9
10
|
|
|
@@ -14,6 +15,7 @@ const createRootReducer = (): Reducer =>
|
|
|
14
15
|
creator: creatorReducer,
|
|
15
16
|
drawing: drawingReducer,
|
|
16
17
|
highlight: highlightReducer,
|
|
18
|
+
boundingBoxHighlights: boundingBoxHighlightsReducer,
|
|
17
19
|
options: optionsReducer,
|
|
18
20
|
users: usersReducer,
|
|
19
21
|
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import eventManager from '../../common/EventManager';
|
|
2
|
+
import { AppState } from '../types';
|
|
3
|
+
import { Event } from '../../@types';
|
|
4
|
+
import { getSelectedBoundingBoxHighlightId } from '../boundingBoxHighlights';
|
|
5
|
+
|
|
6
|
+
const handleNavigateBoundingBoxHighlight = (_prevState: AppState, nextState: AppState): void => {
|
|
7
|
+
const highlightId = getSelectedBoundingBoxHighlightId(nextState);
|
|
8
|
+
if (highlightId) {
|
|
9
|
+
eventManager.emit(Event.BOUNDING_BOX_HIGHLIGHT_NAVIGATE, highlightId);
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export { handleNavigateBoundingBoxHighlight };
|
|
@@ -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,33 +2,72 @@ 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';
|
|
14
|
+
import { navigateBoundingBoxHighlightAction } from '../boundingBoxHighlights/actions';
|
|
9
15
|
import { EventHandlerMap } from './types';
|
|
10
16
|
import { handleActiveAnnotationEvents } from './active';
|
|
11
17
|
import { handleAnnotationsInitialized } from './init';
|
|
18
|
+
import { handleNavigateBoundingBoxHighlight } from './boundingBoxHighlightNav';
|
|
12
19
|
import { handleCreateErrorEvents, handleCreatePendingEvents, handleCreateSuccessEvents } from './create';
|
|
20
|
+
import { handleDeleteErrorEvents, handleDeletePendingEvents, handleDeleteSuccessEvents } from './delete';
|
|
13
21
|
import { handleFetchErrorEvents } from './fetch';
|
|
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';
|
|
14
38
|
import { handleResetCreatorAction, handleSetStagedAction } from './staged';
|
|
15
39
|
import { handleSetStatusAction } from './status';
|
|
16
|
-
import {
|
|
40
|
+
import { handleUpdateErrorEvents, handleUpdatePendingEvents, handleUpdateSuccessEvents } from './update';
|
|
17
41
|
import { resetCreatorAction, setStagedAction, setStatusAction } from '../creator';
|
|
18
42
|
import { toggleAnnotationModeAction } from '../common/actions';
|
|
19
43
|
|
|
20
|
-
|
|
21
|
-
const eventHandlers: EventHandlerMap = {
|
|
44
|
+
export const eventHandlers: EventHandlerMap = {
|
|
22
45
|
[createAnnotationAction.fulfilled.toString()]: handleCreateSuccessEvents,
|
|
23
46
|
[createAnnotationAction.pending.toString()]: handleCreatePendingEvents,
|
|
24
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,
|
|
25
57
|
[fetchAnnotationsAction.rejected.toString()]: handleFetchErrorEvents,
|
|
58
|
+
[navigateBoundingBoxHighlightAction.toString()]: handleNavigateBoundingBoxHighlight,
|
|
26
59
|
[resetCreatorAction.toString()]: handleResetCreatorAction,
|
|
27
60
|
[setActiveAnnotationIdAction.toString()]: handleActiveAnnotationEvents,
|
|
28
61
|
[setIsInitialized.toString()]: handleAnnotationsInitialized,
|
|
29
62
|
[setStagedAction.toString()]: handleSetStagedAction,
|
|
30
63
|
[setStatusAction.toString()]: handleSetStatusAction,
|
|
31
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,
|
|
32
71
|
};
|
|
33
72
|
|
|
34
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 };
|
|
@@ -0,0 +1,39 @@
|
|
|
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 ReplyUpdateArg = {
|
|
7
|
+
annotationId: string;
|
|
8
|
+
replyId: string;
|
|
9
|
+
payload: { message?: string; status?: string };
|
|
10
|
+
};
|
|
11
|
+
type ReplyUpdatePayload = { annotationId: string; reply: Reply };
|
|
12
|
+
|
|
13
|
+
const emitReplyUpdateEvent = (
|
|
14
|
+
action: AsyncAction<ReplyUpdateArg, ReplyUpdatePayload>,
|
|
15
|
+
status: Status,
|
|
16
|
+
): void => {
|
|
17
|
+
const { error, meta: { arg, requestId } = {}, payload } = action;
|
|
18
|
+
if (status === Status.SUCCESS && !payload?.reply) return;
|
|
19
|
+
const annotationId = payload?.annotationId ?? arg?.annotationId;
|
|
20
|
+
const annotationReply = payload?.reply ?? (arg ? { id: arg.replyId } : undefined);
|
|
21
|
+
eventManager.emit(Event.ANNOTATION_REPLY_UPDATE, {
|
|
22
|
+
annotation: annotationId ? { id: annotationId } : undefined,
|
|
23
|
+
annotationReply,
|
|
24
|
+
error,
|
|
25
|
+
meta: { requestId, status },
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const replyUpdateHandler = (status: Status) => (
|
|
30
|
+
_prev: AppState,
|
|
31
|
+
_next: AppState,
|
|
32
|
+
action: AsyncAction,
|
|
33
|
+
): void => emitReplyUpdateEvent(action as AsyncAction<ReplyUpdateArg, ReplyUpdatePayload>, status);
|
|
34
|
+
|
|
35
|
+
const handleReplyUpdateErrorEvents = replyUpdateHandler(Status.ERROR);
|
|
36
|
+
const handleReplyUpdatePendingEvents = replyUpdateHandler(Status.PENDING);
|
|
37
|
+
const handleReplyUpdateSuccessEvents = replyUpdateHandler(Status.SUCCESS);
|
|
38
|
+
|
|
39
|
+
export { handleReplyUpdateErrorEvents, handleReplyUpdatePendingEvents, handleReplyUpdateSuccessEvents };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Action, SerializedError } from '@reduxjs/toolkit';
|
|
2
2
|
import { AppState } from '../types';
|
|
3
|
-
import { Annotation
|
|
3
|
+
import { Annotation } from '../../@types';
|
|
4
4
|
|
|
5
5
|
export enum Status {
|
|
6
6
|
ERROR = 'error',
|
|
@@ -22,10 +22,10 @@ export interface ThunkMeta<M> {
|
|
|
22
22
|
requestId: string;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
export interface AsyncAction extends Action {
|
|
25
|
+
export interface AsyncAction<TArg = unknown, TPayload = unknown> extends Action {
|
|
26
26
|
error?: SerializedError;
|
|
27
|
-
meta?: ThunkMeta<
|
|
28
|
-
payload?:
|
|
27
|
+
meta?: ThunkMeta<TArg>;
|
|
28
|
+
payload?: TPayload;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
export type EventHandler = (prevState: AppState, nextState: AppState, action: Action | AsyncAction) => void;
|