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

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "box-annotations",
3
- "version": "5.2.1-beta.11",
3
+ "version": "5.2.1-beta.13",
4
4
  "description": "Box Annotations",
5
5
  "author": "Box (https://www.box.com/)",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -28,13 +28,13 @@
28
28
  "@babel/preset-react": "^7.25.9",
29
29
  "@babel/preset-typescript": "^7.25.9",
30
30
  "@box/frontend": "^10.0.0",
31
- "@box/blueprint-web": "^14.27.0",
32
- "@box/blueprint-web-assets": "^4.116.6",
33
- "@box/collaboration-popover": "^1.61.30",
31
+ "@box/blueprint-web": "^14.30.0",
32
+ "@box/blueprint-web-assets": "^4.117.2",
33
+ "@box/collaboration-popover": "^1.62.3",
34
34
  "@box/languages": "^1.1.0",
35
- "@box/readable-time": "^1.40.30",
36
- "@box/threaded-annotations": "^1.86.0",
37
- "@box/user-selector": "^1.75.30",
35
+ "@box/readable-time": "^1.41.3",
36
+ "@box/threaded-annotations": "^1.89.1",
37
+ "@box/user-selector": "^1.76.3",
38
38
  "@cfaester/enzyme-adapter-react-18": "^0.8.0",
39
39
  "@commitlint/cli": "^8.3.5",
40
40
  "@commitlint/config-conventional": "^8.2.0",
@@ -128,12 +128,12 @@
128
128
  "worker-farm": "^1.7.0"
129
129
  },
130
130
  "peerDependencies": {
131
- "@box/blueprint-web": "^14.27.0",
132
- "@box/blueprint-web-assets": "^4.116.6",
133
- "@box/collaboration-popover": "^1.61.30",
134
- "@box/readable-time": "^1.40.30",
135
- "@box/threaded-annotations": "^1.86.0",
136
- "@box/user-selector": "^1.75.30"
131
+ "@box/blueprint-web": "^14.30.0",
132
+ "@box/blueprint-web-assets": "^4.117.2",
133
+ "@box/collaboration-popover": "^1.62.3",
134
+ "@box/readable-time": "^1.41.3",
135
+ "@box/threaded-annotations": "^1.89.1",
136
+ "@box/user-selector": "^1.76.3"
137
137
  },
138
138
  "scripts": {
139
139
  "build": "yarn setup && yarn build:prod:dist",
@@ -86,6 +86,13 @@ 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;
@@ -95,6 +102,7 @@ export interface Reply {
95
102
  id: string;
96
103
  type: string;
97
104
  };
105
+ permissions?: ReplyPermissions;
98
106
  type: 'reply';
99
107
  }
100
108
 
