box-annotations 5.2.1-beta.1 → 5.2.1-beta.11

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 (68) hide show
  1. package/dist/annotations.css +3 -4
  2. package/dist/annotations.js +1 -1
  3. package/dist/annotations.js.LICENSE.txt +3 -43
  4. package/i18n/en-US.js +4 -0
  5. package/i18n/en-US.properties +8 -0
  6. package/i18n/json/src/components/BoundingBoxHighlight/messages.json +1 -0
  7. package/i18n/json/src/components/Popups/messages.json +1 -1
  8. package/package.json +28 -15
  9. package/src/@types/events.ts +4 -0
  10. package/src/@types/global.ts +0 -9
  11. package/src/@types/model.ts +5 -0
  12. package/src/BoxAnnotations.ts +3 -1
  13. package/src/adapters/threadedAnnotationsAdapters.ts +155 -0
  14. package/src/api/types.ts +28 -1
  15. package/src/boundingBoxHighlight/BoundingBoxHighlightContainer.tsx +33 -0
  16. package/src/boundingBoxHighlight/BoundingBoxHighlightManager.tsx +19 -0
  17. package/src/boundingBoxHighlight/index.ts +1 -0
  18. package/src/common/BaseAnnotator.scss +2 -1
  19. package/src/common/BaseAnnotator.ts +129 -0
  20. package/src/common/DeselectListener.tsx +3 -1
  21. package/src/components/BoundingBoxHighlight/BoundingBoxHighlightList.scss +8 -0
  22. package/src/components/BoundingBoxHighlight/BoundingBoxHighlightList.tsx +67 -0
  23. package/src/components/BoundingBoxHighlight/BoundingBoxHighlightNav.scss +73 -0
  24. package/src/components/BoundingBoxHighlight/BoundingBoxHighlightNav.tsx +68 -0
  25. package/src/components/BoundingBoxHighlight/BoundingBoxHighlightRect.scss +22 -0
  26. package/src/components/BoundingBoxHighlight/BoundingBoxHighlightRect.tsx +77 -0
  27. package/src/components/BoundingBoxHighlight/index.ts +4 -0
  28. package/src/components/BoundingBoxHighlight/messages.ts +14 -0
  29. package/src/components/Popups/PopupReply.tsx +8 -28
  30. package/src/components/Popups/PopupV2.scss +28 -0
  31. package/src/components/Popups/PopupV2.tsx +322 -0
  32. package/src/components/Popups/messages.ts +10 -0
  33. package/src/document/DocumentAnnotator.ts +76 -8
  34. package/src/drawing/DrawingAnnotations.tsx +3 -0
  35. package/src/drawing/DrawingAnnotationsContainer.tsx +3 -0
  36. package/src/drawing/DrawingCreator.tsx +9 -16
  37. package/src/drawing/DrawingTarget.tsx +1 -0
  38. package/src/highlight/HighlightTarget.tsx +1 -0
  39. package/src/icons/ChevronLeft.tsx +24 -0
  40. package/src/icons/ChevronRight.tsx +24 -0
  41. package/src/image/ImageAnnotator.ts +48 -2
  42. package/src/media/MediaAnnotator.ts +1 -1
  43. package/src/popup/PopupContainer.tsx +11 -1
  44. package/src/popup/PopupLayer.tsx +66 -22
  45. package/src/popup/PopupManager.tsx +21 -3
  46. package/src/region/RegionAnnotation.tsx +1 -0
  47. package/src/region/RegionCreation.tsx +4 -7
  48. package/src/region/RegionCreationContainer.tsx +2 -2
  49. package/src/region/RegionCreator.tsx +9 -17
  50. package/src/store/annotations/actions.ts +78 -3
  51. package/src/store/annotations/reducer.ts +23 -1
  52. package/src/store/boundingBoxHighlights/actions.ts +6 -0
  53. package/src/store/boundingBoxHighlights/index.ts +4 -0
  54. package/src/store/boundingBoxHighlights/reducer.ts +33 -0
  55. package/src/store/boundingBoxHighlights/selectors.ts +12 -0
  56. package/src/store/boundingBoxHighlights/types.ts +13 -0
  57. package/src/store/createRootReducer.ts +2 -0
  58. package/src/store/eventing/boundingBoxHighlightNav.ts +13 -0
  59. package/src/store/eventing/middleware.ts +3 -0
  60. package/src/store/index.ts +1 -0
  61. package/src/store/options/actions.ts +2 -0
  62. package/src/store/options/reducer.ts +9 -2
  63. package/src/store/options/selectors.ts +4 -0
  64. package/src/store/options/types.ts +6 -0
  65. package/src/store/types.ts +2 -0
  66. package/src/utils/rotate.ts +43 -0
  67. package/src/utils/scroll.ts +8 -3
  68. package/src/components/Popups/PopupReplyV2.tsx +0 -10
