Skip to content

Commit aaac6f9

Browse files
marrejalexr00
andauthored
Add additional actions to CommentThread (microsoft#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 <[email protected]>
1 parent f675b2b commit aaac6f9

File tree

14 files changed

+214
-17
lines changed

14 files changed

+214
-17
lines changed

src/vs/base/browser/ui/button/button.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@
6969

7070
.monaco-button-dropdown > .monaco-button.monaco-dropdown-button {
7171
border-left-width: 0 !important;
72+
border-radius: 0 2px 2px 0;
73+
}
74+
75+
.monaco-button-dropdown > .monaco-button.monaco-text-button {
76+
border-radius: 2px 0 0 2px;
7277
}
7378

7479
.monaco-description-button {

src/vs/base/browser/ui/button/button.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -257,9 +257,10 @@ export class ButtonWithDropdown extends Disposable implements IButton {
257257
this.separatorContainer.style.borderTop = '1px solid ' + border;
258258
this.separatorContainer.style.borderBottom = '1px solid ' + border;
259259
}
260-
this.separatorContainer.style.backgroundColor = options.buttonBackground ?? '';
261-
this.separator.style.backgroundColor = options.buttonSeparator ?? '';
262260

261+
const buttonBackground = options.secondary ? options.buttonSecondaryBackground : options.buttonBackground;
262+
this.separatorContainer.style.backgroundColor = buttonBackground ?? '';
263+
this.separator.style.backgroundColor = options.buttonSeparator ?? '';
263264

264265
this.dropdownButton = this._register(new Button(this.element, { ...options, title: false, supportIcons: true }));
265266
this.dropdownButton.element.title = localize("button dropdown more actions", 'More Actions...');

src/vs/base/common/marshallingIds.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export const enum MarshalledId {
1212
ScmProvider,
1313
CommentController,
1414
CommentThread,
15+
CommentThreadInstance,
1516
CommentThreadReply,
1617
CommentNode,
1718
CommentThreadNode,

src/vs/platform/actions/common/actions.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ export class MenuId {
128128
static readonly ViewTitleContext = new MenuId('ViewTitleContext');
129129
static readonly CommentThreadTitle = new MenuId('CommentThreadTitle');
130130
static readonly CommentThreadActions = new MenuId('CommentThreadActions');
131+
static readonly CommentThreadAdditionalActions = new MenuId('CommentThreadAdditionalActions');
131132
static readonly CommentThreadTitleContext = new MenuId('CommentThreadTitleContext');
132133
static readonly CommentThreadCommentContext = new MenuId('CommentThreadCommentContext');
133134
static readonly CommentTitle = new MenuId('CommentTitle');

src/vs/workbench/api/common/extHostComments.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo
6666
}
6767

6868
return commentThread.value;
69-
} else if (arg && arg.$mid === MarshalledId.CommentThreadReply) {
69+
} else if (arg && (arg.$mid === MarshalledId.CommentThreadReply || arg.$mid === MarshalledId.CommentThreadInstance)) {
7070
const commentController = this._commentControllers.get(arg.thread.commentControlHandle);
7171

7272
if (!commentController) {
@@ -79,6 +79,10 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo
7979
return arg;
8080
}
8181

82+
if (arg.$mid === MarshalledId.CommentThreadInstance) {
83+
return commentThread.value;
84+
}
85+
8286
return {
8387
thread: commentThread.value,
8488
text: arg.text

src/vs/workbench/contrib/comments/browser/commentFormActions.ts

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { Button } from 'vs/base/browser/ui/button/button';
6+
import { Button, ButtonWithDropdown, IButton } from 'vs/base/browser/ui/button/button';
77
import { IAction } from 'vs/base/common/actions';
88
import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
9-
import { IMenu } from 'vs/platform/actions/common/actions';
9+
import { IMenu, SubmenuItemAction } from 'vs/platform/actions/common/actions';
1010
import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles';
11+
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
1112

1213
export class CommentFormActions implements IDisposable {
1314
private _buttonElements: HTMLElement[] = [];
@@ -17,29 +18,49 @@ export class CommentFormActions implements IDisposable {
1718
constructor(
1819
private container: HTMLElement,
1920
private actionHandler: (action: IAction) => void,
21+
private contextMenuService?: IContextMenuService
2022
) { }
2123

22-
setActions(menu: IMenu) {
24+
setActions(menu: IMenu, hasOnlySecondaryActions: boolean = false) {
2325
this._toDispose.clear();
2426

2527
this._buttonElements.forEach(b => b.remove());
2628

2729
const groups = menu.getActions({ shouldForwardArgs: true });
28-
let isPrimary: boolean = true;
30+
let isPrimary: boolean = !hasOnlySecondaryActions;
2931
for (const group of groups) {
3032
const [, actions] = group;
3133

3234
this._actions = actions;
3335
for (const action of actions) {
34-
const button = new Button(this.container, { secondary: !isPrimary, ...defaultButtonStyles });
36+
const submenuAction = action as SubmenuItemAction;
37+
38+
// Use the first action from the submenu as the primary button.
39+
const appliedAction: IAction = submenuAction.actions?.length > 0 ? submenuAction.actions[0] : action;
40+
let button: IButton | undefined;
41+
42+
// Use dropdown only if submenu contains more than 1 action.
43+
if (submenuAction.actions?.length > 1 && this.contextMenuService) {
44+
button = new ButtonWithDropdown(this.container,
45+
{
46+
contextMenuProvider: this.contextMenuService,
47+
actions: submenuAction.actions.slice(1),
48+
addPrimaryActionToDropdown: false,
49+
secondary: !isPrimary,
50+
...defaultButtonStyles
51+
});
52+
} else {
53+
button = new Button(this.container, { secondary: !isPrimary, ...defaultButtonStyles });
54+
}
55+
3556
isPrimary = false;
3657
this._buttonElements.push(button.element);
3758

3859
this._toDispose.add(button);
39-
this._toDispose.add(button.onDidClick(() => this.actionHandler(action)));
60+
this._toDispose.add(button.onDidClick(() => this.actionHandler(appliedAction)));
4061

41-
button.enabled = action.enabled;
42-
button.label = action.label;
62+
button.enabled = appliedAction.enabled;
63+
button.label = appliedAction.label;
4364
}
4465
}
4566
}

src/vs/workbench/contrib/comments/browser/commentMenus.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ export class CommentMenus implements IDisposable {
2323
return this.getMenu(MenuId.CommentThreadActions, contextKeyService);
2424
}
2525

26+
getCommentThreadAdditionalActions(contextKeyService: IContextKeyService): IMenu {
27+
return this.getMenu(MenuId.CommentThreadAdditionalActions, contextKeyService);
28+
}
29+
2630
getCommentTitleActions(comment: Comment, contextKeyService: IContextKeyService): IMenu {
2731
return this.getMenu(MenuId.CommentTitle, contextKeyService);
2832
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import * as dom from 'vs/base/browser/dom';
7+
8+
import { IAction } from 'vs/base/common/actions';
9+
import { IMenu, SubmenuItemAction } from 'vs/platform/actions/common/actions';
10+
import { Disposable } from 'vs/base/common/lifecycle';
11+
import { MarshalledId } from 'vs/base/common/marshallingIds';
12+
import { IRange } from 'vs/editor/common/core/range';
13+
import * as languages from 'vs/editor/common/languages';
14+
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
15+
import { CommentFormActions } from 'vs/workbench/contrib/comments/browser/commentFormActions';
16+
import { CommentMenus } from 'vs/workbench/contrib/comments/browser/commentMenus';
17+
import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange';
18+
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
19+
20+
export class CommentThreadAdditionalActions<T extends IRange | ICellRange> extends Disposable {
21+
private _container: HTMLElement | null;
22+
private _buttonBar: HTMLElement | null;
23+
private _commentFormActions!: CommentFormActions;
24+
25+
constructor(
26+
container: HTMLElement,
27+
private _commentThread: languages.CommentThread<T>,
28+
private _contextKeyService: IContextKeyService,
29+
private _commentMenus: CommentMenus,
30+
private _actionRunDelegate: (() => void) | null,
31+
@IContextMenuService private contextMenuService: IContextMenuService,
32+
) {
33+
super();
34+
35+
this._container = dom.append(container, dom.$('.comment-additional-actions'));
36+
dom.append(this._container, dom.$('.section-separator'));
37+
38+
this._buttonBar = dom.append(this._container, dom.$('.button-bar'));
39+
this._createAdditionalActions(this._buttonBar);
40+
}
41+
42+
private _showMenu() {
43+
this._container?.classList.remove('hidden');
44+
}
45+
46+
private _hideMenu() {
47+
this._container?.classList.add('hidden');
48+
}
49+
50+
private _enableDisableMenu(menu: IMenu) {
51+
const groups = menu.getActions({ shouldForwardArgs: true });
52+
53+
// Show the menu if at least one action is enabled.
54+
for (const group of groups) {
55+
const [, actions] = group;
56+
for (const action of actions) {
57+
if (action.enabled) {
58+
this._showMenu();
59+
return;
60+
}
61+
62+
for (const subAction of (action as SubmenuItemAction).actions ?? []) {
63+
if (subAction.enabled) {
64+
this._showMenu();
65+
return;
66+
}
67+
}
68+
}
69+
}
70+
71+
this._hideMenu();
72+
}
73+
74+
75+
private _createAdditionalActions(container: HTMLElement) {
76+
const menu = this._commentMenus.getCommentThreadAdditionalActions(this._contextKeyService);
77+
this._register(menu);
78+
this._register(menu.onDidChange(() => {
79+
this._commentFormActions.setActions(menu);
80+
this._enableDisableMenu(menu);
81+
}));
82+
83+
this._commentFormActions = new CommentFormActions(container, async (action: IAction) => {
84+
this._actionRunDelegate?.();
85+
86+
action.run({
87+
thread: this._commentThread,
88+
$mid: MarshalledId.CommentThreadInstance
89+
});
90+
91+
}, this.contextMenuService);
92+
93+
this._register(this._commentFormActions);
94+
this._commentFormActions.setActions(menu, /*hasOnlySecondaryActions*/ true);
95+
this._enableDisableMenu(menu);
96+
}
97+
}

src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { CommentReply } from 'vs/workbench/contrib/comments/browser/commentReply
1717
import { ICommentService } from 'vs/workbench/contrib/comments/browser/commentService';
1818
import { CommentThreadBody } from 'vs/workbench/contrib/comments/browser/commentThreadBody';
1919
import { CommentThreadHeader } from 'vs/workbench/contrib/comments/browser/commentThreadHeader';
20+
import { CommentThreadAdditionalActions } from 'vs/workbench/contrib/comments/browser/commentThreadAdditionalActions';
2021
import { CommentContextKeys } from 'vs/workbench/contrib/comments/common/commentContextKeys';
2122
import { ICommentThreadWidget } from 'vs/workbench/contrib/comments/common/commentThreadWidget';
2223
import { IColorTheme } from 'vs/platform/theme/common/themeService';
@@ -35,6 +36,7 @@ export class CommentThreadWidget<T extends IRange | ICellRange = IRange> extends
3536
private _header!: CommentThreadHeader<T>;
3637
private _body!: CommentThreadBody<T>;
3738
private _commentReply?: CommentReply<T>;
39+
private _additionalActions?: CommentThreadAdditionalActions<T>;
3840
private _commentMenus: CommentMenus;
3941
private _commentThreadDisposables: IDisposable[] = [];
4042
private _threadIsEmpty: IContextKey<boolean>;
@@ -177,6 +179,7 @@ export class CommentThreadWidget<T extends IRange | ICellRange = IRange> extends
177179
if (this._commentThread.canReply) {
178180
this._createCommentForm();
179181
}
182+
this._createAdditionalActions();
180183

181184
this._register(this._body.onDidResize(dimension => {
182185
this._refresh(dimension);
@@ -239,6 +242,19 @@ export class CommentThreadWidget<T extends IRange | ICellRange = IRange> extends
239242
this._register(this._commentReply);
240243
}
241244

245+
private _createAdditionalActions() {
246+
this._additionalActions = this._scopedInstatiationService.createInstance(
247+
CommentThreadAdditionalActions,
248+
this._body.container,
249+
this._commentThread,
250+
this._contextKeyService,
251+
this._commentMenus,
252+
this._containerDelegate.actionRunner,
253+
);
254+
255+
this._register(this._additionalActions);
256+
}
257+
242258
getCommentCoords(commentUniqueId: number) {
243259
return this._body.getCommentCoords(commentUniqueId);
244260
}

src/vs/workbench/contrib/comments/browser/media/review.css

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,42 @@
246246
word-wrap: break-word;
247247
}
248248

249+
250+
.review-widget .body .comment-additional-actions {
251+
margin: 10px 20px;
252+
}
253+
254+
.review-widget .body .comment-additional-actions .section-separator {
255+
border-top: 1px solid var(--vscode-menu-separatorBackground);
256+
margin: 14px 0;
257+
}
258+
259+
.review-widget .body .comment-additional-actions .button-bar {
260+
display: flex;
261+
white-space: nowrap;
262+
}
263+
264+
.review-widget .body .comment-additional-actions .monaco-button,
265+
.review-widget .body .comment-additional-actions .monaco-text-button,
266+
.review-widget .body .comment-additional-actions .monaco-button-dropdown {
267+
display: flex;
268+
width: auto;
269+
}
270+
271+
.review-widget .body .comment-additional-actions .button-bar>.monaco-text-button,
272+
.review-widget .body .comment-additional-actions .button-bar>.monaco-button-dropdown {
273+
margin: 0 10px 0 0;
274+
}
275+
276+
.review-widget .body .comment-additional-actions .button-bar .monaco-text-button {
277+
padding: 4px 10px;
278+
}
279+
280+
281+
.review-widget .body .comment-additional-actions .codicon-drop-down-button {
282+
align-items: center;
283+
}
284+
249285
.review-widget .body .comment-form.expand .review-thread-reply-button {
250286
display: none;
251287
}
@@ -295,7 +331,7 @@
295331
.review-widget .body .comment-form .form-actions,
296332
.review-widget .body .edit-container .form-actions {
297333
overflow: auto;
298-
padding: 10px 0;
334+
margin: 10px 0;
299335
}
300336

301337
.review-widget .body .edit-textarea {

0 commit comments

Comments
 (0)