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
@@ -39,7 +39,7 @@ object-assign
39
39
  * LICENSE file in the root directory of this source tree.
40
40
  */
41
41
 
42
- /** @license React v16.12.0
42
+ /** @license React v16.13.1
43
43
  * react-is.production.min.js
44
44
  *
45
45
  * Copyright (c) Facebook, Inc. and its affiliates.
package/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  import './dist/annotations.css';
2
- import './dist/annotations.js';
2
+ import BoxAnnotations from './dist/annotations.js';
3
3
 
4
4
  export default BoxAnnotations;
package/package.json CHANGED
@@ -1,9 +1,14 @@
1
1
  {
2
2
  "name": "box-annotations",
3
- "version": "5.2.1-beta.7",
3
+ "version": "5.3.0",
4
4
  "description": "Box Annotations",
5
5
  "author": "Box (https://www.box.com/)",
6
6
  "license": "SEE LICENSE IN LICENSE",
7
+ "sideEffects": [
8
+ "./dist/annotations.js",
9
+ "**/*.css",
10
+ "**/*.scss"
11
+ ],
7
12
  "repository": {
8
13
  "type": "git",
9
14
  "url": "git@github.com:box/box-annotations.git"
@@ -12,7 +17,7 @@
12
17
  "registry": "https://registry.npmjs.org/"
13
18
  },
14
19
  "engines": {
15
- "node": ">=18.x",
20
+ "node": ">=20.x",
16
21
  "yarn": ">=1.10.x"
17
22
  },
18
23
  "devDependencies": {
@@ -27,14 +32,17 @@
27
32
  "@babel/preset-env": "^7.25.9",
28
33
  "@babel/preset-react": "^7.25.9",
29
34
  "@babel/preset-typescript": "^7.25.9",
35
+ "@box/blueprint-web": "^16.12.0",
36
+ "@box/blueprint-web-assets": "^5.5.12",
37
+ "@box/collaboration-popover": "^2.1.32",
38
+ "@box/combobox-with-api": "^1.42.22",
30
39
  "@box/frontend": "^10.0.0",
31
- "@box/blueprint-web": "^14.12.2",
32
- "@box/blueprint-web-assets": "^4.114.3",
33
- "@box/collaboration-popover": "^1.60.2",
40
+ "@box/item-icon": "^3.2.0",
34
41
  "@box/languages": "^1.1.0",
35
- "@box/readable-time": "^1.39.52",
36
- "@box/threaded-annotations": "^1.80.0",
37
- "@box/user-selector": "^1.74.52",
42
+ "@box/readable-time": "^2.1.32",
43
+ "@box/threaded-annotations": "^4.11.0",
44
+ "@box/types": "^2.1.8",
45
+ "@box/user-selector": "^2.1.35",
38
46
  "@cfaester/enzyme-adapter-react-18": "^0.8.0",
39
47
  "@commitlint/cli": "^8.3.5",
40
48
  "@commitlint/config-conventional": "^8.2.0",
@@ -48,7 +56,7 @@
48
56
  "@types/classnames": "^2.2.10",
49
57
  "@types/draft-js": "^0.10.40",
50
58
  "@types/enzyme": "^3.10.5",
51
- "@types/jest": "^24.9.0",
59
+ "@types/jest": "^29.5.14",
52
60
  "@types/lodash": "^4.14.150",
53
61
  "@types/react": "18.3.12",
54
62
  "@types/react-dom": "18.3.1",
@@ -59,7 +67,7 @@
59
67
  "autoprefixer": "^10.4.19",
60
68
  "axios": "^0.24.0",
61
69
  "babel-eslint": "^10.1.0",
62
- "babel-jest": "^24.9.0",
70
+ "babel-jest": "^29.7.0",
63
71
  "babel-loader": "^9.2.1",
64
72
  "babel-plugin-istanbul": "^6.0.0",
65
73
  "babel-plugin-react-intl": "^7.5.1",
@@ -73,7 +81,7 @@
73
81
  "core-js": "^3.38.1",
74
82
  "css-loader": "^7.1.2",
75
83
  "css-minimizer-webpack-plugin": "^7.0.4",
76
- "cypress": "^4.4.1",
84
+ "cypress": "^15.20.1",
77
85
  "draft-js": "0.10.5",
78
86
  "enzyme": "^3.11.0",
79
87
  "enzyme-to-json": "^3.4.4",
@@ -93,9 +101,9 @@
93
101
  "eslint-plugin-react-hooks": "^4.6.0",
94
102
  "formik": "^2.1.4",
95
103
  "husky": "^3.1.0",
96
- "jest": "^24.9.0",
97
- "jest-canvas-mock": "^2.4.0",
98
- "jest-environment-jsdom-sixteen": "^1.0.3",
104
+ "jest": "^29.7.0",
105
+ "jest-canvas-mock": "^2.5.2",
106
+ "jest-environment-jsdom": "^29.7.0",
99
107
  "lint-staged": "^9.5.0",
100
108
  "lodash": "^4.17.15",
101
109
  "mini-css-extract-plugin": "^2.10.1",
@@ -121,19 +129,22 @@
121
129
  "tabbable": "^1.1.3",
122
130
  "typescript": "4.9.5",
123
131
  "uuid": "^8.3.1",
124
- "wait-on": "^4.0.2",
132
+ "wait-on": "^9.0.5",
125
133
  "webpack": "^5.105.4",
126
134
  "webpack-cli": "^5.1.4",
127
135
  "webpack-dev-server": "^5.2.3",
128
136
  "worker-farm": "^1.7.0"
129
137
  },