@@ -0,0 +1,24 @@
1
+ import * as React from 'react';
2
+
3
+ function ChevronRight(props: React.SVGProps<SVGSVGElement>): JSX.Element {
4
+ return (
5
+ <svg
6
+ data-testid="ChevronRight"
7
+ fill="none"
8
+ focusable="false"
9
+ height="1em"
10
+ role="img"
11
+ viewBox="0 0 24 24"
12
+ width="1em"
13
+ xmlns="http://www.w3.org/2000/svg"
14
+ {...props}
15
+ >
16
+ <path
17
+ d="M9.293 4.293a1 1 0 0 1 1.414 0L17 10.586a2 2 0 0 1 0 2.828l-6.293 6.293a1 1 0 0 1-1.414-1.414L15.586 12 9.293 5.707a1 1 0 0 1 0-1.414Z"
18
+ fill="currentColor"
19
+ />
20
+ </svg>
21
+ );
22
+ }
23
+
24
+ export default ChevronRight;
@@ -1,15 +1,18 @@
1
1
  import { Unsubscribe } from 'redux';
2
2
  import BaseAnnotator, { Options } from '../common/BaseAnnotator';
3
3
  import PopupManager from '../popup/PopupManager';
4
+ import { BoundingBoxHighlightManager } from '../boundingBoxHighlight';
4
5
  import { centerDrawing, DrawingManager, isDrawing } from '../drawing';
5
6
  import { centerRegion, isRegion, RegionCreationManager, RegionManager } from '../region';
6
7
  import { CreatorStatus, getCreatorStatus } from '../store/creator';
7
- import { getAnnotation, getFileId, getIsCurrentFileVersion, getRotation } from '../store';
8
+ import { getAnnotation, getFileId, getIsCurrentFileVersion, getRotation, getViewMode } from '../store';
8
9
  import { getRotatedPosition } from '../utils/rotate';
9
10
  import { Manager } from '../common/BaseManager';
10
11
  import { scrollToLocation } from '../utils/scroll';
11
12
  import './ImageAnnotator.scss';
12
13
 
14
+ import type { ViewMode } from '../store/options/types';
15
+
13
16
  export const CSS_IS_DRAWING_CLASS = 'ba-is-drawing';
14
17
 
