@lark-apaas/coding-template-nestjs-react-fullstack 0.1.55-alpha.20261101083044 → 0.1.55

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 (25) hide show
  1. package/package.json +1 -1
  2. package/template/client/src/components/business-ui/README.md +0 -2
  3. package/template/package-lock.json +13 -529
  4. package/template/package.json +4 -15
  5. package/template/client/src/components/business-ui/record-comments/README.md +0 -83
  6. package/template/client/src/components/business-ui/record-comments/app-source.ts +0 -182
  7. package/template/client/src/components/business-ui/record-comments/assets/reaction-sprite.png +0 -0
  8. package/template/client/src/components/business-ui/record-comments/comment-panel.contract.ts +0 -223
  9. package/template/client/src/components/business-ui/record-comments/comment-panel.tsx +0 -202
  10. package/template/client/src/components/business-ui/record-comments/index.ts +0 -7
  11. package/template/client/src/components/business-ui/record-comments/internal/composer.tsx +0 -529
  12. package/template/client/src/components/business-ui/record-comments/internal/content.tsx +0 -213
  13. package/template/client/src/components/business-ui/record-comments/internal/discussion.tsx +0 -535
  14. package/template/client/src/components/business-ui/record-comments/internal/editor.tsx +0 -675
  15. package/template/client/src/components/business-ui/record-comments/internal/icons.tsx +0 -148
  16. package/template/client/src/components/business-ui/record-comments/internal/images.tsx +0 -155
  17. package/template/client/src/components/business-ui/record-comments/internal/mentions.tsx +0 -163
  18. package/template/client/src/components/business-ui/record-comments/internal/model.ts +0 -581
  19. package/template/client/src/components/business-ui/record-comments/internal/mutations.ts +0 -170
  20. package/template/client/src/components/business-ui/record-comments/internal/reaction-catalog.ts +0 -196
  21. package/template/client/src/components/business-ui/record-comments/internal/reactions.tsx +0 -681
  22. package/template/client/src/components/business-ui/record-comments/internal/styles.css +0 -1109
  23. package/template/client/src/components/business-ui/record-comments/internal/suggestions.tsx +0 -456
  24. package/template/client/src/components/business-ui/record-comments/internal/view-types.ts +0 -82
  25. package/template/client/src/components/business-ui/record-comments/types.ts +0 -118
