box-annotations 5.2.1-beta.8 → 5.3.1-beta.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.
Files changed (121) hide show
  1. package/dist/annotations.css +1 -1
  2. package/dist/annotations.js +1 -1
  3. package/dist/annotations.js.LICENSE.txt +1 -10
  4. package/i18n/bn-IN.js +6 -0
  5. package/i18n/bn-IN.properties +12 -0
  6. package/i18n/da-DK.js +6 -0
  7. package/i18n/da-DK.properties +12 -0
  8. package/i18n/de-DE.js +6 -0
  9. package/i18n/de-DE.properties +12 -0
  10. package/i18n/en-AU.js +6 -0
  11. package/i18n/en-AU.properties +12 -0
  12. package/i18n/en-CA.js +6 -0
  13. package/i18n/en-CA.properties +12 -0
  14. package/i18n/en-GB.js +6 -0
  15. package/i18n/en-GB.properties +12 -0
  16. package/i18n/es-419.js +6 -0
  17. package/i18n/es-419.properties +12 -0
  18. package/i18n/es-ES.js +6 -0
  19. package/i18n/es-ES.properties +12 -0
  20. package/i18n/fi-FI.js +6 -0
  21. package/i18n/fi-FI.properties +12 -0
  22. package/i18n/fr-CA.js +6 -0
  23. package/i18n/fr-CA.properties +12 -0
  24. package/i18n/fr-FR.js +6 -0
  25. package/i18n/fr-FR.properties +12 -0
  26. package/i18n/hi-IN.js +6 -0
  27. package/i18n/hi-IN.properties +12 -0
  28. package/i18n/it-IT.js +6 -0
  29. package/i18n/it-IT.properties +12 -0
  30. package/i18n/ja-JP.js +7 -1
  31. package/i18n/ja-JP.properties +13 -1
  32. package/i18n/ko-KR.js +6 -0
  33. package/i18n/ko-KR.properties +12 -0
  34. package/i18n/nb-NO.js +6 -0
  35. package/i18n/nb-NO.properties +12 -0
  36. package/i18n/nl-NL.js +6 -0
  37. package/i18n/nl-NL.properties +12 -0
  38. package/i18n/pl-PL.js +6 -0
  39. package/i18n/pl-PL.properties +12 -0
  40. package/i18n/pt-BR.js +6 -0
  41. package/i18n/pt-BR.properties +12 -0
  42. package/i18n/ru-RU.js +6 -0
  43. package/i18n/ru-RU.properties +12 -0
  44. package/i18n/sv-SE.js +6 -0
  45. package/i18n/sv-SE.properties +12 -0
  46. package/i18n/tr-TR.js +6 -0
  47. package/i18n/tr-TR.properties +12 -0
  48. package/i18n/zh-CN.js +6 -0
  49. package/i18n/zh-CN.properties +12 -0
  50. package/i18n/zh-TW.js +6 -0
  51. package/i18n/zh-TW.properties +12 -0
  52. package/index.js +1 -1
  53. package/package.json +54 -42
  54. package/src/@types/api.ts +3 -2
  55. package/src/@types/events.ts +27 -10
  56. package/src/@types/global.ts +0 -9
  57. package/src/@types/model.ts +9 -0
  58. package/src/BoxAnnotations.ts +6 -2
  59. package/src/adapters/threadedAnnotationsAdapters.ts +49 -28
  60. package/src/api/APIFactory.ts +6 -1
  61. package/src/api/types.ts +23 -1
  62. package/src/boundingBoxHighlight/BoundingBoxHighlightContainer.tsx +4 -1
  63. package/src/common/AnnotationCallbacksContext.ts +7 -0
  64. package/src/common/BaseAnnotator.scss +12 -0
  65. package/src/common/BaseAnnotator.ts +32 -2
  66. package/src/common/BaseManager.ts +2 -0
  67. package/src/common/DeselectListener.tsx +1 -1
  68. package/src/common/withProviders.tsx +6 -2
  69. package/src/components/BoundingBoxHighlight/BoundingBoxHighlightNav.scss +3 -3
  70. package/src/components/BoundingBoxHighlight/BoundingBoxHighlightRect.scss +11 -6
  71. package/src/components/Popups/PopupBase.scss +2 -2
  72. package/src/components/Popups/PopupDrawingToolbar.scss +4 -4
  73. package/src/components/Popups/PopupDrawingToolbar.tsx +0 -7
  74. package/src/components/Popups/PopupList.scss +1 -1
  75. package/src/components/Popups/PopupV2.scss +2 -2
  76. package/src/components/Popups/PopupV2.tsx +128 -72
  77. package/src/components/ReplyForm/ReplyForm.tsx +1 -8
  78. package/src/constants.ts +2 -3
  79. package/src/document/DocumentAnnotator.ts +29 -5
  80. package/src/document/docUtil.ts +8 -1
  81. package/src/drawing/DrawingAnnotations.tsx +30 -21
  82. package/src/drawing/DrawingAnnotationsContainer.tsx +3 -0
  83. package/src/drawing/DrawingCreator.tsx +9 -16
  84. package/src/drawing/DrawingCursor.ts +2 -1
  85. package/src/drawing/DrawingManager.tsx +21 -2
  86. package/src/highlight/HighlightAnnotations.tsx +28 -14
  87. package/src/highlight/HighlightContainer.tsx +3 -0
  88. package/src/highlight/HighlightCreator.scss +9 -2
  89. package/src/highlight/HighlightCreatorManager.ts +8 -5
  90. package/src/highlight/HighlightListener.ts +4 -2
  91. package/src/highlight/HighlightManager.tsx +13 -2
  92. package/src/image/ImageAnnotator.ts +2 -1
  93. package/src/media/MediaAnnotator.ts +39 -16
  94. package/src/popup/PopupLayer.tsx +38 -11
  95. package/src/popup/PopupManager.tsx +4 -2
  96. package/src/region/RegionAnnotations.tsx +2 -3
  97. package/src/region/RegionCreation.tsx +15 -9
  98. package/src/region/RegionCreationContainer.tsx +2 -2
  99. package/src/region/RegionCreationManager.tsx +8 -1
  100. package/src/region/RegionCreator.scss +9 -3
  101. package/src/region/RegionCreator.tsx +9 -17
  102. package/src/region/RegionManager.tsx +8 -1
  103. package/src/region/RegionRect.scss +17 -9
  104. package/src/store/annotations/actions.ts +104 -33
  105. package/src/store/annotations/reducer.ts +49 -1
  106. package/src/store/creator/selectors.ts +6 -6
  107. package/src/store/eventing/create.ts +6 -4
  108. package/src/store/eventing/delete.ts +26 -0
  109. package/src/store/eventing/middleware.ts +53 -15
  110. package/src/store/eventing/replyCreate.ts +30 -0
  111. package/src/store/eventing/replyDelete.ts +31 -0
  112. package/src/store/eventing/replyUpdate.ts +35 -0
  113. package/src/store/eventing/types.ts +4 -4
  114. package/src/store/eventing/update.ts +28 -0
  115. package/src/store/highlight/actions.ts +50 -19
  116. package/src/store/highlight/types.ts +1 -0
  117. package/src/store/options/selectors.ts +2 -2
  118. package/src/store/options/types.ts +2 -2
  119. package/src/test-utils/videoAnnotations.tsx +11 -11
  120. package/src/utils/rotate.ts +43 -0
  121. package/src/utils/useVideoTiming.ts +9 -13
