box-annotations 5.2.1-beta.12 → 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.12",
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>({});
@@ -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
  };
@@ -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
  );
@@ -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}
@@ -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
  });
@@ -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
  });
@@ -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
  );