@opensumi/ide-comments 2.21.7-rc-1670229502.0 → 2.21.7
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/lib/browser/comments-feature.registry.js.map +1 -1
- package/lib/browser/comments-thread.js +6 -6
- package/lib/browser/comments-thread.js.map +1 -1
- package/lib/browser/comments-zone.service.js.map +1 -1
- package/lib/browser/comments-zone.view.js +1 -1
- package/lib/browser/comments-zone.view.js.map +1 -1
- package/lib/browser/comments.contribution.js.map +1 -1
- package/lib/browser/comments.service.js.map +1 -1
- package/lib/browser/index.js.map +1 -1
- package/lib/common/index.d.ts +4 -4
- package/lib/common/index.d.ts.map +1 -1
- package/package.json +11 -12
- package/src/browser/comment-reactions.view.tsx +0 -109
- package/src/browser/comments-body.tsx +0 -57
- package/src/browser/comments-feature.registry.ts +0 -91
- package/src/browser/comments-item.view.tsx +0 -362
- package/src/browser/comments-panel.view.tsx +0 -127
- package/src/browser/comments-textarea.view.tsx +0 -194
- package/src/browser/comments-thread.ts +0 -300
- package/src/browser/comments-zone.service.ts +0 -29
- package/src/browser/comments-zone.view.tsx +0 -206
- package/src/browser/comments.contribution.ts +0 -196
- package/src/browser/comments.module.less +0 -210
- package/src/browser/comments.service.ts +0 -540
- package/src/browser/index.ts +0 -26
- package/src/browser/markdown.style.ts +0 -25
- package/src/browser/mentions.style.ts +0 -55
- package/src/common/index.ts +0 -707
- package/src/index.ts +0 -1
|
@@ -1,362 +0,0 @@
|
|
|
1
|
-
import { observer } from 'mobx-react-lite';
|
|
2
|
-
import React from 'react';
|
|
3
|
-
|
|
4
|
-
import { Button } from '@opensumi/ide-components';
|
|
5
|
-
import {
|
|
6
|
-
useInjectable,
|
|
7
|
-
localize,
|
|
8
|
-
IContextKeyService,
|
|
9
|
-
isUndefined,
|
|
10
|
-
IMarkdownString,
|
|
11
|
-
toLocalString,
|
|
12
|
-
toMarkdownHtml,
|
|
13
|
-
} from '@opensumi/ide-core-browser';
|
|
14
|
-
import { InlineActionBar } from '@opensumi/ide-core-browser/lib/components/actions';
|
|
15
|
-
import { AbstractMenuService, MenuId, IMenu } from '@opensumi/ide-core-browser/lib/menu/next';
|
|
16
|
-
|
|
17
|
-
import {
|
|
18
|
-
IThreadComment,
|
|
19
|
-
ICommentsCommentTitle,
|
|
20
|
-
CommentMode,
|
|
21
|
-
ICommentReply,
|
|
22
|
-
ICommentsCommentContext,
|
|
23
|
-
ICommentsZoneWidget,
|
|
24
|
-
ICommentsFeatureRegistry,
|
|
25
|
-
ICommentsThread,
|
|
26
|
-
} from '../common';
|
|
27
|
-
|
|
28
|
-
import { CommentReactions, CommentReactionSwitcher } from './comment-reactions.view';
|
|
29
|
-
import { CommentsBody } from './comments-body';
|
|
30
|
-
import { CommentsTextArea } from './comments-textarea.view';
|
|
31
|
-
import styles from './comments.module.less';
|
|
32
|
-
|
|
33
|
-
// TODO: 更好的时间格式化组件
|
|
34
|
-
const Timestamp: React.FC<{ timestamp: string }> = ({ timestamp }) => {
|
|
35
|
-
const formatTimestamp = React.useMemo(() => {
|
|
36
|
-
const date = new Date(timestamp);
|
|
37
|
-
return toLocalString(date);
|
|
38
|
-
}, [timestamp]);
|
|
39
|
-
|
|
40
|
-
return <span className={styles.comment_item_timestamp}>{formatTimestamp}</span>;
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
const useCommentContext = (
|
|
44
|
-
contextKeyService: IContextKeyService,
|
|
45
|
-
comment: IThreadComment,
|
|
46
|
-
): [
|
|
47
|
-
string | IMarkdownString,
|
|
48
|
-
React.Dispatch<React.SetStateAction<string | IMarkdownString>>,
|
|
49
|
-
(event: React.ChangeEvent<HTMLTextAreaElement>) => void,
|
|
50
|
-
IMenu,
|
|
51
|
-
IMenu,
|
|
52
|
-
(files: FileList) => Promise<void>,
|
|
53
|
-
] => {
|
|
54
|
-
const menuService = useInjectable<AbstractMenuService>(AbstractMenuService);
|
|
55
|
-
const { body, contextValue } = comment;
|
|
56
|
-
const [textValue, setTextValue] = React.useState<string | IMarkdownString>('');
|
|
57
|
-
const commentsFeatureRegistry = useInjectable<ICommentsFeatureRegistry>(ICommentsFeatureRegistry);
|
|
58
|
-
const fileUploadHandler = React.useMemo(() => commentsFeatureRegistry.getFileUploadHandler(), []);
|
|
59
|
-
// set textValue when body changed
|
|
60
|
-
React.useEffect(() => {
|
|
61
|
-
setTextValue(body);
|
|
62
|
-
}, [body]);
|
|
63
|
-
|
|
64
|
-
// Each comment has its own commentContext and commentTitleContext.
|
|
65
|
-
const commentContextService = React.useMemo(() => contextKeyService.createScoped(), []);
|
|
66
|
-
// it's value will true when textarea is empty
|
|
67
|
-
const commentIsEmptyContext = React.useMemo(
|
|
68
|
-
() => commentContextService.createKey<boolean>('commentIsEmpty', !comment.body),
|
|
69
|
-
[],
|
|
70
|
-
);
|
|
71
|
-
// below the comment textarea
|
|
72
|
-
const commentContext = React.useMemo(
|
|
73
|
-
() => menuService.createMenu(MenuId.CommentsCommentContext, commentContextService),
|
|
74
|
-
[],
|
|
75
|
-
);
|
|
76
|
-
// after the comment body
|
|
77
|
-
const commentTitleContext = React.useMemo(
|
|
78
|
-
() => menuService.createMenu(MenuId.CommentsCommentTitle, commentContextService),
|
|
79
|
-
[],
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
const itemCommentContext = React.useRef(commentContextService.createKey('comment', contextValue));
|
|
83
|
-
|
|
84
|
-
React.useEffect(() => {
|
|
85
|
-
itemCommentContext.current.set(contextValue);
|
|
86
|
-
}, [contextValue]);
|
|
87
|
-
|
|
88
|
-
const onChangeTextArea = React.useCallback((event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
89
|
-
commentIsEmptyContext.set(!event.target.value);
|
|
90
|
-
setTextValue(event.target.value);
|
|
91
|
-
}, []);
|
|
92
|
-
|
|
93
|
-
const handleDragFiles = React.useCallback(
|
|
94
|
-
async (files: FileList) => {
|
|
95
|
-
if (fileUploadHandler) {
|
|
96
|
-
const appendText = await fileUploadHandler(textValue, files);
|
|
97
|
-
setTextValue((text) => {
|
|
98
|
-
const value = text + appendText;
|
|
99
|
-
commentIsEmptyContext.set(!value);
|
|
100
|
-
return value;
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
},
|
|
104
|
-
[textValue],
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
return [textValue, setTextValue, onChangeTextArea, commentContext, commentTitleContext, handleDragFiles];
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
const ReplyItem: React.FC<{
|
|
111
|
-
reply: IThreadComment;
|
|
112
|
-
thread: ICommentsThread;
|
|
113
|
-
}> = observer(({ reply, thread }) => {
|
|
114
|
-
const { contextKeyService } = thread;
|
|
115
|
-
const { author, label, body, mode, timestamp } = reply;
|
|
116
|
-
const iconUrl = author.iconPath?.toString();
|
|
117
|
-
const [textValue, setTextValue, onChangeTextArea, commentContext, commentTitleContext, handleDragFiles] =
|
|
118
|
-
useCommentContext(contextKeyService, reply);
|
|
119
|
-
|
|
120
|
-
// 判断是正常 Inline Text 还是 Markdown Text
|
|
121
|
-
const isInlineText = React.useMemo(() => {
|
|
122
|
-
const parsedStr = toMarkdownHtml(typeof body === 'string' ? body : body.value);
|
|
123
|
-
// 解析出来非纯p标签的则为Markdown Text
|
|
124
|
-
const isInline = /^\<p\>[^<>]+\<\/p\>\n$/.test(parsedStr);
|
|
125
|
-
return isInline;
|
|
126
|
-
}, [body]);
|
|
127
|
-
|
|
128
|
-
return (
|
|
129
|
-
<div className={styles.reply_item}>
|
|
130
|
-
{isUndefined(mode) || mode === CommentMode.Preview ? (
|
|
131
|
-
<div>
|
|
132
|
-
{isInlineText ? (
|
|
133
|
-
<>
|
|
134
|
-
{iconUrl && <img className={styles.reply_item_icon} src={iconUrl} alt={author.name} />}
|
|
135
|
-
<span className={styles.comment_item_author_name}>{author.name}</span>
|
|
136
|
-
{timestamp && <Timestamp timestamp={timestamp} />}
|
|
137
|
-
{typeof label === 'string' ? <span className={styles.comment_item_label}>{label}</span> : label}
|
|
138
|
-
{' : '}
|
|
139
|
-
<span className={styles.comment_item_body}>{body}</span>
|
|
140
|
-
{reply.reactions && reply.reactions.length > 0 && (
|
|
141
|
-
<CommentReactionSwitcher className={styles.reply_item_title} thread={thread} comment={reply} />
|
|
142
|
-
)}
|
|
143
|
-
<InlineActionBar<ICommentsCommentTitle>
|
|
144
|
-
separator='inline'
|
|
145
|
-
className={styles.reply_item_title}
|
|
146
|
-
menus={commentTitleContext}
|
|
147
|
-
context={[
|
|
148
|
-
{
|
|
149
|
-
thread,
|
|
150
|
-
comment: reply,
|
|
151
|
-
menuId: MenuId.CommentsCommentTitle,
|
|
152
|
-
},
|
|
153
|
-
]}
|
|
154
|
-
type='icon'
|
|
155
|
-
/>
|
|
156
|
-
</>
|
|
157
|
-
) : (
|
|
158
|
-
<>
|
|
159
|
-
<div className={styles.comment_item_markdown_header}>
|
|
160
|
-
<div>
|
|
161
|
-
{iconUrl && <img className={styles.reply_item_icon} src={iconUrl} alt={author.name} />}
|
|
162
|
-
<span className={styles.comment_item_author_name}>{author.name}</span>
|
|
163
|
-
{timestamp && <Timestamp timestamp={timestamp} />}
|
|
164
|
-
{typeof label === 'string' ? <span className={styles.comment_item_label}>{label}</span> : label}
|
|
165
|
-
{' : '}
|
|
166
|
-
</div>
|
|
167
|
-
<InlineActionBar<ICommentsCommentTitle>
|
|
168
|
-
separator='inline'
|
|
169
|
-
className={styles.reply_item_title}
|
|
170
|
-
menus={commentTitleContext}
|
|
171
|
-
context={[
|
|
172
|
-
{
|
|
173
|
-
thread,
|
|
174
|
-
comment: reply,
|
|
175
|
-
menuId: MenuId.CommentsCommentTitle,
|
|
176
|
-
},
|
|
177
|
-
]}
|
|
178
|
-
type='icon'
|
|
179
|
-
/>
|
|
180
|
-
</div>
|
|
181
|
-
<CommentsBody body={body} />
|
|
182
|
-
</>
|
|
183
|
-
)}
|
|
184
|
-
</div>
|
|
185
|
-
) : (
|
|
186
|
-
<div>
|
|
187
|
-
<CommentsTextArea
|
|
188
|
-
value={typeof textValue === 'string' ? textValue : textValue.value}
|
|
189
|
-
autoFocus={true}
|
|
190
|
-
onChange={onChangeTextArea}
|
|
191
|
-
dragFiles={handleDragFiles}
|
|
192
|
-
/>
|
|
193
|
-
<InlineActionBar<ICommentsCommentContext>
|
|
194
|
-
className={styles.comment_item_reply}
|
|
195
|
-
menus={commentContext}
|
|
196
|
-
context={[
|
|
197
|
-
{
|
|
198
|
-
thread,
|
|
199
|
-
comment: reply,
|
|
200
|
-
body: textValue,
|
|
201
|
-
menuId: MenuId.CommentsCommentContext,
|
|
202
|
-
},
|
|
203
|
-
]}
|
|
204
|
-
type='button'
|
|
205
|
-
separator='inline'
|
|
206
|
-
afterClick={() => {
|
|
207
|
-
// restore textarea value
|
|
208
|
-
setTextValue(body);
|
|
209
|
-
}}
|
|
210
|
-
/>
|
|
211
|
-
</div>
|
|
212
|
-
)}
|
|
213
|
-
{reply.reactions && reply.reactions.length > 0 && <CommentReactions thread={thread} comment={reply} />}
|
|
214
|
-
</div>
|
|
215
|
-
);
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
export const CommentItem: React.FC<{
|
|
219
|
-
thread: ICommentsThread;
|
|
220
|
-
commentThreadContext: IMenu;
|
|
221
|
-
widget: ICommentsZoneWidget;
|
|
222
|
-
}> = observer(({ thread, commentThreadContext, widget }) => {
|
|
223
|
-
const { readOnly, contextKeyService } = thread;
|
|
224
|
-
const [showReply, setShowReply] = React.useState(false);
|
|
225
|
-
const [replyText, setReplyText] = React.useState('');
|
|
226
|
-
const [comment, ...replies] = thread.comments;
|
|
227
|
-
const { author, label, body, mode, timestamp } = comment;
|
|
228
|
-
const iconUrl = author.iconPath?.toString();
|
|
229
|
-
const [textValue, setTextValue, onChangeTextArea, commentContext, commentTitleContext, handleDragFiles] =
|
|
230
|
-
useCommentContext(contextKeyService, comment);
|
|
231
|
-
const commentsFeatureRegistry = useInjectable<ICommentsFeatureRegistry>(ICommentsFeatureRegistry);
|
|
232
|
-
const fileUploadHandler = React.useMemo(() => commentsFeatureRegistry.getFileUploadHandler(), []);
|
|
233
|
-
const replyIsEmptyContext = React.useMemo(() => contextKeyService.createKey<boolean>('commentIsEmpty', true), []);
|
|
234
|
-
|
|
235
|
-
// modify reply
|
|
236
|
-
function onChangeReply(event: React.ChangeEvent<HTMLTextAreaElement>) {
|
|
237
|
-
replyIsEmptyContext.set(!event.target.value);
|
|
238
|
-
setReplyText(event.target.value);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
const handleDragFilesToReply = React.useCallback(
|
|
242
|
-
async (files: FileList) => {
|
|
243
|
-
if (fileUploadHandler) {
|
|
244
|
-
const appendText = await fileUploadHandler(textValue, files);
|
|
245
|
-
setReplyText((text) => {
|
|
246
|
-
const value = text + appendText;
|
|
247
|
-
replyIsEmptyContext.set(!value);
|
|
248
|
-
return value;
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
},
|
|
252
|
-
[replyText],
|
|
253
|
-
);
|
|
254
|
-
|
|
255
|
-
return (
|
|
256
|
-
<div className={styles.comment_item}>
|
|
257
|
-
{iconUrl && <img className={styles.comment_item_icon} src={iconUrl} alt={author.name} />}
|
|
258
|
-
<div className={styles.comment_item_content}>
|
|
259
|
-
<div className={styles.comment_item_head}>
|
|
260
|
-
<div className={styles.comment_item_name}>
|
|
261
|
-
<span className={styles.comment_item_author_name}>{author.name}</span>
|
|
262
|
-
{timestamp && <Timestamp timestamp={timestamp} />}
|
|
263
|
-
{typeof label === 'string' ? <span className={styles.comment_item_label}>{label}</span> : label}
|
|
264
|
-
</div>
|
|
265
|
-
<div className={styles.comment_item_actions}>
|
|
266
|
-
{comment.reactions && comment.reactions.length > 0 && (
|
|
267
|
-
<CommentReactionSwitcher thread={thread} comment={comment} />
|
|
268
|
-
)}
|
|
269
|
-
{!readOnly && (
|
|
270
|
-
<Button
|
|
271
|
-
className={styles.comment_item_reply_button}
|
|
272
|
-
size='small'
|
|
273
|
-
type='secondary'
|
|
274
|
-
onClick={() => setShowReply(true)}
|
|
275
|
-
>
|
|
276
|
-
{localize('comments.thread.action.reply')}
|
|
277
|
-
</Button>
|
|
278
|
-
)}
|
|
279
|
-
<InlineActionBar<ICommentsCommentTitle>
|
|
280
|
-
menus={commentTitleContext}
|
|
281
|
-
context={[
|
|
282
|
-
{
|
|
283
|
-
thread,
|
|
284
|
-
comment,
|
|
285
|
-
menuId: MenuId.CommentsCommentTitle,
|
|
286
|
-
},
|
|
287
|
-
]}
|
|
288
|
-
type='button'
|
|
289
|
-
/>
|
|
290
|
-
</div>
|
|
291
|
-
</div>
|
|
292
|
-
{isUndefined(mode) || mode === CommentMode.Preview ? (
|
|
293
|
-
<CommentsBody body={body} />
|
|
294
|
-
) : (
|
|
295
|
-
<div>
|
|
296
|
-
<CommentsTextArea
|
|
297
|
-
value={typeof textValue === 'string' ? textValue : textValue.value}
|
|
298
|
-
autoFocus={true}
|
|
299
|
-
onChange={onChangeTextArea}
|
|
300
|
-
dragFiles={handleDragFiles}
|
|
301
|
-
/>
|
|
302
|
-
<InlineActionBar<ICommentsCommentContext>
|
|
303
|
-
className={styles.comment_item_context}
|
|
304
|
-
menus={commentContext}
|
|
305
|
-
context={[
|
|
306
|
-
{
|
|
307
|
-
thread,
|
|
308
|
-
comment,
|
|
309
|
-
body: textValue,
|
|
310
|
-
menuId: MenuId.CommentsCommentContext,
|
|
311
|
-
},
|
|
312
|
-
]}
|
|
313
|
-
separator='inline'
|
|
314
|
-
type='button'
|
|
315
|
-
afterClick={() => {
|
|
316
|
-
// restore textarea value
|
|
317
|
-
setTextValue(body);
|
|
318
|
-
}}
|
|
319
|
-
/>
|
|
320
|
-
</div>
|
|
321
|
-
)}
|
|
322
|
-
{comment.reactions && comment.reactions.length > 0 && <CommentReactions thread={thread} comment={comment} />}
|
|
323
|
-
{(replies.length > 0 || showReply) && (
|
|
324
|
-
<div className={styles.comment_item_reply_wrap}>
|
|
325
|
-
{replies.map((reply) => (
|
|
326
|
-
<ReplyItem key={reply.id} thread={thread} reply={reply} />
|
|
327
|
-
))}
|
|
328
|
-
{showReply && (
|
|
329
|
-
<div>
|
|
330
|
-
<CommentsTextArea
|
|
331
|
-
autoFocus={true}
|
|
332
|
-
value={replyText}
|
|
333
|
-
onChange={onChangeReply}
|
|
334
|
-
placeholder={`${localize('comments.reply.placeholder')}...`}
|
|
335
|
-
dragFiles={handleDragFilesToReply}
|
|
336
|
-
/>
|
|
337
|
-
<InlineActionBar<ICommentReply>
|
|
338
|
-
className={styles.comment_item_reply}
|
|
339
|
-
menus={commentThreadContext}
|
|
340
|
-
context={[
|
|
341
|
-
{
|
|
342
|
-
thread,
|
|
343
|
-
text: replyText,
|
|
344
|
-
widget,
|
|
345
|
-
menuId: MenuId.CommentsCommentThreadContext,
|
|
346
|
-
},
|
|
347
|
-
]}
|
|
348
|
-
separator='inline'
|
|
349
|
-
type='button'
|
|
350
|
-
afterClick={() => {
|
|
351
|
-
setReplyText('');
|
|
352
|
-
setShowReply(false);
|
|
353
|
-
}}
|
|
354
|
-
/>
|
|
355
|
-
</div>
|
|
356
|
-
)}
|
|
357
|
-
</div>
|
|
358
|
-
)}
|
|
359
|
-
</div>
|
|
360
|
-
</div>
|
|
361
|
-
);
|
|
362
|
-
});
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import clx from 'classnames';
|
|
2
|
-
import { observer } from 'mobx-react-lite';
|
|
3
|
-
import React from 'react';
|
|
4
|
-
|
|
5
|
-
import { useInjectable, isUndefined, ViewState, IEventBus, localize } from '@opensumi/ide-core-browser';
|
|
6
|
-
import { DeprecatedRecycleTree } from '@opensumi/ide-core-browser/lib/components';
|
|
7
|
-
import { WorkbenchEditorService } from '@opensumi/ide-editor';
|
|
8
|
-
|
|
9
|
-
import { ICommentsService, ICommentsTreeNode, CommentPanelCollapse, ICommentsFeatureRegistry } from '../common';
|
|
10
|
-
|
|
11
|
-
import styles from './comments.module.less';
|
|
12
|
-
|
|
13
|
-
export const CommentsPanel = observer<{ viewState: ViewState; className?: string }>((props) => {
|
|
14
|
-
const commentsService = useInjectable<ICommentsService>(ICommentsService);
|
|
15
|
-
const workbenchEditorService = useInjectable<WorkbenchEditorService>(WorkbenchEditorService);
|
|
16
|
-
const commentsFeatureRegistry = useInjectable<ICommentsFeatureRegistry>(ICommentsFeatureRegistry);
|
|
17
|
-
const [treeNodes, setTreeNodes] = React.useState<ICommentsTreeNode[]>([]);
|
|
18
|
-
const eventBus: IEventBus = useInjectable(IEventBus);
|
|
19
|
-
|
|
20
|
-
React.useEffect(() => {
|
|
21
|
-
eventBus.on(CommentPanelCollapse, () => {
|
|
22
|
-
setTreeNodes((nodes) =>
|
|
23
|
-
nodes.map((node) => {
|
|
24
|
-
if (!isUndefined(node.expanded)) {
|
|
25
|
-
node.expanded = false;
|
|
26
|
-
}
|
|
27
|
-
return node;
|
|
28
|
-
}),
|
|
29
|
-
);
|
|
30
|
-
});
|
|
31
|
-
}, []);
|
|
32
|
-
|
|
33
|
-
const getRenderTree = React.useCallback(
|
|
34
|
-
(nodes: ICommentsTreeNode[]) =>
|
|
35
|
-
nodes.filter((node) => {
|
|
36
|
-
if (node && node.parent) {
|
|
37
|
-
if (node.parent.expanded === false || node.parent.parent?.expanded === false) {
|
|
38
|
-
return false;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
return true;
|
|
42
|
-
}),
|
|
43
|
-
[],
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
React.useEffect(() => {
|
|
47
|
-
setTreeNodes(commentsService.commentsTreeNodes);
|
|
48
|
-
}, [commentsService.commentsTreeNodes]);
|
|
49
|
-
|
|
50
|
-
const handleSelect = React.useCallback(
|
|
51
|
-
([item]: [ICommentsTreeNode]) => {
|
|
52
|
-
// 可能点击到空白位置
|
|
53
|
-
if (!item) {
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (!isUndefined(item.expanded)) {
|
|
58
|
-
const newNodes = treeNodes.map((node) => {
|
|
59
|
-
if (node.id === item.id) {
|
|
60
|
-
node.expanded = !node.expanded;
|
|
61
|
-
}
|
|
62
|
-
node.selected = node.id === item.id;
|
|
63
|
-
return node;
|
|
64
|
-
});
|
|
65
|
-
setTreeNodes(newNodes);
|
|
66
|
-
} else {
|
|
67
|
-
const newNodes = treeNodes.map((node) => {
|
|
68
|
-
node.selected = node.id === item.id;
|
|
69
|
-
return node;
|
|
70
|
-
});
|
|
71
|
-
setTreeNodes(newNodes);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (item.onSelect) {
|
|
75
|
-
item.onSelect(item);
|
|
76
|
-
} else {
|
|
77
|
-
workbenchEditorService.open(item.uri!, {
|
|
78
|
-
range: item.thread.range,
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
},
|
|
82
|
-
[workbenchEditorService, treeNodes],
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
const commentsPanelOptions = React.useMemo(() => commentsFeatureRegistry.getCommentsPanelOptions(), []);
|
|
86
|
-
|
|
87
|
-
const headerComponent = React.useMemo(() => commentsPanelOptions.header, [commentsPanelOptions]);
|
|
88
|
-
|
|
89
|
-
const treeHeight = React.useMemo(
|
|
90
|
-
() => props.viewState.height - (headerComponent?.height || 0),
|
|
91
|
-
[props.viewState.height],
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
const scrollContainerStyle = React.useMemo(
|
|
95
|
-
() => ({
|
|
96
|
-
width: '100%',
|
|
97
|
-
height: treeHeight,
|
|
98
|
-
}),
|
|
99
|
-
[treeHeight],
|
|
100
|
-
);
|
|
101
|
-
|
|
102
|
-
const defaultPlaceholder = React.useMemo(() => commentsPanelOptions.defaultPlaceholder, [commentsPanelOptions]);
|
|
103
|
-
|
|
104
|
-
const nodes = getRenderTree(treeNodes);
|
|
105
|
-
|
|
106
|
-
return (
|
|
107
|
-
<div className={clx(props.className, styles.comment_panel)}>
|
|
108
|
-
{headerComponent?.component}
|
|
109
|
-
{nodes.length ? (
|
|
110
|
-
<DeprecatedRecycleTree
|
|
111
|
-
containerHeight={treeHeight}
|
|
112
|
-
scrollContainerStyle={scrollContainerStyle}
|
|
113
|
-
nodes={nodes}
|
|
114
|
-
foldable={true}
|
|
115
|
-
outline={false}
|
|
116
|
-
onSelect={(item) => handleSelect(item)}
|
|
117
|
-
leftPadding={20}
|
|
118
|
-
{...commentsPanelOptions.recycleTreeProps}
|
|
119
|
-
/>
|
|
120
|
-
) : !defaultPlaceholder || typeof defaultPlaceholder === 'string' ? (
|
|
121
|
-
<div className={styles.panel_placeholder}>{defaultPlaceholder || localize('comments.panel.placeholder')}</div>
|
|
122
|
-
) : (
|
|
123
|
-
defaultPlaceholder
|
|
124
|
-
)}
|
|
125
|
-
</div>
|
|
126
|
-
);
|
|
127
|
-
});
|
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { MentionsInput, Mention } from 'react-mentions';
|
|
3
|
-
|
|
4
|
-
import { Tabs } from '@opensumi/ide-components';
|
|
5
|
-
import { localize, useInjectable } from '@opensumi/ide-core-browser';
|
|
6
|
-
|
|
7
|
-
import { ICommentsFeatureRegistry } from '../common';
|
|
8
|
-
|
|
9
|
-
import { CommentsBody } from './comments-body';
|
|
10
|
-
import styles from './comments.module.less';
|
|
11
|
-
import { getMentionBoxStyle } from './mentions.style';
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
export interface ICommentTextAreaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
|
|
15
|
-
focusDelay?: number;
|
|
16
|
-
minRows?: number;
|
|
17
|
-
maxRows?: number;
|
|
18
|
-
initialHeight?: string;
|
|
19
|
-
value: string;
|
|
20
|
-
dragFiles?: (files: FileList) => void;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const defaultTrigger = '@';
|
|
24
|
-
const defaultMarkup = '@[__display__](__id__)';
|
|
25
|
-
const defaultDisplayTransform = (id: string, display: string) => `@${display}`;
|
|
26
|
-
|
|
27
|
-
export const CommentsTextArea = React.forwardRef<HTMLTextAreaElement, ICommentTextAreaProps>((props, ref) => {
|
|
28
|
-
const {
|
|
29
|
-
focusDelay = 0,
|
|
30
|
-
autoFocus = false,
|
|
31
|
-
placeholder = '',
|
|
32
|
-
onFocus,
|
|
33
|
-
onBlur,
|
|
34
|
-
onChange,
|
|
35
|
-
maxRows = 10,
|
|
36
|
-
minRows = 2,
|
|
37
|
-
value,
|
|
38
|
-
initialHeight,
|
|
39
|
-
dragFiles,
|
|
40
|
-
} = props;
|
|
41
|
-
const [index, setIndex] = React.useState(0);
|
|
42
|
-
const commentsFeatureRegistry = useInjectable<ICommentsFeatureRegistry>(ICommentsFeatureRegistry);
|
|
43
|
-
const inputRef = React.useRef<HTMLTextAreaElement | null>(null);
|
|
44
|
-
const mentionsRef = React.useRef<HTMLDivElement | null>(null);
|
|
45
|
-
const itemRef = React.useRef<HTMLDivElement | null>(null);
|
|
46
|
-
// make `ref` to input works
|
|
47
|
-
React.useImperativeHandle(ref, () => inputRef.current!);
|
|
48
|
-
|
|
49
|
-
const handleFileSelect = React.useCallback(
|
|
50
|
-
async (event: DragEvent) => {
|
|
51
|
-
event.stopPropagation();
|
|
52
|
-
event.preventDefault();
|
|
53
|
-
|
|
54
|
-
const files = event.dataTransfer?.files; // FileList object.
|
|
55
|
-
if (files && dragFiles) {
|
|
56
|
-
await dragFiles(files);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (inputRef.current) {
|
|
60
|
-
inputRef.current.focus();
|
|
61
|
-
selectLastPosition(inputRef.current.value);
|
|
62
|
-
}
|
|
63
|
-
},
|
|
64
|
-
[dragFiles],
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
const handleDragOver = React.useCallback((event) => {
|
|
68
|
-
event.stopPropagation();
|
|
69
|
-
event.preventDefault();
|
|
70
|
-
event.dataTransfer.dropEffect = 'copy';
|
|
71
|
-
}, []);
|
|
72
|
-
|
|
73
|
-
const selectLastPosition = React.useCallback((value) => {
|
|
74
|
-
const textarea = inputRef.current;
|
|
75
|
-
if (textarea) {
|
|
76
|
-
const position = value.toString().length;
|
|
77
|
-
textarea.setSelectionRange(position, position);
|
|
78
|
-
}
|
|
79
|
-
}, []);
|
|
80
|
-
|
|
81
|
-
React.useEffect(() => {
|
|
82
|
-
const textarea = inputRef.current;
|
|
83
|
-
if (!textarea) {
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
if (initialHeight && textarea.style) {
|
|
87
|
-
textarea.style.height = initialHeight;
|
|
88
|
-
}
|
|
89
|
-
if (focusDelay) {
|
|
90
|
-
setTimeout(() => {
|
|
91
|
-
textarea.focus({
|
|
92
|
-
preventScroll: true,
|
|
93
|
-
});
|
|
94
|
-
}, focusDelay);
|
|
95
|
-
}
|
|
96
|
-
// auto set last selection
|
|
97
|
-
selectLastPosition(value);
|
|
98
|
-
function handleMouseWheel(event: Event) {
|
|
99
|
-
const target = event.target as Element;
|
|
100
|
-
if (target) {
|
|
101
|
-
if (
|
|
102
|
-
// 当前文本框出现滚动时,防止被编辑器滚动拦截,阻止冒泡
|
|
103
|
-
(target.nodeName.toUpperCase() === 'TEXTAREA' && target.scrollHeight > target.clientHeight) ||
|
|
104
|
-
// 当是在弹出的提及里滚动,防止被编辑器滚动拦截,阻止冒泡
|
|
105
|
-
target.nodeName.toUpperCase() === 'UL' ||
|
|
106
|
-
target.parentElement?.nodeName.toUpperCase() === 'UL' ||
|
|
107
|
-
target.parentElement?.parentElement?.nodeName.toUpperCase() === 'UL'
|
|
108
|
-
) {
|
|
109
|
-
event.stopPropagation();
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
mentionsRef.current?.addEventListener('mousewheel', handleMouseWheel, true);
|
|
114
|
-
return () => {
|
|
115
|
-
mentionsRef.current?.removeEventListener('mousewheel', handleMouseWheel, true);
|
|
116
|
-
};
|
|
117
|
-
}, []);
|
|
118
|
-
|
|
119
|
-
React.useEffect(() => {
|
|
120
|
-
if (index === 0) {
|
|
121
|
-
setTimeout(() => {
|
|
122
|
-
inputRef.current?.focus({
|
|
123
|
-
preventScroll: true,
|
|
124
|
-
});
|
|
125
|
-
}, focusDelay);
|
|
126
|
-
selectLastPosition(value);
|
|
127
|
-
}
|
|
128
|
-
}, [index]);
|
|
129
|
-
|
|
130
|
-
const style = React.useMemo(
|
|
131
|
-
() =>
|
|
132
|
-
getMentionBoxStyle({
|
|
133
|
-
minRows,
|
|
134
|
-
maxRows,
|
|
135
|
-
}),
|
|
136
|
-
[minRows, maxRows],
|
|
137
|
-
);
|
|
138
|
-
|
|
139
|
-
const mentionsOptions = React.useMemo(() => commentsFeatureRegistry.getMentionsOptions(), [commentsFeatureRegistry]);
|
|
140
|
-
|
|
141
|
-
const providerData = React.useCallback(
|
|
142
|
-
async (query: string, callback) => {
|
|
143
|
-
if (mentionsOptions.providerData) {
|
|
144
|
-
const data = await mentionsOptions.providerData(query);
|
|
145
|
-
callback(data);
|
|
146
|
-
} else {
|
|
147
|
-
callback([]);
|
|
148
|
-
}
|
|
149
|
-
},
|
|
150
|
-
[mentionsOptions],
|
|
151
|
-
);
|
|
152
|
-
|
|
153
|
-
return (
|
|
154
|
-
<div className={styles.textarea_container}>
|
|
155
|
-
<Tabs
|
|
156
|
-
mini
|
|
157
|
-
value={index}
|
|
158
|
-
onChange={(index: number) => setIndex(index)}
|
|
159
|
-
tabs={[localize('comments.thread.textarea.write'), localize('comments.thread.textarea.preview')]}
|
|
160
|
-
/>
|
|
161
|
-
<div>
|
|
162
|
-
{index === 0 ? (
|
|
163
|
-
<div ref={mentionsRef}>
|
|
164
|
-
<MentionsInput
|
|
165
|
-
autoFocus={autoFocus}
|
|
166
|
-
onDragOver={handleDragOver}
|
|
167
|
-
onDrop={handleFileSelect}
|
|
168
|
-
inputRef={inputRef}
|
|
169
|
-
ref={itemRef}
|
|
170
|
-
value={value}
|
|
171
|
-
placeholder={placeholder}
|
|
172
|
-
onChange={onChange}
|
|
173
|
-
onFocus={onFocus}
|
|
174
|
-
onBlur={onBlur}
|
|
175
|
-
style={style}
|
|
176
|
-
>
|
|
177
|
-
<Mention
|
|
178
|
-
markup={mentionsOptions.markup || defaultMarkup}
|
|
179
|
-
renderSuggestion={mentionsOptions.renderSuggestion}
|
|
180
|
-
trigger={defaultTrigger}
|
|
181
|
-
data={providerData}
|
|
182
|
-
displayTransform={mentionsOptions.displayTransform || defaultDisplayTransform}
|
|
183
|
-
/>
|
|
184
|
-
</MentionsInput>
|
|
185
|
-
</div>
|
|
186
|
-
) : (
|
|
187
|
-
<div className={styles.textarea_preview}>
|
|
188
|
-
<CommentsBody body={value} />
|
|
189
|
-
</div>
|
|
190
|
-
)}
|
|
191
|
-
</div>
|
|
192
|
-
</div>
|
|
193
|
-
);
|
|
194
|
-
});
|