@lark-apaas/coding-template-nestjs-react-fullstack 0.1.34 → 0.1.36-alpha.20260901035538

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.
Files changed (33) hide show
  1. package/package.json +1 -1
  2. package/template/client/src/components/ui/markdown.tsx +43 -0
  3. package/template/client/src/index.css +0 -1
  4. package/template/nest-cli.json +10 -9
  5. package/template/package-lock.json +10325 -18965
  6. package/template/package.json +7 -70
  7. package/template/scripts/build.sh +12 -0
  8. package/template/vite.config.ts +1 -1
  9. package/template/client/src/components/business-ui/tiptap-editor/README.md +0 -324
  10. package/template/client/src/components/business-ui/tiptap-editor/components/attachment-toolbar-button.tsx +0 -74
  11. package/template/client/src/components/business-ui/tiptap-editor/components/blockquote-toolbar-button.tsx +0 -41
  12. package/template/client/src/components/business-ui/tiptap-editor/components/code-block-toolbar-button.tsx +0 -41
  13. package/template/client/src/components/business-ui/tiptap-editor/components/color-highlight-toolbar-button.tsx +0 -141
  14. package/template/client/src/components/business-ui/tiptap-editor/components/heading-toolbar-button.tsx +0 -99
  15. package/template/client/src/components/business-ui/tiptap-editor/components/horizontal-rule-toolbar-button.tsx +0 -39
  16. package/template/client/src/components/business-ui/tiptap-editor/components/image-upload-toolbar-button.tsx +0 -65
  17. package/template/client/src/components/business-ui/tiptap-editor/components/link-edit-form.tsx +0 -127
  18. package/template/client/src/components/business-ui/tiptap-editor/components/link-hover-toolbar.tsx +0 -380
  19. package/template/client/src/components/business-ui/tiptap-editor/components/link-toolbar-button.tsx +0 -96
  20. package/template/client/src/components/business-ui/tiptap-editor/components/list-toolbar-button.tsx +0 -75
  21. package/template/client/src/components/business-ui/tiptap-editor/components/mark-toolbar-button.tsx +0 -118
  22. package/template/client/src/components/business-ui/tiptap-editor/components/text-align-toolbar-button.tsx +0 -84
  23. package/template/client/src/components/business-ui/tiptap-editor/components/undo-redo-toolbar-button.tsx +0 -54
  24. package/template/client/src/components/business-ui/tiptap-editor/extensions/attachment.tsx +0 -539
  25. package/template/client/src/components/business-ui/tiptap-editor/extensions/code-block-shiki.tsx +0 -236
  26. package/template/client/src/components/business-ui/tiptap-editor/extensions/complete-kit.ts +0 -260
  27. package/template/client/src/components/business-ui/tiptap-editor/extensions/image.tsx +0 -456
  28. package/template/client/src/components/business-ui/tiptap-editor/hooks/use-tiptap-editor.ts +0 -47
  29. package/template/client/src/components/business-ui/tiptap-editor/index.ts +0 -38
  30. package/template/client/src/components/business-ui/tiptap-editor/tiptap-editor-complete.tsx +0 -112
  31. package/template/client/src/components/business-ui/tiptap-editor/tiptap-editor.tsx +0 -169
  32. package/template/client/src/components/ui/streamdown.tsx +0 -186
  33. package/template/client/src/lib/shiki.ts +0 -92
