Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions src/backend/queries/loadCommits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const eolRegex = /\r\n|\r|\n/g;
const gitLogSeparator = "XX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb";

type LoadCommitsInput = {
branchName: string;
branchNames: string[];
maxCommits: number;
showRemoteBranches: boolean;
hard: boolean;
Expand Down Expand Up @@ -55,19 +55,19 @@ async function getRefs(git: SimpleGit, showRemoteBranches: boolean): Promise<Git

async function getLog(
git: SimpleGit,
branch: string,
branches: string[],
maxCommits: number,
showRemoteBranches: boolean,
dateType: DateType
): Promise<GitLogEntry[]> {
const dateField = dateType === "Author Date" ? "%at" : "%ct";
const format = ["%H", "%P", "%an", "%ae", dateField, "%s"].join(gitLogSeparator);
const args = ["log", `--max-count=${maxCommits}`, `--format=${format}`, "--date-order"];
if (branch !== "") {
args.push(branch);
} else {
if (branches.length === 1 && branches[0] === "") {
args.push("--branches", "--tags");
if (showRemoteBranches) args.push("--remotes");
} else {
args.push(...branches);
Comment on lines +66 to +70
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Chzxxuanzheng This is a valid concern. When someone passes ["", "main"], the input will go directly into the else branch. I think the best solution is to use a discriminated union so that the input is normalized before reaching the backend.

}
Comment thread
Chzxxuanzheng marked this conversation as resolved.
try {
const stdout = await git.raw(args);
Expand Down Expand Up @@ -105,11 +105,11 @@ export async function loadCommits(
git: SimpleGit,
input: LoadCommitsInput
): Promise<QueryResult<"loadCommits">> {
const { branchName, maxCommits, showRemoteBranches, hard, dateType, showUncommittedChanges } =
const { branchNames, maxCommits, showRemoteBranches, hard, dateType, showUncommittedChanges } =
input;

const [rawCommits, refData] = await Promise.all([
getLog(git, branchName, maxCommits + 1, showRemoteBranches, dateType),
getLog(git, branchNames, maxCommits + 1, showRemoteBranches, dateType),
getRefs(git, showRemoteBranches)
]);

Expand Down
2 changes: 1 addition & 1 deletion src/backend/types/queries.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ type QueryPayloads = {
loadCommits: {
request: {
repo: string;
branchName: string;
branchNames: string[];
maxCommits: number;
showRemoteBranches: boolean;
hard: boolean;
Expand Down
2 changes: 1 addition & 1 deletion src/extension/messageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export function registerMessageHandlers(
bridge.post({
command: "loadCommits",
...(await loadCommits(gitClient.getInstance(), {
branchName: msg.branchName,
branchNames: msg.branchNames,
maxCommits: msg.maxCommits,
showRemoteBranches: msg.showRemoteBranches,
hard: msg.hard,
Expand Down
247 changes: 195 additions & 52 deletions src/webview/dropdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,20 @@ interface DropdownOption {
value: string;
}

export class Dropdown {
private options: DropdownOption[] = [];
private selectedOption: number = 0;
private dropdownVisible: boolean = false;
private showInfo: boolean;
private changeCallback: { (value: string): void };
abstract class AbstractDropdown {
protected options: DropdownOption[] = [];
protected dropdownVisible: boolean = false;
protected showInfo: boolean;

private elem: HTMLElement;
private currentValueElem: HTMLDivElement;
private menuElem: HTMLDivElement;
private optionsElem: HTMLDivElement;
private noResultsElem: HTMLDivElement;
private filterInput: HTMLInputElement;
protected elem: HTMLElement;
protected currentValueElem: HTMLDivElement;
protected menuElem: HTMLDivElement;
protected optionsElem: HTMLDivElement;
protected noResultsElem: HTMLDivElement;
protected filterInput: HTMLInputElement;

constructor(
id: string,
showInfo: boolean,
dropdownType: string,
changeCallback: { (value: string): void }
) {
constructor(id: string, showInfo: boolean, dropdownType: string) {
this.showInfo = showInfo;
this.changeCallback = changeCallback;
this.elem = document.getElementById(id)!;

let filter = document.createElement("div");
Expand Down Expand Up @@ -59,28 +51,15 @@ export class Dropdown {
this.dropdownVisible = !this.dropdownVisible;
if (this.dropdownVisible) {
this.filterInput.value = "";
this.filter();
this.onFilterChange();
}
this.elem.classList.toggle("dropdownOpen");
if (this.dropdownVisible) this.filterInput.focus();
} else if (this.dropdownVisible) {
if ((<HTMLElement>e.target).closest(".dropdown") !== this.elem) {
this.close();
} else {
let option = <HTMLElement | null>(<HTMLElement>e.target).closest(".dropdownOption");
if (
option !== null &&
option.parentNode === this.optionsElem &&
typeof option.dataset.id !== "undefined"
) {
let selectedOption = parseInt(option.dataset.id!);
this.close();
if (this.selectedOption !== selectedOption) {
this.selectedOption = selectedOption;
this.render();
this.changeCallback(this.options[this.selectedOption].value);
}
}
this.handleOptionClick(e.target as HTMLElement);
}
}
},
Expand All @@ -94,7 +73,69 @@ export class Dropdown {
},
true
);
this.filterInput.addEventListener("keyup", () => this.filter());
this.filterInput.addEventListener("keyup", () => this.onFilterChange());
}

protected handleOptionClick(target: HTMLElement) {
let option = <HTMLElement | null>target.closest(".dropdownOption");
if (
option !== null &&
option.parentNode === this.optionsElem &&
typeof option.dataset.id !== "undefined"
) {
this.onOptionSelected(parseInt(option.dataset.id!));
}
}

protected abstract onOptionSelected(index: number): void;
protected abstract render(): void;
protected abstract onFilterChange(): void;

public abstract setOptions(options: DropdownOption[], ...args: unknown[]): void;

public refresh() {
if (this.options.length > 0) this.render();
}

protected filter() {
let val = this.filterInput.value.toLowerCase(),
match,
matches = false;
for (let i = 0; i < this.options.length; i++) {
match = this.options[i].name.toLowerCase().indexOf(val) > -1;
(<HTMLElement>this.optionsElem.children[i]).style.display = match ? "block" : "none";
if (match) matches = true;
}
this.filterInput.style.display = "block";
this.noResultsElem.style.display = matches ? "none" : "block";
}

protected close() {
this.elem.classList.remove("dropdownOpen");
this.dropdownVisible = false;
}

protected updateCurrentValueWidth() {
this.currentValueElem.style.width =
Math.max(
this.menuElem.offsetWidth + (this.showInfo && this.menuElem.offsetHeight < 272 ? 0 : 12),
130
) + "px";
}
}

export class Dropdown extends AbstractDropdown {
private selectedOption: number = 0;
private changeCallback: { (value: string): void };

constructor(
id: string,
showInfo: boolean,
dropdownType: string,
changeCallback: { (value: string): void }
) {
super(id, showInfo, dropdownType);
this.changeCallback = changeCallback;
}

public setOptions(options: DropdownOption[], selected: string) {
Expand All @@ -110,11 +151,16 @@ export class Dropdown {
this.render();
}

public refresh() {
if (this.options.length > 0) this.render();
protected onOptionSelected(index: number): void {
this.close();
if (this.selectedOption !== index) {
this.selectedOption = index;
this.render();
this.changeCallback(this.options[this.selectedOption].value);
}
}

private render() {
protected render() {
this.elem.classList.add("loaded");
this.currentValueElem.innerHTML = escapeHtml(this.options[this.selectedOption].name);
let html = "";
Expand Down Expand Up @@ -142,30 +188,127 @@ export class Dropdown {
this.menuElem.style.cssText = "opacity:0; display:block;";
// Width must be at least 130px for the filter elements. Max height for the dropdown is [filter (31px) + 9.5 * dropdown item (28px) = 297px]
// Don't need to add 12px if showing info icons and scrollbar isn't needed. The scrollbar isn't needed if: menuElem height + filter input (25px) < 297px
this.currentValueElem.style.width =
Math.max(
this.menuElem.offsetWidth + (this.showInfo && this.menuElem.offsetHeight < 272 ? 0 : 12),
130
) + "px";
this.updateCurrentValueWidth();
this.menuElem.style.cssText = "right:0; overflow-y:auto; max-height:297px;";
if (this.dropdownVisible) this.filter();
}

private filter() {
let val = this.filterInput.value.toLowerCase(),
match,
matches = false;
protected onFilterChange() {
this.filter();
}
}

export class CheckboxDropdown extends AbstractDropdown {
private selectedOptions: Set<string> = new Set();
private multipleChangeCallback: { (values: string[], change: string): void };

constructor(
id: string,
showInfo: boolean,
dropdownType: string,
changeCallback: { (values: string[], change: string): void }
) {
super(id, showInfo, dropdownType);
this.multipleChangeCallback = changeCallback;
}

public setOptions(options: DropdownOption[], selectedValues: string[]) {
this.options = options;
this.selectedOptions.clear();
for (const value of selectedValues) {
this.selectedOptions.add(value);
}
if (options.length <= 1) this.close();
this.render();
}

public setSelected(value: string) {
if (!this.options.some((i) => i.value === value))
throw new Error(
`unknown option: "${value}", available options: ${this.options.map((i) => `"${i.value}"`).join(", ")}`
);
Comment on lines +226 to +229
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (!this.options.some((i) => i.value === value))
throw new Error(
`unknown option: "${value}", available options: ${this.options.map((i) => `"${i.value}"`).join(", ")}`
);
if (!this.options.some((i) => i.value === value)) {
console.error(`CheckboxDropdown.setSelected: unknown option "${value}"`);
return;
}

The exception will crash the UI.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

emm...I think adding a global error popup would be better. If we just use console, the user won't notice anything unless they open the DevTools. However, I'm worried that adding too much might make the PR go out of scope.

this.selectedOptions.add(value);
this.render();
}

public setUnSelected(value: string) {
if (!this.options.some((i) => i.value === value))
throw new Error(
`unknown option: "${value}", available options: ${this.options.map((i) => `"${i.value}"`).join(", ")}`
);
Comment on lines +235 to +238
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (!this.options.some((i) => i.value === value))
throw new Error(
`unknown option: "${value}", available options: ${this.options.map((i) => `"${i.value}"`).join(", ")}`
);
if (!this.options.some((i) => i.value === value)) {
console.error(`CheckboxDropdown.setUnSelected: unknown option "${value}"`);
return;
}

this.selectedOptions.delete(value);
Comment thread
Chzxxuanzheng marked this conversation as resolved.
this.render();
}

protected onOptionSelected(index: number): void {
const value = this.options[index].value;
if (this.selectedOptions.has(value)) {
this.selectedOptions.delete(value);
} else {
this.selectedOptions.add(value);
}
this.render();
this.emitChange(value);
}

protected render() {
this.elem.classList.add("loaded");

const selectedNames = this.options
.filter((i) => this.selectedOptions.has(i.value))
.map((i) => i.name)
.join(" & ");
this.currentValueElem.innerHTML = escapeHtml(selectedNames);

let html = "";
for (const [i, option] of Object.entries(this.options)) {
const isSelected = this.selectedOptions.has(option.value);
html +=
'<div class="dropdownOption' +
(isSelected ? " selected" : "") +
'" data-id="' +
i +
'"><input type="checkbox" class="dropdownCheckbox" ' +
(isSelected ? "checked" : "") +
Comment thread
Chzxxuanzheng marked this conversation as resolved.
"/> " +
escapeHtml(option.name) +
(this.showInfo
? '<div class="dropdownOptionInfo" title="' +
escapeHtml(option.value) +
'">' +
svgIcons.info +
"</div>"
: "") +
"</div>";
}
this.optionsElem.className = "dropdownOptions" + (this.showInfo ? " showInfo" : "");
this.optionsElem.innerHTML = html;
this.filterInput.style.display = "none";
this.noResultsElem.style.display = "none";
this.menuElem.style.cssText = "opacity:0; display:block;";
this.updateCurrentValueWidth();
this.menuElem.style.cssText = "right:0; overflow-y:auto; max-height:297px;";

if (this.dropdownVisible) this.filterCheckboxes();
}

protected onFilterChange() {
this.filterCheckboxes();
}

private filterCheckboxes() {
const val = this.filterInput.value.toLowerCase();
let matches = false;
for (let i = 0; i < this.options.length; i++) {
match = this.options[i].name.toLowerCase().indexOf(val) > -1;
(<HTMLElement>this.optionsElem.children[i]).style.display = match ? "block" : "none";
const match = this.options[i].name.toLowerCase().indexOf(val) > -1;
(this.optionsElem.children[i] as HTMLElement).style.display = match ? "block" : "none";
if (match) matches = true;
}
this.filterInput.style.display = "block";
this.noResultsElem.style.display = matches ? "none" : "block";
}

private close() {
this.elem.classList.remove("dropdownOpen");
this.dropdownVisible = false;
private emitChange(change: string) {
this.multipleChangeCallback(Array.from(this.selectedOptions), change);
}
}
2 changes: 1 addition & 1 deletion src/webview/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ declare global {
commits: GitCommitNode[];
commitHead: string | null;
avatars: AvatarImageCollection;
currentBranch: string | null;
currentBranches: string[] | null;
currentRepo: string;
moreCommitsAvailable: boolean;
maxCommits: number;
Expand Down
Loading
Loading