summaryrefslogtreecommitdiffstats
path: root/polygerrit-ui/app/utils/comment-util.ts
diff options
context:
space:
mode:
Diffstat (limited to 'polygerrit-ui/app/utils/comment-util.ts')
-rw-r--r--polygerrit-ui/app/utils/comment-util.ts312
1 files changed, 217 insertions, 95 deletions
diff --git a/polygerrit-ui/app/utils/comment-util.ts b/polygerrit-ui/app/utils/comment-util.ts
index 5b08fabbc5..669c4916c7 100644
--- a/polygerrit-ui/app/utils/comment-util.ts
+++ b/polygerrit-ui/app/utils/comment-util.ts
@@ -29,85 +29,138 @@ import {
RevisionPatchSetNum,
AccountInfo,
AccountDetailInfo,
+ ChangeMessageInfo,
+ VotingRangeInfo,
} from '../types/common';
-import {CommentSide, Side, SpecialFilePath} from '../constants/constants';
+import {CommentSide, SpecialFilePath} from '../constants/constants';
import {parseDate} from './date-util';
-import {LineNumber} from '../elements/diff/gr-diff/gr-diff-line';
import {CommentIdToCommentThreadMap} from '../elements/diff/gr-comment-api/gr-comment-api';
import {isMergeParent, getParentIndex} from './patch-set-util';
import {DiffInfo} from '../types/diff';
+import {LineNumber} from '../api/diff';
+import {FormattedReviewerUpdateInfo} from '../types/types';
export interface DraftCommentProps {
- __draft?: boolean;
- __draftID?: string;
- __date?: Date;
+ // This must be true for all drafts. Drafts received from the backend will be
+ // modified immediately with __draft:true before allowing them to get into
+ // the application state.
+ __draft: boolean;
}
-export type DraftInfo = CommentBasics & DraftCommentProps;
-
-/**
- * Each of the type implements or extends CommentBasics.
- */
-export type Comment = DraftInfo | CommentInfo | RobotCommentInfo;
-
-export interface UIStateCommentProps {
- collapsed?: boolean;
+export interface UnsavedCommentProps {
+ // This must be true for all unsaved comment drafts. An unsaved draft is
+ // always just local to a comment component like <gr-comment> or
+ // <gr-comment-thread>. Unsaved drafts will never appear in the application
+ // state.
+ __unsaved: boolean;
}
-export interface UIStateDraftProps {
- __editing?: boolean;
-}
-
-export type UIDraft = DraftInfo & UIStateCommentProps & UIStateDraftProps;
-
-export type UIHuman = CommentInfo & UIStateCommentProps;
+export type DraftInfo = CommentInfo & DraftCommentProps;
-export type UIRobot = RobotCommentInfo & UIStateCommentProps;
+export type UnsavedInfo = CommentBasics & UnsavedCommentProps;
-export type UIComment = UIHuman | UIRobot | UIDraft;
+export type Comment = UnsavedInfo | DraftInfo | CommentInfo | RobotCommentInfo;
export type CommentMap = {[path: string]: boolean};
-export function isRobot<T extends CommentInfo>(
+export function isRobot<T extends CommentBasics>(
x: T | DraftInfo | RobotCommentInfo | undefined
): x is RobotCommentInfo {
return !!x && !!(x as RobotCommentInfo).robot_id;
}
-export function isDraft<T extends CommentInfo>(
- x: T | UIDraft | undefined
-): x is UIDraft {
- return !!x && !!(x as UIDraft).__draft;
+export function isDraft<T extends CommentBasics>(
+ x: T | DraftInfo | undefined
+): x is DraftInfo {
+ return !!x && !!(x as DraftInfo).__draft;
+}
+
+export function isUnsaved<T extends CommentBasics>(
+ x: T | UnsavedInfo | undefined
+): x is UnsavedInfo {
+ return !!x && !!(x as UnsavedInfo).__unsaved;
+}
+
+export function isDraftOrUnsaved<T extends CommentBasics>(
+ x: T | DraftInfo | UnsavedInfo | undefined
+): x is UnsavedInfo | DraftInfo {
+ return isDraft(x) || isUnsaved(x);
}
interface SortableComment {
- __draft?: boolean;
- __date?: Date;
- updated?: Timestamp;
- id?: UrlEncodedCommentId;
+ updated: Timestamp;
+ id: UrlEncodedCommentId;
+}
+
+export interface ChangeMessage extends ChangeMessageInfo {
+ // TODO(TS): maybe should be an enum instead
+ type: string;
+ expanded: boolean;
+ commentThreads: CommentThread[];
}
+export function isFormattedReviewerUpdate(
+ message: ChangeMessage
+): message is ChangeMessage & FormattedReviewerUpdateInfo {
+ return message.type === 'REVIEWER_UPDATE';
+}
+
+export type LabelExtreme = {[labelName: string]: VotingRangeInfo};
+
+export const PATCH_SET_PREFIX_PATTERN =
+ /^(?:Uploaded\s*)?[Pp]atch [Ss]et \d+:\s*(.*)/;
+
export function sortComments<T extends SortableComment>(comments: T[]): T[] {
return comments.slice(0).sort((c1, c2) => {
- const d1 = !!c1.__draft;
- const d2 = !!c2.__draft;
+ const d1 = isDraft(c1);
+ const d2 = isDraft(c2);
if (d1 !== d2) return d1 ? 1 : -1;
- const date1 = (c1.updated && parseDate(c1.updated)) || c1.__date;
- const date2 = (c2.updated && parseDate(c2.updated)) || c2.__date;
- const dateDiff = date1!.valueOf() - date2!.valueOf();
+ const date1 = parseDate(c1.updated);
+ const date2 = parseDate(c2.updated);
+ const dateDiff = date1.valueOf() - date2.valueOf();
if (dateDiff !== 0) return dateDiff;
- const id1 = c1.id ?? '';
- const id2 = c2.id ?? '';
+ const id1 = c1.id;
+ const id2 = c2.id;
return id1.localeCompare(id2);
});
}
-export function createCommentThreads(
- comments: UIComment[],
- patchRange?: PatchRange
-) {
+export function createUnsavedComment(thread: CommentThread): UnsavedInfo {
+ return {
+ path: thread.path,
+ patch_set: thread.patchNum,
+ side: thread.commentSide ?? CommentSide.REVISION,
+ line: typeof thread.line === 'number' ? thread.line : undefined,
+ range: thread.range,
+ parent: thread.mergeParentNum,
+ message: '',
+ unresolved: true,
+ __unsaved: true,
+ };
+}
+
+export function createUnsavedReply(
+ replyingTo: CommentInfo,
+ message: string,
+ unresolved: boolean
+): UnsavedInfo {
+ return {
+ path: replyingTo.path,
+ patch_set: replyingTo.patch_set,
+ side: replyingTo.side,
+ line: replyingTo.line,
+ range: replyingTo.range,
+ parent: replyingTo.parent,
+ in_reply_to: replyingTo.id,
+ message,
+ unresolved,
+ __unsaved: true,
+ };
+}
+
+export function createCommentThreads(comments: CommentInfo[]) {
const sortedComments = sortComments(comments);
const threads: CommentThread[] = [];
const idThreadMap: CommentIdToCommentThreadMap = {};
@@ -129,7 +182,6 @@ export function createCommentThreads(
const newThread: CommentThread = {
comments: [comment],
patchNum: comment.patch_set,
- diffSide: Side.LEFT,
commentSide: comment.side ?? CommentSide.REVISION,
mergeParentNum: comment.parent,
path: comment.path,
@@ -137,13 +189,6 @@ export function createCommentThreads(
range: comment.range,
rootId: comment.id,
};
- if (patchRange) {
- if (isInBaseOfPatchRange(comment, patchRange))
- newThread.diffSide = Side.LEFT;
- else if (isInRevisionOfPatchRange(comment, patchRange))
- newThread.diffSide = Side.RIGHT;
- else throw new Error('comment does not belong in given patchrange');
- }
if (!comment.line && !comment.range) {
newThread.line = 'FILE';
}
@@ -154,68 +199,126 @@ export function createCommentThreads(
}
export interface CommentThread {
- comments: UIComment[];
+ /**
+ * This can only contain at most one draft. And if so, then it is the last
+ * comment in this list. This must not contain unsaved drafts.
+ */
+ comments: Array<CommentInfo | DraftInfo | RobotCommentInfo>;
+ /**
+ * Identical to the id of the first comment. If this is undefined, then the
+ * thread only contains an unsaved draft.
+ */
+ rootId?: UrlEncodedCommentId;
+ /**
+ * Note that all location information is typically identical to that of the
+ * first comment, but not for ported comments!
+ */
path: string;
commentSide: CommentSide;
/* mergeParentNum is the merge parent number only valid for merge commits
when commentSide is PARENT.
mergeParentNum is undefined for auto merge commits
+ Same as `parent` in CommentInfo.
*/
mergeParentNum?: number;
patchNum?: PatchSetNum;
+ /* Different from CommentInfo, which just keeps the line undefined for
+ FILE comments. */
line?: LineNumber;
- /* rootId is optional since we create a empty comment thread element for
- drafts and then create the draft which becomes the root */
- rootId?: UrlEncodedCommentId;
- diffSide?: Side;
range?: CommentRange;
- ported?: boolean; // is the comment ported over from a previous patchset
- rangeInfoLost?: boolean; // if BE was unable to determine a range for this
+ /**
+ * Was the thread ported over from its original location to a newer patchset?
+ * If yes, then the location information above contains the ported location,
+ * but the comments still have the original location set.
+ */
+ ported?: boolean;
+ /**
+ * Only relevant when ported:true. Means that no ported range could be
+ * computed. `line` and `range` can be undefined then.
+ */
+ rangeInfoLost?: boolean;
}
-export function getLastComment(thread?: CommentThread): UIComment | undefined {
- const len = thread?.comments.length;
- return thread && len ? thread.comments[len - 1] : undefined;
+export function equalLocation(t1?: CommentThread, t2?: CommentThread) {
+ if (t1 === t2) return true;
+ if (t1 === undefined || t2 === undefined) return false;
+ return (
+ t1.path === t2.path &&
+ t1.patchNum === t2.patchNum &&
+ t1.commentSide === t2.commentSide &&
+ t1.line === t2.line &&
+ t1.range?.start_line === t2.range?.start_line &&
+ t1.range?.start_character === t2.range?.start_character &&
+ t1.range?.end_line === t2.range?.end_line &&
+ t1.range?.end_character === t2.range?.end_character
+ );
}
-export function getFirstComment(thread?: CommentThread): UIComment | undefined {
- return thread?.comments?.[0];
+export function getLastComment(thread: CommentThread): CommentInfo | undefined {
+ const len = thread.comments.length;
+ return thread.comments[len - 1];
}
-export function countComments(thread?: CommentThread) {
- return thread?.comments?.length ?? 0;
+export function getLastPublishedComment(
+ thread: CommentThread
+): CommentInfo | undefined {
+ const publishedComments = thread.comments.filter(c => !isDraftOrUnsaved(c));
+ const len = publishedComments.length;
+ return publishedComments[len - 1];
}
-export function isPatchsetLevel(thread?: CommentThread): boolean {
- return thread?.path === SpecialFilePath.PATCHSET_LEVEL_COMMENTS;
+export function getFirstComment(
+ thread: CommentThread
+): CommentInfo | undefined {
+ return thread.comments[0];
}
-export function isUnresolved(thread?: CommentThread): boolean {
+export function countComments(thread: CommentThread) {
+ return thread.comments.length;
+}
+
+export function isPatchsetLevel(thread: CommentThread): boolean {
+ return thread.path === SpecialFilePath.PATCHSET_LEVEL_COMMENTS;
+}
+
+export function isUnresolved(thread: CommentThread): boolean {
return !isResolved(thread);
}
-export function isResolved(thread?: CommentThread): boolean {
- return !getLastComment(thread)?.unresolved;
+export function isResolved(thread: CommentThread): boolean {
+ const lastUnresolved = getLastComment(thread)?.unresolved;
+ return !lastUnresolved ?? false;
}
-export function isDraftThread(thread?: CommentThread): boolean {
+export function isDraftThread(thread: CommentThread): boolean {
return isDraft(getLastComment(thread));
}
-export function isRobotThread(thread?: CommentThread): boolean {
+export function isRobotThread(thread: CommentThread): boolean {
return isRobot(getFirstComment(thread));
}
-export function hasHumanReply(thread?: CommentThread): boolean {
+export function hasHumanReply(thread: CommentThread): boolean {
return countComments(thread) > 1 && !isRobot(getLastComment(thread));
}
+export function lastUpdated(thread: CommentThread): Date | undefined {
+ // We don't want to re-sort comments when you save a draft reply, so
+ // we stick to the timestampe of the last *published* comment.
+ const lastUpdated =
+ getLastPublishedComment(thread)?.updated ?? getLastComment(thread)?.updated;
+ return lastUpdated !== undefined ? parseDate(lastUpdated) : undefined;
+}
/**
* Whether the given comment should be included in the base side of the
* given patch range.
*/
export function isInBaseOfPatchRange(
- comment: CommentBasics,
+ comment: {
+ patch_set?: PatchSetNum;
+ side?: CommentSide;
+ parent?: number;
+ },
range: PatchRange
) {
// If the base of the patch range is a parent of a merge, and the comment
@@ -249,7 +352,10 @@ export function isInBaseOfPatchRange(
* given patch range.
*/
export function isInRevisionOfPatchRange(
- comment: CommentBasics,
+ comment: {
+ patch_set?: PatchSetNum;
+ side?: CommentSide;
+ },
range: PatchRange
) {
return (
@@ -271,7 +377,7 @@ export function isInPatchRange(
}
export function getPatchRangeForCommentUrl(
- comment: UIComment,
+ comment: Comment,
latestPatchNum: RevisionPatchSetNum
) {
if (!comment.patch_set) throw new Error('Missing comment.patch_set');
@@ -279,7 +385,7 @@ export function getPatchRangeForCommentUrl(
// TODO(dhruvsri): Add handling for comment left on parents of merge commits
if (comment.side === CommentSide.PARENT) {
if (comment.patch_set === ParentPatchSetNum)
- throw new Error('diffSide cannot be PARENT');
+ throw new Error('comment.patch_set cannot be PARENT');
return {
patchNum: comment.patch_set as RevisionPatchSetNum,
basePatchNum: ParentPatchSetNum,
@@ -291,7 +397,7 @@ export function getPatchRangeForCommentUrl(
};
} else {
return {
- patchNum: latestPatchNum as RevisionPatchSetNum,
+ patchNum: latestPatchNum,
basePatchNum: comment.patch_set as BasePatchSetNum,
};
}
@@ -355,30 +461,46 @@ export function getCommentAuthors(
return authors;
}
-export function computeId(comment: UIComment) {
- if (comment.id) return comment.id;
- if (isDraft(comment)) return comment.__draftID;
- throw new Error('Missing id in root comment.');
-}
-
/**
- * Add path info to every comment as CommentInfo returned
- * from server does not have that.
- *
- * TODO(taoalpha): should consider changing BE to send path
- * back within CommentInfo
+ * Add path info to every comment as CommentInfo returned from server does not
+ * have that.
*/
export function addPath<T>(comments: {[path: string]: T[]} = {}): {
[path: string]: Array<T & {path: string}>;
} {
const updatedComments: {[path: string]: Array<T & {path: string}>} = {};
for (const filePath of Object.keys(comments)) {
- const allCommentsForPath = comments[filePath] || [];
- if (allCommentsForPath.length) {
- updatedComments[filePath] = allCommentsForPath.map(comment => {
- return {...comment, path: filePath};
- });
- }
+ updatedComments[filePath] = (comments[filePath] || []).map(comment => {
+ return {...comment, path: filePath};
+ });
}
return updatedComments;
}
+
+/**
+ * Add __draft:true to all drafts returned from server so that they can be told
+ * apart from published comments easily.
+ */
+export function addDraftProp(
+ draftsByPath: {[path: string]: CommentInfo[]} = {}
+) {
+ const updated: {[path: string]: DraftInfo[]} = {};
+ for (const filePath of Object.keys(draftsByPath)) {
+ updated[filePath] = (draftsByPath[filePath] ?? []).map(draft => {
+ return {...draft, __draft: true};
+ });
+ }
+ return updated;
+}
+
+export function reportingDetails(comment: CommentBasics) {
+ return {
+ id: comment?.id,
+ message_length: comment?.message?.trim().length,
+ in_reply_to: comment?.in_reply_to,
+ unresolved: comment?.unresolved,
+ path_length: comment?.path?.length,
+ line: comment?.range?.start_line ?? comment?.line,
+ unsaved: isUnsaved(comment),
+ };
+}