box-annotations 5.2.1-beta.13 → 5.2.1-beta.17

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.
Files changed (34) hide show
  1. package/dist/annotations.css +1 -1
  2. package/dist/annotations.js +1 -1
  3. package/package.json +13 -13
  4. package/src/@types/api.ts +3 -2
  5. package/src/@types/events.ts +27 -10
  6. package/src/@types/model.ts +1 -0
  7. package/src/adapters/threadedAnnotationsAdapters.ts +17 -1
  8. package/src/api/APIFactory.ts +6 -1
  9. package/src/api/types.ts +23 -1
  10. package/src/common/BaseAnnotator.scss +2 -1
  11. package/src/common/BaseAnnotator.ts +27 -2
  12. package/src/components/Popups/PopupV2.tsx +46 -10
  13. package/src/document/DocumentAnnotator.ts +1 -1
  14. package/src/document/docUtil.ts +2 -1
  15. package/src/highlight/HighlightAnnotations.tsx +27 -14
  16. package/src/highlight/HighlightContainer.tsx +3 -0
  17. package/src/highlight/HighlightCreatorManager.ts +8 -5
  18. package/src/highlight/HighlightListener.ts +4 -2
  19. package/src/highlight/HighlightManager.tsx +13 -2
  20. package/src/popup/PopupLayer.tsx +32 -4
  21. package/src/store/annotations/actions.ts +89 -0
  22. package/src/store/annotations/reducer.ts +48 -0
  23. package/src/store/eventing/create.ts +3 -3
  24. package/src/store/eventing/delete.ts +27 -0
  25. package/src/store/eventing/middleware.ts +40 -4
  26. package/src/store/eventing/replyCreate.ts +34 -0
  27. package/src/store/eventing/replyDelete.ts +35 -0
  28. package/src/store/eventing/replyUpdate.ts +39 -0
  29. package/src/store/eventing/types.ts +4 -4
  30. package/src/store/eventing/update.ts +29 -0
  31. package/src/store/highlight/actions.ts +32 -3
  32. package/src/store/highlight/types.ts +1 -0
  33. package/src/store/options/selectors.ts +2 -2
  34. package/src/store/options/types.ts +2 -2
@@ -45,6 +45,8 @@ const modeStagedMap: { [M in Mode]?: (staged: CreatorItem | null) => boolean } =
45
45
  [Mode.REGION]: isCreatorStagedRegion,
46
46
  };
47
47
 
48
+ const ACTIVE_TARGET_OBSERVER_TIMEOUT_MS = 10000;
49
+
48
50
  const PopupLayer = (props: Props): JSX.Element | null => {
49
51
  const {
50
52
  activeAnnotationId,
@@ -98,13 +100,39 @@ const PopupLayer = (props: Props): JSX.Element | null => {
98
100
  setReference(referenceId ? document.querySelector(`[data-ba-reference-id="${referenceId}"]`) : null);
99
101
  }, [referenceId]);
100
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.
101
105
  React.useEffect(() => {
102
- if (activeAnnotationId && isThreadedAnnotation) {
103
- const el = document.querySelector(`[data-ba-annotation-id="${CSS.escape(activeAnnotationId)}"]`);
104
- setActiveReference(el);
105
- } else {
106
+ if (!activeAnnotationId || !isThreadedAnnotation) {
106
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;
107
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
+ };
108
136
  }, [activeAnnotationId, isThreadedAnnotation]);
109
137
 
110
138
  const showCreator = canCreate && canReply && reference && staged;
@@ -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 { handleNavigateBoundingBoxHighlight } from './boundingBoxHighlightNav';
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 { handleToggleAnnotationModeAction } from './mode';
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
- // Array of event handlers based on redux action. To add handling for new events add an entry keyed by action
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 };
@@ -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, NewAnnotation } from '../../@types';
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<NewAnnotation>;
28
- payload?: Annotation;
27
+ meta?: ThunkMeta<TArg>;
28
+ payload?: TPayload;
29
29
  }
30
30
 
31
31
  export type EventHandler = (prevState: AppState, nextState: AppState, action: Action | AsyncAction) => void;
@@ -0,0 +1,29 @@
1
+ import eventManager from '../../common/EventManager';
2
+ import { Annotation , Event } from '../../@types';
3
+ import { AppState } from '../types';
4
+ import { AsyncAction, Status } from './types';
5
+
6
+ type UpdateArg = { annotationId: string; payload: { message?: string; status?: string } };
7
+
8
+ const emitUpdateEvent = (action: AsyncAction<UpdateArg, Annotation>, status: Status): void => {
9
+ const { error, meta: { arg, requestId } = {}, payload } = action;
10
+ if (status === Status.SUCCESS && !payload) return;
11
+ const annotation = payload ?? (arg ? { id: arg.annotationId } : undefined);
12
+ eventManager.emit(Event.ANNOTATION_UPDATE, {
13
+ annotation,
14
+ error,
15
+ meta: { requestId, status },
16
+ });
17
+ };
18
+
19
+ const updateHandler = (status: Status) => (
20
+ _prev: AppState,
21
+ _next: AppState,
22
+ action: AsyncAction,
23
+ ): void => emitUpdateEvent(action as AsyncAction<UpdateArg, Annotation>, status);
24
+
25
+ const handleUpdateErrorEvents = updateHandler(Status.ERROR);
26
+ const handleUpdatePendingEvents = updateHandler(Status.PENDING);
27
+ const handleUpdateSuccessEvents = updateHandler(Status.SUCCESS);
28
+
29
+ export { handleUpdateErrorEvents, handleUpdatePendingEvents, handleUpdateSuccessEvents };
@@ -1,13 +1,16 @@
1
1
  import { createAction } from '@reduxjs/toolkit';
