box-annotations 5.2.1-beta.7 → 5.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/annotations.css +1 -1
  2. package/dist/annotations.js +1 -1
  3. package/dist/annotations.js.LICENSE.txt +1 -1
  4. package/index.js +1 -1
  5. package/package.json +33 -22
  6. package/src/@types/api.ts +3 -2
  7. package/src/@types/events.ts +27 -10
  8. package/src/@types/global.ts +0 -9
  9. package/src/@types/model.ts +9 -0
  10. package/src/BoxAnnotations.ts +5 -1
  11. package/src/adapters/threadedAnnotationsAdapters.ts +39 -26
  12. package/src/api/APIFactory.ts +6 -1
  13. package/src/api/types.ts +23 -1
  14. package/src/common/AnnotationCallbacksContext.ts +7 -0
  15. package/src/common/BaseAnnotator.scss +14 -1
  16. package/src/common/BaseAnnotator.ts +46 -2
  17. package/src/common/BaseManager.ts +2 -0
  18. package/src/common/DeselectListener.tsx +3 -1
  19. package/src/common/withProviders.tsx +6 -2
  20. package/src/components/Popups/PopupDrawingToolbar.tsx +0 -7
  21. package/src/components/Popups/PopupV2.scss +4 -1
  22. package/src/components/Popups/PopupV2.tsx +122 -47
  23. package/src/document/DocumentAnnotator.ts +8 -4
  24. package/src/document/docUtil.ts +2 -1
  25. package/src/drawing/DrawingAnnotations.tsx +23 -14
  26. package/src/drawing/DrawingAnnotationsContainer.tsx +3 -0
  27. package/src/drawing/DrawingCreator.tsx +9 -16
  28. package/src/drawing/DrawingManager.tsx +13 -2
  29. package/src/highlight/HighlightAnnotations.tsx +27 -14
  30. package/src/highlight/HighlightContainer.tsx +3 -0
  31. package/src/highlight/HighlightCreatorManager.ts +8 -5
  32. package/src/highlight/HighlightListener.ts +4 -2
  33. package/src/highlight/HighlightManager.tsx +13 -2
  34. package/src/image/ImageAnnotator.ts +3 -2
  35. package/src/media/MediaAnnotator.ts +2 -1
  36. package/src/popup/PopupContainer.tsx +8 -1
  37. package/src/popup/PopupLayer.tsx +41 -6
  38. package/src/popup/PopupManager.tsx +24 -4
  39. package/src/region/RegionCreation.tsx +11 -8
  40. package/src/region/RegionCreationContainer.tsx +2 -2
  41. package/src/region/RegionCreator.tsx +9 -17
  42. package/src/store/annotations/actions.ts +89 -0
  43. package/src/store/annotations/reducer.ts +48 -0
  44. package/src/store/eventing/create.ts +3 -3
  45. package/src/store/eventing/delete.ts +27 -0
  46. package/src/store/eventing/middleware.ts +40 -4
  47. package/src/store/eventing/replyCreate.ts +34 -0
  48. package/src/store/eventing/replyDelete.ts +35 -0
  49. package/src/store/eventing/replyUpdate.ts +39 -0
  50. package/src/store/eventing/types.ts +4 -4
  51. package/src/store/eventing/update.ts +29 -0
  52. package/src/store/highlight/actions.ts +32 -3
  53. package/src/store/highlight/types.ts +1 -0
  54. package/src/store/options/selectors.ts +2 -2
  55. package/src/store/options/types.ts +2 -2
  56. package/src/utils/rotate.ts +43 -0