@@ -1,202 +0,0 @@
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';
8
-
9
- function ReadStatus({ loading, error, onRetry, empty, hasMore }: Pick<CommentPanelProps, 'loading' | 'error' | 'onRetry'> & { empty: boolean; hasMore?: boolean }) {
10
- const [failure, setFailure] = useState('');
11
- const [retrying, setRetrying] = useState(false);
12
- const busy = useRef(false);
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>
32
- </div>;
33
- }
34
-
35
- function nearestScrollContainer(node: HTMLElement): HTMLElement | null {
36
- for (let parent = node.parentElement; parent; parent = parent.parentElement) {
37
- if (/(auto|scroll)/.test(getComputedStyle(parent).overflowY) && parent.clientHeight > 0) return parent;
38
- }
39
- return null;
40
- }
41
-
42
- function PaginationFooter({ pagination, loading, getScrollContainer }: Pick<CommentPanelProps, 'pagination' | 'loading' | 'getScrollContainer'>) {
43
- const sentinel = useRef<HTMLDivElement>(null);
44
- const root = useRef<HTMLElement | null>(null);
45
- const [gate] = useState(() => new CommentPageLoader());
46
- const [, refresh] = useReducer((value: number) => value + 1, 0);
47
- const latest = useRef({ pagination, loading });
48
- latest.current = { pagination, loading };
49
- const alive = useRef(true);
50
- const run = useRef((manual = false) => { void manual; });
51
- run.current = (manual = false) => {
52
- const current = latest.current;
53
- if (!alive.current || current.loading || !current.pagination?.hasMore || current.pagination.loading || gate.pending) return;
54
- if (!manual && (gate.error || current.pagination.error || gate.automaticLoads >= 3)) return;
55
- const promise = gate.load(current.pagination, manual);
56
- refresh();
57
- void promise.finally(() => { if (alive.current) refresh(); });
58
- };
59
- const check = useCallback(() => {
60
- const element = sentinel.current;
61
- if (!element) return;
62
- const bounds = element.getBoundingClientRect();
63
- const viewport = root.current?.getBoundingClientRect();
64
- const visibleTop = Math.max(0, viewport?.top ?? 0);
65
- const visibleBottom = Math.min(window.innerHeight, viewport?.bottom ?? window.innerHeight);
66
- if (visibleBottom > visibleTop && bounds.top <= visibleBottom + 200 && bounds.bottom >= visibleTop)
67
- run.current();
68
- }, []);
69
- useEffect(() => {
70
- alive.current = true;
71
- const element = sentinel.current;
72
- if (!element) return;
73
- root.current = getScrollContainer ? getScrollContainer() : nearestScrollContainer(element);
74
- const container = root.current ?? window;
75
- const observer = new IntersectionObserver(() => check(), { root: root.current, rootMargin: '0px 0px 200px 0px' });
76
- observer.observe(element);
77
- const resize = new ResizeObserver(check);
78
- resize.observe(root.current ?? document.documentElement);
79
- const resume = (event: Event) => {
80
- if (event instanceof WheelEvent && event.deltaY > 0) gate.automaticLoads = 0;
81
- if (event instanceof KeyboardEvent && ['PageDown', 'End', 'ArrowDown'].includes(event.key)) gate.automaticLoads = 0;
82
- check();
83
- };
84
- container.addEventListener('scroll', check, { passive: true });
85
- if (root.current) window.addEventListener('scroll', check, { passive: true });
86
- container.addEventListener('wheel', resume, { passive: true });
87
- container.addEventListener('keydown', resume);
88
- return () => {
89
- alive.current = false;
90
- observer.disconnect(); resize.disconnect();
91
- container.removeEventListener('scroll', check);
92
- window.removeEventListener('scroll', check);
93
- container.removeEventListener('wheel', resume);
94
- container.removeEventListener('keydown', resume);
95
- };
96
- }, [check, gate, getScrollContainer]);
97
- useEffect(() => {
98
- const frame = requestAnimationFrame(check);
99
- return () => cancelAnimationFrame(frame);
100
- });
101
- const error = pagination?.hasMore && (gate.error || pagination.error);
102
- const pending = gate.pending || pagination?.loading;
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></>
105
- : pending ? <LoaderCircle className="size-4 animate-spin text-primary" aria-label="正在加载更多评论" />
106
- : pagination?.hasMore ? <button type="button" disabled={loading} onClick={() => run.current(true)}>继续加载</button>
107
- : null)}
108
- </div>;
109
- }
110
-
111
- function PanelSession(props: CommentPanelProps) {
112
- const latest = useRef(props);
113
- latest.current = props;
114
- const root = useRef<HTMLDivElement>(null);
115
- const locks = useRef(new Set<string>());
116
- const [submitted, setSubmitted] = useState<string>();
117
- const [mutations] = useState(() => new CommentMutations());
118
- useSyncExternalStore(mutations.subscribe, mutations.getVersion, mutations.getVersion);
119
- const base = buildPanelThreads(props);
120
- const comments = mutations.view(base);
121
- useEffect(() => {
122
- mutations.activate();
123
- return () => mutations.dispose();
124
- }, [mutations]);
125
- useEffect(() => { mutations.reconcile(base); }, [base, mutations]);
126
- useEffect(() => {
127
- if (!submitted) return;
128
- const target = root.current?.querySelector(`[data-reply-id="${CSS.escape(mutations.replyID(submitted))}"]`);
129
- if (target) { target.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); setSubmitted(undefined); }
130
- }, [comments, submitted, mutations]);
131
- async function locked<T>(key: string, action: () => Promise<T>): Promise<T> {
132
- if (locks.current.has(key)) throw new Error('该评论正在处理中,请稍后重试');
133
- locks.current.add(key);
134
- try { return await action(); }
135
- finally { locks.current.delete(key); }
136
- }
137
- const allowCompose = Boolean(props.onAddComment || props.images?.onAddComment);
138
- const currentUser = props.currentUserID == null ? undefined
139
- : { kind: 'user' as const, idType: 'apaas-user' as const, id: String(props.currentUserID) };
140
- return <div ref={root} className={`replica-comment-panel ${props.className ?? ''}`} style={props.style}>
141
- <DiscussionPanel
142
- source={props.mentionSource}
143
- currentUser={currentUser}
144
- comments={comments}
145
- allowCompose={allowCompose}
146
- optimisticMutations
147
- onRetryMutation={(id) => mutations.retry(id)}
148
- showReactions={Boolean(props.reactions)}
149
- allowReact={Boolean(props.reactions?.onChange)}
150
- enableImages={Boolean(props.images?.onAddComment)}
151
- enableEditImages={Boolean(props.images?.onUpdateComment)}
152
- enablePrivateComments={false}
153
- enableDocuments={props.enableDocuments ?? false}
154
- autoScrollNewReplies={false}
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} />}
158
- listFooter={props.pagination && <PaginationFooter pagination={props.pagination} loading={props.loading || Boolean(props.error)} getScrollContainer={props.getScrollContainer} />}
159
- onMentionClick={props.onMentionClick}
160
- getCommentLink={(commentID, reply) => props.getCommentLink?.({ recordID: props.recordID, commentID, replyID: reply.replyID }) ?? ''}
161
- onSubmit={(draft) => {
162
- const current = latest.current;
163
- preparePanelComment(current, draft); // Validate before accepting/clearing the draft.
164
- const thread = base.find((item) => item.commentID === draft.commentID);
165
- const id = mutations.add(thread, draft, currentUser, () => preparePanelComment(latest.current, draft)());
166
- setSubmitted(id);
167
- }}
168
- onEdit={(commentID, replyID, value, images) => {
169
- const draft = { value, images, private: false };
170
- preparePanelEdit(latest.current, commentID, replyID, draft);
171
- const thread = base.find((item) => item.commentID === commentID)!;
172
- mutations.edit(thread, replyID, draft, () => preparePanelEdit(latest.current, commentID, replyID, draft)());
173
- }}
174
- onDelete={(commentID, replyID) => {
175
- if (!replyID) throw new Error('未选择要删除的评论');
176
- preparePanelDeletion(latest.current, commentID, replyID);
177
- const thread = base.find((item) => item.commentID === commentID)!;
178
- mutations.delete(thread, replyID, () => preparePanelDeletion(latest.current, commentID, replyID)());
179
- }}
180
- onToggleReaction={async (commentID, replyID, emojiKey) => {
181
- await locked(replyID, async () => {
182
- const current = latest.current;
183
- const feature = current.reactions;
184
- if (!feature?.onChange) throw new Error('未提供回应写入能力');
185
- const target: CommentTarget = { recordID: current.recordID, commentID, replyID };
186
- if (feature.canChange?.(target) === false) throw new Error('表情状态尚未加载或当前身份不可用');
187
- const existing = feature.getReactions(target).find((reaction) => reaction.emojiKey === emojiKey ||
188
- (emojiKey === 'THUMBSUP' && reaction.emojiKey === 'MediumThumbsup'));
189
- await feature.onChange({ ...target, emojiKey: existing?.emojiKey ?? emojiKey, action: existing?.reactedByMe ? 'remove' : 'add' });
190
- });
191
- }}
192
- loadReactionDetails={props.reactions?.loadUsers ? (commentID, replyID, signal) => loadPanelReactionDetails(
193
- latest.current, { recordID: props.recordID, commentID, replyID }, signal,
194
- ) : undefined}
195
- />
196
- </div>;
197
- }
198
-
199
- /** Controlled full comment UI; all persistence is supplied by the host. */
200
- export function CommentPanel(props: CommentPanelProps) {
201
- return <PanelSession key={JSON.stringify([props.scopeKey, props.recordID])} {...props} />;
202
- }
@@ -1,7 +0,0 @@
1
- export { CommentPanel } from './comment-panel';
2
- export { createAppCommentSource, withDocumentSource } from './app-source';
3
- export type * from './comment-panel.contract';
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';