Skip to content

Commit cb7f9ae

Browse files
committed
mhutchie#417 New context menu actions for branches and remote branches, to select / unselect the branch in the Branches Dropdown.
1 parent 1d9c808 commit cb7f9ae

File tree

6 files changed

+153
-26
lines changed

6 files changed

+153
-26
lines changed

package.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,14 @@
170170
"type": "boolean",
171171
"title": "Create Archive"
172172
},
173+
"selectInBranchesDropdown": {
174+
"type": "boolean",
175+
"title": "Select in Branches Dropdown"
176+
},
177+
"unselectInBranchesDropdown": {
178+
"type": "boolean",
179+
"title": "Unselect in Branches Dropdown"
180+
},
173181
"copyName": {
174182
"type": "boolean",
175183
"title": "Copy Branch Name to Clipboard"
@@ -256,6 +264,14 @@
256264
"type": "boolean",
257265
"title": "Create Archive"
258266
},
267+
"selectInBranchesDropdown": {
268+
"type": "boolean",
269+
"title": "Select in Branches Dropdown"
270+
},
271+
"unselectInBranchesDropdown": {
272+
"type": "boolean",
273+
"title": "Unselect in Branches Dropdown"
274+
},
259275
"copyName": {
260276
"type": "boolean",
261277
"title": "Copy Branch Name to Clipboard"

src/config.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,11 @@ class Config {
7979
* Get the value of the `git-graph.contextMenuActionsVisibility` Extension Setting.
8080
*/
8181
get contextMenuActionsVisibility(): ContextMenuActionsVisibility {
82-
let userConfig = this.config.get('contextMenuActionsVisibility', {});
83-
let config = {
84-
branch: { checkout: true, rename: true, delete: true, merge: true, rebase: true, push: true, createPullRequest: true, createArchive: true, copyName: true },
82+
const userConfig = this.config.get('contextMenuActionsVisibility', {});
83+
const config: ContextMenuActionsVisibility = {
84+
branch: { checkout: true, rename: true, delete: true, merge: true, rebase: true, push: true, createPullRequest: true, createArchive: true, selectInBranchesDropdown: true, unselectInBranchesDropdown: true, copyName: true },
8585
commit: { addTag: true, createBranch: true, checkout: true, cherrypick: true, revert: true, drop: true, merge: true, rebase: true, reset: true, copyHash: true, copySubject: true },
86-
remoteBranch: { checkout: true, delete: true, fetch: true, merge: true, pull: true, createPullRequest: true, createArchive: true, copyName: true },
86+
remoteBranch: { checkout: true, delete: true, fetch: true, merge: true, pull: true, createPullRequest: true, createArchive: true, selectInBranchesDropdown: true, unselectInBranchesDropdown: true, copyName: true },
8787
stash: { apply: true, createBranch: true, pop: true, drop: true, copyName: true, copyHash: true },
8888
tag: { viewDetails: true, delete: true, push: true, createArchive: true, copyName: true },
8989
uncommittedChanges: { stash: true, reset: true, clean: true, openSourceControlView: true }

src/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,8 @@ export interface ContextMenuActionsVisibility {
340340
readonly push: boolean;
341341
readonly createPullRequest: boolean;
342342
readonly createArchive: boolean;
343+
readonly selectInBranchesDropdown: boolean;
344+
readonly unselectInBranchesDropdown: boolean;
343345
readonly copyName: boolean;
344346
};
345347
readonly commit: {
@@ -363,6 +365,8 @@ export interface ContextMenuActionsVisibility {
363365
readonly pull: boolean;
364366
readonly createPullRequest: boolean;
365367
readonly createArchive: boolean;
368+
readonly selectInBranchesDropdown: boolean;
369+
readonly unselectInBranchesDropdown: boolean;
366370
readonly copyName: boolean;
367371
};
368372
readonly stash: {

tests/config.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,8 @@ describe('Config', () => {
272272
push: true,
273273
createPullRequest: true,
274274
createArchive: true,
275+
selectInBranchesDropdown: true,
276+
unselectInBranchesDropdown: true,
275277
copyName: true
276278
},
277279
commit: {
@@ -295,6 +297,8 @@ describe('Config', () => {
295297
pull: true,
296298
createPullRequest: true,
297299
createArchive: true,
300+
selectInBranchesDropdown: true,
301+
unselectInBranchesDropdown: true,
298302
copyName: true
299303
},
300304
stash: {
@@ -337,6 +341,8 @@ describe('Config', () => {
337341
push: true,
338342
createPullRequest: true,
339343
createArchive: true,
344+
selectInBranchesDropdown: true,
345+
unselectInBranchesDropdown: true,
340346
copyName: true
341347
},
342348
commit: {
@@ -360,6 +366,8 @@ describe('Config', () => {
360366
pull: true,
361367
createPullRequest: true,
362368
createArchive: true,
369+
selectInBranchesDropdown: true,
370+
unselectInBranchesDropdown: true,
363371
copyName: true
364372
},
365373
stash: {
@@ -417,6 +425,8 @@ describe('Config', () => {
417425
push: true,
418426
createPullRequest: true,
419427
createArchive: true,
428+
selectInBranchesDropdown: true,
429+
unselectInBranchesDropdown: true,
420430
copyName: true
421431
},
422432
commit: {
@@ -440,6 +450,8 @@ describe('Config', () => {
440450
pull: true,
441451
createPullRequest: true,
442452
createArchive: true,
453+
selectInBranchesDropdown: true,
454+
unselectInBranchesDropdown: true,
443455
copyName: true
444456
},
445457
stash: {

web/dropdown.ts

Lines changed: 92 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@ interface DropdownOption {
88
* Implements the dropdown inputs used in the Git Graph View's top control bar.
99
*/
1010
class Dropdown {
11+
private readonly showInfo: boolean;
12+
private readonly multipleAllowed: boolean;
13+
private readonly changeCallback: (values: string[]) => void;
14+
1115
private options: ReadonlyArray<DropdownOption> = [];
1216
private optionsSelected: boolean[] = [];
13-
private lastSelected: number = 0;
14-
private numSelected: number = 0;
17+
private lastSelected: number = 0; // Only used when multipleAllowed === false
1518
private dropdownVisible: boolean = false;
16-
private showInfo: boolean;
17-
private multipleAllowed: boolean;
18-
private changeCallback: { (values: string[]): void };
1919
private lastClicked: number = 0;
2020
private doubleClickTimeout: NodeJS.Timer | null = null;
2121

@@ -35,7 +35,7 @@ class Dropdown {
3535
* @param changeCallback A callback to be invoked when the selected item(s) of the dropdown changes.
3636
* @returns The Dropdown instance.
3737
*/
38-
constructor(id: string, showInfo: boolean, multipleAllowed: boolean, dropdownType: string, changeCallback: { (values: string[]): void }) {
38+
constructor(id: string, showInfo: boolean, multipleAllowed: boolean, dropdownType: string, changeCallback: (values: string[]) => void) {
3939
this.showInfo = showInfo;
4040
this.multipleAllowed = multipleAllowed;
4141
this.changeCallback = changeCallback;
@@ -78,9 +78,9 @@ class Dropdown {
7878
if ((<HTMLElement>e.target).closest('.dropdown') !== this.elem) {
7979
this.close();
8080
} else {
81-
let option = <HTMLElement | null>(<HTMLElement>e.target).closest('.dropdownOption');
81+
const option = <HTMLElement | null>(<HTMLElement>e.target).closest('.dropdownOption');
8282
if (option !== null && option.parentNode === this.optionsElem && typeof option.dataset.id !== 'undefined') {
83-
this.selectOption(parseInt(option.dataset.id!));
83+
this.onOptionClick(parseInt(option.dataset.id!));
8484
}
8585
}
8686
}
@@ -97,27 +97,104 @@ class Dropdown {
9797
public setOptions(options: ReadonlyArray<DropdownOption>, optionsSelected: string[]) {
9898
this.options = options;
9999
this.optionsSelected = [];
100-
this.numSelected = 0;
101100
let selectedOption = -1, isSelected;
102101
for (let i = 0; i < options.length; i++) {
103102
isSelected = optionsSelected.includes(options[i].value);
104103
this.optionsSelected[i] = isSelected;
105104
if (isSelected) {
106105
selectedOption = i;
107-
this.numSelected++;
108106
}
109107
}
110108
if (selectedOption === -1) {
111109
selectedOption = 0;
112110
this.optionsSelected[selectedOption] = true;
113-
this.numSelected++;
114111
}
115112
this.lastSelected = selectedOption;
116113
if (this.dropdownVisible && options.length <= 1) this.close();
117114
this.render();
118115
this.clearDoubleClickTimeout();
119116
}
120117

118+
/**
119+
* Is a value selected in the dropdown (respecting "Show All")
120+
* @param value The value to check.
121+
* @returns TRUE => The value is selected, FALSE => The value is not selected.
122+
*/
123+
public isSelected(value: string) {
124+
if (this.options.length > 0) {
125+
if (this.multipleAllowed && this.optionsSelected[0]) {
126+
// Multiple options can be selected, and "Show All" is selected.
127+
return true;
128+
}
129+
const optionIndex = this.options.findIndex((option) => option.value === value);
130+
if (optionIndex > -1 && this.optionsSelected[optionIndex]) {
131+
// The specific option is selected
132+
return true;
133+
}
134+
}
135+
return false;
136+
}
137+
138+
/**
139+
* Select a specific value in the dropdown.
140+
* @param value The value to select.
141+
*/
142+
public selectOption(value: string) {
143+
const optionIndex = this.options.findIndex((option) => value === option.value);
144+
if (this.multipleAllowed && optionIndex > -1 && !this.optionsSelected[optionIndex]) {
145+
// Select the option with the specified value
146+
this.optionsSelected[optionIndex] = true;
147+
148+
if (!this.optionsSelected[0] && this.optionsSelected.slice(1).every((selected) => selected)) {
149+
// All options are selected, so simplify selected items to just be "Show All"
150+
this.optionsSelected[0] = true;
151+
for (let i = 1; i < this.optionsSelected.length; i++) {
152+
this.optionsSelected[i] = false;
153+
}
154+
}
155+
156+
// A change has occurred, re-render the dropdown options
157+
const menuScroll = this.menuElem.scrollTop;
158+
this.render();
159+
if (this.dropdownVisible) {
160+
this.menuElem.scroll(0, menuScroll);
161+
}
162+
this.changeCallback(this.getSelectedOptions(false));
163+
}
164+
}
165+
166+
/**
167+
* Unselect a specific value in the dropdown.
168+
* @param value The value to unselect.
169+
*/
170+
public unselectOption(value: string) {
171+
const optionIndex = this.options.findIndex((option) => value === option.value);
172+
if (this.multipleAllowed && optionIndex > -1 && (this.optionsSelected[0] || this.optionsSelected[optionIndex])) {
173+
if (this.optionsSelected[0]) {
174+
// Show All is currently selected, so unselect it, and select all branch options
175+
this.optionsSelected[0] = false;
176+
for (let i = 1; i < this.optionsSelected.length; i++) {
177+
this.optionsSelected[i] = true;
178+
}
179+
}
180+
181+
// Unselect the option with the specified value
182+
this.optionsSelected[optionIndex] = false;
183+
if (this.optionsSelected.every(selected => !selected)) {
184+
// All items have been unselected, select "Show All"
185+
this.optionsSelected[0] = true;
186+
}
187+
188+
// A change has occurred, re-render the dropdown options
189+
const menuScroll = this.menuElem.scrollTop;
190+
this.render();
191+
if (this.dropdownVisible) {
192+
this.menuElem.scroll(0, menuScroll);
193+
}
194+
this.changeCallback(this.getSelectedOptions(false));
195+
}
196+
}
197+
121198
/**
122199
* Refresh the rendered dropdown to apply style changes.
123200
*/
@@ -209,7 +286,7 @@ class Dropdown {
209286
* Select a dropdown option.
210287
* @param option The index of the option to select.
211288
*/
212-
private selectOption(option: number) {
289+
private onOptionClick(option: number) {
213290
// Note: Show All is always the first option (0 index) when multiple selected items are allowed
214291
let change = false;
215292
let doubleClick = this.doubleClickTimeout !== null && this.lastClicked === option;
@@ -218,10 +295,8 @@ class Dropdown {
218295
if (doubleClick) {
219296
// Double click
220297
if (this.multipleAllowed && option === 0) {
221-
this.numSelected = 1;
222298
for (let i = 1; i < this.optionsSelected.length; i++) {
223299
this.optionsSelected[i] = !this.optionsSelected[i];
224-
if (this.optionsSelected[i]) this.numSelected++;
225300
}
226301
change = true;
227302
}
@@ -236,27 +311,22 @@ class Dropdown {
236311
for (let i = 1; i < this.optionsSelected.length; i++) {
237312
this.optionsSelected[i] = false;
238313
}
239-
this.numSelected = 1;
240314
change = true;
241315
}
242316
} else {
243317
if (this.optionsSelected[0]) {
318+
// Deselect "Show All" if it is enabled
244319
this.optionsSelected[0] = false;
245-
this.numSelected--;
246320
}
247321

248-
this.numSelected += this.optionsSelected[option] ? -1 : 1;
249322
this.optionsSelected[option] = !this.optionsSelected[option];
250323

251-
if (this.numSelected === 0) {
324+
if (this.optionsSelected.every(selected => !selected)) {
325+
// All items have been unselected, select "Show All"
252326
this.optionsSelected[0] = true;
253-
this.numSelected = 1;
254327
}
255328
change = true;
256329
}
257-
if (change && this.optionsSelected[option]) {
258-
this.lastSelected = option;
259-
}
260330
} else {
261331
// Only a single dropdown option can be selected
262332
this.close();

web/main.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -976,6 +976,7 @@ class GitGraphView {
976976

977977
private getBranchContextMenuActions(target: DialogTarget & RefTarget): ContextMenuActions {
978978
const refName = target.ref, visibility = this.config.contextMenuActionsVisibility.branch;
979+
const isSelectedInBranchesDropdown = this.branchDropdown.isSelected(refName);
979980
return [[
980981
{
981982
title: 'Checkout Branch',
@@ -1078,6 +1079,17 @@ class GitGraphView {
10781079
runAction({ command: 'createArchive', repo: this.currentRepo, ref: refName }, 'Creating Archive');
10791080
}
10801081
},
1082+
{
1083+
title: 'Select in Branches Dropdown',
1084+
visible: visibility.selectInBranchesDropdown && !isSelectedInBranchesDropdown,
1085+
onClick: () => this.branchDropdown.selectOption(refName)
1086+
},
1087+
{
1088+
title: 'Unselect in Branches Dropdown',
1089+
visible: visibility.unselectInBranchesDropdown && isSelectedInBranchesDropdown,
1090+
onClick: () => this.branchDropdown.unselectOption(refName)
1091+
}
1092+
], [
10811093
{
10821094
title: 'Copy Branch Name to Clipboard',
10831095
visible: visibility.copyName,
@@ -1231,6 +1243,8 @@ class GitGraphView {
12311243
private getRemoteBranchContextMenuActions(remote: string, target: DialogTarget & RefTarget): ContextMenuActions {
12321244
const refName = target.ref, visibility = this.config.contextMenuActionsVisibility.remoteBranch;
12331245
const branchName = remote !== '' ? refName.substring(remote.length + 1) : '';
1246+
const prefixedRefName = 'remotes/' + refName;
1247+
const isSelectedInBranchesDropdown = this.branchDropdown.isSelected(prefixedRefName);
12341248
return [[
12351249
{
12361250
title: 'Checkout Branch' + ELLIPSIS,
@@ -1297,6 +1311,17 @@ class GitGraphView {
12971311
runAction({ command: 'createArchive', repo: this.currentRepo, ref: refName }, 'Creating Archive');
12981312
}
12991313
},
1314+
{
1315+
title: 'Select in Branches Dropdown',
1316+
visible: visibility.selectInBranchesDropdown && !isSelectedInBranchesDropdown,
1317+
onClick: () => this.branchDropdown.selectOption(prefixedRefName)
1318+
},
1319+
{
1320+
title: 'Unselect in Branches Dropdown',
1321+
visible: visibility.unselectInBranchesDropdown && isSelectedInBranchesDropdown,
1322+
onClick: () => this.branchDropdown.unselectOption(prefixedRefName)
1323+
}
1324+
], [
13001325
{
13011326
title: 'Copy Branch Name to Clipboard',
13021327
visible: visibility.copyName,

0 commit comments

Comments
 (0)