snapshot of https://github.com/dream-num/univer.git (dev, 2026-09-16; blobs > 131072B omitted)
Some checks failed
📦 Build / build (push) Has been cancelled
🧪 CI Test / test shard (1/4) (push) Has been cancelled
🧪 CI Test / test shard (2/4) (push) Has been cancelled
🧪 CI Test / test shard (3/4) (push) Has been cancelled
🧪 CI Test / test shard (4/4) (push) Has been cancelled
🧪 CI Test / test (push) Has been cancelled
🧪 CI Test / eslint (push) Has been cancelled
🧪 CI Test / typecheck (push) Has been cancelled
🔫 Sync to Pro / dispatch-sync (push) Has been cancelled
🔮 Sync Mirror / sync (push) Has been cancelled
Some checks failed
📦 Build / build (push) Has been cancelled
🧪 CI Test / test shard (1/4) (push) Has been cancelled
🧪 CI Test / test shard (2/4) (push) Has been cancelled
🧪 CI Test / test shard (3/4) (push) Has been cancelled
🧪 CI Test / test shard (4/4) (push) Has been cancelled
🧪 CI Test / test (push) Has been cancelled
🧪 CI Test / eslint (push) Has been cancelled
🧪 CI Test / typecheck (push) Has been cancelled
🔫 Sync to Pro / dispatch-sync (push) Has been cancelled
🔮 Sync Mirror / sync (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,304 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IDocumentBody } from '@univerjs/core';
|
||||
import type { IThreadCommentQuery, IThreadInfo } from '../models/thread-comment.model';
|
||||
import type { IThreadCommentAnchor, ThreadCommentAnchorKind } from '../types/comment-anchor';
|
||||
import { generateRandomId, ICommandService, Inject, UserManagerService } from '@univerjs/core';
|
||||
import {
|
||||
AddCommentCommand,
|
||||
DeleteCommentCommand,
|
||||
DeleteCommentTreeCommand,
|
||||
ResolveCommentCommand,
|
||||
UpdateCommentCommand,
|
||||
} from '../commands/commands/comment.command';
|
||||
import { getDT } from '../common/utils';
|
||||
import { ThreadCommentModel } from '../models/thread-comment.model';
|
||||
import { deserializeThreadCommentAnchor, serializeThreadCommentAnchor } from '../types/comment-anchor';
|
||||
|
||||
export interface ICreateThreadCommentOptions {
|
||||
/** Univer unit that owns the comment. */
|
||||
unitId: string;
|
||||
/** Sheet, page, table, or product subunit that owns the comment. */
|
||||
subUnitId: string;
|
||||
/** Stable, serializable product anchor. */
|
||||
anchor: IThreadCommentAnchor;
|
||||
/** Plain text or a Univer document body for rich comment content. */
|
||||
content: ThreadCommentContent;
|
||||
/** Attachment resource IDs. */
|
||||
attachments?: string[];
|
||||
/** Caller-supplied stable comment ID, useful for idempotent agent workflows. */
|
||||
id?: string;
|
||||
/** Caller-supplied thread ID. Defaults to the root comment ID. */
|
||||
threadId?: string;
|
||||
/** Author user ID. Defaults to the current user managed by `UserManagerService`. */
|
||||
personId?: string;
|
||||
/** Creation time. Defaults to the current time. */
|
||||
dateTime?: Date;
|
||||
}
|
||||
|
||||
export interface IReplyThreadCommentOptions {
|
||||
/** Univer unit that owns the thread. */
|
||||
unitId: string;
|
||||
/** Sheet, page, table, or product subunit that owns the thread. */
|
||||
subUnitId: string;
|
||||
/** Stable ID of the root thread being replied to. */
|
||||
threadId: string;
|
||||
/** Plain text or a Univer document body for rich reply content. */
|
||||
content: ThreadCommentContent;
|
||||
/** Attachment resource IDs. */
|
||||
attachments?: string[];
|
||||
/** Caller-supplied stable reply ID, useful for idempotent agent workflows. */
|
||||
id?: string;
|
||||
/** Author user ID. Defaults to the current user managed by `UserManagerService`. */
|
||||
personId?: string;
|
||||
/** Creation time. Defaults to the current time. */
|
||||
dateTime?: Date;
|
||||
}
|
||||
|
||||
export interface IUpdateThreadCommentOptions {
|
||||
/** Univer unit that owns the comment. */
|
||||
unitId: string;
|
||||
/** Sheet, page, table, or product subunit that owns the comment. */
|
||||
subUnitId: string;
|
||||
/** Stable ID of the root comment or reply to update. */
|
||||
commentId: string;
|
||||
/** Replacement plain text or Univer document body. */
|
||||
content: ThreadCommentContent;
|
||||
/** Replacement attachment resource IDs. Omit to preserve the current attachments. */
|
||||
attachments?: string[];
|
||||
/** Update time. Defaults to the current time. */
|
||||
dateTime?: Date;
|
||||
}
|
||||
|
||||
export interface IDeleteThreadCommentOptions {
|
||||
/** Univer unit that owns the comment. */
|
||||
unitId: string;
|
||||
/** Sheet, page, table, or product subunit that owns the comment. */
|
||||
subUnitId: string;
|
||||
/** Stable ID of the root comment or reply to delete. */
|
||||
commentId: string;
|
||||
/** Deletes the complete root and reply tree when true. */
|
||||
deleteThread?: boolean;
|
||||
}
|
||||
|
||||
export interface IResolveThreadCommentOptions {
|
||||
/** Univer unit that owns the thread. */
|
||||
unitId: string;
|
||||
/** Sheet, page, table, or product subunit that owns the thread. */
|
||||
subUnitId: string;
|
||||
/** Stable ID of the root comment or a reply in the target thread. */
|
||||
commentId: string;
|
||||
/** Defaults to `true`. Pass `false` to reopen the thread. */
|
||||
resolved?: boolean;
|
||||
}
|
||||
|
||||
/** Plain text or a Univer document body used as comment content. */
|
||||
export type ThreadCommentContent = string | IDocumentBody;
|
||||
|
||||
const DOCUMENT_BODY_ARRAY_FIELDS = [
|
||||
'renderedPageBreaks',
|
||||
'textRuns',
|
||||
'paragraphs',
|
||||
'sectionBreaks',
|
||||
'customBlocks',
|
||||
'tables',
|
||||
'columnGroups',
|
||||
'blockRanges',
|
||||
'customRanges',
|
||||
'customDecorations',
|
||||
] as const;
|
||||
|
||||
export function isThreadCommentDocumentBody(value: unknown): value is IDocumentBody {
|
||||
if (!value || typeof value !== 'object') {
|
||||
return false;
|
||||
}
|
||||
if (typeof Reflect.get(value, 'dataStream') !== 'string') {
|
||||
return false;
|
||||
}
|
||||
if (DOCUMENT_BODY_ARRAY_FIELDS.some((field) => {
|
||||
const fieldValue = Reflect.get(value, field);
|
||||
return fieldValue !== undefined && !Array.isArray(fieldValue);
|
||||
})) {
|
||||
return false;
|
||||
}
|
||||
const payloads = Reflect.get(value, 'payloads');
|
||||
return payloads === undefined || (
|
||||
payloads !== null
|
||||
&& typeof payloads === 'object'
|
||||
&& !Array.isArray(payloads)
|
||||
&& Object.values(payloads).every((item) => typeof item === 'string')
|
||||
);
|
||||
}
|
||||
|
||||
export function normalizeThreadCommentContent(content: ThreadCommentContent): IDocumentBody {
|
||||
if (typeof content !== 'string') {
|
||||
if (!isThreadCommentDocumentBody(content)) {
|
||||
throw new TypeError('Invalid thread comment content');
|
||||
}
|
||||
if (!content.dataStream.replace(/[\r\n]/g, '').trim()) {
|
||||
throw new TypeError('Thread comment content cannot be empty');
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
if (!content.trim()) {
|
||||
throw new TypeError('Thread comment content cannot be empty');
|
||||
}
|
||||
|
||||
const dataStream = content.replace(/\r\n?|\n/g, '\r').replace(/\r*$/, '\r\n');
|
||||
return { dataStream };
|
||||
}
|
||||
|
||||
export interface IFacadeThreadCommentInfo extends Omit<IThreadInfo, 'relativeUsers'> {
|
||||
/**
|
||||
* Product anchor category. Facade query results always populate this field, including for legacy
|
||||
* Sheet cell and Document range comments. It remains optional for source compatibility with
|
||||
* existing `IFacadeThreadCommentInfo` implementations.
|
||||
*/
|
||||
anchorKind?: ThreadCommentAnchorKind | null;
|
||||
/** Parsed stable product anchor, or `null` for a legacy comment reference. */
|
||||
anchor: IThreadCommentAnchor | null;
|
||||
/** Unique IDs of authors participating in the root and reply tree. */
|
||||
relativeUserIds: string[];
|
||||
}
|
||||
|
||||
function toFacadeInfo(thread: IThreadInfo, anchorKind: ThreadCommentAnchorKind | null): IFacadeThreadCommentInfo {
|
||||
return {
|
||||
unitId: thread.unitId,
|
||||
subUnitId: thread.subUnitId,
|
||||
threadId: thread.threadId,
|
||||
root: thread.root,
|
||||
children: thread.children,
|
||||
anchorKind,
|
||||
anchor: deserializeThreadCommentAnchor(thread.root.ref),
|
||||
relativeUserIds: Array.from(thread.relativeUsers),
|
||||
};
|
||||
}
|
||||
|
||||
export class ThreadCommentFacadeService {
|
||||
constructor(
|
||||
@Inject(ICommandService) private readonly _commandService: ICommandService,
|
||||
@Inject(ThreadCommentModel) private readonly _model: ThreadCommentModel,
|
||||
@Inject(UserManagerService) private readonly _userManagerService: UserManagerService
|
||||
) {}
|
||||
|
||||
createCommentAsync(options: ICreateThreadCommentOptions): Promise<boolean> {
|
||||
const id = options.id ?? generateRandomId();
|
||||
const threadId = options.threadId ?? id;
|
||||
const personId = options.personId ?? this._userManagerService.getCurrentUser().userID;
|
||||
|
||||
return this._commandService.executeCommand(AddCommentCommand.id, {
|
||||
unitId: options.unitId,
|
||||
subUnitId: options.subUnitId,
|
||||
comment: {
|
||||
id,
|
||||
threadId,
|
||||
unitId: options.unitId,
|
||||
subUnitId: options.subUnitId,
|
||||
ref: serializeThreadCommentAnchor(options.anchor),
|
||||
text: normalizeThreadCommentContent(options.content),
|
||||
attachments: options.attachments ?? [],
|
||||
dT: getDT(options.dateTime),
|
||||
personId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
replyCommentAsync(options: IReplyThreadCommentOptions): Promise<boolean> {
|
||||
const root = this._model.getRootComment(options.unitId, options.subUnitId, options.threadId);
|
||||
if (!root) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
return this._commandService.executeCommand(AddCommentCommand.id, {
|
||||
unitId: options.unitId,
|
||||
subUnitId: options.subUnitId,
|
||||
comment: {
|
||||
id: options.id ?? generateRandomId(),
|
||||
threadId: options.threadId,
|
||||
parentId: root.id,
|
||||
unitId: options.unitId,
|
||||
subUnitId: options.subUnitId,
|
||||
ref: root.ref,
|
||||
text: normalizeThreadCommentContent(options.content),
|
||||
attachments: options.attachments ?? [],
|
||||
dT: getDT(options.dateTime),
|
||||
personId: options.personId ?? this._userManagerService.getCurrentUser().userID,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
updateCommentAsync(options: IUpdateThreadCommentOptions): Promise<boolean> {
|
||||
return this._commandService.executeCommand(UpdateCommentCommand.id, {
|
||||
unitId: options.unitId,
|
||||
subUnitId: options.subUnitId,
|
||||
payload: {
|
||||
commentId: options.commentId,
|
||||
text: normalizeThreadCommentContent(options.content),
|
||||
attachments: options.attachments,
|
||||
updateT: getDT(options.dateTime),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
deleteCommentAsync(options: IDeleteThreadCommentOptions): Promise<boolean> {
|
||||
return this._commandService.executeCommand(
|
||||
options.deleteThread ? DeleteCommentTreeCommand.id : DeleteCommentCommand.id,
|
||||
{
|
||||
unitId: options.unitId,
|
||||
subUnitId: options.subUnitId,
|
||||
commentId: options.commentId,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
resolveCommentAsync(options: IResolveThreadCommentOptions): Promise<boolean> {
|
||||
return this._commandService.executeCommand(ResolveCommentCommand.id, {
|
||||
unitId: options.unitId,
|
||||
subUnitId: options.subUnitId,
|
||||
commentId: options.commentId,
|
||||
resolved: options.resolved ?? true,
|
||||
});
|
||||
}
|
||||
|
||||
getComments(query: IThreadCommentQuery = {}): IFacadeThreadCommentInfo[] {
|
||||
return this._model.query(query).map((thread) => toFacadeInfo(
|
||||
thread,
|
||||
this._model.getAnchorKind(thread.unitId, thread.root.ref)
|
||||
));
|
||||
}
|
||||
|
||||
async listCommentsAsync(query: IThreadCommentQuery = {}): Promise<IFacadeThreadCommentInfo[]> {
|
||||
const knownThreads = this._model.query({ unitIds: query.unitIds, subUnitIds: query.subUnitIds });
|
||||
const groups = new Map<string, { unitId: string; subUnitId: string; threadIds: string[] }>();
|
||||
knownThreads.forEach((thread) => {
|
||||
const key = `${thread.unitId}\0${thread.subUnitId}`;
|
||||
const group = groups.get(key) ?? { unitId: thread.unitId, subUnitId: thread.subUnitId, threadIds: [] };
|
||||
group.threadIds.push(thread.threadId);
|
||||
groups.set(key, group);
|
||||
});
|
||||
await Promise.all(Array.from(groups.values()).map((group) => this._model.syncThreadComments(
|
||||
group.unitId,
|
||||
group.subUnitId,
|
||||
group.threadIds
|
||||
)));
|
||||
return this._model.query(query).map((thread) => toFacadeInfo(
|
||||
thread,
|
||||
this._model.getAnchorKind(thread.unitId, thread.root.ref)
|
||||
));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user