@@ -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
- const { height, width } = creatorEl.getBoundingClientRect();
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] = getPosition(x, y);
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] = getPosition(x, y);
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
@@ -14,6 +14,13 @@ export default class RegionListManager extends BaseManager {
14
14
  this.root = ReactDOM.createRoot(this.reactEl);
15
15
  }
16
16
 
17
- this.root.render(<RegionAnnotationsContainer location={this.location} referenceEl={this.referenceEl} targetType={this.targetType} {...props} />);
17
+ this.root.render(
18
+ <RegionAnnotationsContainer
19
+ location={this.location}
20
+ referenceEl={this.referenceEl}
21
+ targetType={this.targetType}
22
+ {...props}
23
+ />,
24
+ );
18
25
  }
19
26
  }
@@ -2,7 +2,7 @@
2
2
 
3
3
  $ba-RegionRect-color: $bdl-yellorange;
4
4
  $ba-RegionRect-bgColor--default: rgba($ba-RegionRect-color, 0); // Default color uses zero alpha value for transition
5
- $ba-RegionRect-bgColor--hover: rgba($ba-RegionRect-color, .1);
5
+ $ba-RegionRect-bgColor--hover: rgba($ba-RegionRect-color, 0.1);
6
6
  $ba-RegionRect-bgColor--active: rgba($ba-RegionRect-color, 0);
7
7
  $ba-RegionRect-transitionDelay: 25ms;
8
8
  $ba-RegionRect-transitionDuration: 200ms;