2
- import { combineRects } from '../../highlight/highlightUtil';
2
+ import { combineRects, getBoundingRect } from '../../highlight/highlightUtil';
3
+ import { getElementLocalPosition } from '../../utils/rotate';
3
4
  import { SelectionItem } from './types';
4
5
  import { Shape } from '../../@types';
5
6
 
6
7
  export type SelectionArg = {
8
+ containerEl?: HTMLElement | null;
7
9
  containerRect: DOMRect;
8
10
  hasError?: boolean;
9
11
  location: number;
10
12
  range: Range;
13
+ rotation?: number;
11
14
  };
12
15
 
13
16
  type Payload = {
@@ -83,7 +86,7 @@ export const setSelectionAction = createAction(
83
86
  };
84
87
  }
85
88
 
86
- const { containerRect, hasError, location, range } = arg;
89
+ const { containerEl, containerRect, hasError, location, range, rotation } = arg;
87
90
 
88
91
  let rects = Array.from(range.getClientRects());
89
92
  // getClientRects on IE/Edge might return 0 rects
@@ -91,12 +94,38 @@ export const setSelectionAction = createAction(
91
94
  rects = getClientRects(range);
92
95
  }
93
96
 
97
+ const screenShapes = rects.map(getShape);
98
+
99
+ if (rotation && containerEl) {
100
+ // Convert screen rects to element-local coordinates
101
+ const localRects = screenShapes.map(shape => {
102
+ const [lx1, ly1] = getElementLocalPosition(shape.x, shape.y, containerEl, rotation);
103
+ const [lx2, ly2] = getElementLocalPosition(shape.x + shape.width, shape.y + shape.height, containerEl, rotation);
104
+ return {
105
+ x: Math.min(lx1, lx2),
106
+ y: Math.min(ly1, ly2),
107
+ width: Math.abs(lx2 - lx1),
108
+ height: Math.abs(ly2 - ly1),
109
+ };
110
+ });
111
+
112
+ return {
113
+ payload: {
114
+ containerRect: { x: 0, y: 0, width: containerEl.offsetWidth, height: containerEl.offsetHeight },
115
+ hasError,
116
+ location,
117
+ popupRect: getBoundingRect(screenShapes),
118
+ rects: combineRects(localRects),
119
+ },
120
+ };
121
+ }
122
+
94
123
  return {
95
124
  payload: {
96
125
  containerRect: getShape(containerRect),
97
126
  hasError,
98
127
  location,
99
- rects: combineRects(rects.map(getShape)),
128
+ rects: combineRects(screenShapes),
100
129
  },
101
130
  };
102
131
  },
@@ -10,5 +10,6 @@ export type SelectionItem = {
10
10
  containerRect: Shape;
11
11
  hasError?: boolean;
12
12
  location: number;
13
+ popupRect?: Shape;
13
14
  rects: Array<Shape>;
14
15
  };
@@ -1,6 +1,6 @@
1
1
  import getProp from 'lodash/get';
2
2
  import { AppState } from '../types';
3
- import { Permissions } from '../../@types';
3
+ import { Permissions, Token } from '../../@types';
4
4
  import { Features } from '../../BoxAnnotations';
5
5
  import { ViewMode } from './types';
6
6
 
@@ -15,6 +15,6 @@ export const getIsCurrentFileVersion = (state: State): boolean => state.options.
15
15
  export const getPermissions = (state: State): Permissions => state.options.permissions;
16
16
  export const getRotation = (state: State): number => state.options.rotation;
17
17
  export const getScale = (state: State): number => state.options.scale;
18
- export const getToken = (state: State): string => state.options.token;
18
+ export const getToken = (state: State): Token => state.options.token;
19
19
  export const isFeatureEnabled = (state: State, featurename: string): boolean =>
20
20
  getProp(getFeatures(state), featurename, false);
@@ -1,5 +1,5 @@
1
1
  import { Features } from '../../BoxAnnotations';
2
- import { Permissions } from '../../@types';
2
+ import { Permissions, Token } from '../../@types';
3
3
 
4
4
  /** View mode: annotations and bounding boxes are mutually exclusive. */
5
5
  export type ViewMode = 'annotations' | 'boundingBoxes';
@@ -13,6 +13,6 @@ export type OptionsState = {
13
13
  permissions: Permissions;
14
14
  rotation: number;
15
15
  scale: number;
16
- token: string;
16
+ token: Token;
17
17
  viewMode: ViewMode;
18
18
  };