@@ -1,84 +0,0 @@
1
- 'use client';
2
-
3
- import {
4
- AlignCenter,
5
- AlignLeft,
6
- AlignRight,
7
- Check,
8
- ChevronDown,
9
- } from 'lucide-react';
10
-
11
- import { useTiptapEditor } from '@/components/business-ui/tiptap-editor/hooks/use-tiptap-editor';
12
- import { cn } from '@/lib/utils';
13
- import { Button } from '@/components/ui/button';
14
- import {
15
- DropdownMenu,
16
- DropdownMenuContent,
17
- DropdownMenuItem,
18
- DropdownMenuTrigger,
19
- } from '@/components/ui/dropdown-menu';
20
-
21
- const ALIGN_OPTIONS = [
22
- { value: 'left', label: '左对齐', icon: AlignLeft },
23
- { value: 'center', label: '居中对齐', icon: AlignCenter },
24
- { value: 'right', label: '右对齐', icon: AlignRight },
25
- ] as const;
26
-
27
- export function TextAlignToolbarButton() {
28
- const { editor } = useTiptapEditor();
29
-
30
- if (!editor) return null;
31
-
32
- const getCurrentAlignment = () => {
33
- if (editor.isActive({ textAlign: 'center' })) return 'center';
34
- if (editor.isActive({ textAlign: 'right' })) return 'right';
35
- return 'left';
36
- };
37
-
38
- const isDefaultLeft =
39
- !editor.isActive({ textAlign: 'center' }) &&
40
- !editor.isActive({ textAlign: 'right' }) &&
41
- !editor.isActive({ textAlign: 'justify' });
42
-
43
- const alignment = getCurrentAlignment();
44
- const currentOption = ALIGN_OPTIONS.find((opt) => opt.value === alignment);
45
- const Icon = currentOption?.icon ?? AlignLeft;
46
-
47
- return (
48
- <DropdownMenu>
49
- <DropdownMenuTrigger asChild>
50
- <Button variant="ghost" size="sm" className="h-6 gap-0.5 px-2">
51
- <Icon className="size-4" />
52
- <ChevronDown className="size-3.5 text-muted-foreground" />
53
- <span className="sr-only">文字对齐</span>
54
- </Button>
55
- </DropdownMenuTrigger>
56
- <DropdownMenuContent align="start" className="w-50">
57
- {ALIGN_OPTIONS.map((option) => {
58
- const OptionIcon = option.icon;
59
- const active =
60
- option.value === 'left'
61
- ? editor.isActive({ textAlign: 'left' }) || isDefaultLeft
62
- : editor.isActive({ textAlign: option.value });
63
-
64
- return (
65
- <DropdownMenuItem
66
- key={option.value}
67
- onClick={() =>
68
- editor.chain().focus().setTextAlign(option.value).run()
69
- }
70
- disabled={!editor.can().setTextAlign(option.value)}
71
- className={cn('justify-between', active && 'bg-accent')}
72
- >
73
- <span className="flex items-center gap-2">
74
- <OptionIcon className="size-4" />
75
- {option.label}
76
- </span>
77
- {active && <Check className="size-4" />}
78
- </DropdownMenuItem>
79
- );
80
- })}
81
- </DropdownMenuContent>
82
- </DropdownMenu>
83
- );
84
- }
@@ -1,54 +0,0 @@
1
- 'use client';
2
-
3
- import { Redo, Undo } from 'lucide-react';
4
-
5
- import { useTiptapEditor } from '@/components/business-ui/tiptap-editor/hooks/use-tiptap-editor';
6
- import { Button } from '@/components/ui/button';
7
- import {
8
- Tooltip,
9
- TooltipContent,
10
- TooltipProvider,
11
- TooltipTrigger,
12
- } from '@/components/ui/tooltip';
13
-
14
- interface UndoRedoToolbarButtonProps {
15
- action: 'undo' | 'redo';
16
- }
17
-
18
- export function UndoRedoToolbarButton({ action }: UndoRedoToolbarButtonProps) {
19
- const { editor } = useTiptapEditor();
20
-
21
- if (!editor) return null;
22
-
23
- const canExecute =
24
- action === 'undo' ? editor.can().undo() : editor.can().redo();
25
- const Icon = action === 'undo' ? Undo : Redo;
26
-
27
- return (
28
- <TooltipProvider>
29
- <Tooltip delayDuration={700}>
30
- <TooltipTrigger asChild>
31
- <Button
32
- variant="ghost"
33
- size="sm"
34
- onClick={() =>
35
- action === 'undo'
36
- ? editor.chain().focus().undo().run()
37
- : editor.chain().focus().redo().run()
38
- }
39
- disabled={!canExecute}
40
- className="size-6 px-0"
41
- >
42
- <Icon className="size-4" />
43
- <span className="sr-only">
44
- {action === 'undo' ? '撤销' : '重做'}
45
- </span>
46
- </Button>
47
- </TooltipTrigger>
48
- <TooltipContent>
49
- <p>{action === 'undo' ? '撤销(⌘+Z)' : '重做(⌘+Shift+Z)'}</p>
50
- </TooltipContent>
51
- </Tooltip>
52
- </TooltipProvider>
53
- );
54
- }
@@ -1,539 +0,0 @@
1
- import * as React from 'react';
2
- import { mergeAttributes, Node, type CommandProps } from '@tiptap/core';
3
- import { type Node as ProseMirrorNode } from '@tiptap/pm/model';
4
- import {
5
- NodeViewWrapper,
6
- ReactNodeViewRenderer,
7
- type NodeViewProps,
8
- } from '@tiptap/react';
9
- import { CircleAlert, DownloadIcon, EyeIcon, Trash2Icon } from 'lucide-react';
10
- import { nanoid } from 'nanoid';
11
- import { toast } from 'sonner';
12
-
13
- import { uploadFile } from '@/components/business-ui/api/files/service';
14
- import { cn } from '@/lib/utils';
15
- import { Button } from '@/components/ui/button';
16
- import { FileAeColorfulIcon } from '@/components/ui/icons/file-ae-colorful-icon';
17
- import { FileAiColorfulIcon } from '@/components/ui/icons/file-ai-colorful-icon';
18
- import { FileAudioColorfulIcon } from '@/components/ui/icons/file-audio-colorful-icon';
19
- import { FileCodeColorfulIcon } from '@/components/ui/icons/file-code-colorful-icon';
20
- import { FileCsvColorfulIcon } from '@/components/ui/icons/file-csv-colorful-icon';
21
- import { FileKeynoteColorfulIcon } from '@/components/ui/icons/file-keynote-colorful-icon';
22
- import { FilePagesColorfulIcon } from '@/components/ui/icons/file-pages-colorful-icon';
23
- import { FilePsColorfulIcon } from '@/components/ui/icons/file-ps-colorful-icon';
24
- import { FileSketchColorfulIcon } from '@/components/ui/icons/file-sketch-colorful-icon';
25
- import { FileWikiExcelColorfulIcon } from '@/components/ui/icons/file-wiki-excel-colorful-icon';
26
- import { FileWikiImageColorfulIcon } from '@/components/ui/icons/file-wiki-image-colorful-icon';
27
- import { FileWikiPdfColorfulIcon } from '@/components/ui/icons/file-wiki-pdf-colorful-icon';
28
- import { FileWikiPptColorfulIcon } from '@/components/ui/icons/file-wiki-ppt-colorful-icon';
29
- import { FileWikiTextColorfulIcon } from '@/components/ui/icons/file-wiki-text-colorful-icon';
30
- import { FileWikiUnknownColorfulIcon } from '@/components/ui/icons/file-wiki-unknown-colorful-icon';
31
- import { FileWikiVideoColorfulIcon } from '@/components/ui/icons/file-wiki-video-colorful-icon';
32
- import { FileWikiWordColorfulIcon } from '@/components/ui/icons/file-wiki-word-colorful-icon';
33
- import { FileWikiZipColorfulIcon } from '@/components/ui/icons/file-wiki-zip-colorful-icon';
34
- import { Spinner } from '@/components/ui/spinner';
35
-
36
- export interface AttachmentAttributes {
37
- url: string;
38
- fileName?: string;
39
- fileSize?: string;
40
- fileType?: string;
41
- fileExt?: string;
42
- uploadId?: string | null;
43
- }
44
-
45
- export type AttachmentUploadFn = (file: File) => Promise<string>;
46
-
47
- export interface AttachmentExtensionOptions {
48
- upload?: AttachmentUploadFn;
49
- }
50
-
51
- declare module '@tiptap/core' {
52
- interface Commands<ReturnType> {
53
- attachment: {
54
- setAttachment: (attributes: AttachmentAttributes) => ReturnType;
55
- insertAttachments: (files: File[]) => ReturnType;
56
- };
57
- }
58
- }
59
-
60
- function findAttachmentByUploadId(doc: ProseMirrorNode, uploadId: string) {
61
- let foundPos: number | null = null;
62
- doc.descendants((node: ProseMirrorNode, pos: number) => {
63
- if (foundPos != null) return false;
64
- if (node.type?.name === 'attachment' && node.attrs?.uploadId === uploadId) {
65
- foundPos = pos;
66
- return false;
67
- }
68
- return true;
69
- });
70
- return foundPos;
71
- }
72
-
73
- function fileToAttachmentAttributes(
74
- file: File,
75
- uploadId: string,
76
- ): AttachmentAttributes {
77
- const fileSize = (file.size / 1024).toFixed(2) + ' KB';
78
- const fileExt = file.name.split('.').pop()?.toLowerCase() || '';
79
- const fileName = fileExt
80
- ? file.name.slice(0, -(fileExt.length + 1))
81
- : file.name;
82
- const fileType = file.type || fileExt || '';
83
-
84
- return {
85
- url: '',
86
- fileName,
87
- fileExt,
88
- fileSize,
89
- fileType,
90
- uploadId,
91
- };
92
- }
93
-
94
- function withDownloadParam(url: string) {
95
- // blob URL 追加 search 会导致地址失效
96
- if (url.startsWith('blob:')) return url;
97
-
98
- try {
99
- const u = new URL(url, window.location.href);
100
- u.searchParams.set('download', 'true');
101
- return u.toString();
102
- } catch {
103
- return url;
104
- }
105
- }
106
-
107
- function getFileIcon(fileType?: string) {
108
- if (!fileType) return FileWikiUnknownColorfulIcon;
109
-
110
- const type = fileType.toLowerCase();
111
-
112
- switch (type) {
113
- case 'jpg':
114
- case 'jpeg':
115
- case 'png':
116
- case 'gif':
117
- case 'svg':
118
- case 'webp':
119
- return FileWikiImageColorfulIcon;
120
- case 'mp4':
121
- case 'webm':
122
- case 'mov':
123
- return FileWikiVideoColorfulIcon;
124
- case 'mp3':
125
- case 'wav':
126
- case 'ogg':
127
- return FileAudioColorfulIcon;
128
- case 'pdf':
129
- return FileWikiPdfColorfulIcon;
130
- case 'doc':
131
- case 'docx':
132
- case 'rtf':
133
- return FileWikiWordColorfulIcon;
134
- case 'xls':
135
- case 'xlsx':
136
- return FileWikiExcelColorfulIcon;
137
- case 'csv':
138
- return FileCsvColorfulIcon;
139
- case 'ppt':
140
- case 'pptx':
141
- return FileWikiPptColorfulIcon;
142
- case 'txt':
143
- case 'md':
144
- return FileWikiTextColorfulIcon;
145
- case 'zip':
146
- case 'rar':
147
- case '7z':
148
- case 'tar':
149
- case 'gz':
150
- return FileWikiZipColorfulIcon;
151
- case 'js':
152
- case 'ts':
153
- case 'jsx':
154
- case 'tsx':
155
- case 'html':
156
- case 'css':
157
- case 'json':
158
- case 'py':
159
- case 'java':
160
- case 'c':
161
- case 'cpp':
162
- case 'go':
163
- case 'rs':
164
- case 'php':
165
- return FileCodeColorfulIcon;
166
- case 'ai':
167
- return FileAiColorfulIcon;
168
- case 'psd':
169
- case 'ps':
170
- return FilePsColorfulIcon;
171
- case 'ae':
172
- return FileAeColorfulIcon;
173
- case 'sketch':
174
- return FileSketchColorfulIcon;
175
- case 'key':
176
- case 'keynote':
177
- return FileKeynoteColorfulIcon;
178
- case 'pages':
179
- return FilePagesColorfulIcon;
180
- default:
181
- return FileWikiUnknownColorfulIcon;
182
- }
183
- }
184
-
185
- function AttachmentNodeView(props: NodeViewProps) {
186
- const { url, fileName, fileExt, fileSize, fileType, uploadId } = props.node
187
- .attrs as AttachmentAttributes;
188
-
189
- const uploading = Boolean(uploadId);
190
-
191
- const effectiveUrl = url || '';
192
- const displayName =
193
- fileName ||
194
- (effectiveUrl ? effectiveUrl.split('/').pop() : '') ||
195
- 'Untitled';
196
- const displayExt = fileExt || '';
197
-
198
- const iconComponent = getFileIcon(displayExt || fileType);
199
-
200
- const deleteNode = () => {
201
- props.deleteNode();
202
- };
203
-
204
- const handleDownload = (e: React.MouseEvent) => {
205
- e.preventDefault();
206
- if (!effectiveUrl || uploading) return;
207
-
208
- const downloadUrl = withDownloadParam(effectiveUrl);
209
-
210
- const filename =
211
- displayName + (displayExt ? `.${displayExt}` : '') || '附件';
212
-
213
- try {
214
- const a = document.createElement('a');
215
- a.href = downloadUrl;
216
- a.download = filename;
217
- a.rel = 'noopener';
218
- document.body.appendChild(a);
219
- a.click();
220
- a.remove();
221
- } catch {
222
- window.open(downloadUrl, '_blank');
223
- }
224
- };
225
-
226
- const handlePreview = (e: React.MouseEvent) => {
227
- e.preventDefault();
228
- if (!effectiveUrl || uploading) return;
229
- window.open(effectiveUrl, '_blank');
230
- };
231
-
232
- const highlighted = props.selected;
233
- const error = !uploading && !effectiveUrl;
234
-
235
- return (
236
- <NodeViewWrapper className="my-3 select-none">
237
- <div
238
- aria-invalid={error || undefined}
239
- className={cn(
240
- 'relative flex w-full items-center gap-2 rounded-md border bg-card px-3 py-2 text-card-foreground transition-colors',
241
- uploading && 'opacity-70',
242
- 'hover:border-primary aria-invalid:border-destructive aria-invalid:hover:border-destructive',
243
- highlighted && 'border-primary',
244
- )}
245
- >
246
- <div className="flex shrink-0 items-center justify-center text-muted-foreground">
247
- {React.createElement(iconComponent, {
248
- className: 'size-6',
249
- strokeWidth: 1.5,
250
- })}
251
- </div>
252
-
253
- <div className="flex min-w-0 flex-1 flex-col gap-0.5">
254
- <div className="truncate text-sm/[22px] font-normal text-foreground">
255
- {displayName}
256
- {displayExt ? `.${displayExt}` : ''}
257
- </div>
258
- <div className="truncate text-xs text-muted-foreground">
259
- {fileSize || 'Unknown size'}
260
- </div>
261
- </div>
262
-
263
- <div className="flex items-center gap-2">
264
- {uploading ? (
265
- <div className="flex items-center">
266
- <Spinner className="text-muted-foreground" />
267
- </div>
268
- ) : (
269
- <div className="flex items-center gap-2">
270
- {error ? (
271
- <Button
272
- type="button"
273
- variant="ghost"
274
- size="icon"
275
- aria-label="附件不可用"
276
- title="附件不可用"
277
- onClick={() => {
278
- toast.error('该附件链接缺失,请重新上传');
279
- }}
280
- >
281
- <CircleAlert className="size-4 text-destructive" />
282
- </Button>
283
- ) : (
284
- <>
285
- <Button
286
- type="button"
287
- variant="ghost"
288
- size="icon"
289
- onClick={handlePreview}
290
- disabled={!effectiveUrl}
291
- aria-label="预览"
292
- title="预览"
293
- >
294
- <EyeIcon className="size-4" />
295
- </Button>
296
-
297
- <Button
298
- type="button"
299
- variant="ghost"
300
- size="icon"
301
- onClick={handleDownload}
302
- disabled={!effectiveUrl}
303
- aria-label="下载"
304
- title="下载"
305
- >
306
- <DownloadIcon className="size-4" />
307
- </Button>
308
- </>
309
- )}
310
-
311
- {props.editor.isEditable && (
312
- <Button
313
- type="button"
314
- variant="ghost"
315
- size="icon"
316
- onClick={deleteNode}
317
- aria-label="删除"
318
- title="删除"
319
- >
320
- <Trash2Icon className="size-4" />
321
- </Button>
322
- )}
323
- </div>
324
- )}
325
- </div>
326
- </div>
327
- </NodeViewWrapper>
328
- );
329
- }
330
-
331
- export const Attachment = Node.create<AttachmentExtensionOptions>({
332
- name: 'attachment',
333
- group: 'block',
334
- atom: true,
335
-
336
- addOptions() {
337
- return {
338
- upload: async (file: File) => {
339
- const data = await uploadFile(file);
340
- return data.url;
341
- },
342
- };
343
- },
344
-
345
- addAttributes() {
346
- return {
347
- url: {
348
- default: null,
349
- parseHTML: (element) => {
350
- return element.getAttribute('data-url') || null;
351
- },
352
- renderHTML: (attributes) => {
353
- return {
354
- 'data-url': attributes.url || null,
355
- };
356
- },
357
- },
358
- fileName: {
359
- default: null,
360
- parseHTML: (element) => {
361
- return element.getAttribute('data-file-name') || null;
362
- },
363
- renderHTML: (attributes) => {
364
- return {
365
- 'data-file-name': attributes.fileName || null,
366
- };
367
- },
368
- },
369
- fileSize: {
370
- default: null,
371
- parseHTML: (element) => {
372
- return element.getAttribute('data-file-size') || null;
373
- },
374
- renderHTML: (attributes) => {
375
- return {
376
- 'data-file-size': attributes.fileSize || null,
377
- };
378
- },
379
- },
380
- fileType: {
381
- default: null,
382
- parseHTML: (element) => {
383
- return element.getAttribute('data-file-type') || null;
384
- },
385
- renderHTML: (attributes) => {
386
- return {
387
- 'data-file-type': attributes.fileType || null,
388
- };
389
- },
390
- },
391
- fileExt: {
392
- default: null,
393
- parseHTML: (element) => {
394
- return element.getAttribute('data-file-ext') || null;
395
- },
396
- renderHTML: (attributes) => {
397
- return {
398
- 'data-file-ext': attributes.fileExt || null,
399
- };
400
- },
401
- },
402
- uploadId: {
403
- default: null,
404
- rendered: false,
405
- },
406
- };
407
- },
408
-
409
- parseHTML() {
410
- return [{ tag: 'div[data-type="attachment"]' }];
411
- },
412
-
413
- renderHTML({ node, HTMLAttributes }) {
414
- const attrs = node.attrs as AttachmentAttributes;
415
- const url = attrs.url || '';
416
- const fileName = attrs.fileName || '';
417
- const fileExt = attrs.fileExt || '';
418
- const fileSize = attrs.fileSize || '';
419
-
420
- const displayTitle =
421
- fileName + (fileExt ? `.${fileExt}` : '') || 'Attachment';
422
-
423
- const meta = fileSize;
424
-
425
- const merged = mergeAttributes(HTMLAttributes, {
426
- 'data-type': 'attachment',
427
- title: displayTitle,
428
- });
429
-
430
- if (!url) {
431
- return ['div', merged];
432
- }
433
-
434
- return [
435
- 'div',
436
- merged,
437
- [
438
- 'a',
439
- {
440
- href: url,
441
- target: '_blank',
442
- rel: 'noopener noreferrer',
443
- },
444
- ['span', {}, displayTitle],
445
- meta ? ['span', {}, ` (${meta})`] : '',
446
- ],
447
- ];
448
- },
449
-
450
- addNodeView() {
451
- return ReactNodeViewRenderer(AttachmentNodeView);
452
- },
453
-
454
- addCommands() {
455
- return {
456
- setAttachment:
457
- (attributes: AttachmentAttributes) =>
458
- ({ commands }: CommandProps) => {
459
- return commands.insertContent({
460
- type: this.name,
461
- attrs: attributes,
462
- });
463
- },
464
-
465
- insertAttachments:
466
- (files: File[]) =>
467
- ({ editor, commands }: CommandProps) => {
468
- if (!files || files.length === 0) return false;
469
-
470
- const upload = this.options.upload;
471
- if (!upload) return false;
472
-
473
- const uploads = files.map((file) => {
474
- const uploadId = nanoid();
475
- return {
476
- file,
477
- uploadId,
478
- attrs: fileToAttachmentAttributes(file, uploadId),
479
- };
480
- });
481
-
482
- // 一次性插入多个节点,避免 NodeSelection 导致后续插入替换掉前一个节点
483
- const inserted = commands.insertContent([
484
- ...uploads.map(({ attrs }) => ({
485
- type: this.name,
486
- attrs,
487
- })),
488
- { type: 'paragraph' },
489
- ]);
490
-
491
- // 并发上传:不阻塞命令返回
492
- void Promise.all(
493
- uploads.map(async ({ file, uploadId }) => {
494
- try {
495
- const url = await upload(file);
496
-
497
- // Ensure state is settled.
498
- await new Promise((resolve) => requestAnimationFrame(resolve));
499
-
500
- const pos = findAttachmentByUploadId(
501
- editor.state.doc,
502
- uploadId,
503
- );
504
- if (pos == null) return;
505
-
506
- const node = editor.state.doc.nodeAt(pos);
507
- if (!node || node.type.name !== this.name) return;
508
-
509
- editor
510
- .chain()
511
- .command(({ tr }) => {
512
- tr.setNodeMarkup(pos, undefined, {
513
- ...node.attrs,
514
- url,
515
- uploadId: null,
516
- });
517
- return true;
518
- })
519
- .run();
520
- } catch {
521
- const pos = findAttachmentByUploadId(
522
- editor.state.doc,
523
- uploadId,
524
- );
525
- if (pos != null) {
526
- editor
527
- .chain()
528
- .deleteRange({ from: pos, to: pos + 1 })
529
- .run();
530
- }
531
- }
532
- }),
533
- );
534
-
535
- return inserted;
536
- },
537
- };
538
- },
539
- });