mirror of
https://github.com/microsoft/vscode.git
synced 2026-07-13 21:47:12 -05:00
Add additional actions to CommentThread (#162750)
* # Add additional actions to CommentThread * Add in forgotten property * # missing declaration * type change * Fix merge conflict errors * # add proposed changes + fix styling * # Allow "secondary action only" buttons * # Add dropdown button styling fixes * # add default button styling * # add better styling to the dropdown button * Remove duplicate css rule Co-authored-by: Alex Ross <alros@microsoft.com>
This commit is contained in:
@@ -69,6 +69,11 @@
|
||||
|
||||
.monaco-button-dropdown > .monaco-button.monaco-dropdown-button {
|
||||
border-left-width: 0 !important;
|
||||
border-radius: 0 2px 2px 0;
|
||||
}
|
||||
|
||||
.monaco-button-dropdown > .monaco-button.monaco-text-button {
|
||||
border-radius: 2px 0 0 2px;
|
||||
}
|
||||
|
||||
.monaco-description-button {
|
||||
|
||||
@@ -257,9 +257,10 @@ export class ButtonWithDropdown extends Disposable implements IButton {
|
||||
this.separatorContainer.style.borderTop = '1px solid ' + border;
|
||||
this.separatorContainer.style.borderBottom = '1px solid ' + border;
|
||||
}
|
||||
this.separatorContainer.style.backgroundColor = options.buttonBackground ?? '';
|
||||
this.separator.style.backgroundColor = options.buttonSeparator ?? '';
|
||||
|
||||
const buttonBackground = options.secondary ? options.buttonSecondaryBackground : options.buttonBackground;
|
||||
this.separatorContainer.style.backgroundColor = buttonBackground ?? '';
|
||||
this.separator.style.backgroundColor = options.buttonSeparator ?? '';
|
||||
|
||||
this.dropdownButton = this._register(new Button(this.element, { ...options, title: false, supportIcons: true }));
|
||||
this.dropdownButton.element.title = localize("button dropdown more actions", 'More Actions...');
|
||||
|
||||
@@ -12,6 +12,7 @@ export const enum MarshalledId {
|
||||
ScmProvider,
|
||||
CommentController,
|
||||
CommentThread,
|
||||
CommentThreadInstance,
|
||||
CommentThreadReply,
|
||||
CommentNode,
|
||||
CommentThreadNode,
|
||||
|
||||
@@ -128,6 +128,7 @@ export class MenuId {
|
||||
static readonly ViewTitleContext = new MenuId('ViewTitleContext');
|
||||
static readonly CommentThreadTitle = new MenuId('CommentThreadTitle');
|
||||
static readonly CommentThreadActions = new MenuId('CommentThreadActions');
|
||||
static readonly CommentThreadAdditionalActions = new MenuId('CommentThreadAdditionalActions');
|
||||
static readonly CommentThreadTitleContext = new MenuId('CommentThreadTitleContext');
|
||||
static readonly CommentThreadCommentContext = new MenuId('CommentThreadCommentContext');
|
||||
static readonly CommentTitle = new MenuId('CommentTitle');
|
||||
|
||||
@@ -66,7 +66,7 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo
|
||||
}
|
||||
|
||||
return commentThread.value;
|
||||
} else if (arg && arg.$mid === MarshalledId.CommentThreadReply) {
|
||||
} else if (arg && (arg.$mid === MarshalledId.CommentThreadReply || arg.$mid === MarshalledId.CommentThreadInstance)) {
|
||||
const commentController = this._commentControllers.get(arg.thread.commentControlHandle);
|
||||
|
||||
if (!commentController) {
|
||||
@@ -79,6 +79,10 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo
|
||||
return arg;
|
||||
}
|
||||
|
||||
if (arg.$mid === MarshalledId.CommentThreadInstance) {
|
||||
return commentThread.value;
|
||||
}
|
||||
|
||||
return {
|
||||
thread: commentThread.value,
|
||||
text: arg.text
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Button } from 'vs/base/browser/ui/button/button';
|
||||
import { Button, ButtonWithDropdown, IButton } from 'vs/base/browser/ui/button/button';
|
||||
import { IAction } from 'vs/base/common/actions';
|
||||
import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IMenu } from 'vs/platform/actions/common/actions';
|
||||
import { IMenu, SubmenuItemAction } from 'vs/platform/actions/common/actions';
|
||||
import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles';
|
||||
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||
|
||||
export class CommentFormActions implements IDisposable {
|
||||
private _buttonElements: HTMLElement[] = [];
|
||||
@@ -17,29 +18,49 @@ export class CommentFormActions implements IDisposable {
|
||||
constructor(
|
||||
private container: HTMLElement,
|
||||
private actionHandler: (action: IAction) => void,
|
||||
private contextMenuService?: IContextMenuService
|
||||
) { }
|
||||
|
||||
setActions(menu: IMenu) {
|
||||
setActions(menu: IMenu, hasOnlySecondaryActions: boolean = false) {
|
||||
this._toDispose.clear();
|
||||
|
||||
this._buttonElements.forEach(b => b.remove());
|
||||
|
||||
const groups = menu.getActions({ shouldForwardArgs: true });
|
||||
let isPrimary: boolean = true;
|
||||
let isPrimary: boolean = !hasOnlySecondaryActions;
|
||||
for (const group of groups) {
|
||||
const [, actions] = group;
|
||||
|
||||
this._actions = actions;
|
||||
for (const action of actions) {
|
||||
const button = new Button(this.container, { secondary: !isPrimary, ...defaultButtonStyles });
|
||||
const submenuAction = action as SubmenuItemAction;
|
||||
|
||||
// Use the first action from the submenu as the primary button.
|
||||
const appliedAction: IAction = submenuAction.actions?.length > 0 ? submenuAction.actions[0] : action;
|
||||
let button: IButton | undefined;
|
||||
|
||||
// Use dropdown only if submenu contains more than 1 action.
|
||||
if (submenuAction.actions?.length > 1 && this.contextMenuService) {
|
||||
button = new ButtonWithDropdown(this.container,
|
||||
{
|
||||
contextMenuProvider: this.contextMenuService,
|
||||
actions: submenuAction.actions.slice(1),
|
||||
addPrimaryActionToDropdown: false,
|
||||
secondary: !isPrimary,
|
||||
...defaultButtonStyles
|
||||
});
|
||||
} else {
|
||||
button = new Button(this.container, { secondary: !isPrimary, ...defaultButtonStyles });
|
||||
}
|
||||
|
||||
isPrimary = false;
|
||||
this._buttonElements.push(button.element);
|
||||
|
||||
this._toDispose.add(button);
|
||||
this._toDispose.add(button.onDidClick(() => this.actionHandler(action)));
|
||||
this._toDispose.add(button.onDidClick(() => this.actionHandler(appliedAction)));
|
||||
|
||||
button.enabled = action.enabled;
|
||||
button.label = action.label;
|
||||
button.enabled = appliedAction.enabled;
|
||||
button.label = appliedAction.label;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,10 @@ export class CommentMenus implements IDisposable {
|
||||
return this.getMenu(MenuId.CommentThreadActions, contextKeyService);
|
||||
}
|
||||
|
||||
getCommentThreadAdditionalActions(contextKeyService: IContextKeyService): IMenu {
|
||||
return this.getMenu(MenuId.CommentThreadAdditionalActions, contextKeyService);
|
||||
}
|
||||
|
||||
getCommentTitleActions(comment: Comment, contextKeyService: IContextKeyService): IMenu {
|
||||
return this.getMenu(MenuId.CommentTitle, contextKeyService);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
|
||||
import { IAction } from 'vs/base/common/actions';
|
||||
import { IMenu, SubmenuItemAction } from 'vs/platform/actions/common/actions';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { MarshalledId } from 'vs/base/common/marshallingIds';
|
||||
import { IRange } from 'vs/editor/common/core/range';
|
||||
import * as languages from 'vs/editor/common/languages';
|
||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { CommentFormActions } from 'vs/workbench/contrib/comments/browser/commentFormActions';
|
||||
import { CommentMenus } from 'vs/workbench/contrib/comments/browser/commentMenus';
|
||||
import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange';
|
||||
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||
|
||||
export class CommentThreadAdditionalActions<T extends IRange | ICellRange> extends Disposable {
|
||||
private _container: HTMLElement | null;
|
||||
private _buttonBar: HTMLElement | null;
|
||||
private _commentFormActions!: CommentFormActions;
|
||||
|
||||
constructor(
|
||||
container: HTMLElement,
|
||||
private _commentThread: languages.CommentThread<T>,
|
||||
private _contextKeyService: IContextKeyService,
|
||||
private _commentMenus: CommentMenus,
|
||||
private _actionRunDelegate: (() => void) | null,
|
||||
@IContextMenuService private contextMenuService: IContextMenuService,
|
||||
) {
|
||||
super();
|
||||
|
||||
this._container = dom.append(container, dom.$('.comment-additional-actions'));
|
||||
dom.append(this._container, dom.$('.section-separator'));
|
||||
|
||||
this._buttonBar = dom.append(this._container, dom.$('.button-bar'));
|
||||
this._createAdditionalActions(this._buttonBar);
|
||||
}
|
||||
|
||||
private _showMenu() {
|
||||
this._container?.classList.remove('hidden');
|
||||
}
|
||||
|
||||
private _hideMenu() {
|
||||
this._container?.classList.add('hidden');
|
||||
}
|
||||
|
||||
private _enableDisableMenu(menu: IMenu) {
|
||||
const groups = menu.getActions({ shouldForwardArgs: true });
|
||||
|
||||
// Show the menu if at least one action is enabled.
|
||||
for (const group of groups) {
|
||||
const [, actions] = group;
|
||||
for (const action of actions) {
|
||||
if (action.enabled) {
|
||||
this._showMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
for (const subAction of (action as SubmenuItemAction).actions ?? []) {
|
||||
if (subAction.enabled) {
|
||||
this._showMenu();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._hideMenu();
|
||||
}
|
||||
|
||||
|
||||
private _createAdditionalActions(container: HTMLElement) {
|
||||
const menu = this._commentMenus.getCommentThreadAdditionalActions(this._contextKeyService);
|
||||
this._register(menu);
|
||||
this._register(menu.onDidChange(() => {
|
||||
this._commentFormActions.setActions(menu);
|
||||
this._enableDisableMenu(menu);
|
||||
}));
|
||||
|
||||
this._commentFormActions = new CommentFormActions(container, async (action: IAction) => {
|
||||
this._actionRunDelegate?.();
|
||||
|
||||
action.run({
|
||||
thread: this._commentThread,
|
||||
$mid: MarshalledId.CommentThreadInstance
|
||||
});
|
||||
|
||||
}, this.contextMenuService);
|
||||
|
||||
this._register(this._commentFormActions);
|
||||
this._commentFormActions.setActions(menu, /*hasOnlySecondaryActions*/ true);
|
||||
this._enableDisableMenu(menu);
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import { CommentReply } from 'vs/workbench/contrib/comments/browser/commentReply
|
||||
import { ICommentService } from 'vs/workbench/contrib/comments/browser/commentService';
|
||||
import { CommentThreadBody } from 'vs/workbench/contrib/comments/browser/commentThreadBody';
|
||||
import { CommentThreadHeader } from 'vs/workbench/contrib/comments/browser/commentThreadHeader';
|
||||
import { CommentThreadAdditionalActions } from 'vs/workbench/contrib/comments/browser/commentThreadAdditionalActions';
|
||||
import { CommentContextKeys } from 'vs/workbench/contrib/comments/common/commentContextKeys';
|
||||
import { ICommentThreadWidget } from 'vs/workbench/contrib/comments/common/commentThreadWidget';
|
||||
import { IColorTheme } from 'vs/platform/theme/common/themeService';
|
||||
@@ -35,6 +36,7 @@ export class CommentThreadWidget<T extends IRange | ICellRange = IRange> extends
|
||||
private _header!: CommentThreadHeader<T>;
|
||||
private _body!: CommentThreadBody<T>;
|
||||
private _commentReply?: CommentReply<T>;
|
||||
private _additionalActions?: CommentThreadAdditionalActions<T>;
|
||||
private _commentMenus: CommentMenus;
|
||||
private _commentThreadDisposables: IDisposable[] = [];
|
||||
private _threadIsEmpty: IContextKey<boolean>;
|
||||
@@ -177,6 +179,7 @@ export class CommentThreadWidget<T extends IRange | ICellRange = IRange> extends
|
||||
if (this._commentThread.canReply) {
|
||||
this._createCommentForm();
|
||||
}
|
||||
this._createAdditionalActions();
|
||||
|
||||
this._register(this._body.onDidResize(dimension => {
|
||||
this._refresh(dimension);
|
||||
@@ -239,6 +242,19 @@ export class CommentThreadWidget<T extends IRange | ICellRange = IRange> extends
|
||||
this._register(this._commentReply);
|
||||
}
|
||||
|
||||
private _createAdditionalActions() {
|
||||
this._additionalActions = this._scopedInstatiationService.createInstance(
|
||||
CommentThreadAdditionalActions,
|
||||
this._body.container,
|
||||
this._commentThread,
|
||||
this._contextKeyService,
|
||||
this._commentMenus,
|
||||
this._containerDelegate.actionRunner,
|
||||
);
|
||||
|
||||
this._register(this._additionalActions);
|
||||
}
|
||||
|
||||
getCommentCoords(commentUniqueId: number) {
|
||||
return this._body.getCommentCoords(commentUniqueId);
|
||||
}
|
||||
|
||||
@@ -246,6 +246,42 @@
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
|
||||
.review-widget .body .comment-additional-actions {
|
||||
margin: 10px 20px;
|
||||
}
|
||||
|
||||
.review-widget .body .comment-additional-actions .section-separator {
|
||||
border-top: 1px solid var(--vscode-menu-separatorBackground);
|
||||
margin: 14px 0;
|
||||
}
|
||||
|
||||
.review-widget .body .comment-additional-actions .button-bar {
|
||||
display: flex;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.review-widget .body .comment-additional-actions .monaco-button,
|
||||
.review-widget .body .comment-additional-actions .monaco-text-button,
|
||||
.review-widget .body .comment-additional-actions .monaco-button-dropdown {
|
||||
display: flex;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.review-widget .body .comment-additional-actions .button-bar>.monaco-text-button,
|
||||
.review-widget .body .comment-additional-actions .button-bar>.monaco-button-dropdown {
|
||||
margin: 0 10px 0 0;
|
||||
}
|
||||
|
||||
.review-widget .body .comment-additional-actions .button-bar .monaco-text-button {
|
||||
padding: 4px 10px;
|
||||
}
|
||||
|
||||
|
||||
.review-widget .body .comment-additional-actions .codicon-drop-down-button {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.review-widget .body .comment-form.expand .review-thread-reply-button {
|
||||
display: none;
|
||||
}
|
||||
@@ -295,7 +331,7 @@
|
||||
.review-widget .body .comment-form .form-actions,
|
||||
.review-widget .body .edit-container .form-actions {
|
||||
overflow: auto;
|
||||
padding: 10px 0;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.review-widget .body .edit-textarea {
|
||||
|
||||
@@ -287,13 +287,8 @@
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
/* split commit button */
|
||||
.scm-view .button-container > .monaco-button-dropdown > .monaco-dropdown-button.codicon-drop-down-button {
|
||||
border-radius: 0 2px 2px 0;
|
||||
}
|
||||
|
||||
.scm-view .button-container > .monaco-button-dropdown > .monaco-button.monaco-text-button {
|
||||
border-radius: 2px 0 0 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -159,6 +159,13 @@ const apiMenus: IAPIMenu[] = [
|
||||
description: localize('commentThread.actions', "The contributed comment thread context menu, rendered as buttons below the comment editor"),
|
||||
supportsSubmenus: false
|
||||
},
|
||||
{
|
||||
key: 'comments/commentThread/additionalActions',
|
||||
id: MenuId.CommentThreadAdditionalActions,
|
||||
description: localize('commentThread.actions', "The contributed comment thread context menu, rendered as buttons below the comment editor"),
|
||||
supportsSubmenus: true,
|
||||
proposed: 'contribCommentThreadAdditionalMenu'
|
||||
},
|
||||
{
|
||||
key: 'comments/commentThread/title/context',
|
||||
id: MenuId.CommentThreadTitleContext,
|
||||
|
||||
@@ -10,6 +10,7 @@ export const allApiProposals = Object.freeze({
|
||||
codiconDecoration: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.codiconDecoration.d.ts',
|
||||
commentsResolvedState: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.commentsResolvedState.d.ts',
|
||||
contribCommentPeekContext: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribCommentPeekContext.d.ts',
|
||||
contribCommentThreadAdditionalMenu: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribCommentThreadAdditionalMenu.d.ts',
|
||||
contribEditSessions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribEditSessions.d.ts',
|
||||
contribEditorContentMenu: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribEditorContentMenu.d.ts',
|
||||
contribLabelFormatterWorkspaceTooltip: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribLabelFormatterWorkspaceTooltip.d.ts',
|
||||
|
||||
8
src/vscode-dts/vscode.proposed.contribCommentThreadAdditionalMenu.d.ts
vendored
Normal file
8
src/vscode-dts/vscode.proposed.contribCommentThreadAdditionalMenu.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// empty placeholder for comment thread additional menus
|
||||
|
||||
// https://github.com/microsoft/vscode/issues/163281
|
||||
Reference in New Issue
Block a user