@@ -21,29 +21,37 @@ $ba-RegionRect-transitionDuration: 200ms;
21
21
 
22
22
  &::after {
23
23
  position: absolute;
24
- top: $border-size-outer;
25
- right: $border-size-outer;
26
- bottom: $border-size-outer;
27
- left: $border-size-outer;
24
+ inset: $border-size-outer;
28
25
  display: block;
29
26
  background-color: $ba-RegionRect-bgColor--default;
30
27
  border-radius: $border-radius;
31
- box-shadow: 0 0 0 $border-size-inner rgba($white, 1), 0 0 0 $border-size-outer rgba($ba-RegionRect-color, .6), 0 0 0 0 rgba(0, 0, 0, 0);
32
- transition: background-color $ba-RegionRect-transitionDuration ease, box-shadow $ba-RegionRect-transitionDuration ease;
28
+ box-shadow:
29
+ 0 0 0 $border-size-inner rgba($white, 1),
30
+ 0 0 0 $border-size-outer rgba($ba-RegionRect-color, 0.6),
31
+ 0 0 0 0 rgb(0 0 0 / 0%);
32
+ transition:
33
+ background-color $ba-RegionRect-transitionDuration ease,
34
+ box-shadow $ba-RegionRect-transitionDuration ease;
33
35
  content: '';
34
36
  }
35
37
 
36
38
  &:hover {
37
39
  &::after {
38
40
  background-color: $ba-RegionRect-bgColor--hover;
39
- box-shadow: 0 0 0 $border-size-inner rgba($white, 1), 0 0 0 $border-size-outer rgba($ba-RegionRect-color, 1), 0 8px 16px 0 rgba(0, 0, 0, .1);
41
+ box-shadow:
42
+ 0 0 0 $border-size-inner rgba($white, 1),
43
+ 0 0 0 $border-size-outer rgba($ba-RegionRect-color, 1),
44
+ 0 8px 16px 0 rgb(0 0 0 / 10%);
40
45
  }
41
46
  }
42
47
 
43
48
  &.is-active {
44
49
  &::after {
45
50
  background-color: $ba-RegionRect-bgColor--active;
46
- box-shadow: 0 0 0 $border-size-inner rgba($ba-RegionRect-color, 1), 0 0 0 $border-size-outer rgba($ba-RegionRect-color, 1), 0 8px 52px 0 rgba(0, 0, 0, .2);
51
+ box-shadow:
52
+ 0 0 0 $border-size-inner rgba($ba-RegionRect-color, 1),
53
+ 0 0 0 $border-size-outer rgba($ba-RegionRect-color, 1),
54
+ 0 8px 52px 0 rgb(0 0 0 / 20%);
47
55
  transition-delay: $ba-RegionRect-transitionDelay;
48
56
  }
49
57
  }
@@ -53,27 +53,24 @@ export const createReplyAction = createAsyncThunk<
53
53
  { annotationId: string; reply: Reply },
54
54
  { annotationId: string; message: string },
55
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 };
56
+ >('CREATE_REPLY', async ({ annotationId, message }, { extra, getState, signal }) => {
57
+ const client = extra.api.getAnnotationsAPI();
58
+ const state = getState();
59
+ const fileId = getFileId(state);
60
+ const filePermissions = getPermissions(state);
61
+ const annotation = getAnnotation(state, annotationId);
62
+ const permissions = { ...filePermissions, ...annotation?.permissions };
65
63
 
66
- signal.addEventListener('abort', () => {
67
- client.destroy();
68
- });
64
+ signal.addEventListener('abort', () => {
65
+ client.destroy();
66
+ });
69
67
 
70
- const reply = await new Promise<Reply>((resolve, reject) => {
71
- client.createAnnotationReply(fileId, annotationId, permissions, message, resolve, reject);
72
- });
68
+ const reply = await new Promise<Reply>((resolve, reject) => {
69
+ client.createAnnotationReply(fileId, annotationId, permissions, message, resolve, reject);
70
+ });
73
71
 
74
- return { annotationId, reply };
75
- },
76
- );
72
+ return { annotationId, reply };
73
+ });
77
74
 