@@ -13,6 +13,7 @@ import {
13
13
  getCreatorStatus,
14
14
  getIsPromoting,
15
15
  getIsSelecting,
16
+ getRotation,
16
17
  getSelectionForLocation,
17
18
  isCreatorStagedHighlight,
18
19
  Mode,
@@ -33,6 +34,7 @@ export type Props = {
33
34
  isPending: boolean;
34
35
  isPromoting: boolean;
35
36
  isSelecting: boolean;
37
+ rotation: number;
36
38
  selection: SelectionItem | null;
37
39
  staged: CreatorItemHighlight | null;
38
40
  };
@@ -47,6 +49,7 @@ export const mapStateToProps = (state: AppState, { location }: { location: numbe
47
49
  isPending: getCreatorStatus(state) === CreatorStatus.pending,
48
50
  isPromoting: getIsPromoting(state),
49
51
  isSelecting: getIsSelecting(state),
52
+ rotation: getRotation(state),
50
53
  selection: getSelectionForLocation(state, location),
51
54
  staged: isCreatorStagedHighlight(staged) ? staged : null,
52
55
  };
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  AppStore,
3
3
  getIsSelecting,
4
+ getRotation,
4
5
  SelectionArg as Selection,
5
6
  setIsSelectingAction,
6
7
  setSelectionAction,
@@ -41,18 +42,18 @@ export default class HighlightCreatorManager implements Manager {
41
42
  this.store.dispatch(setSelectionAction(null));
42
43
 
43
44
  this.referenceEl.addEventListener('mousedown', this.handleMouseDown);
44
- this.referenceEl.addEventListener('mouseup', this.handleMouseUp);
45
+ document.addEventListener('mouseup', this.handleMouseUp);
45
46
  }
46
47
 
47
- style(styles: Partial<CSSStyleDeclaration>): CSSStyleDeclaration {
48
- return Object.assign(this.referenceEl.style, styles);
48
+ style(): CSSStyleDeclaration {
49
+ return this.referenceEl.style;
49
50
  }
50
51
 
51
52
  destroy(): void {
52
53
  clearTimeout(this.selectionChangeTimer);
53
54
 
54
55
  this.referenceEl.removeEventListener('mousedown', this.handleMouseDown);
55
- this.referenceEl.removeEventListener('mouseup', this.handleMouseUp);
56
+ document.removeEventListener('mouseup', this.handleMouseUp);
56
57
  }
57
58
 
58
59
  handleMouseDown = ({ buttons }: MouseEvent): void => {
@@ -72,7 +73,9 @@ export default class HighlightCreatorManager implements Manager {
72
73
  }
73
74
 
74
75
  this.selectionChangeTimer = window.setTimeout(() => {
75
- this.store.dispatch(setSelectionAction(this.getSelection()));
76
+ const selection = this.getSelection();
77
+ const rotation = getRotation(this.store.getState());
78
+ this.store.dispatch(setSelectionAction(selection ? { ...selection, rotation } : null));
76
79
  this.store.dispatch(setIsSelectingAction(false));
77
80
  }, this.selectionChangeDelay);
78
81
  };
@@ -1,5 +1,5 @@
1
1
  import debounce from 'lodash/debounce';
2
- import { AppStore, getIsSelecting, SelectionArg as Selection, setSelectionAction } from '../store';
2
+ import { AppStore, getIsSelecting, getRotation, SelectionArg as Selection, setSelectionAction } from '../store';
3
3
 
4
4
  export type Options = {
5
5
  getSelection: () => Selection | null;
@@ -30,7 +30,9 @@ export default class HighlightListener {
30
30
  return;
31
31
  }
32
32
 
33
- this.store.dispatch(setSelectionAction(this.getSelection()));
33
+ const selection = this.getSelection();
34
+ const rotation = getRotation(this.store.getState());
35
+ this.store.dispatch(setSelectionAction(selection ? { ...selection, rotation } : null));
34
36
  };
35
37
 
36
38
  debounceHandleSelectionChange = debounce(this.handleSelectionChange, SELECTION_CHANGE_DEBOUNCE);
@@ -1,9 +1,20 @@
1
1
  import * as React from 'react';
2
2
  import * as ReactDOM from 'react-dom/client';
3
- import BaseManager, { Props } from '../common/BaseManager';
3
+ import BaseManager, { Options as BaseOptions, Props } from '../common/BaseManager';
4
4
  import HighlightContainer from './HighlightContainer';
5
5
 
6
+ export type Options = BaseOptions & {
7
+ popupPortalEl?: HTMLElement | null;
8
+ };
9
+
6
10
  export default class HighlightManager extends BaseManager {
11
+ popupPortalEl?: HTMLElement | null;
12
+
13
+ constructor({ popupPortalEl, ...options }: Options) {
14
+ super(options);
15
+ this.popupPortalEl = popupPortalEl;
16
+ }
17
+
7
18
  decorate(): void {
8
19
  this.reactEl.classList.add('ba-Layer--highlight');
9
20
  this.reactEl.dataset.testid = 'ba-Layer--highlight';
@@ -14,6 +25,6 @@ export default class HighlightManager extends BaseManager {
14
25
  this.root = ReactDOM.createRoot(this.reactEl);
15
26
  }
16
27
 
17
- this.root.render(<HighlightContainer location={this.location} {...props} />);
28
+ this.root.render(<HighlightContainer location={this.location} popupPortalEl={this.popupPortalEl} {...props} />);
18
29
  }
19
30
  }
@@ -73,8 +73,8 @@ export default class ImageAnnotator extends BaseAnnotator {
73
73
  return this.managers;
74
74
  }
75
75
 
76
- this.managers.add(new PopupManager({ referenceEl, resinTags }));
77
- this.managers.add(new DrawingManager({ referenceEl, resinTags }));
76
+ this.managers.add(new PopupManager({ popupPortalEl: this.popupPortalEl, referenceEl, resinTags }));
77
+ this.managers.add(new DrawingManager({ popupPortalEl: this.popupPortalEl, referenceEl, resinTags }));
78
78
  this.managers.add(new RegionManager({ referenceEl, resinTags }));
79
79
  this.managers.add(new RegionCreationManager({ referenceEl, resinTags }));
80
80
  }
@@ -124,6 +124,7 @@ export default class ImageAnnotator extends BaseAnnotator {
124
124
  });
125
125
 
126
126
  manager.render({
127
+ callbacks: { onCopyLink: this.onCopyLink },
127
128
  intl: this.intl,
128
129
  store: this.store,
129
130
  });
@@ -46,7 +46,7 @@ export default class MediaAnnotator extends BaseAnnotator {
46
46
 
47
47
 
48
48
  addManagers(referenceEl: HTMLVideoElement, resinTags: { fileid: string|null; iscurrent: boolean }): void {
49
- this.managers.add(new PopupManager({ location: MEDIA_LOCATION_INDEX, referenceEl, resinTags, targetType: TARGET_TYPE.FRAME }));
49
+ this.managers.add(new PopupManager({ location: MEDIA_LOCATION_INDEX, popupPortalEl: this.popupPortalEl, referenceEl, resinTags, targetType: TARGET_TYPE.FRAME }));
50
50
  this.managers.add(new DrawingManager({ location: MEDIA_LOCATION_INDEX, referenceEl, resinTags, targetType: TARGET_TYPE.FRAME }));
51
51
  this.managers.add(new RegionManager({ location: MEDIA_LOCATION_INDEX, referenceEl, resinTags, targetType: TARGET_TYPE.FRAME }));
52
52
  this.managers.add(new RegionCreationManager({ location: MEDIA_LOCATION_INDEX, referenceEl, resinTags, targetType: TARGET_TYPE.FRAME }));
@@ -108,6 +108,7 @@ export default class MediaAnnotator extends BaseAnnotator {
108
108
  width: `${referenceEl.offsetWidth}px`,
109
109
  });
110
110
  manager.render({
111
+ callbacks: { onCopyLink: this.onCopyLink },
111
112
  intl: this.intl,
112
113
  store: this.store,
113
114
  });
@@ -20,6 +20,13 @@ import { isFeatureEnabled } from '../store/options';
20
20
  import { createDrawingAction } from '../drawing/actions';
21
21
  import { createHighlightAction } from '../highlight/actions';
22
22
  import { createRegionAction } from '../region';
23
+ import { TARGET_TYPE } from '../constants';
24
+
25
+ export type OwnProps = {
26
+ location: number;
27
+ popupPortalEl?: HTMLElement | null;
28
+ targetType?: TARGET_TYPE;
29
+ };
23
30
 
24
31
  export type Props = {
25
32
  activeAnnotationId: string | null;
@@ -32,7 +39,7 @@ export type Props = {
32
39
  status: CreatorStatus;
33
40
  };
34
41
 
35
- export const mapStateToProps = (state: AppState, { location }: { location: number }): Props => {
42
+ export const mapStateToProps = (state: AppState, { location }: OwnProps): Props => {
36
43
  return {
37
44
  activeAnnotationId: getActiveAnnotationId(state),
38
45
  isPromoting: getIsPromoting(state),
@@ -1,4 +1,5 @@
1
1
  import * as React from 'react';
2
+ import * as ReactDOM from 'react-dom';
2
3
  import noop from 'lodash/noop';
3
4
  import PopupReply from '../components/Popups/PopupReply';
4
5
  import PopupV2 from '../components/Popups/PopupV2';
@@ -29,6 +30,7 @@ export type Props = {
29
30
  location: number;
30
31
  message: string;
31
32
  mode: Mode;
33
+ popupPortalEl?: HTMLElement | null;
32
34
  referenceId: string | null;
33
35
  resetCreator: () => void;
34
36
  setMessage: (message: string) => void;
@@ -43,6 +45,8 @@ const modeStagedMap: { [M in Mode]?: (staged: CreatorItem | null) => boolean } =
43
45
  [Mode.REGION]: isCreatorStagedRegion,
44
46
  };
45
47
 
48
+ const ACTIVE_TARGET_OBSERVER_TIMEOUT_MS = 10000;
49
+
46
50
  const PopupLayer = (props: Props): JSX.Element | null => {
47
51
  const {
48
52
  activeAnnotationId,
@@ -53,6 +57,7 @@ const PopupLayer = (props: Props): JSX.Element | null => {
53
57
  isThreadedAnnotation = false,
54
58
  message,
55
59
  mode,
60
+ popupPortalEl,
56
61
  referenceId,
57
62
  resetCreator,
58
63
  setMessage,
@@ -95,13 +100,39 @@ const PopupLayer = (props: Props): JSX.Element | null => {
95
100
  setReference(referenceId ? document.querySelector(`[data-ba-reference-id="${referenceId}"]`) : null);
96
101
  }, [referenceId]);
97
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.
98
105
  React.useEffect(() => {
99
- if (activeAnnotationId && isThreadedAnnotation) {
100
- const el = document.querySelector(`[data-ba-annotation-id="${CSS.escape(activeAnnotationId)}"]`);
101
- setActiveReference(el);
102
- } else {
106
+ if (!activeAnnotationId || !isThreadedAnnotation) {
103
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;
104
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
+ };
105
136
  }, [activeAnnotationId, isThreadedAnnotation]);
106
137
 
107
138
  const showCreator = canCreate && canReply && reference && staged;
@@ -111,6 +142,7 @@ const PopupLayer = (props: Props): JSX.Element | null => {
111
142
  <div className="ba-PopupLayer-popup">
112
143
  <PopupV2
113
144
  onSubmit={handleSubmit}
145
+ popupPortalEl={popupPortalEl}
114
146
  reference={reference}
115
147
  />
116
148
  </div>
@@ -118,7 +150,8 @@ const PopupLayer = (props: Props): JSX.Element | null => {
118
150
  }
119
151
 
120
152
  if (showCreator) {
121
- return (
153
+ if (!popupPortalEl) return null;
154
+ return ReactDOM.createPortal(
122
155
  <div className="ba-PopupLayer-popup">
123
156
  <PopupReply
124
157
  isPending={isPending}
@@ -128,7 +161,8 @@ const PopupLayer = (props: Props): JSX.Element | null => {
128
161
  reference={reference}
129
162
  value={message}
130
163
  />
131
- </div>
164
+ </div>,
165
+ popupPortalEl,
132
166
  );
133
167
  }
134
168
 
@@ -138,6 +172,7 @@ const PopupLayer = (props: Props): JSX.Element | null => {
138
172
  <PopupV2
139
173
  annotationId={activeAnnotationId}
140
174
  onSubmit={handleSubmit}
175
+ popupPortalEl={popupPortalEl}
141
176
  reference={activeReference}
142
177
  />
143
178
  </div>
@@ -1,19 +1,39 @@
1
1
  import * as React from 'react';
2
2
  import * as ReactDOM from 'react-dom/client';
3
- import BaseManager, { Props } from '../common/BaseManager';
3
+ import BaseManager, { Options as BaseOptions, Props } from '../common/BaseManager';
4
4
  import PopupContainer from './PopupContainer';
5
5
 
6
+ export type Options = BaseOptions & {
7
+ popupPortalEl?: HTMLElement | null;
8
+ };
9
+
6
10
  export default class PopupManager extends BaseManager {
11
+ popupPortalEl?: HTMLElement | null;
12
+
13
+ constructor({ popupPortalEl, ...options }: Options) {
14
+ super(options);
15
+ this.popupPortalEl = popupPortalEl;
16
+ }
17
+
7
18
  decorate(): void {
8
19
  this.reactEl.classList.add('ba-Layer--popup');
9
20
  this.reactEl.dataset.testid = 'ba-Layer--popup';
10
21
  }
11
22
 
12
- render(props: Props): void {
23
+ render({ callbacks, intl, store }: Props): void {
13
24
  if (!this.root) {
14
25
  this.root = ReactDOM.createRoot(this.reactEl);
15
26
  }
16
-
17
- this.root.render(<PopupContainer location={this.location} {...props} targetType={this.targetType} />);
27
+
28
+ this.root.render(
29
+ <PopupContainer
30
+ callbacks={callbacks}
31
+ intl={intl}
32
+ location={this.location}
33
+ popupPortalEl={this.popupPortalEl}
34
+ store={store}
35
+ targetType={this.targetType}
36
+ />,
37
+ );
18
38
  }
19
39
  }
@@ -9,9 +9,9 @@ import { getVideoCurrentTimeInMilliseconds } from '../utils/useVideoTiming';
9
9
 
10
10
  type Props = {
11
11
  isCreating: boolean;
12
- isRotated: boolean;
13
12
  location: number;
14
13
  resetCreator: () => void;
14
+ rotation: number;
15
15
  setReferenceId: (uuid: string) => void;
16
16
  setStaged: (staged: CreatorItemRegion | null) => void;
17
17
  setStatus: (status: CreatorStatus) => void;
@@ -27,13 +27,19 @@ type State = {
27
27
  export default class RegionCreation extends React.PureComponent<Props, State> {
28
28
  static defaultProps = {
29
29
  isCreating: false,
30
- isRotated: false,
30
+ rotation: 0,
31
31
  };
32
32
 
33
33
  state: State = {};
34
34
 
35
35
  handleAbort = (): void => {
36
- const { resetCreator } = this.props;
36
+ const { resetCreator, staged } = this.props;
37
+
38
+ // Click-away (a click too small to start a new region) must not discard an in-progress comment/annotation
39
+ if (staged) {
40
+ return;
41
+ }
42
+
37
43
  resetCreator();
38
44
  };
39
45
 
@@ -57,11 +63,7 @@ export default class RegionCreation extends React.PureComponent<Props, State> {
57
63
  };
58
64
 
59
65
  render(): JSX.Element | null {
60
- const { isCreating, isRotated, staged } = this.props;
61
-
62
- if (isRotated) {
63
- return null;
64
- }
66
+ const { isCreating, rotation, staged } = this.props;
65
67
 
66
68
  return (
67
69
  <>
@@ -71,6 +73,7 @@ export default class RegionCreation extends React.PureComponent<Props, State> {
71
73
  onAbort={this.handleAbort}
72
74
  onStart={this.handleStart}
73
75
  onStop={this.handleStop}
76
+ rotation={rotation}
74
77
  />
75
78
  )}
76
79
 
@@ -19,7 +19,7 @@ import withProviders from '../common/withProviders';
19
19
 
20
20
  export type Props = {
21
21
  isCreating: boolean;
22
- isRotated: boolean;
22
+ rotation: number;
23
23
  staged: CreatorItemRegion | null;
24
24
  };
25
25
 
@@ -28,7 +28,7 @@ export const mapStateToProps = (state: AppState, { location }: { location: numbe
28
28
 
29
29
  return {
30
30
  isCreating: getAnnotationMode(state) === Mode.REGION && getCreatorStatus(state) !== CreatorStatus.pending,
31
- isRotated: !!getRotation(state),
31
+ rotation: getRotation(state),
32
32
  staged: isCreatorStagedRegion(staged) ? staged : null,
33
33
  };
34
34
  };
@@ -4,6 +4,7 @@ import PointerCapture, { Status as DrawingStatus } from '../components/PointerCa
4
4
  import PopupCursor from '../components/Popups/PopupCursor';
5
5
  import RegionRect, { RegionRectRef } from './RegionRect';
6
6
  import useAutoScroll from '../common/useAutoScroll';
7
+ import { getElementLocalPosition } from '../utils/rotate';
7
8
  import { Rect } from '../@types';
8
9
  import { styleShape } from './regionUtil';
9
10
  import './RegionCreator.scss';
@@ -13,6 +14,7 @@ type Props = {
13
14
  onAbort: () => void;
14
15
  onStart: () => void;
15
16
  onStop: (shape: Rect) => void;
17
+ rotation?: number;
16
18
  };
17
19
 
18
20
  const MIN_X = 0; // Minimum region x position must be positive
@@ -22,7 +24,7 @@ const MIN_SIZE = 10; // Minimum region size must be large enough to be clickable
22
24
  const isValid = (x1: number, y1: number, x2: number, y2: number): boolean =>
23
25
  Math.abs(x2 - x1) >= MIN_SIZE || Math.abs(y2 - y1) >= MIN_SIZE;
24
26
 
25
- export default function RegionCreator({ className, onAbort, onStart, onStop }: Props): JSX.Element {
27
+ export default function RegionCreator({ className, onAbort, onStart, onStop, rotation = 0 }: Props): JSX.Element {
26
28
  const [drawingStatus, setDrawingStatus] = React.useState<DrawingStatus>(DrawingStatus.init);
27
29
  const [isHovering, setIsHovering] = React.useState<boolean>(false);
28
30
  const creatorElRef = React.useRef<HTMLDivElement>(null);
@@ -34,18 +36,6 @@ export default function RegionCreator({ className, onAbort, onStart, onStop }: P
34
36
  const regionRectRef = React.useRef<RegionRectRef>(null);
35
37
  const renderHandleRef = React.useRef<number | null>(null);
36
38
 
37
- // Drawing Helpers
38
- const getPosition = (x: number, y: number): [number, number] => {
39
- const { current: creatorEl } = creatorElRef;
40
-
41
- if (!creatorEl) {
42
- return [x, y];
43
- }
44
-
45
- // Calculate the new position based on the mouse position less the page offset
46
- const { left, top } = creatorEl.getBoundingClientRect();
47
- return [x - left, y - top];
48
- };
49
39
  const getShape = (): Rect | null => {
50
40
  const { current: creatorEl } = creatorElRef;
51
41
  const { current: x1 } = positionX1Ref;
@@ -57,7 +47,9 @@ export default function RegionCreator({ className, onAbort, onStart, onStop }: P
57
47
  return null;
58
48
  }
59
49
 
60
- 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
@@ -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);