@@ -16,6 +16,7 @@ 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 = {
@@ -82,35 +82,28 @@ export const replyToTextMessage = (reply: Reply): TextMessageTypeV2 => ({
82
82
  id: reply.id,
83
83
  message: deserializeMentionMarkup(reply.message),
84
84
  permissions: {
85
- canDelete: false,
85
+ canDelete: reply.permissions?.can_delete ?? false,
86
86
  canEdit: false,
87
- canReply: true,
88
- canResolve: false,
87
+ canReply: reply.permissions?.can_reply ?? false,
88
+ canResolve: reply.permissions?.can_resolve ?? false,
89
89
  },
90
90
  });
91
91
 
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 => ({
92
+ // The root message shares the annotation's author and permissions; description
93
+ // comes back sparse ({ message } only) from the list endpoint.
94
+ const descriptionToTextMessage = (annotation: Annotation): TextMessageTypeV2 => ({
102
95
  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 ?? '',
96
+ email: annotation.created_by?.login ?? '',
97
+ id: parseInt(annotation.created_by?.id ?? '0', 10),
98
+ name: annotation.created_by?.name ?? '',
106
99
  },
107
- createdAt: new Date(reply.created_at ?? annotation.created_at).getTime(),
108
- id: reply.id ?? annotation.id,
109
- message: deserializeMentionMarkup(reply.message),
100
+ createdAt: new Date(annotation.created_at).getTime(),
101
+ id: annotation.id,
102
+ message: deserializeMentionMarkup(annotation.description?.message ?? ''),
110
103
  permissions: {
111
104
  canDelete: annotation.permissions?.can_delete ?? false,
112
105
  canEdit: annotation.permissions?.can_edit ?? false,
113
- canReply: true,
106
+ canReply: annotation.permissions?.can_reply ?? false,
114
107
  canResolve: annotation.permissions?.can_resolve ?? false,
115
108
  },
116
109
  });
@@ -123,7 +116,7 @@ export const annotationToMessages = (annotation: Annotation): TextMessageTypeV2[
123
116
  const messages: TextMessageTypeV2[] = [];
124
117
 
125
118
  if (annotation.description) {
126
- messages.push(descriptionToTextMessage(annotation, annotation.description));
119
+ messages.push(descriptionToTextMessage(annotation));
127
120
  }
128
121
 
129
122
  if (annotation.replies) {
@@ -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>({});
@@ -28,3 +28,14 @@
28
28
  .ba-Layer--popup {
29
29
  z-index: 4;
30
30
  }
31
+
32
+ .ba-PopupPortal {
33
+ position: relative;
34
+ z-index: 10;
35
+ pointer-events: none;
36
+
37
+ .ba-Popup {
38
+ z-index: 150;
39
+ pointer-events: auto;
40
+ }
41
+ }
@@ -44,6 +44,7 @@ export type Options = {
44
44
  initialViewMode?: ViewMode;
45
45
  intl: IntlOptions;
46
46
  locale?: string;
47
+ onCopyLink?: (params: { annotationId: string; fileVersionId: string }) => void;
47
48
  token: string;
48
49
  };
49
50
 
@@ -70,6 +71,8 @@ export default class BaseAnnotator extends EventEmitter {
70
71
 
71
72
  intl: IntlShape;
72
73
 
74
+ onCopyLink?: (params: { annotationId: string; fileVersionId: string }) => void;
75
+
73
76
  store: store.AppStore;
74
77
 
75
78
  constructor({
@@ -82,6 +85,7 @@ export default class BaseAnnotator extends EventEmitter {
82
85
  initialMode,
83
86
  initialViewMode = 'annotations',
84
87
  intl,
88
+ onCopyLink,
85
89
  token,
86
90
  }: Options) {
87
91
  super();
@@ -111,6 +115,7 @@ export default class BaseAnnotator extends EventEmitter {
111
115
  this.container = container;
112
116
  this.features = features;
113
117
  this.intl = i18n.createIntlProvider(intl);
118
+ this.onCopyLink = onCopyLink;
114
119
  this.store = store.createStore(initialState, {
115
120
  api: new API({ apiHost, token }),
116
121
  });
@@ -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
  };
@@ -8,7 +8,7 @@ export default function DeselectListener(): null {
8
8
  React.useEffect(() => {
9
9
  const handleMouseDown = (event: MouseEvent): void => {
10
10
  // Popup is portaled outside .ba-Layer; native mousedown still bubbles here.
11
- if ((event.target as Element | null)?.closest?.('.ba-PopupV2')) return;
11
+ if ((event.target as Element | null)?.closest?.('.ba-PopupV2, .ba-Popup')) return;
12
12
  dispatch(setActiveAnnotationIdAction(null));
13
13
  };
14
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
  );
@@ -33,12 +33,6 @@ export type Props = {
33
33
 
34
34
  const options: Partial<Options> = {
35
35
  modifiers: [
36
- {
37
- name: 'eventListeners',
38
- options: {
39
- scroll: false,
40
- },
41
- },
42
36
  {
43
37
  name: 'offset',
44
38
  options: {
@@ -48,7 +42,6 @@ const options: Partial<Options> = {
48
42
  {
49
43
  name: 'preventOverflow',
50
44
  options: {
51
- altAxis: true,
52
45
  padding: 50,
53
46
  },
54
47
  },
@@ -17,6 +17,7 @@ import type { JSONContent } from '@tiptap/core';
17
17
  import FocusTrap from 'box-ui-elements/es/components/focus-trap/FocusTrap';
18
18
 
19
19
  import { annotationToMessages, collaboratorToUserContact } from '../../adapters/threadedAnnotationsAdapters';
20
+ import AnnotationCallbacksContext from '../../common/AnnotationCallbacksContext';
20
21
  import {
21
22
  createReplyAction,
22
23
  deleteAnnotationAction,
@@ -24,7 +25,7 @@ import {
24
25
  updateAnnotationAction,
25
26
  } from '../../store/annotations/actions';
26
27
  import { getAnnotation } from '../../store/annotations/selectors';
27
- import { getApiHost, getToken } from '../../store/options';
28
+ import { getApiHost, getFileVersionId, getToken } from '../../store/options';
28
29
  import { fetchCollaboratorsAction } from '../../store/users/actions';
29
30
 
30
31
  import type { AppState, AppThunkDispatch } from '../../store/types';
@@ -80,12 +81,21 @@ const fetchAvatarBlob = async (apiHost: string, token: string, userId: string):
80
81
  const PopupV2 = ({ annotationId, onSubmit, popupPortalEl, reference }: Props): JSX.Element | null => {
81
82
  const intl = useIntl();
82
83
  const dispatch = useDispatch<AppThunkDispatch>();
84
+ const { onCopyLink: consumerOnCopyLink } = React.useContext(AnnotationCallbacksContext);
83
85
  const popupRef = React.useRef<HTMLDivElement>(null);
84
86
  const popperRef = React.useRef<Instance>();
85
87
  const optionsRef = React.useRef<Partial<Options>>(getPopupOptions());
86
88
 
87
89
  const apiHost = useSelector(getApiHost);
90
+ const fileVersionId = useSelector(getFileVersionId);
88
91
  const token = useSelector(getToken);
92
+ const onCopyLink = React.useMemo(
93
+ () =>
94
+ consumerOnCopyLink && annotationId && fileVersionId
95
+ ? () => consumerOnCopyLink({ annotationId, fileVersionId })
96
+ : undefined,
97
+ [consumerOnCopyLink, annotationId, fileVersionId],
98
+ );
89
99
  const annotation = useSelector((state: AppState) =>
90
100
  annotationId ? getAnnotation(state, annotationId) : undefined,
91
101
  );
@@ -235,6 +245,16 @@ const PopupV2 = ({ annotationId, onSubmit, popupPortalEl, reference }: Props): J
235
245
  [annotationId, dispatch, onSubmit],
236
246
  );
237
247
 
248
+ const handleEdit = React.useCallback(
249
+ async (id: string, content: JSONContent | null): Promise<void> => {
250
+ if (!annotationId || id !== annotationId) return;
251
+ const doc = createDocumentNode(content);
252
+ const { text } = serializeMentionMarkup(doc);
253
+ await dispatch(updateAnnotationAction({ annotationId, payload: { message: text } }));
254
+ },
255
+ [annotationId, dispatch],
256
+ );
257
+
238
258
  const handleThreadDelete = React.useCallback(
239
259
  async (): Promise<void> => {
240
260
  if (annotationId) {
@@ -293,7 +313,9 @@ const PopupV2 = ({ annotationId, onSubmit, popupPortalEl, reference }: Props): J
293
313
  isResolved={isResolved}
294
314
  messages={threadMessages}
295
315
  onAvatarClick={noop}
316
+ onCopyLink={onCopyLink}
296
317
  onDelete={noop}
318
+ onEdit={handleEdit}
297
319
  onPost={handlePost}
298
320
  onResolve={handleResolve}
299
321
  onThreadDelete={handleThreadDelete}
@@ -103,7 +103,7 @@ export default class DocumentAnnotator extends BaseAnnotator {
103
103
 
104
104
  // Annotations mode
105
105
  managers.add(new PopupManager({ location: pageNumber, popupPortalEl: this.popupPortalEl, referenceEl: pageReferenceEl, resinTags }));
106
- managers.add(new DrawingManager({ location: pageNumber, referenceEl: pageReferenceEl, resinTags }));
106
+ managers.add(new DrawingManager({ location: pageNumber, popupPortalEl: this.popupPortalEl, referenceEl: pageReferenceEl, resinTags }));
107
107
 
108
108
  const textLayer = pageEl.querySelector('.textLayer') as HTMLElement;
109
109
 
@@ -224,6 +224,7 @@ export default class DocumentAnnotator extends BaseAnnotator {
224
224
  }
225
225
 
226
226
  manager.render({
227
+ callbacks: { onCopyLink: this.onCopyLink },
227
228
  intl: this.intl,
228
229
  store: this.store,
229
230
  });
@@ -1,4 +1,5 @@
1
1
  import React from 'react';
2
+ import ReactDOM from 'react-dom';
2
3
  import classNames from 'classnames';
3
4
  import DecoratedDrawingPath from './DecoratedDrawingPath';
4
5
  import DrawingList from './DrawingList';
@@ -22,6 +23,7 @@ export type Props = {
22
23
  drawnPathGroups: Array<PathGroup>;
23
24
  isCreating: boolean;
24
25
  location: number;
26
+ popupPortalEl?: HTMLElement | null;
25
27
  redoDrawingPathGroup: () => void;
26
28
  resetDrawing: () => void;
27
29
  rotation: number;
@@ -46,6 +48,7 @@ const DrawingAnnotations = (props: Props): JSX.Element => {
46
48
  drawnPathGroups,
47
49
  isCreating,
48
50
  location,
51
+ popupPortalEl,
49
52
  redoDrawingPathGroup,
50
53
  resetDrawing,
51
54
  rotation,
@@ -176,20 +179,22 @@ const DrawingAnnotations = (props: Props): JSX.Element => {
176
179
  />
177
180
  )}
178
181
 
179
- {isCreating && hasPathGroups && drawingSVGGroup && canShowPopupToolbar && (
180
- <PopupDrawingToolbar
181
- ref={popupDrawingToolbarRef}
182
- canComment={hasDrawnPathGroups}
183
- canRedo={hasStashedPathGroups}
184
- canUndo={hasDrawnPathGroups}
185
- className={classNames('ba-DrawingAnnotations-toolbar', { 'ba-is-drawing': isDrawing })}
186
- onDelete={handleDelete}
187
- onRedo={handleRedo}
188
- onReply={handleReply}
189
- onUndo={handleUndo}
190
- reference={drawingSVGGroup}
191
- />
192
- )}
182
+ {isCreating && hasPathGroups && drawingSVGGroup && canShowPopupToolbar && popupPortalEl &&
183
+ ReactDOM.createPortal(
184
+ <PopupDrawingToolbar
185
+ ref={popupDrawingToolbarRef}
186
+ canComment={hasDrawnPathGroups}
187
+ canRedo={hasStashedPathGroups}
188
+ canUndo={hasDrawnPathGroups}
189
+ className={classNames('ba-DrawingAnnotations-toolbar', { 'ba-is-drawing': isDrawing })}
190
+ onDelete={handleDelete}
191
+ onRedo={handleRedo}
192
+ onReply={handleReply}
193
+ onUndo={handleUndo}
194
+ reference={drawingSVGGroup}
195
+ />,
196
+ popupPortalEl,
197
+ )}
193
198
  </>
194
199
  );
195
200
  };
@@ -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 DrawingAnnotationsContainer from './DrawingAnnotationsContainer';
5
5
 
6
+ export type Options = BaseOptions & {
7
+ popupPortalEl?: HTMLElement | null;
8
+ };
9
+
6
10
  export default class DrawingListManager 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--drawing');
9
20
  this.reactEl.dataset.testid = 'ba-Layer--drawing';
@@ -14,6 +25,6 @@ export default class DrawingListManager extends BaseManager {
14
25
  this.root = ReactDOM.createRoot(this.reactEl);
15
26
  }
16
27
 
17
- this.root.render(<DrawingAnnotationsContainer referenceEl={this.referenceEl} targetType={this.targetType} location={this.location} {...props} />);
28
+ this.root.render(<DrawingAnnotationsContainer referenceEl={this.referenceEl} targetType={this.targetType} location={this.location} popupPortalEl={this.popupPortalEl} {...props} />);
18
29
  }
19
30
  }
@@ -74,7 +74,7 @@ export default class ImageAnnotator extends BaseAnnotator {
74
74
  }
75
75
 
76
76
  this.managers.add(new PopupManager({ popupPortalEl: this.popupPortalEl, referenceEl, resinTags }));
77
- this.managers.add(new DrawingManager({ 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
  });
@@ -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
  });
@@ -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';
@@ -121,7 +122,8 @@ const PopupLayer = (props: Props): JSX.Element | null => {
121
122
  }
122
123
 
123
124
  if (showCreator) {
124
- return (
125
+ if (!popupPortalEl) return null;
126
+ return ReactDOM.createPortal(
125
127
  <div className="ba-PopupLayer-popup">
126
128
  <PopupReply
127
129
  isPending={isPending}
@@ -131,7 +133,8 @@ const PopupLayer = (props: Props): JSX.Element | null => {
131
133
  reference={reference}
132
134
  value={message}
133
135
  />
134
- </div>
136
+ </div>,
137
+ popupPortalEl,
135
138
  );
136
139
  }
137
140
 
@@ -20,16 +20,18 @@ export default class PopupManager extends BaseManager {
20
20
  this.reactEl.dataset.testid = 'ba-Layer--popup';
21
21
  }
22
22
 
23
- render(props: Props): void {
23
+ render({ callbacks, intl, store }: Props): void {
24
24
  if (!this.root) {
25
25
  this.root = ReactDOM.createRoot(this.reactEl);
26
26
  }
27
27
 
28
28
  this.root.render(
29
29
  <PopupContainer
30
+ callbacks={callbacks}
31
+ intl={intl}
30
32
  location={this.location}
31
- {...props}
32
33
  popupPortalEl={this.popupPortalEl}
34
+ store={store}
33
35
  targetType={this.targetType}
34
36
  />,
35
37
  );