78
75
  export const deleteAnnotationAction = createAsyncThunk<string, string, AppThunkAPI>(
79
76
  'DELETE_ANNOTATION',
@@ -101,25 +98,99 @@ export const updateAnnotationAction = createAsyncThunk<
101
98
  Annotation,
102
99
  { annotationId: string; payload: { message?: string; status?: string } },
103
100
  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();
101
+ >('UPDATE_ANNOTATION', async ({ annotationId, payload }, { extra, getState, signal }) => {
102
+ const client = extra.api.getAnnotationsAPI();
103
+ const state = getState();
104
+ const fileId = getFileId(state);
105
+ const filePermissions = getPermissions(state);
106
+ const existingAnnotation = getAnnotation(state, annotationId);
107
+ const permissions = { ...filePermissions, ...existingAnnotation?.permissions };
108
+
109
+ signal.addEventListener('abort', () => {
110
+ client.destroy();
111
+ });
112
+
113
+ return new Promise<Annotation>((resolve, reject) => {
114
+ client.updateAnnotation(fileId, annotationId, permissions, payload, resolve, reject);
115
+ });
116
+ });
117
+
118
+ export const updateReplyAction = createAsyncThunk<
119
+ { annotationId: string; reply: Reply },
120
+ { annotationId: string; replyId: string; payload: { message?: string; status?: string } },
121
+ AppThunkAPI
122
+ >('UPDATE_REPLY', async ({ annotationId, replyId, payload }, { extra, getState, signal }) => {
123
+ const client = extra.api.getThreadedCommentsAPI();
124
+ const state = getState();
125
+ const fileId = getFileId(state);
126
+ const annotation = getAnnotation(state, annotationId);
127
+ const reply = annotation?.replies?.find(r => r.id === replyId);
128
+
129
+ if (!reply) {
130
+ throw new Error(`updateReplyAction: reply ${replyId} not found on annotation ${annotationId}`);
131
+ }
132
+
133
+ signal.addEventListener('abort', () => {
134
+ client.destroy();
135
+ });
136
+
137
+ const updated = await new Promise<Reply>((resolve, reject) => {
138
+ client.updateComment({
139
+ commentId: replyId,
140
+ errorCallback: reject,
141
+ fileId,
142
+ message: payload.message,
143
+ permissions: reply.permissions ?? {},
144
+ status: payload.status,
145
+ successCallback: resolve,
116
146
  });
147
+ });
117
148
 
118
- return new Promise<Annotation>((resolve, reject) => {
119
- client.updateAnnotation(fileId, annotationId, permissions, payload, resolve, reject);
149
+ return { annotationId, reply: updated };
150
+ });
151
+
152
+ export const deleteReplyAction = createAsyncThunk<
153
+ { annotationId: string; replyId: string },
154
+ { annotationId: string; replyId: string },
155
+ AppThunkAPI
156
+ >('DELETE_REPLY', async ({ annotationId, replyId }, { extra, getState, signal }) => {
157
+ const client = extra.api.getThreadedCommentsAPI();
158
+ const state = getState();
159
+ const fileId = getFileId(state);
160
+ const annotation = getAnnotation(state, annotationId);
161
+ const reply = annotation?.replies?.find(r => r.id === replyId);
162
+
163
+ if (!reply) {
164
+ throw new Error(`deleteReplyAction: reply ${replyId} not found on annotation ${annotationId}`);
165
+ }
166
+
167
+ signal.addEventListener('abort', () => {
168
+ client.destroy();
169
+ });
170
+
171
+ await new Promise<void>((resolve, reject) => {
172
+ client.deleteComment({
173
+ commentId: replyId,
174
+ errorCallback: reject,
175
+ fileId,
176
+ permissions: reply.permissions ?? {},
177
+ successCallback: resolve,
120
178
  });
121
- },
179
+ });
180
+
181
+ return { annotationId, replyId };
182
+ });
183
+
184
+ export type SidebarAnnotationUpdatePayload = Partial<Annotation> & { id: string };
185
+ export type SidebarReplyMutationPayload = { annotationId: string; reply: Reply };
186
+ export type SidebarReplyDeletePayload = { annotationId: string; replyId: string };
187
+
188
+ export const applySidebarAnnotationUpdateAction = createAction<SidebarAnnotationUpdatePayload>(
189
+ 'APPLY_SIDEBAR_ANNOTATION_UPDATE',
122
190
  );
191
+ export const applySidebarReplyCreateAction = createAction<SidebarReplyMutationPayload>('APPLY_SIDEBAR_REPLY_CREATE');
192
+ export const applySidebarReplyDeleteAction = createAction<SidebarReplyDeletePayload>('APPLY_SIDEBAR_REPLY_DELETE');
193
+ export const applySidebarReplyUpdateAction = createAction<SidebarReplyMutationPayload>('APPLY_SIDEBAR_REPLY_UPDATE');
123
194
 
124
195
  export const removeAnnotationAction = createAction<string>('REMOVE_ANNOTATION');
125
196
  export const setActiveAnnotationIdAction = createAction<string | null>('SET_ACTIVE_ANNOTATION_ID');
@@ -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
 
@@ -31,7 +37,9 @@ const annotationsAllIds = createReducer<AnnotationsState['allIds']>([], builder
31
37
  .addCase(createAnnotationAction.fulfilled, (state, { payload: { id } }) => {
32
38
  state.push(id);
33
39
  })
34
- .addCase(deleteAnnotationAction.fulfilled, (state, { payload: id }) => state.filter(annotationId => annotationId !== id))
40
+ .addCase(deleteAnnotationAction.fulfilled, (state, { payload: id }) =>
41
+ state.filter(annotationId => annotationId !== id),
42
+ )
35
43
  .addCase(removeAnnotationAction, (state, { payload: id }) => state.filter(annotationId => annotationId !== id))
36
44
  .addCase(fetchAnnotationsAction.fulfilled, (state, { payload }) => {
37
45
  payload.entries.forEach(({ id }) => state.indexOf(id) === -1 && state.push(id));
@@ -58,6 +66,46 @@ const annotationsById = createReducer<AnnotationsState['byId']>({}, builder =>
58
66
  .addCase(updateAnnotationAction.fulfilled, (state, { payload }) => {
59
67
  state[payload.id] = isDrawing(payload) ? formatDrawing(payload) : payload;
60
68
  })
69
+ .addCase(updateReplyAction.fulfilled, (state, { payload: { annotationId, reply } }) => {
70
+ const annotation = state[annotationId];
71
+ if (annotation && annotation.replies) {
72
+ annotation.replies = annotation.replies.map(existing => (existing.id === reply.id ? reply : existing));
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;
@@ -11,13 +11,13 @@ export const getCreatorStaged = (state: State): CreatorItem | null => state.crea
11
11
  export const getCreatorStagedForLocation = (state: State, location: number): CreatorItem | null => {
12
12
  const staged = getCreatorStaged(state);
13
13
  // The location is typically a page and this selector was originally meant to only return the staged annotation
14
- // (those that have been drawn but not saved) that belong to the page specified by the location. For documents,
15
- // the annotator creates new sets of managers(highlights, drawings, regions) for each page with the current
16
- // page number being the location e.g. page 4 of 10. This selector prevents a staged annotation from staying on
17
- // the screen when a user navigates to a different page. By default, the location value for managers is 1 which is
14
+ // (those that have been drawn but not saved) that belong to the page specified by the location. For documents,
15
+ // the annotator creates new sets of managers(highlights, drawings, regions) for each page with the current
16
+ // page number being the location e.g. page 4 of 10. This selector prevents a staged annotation from staying on
17
+ // the screen when a user navigates to a different page. By default, the location value for managers is 1 which is
18
18
  // why this selector works for images. Video annotations will not work this way because they reference a timestamp, not a page,
19
- // and the drawing canvas is the video player which has no pages. In this case, we just need to get the staged annotation and do the
20
- // locaton check for MEDIA_LOCATION_INDEX. The MEDIA_LOCATION_INDEX is a special value, set in the manager, that is used to indicate that
19
+ // and the drawing canvas is the video player which has no pages. In this case, we just need to get the staged annotation and do the
20
+ // locaton check for MEDIA_LOCATION_INDEX. The MEDIA_LOCATION_INDEX is a special value, set in the manager, that is used to indicate that
21
21
  // the this is actually a staged video annotation with a timestamp as its location.
22
22
  if (location === MEDIA_LOCATION_INDEX) {
23
23
  return staged;
@@ -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,
@@ -15,8 +15,10 @@ const emitCreateEvent = (action: AsyncAction, status: Status): void => {
15
15
  });
16
16
  };
17
17
 
18
- const createHandler = (status: Status) => (prevState: AppState, nextState: AppState, action: AsyncAction): void =>
19
- emitCreateEvent(action, status);
18
+ const createHandler =
19
+ (status: Status) =>
20
+ (prevState: AppState, nextState: AppState, action: AsyncAction): void =>
21
+ emitCreateEvent(action as AsyncAction<NewAnnotation, Annotation>, status);
20
22
 
21
23
  const handleCreateErrorEvents = createHandler(Status.ERROR);
22
24
  const handleCreatePendingEvents = createHandler(Status.PENDING);
@@ -0,0 +1,26 @@
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 =
18
+ (status: Status) =>
19
+ (_prev: AppState, _next: AppState, action: AsyncAction): void =>
20
+ emitDeleteEvent(action as AsyncAction<string, string>, status);
21
+
22
+ const handleDeleteErrorEvents = deleteHandler(Status.ERROR);
23
+ const handleDeletePendingEvents = deleteHandler(Status.PENDING);
24
+ const handleDeleteSuccessEvents = deleteHandler(Status.SUCCESS);
25
+
26
+ 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,22 +62,30 @@ 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 {
38
- return (store: MiddlewareAPI) => (next: Dispatch) => (action: Action): Action => {
39
- const { type } = action;
40
- // Retrieve the prevState as well as the nextState after the action has modified the state in order
41
- // to pass all that information to the event handler
42
- const prevState = store.getState();
43
- const result = next(action);
44
- const nextState = store.getState();
74
+ return (store: MiddlewareAPI) =>
75
+ (next: Dispatch) =>
76
+ (action: Action): Action => {
77
+ const { type } = action;
78
+ // Retrieve the prevState as well as the nextState after the action has modified the state in order
79
+ // to pass all that information to the event handler
80
+ const prevState = store.getState();
81
+ const result = next(action);
82
+ const nextState = store.getState();
45
83
 
46
- const handler = handlers[type] || noop;
47
- handler(prevState, nextState, action);
84
+ const handler = handlers[type] || noop;
85
+ handler(prevState, nextState, action);
48
86
 
49
- return result;
50
- };
87
+ return result;
88
+ };
51
89
  }
52
90
 
53
91
  export default getEventingMiddleware;
@@ -0,0 +1,30 @@
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 = (action: AsyncAction<ReplyCreateArg, ReplyCreatePayload>, status: Status): void => {
10
+ const { error, meta: { arg, requestId } = {}, payload } = action;
11
+ if (status === Status.SUCCESS && !payload?.reply) return;
12
+ const annotationId = payload?.annotationId ?? arg?.annotationId;
13
+ eventManager.emit(Event.ANNOTATION_REPLY_CREATE, {
14
+ annotation: annotationId ? { id: annotationId } : undefined,
15
+ annotationReply: payload?.reply,
16
+ error,
17
+ meta: { requestId, status },
18
+ });
19
+ };
20
+
21
+ const replyCreateHandler =
22
+ (status: Status) =>
23
+ (_prev: AppState, _next: AppState, action: AsyncAction): void =>
24
+ emitReplyCreateEvent(action as AsyncAction<ReplyCreateArg, ReplyCreatePayload>, status);
25
+
26
+ const handleReplyCreateErrorEvents = replyCreateHandler(Status.ERROR);
27
+ const handleReplyCreatePendingEvents = replyCreateHandler(Status.PENDING);
28
+ const handleReplyCreateSuccessEvents = replyCreateHandler(Status.SUCCESS);
29
+
30
+ export { handleReplyCreateErrorEvents, handleReplyCreatePendingEvents, handleReplyCreateSuccessEvents };
@@ -0,0 +1,31 @@
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 = (action: AsyncAction<ReplyDeleteArg, ReplyDeletePayload>, status: Status): void => {
10
+ const { error, meta: { arg, requestId } = {}, payload } = action;
11
+ if (status === Status.SUCCESS && !payload) return;
12
+ const annotationId = payload?.annotationId ?? arg?.annotationId;
13
+ const replyId = payload?.replyId ?? arg?.replyId;
14
+ eventManager.emit(Event.ANNOTATION_REPLY_DELETE, {
15
+ annotation: annotationId ? { id: annotationId } : undefined,
16
+ annotationReply: replyId ? { id: replyId } : undefined,
17
+ error,
18
+ meta: { requestId, status },
19
+ });
20
+ };
21
+
22
+ const replyDeleteHandler =
23
+ (status: Status) =>
24
+ (_prev: AppState, _next: AppState, action: AsyncAction): void =>
25
+ emitReplyDeleteEvent(action as AsyncAction<ReplyDeleteArg, ReplyDeletePayload>, status);
26
+
27
+ const handleReplyDeleteErrorEvents = replyDeleteHandler(Status.ERROR);
28
+ const handleReplyDeletePendingEvents = replyDeleteHandler(Status.PENDING);
29
+ const handleReplyDeleteSuccessEvents = replyDeleteHandler(Status.SUCCESS);
30
+
31
+ export { handleReplyDeleteErrorEvents, handleReplyDeletePendingEvents, handleReplyDeleteSuccessEvents };