box-annotations 5.2.1-beta.6 → 5.2.1-beta.8
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/dist/annotations.css +3 -4
- package/dist/annotations.js +1 -1
- package/dist/annotations.js.LICENSE.txt +2 -42
- package/i18n/en-US.js +2 -0
- package/i18n/en-US.properties +4 -0
- package/i18n/json/src/components/Popups/messages.json +1 -1
- package/package.json +28 -15
- package/src/@types/model.ts +5 -0
- package/src/adapters/threadedAnnotationsAdapters.ts +155 -0
- package/src/api/types.ts +28 -1
- package/src/common/BaseAnnotator.scss +2 -1
- package/src/common/BaseAnnotator.ts +16 -0
- package/src/common/DeselectListener.tsx +3 -1
- package/src/components/Popups/PopupReply.tsx +8 -28
- package/src/components/Popups/PopupV2.scss +28 -0
- package/src/components/Popups/PopupV2.tsx +320 -0
- package/src/components/Popups/messages.ts +10 -0
- package/src/document/DocumentAnnotator.ts +29 -5
- package/src/drawing/DrawingTarget.tsx +1 -0
- package/src/highlight/HighlightTarget.tsx +1 -0
- package/src/image/ImageAnnotator.ts +1 -1
- package/src/media/MediaAnnotator.ts +1 -1
- package/src/popup/PopupContainer.tsx +11 -1
- package/src/popup/PopupLayer.tsx +66 -22
- package/src/popup/PopupManager.tsx +21 -3
- package/src/region/RegionAnnotation.tsx +1 -0
- package/src/store/annotations/actions.ts +78 -3
- package/src/store/annotations/reducer.ts +17 -0
- package/src/store/options/reducer.ts +3 -1
- package/src/store/options/selectors.ts +2 -0
- package/src/store/options/types.ts +2 -0
- package/src/components/Popups/PopupReplyV2.tsx +0 -10
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import noop from 'lodash/noop';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import ReactDOM from 'react-dom';
|
|
4
|
+
import { useIntl } from 'react-intl';
|
|
5
|
+
import { useDispatch, useSelector } from 'react-redux';
|
|
6
|
+
|
|
7
|
+
import { BlueprintModernizationProvider } from '@box/blueprint-web';
|
|
8
|
+
import {
|
|
9
|
+
MentionContextProvider,
|
|
10
|
+
MessageEditorV2,
|
|
11
|
+
ThreadedAnnotationsV2,
|
|
12
|
+
serializeMentionMarkup,
|
|
13
|
+
} from '@box/threaded-annotations';
|
|
14
|
+
import type { DocumentNodeV2, TextMessageTypeV2 } from '@box/threaded-annotations';
|
|
15
|
+
import type { FetchedAvatarUrls, UserContactType } from '@box/user-selector';
|
|
16
|
+
import type { JSONContent } from '@tiptap/core';
|
|
17
|
+
import FocusTrap from 'box-ui-elements/es/components/focus-trap/FocusTrap';
|
|
18
|
+
|
|
19
|
+
import { annotationToMessages, collaboratorToUserContact } from '../../adapters/threadedAnnotationsAdapters';
|
|
20
|
+
import {
|
|
21
|
+
createReplyAction,
|
|
22
|
+
deleteAnnotationAction,
|
|
23
|
+
setActiveAnnotationIdAction,
|
|
24
|
+
updateAnnotationAction,
|
|
25
|
+
} from '../../store/annotations/actions';
|
|
26
|
+
import { getAnnotation } from '../../store/annotations/selectors';
|
|
27
|
+
import { getApiHost, getToken } from '../../store/options';
|
|
28
|
+
import { fetchCollaboratorsAction } from '../../store/users/actions';
|
|
29
|
+
|
|
30
|
+
import type { AppState, AppThunkDispatch } from '../../store/types';
|
|
31
|
+
|
|
32
|
+
import createPopper, { PopupReference } from './Popper';
|
|
33
|
+
import type { Instance, Options } from './Popper';
|
|
34
|
+
import messages from './messages';
|
|
35
|
+
import './PopupV2.scss';
|
|
36
|
+
|
|
37
|
+
export type Props = {
|
|
38
|
+
annotationId?: string;
|
|
39
|
+
onSubmit: (text: string) => void;
|
|
40
|
+
popupPortalEl?: HTMLElement | null;
|
|
41
|
+
reference: PopupReference;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const getPopupOptions = (): Partial<Options> => ({
|
|
45
|
+
modifiers: [
|
|
46
|
+
{ name: 'flip', options: { altAxis: false, fallbackPlacements: ['top', 'left', 'right'] } },
|
|
47
|
+
{ name: 'offset', options: { offset: [0, 15] } },
|
|
48
|
+
{ name: 'preventOverflow', options: { padding: 5 } },
|
|
49
|
+
],
|
|
50
|
+
placement: 'bottom',
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const createDocumentNode = (content: JSONContent | null): DocumentNodeV2 => {
|
|
54
|
+
if (!content) {
|
|
55
|
+
return { type: 'doc', content: [] };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (content.type === 'doc' && content.content) {
|
|
59
|
+
return content as DocumentNodeV2;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { type: 'doc', content: [content] } as DocumentNodeV2;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// Callers render initials as a fallback on null.
|
|
66
|
+
// A persistent null across all users usually indicates a stale token.
|
|
67
|
+
const fetchAvatarBlob = async (apiHost: string, token: string, userId: string): Promise<string | null> => {
|
|
68
|
+
try {
|
|
69
|
+
const response = await fetch(`${apiHost}/2.0/users/${userId}/avatar?pic_type=large`, {
|
|
70
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
71
|
+
});
|
|
72
|
+
if (!response.ok) return null;
|
|
73
|
+
const blob = await response.blob();
|
|
74
|
+
return URL.createObjectURL(blob);
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const PopupV2 = ({ annotationId, onSubmit, popupPortalEl, reference }: Props): JSX.Element | null => {
|
|
81
|
+
const intl = useIntl();
|
|
82
|
+
const dispatch = useDispatch<AppThunkDispatch>();
|
|
83
|
+
const popupRef = React.useRef<HTMLDivElement>(null);
|
|
84
|
+
const popperRef = React.useRef<Instance>();
|
|
85
|
+
const optionsRef = React.useRef<Partial<Options>>(getPopupOptions());
|
|
86
|
+
|
|
87
|
+
const apiHost = useSelector(getApiHost);
|
|
88
|
+
const token = useSelector(getToken);
|
|
89
|
+
const annotation = useSelector((state: AppState) =>
|
|
90
|
+
annotationId ? getAnnotation(state, annotationId) : undefined,
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const [avatarBlobs, setAvatarBlobs] = React.useState<Record<string, string>>({});
|
|
94
|
+
const avatarCacheRef = React.useRef<Map<string, string>>(new Map());
|
|
95
|
+
const credentialsRef = React.useRef({ apiHost, token });
|
|
96
|
+
credentialsRef.current = { apiHost, token };
|
|
97
|
+
|
|
98
|
+
const getOrFetchAvatarBlob = React.useCallback(
|
|
99
|
+
async (userId: string): Promise<string | null> => {
|
|
100
|
+
const cached = avatarCacheRef.current.get(userId);
|
|
101
|
+
if (cached) return cached;
|
|
102
|
+
const capturedApiHost = apiHost;
|
|
103
|
+
const capturedToken = token;
|
|
104
|
+
const url = await fetchAvatarBlob(capturedApiHost, capturedToken, userId);
|
|
105
|
+
if (!url) return null;
|
|
106
|
+
if (
|
|
107
|
+
credentialsRef.current.apiHost !== capturedApiHost ||
|
|
108
|
+
credentialsRef.current.token !== capturedToken
|
|
109
|
+
) {
|
|
110
|
+
URL.revokeObjectURL(url);
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
const existing = avatarCacheRef.current.get(userId);
|
|
114
|
+
if (existing) {
|
|
115
|
+
URL.revokeObjectURL(url);
|
|
116
|
+
return existing;
|
|
117
|
+
}
|
|
118
|
+
avatarCacheRef.current.set(userId, url);
|
|
119
|
+
return url;
|
|
120
|
+
},
|
|
121
|
+
[apiHost, token],
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
React.useEffect(() => {
|
|
125
|
+
if (popupRef.current) {
|
|
126
|
+
popperRef.current = createPopper(reference, popupRef.current, optionsRef.current);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return () => {
|
|
130
|
+
popperRef.current?.destroy();
|
|
131
|
+
};
|
|
132
|
+
}, [reference]);
|
|
133
|
+
|
|
134
|
+
React.useEffect(() => {
|
|
135
|
+
const cache = avatarCacheRef.current;
|
|
136
|
+
return () => {
|
|
137
|
+
cache.forEach(url => URL.revokeObjectURL(url));
|
|
138
|
+
cache.clear();
|
|
139
|
+
};
|
|
140
|
+
}, []);
|
|
141
|
+
|
|
142
|
+
React.useEffect(() => {
|
|
143
|
+
avatarCacheRef.current.forEach(url => URL.revokeObjectURL(url));
|
|
144
|
+
avatarCacheRef.current.clear();
|
|
145
|
+
setAvatarBlobs({});
|
|
146
|
+
}, [apiHost, token]);
|
|
147
|
+
|
|
148
|
+
const handleEvent = React.useCallback((event: React.SyntheticEvent) => {
|
|
149
|
+
event.stopPropagation();
|
|
150
|
+
}, []);
|
|
151
|
+
|
|
152
|
+
React.useEffect(() => {
|
|
153
|
+
if (!annotation) return undefined;
|
|
154
|
+
|
|
155
|
+
const userIds = Array.from(
|
|
156
|
+
new Set(annotationToMessages(annotation).map(msg => String(msg.author.id))),
|
|
157
|
+
);
|
|
158
|
+
let cancelled = false;
|
|
159
|
+
|
|
160
|
+
Promise.all(userIds.map(async id => [id, await getOrFetchAvatarBlob(id)] as const)).then(entries => {
|
|
161
|
+
if (cancelled) return;
|
|
162
|
+
setAvatarBlobs(prev => {
|
|
163
|
+
const next = { ...prev };
|
|
164
|
+
entries.forEach(([id, url]) => {
|
|
165
|
+
if (url) next[id] = url;
|
|
166
|
+
});
|
|
167
|
+
return next;
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
return () => {
|
|
172
|
+
cancelled = true;
|
|
173
|
+
};
|
|
174
|
+
}, [annotation, getOrFetchAvatarBlob]);
|
|
175
|
+
|
|
176
|
+
const threadMessages: TextMessageTypeV2[] = React.useMemo(() => {
|
|
177
|
+
if (!annotation) return [];
|
|
178
|
+
return annotationToMessages(annotation).map(msg => ({
|
|
179
|
+
...msg,
|
|
180
|
+
author: {
|
|
181
|
+
...msg.author,
|
|
182
|
+
avatarUrl: avatarBlobs[String(msg.author.id)],
|
|
183
|
+
},
|
|
184
|
+
}));
|
|
185
|
+
}, [annotation, avatarBlobs]);
|
|
186
|
+
|
|
187
|
+
const isResolved = annotation?.status === 'resolved';
|
|
188
|
+
const resolvedBy = isResolved
|
|
189
|
+
? annotation?.resolution?.resolved_by?.name ?? annotation?.modified_by?.name
|
|
190
|
+
: undefined;
|
|
191
|
+
const resolvedAtSource = isResolved
|
|
192
|
+
? annotation?.resolution?.resolved_at ?? annotation?.modified_at
|
|
193
|
+
: undefined;
|
|
194
|
+
const resolvedAt = resolvedAtSource ? new Date(resolvedAtSource).getTime() : undefined;
|
|
195
|
+
|
|
196
|
+
const userSelectorProps = React.useMemo(
|
|
197
|
+
() => ({
|
|
198
|
+
allowEmptyQuery: true,
|
|
199
|
+
ariaRoleDescription: intl.formatMessage(messages.ariaLabelMentionSelector),
|
|
200
|
+
fetchAvatarUrls: async (userContacts: UserContactType[]): Promise<FetchedAvatarUrls> => {
|
|
201
|
+
const urls: FetchedAvatarUrls = {};
|
|
202
|
+
await Promise.all(
|
|
203
|
+
userContacts.map(async ({ id }) => {
|
|
204
|
+
const key = String(id);
|
|
205
|
+
const blobUrl = await getOrFetchAvatarBlob(key);
|
|
206
|
+
if (blobUrl) {
|
|
207
|
+
urls[key] = blobUrl;
|
|
208
|
+
}
|
|
209
|
+
}),
|
|
210
|
+
);
|
|
211
|
+
return urls;
|
|
212
|
+
},
|
|
213
|
+
fetchUsers: async (query: string) => {
|
|
214
|
+
const action = await dispatch(fetchCollaboratorsAction(query));
|
|
215
|
+
if (fetchCollaboratorsAction.fulfilled.match(action)) {
|
|
216
|
+
return action.payload.entries.map(collaboratorToUserContact);
|
|
217
|
+
}
|
|
218
|
+
return [];
|
|
219
|
+
},
|
|
220
|
+
loadingAriaLabel: intl.formatMessage(messages.ariaLabelMentionLoading),
|
|
221
|
+
}),
|
|
222
|
+
[dispatch, getOrFetchAvatarBlob, intl],
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
const handlePost = React.useCallback(
|
|
226
|
+
async (content: JSONContent | null): Promise<void> => {
|
|
227
|
+
const doc = createDocumentNode(content);
|
|
228
|
+
const { text } = serializeMentionMarkup(doc);
|
|
229
|
+
if (annotationId) {
|
|
230
|
+
await dispatch(createReplyAction({ annotationId, message: text }));
|
|
231
|
+
} else {
|
|
232
|
+
onSubmit(text);
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
[annotationId, dispatch, onSubmit],
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
const handleThreadDelete = React.useCallback(
|
|
239
|
+
async (): Promise<void> => {
|
|
240
|
+
if (annotationId) {
|
|
241
|
+
await dispatch(deleteAnnotationAction(annotationId));
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
[annotationId, dispatch],
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
const handleResolve = React.useCallback(
|
|
248
|
+
async (): Promise<void> => {
|
|
249
|
+
if (!annotationId) return;
|
|
250
|
+
const result = await dispatch(updateAnnotationAction({ annotationId, payload: { status: 'resolved' } }));
|
|
251
|
+
if (updateAnnotationAction.fulfilled.match(result)) {
|
|
252
|
+
dispatch(setActiveAnnotationIdAction(null));
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
[annotationId, dispatch],
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
const handleUnresolve = React.useCallback(
|
|
259
|
+
async (): Promise<void> => {
|
|
260
|
+
if (!annotationId) return;
|
|
261
|
+
await dispatch(updateAnnotationAction({ annotationId, payload: { status: 'open' } }));
|
|
262
|
+
},
|
|
263
|
+
[annotationId, dispatch],
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
if (!popupPortalEl) return null;
|
|
267
|
+
|
|
268
|
+
return ReactDOM.createPortal(
|
|
269
|
+
<FocusTrap>
|
|
270
|
+
<div
|
|
271
|
+
ref={popupRef}
|
|
272
|
+
aria-label={intl.formatMessage(messages.ariaLabelComment)}
|
|
273
|
+
className="ba-PopupV2"
|
|
274
|
+
data-resin-component={annotationId ? 'popupThreadV2' : 'popupReplyV2'}
|
|
275
|
+
onClick={handleEvent}
|
|
276
|
+
onMouseDown={handleEvent}
|
|
277
|
+
onMouseMove={handleEvent}
|
|
278
|
+
onMouseUp={handleEvent}
|
|
279
|
+
onTouchMove={handleEvent}
|
|
280
|
+
onTouchStart={handleEvent}
|
|
281
|
+
role="presentation"
|
|
282
|
+
>
|
|
283
|
+
<BlueprintModernizationProvider enableModernizedComponents>
|
|
284
|
+
<MentionContextProvider value={{
|
|
285
|
+
collaborationPopoverProps: {
|
|
286
|
+
onSubmit: () => Promise.resolve(),
|
|
287
|
+
},
|
|
288
|
+
}}>
|
|
289
|
+
{annotationId ? (
|
|
290
|
+
<ThreadedAnnotationsV2
|
|
291
|
+
isAnnotations
|
|
292
|
+
isResolved={isResolved}
|
|
293
|
+
messages={threadMessages}
|
|
294
|
+
onAvatarClick={noop}
|
|
295
|
+
onDelete={noop}
|
|
296
|
+
onPost={handlePost}
|
|
297
|
+
onResolve={handleResolve}
|
|
298
|
+
onThreadDelete={handleThreadDelete}
|
|
299
|
+
onUnresolve={handleUnresolve}
|
|
300
|
+
resolvedAt={resolvedAt}
|
|
301
|
+
resolvedBy={resolvedBy}
|
|
302
|
+
userSelectorProps={userSelectorProps}
|
|
303
|
+
/>
|
|
304
|
+
) : (
|
|
305
|
+
<MessageEditorV2
|
|
306
|
+
isFirstAnnotation
|
|
307
|
+
onPost={handlePost}
|
|
308
|
+
userSelectorProps={userSelectorProps}
|
|
309
|
+
/>
|
|
310
|
+
)}
|
|
311
|
+
</MentionContextProvider>
|
|
312
|
+
</BlueprintModernizationProvider>
|
|
313
|
+
<div data-threaded-annotations-portal />
|
|
314
|
+
</div>
|
|
315
|
+
</FocusTrap>,
|
|
316
|
+
popupPortalEl,
|
|
317
|
+
);
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
export default PopupV2;
|
|
@@ -6,6 +6,16 @@ export default defineMessages({
|
|
|
6
6
|
description: 'Aria label description for reply popup',
|
|
7
7
|
defaultMessage: 'Comment',
|
|
8
8
|
},
|
|
9
|
+
ariaLabelMentionLoading: {
|
|
10
|
+
id: 'ba.popup.reply.mentionLoading',
|
|
11
|
+
description: 'Aria label for mention selector loading state',
|
|
12
|
+
defaultMessage: 'Loading users...',
|
|
13
|
+
},
|
|
14
|
+
ariaLabelMentionSelector: {
|
|
15
|
+
id: 'ba.popup.reply.mentionSelector',
|
|
16
|
+
description: 'Aria role description for mention user selector',
|
|
17
|
+
defaultMessage: 'Mention a collaborator',
|
|
18
|
+
},
|
|
9
19
|
ariaLabelReplyField: {
|
|
10
20
|
id: 'ba.popup.reply.field',
|
|
11
21
|
description: 'Aria label description for annotation comment field',
|
|
@@ -15,7 +15,7 @@ import { getAnnotation } from '../store/annotations';
|
|
|
15
15
|
import { BoundingBox } from '../store/boundingBoxHighlights';
|
|
16
16
|
import { getSelection } from './docUtil';
|
|
17
17
|
import { Manager } from '../common/BaseManager';
|
|
18
|
-
import { getFileId, getIsCurrentFileVersion, getViewMode, Mode } from '../store';
|
|
18
|
+
import { getFileId, getIsCurrentFileVersion, getRotation, getViewMode, Mode } from '../store';
|
|
19
19
|
import { scrollToLocation } from '../utils/scroll';
|
|
20
20
|
import './DocumentAnnotator.scss';
|
|
21
21
|
|
|
@@ -24,6 +24,9 @@ import type { ViewMode } from '../store/options/types';
|
|
|
24
24
|
// Pdf.js textLayer enhancement requires waiting for 300ms (they hardcode this magic number)
|
|
25
25
|
const TEXT_LAYER_ENHANCEMENT = 300;
|
|
26
26
|
|
|
27
|
+
// Vertical padding on each page element as provided by Preview SDK (see DocumentAnnotator.scss)
|
|
28
|
+
const PAGE_PADDING = 15;
|
|
29
|
+
|
|
27
30
|
export default class DocumentAnnotator extends BaseAnnotator {
|
|
28
31
|
annotatedEl?: HTMLElement;
|
|
29
32
|
|
|
@@ -99,7 +102,7 @@ export default class DocumentAnnotator extends BaseAnnotator {
|
|
|
99
102
|
}
|
|
100
103
|
|
|
101
104
|
// Annotations mode
|
|
102
|
-
managers.add(new PopupManager({ location: pageNumber, referenceEl: pageReferenceEl, resinTags }));
|
|
105
|
+
managers.add(new PopupManager({ location: pageNumber, popupPortalEl: this.popupPortalEl, referenceEl: pageReferenceEl, resinTags }));
|
|
103
106
|
managers.add(new DrawingManager({ location: pageNumber, referenceEl: pageReferenceEl, resinTags }));
|
|
104
107
|
|
|
105
108
|
const textLayer = pageEl.querySelector('.textLayer') as HTMLElement;
|
|
@@ -196,14 +199,35 @@ export default class DocumentAnnotator extends BaseAnnotator {
|
|
|
196
199
|
renderPage(pageEl: HTMLElement): void {
|
|
197
200
|
const pageManagers = this.getPageManagers(pageEl);
|
|
198
201
|
const pageNumber = this.getPageNumber(pageEl);
|
|
202
|
+
const rotation = getRotation(this.store.getState()) || 0;
|
|
203
|
+
|
|
204
|
+
// Calculate original (unrotated) page dimensions for annotation layer sizing
|
|
205
|
+
const pageWidth = pageEl.clientWidth;
|
|
206
|
+
const pageHeight = pageEl.clientHeight - PAGE_PADDING * 2;
|
|
207
|
+
const isOrthogonal = rotation % 180 !== 0;
|
|
208
|
+
const origWidth = isOrthogonal ? pageHeight : pageWidth;
|
|
209
|
+
const origHeight = isOrthogonal ? pageWidth : pageHeight;
|
|
199
210
|
|
|
200
211
|
// Render annotations for every page
|
|
201
|
-
pageManagers.forEach(manager =>
|
|
212
|
+
pageManagers.forEach(manager => {
|
|
213
|
+
// Apply rotation transform to annotation layers
|
|
214
|
+
if (rotation) {
|
|
215
|
+
manager.style({
|
|
216
|
+
height: `${origHeight}px`,
|
|
217
|
+
left: '50%',
|
|
218
|
+
top: '50%',
|
|
219
|
+
transform: `translate(-50%, -50%) rotate(${rotation}deg)`,
|
|
220
|
+
width: `${origWidth}px`,
|
|
221
|
+
});
|
|
222
|
+
} else {
|
|
223
|
+
manager.style({height: '', left: '', top: '', transform: '', width: ''});
|
|
224
|
+
}
|
|
225
|
+
|
|
202
226
|
manager.render({
|
|
203
227
|
intl: this.intl,
|
|
204
228
|
store: this.store,
|
|
205
|
-
})
|
|
206
|
-
);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
207
231
|
|
|
208
232
|
this.managers.set(pageNumber, pageManagers);
|
|
209
233
|
}
|
|
@@ -64,6 +64,7 @@ export const DrawingTarget = (props: Props, ref: React.Ref<DrawingTargetRef>): J
|
|
|
64
64
|
<a
|
|
65
65
|
ref={ref}
|
|
66
66
|
className={classNames('ba-DrawingTarget', className, { 'is-active': isActive })}
|
|
67
|
+
data-ba-annotation-id={annotationId}
|
|
67
68
|
data-resin-itemid={annotationId}
|
|
68
69
|
data-resin-target="highlightDrawing"
|
|
69
70
|
data-testid={`ba-AnnotationTarget-${annotationId}`}
|
|
@@ -69,6 +69,7 @@ const HighlightTarget = (props: Props, ref: React.Ref<HighlightTargetRef>): JSX.
|
|
|
69
69
|
<a
|
|
70
70
|
ref={ref}
|
|
71
71
|
className={classNames('ba-HighlightTarget', className, { 'is-active': isActive })}
|
|
72
|
+
data-ba-annotation-id={annotationId}
|
|
72
73
|
data-ba-reference-id={uuid}
|
|
73
74
|
data-resin-itemid={annotationId}
|
|
74
75
|
data-resin-target="highlightText"
|
|
@@ -73,7 +73,7 @@ export default class ImageAnnotator extends BaseAnnotator {
|
|
|
73
73
|
return this.managers;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
this.managers.add(new PopupManager({ referenceEl, resinTags }));
|
|
76
|
+
this.managers.add(new PopupManager({ popupPortalEl: this.popupPortalEl, referenceEl, resinTags }));
|
|
77
77
|
this.managers.add(new DrawingManager({ referenceEl, resinTags }));
|
|
78
78
|
this.managers.add(new RegionManager({ referenceEl, resinTags }));
|
|
79
79
|
this.managers.add(new RegionCreationManager({ referenceEl, resinTags }));
|
|
@@ -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 }:
|
|
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),
|
package/src/popup/PopupLayer.tsx
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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}`}
|