15
18
  export default class ImageAnnotator extends BaseAnnotator {
@@ -17,6 +20,9 @@ export default class ImageAnnotator extends BaseAnnotator {
17
20
 
18
21
  managers: Set<Manager> = new Set();
19
22
 
23
+ /** Tracks which view mode the current managers were created for. Used to destroy/recreate when switching. */
24
+ private managersViewMode: ViewMode | null = null;
25
+
20
26
  storeHandler?: Unsubscribe;
21
27
 
22
28
  constructor(options: Options) {
@@ -41,10 +47,18 @@ export default class ImageAnnotator extends BaseAnnotator {
41
47
  return this.containerEl?.querySelector('.bp-image');
42
48
  }
43
49
 
50
+ /** Destroys all managers and clears the cache. Call when switching view modes. */
51
+ private clearManagers(): void {
52
+ this.managers.forEach(manager => manager.destroy());
53
+ this.managers.clear();
54
+ this.managersViewMode = null;
55
+ }
56
+
44
57
  getManagers(parentEl: HTMLElement, referenceEl: HTMLElement): Set<Manager> {
45
58
  const fileId = getFileId(this.store.getState());
46
59
  const isCurrentFileVersion = getIsCurrentFileVersion(this.store.getState());
47
60
  const resinTags = { fileid: fileId, iscurrent: isCurrentFileVersion };
61
+ const viewMode = getViewMode(this.store.getState());
48
62
 
49
63
  this.managers.forEach(manager => {
50
64
  if (!manager.exists(parentEl)) {
@@ -54,7 +68,12 @@ export default class ImageAnnotator extends BaseAnnotator {
54
68
  });
55
69
 
56
70
  if (this.managers.size === 0) {
57
- this.managers.add(new PopupManager({ referenceEl, resinTags }));
71
+ if (viewMode === 'boundingBoxes') {
72
+ this.managers.add(new BoundingBoxHighlightManager({ location: 1, referenceEl, resinTags }));
73
+ return this.managers;
74
+ }
75
+
76
+ this.managers.add(new PopupManager({ popupPortalEl: this.popupPortalEl, referenceEl, resinTags }));
58
77
  this.managers.add(new DrawingManager({ referenceEl, resinTags }));
59
78
  this.managers.add(new RegionManager({ referenceEl, resinTags }));
60
79
  this.managers.add(new RegionCreationManager({ referenceEl, resinTags }));
@@ -84,11 +103,17 @@ export default class ImageAnnotator extends BaseAnnotator {
84
103
  render(): void {
85
104
  const referenceEl = this.getReference();
86
105
  const rotation = getRotation(this.store.getState()) || 0;
106
+ const viewMode = getViewMode(this.store.getState());
87
107
 
88
108
  if (!this.annotatedEl || !referenceEl) {
89
109
  return;
90
110
  }
91
111
 
112
+ if (this.managersViewMode !== null && this.managersViewMode !== viewMode) {
113
+ this.clearManagers();
114
+ }
115
+ this.managersViewMode = viewMode;
116
+
92
117
  this.getManagers(this.annotatedEl, referenceEl).forEach(manager => {
93
118
  manager.style({
94
119
  height: `${referenceEl.offsetHeight}px`,
@@ -107,6 +132,27 @@ export default class ImageAnnotator extends BaseAnnotator {
107
132
  this.postRender();
108
133
  }
109
134
 
135
+ public postRender(): void {
136
+ // DeselectManager is only needed for annotation creation; skip in bounding box mode.
137
+ if (getViewMode(this.store.getState()) === 'boundingBoxes') {
138
+ if (this.deselectManager) {
139
+ this.deselectManager.destroy();
140
+ this.deselectManager = null;
141
+ }
142
+ return;
143
+ }
144
+ super.postRender();
145
+ }
146
+
147
+ protected getScrollReferenceForHighlight(): HTMLElement | null | undefined {
148
+ return this.getReference();
149
+ }
150
+
151
+ protected getAdjustedScrollOffsets(offsets: { x: number; y: number }): { x: number; y: number } {
152
+ const rotation = getRotation(this.store.getState()) || 0;
153
+ return getRotatedPosition(offsets, rotation);
154
+ }
155
+
110
156
  scrollToAnnotation(annotationId: string | null): void {
111
157
  if (!annotationId) {
112
158
  return;
@@ -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 }));
@@ -15,12 +15,21 @@ import {
15
15
  setMessageAction,
16
16
  getCreatorReferenceId,
17
17
  } from '../store';
18
+ import { getActiveAnnotationId } from '../store/annotations/selectors';
18
19
  import { isFeatureEnabled } from '../store/options';
19
20
  import { createDrawingAction } from '../drawing/actions';
20
21
  import { createHighlightAction } from '../highlight/actions';
21
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
+ };
22
30
 
23
31
  export type Props = {
32
+ activeAnnotationId: string | null;
24
33
  isPromoting: boolean;
25
34
  isThreadedAnnotation?: boolean;
26
35
  message: string;
@@ -30,8 +39,9 @@ export type Props = {
30
39
  status: CreatorStatus;
31
40
  };
32
41
 
33
- export const mapStateToProps = (state: AppState, { location }: { location: number }): Props => {
42
+ export const mapStateToProps = (state: AppState, { location }: OwnProps): Props => {
34
43
  return {
44
+ activeAnnotationId: getActiveAnnotationId(state),
35
45
  isPromoting: getIsPromoting(state),
36
46
  isThreadedAnnotation: isFeatureEnabled(state, 'isThreadedAnnotation'),
37
47
  message: getCreatorMessage(state),
@@ -1,6 +1,7 @@
1
1
  import * as React from 'react';
2
2
  import noop from 'lodash/noop';
3
3
  import PopupReply from '../components/Popups/PopupReply';
4
+ import PopupV2 from '../components/Popups/PopupV2';
4
5
  import { CreateArg as DrawingCreateArg } from '../drawing/actions';
5
6
  import { CreateArg as HighlightCreateArg } from '../highlight/actions';
6
7
  import { CreateArg as RegionCreateArg } from '../region/actions';
@@ -13,10 +14,13 @@ import {
13
14
  Mode,
14
15
  } from '../store';
15
16
  import { PopupReference } from '../components/Popups/Popper';
16
- import './PopupLayer.scss';
17
+
17
18
  import { TARGET_TYPE } from '../constants';
18
19
 
20
+ import './PopupLayer.scss';
21
+
19
22
  export type Props = {
23
+ activeAnnotationId: string | null;
20
24
  createDrawing?: (arg: DrawingCreateArg) => void;
21
25
  createHighlight?: (arg: HighlightCreateArg) => void;
22
26
  createRegion?: (arg: RegionCreateArg) => void;
@@ -25,6 +29,7 @@ export type Props = {
25
29
  location: number;
26
30
  message: string;
27
31
  mode: Mode;
32
+ popupPortalEl?: HTMLElement | null;
28
33
  referenceId: string | null;
29
34
  resetCreator: () => void;
30
35
  setMessage: (message: string) => void;
@@ -41,6 +46,7 @@ const modeStagedMap: { [M in Mode]?: (staged: CreatorItem | null) => boolean } =
41
46
 
42
47
  const PopupLayer = (props: Props): JSX.Element | null => {
43
48
  const {
49
+ activeAnnotationId,
44
50
  createDrawing = noop,
45
51
  createHighlight = noop,
46
52
  createRegion = noop,
@@ -48,6 +54,7 @@ const PopupLayer = (props: Props): JSX.Element | null => {
48
54
  isThreadedAnnotation = false,
49
55
  message,
50
56
  mode,
57
+ popupPortalEl,
51
58
  referenceId,
52
59
  resetCreator,
53
60
  setMessage,
@@ -57,6 +64,7 @@ const PopupLayer = (props: Props): JSX.Element | null => {
57
64
  } = props;
58
65
 
59
66
  const [reference, setReference] = React.useState<PopupReference | null>(null);
67
+ const [activeReference, setActiveReference] = React.useState<PopupReference | null>(null);
60
68
  const canCreate = (modeStagedMap[mode]?.(staged ?? null) ?? false) || isPromoting;
61
69
  const canReply = status !== CreatorStatus.started && status !== CreatorStatus.init;
62
70
  const isPending = status === CreatorStatus.pending;
@@ -69,17 +77,19 @@ const PopupLayer = (props: Props): JSX.Element | null => {
69
77
  setMessage(text);
70
78
  };
71
79
 
72
- const handleSubmit = (): void => {
80
+ const handleSubmit = (text?: string): void => {
73
81
  if (!staged) {
74
82
  return;
75
83
  }
76
84
 
85
+ const submitMessage = text ?? message;
86
+
77
87
  if (isCreatorStagedHighlight(staged)) {
78
- createHighlight({ ...staged, message, targetType });
88
+ createHighlight({ ...staged, message: submitMessage, targetType });
79
89
  } else if (isCreatorStagedRegion(staged)) {
80
- createRegion({ ...staged, message, targetType });
90
+ createRegion({ ...staged, message: submitMessage, targetType });
81
91
  } else if (isCreatorStagedDrawing(staged)) {
82
- createDrawing({ ...staged, message, targetType });
92
+ createDrawing({ ...staged, message: submitMessage, targetType });
83
93
  }
84
94
  };
85
95
 
@@ -87,24 +97,58 @@ const PopupLayer = (props: Props): JSX.Element | null => {
87
97
  setReference(referenceId ? document.querySelector(`[data-ba-reference-id="${referenceId}"]`) : null);
88
98
  }, [referenceId]);
89
99
 
100
+ React.useEffect(() => {
101
+ if (activeAnnotationId && isThreadedAnnotation) {
102
+ const el = document.querySelector(`[data-ba-annotation-id="${CSS.escape(activeAnnotationId)}"]`);
103
+ setActiveReference(el);
104
+ } else {
105
+ setActiveReference(null);
106
+ }
107
+ }, [activeAnnotationId, isThreadedAnnotation]);
108
+
109
+ const showCreator = canCreate && canReply && reference && staged;
110
+
111
+ if (showCreator && isThreadedAnnotation) {
112
+ return (
113
+ <div className="ba-PopupLayer-popup">
114
+ <PopupV2
115
+ onSubmit={handleSubmit}
116
+ popupPortalEl={popupPortalEl}
117
+ reference={reference}
118
+ />
119
+ </div>
120
+ );
121
+ }
122
+
123
+ if (showCreator) {
124
+ return (
125
+ <div className="ba-PopupLayer-popup">
126
+ <PopupReply
127
+ isPending={isPending}
128
+ onCancel={handleCancel}
129
+ onChange={handleChange}
130
+ onSubmit={handleSubmit}
131
+ reference={reference}
132
+ value={message}
133
+ />
134
+ </div>
135
+ );
136
+ }
137
+
138
+ if (isThreadedAnnotation && activeAnnotationId && activeReference) {
139
+ return (
140
+ <div className="ba-PopupLayer-popup">
141
+ <PopupV2
142
+ annotationId={activeAnnotationId}
143
+ onSubmit={handleSubmit}
144
+ popupPortalEl={popupPortalEl}
145
+ reference={activeReference}
146
+ />
147
+ </div>
148
+ );
149
+ }
90
150
 
91
- return (
92
- <>
93
- {canCreate && canReply && reference && staged && (
94
- <div className="ba-PopupLayer-popup">
95
- <PopupReply
96
- isPending={isPending}
97
- isThreadedAnnotation={isThreadedAnnotation}
98
- onCancel={handleCancel}
99
- onChange={handleChange}
100
- onSubmit={handleSubmit}
101
- reference={reference}
102
- value={message}
103
- />
104
- </div>
105
- )}
106
- </>
107
- );
151
+ return null;
108
152
  };
109
153
 
110
154
  export default PopupLayer;
@@ -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 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';
@@ -13,7 +24,14 @@ export default class PopupManager extends BaseManager {
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
+ location={this.location}
31
+ {...props}
32
+ popupPortalEl={this.popupPortalEl}
33
+ targetType={this.targetType}
34
+ />,
35
+ );
18
36
  }
19
37
  }
@@ -36,6 +36,7 @@ export const RegionAnnotation = (props: Props, ref: React.Ref<RegionAnnotationRe
36
36
  <button
37
37
  ref={ref}
38
38
  className={classNames('ba-RegionAnnotation', className, { 'is-active': isActive })}
39
+ data-ba-annotation-id={annotationId}
39
40
  data-resin-itemid={annotationId}
40
41
  data-resin-target="highlightRegion"
41
42
  data-testid={`ba-AnnotationTarget-${annotationId}`}
@@ -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,7 +27,7 @@ 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 = {};
@@ -57,11 +57,7 @@ export default class RegionCreation extends React.PureComponent<Props, State> {
57
57
  };
58
58
 
59
59
  render(): JSX.Element | null {
60
- const { isCreating, isRotated, staged } = this.props;
61
-
62
- if (isRotated) {
63
- return null;
64
- }
60
+ const { isCreating, rotation, staged } = this.props;
65
61
 
66
62
  return (
67
63
  <>
@@ -71,6 +67,7 @@ export default class RegionCreation extends React.PureComponent<Props, State> {
71
67
  onAbort={this.handleAbort}
72
68
  onStart={this.handleStart}
73
69
  onStop={this.handleStop}
70
+ rotation={rotation}
74
71
  />
75
72
  )}
76
73
 
@@ -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
@@ -1,8 +1,9 @@
1
1
  import { createAction, createAsyncThunk } from '@reduxjs/toolkit';
2
- import { Annotation, NewAnnotation } from '../../@types';
2
+ import { Annotation, NewAnnotation, Reply } from '../../@types';
3
3
  import { APICollection } from '../../api';
4
4
  import { AppThunkAPI } from '../types';
5
- import { getFileId, getFileVersionId, getPermissions } from '../options';
5
+ import { getAnnotation } from './selectors';
6
+ import { getFileId, getFileVersionId, getPermissions, isFeatureEnabled } from '../options';
6
7
 
7
8
  export const createAnnotationAction = createAsyncThunk<Annotation, NewAnnotation, AppThunkAPI>(
8
9
  'CREATE_ANNOTATION',
@@ -39,9 +40,83 @@ export const fetchAnnotationsAction = createAsyncThunk<APICollection<Annotation>
39
40
  client.destroy();
40
41
  });
41
42
 
43
+ const shouldFetchReplies = isFeatureEnabled({ options: getState().options }, 'isThreadedAnnotation');
44
+
42
45
  // Wrap the client request in a promise to allow it to be returned and cancelled
43
46
  return new Promise<APICollection<Annotation>>((resolve, reject) => {
44
- client.getAnnotations(fileId, fileVersionId, permissions, resolve, reject, 1000, false);
47
+ client.getAnnotations(fileId, fileVersionId, permissions, resolve, reject, 1000, false, shouldFetchReplies);
48
+ });
49
+ },
50
+ );
51
+
52
+ export const createReplyAction = createAsyncThunk<
53
+ { annotationId: string; reply: Reply },
54
+ { annotationId: string; message: string },
55
+ AppThunkAPI
56
+ >(
57
+ 'CREATE_REPLY',
58
+ async ({ annotationId, message }, { extra, getState, signal }) => {
59
+ const client = extra.api.getAnnotationsAPI();
60
+ const state = getState();
61
+ const fileId = getFileId(state);
62
+ const filePermissions = getPermissions(state);
63
+ const annotation = getAnnotation(state, annotationId);
64
+ const permissions = { ...filePermissions, ...annotation?.permissions };
65
+
66
+ signal.addEventListener('abort', () => {
67
+ client.destroy();
68
+ });
69
+
70
+ const reply = await new Promise<Reply>((resolve, reject) => {
71
+ client.createAnnotationReply(fileId, annotationId, permissions, message, resolve, reject);
72
+ });
73
+
74
+ return { annotationId, reply };
75
+ },
76
+ );
77
+
78
+ export const deleteAnnotationAction = createAsyncThunk<string, string, AppThunkAPI>(
79
+ 'DELETE_ANNOTATION',
80
+ async (annotationId, { extra, getState, signal }) => {
81
+ const client = extra.api.getAnnotationsAPI();
82
+ const state = getState();
83
+ const fileId = getFileId(state);
84
+ const filePermissions = getPermissions(state);
85
+ const annotation = getAnnotation(state, annotationId);
86
+ const permissions = { ...filePermissions, ...annotation?.permissions };
87
+
88
+ signal.addEventListener('abort', () => {
89
+ client.destroy();
90
+ });
91
+
92
+ await new Promise<void>((resolve, reject) => {
93
+ client.deleteAnnotation(fileId, annotationId, permissions, resolve, reject);
94
+ });
95
+
96
+ return annotationId;
97
+ },
98
+ );
99
+
100
+ export const updateAnnotationAction = createAsyncThunk<
101
+ Annotation,
102
+ { annotationId: string; payload: { message?: string; status?: string } },
103
+ AppThunkAPI
104
+ >(
105
+ 'UPDATE_ANNOTATION',
106
+ async ({ annotationId, payload }, { extra, getState, signal }) => {
107
+ const client = extra.api.getAnnotationsAPI();
108
+ const state = getState();
109
+ const fileId = getFileId(state);
110
+ const filePermissions = getPermissions(state);
111
+ const existingAnnotation = getAnnotation(state, annotationId);
112
+ const permissions = { ...filePermissions, ...existingAnnotation?.permissions };
113
+
114
+ signal.addEventListener('abort', () => {
115
+ client.destroy();
116
+ });
117
+
118
+ return new Promise<Annotation>((resolve, reject) => {
119
+ client.updateAnnotation(fileId, annotationId, permissions, payload, resolve, reject);
45
120
  });
46
121
  },
47
122
  );
@@ -3,18 +3,27 @@ import { formatDrawing, isDrawing } from '../../drawing/drawingUtil';
3
3
  import { AnnotationsState } from './types';
4
4
  import {
5
5
  createAnnotationAction,
6
+ createReplyAction,
7
+ deleteAnnotationAction,
6
8
  fetchAnnotationsAction,
7
9
  removeAnnotationAction,
8
10
  setActiveAnnotationIdAction,
9
11
  setIsInitialized,
12
+ updateAnnotationAction,
10
13
  } from './actions';
14
+ import { setViewModeAction } from '../options/actions';
11
15
 
12
16
  const activeAnnotationId = createReducer<AnnotationsState['activeId']>(null, builder =>
13
17
  builder
14
18
  /* Preview will set the active ID with a an event which will trigger the url change */
15
19
  .addCase(createAnnotationAction.fulfilled, () => null)
20
+ .addCase(deleteAnnotationAction.fulfilled, (state, { payload: id }) => (state === id ? null : state))
16
21
  .addCase(removeAnnotationAction, (state, { payload: id }) => (state === id ? null : state))
17
- .addCase(setActiveAnnotationIdAction, (state, { payload: annotationId }) => annotationId),
22
+ .addCase(setActiveAnnotationIdAction, (state, { payload: annotationId }) => annotationId)
23
+ .addCase(setViewModeAction, (state, { payload }) => {
24
+ // Clear active annotation when switching to bounding boxes mode
25
+ return payload === 'boundingBoxes' ? null : state;
26
+ }),
18
27
  );
19
28
 
20
29
  const annotationsAllIds = createReducer<AnnotationsState['allIds']>([], builder =>
@@ -22,6 +31,7 @@ const annotationsAllIds = createReducer<AnnotationsState['allIds']>([], builder
22
31
  .addCase(createAnnotationAction.fulfilled, (state, { payload: { id } }) => {
23
32
  state.push(id);
24
33
  })
34
+ .addCase(deleteAnnotationAction.fulfilled, (state, { payload: id }) => state.filter(annotationId => annotationId !== id))
25
35
  .addCase(removeAnnotationAction, (state, { payload: id }) => state.filter(annotationId => annotationId !== id))
26
36
  .addCase(fetchAnnotationsAction.fulfilled, (state, { payload }) => {
27
37
  payload.entries.forEach(({ id }) => state.indexOf(id) === -1 && state.push(id));
@@ -33,9 +43,21 @@ const annotationsById = createReducer<AnnotationsState['byId']>({}, builder =>
33
43
  .addCase(createAnnotationAction.fulfilled, (state, { payload }) => {
34
44
  state[payload.id] = isDrawing(payload) ? formatDrawing(payload) : payload;
35
45
  })
46
+ .addCase(createReplyAction.fulfilled, (state, { payload: { annotationId, reply } }) => {
47
+ const annotation = state[annotationId];
48
+ if (annotation) {
49
+ annotation.replies = [...(annotation.replies ?? []), reply];
50
+ }
51
+ })
52
+ .addCase(deleteAnnotationAction.fulfilled, (state, { payload: id }) => {
53
+ delete state[id];
54
+ })
36
55
  .addCase(removeAnnotationAction, (state, { payload: id }) => {
37
56
  delete state[id];
38
57
  })
58
+ .addCase(updateAnnotationAction.fulfilled, (state, { payload }) => {
59
+ state[payload.id] = isDrawing(payload) ? formatDrawing(payload) : payload;
60
+ })
39
61
  .addCase(fetchAnnotationsAction.fulfilled, (state, { payload }) => {
40
62
  payload.entries.forEach(annotation => {
41
63
  state[annotation.id] = isDrawing(annotation) ? formatDrawing(annotation) : annotation;