box-annotations 5.2.1-beta.14 → 5.2.1-beta.18
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 +1 -1
- package/dist/annotations.js +1 -1
- package/package.json +13 -13
- package/src/@types/api.ts +3 -2
- package/src/@types/events.ts +27 -10
- package/src/@types/model.ts +1 -0
- package/src/adapters/threadedAnnotationsAdapters.ts +17 -1
- package/src/api/APIFactory.ts +6 -1
- package/src/api/types.ts +23 -1
- package/src/common/BaseAnnotator.scss +2 -1
- package/src/common/BaseAnnotator.ts +27 -2
- package/src/components/Popups/PopupV2.tsx +46 -10
- package/src/drawing/DrawingAnnotations.tsx +6 -5
- package/src/popup/PopupLayer.tsx +32 -4
- package/src/store/annotations/actions.ts +89 -0
- package/src/store/annotations/reducer.ts +48 -0
- package/src/store/eventing/create.ts +3 -3
- package/src/store/eventing/delete.ts +27 -0
- package/src/store/eventing/middleware.ts +40 -4
- package/src/store/eventing/replyCreate.ts +34 -0
- package/src/store/eventing/replyDelete.ts +35 -0
- package/src/store/eventing/replyUpdate.ts +39 -0
- package/src/store/eventing/types.ts +4 -4
- package/src/store/eventing/update.ts +29 -0
- package/src/store/options/selectors.ts +2 -2
- package/src/store/options/types.ts +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "box-annotations",
|
|
3
|
-
"version": "5.2.1-beta.
|
|
3
|
+
"version": "5.2.1-beta.18",
|
|
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.
|
|
32
|
-
"@box/blueprint-web-assets": "^4.
|
|
33
|
-
"@box/collaboration-popover": "^1.62.
|
|
31
|
+
"@box/blueprint-web": "^14.38.0",
|
|
32
|
+
"@box/blueprint-web-assets": "^4.120.5",
|
|
33
|
+
"@box/collaboration-popover": "^1.62.27",
|
|
34
34
|
"@box/languages": "^1.1.0",
|
|
35
|
-
"@box/readable-time": "^1.41.
|
|
36
|
-
"@box/threaded-annotations": "^1.
|
|
37
|
-
"@box/user-selector": "^1.76.
|
|
35
|
+
"@box/readable-time": "^1.41.27",
|
|
36
|
+
"@box/threaded-annotations": "^1.93.2",
|
|
37
|
+
"@box/user-selector": "^1.76.27",
|
|
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.
|
|
132
|
-
"@box/blueprint-web-assets": "^4.
|
|
133
|
-
"@box/collaboration-popover": "^1.62.
|
|
134
|
-
"@box/readable-time": "^1.41.
|
|
135
|
-
"@box/threaded-annotations": "^1.
|
|
136
|
-
"@box/user-selector": "^1.76.
|
|
131
|
+
"@box/blueprint-web": "^14.38.0",
|
|
132
|
+
"@box/blueprint-web-assets": "^4.120.5",
|
|
133
|
+
"@box/collaboration-popover": "^1.62.27",
|
|
134
|
+
"@box/readable-time": "^1.41.27",
|
|
135
|
+
"@box/threaded-annotations": "^1.93.2",
|
|
136
|
+
"@box/user-selector": "^1.76.27"
|
|
137
137
|
},
|
|
138
138
|
"scripts": {
|
|
139
139
|
"build": "yarn setup && yarn build:prod:dist",
|
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
|
|
14
|
-
export type
|
|
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;
|
package/src/@types/events.ts
CHANGED
|
@@ -1,19 +1,36 @@
|
|
|
1
1
|
enum Event {
|
|
2
|
-
|
|
2
|
+
// Inbound: emitted by the host; box-annotations listens.
|
|
3
3
|
ACTIVE_SET = 'annotations_active_set',
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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 };
|
package/src/@types/model.ts
CHANGED
|
@@ -69,6 +69,20 @@ export const deserializeMentionMarkup = (text: string): DocumentNodeV2 => {
|
|
|
69
69
|
return { type: 'doc', content };
|
|
70
70
|
};
|
|
71
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Returns the edit timestamp consumers use to render an edited indicator.
|
|
74
|
+
* Compares parsed instants, not raw strings, so equivalent ISO formats
|
|
75
|
+
* (Z vs +00:00, fractional precision) are treated as unedited.
|
|
76
|
+
*/
|
|
77
|
+
const toUpdatedAt = (createdAt: string, modifiedAt: string | undefined): number | undefined => {
|
|
78
|
+
if (!modifiedAt) return undefined;
|
|
79
|
+
const modifiedMs = new Date(modifiedAt).getTime();
|
|
80
|
+
if (Number.isNaN(modifiedMs)) return undefined;
|
|
81
|
+
const createdMs = new Date(createdAt).getTime();
|
|
82
|
+
if (modifiedMs === createdMs) return undefined;
|
|
83
|
+
return modifiedMs;
|
|
84
|
+
};
|
|
85
|
+
|
|
72
86
|
/**
|
|
73
87
|
* Converts a box-annotations Reply to a threaded-annotations TextMessageType.
|
|
74
88
|
*/
|
|
@@ -83,10 +97,11 @@ export const replyToTextMessage = (reply: Reply): TextMessageTypeV2 => ({
|
|
|
83
97
|
message: deserializeMentionMarkup(reply.message),
|
|
84
98
|
permissions: {
|
|
85
99
|
canDelete: reply.permissions?.can_delete ?? false,
|
|
86
|
-
canEdit: false,
|
|
100
|
+
canEdit: reply.permissions?.can_edit ?? false,
|
|
87
101
|
canReply: reply.permissions?.can_reply ?? false,
|
|
88
102
|
canResolve: reply.permissions?.can_resolve ?? false,
|
|
89
103
|
},
|
|
104
|
+
updatedAt: toUpdatedAt(reply.created_at, reply.modified_at),
|
|
90
105
|
});
|
|
91
106
|
|
|
92
107
|
// The root message shares the annotation's author and permissions; description
|
|
@@ -106,6 +121,7 @@ const descriptionToTextMessage = (annotation: Annotation): TextMessageTypeV2 =>
|
|
|
106
121
|
canReply: annotation.permissions?.can_reply ?? false,
|
|
107
122
|
canResolve: annotation.permissions?.can_resolve ?? false,
|
|
108
123
|
},
|
|
124
|
+
updatedAt: toUpdatedAt(annotation.created_at, annotation.modified_at),
|
|
109
125
|
});
|
|
110
126
|
|
|
111
127
|
/**
|
package/src/api/APIFactory.ts
CHANGED
|
@@ -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,
|
|
@@ -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';
|
|
@@ -45,7 +45,7 @@ export type Options = {
|
|
|
45
45
|
intl: IntlOptions;
|
|
46
46
|
locale?: string;
|
|
47
47
|
onCopyLink?: (params: { annotationId: string; fileVersionId: string }) => void;
|
|
48
|
-
token:
|
|
48
|
+
token: Token;
|
|
49
49
|
};
|
|
50
50
|
|
|
51
51
|
export const CSS_CONTAINER_CLASS = 'ba';
|
|
@@ -131,6 +131,11 @@ export default class BaseAnnotator extends EventEmitter {
|
|
|
131
131
|
this.addListener(Event.BOUNDING_BOX_HIGHLIGHT_SELECT, this.handleSelectBoundingBoxHighlight);
|
|
132
132
|
this.addListener(Event.VIEW_MODE_SET, this.handleSetViewMode);
|
|
133
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
|
+
|
|
134
139
|
// Load any required data at startup
|
|
135
140
|
this.hydrate();
|
|
136
141
|
}
|
|
@@ -164,6 +169,10 @@ export default class BaseAnnotator extends EventEmitter {
|
|
|
164
169
|
this.removeListener(Event.BOUNDING_BOX_HIGHLIGHT_NAVIGATE, this.handleNavigateBoundingBoxHighlight);
|
|
165
170
|
this.removeListener(Event.BOUNDING_BOX_HIGHLIGHT_SELECT, this.handleSelectBoundingBoxHighlight);
|
|
166
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);
|
|
167
176
|
}
|
|
168
177
|
|
|
169
178
|
public init(scale = 1, rotation = 0): void {
|
|
@@ -357,6 +366,22 @@ export default class BaseAnnotator extends EventEmitter {
|
|
|
357
366
|
this.setViewMode(viewMode);
|
|
358
367
|
};
|
|
359
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
|
+
|
|
360
385
|
protected hydrate(): void {
|
|
361
386
|
// Redux dispatch method signature doesn't seem to like async actions
|
|
362
387
|
this.store.dispatch<any>(store.fetchAnnotationsAction()); // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
@@ -21,13 +21,16 @@ import AnnotationCallbacksContext from '../../common/AnnotationCallbacksContext'
|
|
|
21
21
|
import {
|
|
22
22
|
createReplyAction,
|
|
23
23
|
deleteAnnotationAction,
|
|
24
|
+
deleteReplyAction,
|
|
24
25
|
setActiveAnnotationIdAction,
|
|
25
26
|
updateAnnotationAction,
|
|
27
|
+
updateReplyAction,
|
|
26
28
|
} from '../../store/annotations/actions';
|
|
27
29
|
import { getAnnotation } from '../../store/annotations/selectors';
|
|
28
|
-
import { getApiHost, getFileVersionId, getToken } from '../../store/options';
|
|
30
|
+
import { getApiHost, getFileId, getFileVersionId, getToken } from '../../store/options';
|
|
29
31
|
import { fetchCollaboratorsAction } from '../../store/users/actions';
|
|
30
32
|
|
|
33
|
+
import type { Token, TokenLiteral, TokenMap } from '../../@types';
|
|
31
34
|
import type { AppState, AppThunkDispatch } from '../../store/types';
|
|
32
35
|
|
|
33
36
|
import createPopper, { PopupReference } from './Popper';
|
|
@@ -63,12 +66,32 @@ const createDocumentNode = (content: JSONContent | null): DocumentNodeV2 => {
|
|
|
63
66
|
return { type: 'doc', content: [content] } as DocumentNodeV2;
|
|
64
67
|
};
|
|
65
68
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
+
const literalToString = (literal: TokenLiteral): string | null => {
|
|
70
|
+
if (!literal) return null;
|
|
71
|
+
if (typeof literal === 'string') return literal;
|
|
72
|
+
return literal.read ?? literal.write ?? null;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const resolveStringToken = async (token: Token, typedFileId: string): Promise<string | null> => {
|
|
76
|
+
const resolved = typeof token === 'function' ? await token(typedFileId) : token;
|
|
77
|
+
if (!resolved) return null;
|
|
78
|
+
if (typeof resolved === 'string') return resolved;
|
|
79
|
+
if (typedFileId in resolved) return literalToString((resolved as TokenMap)[typedFileId]);
|
|
80
|
+
return literalToString(resolved as TokenLiteral);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const fetchAvatarBlob = async (
|
|
84
|
+
apiHost: string,
|
|
85
|
+
token: Token,
|
|
86
|
+
fileId: string | null,
|
|
87
|
+
userId: string,
|
|
88
|
+
): Promise<string | null> => {
|
|
69
89
|
try {
|
|
90
|
+
if (!fileId) return null;
|
|
91
|
+
const stringToken = await resolveStringToken(token, `file_${fileId}`);
|
|
92
|
+
if (!stringToken) return null;
|
|
70
93
|
const response = await fetch(`${apiHost}/2.0/users/${userId}/avatar?pic_type=large`, {
|
|
71
|
-
headers: { Authorization: `Bearer ${
|
|
94
|
+
headers: { Authorization: `Bearer ${stringToken}` },
|
|
72
95
|
});
|
|
73
96
|
if (!response.ok) return null;
|
|
74
97
|
const blob = await response.blob();
|
|
@@ -87,6 +110,7 @@ const PopupV2 = ({ annotationId, onSubmit, popupPortalEl, reference }: Props): J
|
|
|
87
110
|
const optionsRef = React.useRef<Partial<Options>>(getPopupOptions());
|
|
88
111
|
|
|
89
112
|
const apiHost = useSelector(getApiHost);
|
|
113
|
+
const fileId = useSelector(getFileId);
|
|
90
114
|
const fileVersionId = useSelector(getFileVersionId);
|
|
91
115
|
const token = useSelector(getToken);
|
|
92
116
|
const onCopyLink = React.useMemo(
|
|
@@ -111,7 +135,7 @@ const PopupV2 = ({ annotationId, onSubmit, popupPortalEl, reference }: Props): J
|
|
|
111
135
|
if (cached) return cached;
|
|
112
136
|
const capturedApiHost = apiHost;
|
|
113
137
|
const capturedToken = token;
|
|
114
|
-
const url = await fetchAvatarBlob(capturedApiHost, capturedToken, userId);
|
|
138
|
+
const url = await fetchAvatarBlob(capturedApiHost, capturedToken, fileId, userId);
|
|
115
139
|
if (!url) return null;
|
|
116
140
|
if (
|
|
117
141
|
credentialsRef.current.apiHost !== capturedApiHost ||
|
|
@@ -128,7 +152,7 @@ const PopupV2 = ({ annotationId, onSubmit, popupPortalEl, reference }: Props): J
|
|
|
128
152
|
avatarCacheRef.current.set(userId, url);
|
|
129
153
|
return url;
|
|
130
154
|
},
|
|
131
|
-
[apiHost, token],
|
|
155
|
+
[apiHost, fileId, token],
|
|
132
156
|
);
|
|
133
157
|
|
|
134
158
|
React.useEffect(() => {
|
|
@@ -247,10 +271,14 @@ const PopupV2 = ({ annotationId, onSubmit, popupPortalEl, reference }: Props): J
|
|
|
247
271
|
|
|
248
272
|
const handleEdit = React.useCallback(
|
|
249
273
|
async (id: string, content: JSONContent | null): Promise<void> => {
|
|
250
|
-
if (!annotationId
|
|
274
|
+
if (!annotationId) return;
|
|
251
275
|
const doc = createDocumentNode(content);
|
|
252
276
|
const { text } = serializeMentionMarkup(doc);
|
|
253
|
-
|
|
277
|
+
if (id === annotationId) {
|
|
278
|
+
await dispatch(updateAnnotationAction({ annotationId, payload: { message: text } }));
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
await dispatch(updateReplyAction({ annotationId, replyId: id, payload: { message: text } }));
|
|
254
282
|
},
|
|
255
283
|
[annotationId, dispatch],
|
|
256
284
|
);
|
|
@@ -264,6 +292,14 @@ const PopupV2 = ({ annotationId, onSubmit, popupPortalEl, reference }: Props): J
|
|
|
264
292
|
[annotationId, dispatch],
|
|
265
293
|
);
|
|
266
294
|
|
|
295
|
+
const handleDelete = React.useCallback(
|
|
296
|
+
async (id: string): Promise<void> => {
|
|
297
|
+
if (!annotationId || id === annotationId) return;
|
|
298
|
+
await dispatch(deleteReplyAction({ annotationId, replyId: id }));
|
|
299
|
+
},
|
|
300
|
+
[annotationId, dispatch],
|
|
301
|
+
);
|
|
302
|
+
|
|
267
303
|
const handleResolve = React.useCallback(
|
|
268
304
|
async (): Promise<void> => {
|
|
269
305
|
if (!annotationId) return;
|
|
@@ -314,7 +350,7 @@ const PopupV2 = ({ annotationId, onSubmit, popupPortalEl, reference }: Props): J
|
|
|
314
350
|
messages={threadMessages}
|
|
315
351
|
onAvatarClick={noop}
|
|
316
352
|
onCopyLink={onCopyLink}
|
|
317
|
-
onDelete={
|
|
353
|
+
onDelete={handleDelete}
|
|
318
354
|
onEdit={handleEdit}
|
|
319
355
|
onPost={handlePost}
|
|
320
356
|
onResolve={handleResolve}
|
|
@@ -179,8 +179,8 @@ const DrawingAnnotations = (props: Props): JSX.Element => {
|
|
|
179
179
|
/>
|
|
180
180
|
)}
|
|
181
181
|
|
|
182
|
-
{isCreating && hasPathGroups && drawingSVGGroup && canShowPopupToolbar &&
|
|
183
|
-
|
|
182
|
+
{isCreating && hasPathGroups && drawingSVGGroup && canShowPopupToolbar && (() => {
|
|
183
|
+
const toolbar = (
|
|
184
184
|
<PopupDrawingToolbar
|
|
185
185
|
ref={popupDrawingToolbarRef}
|
|
186
186
|
canComment={hasDrawnPathGroups}
|
|
@@ -192,9 +192,10 @@ const DrawingAnnotations = (props: Props): JSX.Element => {
|
|
|
192
192
|
onReply={handleReply}
|
|
193
193
|
onUndo={handleUndo}
|
|
194
194
|
reference={drawingSVGGroup}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
)
|
|
195
|
+
/>
|
|
196
|
+
);
|
|
197
|
+
return popupPortalEl ? ReactDOM.createPortal(toolbar, popupPortalEl) : toolbar;
|
|
198
|
+
})()}
|
|
198
199
|
</>
|
|
199
200
|
);
|
|
200
201
|
};
|
package/src/popup/PopupLayer.tsx
CHANGED
|
@@ -45,6 +45,8 @@ const modeStagedMap: { [M in Mode]?: (staged: CreatorItem | null) => boolean } =
|
|
|
45
45
|
[Mode.REGION]: isCreatorStagedRegion,
|
|
46
46
|
};
|
|
47
47
|
|
|
48
|
+
const ACTIVE_TARGET_OBSERVER_TIMEOUT_MS = 10000;
|
|
49
|
+
|
|
48
50
|
const PopupLayer = (props: Props): JSX.Element | null => {
|
|
49
51
|
const {
|
|
50
52
|
activeAnnotationId,
|
|
@@ -98,13 +100,39 @@ const PopupLayer = (props: Props): JSX.Element | null => {
|
|
|
98
100
|
setReference(referenceId ? document.querySelector(`[data-ba-reference-id="${referenceId}"]`) : null);
|
|
99
101
|
}, [referenceId]);
|
|
100
102
|
|
|
103
|
+
// activeAnnotationId can be set (e.g. via deep link) before the target DOM node mounts;
|
|
104
|
+
// observe until it appears, or give up after ACTIVE_TARGET_OBSERVER_TIMEOUT_MS.
|
|
101
105
|
React.useEffect(() => {
|
|
102
|
-
if (activeAnnotationId
|
|
103
|
-
const el = document.querySelector(`[data-ba-annotation-id="${CSS.escape(activeAnnotationId)}"]`);
|
|
104
|
-
setActiveReference(el);
|
|
105
|
-
} else {
|
|
106
|
+
if (!activeAnnotationId || !isThreadedAnnotation) {
|
|
106
107
|
setActiveReference(null);
|
|
108
|
+
return noop;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const selector = `[data-ba-annotation-id="${CSS.escape(activeAnnotationId)}"]`;
|
|
112
|
+
const existing = document.querySelector(selector);
|
|
113
|
+
if (existing) {
|
|
114
|
+
setActiveReference(existing);
|
|
115
|
+
return noop;
|
|
107
116
|
}
|
|
117
|
+
|
|
118
|
+
setActiveReference(null);
|
|
119
|
+
const observer = new MutationObserver(() => {
|
|
120
|
+
const el = document.querySelector(selector);
|
|
121
|
+
if (el) {
|
|
122
|
+
setActiveReference(el);
|
|
123
|
+
observer.disconnect();
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
127
|
+
|
|
128
|
+
const timeoutId = window.setTimeout(() => {
|
|
129
|
+
observer.disconnect();
|
|
130
|
+
}, ACTIVE_TARGET_OBSERVER_TIMEOUT_MS);
|
|
131
|
+
|
|
132
|
+
return () => {
|
|
133
|
+
observer.disconnect();
|
|
134
|
+
window.clearTimeout(timeoutId);
|
|
135
|
+
};
|
|
108
136
|
}, [activeAnnotationId, isThreadedAnnotation]);
|
|
109
137
|
|
|
110
138
|
const showCreator = canCreate && canReply && reference && staged;
|
|
@@ -121,6 +121,95 @@ export const updateAnnotationAction = createAsyncThunk<
|
|
|
121
121
|
},
|
|
122
122
|
);
|
|
123
123
|
|
|
124
|
+
export const updateReplyAction = createAsyncThunk<
|
|
125
|
+
{ annotationId: string; reply: Reply },
|
|
126
|
+
{ annotationId: string; replyId: string; payload: { message?: string; status?: string } },
|
|
127
|
+
AppThunkAPI
|
|
128
|
+
>(
|
|
129
|
+
'UPDATE_REPLY',
|
|
130
|
+
async ({ annotationId, replyId, payload }, { extra, getState, signal }) => {
|
|
131
|
+
const client = extra.api.getThreadedCommentsAPI();
|
|
132
|
+
const state = getState();
|
|
133
|
+
const fileId = getFileId(state);
|
|
134
|
+
const annotation = getAnnotation(state, annotationId);
|
|
135
|
+
const reply = annotation?.replies?.find(r => r.id === replyId);
|
|
136
|
+
|
|
137
|
+
if (!reply) {
|
|
138
|
+
throw new Error(`updateReplyAction: reply ${replyId} not found on annotation ${annotationId}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
signal.addEventListener('abort', () => {
|
|
142
|
+
client.destroy();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const updated = await new Promise<Reply>((resolve, reject) => {
|
|
146
|
+
client.updateComment({
|
|
147
|
+
commentId: replyId,
|
|
148
|
+
errorCallback: reject,
|
|
149
|
+
fileId,
|
|
150
|
+
message: payload.message,
|
|
151
|
+
permissions: reply.permissions ?? {},
|
|
152
|
+
status: payload.status,
|
|
153
|
+
successCallback: resolve,
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
return { annotationId, reply: updated };
|
|
158
|
+
},
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
export const deleteReplyAction = createAsyncThunk<
|
|
162
|
+
{ annotationId: string; replyId: string },
|
|
163
|
+
{ annotationId: string; replyId: string },
|
|
164
|
+
AppThunkAPI
|
|
165
|
+
>(
|
|
166
|
+
'DELETE_REPLY',
|
|
167
|
+
async ({ annotationId, replyId }, { extra, getState, signal }) => {
|
|
168
|
+
const client = extra.api.getThreadedCommentsAPI();
|
|
169
|
+
const state = getState();
|
|
170
|
+
const fileId = getFileId(state);
|
|
171
|
+
const annotation = getAnnotation(state, annotationId);
|
|
172
|
+
const reply = annotation?.replies?.find(r => r.id === replyId);
|
|
173
|
+
|
|
174
|
+
if (!reply) {
|
|
175
|
+
throw new Error(`deleteReplyAction: reply ${replyId} not found on annotation ${annotationId}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
signal.addEventListener('abort', () => {
|
|
179
|
+
client.destroy();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
await new Promise<void>((resolve, reject) => {
|
|
183
|
+
client.deleteComment({
|
|
184
|
+
commentId: replyId,
|
|
185
|
+
errorCallback: reject,
|
|
186
|
+
fileId,
|
|
187
|
+
permissions: reply.permissions ?? {},
|
|
188
|
+
successCallback: resolve,
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
return { annotationId, replyId };
|
|
193
|
+
},
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
export type SidebarAnnotationUpdatePayload = Partial<Annotation> & { id: string };
|
|
197
|
+
export type SidebarReplyMutationPayload = { annotationId: string; reply: Reply };
|
|
198
|
+
export type SidebarReplyDeletePayload = { annotationId: string; replyId: string };
|
|
199
|
+
|
|
200
|
+
export const applySidebarAnnotationUpdateAction = createAction<SidebarAnnotationUpdatePayload>(
|
|
201
|
+
'APPLY_SIDEBAR_ANNOTATION_UPDATE',
|
|
202
|
+
);
|
|
203
|
+
export const applySidebarReplyCreateAction = createAction<SidebarReplyMutationPayload>(
|
|
204
|
+
'APPLY_SIDEBAR_REPLY_CREATE',
|
|
205
|
+
);
|
|
206
|
+
export const applySidebarReplyDeleteAction = createAction<SidebarReplyDeletePayload>(
|
|
207
|
+
'APPLY_SIDEBAR_REPLY_DELETE',
|
|
208
|
+
);
|
|
209
|
+
export const applySidebarReplyUpdateAction = createAction<SidebarReplyMutationPayload>(
|
|
210
|
+
'APPLY_SIDEBAR_REPLY_UPDATE',
|
|
211
|
+
);
|
|
212
|
+
|
|
124
213
|
export const removeAnnotationAction = createAction<string>('REMOVE_ANNOTATION');
|
|
125
214
|
export const setActiveAnnotationIdAction = createAction<string | null>('SET_ACTIVE_ANNOTATION_ID');
|
|
126
215
|
export const setIsInitialized = createAction('SET_IS_INITIALIZED');
|
|
@@ -2,14 +2,20 @@ import { createReducer, combineReducers } from '@reduxjs/toolkit';
|
|
|
2
2
|
import { formatDrawing, isDrawing } from '../../drawing/drawingUtil';
|
|
3
3
|
import { AnnotationsState } from './types';
|
|
4
4
|
import {
|
|
5
|
+
applySidebarAnnotationUpdateAction,
|
|
6
|
+
applySidebarReplyCreateAction,
|
|
7
|
+
applySidebarReplyDeleteAction,
|
|
8
|
+
applySidebarReplyUpdateAction,
|
|
5
9
|
createAnnotationAction,
|
|
6
10
|
createReplyAction,
|
|
7
11
|
deleteAnnotationAction,
|
|
12
|
+
deleteReplyAction,
|
|
8
13
|
fetchAnnotationsAction,
|
|
9
14
|
removeAnnotationAction,
|
|
10
15
|
setActiveAnnotationIdAction,
|
|
11
16
|
setIsInitialized,
|
|
12
17
|
updateAnnotationAction,
|
|
18
|
+
updateReplyAction,
|
|
13
19
|
} from './actions';
|
|
14
20
|
import { setViewModeAction } from '../options/actions';
|
|
15
21
|
|
|
@@ -58,6 +64,48 @@ const annotationsById = createReducer<AnnotationsState['byId']>({}, builder =>
|
|
|
58
64
|
.addCase(updateAnnotationAction.fulfilled, (state, { payload }) => {
|
|
59
65
|
state[payload.id] = isDrawing(payload) ? formatDrawing(payload) : payload;
|
|
60
66
|
})
|
|
67
|
+
.addCase(updateReplyAction.fulfilled, (state, { payload: { annotationId, reply } }) => {
|
|
68
|
+
const annotation = state[annotationId];
|
|
69
|
+
if (annotation && annotation.replies) {
|
|
70
|
+
annotation.replies = annotation.replies.map(existing =>
|
|
71
|
+
existing.id === reply.id ? reply : existing,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
.addCase(deleteReplyAction.fulfilled, (state, { payload: { annotationId, replyId } }) => {
|
|
76
|
+
const annotation = state[annotationId];
|
|
77
|
+
if (annotation && annotation.replies) {
|
|
78
|
+
annotation.replies = annotation.replies.filter(existing => existing.id !== replyId);
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
.addCase(applySidebarAnnotationUpdateAction, (state, { payload }) => {
|
|
82
|
+
const existing = state[payload.id];
|
|
83
|
+
if (!existing) return;
|
|
84
|
+
Object.entries(payload).forEach(([key, value]) => {
|
|
85
|
+
if (value !== undefined) {
|
|
86
|
+
(existing as Record<string, unknown>)[key] = value;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
})
|
|
90
|
+
.addCase(applySidebarReplyCreateAction, (state, { payload: { annotationId, reply } }) => {
|
|
91
|
+
const annotation = state[annotationId];
|
|
92
|
+
if (!annotation) return;
|
|
93
|
+
const replies = annotation.replies ?? [];
|
|
94
|
+
if (replies.some(existing => existing.id === reply.id)) return;
|
|
95
|
+
annotation.replies = [...replies, reply];
|
|
96
|
+
})
|
|
97
|
+
.addCase(applySidebarReplyUpdateAction, (state, { payload: { annotationId, reply } }) => {
|
|
98
|
+
const annotation = state[annotationId];
|
|
99
|
+
if (!annotation || !annotation.replies) return;
|
|
100
|
+
annotation.replies = annotation.replies.map(existing =>
|
|
101
|
+
existing.id === reply.id ? { ...existing, ...reply } : existing,
|
|
102
|
+
);
|
|
103
|
+
})
|
|
104
|
+
.addCase(applySidebarReplyDeleteAction, (state, { payload: { annotationId, replyId } }) => {
|
|
105
|
+
const annotation = state[annotationId];
|
|
106
|
+
if (!annotation || !annotation.replies) return;
|
|
107
|
+
annotation.replies = annotation.replies.filter(existing => existing.id !== replyId);
|
|
108
|
+
})
|
|
61
109
|
.addCase(fetchAnnotationsAction.fulfilled, (state, { payload }) => {
|
|
62
110
|
payload.entries.forEach(annotation => {
|
|
63
111
|
state[annotation.id] = isDrawing(annotation) ? formatDrawing(annotation) : annotation;
|