130
138
  "peerDependencies": {
131
- "@box/blueprint-web": "^14.12.2",
132
- "@box/blueprint-web-assets": "^4.114.3",
133
- "@box/collaboration-popover": "^1.60.2",
134
- "@box/readable-time": "^1.39.52",
135
- "@box/threaded-annotations": "^1.80.0",
136
- "@box/user-selector": "^1.74.52"
139
+ "@box/blueprint-web": "^16.12.0",
140
+ "@box/blueprint-web-assets": "^5.5.12",
141
+ "@box/collaboration-popover": "^2.1.32",
142
+ "@box/combobox-with-api": "^1.42.22",
143
+ "@box/item-icon": "^3.2.0",
144
+ "@box/readable-time": "^2.1.32",
145
+ "@box/threaded-annotations": "^4.11.0",
146
+ "@box/types": "^2.1.8",
147
+ "@box/user-selector": "^2.1.35"
137
148
  },
138
149
  "scripts": {
139
150
  "build": "yarn setup && yarn build:prod:dist",
@@ -143,7 +154,7 @@
143
154
  "clean": "rm -rf dist reports/* i18n/json i18n/*.js",
144
155
  "cy:open": "yarn cy:wait; yarn cypress open",
145
156
  "cy:run": "yarn cy:wait; yarn cypress run --spec test/integration/**/*.test.js",
146
- "cy:wait": "wait-on http-get://localhost:8001",
157
+ "cy:wait": "wait-on http-get://127.0.0.1:8001",
147
158
  "lint": "npm-run-all lint:*",
148
159
  "lint:js": "eslint .",
149
160
  "lint:scss": "stylelint \"src/**/*.scss\" --syntax scss",
package/src/@types/api.ts CHANGED
@@ -10,5 +10,6 @@ export interface Permissions {
10
10
  }
11
11
 
12
12
  export type TokenLiteral = null | undefined | string | { read: string; write?: string };
13
- export type TokenResolver = () => TokenLiteral | Promise<TokenLiteral>;
14
- export type Token = TokenLiteral | TokenResolver;
13
+ export type TokenMap = { [typedFileId: string]: TokenLiteral };
14
+ export type TokenResolver = (typedFileId?: string) => TokenLiteral | TokenMap | Promise<TokenLiteral | TokenMap>;
15
+ export type Token = TokenLiteral | TokenMap | TokenResolver;
@@ -1,19 +1,36 @@
1
1
  enum Event {
2
- ACTIVE_CHANGE = 'annotations_active_change',
2
+ // Inbound: emitted by the host; box-annotations listens.
3
3
  ACTIVE_SET = 'annotations_active_set',
4
- CREATOR_STAGED_CHANGE = 'creator_staged_change',
5
- CREATOR_STATUS_CHANGE = 'creator_status_change',
4
+ ANNOTATION_REMOVE = 'annotations_remove',
5
+ BOUNDING_BOX_HIGHLIGHTS_SET = 'bounding_box_highlights_set',
6
+ BOUNDING_BOX_HIGHLIGHT_SELECT = 'bounding_box_highlight_select',
7
+ COLOR_SET = 'annotations_color_set',
8
+ VIEW_MODE_SET = 'view_mode_set',
9
+ VISIBLE_SET = 'annotations_visible_set',
10
+
11
+ // Outbound: emitted by box-annotations; the host listens.
12
+ ACTIVE_CHANGE = 'annotations_active_change',
6
13
  ANNOTATION_CREATE = 'annotations_create',
14
+ ANNOTATION_DELETE = 'annotations_delete',
7
15
  ANNOTATION_FETCH_ERROR = 'annotations_fetch_error',
8
- ANNOTATION_REMOVE = 'annotations_remove',
16
+ ANNOTATION_REPLY_CREATE = 'annotations_reply_create',
17
+ ANNOTATION_REPLY_DELETE = 'annotations_reply_delete',
18
+ ANNOTATION_REPLY_UPDATE = 'annotations_reply_update',
19
+ ANNOTATION_UPDATE = 'annotations_update',
9
20
  ANNOTATIONS_INITIALIZED = 'annotations_initialized',
10
21
  ANNOTATIONS_MODE_CHANGE = 'annotations_mode_change',
11
- COLOR_SET = 'annotations_color_set',
12
- BOUNDING_BOX_HIGHLIGHTS_SET = 'bounding_box_highlights_set',
22
+ CREATOR_STAGED_CHANGE = 'creator_staged_change',
23
+ CREATOR_STATUS_CHANGE = 'creator_status_change',
24
+
25
+ // Internal: box-annotations emits and listens to itself (redux state change → DOM side effect).
13
26
  BOUNDING_BOX_HIGHLIGHT_NAVIGATE = 'bounding_box_highlight_navigate',
14
- BOUNDING_BOX_HIGHLIGHT_SELECT = 'bounding_box_highlight_select',
15
- VISIBLE_SET = 'annotations_visible_set',
16
- VIEW_MODE_SET = 'view_mode_set',
27
+ }
28
+
29
+ enum SidebarEvent {
30
+ SIDEBAR_ANNOTATION_UPDATE = 'sidebar.annotations_update',
31
+ SIDEBAR_REPLY_CREATE = 'sidebar.annotations_reply_create',
32
+ SIDEBAR_REPLY_DELETE = 'sidebar.annotations_reply_delete',
33
+ SIDEBAR_REPLY_UPDATE = 'sidebar.annotations_reply_update',
17
34
  }
18
35
 
19
36
  // Existing legacy events, don't rename
@@ -23,4 +40,4 @@ enum LegacyEvent {
23
40
  SCALE = 'scaleannotations',
24
41
  }
25
42
 
26
- export { Event, LegacyEvent };
43
+ export { Event, LegacyEvent, SidebarEvent };
@@ -1,12 +1,3 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-namespace */
2
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
3
- declare namespace NodeJS {
4
- interface Global {
5
- window: any;
6
- BoxAnnotations: any;
7
- }
8
- }
9
-
10
1
  declare module 'box-annotations-locale-data';
11
2
  declare module 'box-elements-messages';
12
3
  declare module 'box-ui-elements/es/*'; // TODO: Figure out why types don't register properly
@@ -86,15 +86,24 @@ export interface Rect extends Position {
86
86
  width: number;
87
87
  }
88
88
 
89
+ export interface ReplyPermissions {
90
+ can_delete?: boolean;
91
+ can_edit?: boolean;
92
+ can_reply?: boolean;
93
+ can_resolve?: boolean;
94
+ }
95
+
89
96
  export interface Reply {
90
97
  created_at: string;
91
98
  created_by: User;
92
99
  id: string;
93
100
  message: string;
101
+ modified_at?: string;
94
102
  parent: {
95
103
  id: string;
96
104
  type: string;
97
105
  };
106
+ permissions?: ReplyPermissions;
98
107
  type: 'reply';
99
108
  }
100
109
 
@@ -16,9 +16,11 @@ type Annotator = {
16
16
  type AnnotationsOptions = {
17
17
  features: Features;
18
18
  intl: IntlOptions;
19
+ onCopyLink?: (params: { annotationId: string; fileVersionId: string }) => void;
19
20
  };
20
21
 
21
22
  export type Features = {
23
+ isRichTextEnabled?: boolean;
22
24
  isThreadedAnnotation?: boolean;
23
25
  [key: string]: boolean | undefined;
24
26
  };
@@ -43,6 +45,8 @@ type ViewerOption = {
43
45
 
44
46
  type ViewerOptions = Record<string, ViewerOption>;
45
47
 
48
+ type GlobalWithBoxAnnotations = typeof globalThis & { BoxAnnotations: typeof BoxAnnotations };
49
+
46
50
  /**
47
51
  * NAME: The name of the annotator.
48
52
  * CONSTRUCTOR: Constructor for the annotator.
@@ -145,5 +149,5 @@ class BoxAnnotations {
145
149
  }
146
150
  }
147
151
 
148
- global.BoxAnnotations = BoxAnnotations;
152
+ (globalThis as GlobalWithBoxAnnotations).BoxAnnotations = BoxAnnotations;
149
153
  export default BoxAnnotations;
@@ -1,4 +1,5 @@
1
1
  import type { DocumentNodeV2, MentionNodeV2, ParagraphNodeV2, TextMessageTypeV2, TextNodeV2 } from '@box/threaded-annotations';
2
+ import { parseMessageMarkdown } from '@box/threaded-annotations';
2
3
 
3
4
  import type { Annotation, Collaborator, Reply, UserMini } from '../@types';
4
5
 
@@ -69,10 +70,27 @@ export const deserializeMentionMarkup = (text: string): DocumentNodeV2 => {
69
70
  return { type: 'doc', content };
70
71
  };
71
72
 
73
+ const toDocumentNode = (text: string, isRichTextEnabled = false): DocumentNodeV2 =>
74
+ isRichTextEnabled ? (parseMessageMarkdown(text) as DocumentNodeV2) : deserializeMentionMarkup(text);
75
+
76
+ /**
77
+ * Returns the edit timestamp consumers use to render an edited indicator.
78
+ * Compares parsed instants, not raw strings, so equivalent ISO formats
79
+ * (Z vs +00:00, fractional precision) are treated as unedited.
80
+ */
81
+ const toUpdatedAt = (createdAt: string, modifiedAt: string | undefined): number | undefined => {
82
+ if (!modifiedAt) return undefined;
83
+ const modifiedMs = new Date(modifiedAt).getTime();
84
+ if (Number.isNaN(modifiedMs)) return undefined;
85
+ const createdMs = new Date(createdAt).getTime();
86
+ if (modifiedMs === createdMs) return undefined;
87
+ return modifiedMs;
88
+ };
89
+
72
90
  /**
73
91
  * Converts a box-annotations Reply to a threaded-annotations TextMessageType.
74
92
  */
75
- export const replyToTextMessage = (reply: Reply): TextMessageTypeV2 => ({
93
+ export const replyToTextMessage = (reply: Reply, isRichTextEnabled = false): TextMessageTypeV2 => ({
76
94
  author: {
77
95
  email: reply.created_by?.login ?? '',
78
96
  id: parseInt(reply.created_by?.id ?? '0', 10),
@@ -80,55 +98,50 @@ export const replyToTextMessage = (reply: Reply): TextMessageTypeV2 => ({
80
98
  },
81
99
  createdAt: new Date(reply.created_at).getTime(),
82
100
  id: reply.id,
83
- message: deserializeMentionMarkup(reply.message),
101
+ message: toDocumentNode(reply.message, isRichTextEnabled),
84
102
  permissions: {
85
- canDelete: false,
86
- canEdit: false,
87
- canReply: true,
88
- canResolve: false,
103
+ canDelete: reply.permissions?.can_delete ?? false,
104
+ canEdit: reply.permissions?.can_edit ?? false,
105
+ canReply: reply.permissions?.can_reply ?? false,
106
+ canResolve: reply.permissions?.can_resolve ?? false,
89
107
  },
108
+ updatedAt: toUpdatedAt(reply.created_at, reply.modified_at),
90
109
  });
91
110
 
92
- /**
93
- * Converts an Annotation's description (root message) to a TextMessageType.
94
- * The root message gets resolve permission from the annotation's permissions.
95
- * Falls back to annotation-level fields when the description object is sparse
96
- * (the list endpoint returns description with only { message }).
97
- */
98
- const descriptionToTextMessage = (
99
- annotation: Annotation,
100
- reply: Reply,
101
- ): TextMessageTypeV2 => ({
111
+ // The root message shares the annotation's author and permissions; description
112
+ // comes back sparse ({ message } only) from the list endpoint.
113
+ const descriptionToTextMessage = (annotation: Annotation, isRichTextEnabled = false): TextMessageTypeV2 => ({
102
114
  author: {
103
- email: (reply.created_by ?? annotation.created_by)?.login ?? '',
104
- id: parseInt((reply.created_by ?? annotation.created_by)?.id ?? '0', 10),
105
- name: (reply.created_by ?? annotation.created_by)?.name ?? '',
115
+ email: annotation.created_by?.login ?? '',
116
+ id: parseInt(annotation.created_by?.id ?? '0', 10),
117
+ name: annotation.created_by?.name ?? '',
106
118
  },
107
- createdAt: new Date(reply.created_at ?? annotation.created_at).getTime(),
108
- id: reply.id ?? annotation.id,
109
- message: deserializeMentionMarkup(reply.message),
119
+ createdAt: new Date(annotation.created_at).getTime(),
120
+ id: annotation.id,
121
+ message: toDocumentNode(annotation.description?.message ?? '', isRichTextEnabled),
110
122
  permissions: {
111
123
  canDelete: annotation.permissions?.can_delete ?? false,
112
124
  canEdit: annotation.permissions?.can_edit ?? false,
113
- canReply: true,
125
+ canReply: annotation.permissions?.can_reply ?? false,
114
126
  canResolve: annotation.permissions?.can_resolve ?? false,
115
127
  },
128
+ updatedAt: toUpdatedAt(annotation.created_at, annotation.modified_at),
116
129
  });
117
130
 
118
131
  /**
119
132
  * Maps a full Annotation (description + replies) to an array of TextMessageType
120
133
  * suitable for ThreadedAnnotationsV2.
121
134
  */
122
- export const annotationToMessages = (annotation: Annotation): TextMessageTypeV2[] => {
135
+ export const annotationToMessages = (annotation: Annotation, isRichTextEnabled = false): TextMessageTypeV2[] => {
123
136
  const messages: TextMessageTypeV2[] = [];
124
137
 
125
138
  if (annotation.description) {
126
- messages.push(descriptionToTextMessage(annotation, annotation.description));
139
+ messages.push(descriptionToTextMessage(annotation, isRichTextEnabled));
127
140
  }
128
141
 
129
142
  if (annotation.replies) {
130
143
  annotation.replies.forEach(reply => {
131
- messages.push(replyToTextMessage(reply));
144
+ messages.push(replyToTextMessage(reply, isRichTextEnabled));
132
145
  });
133
146
  }
134
147
 
@@ -1,7 +1,8 @@
1
1
  import Annotations from 'box-ui-elements/es/api/Annotations';
2
2
  import FileCollaborators from 'box-ui-elements/es/api/FileCollaborators';
3
+ import ThreadedComments from 'box-ui-elements/es/api/ThreadedComments';
3
4
  import { DEFAULT_HOSTNAME_API } from 'box-ui-elements/es/constants';
4
- import { AnnotationsAPI, CollaboratorsAPI, APIOptions } from './types';
5
+ import { AnnotationsAPI, CollaboratorsAPI, APIOptions, ThreadedCommentsAPI } from './types';
5
6
 
6
7
  export default class APIFactory {
7
8
  options: APIOptions;
@@ -21,4 +22,8 @@ export default class APIFactory {
21
22
  getCollaboratorsAPI(): CollaboratorsAPI {
22
23
  return new FileCollaborators(this.options);
23
24
  }
25
+
26
+ getThreadedCommentsAPI(): ThreadedCommentsAPI {
27
+ return new ThreadedComments(this.options);
28
+ }
24
29
  }
package/src/api/types.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Annotation, Collaborator, NewAnnotation, Permissions, Reply, Token } from '../@types';
1
+ import { Annotation, Collaborator, NewAnnotation, Permissions, Reply, ReplyPermissions, Token } from '../@types';
2
2
 
3
3
  export type APICollection<R> = {
4
4
  entries: R[];
@@ -73,6 +73,28 @@ export interface AnnotationsAPI {
73
73
  destroy(): void;
74
74
  }
75
75
 
76
+ export interface ThreadedCommentsAPI {
77
+ deleteComment(args: {
78
+ commentId: string;
79
+ errorCallback: (error: APIError) => void;
80
+ fileId: string | null;
81
+ permissions: ReplyPermissions;
82
+ successCallback: () => void;
83
+ }): void;
84
+
85
+ updateComment(args: {
86
+ commentId: string;
87
+ errorCallback: (error: APIError) => void;
88
+ fileId: string | null;
89
+ message?: string;
90
+ permissions: ReplyPermissions;
91
+ status?: string;
92
+ successCallback: (reply: Reply) => void;
93
+ }): void;
94
+
95
+ destroy(): void;
96
+ }
97
+
76
98
  export interface CollaboratorsAPI {
77
99
  getFileCollaborators(
78
100
  fileId: string | null,
@@ -0,0 +1,7 @@
1
+ import { createContext } from 'react';
2
+
3
+ export type AnnotationCallbacks = {
4
+ onCopyLink?: (params: { annotationId: string; fileVersionId: string }) => void;
5
+ };
6
+
7
+ export default createContext<AnnotationCallbacks>({});
@@ -7,7 +7,8 @@
7
7
  @import '~box-ui-elements/es/styles/common/buttons';
8
8
 
9
9
  &.is-hidden {
10
- .ba-Layer {
10
+ .ba-Layer,
11
+ .ba-PopupPortal {
11
12
  display: none;
12
13
  }
13
14
  }
@@ -27,3 +28,15 @@
27
28
  .ba-Layer--popup {
28
29
  z-index: 4;
29
30
  }
31
+
32
+ .ba-PopupPortal {
33
+ position: relative;
34
+ z-index: 10;
35
+ pointer-events: none;
36
+
37
+ .ba-Popup,
38
+ .ba-PopupV2 {
39
+ z-index: 150;
40
+ pointer-events: auto;
41
+ }
42
+ }
@@ -6,7 +6,7 @@ import DeselectManager from './DeselectManager';
6
6
  import EventEmitter from './EventEmitter';
7
7
  import i18n from '../utils/i18n';
8
8
  import messages from '../messages';
9
- import { Event, IntlOptions, LegacyEvent, Permissions } from '../@types';
9
+ import { Event, IntlOptions, LegacyEvent, Permissions, SidebarEvent, Token } from '../@types';
10
10
  import { BoundingBox, getBoundingBoxHighlights } from '../store/boundingBoxHighlights';
11
11
  import { ViewMode } from '../store/options/types';
12
12
  import { Features } from '../BoxAnnotations';
@@ -44,11 +44,13 @@ export type Options = {
44
44
  initialViewMode?: ViewMode;
45
45
  intl: IntlOptions;
46
46
  locale?: string;
47
- token: string;
47
+ onCopyLink?: (params: { annotationId: string; fileVersionId: string }) => void;
48
+ token: Token;
48
49
  };
49
50
 
50
51
  export const CSS_CONTAINER_CLASS = 'ba';
51
52
  export const CSS_LOADED_CLASS = 'ba-annotations-loaded';
53
+ export const CSS_POPUP_PORTAL_CLASS = 'ba-PopupPortal';
52
54
  export const ANNOTATION_CLASSES: { [M in store.Mode]?: string } = {
53
55
  [store.Mode.HIGHLIGHT]: 'ba-is-create--highlight',
54
56
  [store.Mode.REGION]: 'ba-is-create--region',
@@ -59,6 +61,8 @@ export default class BaseAnnotator extends EventEmitter {
59
61
 
60
62
  containerEl?: HTMLElement | null;
61
63
 
64
+ popupPortalEl?: HTMLElement | null;
65
+
62
66
  container: Container;
63
67
 
64
68
  deselectManager: DeselectManager | null = null;
@@ -67,6 +71,8 @@ export default class BaseAnnotator extends EventEmitter {
67
71
 
68
72
  intl: IntlShape;
69
73
 
74
+ onCopyLink?: (params: { annotationId: string; fileVersionId: string }) => void;
75
+
70
76
  store: store.AppStore;
71
77
 
72
78
  constructor({
@@ -79,6 +85,7 @@ export default class BaseAnnotator extends EventEmitter {
79
85
  initialMode,
80
86
  initialViewMode = 'annotations',
81
87
  intl,
88
+ onCopyLink,
82
89
  token,
83
90
  }: Options) {
84
91
  super();
@@ -108,6 +115,7 @@ export default class BaseAnnotator extends EventEmitter {
108
115
  this.container = container;
109
116
  this.features = features;
110
117
  this.intl = i18n.createIntlProvider(intl);
118
+ this.onCopyLink = onCopyLink;
111
119
  this.store = store.createStore(initialState, {
112
120
  api: new API({ apiHost, token }),
113
121
  });
@@ -123,6 +131,11 @@ export default class BaseAnnotator extends EventEmitter {
123
131
  this.addListener(Event.BOUNDING_BOX_HIGHLIGHT_SELECT, this.handleSelectBoundingBoxHighlight);
124
132
  this.addListener(Event.VIEW_MODE_SET, this.handleSetViewMode);
125
133
 
134
+ this.addListener(SidebarEvent.SIDEBAR_ANNOTATION_UPDATE, this.handleSidebarAnnotationUpdate);
135
+ this.addListener(SidebarEvent.SIDEBAR_REPLY_CREATE, this.handleSidebarReplyCreate);
136
+ this.addListener(SidebarEvent.SIDEBAR_REPLY_UPDATE, this.handleSidebarReplyUpdate);
137
+ this.addListener(SidebarEvent.SIDEBAR_REPLY_DELETE, this.handleSidebarReplyDelete);
138
+
126
139
  // Load any required data at startup
127
140
  this.hydrate();
128
141
  }
@@ -132,6 +145,11 @@ export default class BaseAnnotator extends EventEmitter {
132
145
  this.containerEl.classList.remove(CSS_CONTAINER_CLASS);
133
146
  }
134
147
 
148
+ if (this.popupPortalEl) {
149
+ this.popupPortalEl.remove();
150
+ this.popupPortalEl = null;
151
+ }
152
+
135
153
  if (this.annotatedEl) {
136
154
  this.annotatedEl.classList.remove(CSS_LOADED_CLASS);
137
155
  }
@@ -151,6 +169,10 @@ export default class BaseAnnotator extends EventEmitter {
151
169
  this.removeListener(Event.BOUNDING_BOX_HIGHLIGHT_NAVIGATE, this.handleNavigateBoundingBoxHighlight);
152
170
  this.removeListener(Event.BOUNDING_BOX_HIGHLIGHT_SELECT, this.handleSelectBoundingBoxHighlight);
153
171
  this.removeListener(Event.VIEW_MODE_SET, this.handleSetViewMode);
172
+ this.removeListener(SidebarEvent.SIDEBAR_ANNOTATION_UPDATE, this.handleSidebarAnnotationUpdate);
173
+ this.removeListener(SidebarEvent.SIDEBAR_REPLY_CREATE, this.handleSidebarReplyCreate);
174
+ this.removeListener(SidebarEvent.SIDEBAR_REPLY_UPDATE, this.handleSidebarReplyUpdate);
175
+ this.removeListener(SidebarEvent.SIDEBAR_REPLY_DELETE, this.handleSidebarReplyDelete);
154
176
  }
155
177
 
156
178
  public init(scale = 1, rotation = 0): void {
@@ -166,6 +188,12 @@ export default class BaseAnnotator extends EventEmitter {
166
188
  this.annotatedEl.classList.add(CSS_LOADED_CLASS);
167
189
  this.containerEl.classList.add(CSS_CONTAINER_CLASS);
168
190
 
191
+ if (!this.popupPortalEl) {
192
+ this.popupPortalEl = this.containerEl.ownerDocument.createElement('div');
193
+ this.popupPortalEl.classList.add(CSS_POPUP_PORTAL_CLASS);
194
+ this.containerEl.appendChild(this.popupPortalEl);
195
+ }
196
+
169
197
  // Defer to the child class to render annotations
170
198
  this.render();
171
199
 
@@ -338,6 +366,22 @@ export default class BaseAnnotator extends EventEmitter {
338
366
  this.setViewMode(viewMode);
339
367
  };
340
368
 
369
+ protected handleSidebarAnnotationUpdate = (annotation: store.SidebarAnnotationUpdatePayload): void => {
370
+ this.store.dispatch(store.applySidebarAnnotationUpdateAction(annotation));
371
+ };
372
+
373
+ protected handleSidebarReplyCreate = (payload: store.SidebarReplyMutationPayload): void => {
374
+ this.store.dispatch(store.applySidebarReplyCreateAction(payload));
375
+ };
376
+
377
+ protected handleSidebarReplyUpdate = (payload: store.SidebarReplyMutationPayload): void => {
378
+ this.store.dispatch(store.applySidebarReplyUpdateAction(payload));
379
+ };
380
+
381
+ protected handleSidebarReplyDelete = ({ annotationId, id }: { annotationId: string; id: string }): void => {
382
+ this.store.dispatch(store.applySidebarReplyDeleteAction({ annotationId, replyId: id }));
383
+ };
384
+
341
385
  protected hydrate(): void {
342
386
  // Redux dispatch method signature doesn't seem to like async actions
343
387
  this.store.dispatch<any>(store.fetchAnnotationsAction()); // eslint-disable-line @typescript-eslint/no-explicit-any
@@ -1,6 +1,7 @@
1
1
  import * as ReactDOM from 'react-dom/client';
2
2
  import { IntlShape } from 'react-intl';
3
3
  import { Store } from 'redux';
4
+ import { AnnotationCallbacks } from './AnnotationCallbacksContext';
4
5
  import { applyResinTags } from '../utils/resin';
5
6
  import { TARGET_TYPE } from '../constants';
6
7
 
@@ -14,6 +15,7 @@ export type Options = {
14
15
  };
15
16
 
16
17
  export type Props = {
18
+ callbacks?: AnnotationCallbacks;
17
19
  intl: IntlShape;
18
20
  store: Store;
19
21
  };
@@ -6,7 +6,9 @@ export default function DeselectListener(): null {
6
6
  const dispatch = useDispatch();
7
7
 
8
8
  React.useEffect(() => {
9
- const handleMouseDown = (): void => {
9
+ const handleMouseDown = (event: MouseEvent): void => {
10
+ // Popup is portaled outside .ba-Layer; native mousedown still bubbles here.
11
+ if ((event.target as Element | null)?.closest?.('.ba-PopupV2, .ba-Popup')) return;
10
12
  dispatch(setActiveAnnotationIdAction(null));
11
13
  };
12
14
 
@@ -2,8 +2,10 @@ import * as React from 'react';
2
2
  import { IntlShape, RawIntlProvider } from 'react-intl';
3
3
  import { Provider as StoreProvider } from 'react-redux';
4
4
  import { Store } from 'redux';
5
+ import AnnotationCallbacksContext, { AnnotationCallbacks } from './AnnotationCallbacksContext';
5
6
 
6
7
  type WrapperProps = {
8
+ callbacks?: AnnotationCallbacks;
7
9
  intl: IntlShape;
8
10
  store: Store;
9
11
  };
@@ -11,11 +13,13 @@ type WrapperProps = {
11
13
  type WrapperReturn<P> = React.FC<P & WrapperProps>;
12
14
 
13
15
  export default function withProviders<P extends object>(WrappedComponent: React.ComponentType<P>): WrapperReturn<P> {
14
- return function RootProvider({ intl, store, ...rest }: P & WrapperProps): JSX.Element {
16
+ return function RootProvider({ callbacks, intl, store, ...rest }: P & WrapperProps): JSX.Element {
15
17
  return (
16
18
  <RawIntlProvider value={intl}>
17
19
  <StoreProvider store={store}>
18
- <WrappedComponent {...(rest as P)} />
20
+ <AnnotationCallbacksContext.Provider value={callbacks ?? {}}>
21
+ <WrappedComponent {...(rest as P)} />
22
+ </AnnotationCallbacksContext.Provider>
19
23
  </StoreProvider>
20
24
  </RawIntlProvider>
21
25
  );