Skip to content

Commit

Permalink
#417 New context menu actions for branches and remote branches, to se…
Browse files Browse the repository at this point in the history
…lect / unselect the branch in the Branches Dropdown.
  • Loading branch information
mhutchie committed Dec 23, 2020
1 parent 1d9c808 commit cb7f9ae
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 26 deletions.
16 changes: 16 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,14 @@
"type": "boolean",
"title": "Create Archive"
},
"selectInBranchesDropdown": {
"type": "boolean",
"title": "Select in Branches Dropdown"
},
"unselectInBranchesDropdown": {
"type": "boolean",
"title": "Unselect in Branches Dropdown"
},
"copyName": {
"type": "boolean",
"title": "Copy Branch Name to Clipboard"
Expand Down Expand Up @@ -256,6 +264,14 @@
"type": "boolean",
"title": "Create Archive"
},
"selectInBranchesDropdown": {
"type": "boolean",
"title": "Select in Branches Dropdown"
},
"unselectInBranchesDropdown": {
"type": "boolean",
"title": "Unselect in Branches Dropdown"
},
"copyName": {
"type": "boolean",
"title": "Copy Branch Name to Clipboard"
Expand Down
8 changes: 4 additions & 4 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,11 @@ class Config {
* Get the value of the `git-graph.contextMenuActionsVisibility` Extension Setting.
*/
get contextMenuActionsVisibility(): ContextMenuActionsVisibility {
let userConfig = this.config.get('contextMenuActionsVisibility', {});
let config = {
branch: { checkout: true, rename: true, delete: true, merge: true, rebase: true, push: true, createPullRequest: true, createArchive: true, copyName: true },
const userConfig = this.config.get('contextMenuActionsVisibility', {});
const config: ContextMenuActionsVisibility = {
branch: { checkout: true, rename: true, delete: true, merge: true, rebase: true, push: true, createPullRequest: true, createArchive: true, selectInBranchesDropdown: true, unselectInBranchesDropdown: true, copyName: true },
commit: { addTag: true, createBranch: true, checkout: true, cherrypick: true, revert: true, drop: true, merge: true, rebase: true, reset: true, copyHash: true, copySubject: true },
remoteBranch: { checkout: true, delete: true, fetch: true, merge: true, pull: true, createPullRequest: true, createArchive: true, copyName: true },
remoteBranch: { checkout: true, delete: true, fetch: true, merge: true, pull: true, createPullRequest: true, createArchive: true, selectInBranchesDropdown: true, unselectInBranchesDropdown: true, copyName: true },
stash: { apply: true, createBranch: true, pop: true, drop: true, copyName: true, copyHash: true },
tag: { viewDetails: true, delete: true, push: true, createArchive: true, copyName: true },
uncommittedChanges: { stash: true, reset: true, clean: true, openSourceControlView: true }
Expand Down
4 changes: 4 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,8 @@ export interface ContextMenuActionsVisibility {
readonly push: boolean;
readonly createPullRequest: boolean;
readonly createArchive: boolean;
readonly selectInBranchesDropdown: boolean;
readonly unselectInBranchesDropdown: boolean;
readonly copyName: boolean;
};
readonly commit: {
Expand All @@ -363,6 +365,8 @@ export interface ContextMenuActionsVisibility {
readonly pull: boolean;
readonly createPullRequest: boolean;
readonly createArchive: boolean;
readonly selectInBranchesDropdown: boolean;
readonly unselectInBranchesDropdown: boolean;
readonly copyName: boolean;
};
readonly stash: {
Expand Down
12 changes: 12 additions & 0 deletions tests/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,8 @@ describe('Config', () => {
push: true,
createPullRequest: true,
createArchive: true,
selectInBranchesDropdown: true,
unselectInBranchesDropdown: true,
copyName: true
},
commit: {
Expand All @@ -295,6 +297,8 @@ describe('Config', () => {
pull: true,
createPullRequest: true,
createArchive: true,
selectInBranchesDropdown: true,
unselectInBranchesDropdown: true,
copyName: true
},
stash: {
Expand Down Expand Up @@ -337,6 +341,8 @@ describe('Config', () => {
push: true,
createPullRequest: true,
createArchive: true,
selectInBranchesDropdown: true,
unselectInBranchesDropdown: true,
copyName: true
},
commit: {
Expand All @@ -360,6 +366,8 @@ describe('Config', () => {
pull: true,
createPullRequest: true,
createArchive: true,
selectInBranchesDropdown: true,
unselectInBranchesDropdown: true,
copyName: true
},
stash: {
Expand Down Expand Up @@ -417,6 +425,8 @@ describe('Config', () => {
push: true,
createPullRequest: true,
createArchive: true,
selectInBranchesDropdown: true,
unselectInBranchesDropdown: true,
copyName: true
},
commit: {
Expand All @@ -440,6 +450,8 @@ describe('Config', () => {
pull: true,
createPullRequest: true,
createArchive: true,
selectInBranchesDropdown: true,
unselectInBranchesDropdown: true,
copyName: true
},
stash: {
Expand Down
114 changes: 92 additions & 22 deletions web/dropdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ interface DropdownOption {
* Implements the dropdown inputs used in the Git Graph View's top control bar.
*/
class Dropdown {
private readonly showInfo: boolean;
private readonly multipleAllowed: boolean;
private readonly changeCallback: (values: string[]) => void;

private options: ReadonlyArray<DropdownOption> = [];
private optionsSelected: boolean[] = [];
private lastSelected: number = 0;
private numSelected: number = 0;
private lastSelected: number = 0; // Only used when multipleAllowed === false
private dropdownVisible: boolean = false;
private showInfo: boolean;
private multipleAllowed: boolean;
private changeCallback: { (values: string[]): void };
private lastClicked: number = 0;
private doubleClickTimeout: NodeJS.Timer | null = null;

Expand All @@ -35,7 +35,7 @@ class Dropdown {
* @param changeCallback A callback to be invoked when the selected item(s) of the dropdown changes.
* @returns The Dropdown instance.
*/
constructor(id: string, showInfo: boolean, multipleAllowed: boolean, dropdownType: string, changeCallback: { (values: string[]): void }) {
constructor(id: string, showInfo: boolean, multipleAllowed: boolean, dropdownType: string, changeCallback: (values: string[]) => void) {
this.showInfo = showInfo;
this.multipleAllowed = multipleAllowed;
this.changeCallback = changeCallback;
Expand Down Expand Up @@ -78,9 +78,9 @@ class Dropdown {
if ((<HTMLElement>e.target).closest('.dropdown') !== this.elem) {
this.close();
} else {
let option = <HTMLElement | null>(<HTMLElement>e.target).closest('.dropdownOption');
const option = <HTMLElement | null>(<HTMLElement>e.target).closest('.dropdownOption');
if (option !== null && option.parentNode === this.optionsElem && typeof option.dataset.id !== 'undefined') {
this.selectOption(parseInt(option.dataset.id!));
this.onOptionClick(parseInt(option.dataset.id!));
}
}
}
Expand All @@ -97,27 +97,104 @@ class Dropdown {
public setOptions(options: ReadonlyArray<DropdownOption>, optionsSelected: string[]) {
this.options = options;
this.optionsSelected = [];
this.numSelected = 0;
let selectedOption = -1, isSelected;
for (let i = 0; i < options.length; i++) {
isSelected = optionsSelected.includes(options[i].value);
this.optionsSelected[i] = isSelected;
if (isSelected) {
selectedOption = i;
this.numSelected++;
}
}
if (selectedOption === -1) {
selectedOption = 0;
this.optionsSelected[selectedOption] = true;
this.numSelected++;
}
this.lastSelected = selectedOption;
if (this.dropdownVisible && options.length <= 1) this.close();
this.render();
this.clearDoubleClickTimeout();
}

/**
* Is a value selected in the dropdown (respecting "Show All")
* @param value The value to check.
* @returns TRUE => The value is selected, FALSE => The value is not selected.
*/
public isSelected(value: string) {
if (this.options.length > 0) {
if (this.multipleAllowed && this.optionsSelected[0]) {
// Multiple options can be selected, and "Show All" is selected.
return true;
}
const optionIndex = this.options.findIndex((option) => option.value === value);
if (optionIndex > -1 && this.optionsSelected[optionIndex]) {
// The specific option is selected
return true;
}
}
return false;
}

/**
* Select a specific value in the dropdown.
* @param value The value to select.
*/
public selectOption(value: string) {
const optionIndex = this.options.findIndex((option) => value === option.value);
if (this.multipleAllowed && optionIndex > -1 && !this.optionsSelected[optionIndex]) {
// Select the option with the specified value
this.optionsSelected[optionIndex] = true;

if (!this.optionsSelected[0] && this.optionsSelected.slice(1).every((selected) => selected)) {
// All options are selected, so simplify selected items to just be "Show All"
this.optionsSelected[0] = true;
for (let i = 1; i < this.optionsSelected.length; i++) {
this.optionsSelected[i] = false;
}
}

// A change has occurred, re-render the dropdown options
const menuScroll = this.menuElem.scrollTop;
this.render();
if (this.dropdownVisible) {
this.menuElem.scroll(0, menuScroll);
}
this.changeCallback(this.getSelectedOptions(false));
}
}

/**
* Unselect a specific value in the dropdown.
* @param value The value to unselect.
*/
public unselectOption(value: string) {
const optionIndex = this.options.findIndex((option) => value === option.value);
if (this.multipleAllowed && optionIndex > -1 && (this.optionsSelected[0] || this.optionsSelected[optionIndex])) {
if (this.optionsSelected[0]) {
// Show All is currently selected, so unselect it, and select all branch options
this.optionsSelected[0] = false;
for (let i = 1; i < this.optionsSelected.length; i++) {
this.optionsSelected[i] = true;
}
}

// Unselect the option with the specified value
this.optionsSelected[optionIndex] = false;
if (this.optionsSelected.every(selected => !selected)) {
// All items have been unselected, select "Show All"
this.optionsSelected[0] = true;
}

// A change has occurred, re-render the dropdown options
const menuScroll = this.menuElem.scrollTop;
this.render();
if (this.dropdownVisible) {
this.menuElem.scroll(0, menuScroll);
}
this.changeCallback(this.getSelectedOptions(false));
}
}

/**
* Refresh the rendered dropdown to apply style changes.
*/
Expand Down Expand Up @@ -209,7 +286,7 @@ class Dropdown {
* Select a dropdown option.
* @param option The index of the option to select.
*/
private selectOption(option: number) {
private onOptionClick(option: number) {
// Note: Show All is always the first option (0 index) when multiple selected items are allowed
let change = false;
let doubleClick = this.doubleClickTimeout !== null && this.lastClicked === option;
Expand All @@ -218,10 +295,8 @@ class Dropdown {
if (doubleClick) {
// Double click
if (this.multipleAllowed && option === 0) {
this.numSelected = 1;
for (let i = 1; i < this.optionsSelected.length; i++) {
this.optionsSelected[i] = !this.optionsSelected[i];
if (this.optionsSelected[i]) this.numSelected++;
}
change = true;
}
Expand All @@ -236,27 +311,22 @@ class Dropdown {
for (let i = 1; i < this.optionsSelected.length; i++) {
this.optionsSelected[i] = false;
}
this.numSelected = 1;
change = true;
}
} else {
if (this.optionsSelected[0]) {
// Deselect "Show All" if it is enabled
this.optionsSelected[0] = false;
this.numSelected--;
}

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

if (this.numSelected === 0) {
if (this.optionsSelected.every(selected => !selected)) {
// All items have been unselected, select "Show All"
this.optionsSelected[0] = true;
this.numSelected = 1;
}
change = true;
}
if (change && this.optionsSelected[option]) {
this.lastSelected = option;
}
} else {
// Only a single dropdown option can be selected
this.close();
Expand Down
25 changes: 25 additions & 0 deletions web/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -976,6 +976,7 @@ class GitGraphView {

private getBranchContextMenuActions(target: DialogTarget & RefTarget): ContextMenuActions {
const refName = target.ref, visibility = this.config.contextMenuActionsVisibility.branch;
const isSelectedInBranchesDropdown = this.branchDropdown.isSelected(refName);
return [[
{
title: 'Checkout Branch',
Expand Down Expand Up @@ -1078,6 +1079,17 @@ class GitGraphView {
runAction({ command: 'createArchive', repo: this.currentRepo, ref: refName }, 'Creating Archive');
}
},
{
title: 'Select in Branches Dropdown',
visible: visibility.selectInBranchesDropdown && !isSelectedInBranchesDropdown,
onClick: () => this.branchDropdown.selectOption(refName)
},
{
title: 'Unselect in Branches Dropdown',
visible: visibility.unselectInBranchesDropdown && isSelectedInBranchesDropdown,
onClick: () => this.branchDropdown.unselectOption(refName)
}
], [
{
title: 'Copy Branch Name to Clipboard',
visible: visibility.copyName,
Expand Down Expand Up @@ -1231,6 +1243,8 @@ class GitGraphView {
private getRemoteBranchContextMenuActions(remote: string, target: DialogTarget & RefTarget): ContextMenuActions {
const refName = target.ref, visibility = this.config.contextMenuActionsVisibility.remoteBranch;
const branchName = remote !== '' ? refName.substring(remote.length + 1) : '';
const prefixedRefName = 'remotes/' + refName;
const isSelectedInBranchesDropdown = this.branchDropdown.isSelected(prefixedRefName);
return [[
{
title: 'Checkout Branch' + ELLIPSIS,
Expand Down Expand Up @@ -1297,6 +1311,17 @@ class GitGraphView {
runAction({ command: 'createArchive', repo: this.currentRepo, ref: refName }, 'Creating Archive');
}
},
{
title: 'Select in Branches Dropdown',
visible: visibility.selectInBranchesDropdown && !isSelectedInBranchesDropdown,
onClick: () => this.branchDropdown.selectOption(prefixedRefName)
},
{
title: 'Unselect in Branches Dropdown',
visible: visibility.unselectInBranchesDropdown && isSelectedInBranchesDropdown,
onClick: () => this.branchDropdown.unselectOption(prefixedRefName)
}
], [
{
title: 'Copy Branch Name to Clipboard',
visible: visibility.copyName,
Expand Down

0 comments on commit cb7f9ae

Please sign in to comment.