mirror of
https://github.com/microsoft/vscode.git
synced 2026-05-08 09:08:48 +01:00
Add "sort by updated at" option to comments panel (#221665)
* feat: add "sort by updated at" to comments panel Fixes #149449 * fix: sort comments panel using ObjectTree sorter * fix: address PR review suggestions * Move sorting back into filter menu and fix nits --------- Co-authored-by: Alex Ross <alros@microsoft.com>
This commit is contained in:
@@ -484,6 +484,7 @@ export class CommentsList extends WorkbenchObjectTree<CommentsModel | ResourceWi
|
||||
collapseByDefault: false,
|
||||
overrideStyles: options.overrideStyles,
|
||||
filter: options.filter,
|
||||
sorter: options.sorter,
|
||||
findWidgetEnabled: false,
|
||||
multipleSelectionSupport: false,
|
||||
},
|
||||
|
||||
@@ -24,26 +24,28 @@ import { IOpenerService } from 'vs/platform/opener/common/opener';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
|
||||
import { CommentsViewFilterFocusContextKey, ICommentsView } from 'vs/workbench/contrib/comments/browser/comments';
|
||||
import { CommentsFilters, CommentsFiltersChangeEvent } from 'vs/workbench/contrib/comments/browser/commentsViewActions';
|
||||
import { CommentsFilters, CommentsFiltersChangeEvent, CommentsSortOrder } from 'vs/workbench/contrib/comments/browser/commentsViewActions';
|
||||
import { Memento, MementoObject } from 'vs/workbench/common/memento';
|
||||
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
|
||||
import { FilterOptions } from 'vs/workbench/contrib/comments/browser/commentsFilterOptions';
|
||||
import { CommentThreadApplicability, CommentThreadState } from 'vs/editor/common/languages';
|
||||
import { ITreeElement } from 'vs/base/browser/ui/tree/tree';
|
||||
import { Iterable } from 'vs/base/common/iterator';
|
||||
import { revealCommentThread } from 'vs/workbench/contrib/comments/browser/commentsController';
|
||||
import { registerNavigableContainer } from 'vs/workbench/browser/actions/widgetNavigationCommands';
|
||||
import { CommentsModel, ICommentsModel } from 'vs/workbench/contrib/comments/browser/commentsModel';
|
||||
import { CommentsModel, type ICommentsModel } from 'vs/workbench/contrib/comments/browser/commentsModel';
|
||||
import { IHoverService } from 'vs/platform/hover/browser/hover';
|
||||
import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration';
|
||||
import { AccessibleViewAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions';
|
||||
import type { ITreeElement } from 'vs/base/browser/ui/tree/tree';
|
||||
|
||||
export const CONTEXT_KEY_HAS_COMMENTS = new RawContextKey<boolean>('commentsView.hasComments', false);
|
||||
export const CONTEXT_KEY_SOME_COMMENTS_EXPANDED = new RawContextKey<boolean>('commentsView.someCommentsExpanded', false);
|
||||
export const CONTEXT_KEY_COMMENT_FOCUSED = new RawContextKey<boolean>('commentsView.commentFocused', false);
|
||||
const VIEW_STORAGE_ID = 'commentsViewState';
|
||||
|
||||
function createResourceCommentsIterator(model: ICommentsModel): Iterable<ITreeElement<ResourceWithCommentThreads | CommentNode>> {
|
||||
type CommentsTreeNode = CommentsModel | ResourceWithCommentThreads | CommentNode;
|
||||
|
||||
function createResourceCommentsIterator(model: ICommentsModel): Iterable<ITreeElement<CommentsTreeNode>> {
|
||||
return Iterable.map(model.resourceCommentThreads, m => {
|
||||
const CommentNodeIt = Iterable.from(m.commentThreads);
|
||||
const children = Iterable.map(CommentNodeIt, r => ({ element: r }));
|
||||
@@ -161,6 +163,7 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView {
|
||||
this.filters = this._register(new CommentsFilters({
|
||||
showResolved: this.viewState['showResolved'] !== false,
|
||||
showUnresolved: this.viewState['showUnresolved'] !== false,
|
||||
sortBy: this.viewState['sortBy'],
|
||||
}, this.contextKeyService));
|
||||
this.filter = new Filter(new FilterOptions(this.filterWidget.getFilterText(), this.filters.showResolved, this.filters.showUnresolved));
|
||||
|
||||
@@ -168,6 +171,9 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView {
|
||||
if (event.showResolved || event.showUnresolved) {
|
||||
this.updateFilter();
|
||||
}
|
||||
if (event.sortBy) {
|
||||
this.refresh();
|
||||
}
|
||||
}));
|
||||
this._register(this.filterWidget.onDidChangeFilterText(() => this.updateFilter()));
|
||||
}
|
||||
@@ -177,6 +183,7 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView {
|
||||
this.viewState['filterHistory'] = this.filterWidget.getHistory();
|
||||
this.viewState['showResolved'] = this.filters.showResolved;
|
||||
this.viewState['showUnresolved'] = this.filters.showUnresolved;
|
||||
this.viewState['sortBy'] = this.filters.sortBy;
|
||||
this.stateMemento.saveMemento();
|
||||
super.saveState();
|
||||
}
|
||||
@@ -270,10 +277,10 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView {
|
||||
}
|
||||
}
|
||||
|
||||
private async renderComments(): Promise<void> {
|
||||
private renderComments(): void {
|
||||
this.treeContainer.classList.toggle('hidden', !this.commentService.commentsModel.hasCommentThreads());
|
||||
this.renderMessage();
|
||||
await this.tree?.setChildren(null, createResourceCommentsIterator(this.commentService.commentsModel));
|
||||
this.tree?.setChildren(null, createResourceCommentsIterator(this.commentService.commentsModel));
|
||||
}
|
||||
|
||||
public collapseAll() {
|
||||
@@ -391,8 +398,16 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView {
|
||||
overrideStyles: this.getLocationBasedColors().listOverrideStyles,
|
||||
selectionNavigation: true,
|
||||
filter: this.filter,
|
||||
sorter: {
|
||||
compare: (a: CommentsTreeNode, b: CommentsTreeNode) => {
|
||||
if (this.filters.sortBy === CommentsSortOrder.UpdatedAtDescending && !(a instanceof CommentsModel) && !(b instanceof CommentsModel)) {
|
||||
return a.lastUpdatedAt > b.lastUpdatedAt ? -1 : 1;
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
},
|
||||
keyboardNavigationLabelProvider: {
|
||||
getKeyboardNavigationLabel: (item: CommentsModel | ResourceWithCommentThreads | CommentNode) => {
|
||||
getKeyboardNavigationLabel: (item: CommentsTreeNode) => {
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
@@ -449,11 +464,8 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView {
|
||||
}
|
||||
if (this.isVisible()) {
|
||||
this.hasCommentsContextKey.set(this.commentService.commentsModel.hasCommentThreads());
|
||||
|
||||
this.treeContainer.classList.toggle('hidden', !this.commentService.commentsModel.hasCommentThreads());
|
||||
this.cachedFilterStats = undefined;
|
||||
this.renderMessage();
|
||||
this.tree?.setChildren(null, createResourceCommentsIterator(this.commentService.commentsModel));
|
||||
this.renderComments();
|
||||
|
||||
if (this.tree.getSelection().length === 0 && this.commentService.commentsModel.hasCommentThreads()) {
|
||||
const firstComment = this.commentService.commentsModel.resourceCommentThreads[0].commentThreads[0];
|
||||
|
||||
@@ -10,24 +10,34 @@ import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation
|
||||
import { ContextKeyExpr, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { CommentsViewFilterFocusContextKey, ICommentsView } from 'vs/workbench/contrib/comments/browser/comments';
|
||||
import { registerAction2 } from 'vs/platform/actions/common/actions';
|
||||
import { MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions';
|
||||
import { ViewAction } from 'vs/workbench/browser/parts/views/viewPane';
|
||||
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
|
||||
import { COMMENTS_VIEW_ID } from 'vs/workbench/contrib/comments/browser/commentsTreeViewer';
|
||||
import { FocusedViewContext } from 'vs/workbench/common/contextkeys';
|
||||
import { viewFilterSubmenu } from 'vs/workbench/browser/parts/views/viewFilter';
|
||||
import { Codicon } from 'vs/base/common/codicons';
|
||||
|
||||
export const enum CommentsSortOrder {
|
||||
ResourceAscending = 'resourceAscending',
|
||||
UpdatedAtDescending = 'updatedAtDescending',
|
||||
}
|
||||
|
||||
|
||||
const CONTEXT_KEY_SHOW_RESOLVED = new RawContextKey<boolean>('commentsView.showResolvedFilter', true);
|
||||
const CONTEXT_KEY_SHOW_UNRESOLVED = new RawContextKey<boolean>('commentsView.showUnResolvedFilter', true);
|
||||
const CONTEXT_KEY_SORT_BY = new RawContextKey<CommentsSortOrder>('commentsView.sortBy', CommentsSortOrder.ResourceAscending);
|
||||
|
||||
export interface CommentsFiltersChangeEvent {
|
||||
showResolved?: boolean;
|
||||
showUnresolved?: boolean;
|
||||
sortBy?: CommentsSortOrder;
|
||||
}
|
||||
|
||||
interface CommentsFiltersOptions {
|
||||
showResolved: boolean;
|
||||
showUnresolved: boolean;
|
||||
sortBy: CommentsSortOrder;
|
||||
}
|
||||
|
||||
export class CommentsFilters extends Disposable {
|
||||
@@ -39,6 +49,7 @@ export class CommentsFilters extends Disposable {
|
||||
super();
|
||||
this._showResolved.set(options.showResolved);
|
||||
this._showUnresolved.set(options.showUnresolved);
|
||||
this._sortBy.set(options.sortBy);
|
||||
}
|
||||
|
||||
private readonly _showUnresolved = CONTEXT_KEY_SHOW_UNRESOLVED.bindTo(this.contextKeyService);
|
||||
@@ -63,6 +74,16 @@ export class CommentsFilters extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
private _sortBy = CONTEXT_KEY_SORT_BY.bindTo(this.contextKeyService);
|
||||
get sortBy(): CommentsSortOrder {
|
||||
return this._sortBy.get()!;
|
||||
}
|
||||
set sortBy(sortBy: CommentsSortOrder) {
|
||||
if (this._sortBy.get() !== sortBy) {
|
||||
this._sortBy.set(sortBy);
|
||||
this._onDidChange.fire({ sortBy });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registerAction2(class extends ViewAction<ICommentsView> {
|
||||
@@ -168,3 +189,40 @@ registerAction2(class extends ViewAction<ICommentsView> {
|
||||
view.filters.showResolved = !view.filters.showResolved;
|
||||
}
|
||||
});
|
||||
|
||||
const commentSortSubmenu = new MenuId('submenu.filter.commentSort');
|
||||
MenuRegistry.appendMenuItem(viewFilterSubmenu, {
|
||||
submenu: commentSortSubmenu,
|
||||
title: localize('comment sorts', "Sort By"),
|
||||
group: '2_sort',
|
||||
icon: Codicon.history,
|
||||
when: ContextKeyExpr.equals('view', COMMENTS_VIEW_ID),
|
||||
});
|
||||
|
||||
registerAction2(class extends ViewAction<ICommentsView> {
|
||||
constructor() {
|
||||
super({
|
||||
id: `workbench.actions.${COMMENTS_VIEW_ID}.toggleSortByUpdatedAt`,
|
||||
title: localize('toggle sorting by updated at', "Updated Time"),
|
||||
category: localize('comments', "Comments"),
|
||||
icon: Codicon.history,
|
||||
viewId: COMMENTS_VIEW_ID,
|
||||
toggled: {
|
||||
condition: ContextKeyExpr.equals('commentsView.sortBy', CommentsSortOrder.UpdatedAtDescending),
|
||||
title: localize('sorting by updated at', "Updated Time"),
|
||||
},
|
||||
menu: {
|
||||
id: commentSortSubmenu,
|
||||
group: 'navigation',
|
||||
order: 0,
|
||||
isHiddenByDefault: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async runInView(serviceAccessor: ServicesAccessor, view: ICommentsView): Promise<void> {
|
||||
view.filters.sortBy = view.filters.sortBy === CommentsSortOrder.ResourceAscending
|
||||
? CommentsSortOrder.UpdatedAtDescending
|
||||
: CommentsSortOrder.ResourceAscending;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -42,6 +42,23 @@ export class CommentNode {
|
||||
hasReply(): boolean {
|
||||
return this.replies && this.replies.length !== 0;
|
||||
}
|
||||
|
||||
private _lastUpdatedAt: string | undefined;
|
||||
|
||||
get lastUpdatedAt(): string {
|
||||
if (this._lastUpdatedAt === undefined) {
|
||||
let updatedAt = this.comment.timestamp || '';
|
||||
if (this.replies.length) {
|
||||
const reply = this.replies[this.replies.length - 1];
|
||||
const replyUpdatedAt = reply.lastUpdatedAt;
|
||||
if (replyUpdatedAt > updatedAt) {
|
||||
updatedAt = replyUpdatedAt;
|
||||
}
|
||||
}
|
||||
this._lastUpdatedAt = updatedAt;
|
||||
}
|
||||
return this._lastUpdatedAt;
|
||||
}
|
||||
}
|
||||
|
||||
export class ResourceWithCommentThreads {
|
||||
@@ -71,5 +88,25 @@ export class ResourceWithCommentThreads {
|
||||
|
||||
return commentNodes[0];
|
||||
}
|
||||
|
||||
private _lastUpdatedAt: string | undefined;
|
||||
|
||||
get lastUpdatedAt() {
|
||||
if (this._lastUpdatedAt === undefined) {
|
||||
let updatedAt = '';
|
||||
// Return result without cahcing as we expect data to arrive later
|
||||
if (!this.commentThreads.length) {
|
||||
return updatedAt;
|
||||
}
|
||||
for (const thread of this.commentThreads) {
|
||||
const threadUpdatedAt = thread.lastUpdatedAt;
|
||||
if (threadUpdatedAt && threadUpdatedAt > updatedAt) {
|
||||
updatedAt = threadUpdatedAt;
|
||||
}
|
||||
}
|
||||
this._lastUpdatedAt = updatedAt;
|
||||
}
|
||||
return this._lastUpdatedAt;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user