@lark-apaas/coding-template-nestjs-react-fullstack 0.1.55-alpha.20261101083729 → 0.1.56
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 +1 -1
- package/template/client/src/components/business-ui/README.md +0 -2
- package/template/package-lock.json +13 -529
- package/template/package.json +4 -15
- package/template/client/src/components/business-ui/record-comments/README.md +0 -83
- package/template/client/src/components/business-ui/record-comments/app-source.ts +0 -182
- package/template/client/src/components/business-ui/record-comments/assets/reaction-sprite.png +0 -0
- package/template/client/src/components/business-ui/record-comments/comment-panel.contract.ts +0 -223
- package/template/client/src/components/business-ui/record-comments/comment-panel.tsx +0 -202
- package/template/client/src/components/business-ui/record-comments/index.ts +0 -7
- package/template/client/src/components/business-ui/record-comments/internal/composer.tsx +0 -529
- package/template/client/src/components/business-ui/record-comments/internal/content.tsx +0 -213
- package/template/client/src/components/business-ui/record-comments/internal/discussion.tsx +0 -535
- package/template/client/src/components/business-ui/record-comments/internal/editor.tsx +0 -675
- package/template/client/src/components/business-ui/record-comments/internal/icons.tsx +0 -148
- package/template/client/src/components/business-ui/record-comments/internal/images.tsx +0 -155
- package/template/client/src/components/business-ui/record-comments/internal/mentions.tsx +0 -163
- package/template/client/src/components/business-ui/record-comments/internal/model.ts +0 -581
- package/template/client/src/components/business-ui/record-comments/internal/mutations.ts +0 -170
- package/template/client/src/components/business-ui/record-comments/internal/reaction-catalog.ts +0 -196
- package/template/client/src/components/business-ui/record-comments/internal/reactions.tsx +0 -681
- package/template/client/src/components/business-ui/record-comments/internal/styles.css +0 -1109
- package/template/client/src/components/business-ui/record-comments/internal/suggestions.tsx +0 -456
- package/template/client/src/components/business-ui/record-comments/internal/view-types.ts +0 -82
- package/template/client/src/components/business-ui/record-comments/types.ts +0 -118
|
@@ -1,529 +0,0 @@
|
|
|
1
|
-
import { emptyComment, normalizeValue, readApiContent, COMMENT_THEME_CLASS, commentFailureMessage } from './model';
|
|
2
|
-
import type { CommentInputProps, CommentInputHandle, CommentEditorProps, CommentValue } from '../types';
|
|
3
|
-
import { useMentionContext } from './mentions';
|
|
4
|
-
import { CommentContent } from './content';
|
|
5
|
-
import { cn } from '@/lib/utils';
|
|
6
|
-
import { Button } from '@/components/ui/button';
|
|
7
|
-
import { ErrorBoundary } from 'react-error-boundary';
|
|
8
|
-
import * as React from 'react';
|
|
9
|
-
import { useEffect, useImperativeHandle, useCallback, useRef, useState, type ComponentType } from 'react';
|
|
10
|
-
import type { DiscussionDraft, DiscussionImage } from './view-types';
|
|
11
|
-
import { COMMENT_IMAGE_ACCEPT, prepareCommentImage } from './images';
|
|
12
|
-
import { DiscussionIcon } from './icons';
|
|
13
|
-
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
|
14
|
-
import { LockKeyhole, X } from 'lucide-react';
|
|
15
|
-
|
|
16
|
-
let modulePromise: Promise<typeof import('./editor')> | undefined;
|
|
17
|
-
function loadEditor() {
|
|
18
|
-
// Reset rejected imports so explicit retry actually executes import again.
|
|
19
|
-
return (modulePromise ||= import('./editor').catch((error) => {
|
|
20
|
-
modulePromise = undefined;
|
|
21
|
-
throw error;
|
|
22
|
-
}));
|
|
23
|
-
}
|
|
24
|
-
export function preloadCommentEditor() {
|
|
25
|
-
void loadEditor().catch(() => {});
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** Public entry contains no static import of Tiptap. A session change resets all work. */
|
|
29
|
-
export function CommentInput(props: CommentInputProps) {
|
|
30
|
-
const { store } = useMentionContext();
|
|
31
|
-
try {
|
|
32
|
-
normalizeValue(props.value);
|
|
33
|
-
} catch {
|
|
34
|
-
return (
|
|
35
|
-
<p role="alert" className="text-sm text-destructive">
|
|
36
|
-
评论内容格式无效,无法编辑
|
|
37
|
-
</p>
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
return (
|
|
41
|
-
<ErrorBoundary
|
|
42
|
-
key={JSON.stringify([store.source.scopeKey, props.documentKey])}
|
|
43
|
-
fallbackRender={({ resetErrorBoundary }) => (
|
|
44
|
-
<div
|
|
45
|
-
role="alert"
|
|
46
|
-
className="rounded-lg border p-3 text-sm text-destructive"
|
|
47
|
-
>
|
|
48
|
-
编辑器发生错误,输入内容已保留。
|
|
49
|
-
<Button
|
|
50
|
-
type="button"
|
|
51
|
-
variant="outline"
|
|
52
|
-
size="sm"
|
|
53
|
-
onClick={resetErrorBoundary}
|
|
54
|
-
>
|
|
55
|
-
重新加载
|
|
56
|
-
</Button>
|
|
57
|
-
</div>
|
|
58
|
-
)}
|
|
59
|
-
>
|
|
60
|
-
<InputSession {...props} />
|
|
61
|
-
</ErrorBoundary>
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function InputSession(props: CommentInputProps) {
|
|
66
|
-
const [Editor, setEditor] =
|
|
67
|
-
React.useState<ComponentType<CommentEditorProps> | null>(null);
|
|
68
|
-
const latest = useRef(props);
|
|
69
|
-
latest.current = props;
|
|
70
|
-
const editorRef = useRef<CommentInputHandle | null>(null);
|
|
71
|
-
const editable = useRef(true);
|
|
72
|
-
const [enabled, setEnabled] = useState(true);
|
|
73
|
-
const loadingRef = useRef(false);
|
|
74
|
-
const [loading, setLoading] = useState(false);
|
|
75
|
-
const [error, setError] = useState('');
|
|
76
|
-
const alive = useRef(true);
|
|
77
|
-
useEffect(() => {
|
|
78
|
-
alive.current = true;
|
|
79
|
-
return () => {
|
|
80
|
-
alive.current = false;
|
|
81
|
-
};
|
|
82
|
-
}, []);
|
|
83
|
-
const activate = useCallback(async () => {
|
|
84
|
-
if (loadingRef.current || latest.current.disabled || !editable.current)
|
|
85
|
-
return;
|
|
86
|
-
loadingRef.current = true;
|
|
87
|
-
setLoading(true);
|
|
88
|
-
setError('');
|
|
89
|
-
try {
|
|
90
|
-
const module = await loadEditor();
|
|
91
|
-
if (alive.current) setEditor(() => module.default);
|
|
92
|
-
} catch {
|
|
93
|
-
if (alive.current) setError('编辑器加载失败,请重试');
|
|
94
|
-
} finally {
|
|
95
|
-
loadingRef.current = false;
|
|
96
|
-
if (alive.current) setLoading(false);
|
|
97
|
-
}
|
|
98
|
-
}, []);
|
|
99
|
-
useImperativeHandle(
|
|
100
|
-
props.ref,
|
|
101
|
-
() => ({
|
|
102
|
-
getContent: () =>
|
|
103
|
-
editorRef.current?.getContent() ?? readApiContent(latest.current.value),
|
|
104
|
-
clear() {
|
|
105
|
-
if (!alive.current) return;
|
|
106
|
-
if (editorRef.current) editorRef.current.clear();
|
|
107
|
-
else {
|
|
108
|
-
const value = emptyComment();
|
|
109
|
-
latest.current = { ...latest.current, value };
|
|
110
|
-
latest.current.onChange(value);
|
|
111
|
-
}
|
|
112
|
-
},
|
|
113
|
-
setEditable(next) {
|
|
114
|
-
if (!alive.current) return;
|
|
115
|
-
editable.current = next;
|
|
116
|
-
editorRef.current?.setEditable(next && !latest.current.disabled);
|
|
117
|
-
setEnabled(next);
|
|
118
|
-
},
|
|
119
|
-
focus() {
|
|
120
|
-
if (!alive.current) return;
|
|
121
|
-
if (editorRef.current) editorRef.current.focus();
|
|
122
|
-
else void activate();
|
|
123
|
-
},
|
|
124
|
-
}),
|
|
125
|
-
[activate],
|
|
126
|
-
);
|
|
127
|
-
const { ref: _ref, ...editorProps } = props;
|
|
128
|
-
if (Editor)
|
|
129
|
-
return (
|
|
130
|
-
<Editor
|
|
131
|
-
{...editorProps}
|
|
132
|
-
disabled={props.disabled || !enabled}
|
|
133
|
-
editorRef={editorRef}
|
|
134
|
-
/>
|
|
135
|
-
);
|
|
136
|
-
return (
|
|
137
|
-
<div className={cn(COMMENT_THEME_CLASS, 'replica-input', props.className)}>
|
|
138
|
-
<div className="relative flex min-h-[34px] flex-col justify-center rounded-[6px] border-[0.5px] border-border bg-card px-3 py-[5px] transition-colors hover:border-primary focus-within:border-primary">
|
|
139
|
-
<div
|
|
140
|
-
aria-hidden
|
|
141
|
-
className="pointer-events-none w-full text-sm leading-[22px]"
|
|
142
|
-
>
|
|
143
|
-
{loading || error ? (
|
|
144
|
-
<span className="text-muted-foreground">
|
|
145
|
-
{loading ? '正在加载编辑器…' : '重试加载编辑器'}
|
|
146
|
-
</span>
|
|
147
|
-
) : props.value.content.length ? (
|
|
148
|
-
<CommentContent value={props.value} mentionVariant="pill" />
|
|
149
|
-
) : (
|
|
150
|
-
<span className="text-muted-foreground">
|
|
151
|
-
{props.placeholder || '输入评论'}
|
|
152
|
-
</span>
|
|
153
|
-
)}
|
|
154
|
-
</div>
|
|
155
|
-
<Button
|
|
156
|
-
type="button"
|
|
157
|
-
variant="ghost"
|
|
158
|
-
className="no-default-hover-elevate no-default-active-elevate absolute inset-0 h-full min-h-0 w-full rounded-[6px] p-0"
|
|
159
|
-
disabled={props.disabled || !enabled || loading}
|
|
160
|
-
aria-busy={loading}
|
|
161
|
-
onPointerEnter={preloadCommentEditor}
|
|
162
|
-
onFocus={preloadCommentEditor}
|
|
163
|
-
onClick={() => {
|
|
164
|
-
void activate();
|
|
165
|
-
}}
|
|
166
|
-
>
|
|
167
|
-
<span className="sr-only">
|
|
168
|
-
{error ? '重试加载编辑器' : props.placeholder || '输入评论'}
|
|
169
|
-
</span>
|
|
170
|
-
</Button>
|
|
171
|
-
</div>
|
|
172
|
-
{error ? (
|
|
173
|
-
<p role="alert" className="mt-2 text-xs text-destructive">
|
|
174
|
-
{error}
|
|
175
|
-
</p>
|
|
176
|
-
) : null}
|
|
177
|
-
</div>
|
|
178
|
-
);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
export function DiscussionComposer({
|
|
182
|
-
sessionKey,
|
|
183
|
-
initialValue,
|
|
184
|
-
initialImages = [],
|
|
185
|
-
placeholder = '添加新的评论...',
|
|
186
|
-
replyName,
|
|
187
|
-
replyMention,
|
|
188
|
-
replyIntentKey,
|
|
189
|
-
commentID,
|
|
190
|
-
onCancel,
|
|
191
|
-
onSubmit,
|
|
192
|
-
disabled,
|
|
193
|
-
enableDocuments,
|
|
194
|
-
editing = false,
|
|
195
|
-
enableImages = true,
|
|
196
|
-
enablePrivateComments = true,
|
|
197
|
-
}: {
|
|
198
|
-
sessionKey: string;
|
|
199
|
-
initialValue?: CommentValue;
|
|
200
|
-
initialImages?: DiscussionImage[];
|
|
201
|
-
placeholder?: string;
|
|
202
|
-
replyName?: string;
|
|
203
|
-
replyIntentKey?: string;
|
|
204
|
-
commentID?: string;
|
|
205
|
-
replyMention?: Extract<CommentValue['content'][number], { type: 'mention' }>;
|
|
206
|
-
onCancel?: () => void;
|
|
207
|
-
onSubmit: (draft: DiscussionDraft) => Promise<void> | void;
|
|
208
|
-
disabled?: boolean;
|
|
209
|
-
enableDocuments?: boolean;
|
|
210
|
-
editing?: boolean;
|
|
211
|
-
enableImages?: boolean;
|
|
212
|
-
enablePrivateComments?: boolean;
|
|
213
|
-
}) {
|
|
214
|
-
const [value, setValue] = useState<CommentValue>(
|
|
215
|
-
initialValue || emptyComment,
|
|
216
|
-
);
|
|
217
|
-
const [images, setImages] = useState(initialImages);
|
|
218
|
-
const [privateComment, setPrivate] = useState(false);
|
|
219
|
-
const [sending, setSending] = useState(false);
|
|
220
|
-
const [validating, setValidating] = useState(0);
|
|
221
|
-
const [error, setError] = useState('');
|
|
222
|
-
const input = useRef<CommentInputHandle>(null);
|
|
223
|
-
const fileInput = useRef<HTMLInputElement>(null);
|
|
224
|
-
const busy = useRef(false);
|
|
225
|
-
const pending = useRef(0);
|
|
226
|
-
const alive = useRef(true);
|
|
227
|
-
const ownedUrls = useRef(new Set<string>());
|
|
228
|
-
const ready =
|
|
229
|
-
images.length > 0 ||
|
|
230
|
-
value.content.some(
|
|
231
|
-
(part) =>
|
|
232
|
-
part.type === 'mention' ||
|
|
233
|
-
part.type === 'link' ||
|
|
234
|
-
(part.type === 'text' && part.text.trim()),
|
|
235
|
-
);
|
|
236
|
-
useEffect(() => {
|
|
237
|
-
alive.current = true;
|
|
238
|
-
return () => {
|
|
239
|
-
alive.current = false;
|
|
240
|
-
ownedUrls.current.forEach((url) => URL.revokeObjectURL(url));
|
|
241
|
-
};
|
|
242
|
-
}, []);
|
|
243
|
-
useEffect(() => {
|
|
244
|
-
if (editing || replyName) input.current?.focus();
|
|
245
|
-
}, [editing, replyName]);
|
|
246
|
-
const latestReplyMention = useRef(replyMention);
|
|
247
|
-
latestReplyMention.current = replyMention;
|
|
248
|
-
const latestValue = useRef(value);
|
|
249
|
-
latestValue.current = value;
|
|
250
|
-
const focusAfterReply = useRef(false);
|
|
251
|
-
useEffect(() => {
|
|
252
|
-
if (!replyIntentKey) return;
|
|
253
|
-
const mention = latestReplyMention.current;
|
|
254
|
-
const first = latestValue.current.content[0];
|
|
255
|
-
if (
|
|
256
|
-
mention &&
|
|
257
|
-
!(first?.type === 'mention' && first.ref.id === mention.ref.id)
|
|
258
|
-
) {
|
|
259
|
-
focusAfterReply.current = true;
|
|
260
|
-
setValue((previous) => ({
|
|
261
|
-
version: 1,
|
|
262
|
-
content: [mention, { type: 'text', text: ' ' }, ...previous.content],
|
|
263
|
-
}));
|
|
264
|
-
} else input.current?.focus();
|
|
265
|
-
}, [replyIntentKey]);
|
|
266
|
-
useEffect(() => {
|
|
267
|
-
if (focusAfterReply.current) {
|
|
268
|
-
focusAfterReply.current = false;
|
|
269
|
-
input.current?.focus();
|
|
270
|
-
}
|
|
271
|
-
}, [value]);
|
|
272
|
-
const submit = async () => {
|
|
273
|
-
if (!ready || busy.current || pending.current || disabled) return;
|
|
274
|
-
busy.current = true;
|
|
275
|
-
setSending(true);
|
|
276
|
-
setError('');
|
|
277
|
-
input.current?.setEditable(false);
|
|
278
|
-
// The host owns successful attachments, including when submitting unmounts this editor.
|
|
279
|
-
const transferred = images.filter((image) =>
|
|
280
|
-
ownedUrls.current.delete(image.url),
|
|
281
|
-
);
|
|
282
|
-
try {
|
|
283
|
-
await onSubmit({ value, images, private: privateComment, commentID });
|
|
284
|
-
if (!alive.current) return;
|
|
285
|
-
setImages([]);
|
|
286
|
-
setValue(emptyComment());
|
|
287
|
-
input.current?.clear();
|
|
288
|
-
setPrivate(false);
|
|
289
|
-
} catch (cause) {
|
|
290
|
-
transferred.forEach((image) => {
|
|
291
|
-
if (alive.current) ownedUrls.current.add(image.url);
|
|
292
|
-
else URL.revokeObjectURL(image.url);
|
|
293
|
-
});
|
|
294
|
-
if (alive.current)
|
|
295
|
-
setError(commentFailureMessage(cause, '发送失败,请重试'));
|
|
296
|
-
} finally {
|
|
297
|
-
busy.current = false;
|
|
298
|
-
if (alive.current) {
|
|
299
|
-
setSending(false);
|
|
300
|
-
input.current?.setEditable(true);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
};
|
|
304
|
-
const addFiles = async (files: FileList | File[]) => {
|
|
305
|
-
if (!enableImages || disabled || busy.current) return;
|
|
306
|
-
pending.current++;
|
|
307
|
-
setValidating(pending.current);
|
|
308
|
-
setError('');
|
|
309
|
-
const result = await Promise.allSettled(
|
|
310
|
-
Array.from(files).map((file) =>
|
|
311
|
-
prepareCommentImage(file, ownedUrls.current),
|
|
312
|
-
),
|
|
313
|
-
);
|
|
314
|
-
const accepted = result.flatMap((item) =>
|
|
315
|
-
item.status === 'fulfilled' ? [item.value] : [],
|
|
316
|
-
);
|
|
317
|
-
const failures = result.flatMap((item) =>
|
|
318
|
-
item.status === 'rejected'
|
|
319
|
-
? [item.reason instanceof Error ? item.reason.message : '图片无法读取']
|
|
320
|
-
: [],
|
|
321
|
-
);
|
|
322
|
-
pending.current--;
|
|
323
|
-
if (!alive.current) {
|
|
324
|
-
accepted.forEach((image) => {
|
|
325
|
-
ownedUrls.current.delete(image.url);
|
|
326
|
-
URL.revokeObjectURL(image.url);
|
|
327
|
-
});
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
setImages((previous) => [...previous, ...accepted]);
|
|
331
|
-
setError(failures.join(';'));
|
|
332
|
-
setValidating(pending.current);
|
|
333
|
-
input.current?.focus();
|
|
334
|
-
};
|
|
335
|
-
const removeImage = (image: DiscussionImage) => {
|
|
336
|
-
if (ownedUrls.current.delete(image.url)) URL.revokeObjectURL(image.url);
|
|
337
|
-
setImages((previous) => previous.filter((item) => item.id !== image.id));
|
|
338
|
-
};
|
|
339
|
-
const blocked = disabled || sending;
|
|
340
|
-
return (
|
|
341
|
-
<div
|
|
342
|
-
className={`replica-composer-area ${editing ? 'replica-edit-area' : ''}`}
|
|
343
|
-
onKeyDown={(event) => {
|
|
344
|
-
// Escape may dismiss suggestions or a file chooser; it must not detach a reply.
|
|
345
|
-
if (
|
|
346
|
-
event.key === 'Escape' &&
|
|
347
|
-
!event.defaultPrevented &&
|
|
348
|
-
editing &&
|
|
349
|
-
!busy.current
|
|
350
|
-
)
|
|
351
|
-
onCancel?.();
|
|
352
|
-
}}
|
|
353
|
-
>
|
|
354
|
-
<div
|
|
355
|
-
className={`replica-composer ${editing ? 'replica-composer-edit' : ''} ${privateComment ? 'replica-composer-private' : ''}`}
|
|
356
|
-
onPasteCapture={(event) => {
|
|
357
|
-
if (blocked || !enableImages) return;
|
|
358
|
-
if (event.clipboardData.files.length) {
|
|
359
|
-
event.preventDefault();
|
|
360
|
-
event.stopPropagation();
|
|
361
|
-
void addFiles(event.clipboardData.files);
|
|
362
|
-
}
|
|
363
|
-
}}
|
|
364
|
-
onDragOver={(event) => {
|
|
365
|
-
if (enableImages && event.dataTransfer.types.includes('Files'))
|
|
366
|
-
event.preventDefault();
|
|
367
|
-
}}
|
|
368
|
-
onDropCapture={(event) => {
|
|
369
|
-
if (enableImages && event.dataTransfer.files.length) {
|
|
370
|
-
event.preventDefault();
|
|
371
|
-
event.stopPropagation();
|
|
372
|
-
if (!blocked) void addFiles(event.dataTransfer.files);
|
|
373
|
-
}
|
|
374
|
-
}}
|
|
375
|
-
>
|
|
376
|
-
<CommentInput
|
|
377
|
-
ref={input}
|
|
378
|
-
documentKey={sessionKey}
|
|
379
|
-
value={value}
|
|
380
|
-
onChange={setValue}
|
|
381
|
-
placeholder={placeholder}
|
|
382
|
-
disabled={blocked}
|
|
383
|
-
enableDocuments={enableDocuments}
|
|
384
|
-
submitMode="mod-enter"
|
|
385
|
-
onSubmitRequest={() => {
|
|
386
|
-
void submit();
|
|
387
|
-
}}
|
|
388
|
-
/>
|
|
389
|
-
{images.length > 0 && (
|
|
390
|
-
<div className="replica-draft-images">
|
|
391
|
-
{images.map((image) => (
|
|
392
|
-
<div key={image.id} className="replica-draft-image">
|
|
393
|
-
<img src={image.url} alt={image.name} />
|
|
394
|
-
<button
|
|
395
|
-
type="button"
|
|
396
|
-
aria-label={`移除图片 ${image.name}`}
|
|
397
|
-
disabled={sending}
|
|
398
|
-
onClick={() => removeImage(image)}
|
|
399
|
-
>
|
|
400
|
-
<span>
|
|
401
|
-
<X size={13} />
|
|
402
|
-
</span>
|
|
403
|
-
</button>
|
|
404
|
-
</div>
|
|
405
|
-
))}
|
|
406
|
-
</div>
|
|
407
|
-
)}
|
|
408
|
-
<div className="replica-composer-toolbar">
|
|
409
|
-
{privateComment && (
|
|
410
|
-
<span className="replica-private-hint">
|
|
411
|
-
<LockKeyhole size={12} />
|
|
412
|
-
私有评论
|
|
413
|
-
</span>
|
|
414
|
-
)}
|
|
415
|
-
<div className="replica-composer-tools">
|
|
416
|
-
{enableImages && <><button
|
|
417
|
-
type="button"
|
|
418
|
-
className="replica-icon-button"
|
|
419
|
-
title="添加图片"
|
|
420
|
-
aria-label="添加图片"
|
|
421
|
-
disabled={blocked}
|
|
422
|
-
onClick={() => fileInput.current?.click()}
|
|
423
|
-
>
|
|
424
|
-
<DiscussionIcon name="image" width={16} height={16} />
|
|
425
|
-
</button>
|
|
426
|
-
<input
|
|
427
|
-
ref={fileInput}
|
|
428
|
-
type="file"
|
|
429
|
-
hidden
|
|
430
|
-
multiple
|
|
431
|
-
accept={COMMENT_IMAGE_ACCEPT}
|
|
432
|
-
onChange={(event) => {
|
|
433
|
-
if (event.target.files) void addFiles(event.target.files);
|
|
434
|
-
event.target.value = '';
|
|
435
|
-
}}
|
|
436
|
-
/>
|
|
437
|
-
</>}
|
|
438
|
-
{enablePrivateComments && !replyName && !editing && (
|
|
439
|
-
<Popover>
|
|
440
|
-
<PopoverTrigger asChild>
|
|
441
|
-
<button
|
|
442
|
-
type="button"
|
|
443
|
-
className="replica-icon-button"
|
|
444
|
-
title="更多"
|
|
445
|
-
aria-label="评论设置"
|
|
446
|
-
disabled={blocked}
|
|
447
|
-
>
|
|
448
|
-
<DiscussionIcon name="more" />
|
|
449
|
-
</button>
|
|
450
|
-
</PopoverTrigger>
|
|
451
|
-
<PopoverContent
|
|
452
|
-
data-replica-comment-overlay
|
|
453
|
-
side="top"
|
|
454
|
-
align="end"
|
|
455
|
-
sideOffset={8}
|
|
456
|
-
className={`${COMMENT_THEME_CLASS} replica-private-menu`}
|
|
457
|
-
>
|
|
458
|
-
<div className="replica-private-heading">
|
|
459
|
-
<LockKeyhole size={20} />
|
|
460
|
-
<span>私有评论</span>
|
|
461
|
-
<button
|
|
462
|
-
type="button"
|
|
463
|
-
className="replica-switch"
|
|
464
|
-
role="switch"
|
|
465
|
-
aria-label="私有评论"
|
|
466
|
-
aria-checked={privateComment}
|
|
467
|
-
onClick={() => setPrivate((previous) => !previous)}
|
|
468
|
-
>
|
|
469
|
-
<span />
|
|
470
|
-
</button>
|
|
471
|
-
</div>
|
|
472
|
-
<p>
|
|
473
|
-
开启后,仅提及的协作者、多维表格所有者、有多维表格管理权限的用户、记录创建人和最近修改人可见。
|
|
474
|
-
</p>
|
|
475
|
-
</PopoverContent>
|
|
476
|
-
</Popover>
|
|
477
|
-
)}
|
|
478
|
-
{!editing && (
|
|
479
|
-
<>
|
|
480
|
-
{(enableImages || (enablePrivateComments && !replyName)) && (
|
|
481
|
-
<span className="replica-toolbar-divider" />
|
|
482
|
-
)}
|
|
483
|
-
<button
|
|
484
|
-
type="button"
|
|
485
|
-
className="replica-send-button"
|
|
486
|
-
title="发送(⌘/Ctrl+Enter)"
|
|
487
|
-
aria-label="发送评论"
|
|
488
|
-
disabled={!ready || blocked || validating > 0}
|
|
489
|
-
onClick={() => {
|
|
490
|
-
void submit();
|
|
491
|
-
}}
|
|
492
|
-
>
|
|
493
|
-
<DiscussionIcon name="send" width={16} height={16} />
|
|
494
|
-
</button>
|
|
495
|
-
</>
|
|
496
|
-
)}
|
|
497
|
-
</div>
|
|
498
|
-
</div>
|
|
499
|
-
</div>
|
|
500
|
-
{editing && (
|
|
501
|
-
<div className="replica-edit-actions">
|
|
502
|
-
<button type="button" disabled={sending} onClick={onCancel}>
|
|
503
|
-
取消
|
|
504
|
-
</button>
|
|
505
|
-
<button
|
|
506
|
-
type="button"
|
|
507
|
-
aria-label="确认编辑"
|
|
508
|
-
disabled={!ready || blocked || validating > 0}
|
|
509
|
-
onClick={() => {
|
|
510
|
-
void submit();
|
|
511
|
-
}}
|
|
512
|
-
>
|
|
513
|
-
确认
|
|
514
|
-
</button>
|
|
515
|
-
</div>
|
|
516
|
-
)}
|
|
517
|
-
{validating > 0 && (
|
|
518
|
-
<span role="status" className="sr-only">
|
|
519
|
-
正在读取图片
|
|
520
|
-
</span>
|
|
521
|
-
)}
|
|
522
|
-
{error && (
|
|
523
|
-
<p className="replica-error" role="alert">
|
|
524
|
-
{error}
|
|
525
|
-
</p>
|
|
526
|
-
)}
|
|
527
|
-
</div>
|
|
528
|
-
);
|
|
529
|
-
}
|