@lark-apaas/coding-template-nestjs-react-fullstack 0.1.55-alpha.20261101040602 → 0.1.55-alpha.20261101050259
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/record-comments/README.md +27 -2
- package/template/client/src/components/business-ui/record-comments/app-source.ts +5 -8
- package/template/client/src/components/business-ui/record-comments/comment-panel.contract.ts +5 -5
- package/template/client/src/components/business-ui/record-comments/comment-panel.tsx +32 -24
- package/template/client/src/components/business-ui/record-comments/index.ts +4 -4
- package/template/client/src/components/business-ui/record-comments/{discussion-composer.tsx → internal/composer.tsx} +178 -14
- package/template/client/src/components/business-ui/record-comments/{comment-content.tsx → internal/content.tsx} +5 -24
- package/template/client/src/components/business-ui/record-comments/{discussion-panel.tsx → internal/discussion.tsx} +11 -22
- package/template/client/src/components/business-ui/record-comments/{editor.tsx → internal/editor.tsx} +276 -39
- package/template/client/src/components/business-ui/record-comments/{document-icons.ts → internal/icons.tsx} +129 -0
- package/template/client/src/components/business-ui/record-comments/{discussion-image-viewer.tsx → internal/images.tsx} +54 -4
- package/template/client/src/components/business-ui/record-comments/{entity-store.ts → internal/mentions.tsx} +73 -7
- package/template/client/src/components/business-ui/record-comments/internal/model.ts +581 -0
- package/template/client/src/components/business-ui/record-comments/{comment-panel-optimistic.ts → internal/mutations.ts} +4 -5
- package/template/client/src/components/business-ui/record-comments/{reaction-catalog.ts → internal/reaction-catalog.ts} +2 -0
- package/template/client/src/components/business-ui/record-comments/{comment-reactions.tsx → internal/reactions.tsx} +10 -25
- package/template/client/src/components/business-ui/record-comments/{discussion.css → internal/styles.css} +400 -0
- package/template/client/src/components/business-ui/record-comments/{mention-suggestions.tsx → internal/suggestions.tsx} +129 -10
- package/template/client/src/components/business-ui/record-comments/{discussion-types.ts → internal/view-types.ts} +3 -8
- package/template/client/src/components/business-ui/record-comments/comment-input.tsx +0 -190
- package/template/client/src/components/business-ui/record-comments/comment-panel-model.ts +0 -198
- package/template/client/src/components/business-ui/record-comments/comment-reactions.css +0 -220
- package/template/client/src/components/business-ui/record-comments/content.ts +0 -330
- package/template/client/src/components/business-ui/record-comments/discussion-time.ts +0 -28
- package/template/client/src/components/business-ui/record-comments/document-icon.tsx +0 -104
- package/template/client/src/components/business-ui/record-comments/entity-mention.ts +0 -281
- package/template/client/src/components/business-ui/record-comments/icons.tsx +0 -28
- package/template/client/src/components/business-ui/record-comments/image-files.ts +0 -51
- package/template/client/src/components/business-ui/record-comments/mention-appearance.css +0 -11
- package/template/client/src/components/business-ui/record-comments/mention-appearance.ts +0 -36
- package/template/client/src/components/business-ui/record-comments/mention-provider.tsx +0 -81
- package/template/client/src/components/business-ui/record-comments/mention-suggestions.css +0 -125
- package/template/client/src/components/business-ui/record-comments/use-mention-search.ts +0 -124
package/package.json
CHANGED
|
@@ -1,9 +1,27 @@
|
|
|
1
1
|
# Record comments
|
|
2
2
|
|
|
3
|
-
完整受控记录评论组件,入口 `CommentPanel`。公共合同见 `comment-panel.contract.ts`、`types.ts`,统一从 `index.ts`
|
|
3
|
+
完整受控记录评论组件,入口 `CommentPanel`。公共合同见 `comment-panel.contract.ts`、`types.ts`,统一从 `index.ts` 导入;目录内 internal 实现、集中样式和 `assets/reaction-sprite.png` 必须整体分发。不依赖旧 comment 目录,不包含测试应用、插件实例或表格配置。模板已配置 React、Tiptap 3.20.0、floating-ui 及 ui primitives 依赖。
|
|
4
4
|
|
|
5
5
|
组件管理现有样式、回复归属、编辑器、人员弹层、菜单、分页触发和乐观更新;评论 CRUD、身份、权限、插件调用、服务端缓存、分页游标、表情及文档请求由宿主提供。
|
|
6
6
|
|
|
7
|
+
## Agent 接入与维护路径
|
|
8
|
+
|
|
9
|
+
普通接入只读本说明和 `comment-panel.contract.ts`,从目录入口导入;人员/文档来源的详细类型按需查 `types.ts`。新增页面只在宿主 hook 中实现读取与写入,不需要遍历或修改内部编辑器。
|
|
10
|
+
|
|
11
|
+
| 位置 | 职责 |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
| `index.ts` | 唯一推荐导入入口:组件、公共类型、目录适配器和 URL 工具 |
|
|
14
|
+
| `comment-panel.contract.ts` / `types.ts` | 评论受控接口及提及来源类型 |
|
|
15
|
+
| `comment-panel.tsx` | 装配受控数据、分页和乐观更新 |
|
|
16
|
+
| `app-source.ts` | 可选的妙搭人员目录适配器,不含评论 CRUD |
|
|
17
|
+
| `internal/model.ts` / `mutations.ts` | 内容转换、命令校验、时间/提及规则、乐观更新状态 |
|
|
18
|
+
| `internal/discussion.tsx` / `composer.tsx` | 评论展示、菜单、回复和输入区 |
|
|
19
|
+
| `internal/editor.tsx` / `suggestions.tsx` / `mentions.tsx` | Tiptap、候选选择与引用解析缓存 |
|
|
20
|
+
| `internal/content.tsx` / `icons.tsx` / `images.tsx` / `reactions.tsx` | 内容展示、图标、图片扩展和表情回应 |
|
|
21
|
+
| `internal/view-types.ts` / `reaction-catalog.ts` / `styles.css` | 内部视图类型、静态表情数据、集中样式 |
|
|
22
|
+
|
|
23
|
+
`internal/` 只由组件实现与组件测试引用;应用代码不深引其内部文件。文档适配器如需 URL 校验/识别,从入口(纯数据适配器也可从 `app-source.ts`)导入 `safeUrl/documentRefFromUrl`。样式由 `CommentPanel` 一次加载,分发时保留整个目录和 assets;不需要逐个导入 CSS。修复内部行为时沿上表定位一个职责,不为每个小工具再拆文件。
|
|
24
|
+
|
|
7
25
|
## 接入
|
|
8
26
|
|
|
9
27
|
必填 `scopeKey/recordID/comments/mentionSource`。可选参数完整类型见 `CommentPanelProps`。
|
|
@@ -48,7 +66,7 @@ export function RecordComments(props: Props) {
|
|
|
48
66
|
|
|
49
67
|
完整 Action 映射、身份约束和接入验收见配套 `plugin-guide/references/record-comments.md`。模板不含 mock 路由;仅 mock 通过不能声称真实链路完成。
|
|
50
68
|
|
|
51
|
-
人员提及悬浮沿用官方颜色:普通人员浅蓝底、自身蓝色胶囊变亮、无权限人员灰底。样式由
|
|
69
|
+
人员提及悬浮沿用官方颜色:普通人员浅蓝底、自身蓝色胶囊变亮、无权限人员灰底。样式由 internal/styles.css 的组件专属类提供,不依赖宿主 accent 配色。
|
|
52
70
|
|
|
53
71
|
|
|
54
72
|
## 交互接入约束
|
|
@@ -56,3 +74,10 @@ export function RecordComments(props: Props) {
|
|
|
56
74
|
宿主复用组件已有交互,不额外包装删除确认、成功 toast 或“已显示全部评论”提示。复制链接成功后只关闭菜单;编辑操作按钮保持“确认”,等待态不新增“保存中”文字。回复自己也会插入自身提及;未知作者不能伪造提及。保持人员搜索和提及悬浮样式,不用宿主全局 button/a/p 样式覆盖组件。
|
|
57
75
|
|
|
58
76
|
保留读写失败的恢复入口和无障碍状态;未知写入结果不得开放盲目重试。新增提示或改变状态流前,应取得官方对应状态的操作证据;本地受控故障测试不等同于真实 Action 验收,未覆盖的权限/网络/图片能力需明确标注。
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
初始无数据时,加载、空态和读取错误共用同一个 118px 状态占位(沿用原空态高度),长错误在占位内滚动;状态切换不会移动输入框。已有评论的后台读取错误保留列表并使用紧凑提示。宿主仍控制整体 height/maxHeight,不需要为三种状态分别补高度。重试失败后,外部 error 清除会同时清理旧的重试错误。
|
|
80
|
+
|
|
81
|
+
空列表的分页提示也计入同一个读取占位区;后续页耗尽不会额外收缩面板。受限高度容器中该区域可随可用空间收缩,长分页错误在区域内滚动。
|
|
82
|
+
|
|
83
|
+
若宿主在身份或权限查询完成后才提供写回调,输入框会稍后挂载;宿主可通过 `style={{ minHeight: 'min(250px, 60vh)', maxHeight: '60vh' }}` 预留整体空间,避免身份加载引起的高度变化。不要为占位提前开放写入能力。
|
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { CommentDataSource, CommentDocumentSource, MentionOption, MentionResult } from './types';
|
|
2
|
+
import { mentionKey } from './internal/model';
|
|
2
3
|
import { getI18nText } from '../utils/user';
|
|
3
|
-
import {
|
|
4
|
-
import type {
|
|
5
|
-
CommentDataSource,
|
|
6
|
-
CommentDocumentSource,
|
|
7
|
-
MentionOption,
|
|
8
|
-
MentionResult,
|
|
9
|
-
} from './types';
|
|
4
|
+
import { listUsersByIds, searchUsers } from '../api/users/service';
|
|
10
5
|
|
|
11
6
|
function avatarUrl(avatar: unknown): string | undefined {
|
|
12
7
|
if (typeof avatar === 'string') return avatar;
|
|
@@ -183,3 +178,5 @@ export function withDocumentSource(
|
|
|
183
178
|
},
|
|
184
179
|
};
|
|
185
180
|
}
|
|
181
|
+
|
|
182
|
+
export { documentRefFromUrl, safeUrl } from './internal/model';
|
package/template/client/src/components/business-ui/record-comments/comment-panel.contract.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
+
import type { CommentDataSource, CommentMentionClickHandler } from './types';
|
|
2
|
+
import type { CSSProperties } from 'react';
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
5
|
* Public contract for the controlled CommentPanel entry point.
|
|
3
6
|
* Requests, credentials, pagination cursors and server caches belong to the host.
|
|
4
7
|
* The panel owns optimistic drafts and per-message mutation state.
|
|
5
8
|
*/
|
|
6
|
-
import type { CSSProperties } from 'react';
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
CommentMentionClickHandler,
|
|
11
|
-
} from './types';
|
|
10
|
+
|
|
11
|
+
|
|
12
12
|
|
|
13
13
|
export type CommentReadContent =
|
|
14
14
|
| { type: 'text'; text: string }
|
|
@@ -1,27 +1,34 @@
|
|
|
1
|
-
import { useCallback, useEffect, useReducer, useRef, useState, useSyncExternalStore } from 'react';
|
|
2
|
-
import { LoaderCircle } from 'lucide-react';
|
|
3
|
-
import { DiscussionPanel } from './discussion-panel';
|
|
4
|
-
import {
|
|
5
|
-
buildPanelThreads, CommentPageLoader, preparePanelDeletion, preparePanelEdit,
|
|
6
|
-
loadPanelReactionDetails, preparePanelComment,
|
|
7
|
-
} from './comment-panel-model';
|
|
8
|
-
import { CommentMutations } from './comment-panel-optimistic';
|
|
9
1
|
import type { CommentPanelProps, CommentTarget } from './comment-panel.contract';
|
|
2
|
+
import { CommentMutations } from './internal/mutations';
|
|
3
|
+
import { buildPanelThreads, CommentPageLoader, preparePanelDeletion, preparePanelEdit, loadPanelReactionDetails, preparePanelComment } from './internal/model';
|
|
4
|
+
import { DiscussionPanel } from './internal/discussion';
|
|
5
|
+
import { LoaderCircle } from 'lucide-react';
|
|
6
|
+
import { useCallback, useEffect, useReducer, useRef, useState, useSyncExternalStore } from 'react';
|
|
7
|
+
import './internal/styles.css';
|
|
10
8
|
|
|
11
|
-
function ReadStatus({ loading, error, onRetry }: Pick<CommentPanelProps, 'loading' | 'error' | 'onRetry'>) {
|
|
9
|
+
function ReadStatus({ loading, error, onRetry, empty, hasMore }: Pick<CommentPanelProps, 'loading' | 'error' | 'onRetry'> & { empty: boolean; hasMore?: boolean }) {
|
|
12
10
|
const [failure, setFailure] = useState('');
|
|
13
11
|
const [retrying, setRetrying] = useState(false);
|
|
14
12
|
const busy = useRef(false);
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
13
|
+
// A successful external read must clear a previous retry failure as well.
|
|
14
|
+
useEffect(() => setFailure(''), [error]);
|
|
15
|
+
const pending = loading || retrying;
|
|
16
|
+
const message = failure || error;
|
|
17
|
+
if (!empty && !pending && !message) return null;
|
|
18
|
+
const state = pending ? 'loading' : message ? 'error' : 'empty';
|
|
19
|
+
return <div className={`replica-panel-status${empty ? ' replica-panel-placeholder' : ''}`}
|
|
20
|
+
data-state={state} role={state === 'error' ? 'alert' : 'status'} aria-busy={pending}>
|
|
21
|
+
<div className="replica-panel-status-content">
|
|
22
|
+
<span>{pending ? <LoaderCircle className="size-4 animate-spin text-primary" aria-label="正在加载评论" />
|
|
23
|
+
: message || (empty && !hasMore ? '当前记录暂无评论' : null)}</span>
|
|
24
|
+
{!pending && message && onRetry && <button type="button" onClick={async () => {
|
|
25
|
+
if (busy.current) return;
|
|
26
|
+
busy.current = true; setRetrying(true); setFailure('');
|
|
27
|
+
try { await onRetry(); }
|
|
28
|
+
catch (cause) { setFailure(cause instanceof Error ? cause.message : '刷新失败,请重试'); }
|
|
29
|
+
finally { busy.current = false; setRetrying(false); }
|
|
30
|
+
}}>重试刷新</button>}
|
|
31
|
+
</div>
|
|
25
32
|
</div>;
|
|
26
33
|
}
|
|
27
34
|
|
|
@@ -93,11 +100,11 @@ function PaginationFooter({ pagination, loading, getScrollContainer }: Pick<Comm
|
|
|
93
100
|
});
|
|
94
101
|
const error = pagination?.hasMore && (gate.error || pagination.error);
|
|
95
102
|
const pending = gate.pending || pagination?.loading;
|
|
96
|
-
return <div ref={sentinel} className=
|
|
97
|
-
{error ? <><span role="alert">{error}</span><button type="button" disabled={pending} onClick={() => run.current(true)}>重试加载</button></>
|
|
103
|
+
return <div ref={sentinel} className={pagination?.hasMore ? 'replica-panel-pagination' : undefined} aria-live="polite">
|
|
104
|
+
{pagination?.hasMore && (error ? <><span role="alert">{error}</span><button type="button" disabled={pending} onClick={() => run.current(true)}>重试加载</button></>
|
|
98
105
|
: pending ? <LoaderCircle className="size-4 animate-spin text-primary" aria-label="正在加载更多评论" />
|
|
99
106
|
: pagination?.hasMore ? <button type="button" disabled={loading} onClick={() => run.current(true)}>继续加载</button>
|
|
100
|
-
: null}
|
|
107
|
+
: null)}
|
|
101
108
|
</div>;
|
|
102
109
|
}
|
|
103
110
|
|
|
@@ -145,8 +152,9 @@ function PanelSession(props: CommentPanelProps) {
|
|
|
145
152
|
enablePrivateComments={false}
|
|
146
153
|
enableDocuments={props.enableDocuments ?? false}
|
|
147
154
|
autoScrollNewReplies={false}
|
|
148
|
-
emptyState={
|
|
149
|
-
listHeader={<ReadStatus loading={props.loading} error={props.error} onRetry={props.onRetry}
|
|
155
|
+
emptyState={null}
|
|
156
|
+
listHeader={<ReadStatus loading={props.loading} error={props.error} onRetry={props.onRetry}
|
|
157
|
+
empty={!comments.some((thread) => thread.replies.length > 0)} hasMore={props.pagination?.hasMore} />}
|
|
150
158
|
listFooter={props.pagination && <PaginationFooter pagination={props.pagination} loading={props.loading || Boolean(props.error)} getScrollContainer={props.getScrollContainer} />}
|
|
151
159
|
onMentionClick={props.onMentionClick}
|
|
152
160
|
getCommentLink={(commentID, reply) => props.getCommentLink?.({ recordID: props.recordID, commentID, replyID: reply.replyID }) ?? ''}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { CommentPanel } from './comment-panel';
|
|
2
2
|
export { createAppCommentSource, withDocumentSource } from './app-source';
|
|
3
3
|
export type * from './comment-panel.contract';
|
|
4
|
-
export type {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
} from './
|
|
4
|
+
export type { CommentDataSource, CommentDocumentSource, CommentMentionClickHandler, MentionRef, MentionOption, MentionResult, MentionKind, DocumentMentionMetadata } from './types';
|
|
5
|
+
|
|
6
|
+
// URL helpers for external document adapters; no requests or permissions inside.
|
|
7
|
+
export { documentRefFromUrl, safeUrl } from './internal/model';
|
|
@@ -1,18 +1,182 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
} from '
|
|
8
|
-
import
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
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
12
|
import { DiscussionIcon } from './icons';
|
|
13
|
-
import {
|
|
14
|
-
import
|
|
15
|
-
|
|
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
|
+
}
|
|
16
180
|
|
|
17
181
|
export function DiscussionComposer({
|
|
18
182
|
sessionKey,
|
|
@@ -1,28 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
USER_MENTION_CLASS,
|
|
5
|
-
INPUT_USER_MENTION_CLASS,
|
|
6
|
-
COMMENT_THEME_CLASS,
|
|
7
|
-
userMentionLabelClass,
|
|
8
|
-
atUserLabel,
|
|
9
|
-
DOCUMENT_MENTION_CLASS,
|
|
10
|
-
DOCUMENT_MENTION_UNREADABLE_CLASS,
|
|
11
|
-
} from './mention-appearance';
|
|
12
|
-
import * as React from 'react';
|
|
1
|
+
import type { CommentMentionClickHandler, CommentApiContent, CommentPart, CommentValue } from '../types';
|
|
2
|
+
import { useMentionContext, useMentionEntity, useResolveComment } from './mentions';
|
|
3
|
+
import { mentionLabel, normalizeValue, safeUrl, USER_MENTION_CLASS, INPUT_USER_MENTION_CLASS, COMMENT_THEME_CLASS, userMentionLabelClass, atUserLabel, DOCUMENT_MENTION_CLASS, DOCUMENT_MENTION_UNREADABLE_CLASS, fromApiContent } from './model';
|
|
13
4
|
import { cn } from '@/lib/utils';
|
|
14
|
-
import
|
|
15
|
-
import {
|
|
16
|
-
useMentionContext,
|
|
17
|
-
useMentionEntity,
|
|
18
|
-
useResolveComment,
|
|
19
|
-
} from './mention-provider';
|
|
20
|
-
import type {
|
|
21
|
-
CommentMentionClickHandler,
|
|
22
|
-
CommentApiContent,
|
|
23
|
-
CommentPart,
|
|
24
|
-
CommentValue,
|
|
25
|
-
} from './types';
|
|
5
|
+
import * as React from 'react';
|
|
6
|
+
import { DocumentIcon } from './icons';
|
|
26
7
|
|
|
27
8
|
function Mention({
|
|
28
9
|
part,
|
|
@@ -1,26 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
Popover,
|
|
5
|
-
PopoverContent,
|
|
6
|
-
PopoverTrigger,
|
|
7
|
-
} from '@/components/ui/popover';
|
|
8
|
-
import { MentionProvider, useMentionEntity } from './mention-provider';
|
|
9
|
-
import { CommentContent } from './comment-content';
|
|
10
|
-
import { CommentReactionEntry, CommentReactionList } from './comment-reactions';
|
|
11
|
-
import { commentFailureMessage } from './comment-panel-model';
|
|
12
|
-
import { DiscussionComposer } from './discussion-composer';
|
|
13
|
-
import { DiscussionImageViewer } from './discussion-image-viewer';
|
|
1
|
+
import type { DiscussionReply, DiscussionImage, DiscussionPanelProps } from './view-types';
|
|
2
|
+
import type { MentionRef } from '../types';
|
|
3
|
+
import { COMMENT_THEME_CLASS, formatCommentTime, commentFailureMessage } from './model';
|
|
14
4
|
import { DiscussionIcon } from './icons';
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import
|
|
18
|
-
import
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
} from '
|
|
23
|
-
import './discussion.css';
|
|
5
|
+
import { DiscussionImageViewer } from './images';
|
|
6
|
+
import { DiscussionComposer } from './composer';
|
|
7
|
+
import { CommentReactionEntry, CommentReactionList } from './reactions';
|
|
8
|
+
import { CommentContent } from './content';
|
|
9
|
+
import { MentionProvider, useMentionEntity } from './mentions';
|
|
10
|
+
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
|
11
|
+
import { LockKeyhole } from 'lucide-react';
|
|
12
|
+
import { useEffect, useRef, useState } from 'react';
|
|
24
13
|
|
|
25
14
|
function PersonAvatar({
|
|
26
15
|
refValue,
|