diff --git a/karma.conf.js b/karma.conf.js index de76f4aadc..4e68f2e156 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -43,7 +43,7 @@ module.exports = function(config) { autoWatchBatchDelay: 1000, browsers: ["CustomElectron"], - browserNoActivityTimeout: 300000, + browserNoActivityTimeout: 3000000, customLaunchers: { CustomElectron: { base: "Electron", diff --git a/src/@batch-flask/core/commands/command-registry/command-registry.spec.ts b/src/@batch-flask/core/commands/command-registry/command-registry.spec.ts index 6f57e3491b..68ad439f8a 100644 --- a/src/@batch-flask/core/commands/command-registry/command-registry.spec.ts +++ b/src/@batch-flask/core/commands/command-registry/command-registry.spec.ts @@ -2,6 +2,7 @@ import { CommandRegistry } from "./command-registry"; const cmd1 = { id: "foo", + description: "My foo command", binding: "ctrl+f", execute: () => null, }; @@ -24,6 +25,7 @@ describe("CommandRegistry", () => { const cmd2 = { id: "bar", binding: "ctrl+b", + description: "My bar command", when: (context) => context.has("isFocused"), execute: () => null, }; @@ -38,6 +40,7 @@ describe("CommandRegistry", () => { const cmd2 = { id: "foo", + description: "My bar command", binding: "ctrl+b", when: (context) => context.has("isFocused"), execute: () => null, diff --git a/src/@batch-flask/core/commands/command-registry/command-registry.ts b/src/@batch-flask/core/commands/command-registry/command-registry.ts index 7623e7255b..b79ece1bad 100644 --- a/src/@batch-flask/core/commands/command-registry/command-registry.ts +++ b/src/@batch-flask/core/commands/command-registry/command-registry.ts @@ -4,6 +4,7 @@ import { CommandContext } from "../context"; export interface Command { id: string; + description: string; binding: string; when?: (context: CommandContext) => boolean; execute: (injector: Injector, context: CommandContext) => Promise | void; diff --git a/src/@batch-flask/core/commands/keybindings/keybindings.service.spec.ts b/src/@batch-flask/core/commands/keybindings/keybindings.service.spec.ts index 2959ea1c26..8916ffe82c 100644 --- a/src/@batch-flask/core/commands/keybindings/keybindings.service.spec.ts +++ b/src/@batch-flask/core/commands/keybindings/keybindings.service.spec.ts @@ -21,17 +21,20 @@ describe("Keybinding service", () => { CommandRegistry.register({ id: "foo", + description: "My foo command", binding: "ctrl+f", execute: cmd1Spy, }); CommandRegistry.register({ id: "bar", + description: "My bar command", binding: "ctrl+b", when: (context) => context.has("barAllowed"), execute: cmd2Spy, }); CommandRegistry.register({ id: "barAlt", + description: "My other command", binding: "ctrl+b", when: (context) => !context.has("barAllowed"), execute: cmd3Spy, diff --git a/src/@batch-flask/core/commands/keybindings/keybindings.service.ts b/src/@batch-flask/core/commands/keybindings/keybindings.service.ts index 6dad0d51e3..a0e269907d 100644 --- a/src/@batch-flask/core/commands/keybindings/keybindings.service.ts +++ b/src/@batch-flask/core/commands/keybindings/keybindings.service.ts @@ -1,15 +1,18 @@ import { Injectable, Injector } from "@angular/core"; import { KeyModifier } from "@batch-flask/core/keys"; import { log } from "@batch-flask/utils"; -import { Subscription, fromEvent, merge } from "rxjs"; +import { BehaviorSubject, Observable, Subscription, fromEvent, merge } from "rxjs"; import { map, tap } from "rxjs/operators"; import { Command, CommandRegistry } from "../command-registry"; import { CommandContext, ContextService } from "../context"; @Injectable({ providedIn: "root" }) export class KeyBindingsService { - private _keyBindings = new Map(); - constructor(private contextService: ContextService, private injector: Injector) { } + public keyBindings: Observable>; + private _keyBindings = new BehaviorSubject(new Map()); + constructor(private contextService: ContextService, private injector: Injector) { + this.keyBindings = this._keyBindings.asObservable(); + } public listen(): Subscription { this._loadCommands(); @@ -36,8 +39,8 @@ export class KeyBindingsService { } public dispatch(binding: KeyBinding, context: CommandContext): boolean { - if (this._keyBindings.has(binding.hash)) { - const commands = this._keyBindings.get(binding.hash)!; + if (this._keyBindings.value.has(binding.hash)) { + const commands = this._keyBindings.value.get(binding.hash)!; const matchingCommands = commands.filter(x => x.when == null || x.when(context)); if (matchingCommands.length === 0) { return false; @@ -53,28 +56,30 @@ export class KeyBindingsService { } private _loadCommands() { + const map = new Map(); const commands = CommandRegistry.getCommands(); for (const command of commands) { - const binding = parseKeyBinding(command.binding); - if (this._keyBindings.has(binding.hash)) { - this._keyBindings.get(binding.hash)!.push(command); + const binding = KeyBinding.parse(command.binding); + if (map.has(binding.hash)) { + map.get(binding.hash)!.push(command); } else { - this._keyBindings.set(binding.hash, [command]); + map.set(binding.hash, [command]); } } - } -} -export function parseKeyBinding(value: string): KeyBinding { - const keys = value.replace(/ /g, "").toLowerCase().split("+"); - return new KeyBinding(keys); + this._keyBindings.next(map); + } } export class KeyBinding { + public static parse(value: string): KeyBinding { + const keys = value.replace(/ /g, "").toLowerCase().split("+"); + return new KeyBinding(keys); + } + public readonly mods: KeyModifier[] = []; public readonly keys: string[] = []; public readonly hash: string; - constructor(keys: string[]) { const result = this._extractModifiers(keys); this.keys = result.keys; diff --git a/src/@batch-flask/ui/index.ts b/src/@batch-flask/ui/index.ts index 2aec19f6e5..192458b9ed 100644 --- a/src/@batch-flask/ui/index.ts +++ b/src/@batch-flask/ui/index.ts @@ -14,6 +14,7 @@ export * from "./entity-commands"; export * from "./file"; export * from "./form"; export * from "./i18n"; +export * from "./keybindings"; export * from "./loading"; export * from "./metrics-monitor"; export * from "./notifications"; diff --git a/src/@batch-flask/ui/keybindings/index.ts b/src/@batch-flask/ui/keybindings/index.ts new file mode 100644 index 0000000000..c4ca7f9662 --- /dev/null +++ b/src/@batch-flask/ui/keybindings/index.ts @@ -0,0 +1,2 @@ +export * from "./keybindings.module"; +export * from "./keybindings.component"; diff --git a/src/@batch-flask/ui/keybindings/keybindings.component.spec.ts b/src/@batch-flask/ui/keybindings/keybindings.component.spec.ts new file mode 100644 index 0000000000..80fa35912f --- /dev/null +++ b/src/@batch-flask/ui/keybindings/keybindings.component.spec.ts @@ -0,0 +1,166 @@ +import { Component, DebugElement } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { FormsModule, ReactiveFormsModule } from "@angular/forms"; +import { By } from "@angular/platform-browser"; +import { RouterTestingModule } from "@angular/router/testing"; +import { CommandRegistry, KeyBindingsService } from "@batch-flask/core"; +import { ElectronTestingModule } from "@batch-flask/electron/testing"; +import { of } from "rxjs"; +import { updateInput } from "test/utils/helpers"; +import { FormModule } from ".."; +import { TableTestingModule } from "../testing"; +import { KeyBindingsComponent } from "./keybindings.component"; + +@Component({ + template: ``, +}) +class TestComponent { +} + +const fooCmd = { + id: "foo", + description: "My foo command", + binding: "ctrl+f", + execute: () => null, +}; + +const barCmd = { + id: "bar", + description: "My bar command", + binding: "ctrl+b", + when: (context) => context.has("barAllowed"), + execute: () => null, +}; + +const barAltCmd = { + id: "barAlt", + description: "My other command", + binding: "ctrl+b", + when: (context) => !context.has("barAllowed"), + execute: () => null, +}; + +const overrideCmd = { + id: "override", + description: "My command override", + binding: "ctrl+d", + execute: () => null, +}; + +const keybindingsMap = new Map() + .set("ctrl+f", [fooCmd]) + .set("ctrl+b", [barCmd, barAltCmd]) + .set("ctrl+o", [overrideCmd]); + +describe("KeyBindingsComponent", () => { + let fixture: ComponentFixture; + let de: DebugElement; + let keyBindingServiceSpy; + let searchEl: DebugElement; + + beforeEach(() => { + + keyBindingServiceSpy = { + keyBindings: of(keybindingsMap), + }; + CommandRegistry.register(fooCmd); + CommandRegistry.register(barCmd); + CommandRegistry.register(barAltCmd); + CommandRegistry.register(overrideCmd); + + TestBed.configureTestingModule({ + imports: [ + FormsModule, + ReactiveFormsModule, + FormModule, + TableTestingModule, + ElectronTestingModule, + RouterTestingModule, + ], + declarations: [KeyBindingsComponent, TestComponent], + providers: [ + { provide: KeyBindingsService, useValue: keyBindingServiceSpy }, + ], + }); + fixture = TestBed.createComponent(TestComponent); + de = fixture.debugElement.query(By.css("bl-keybindings")); + fixture.detectChanges(); + + searchEl = de.query(By.css("input.search")); + }); + + afterEach(() => { + (CommandRegistry as any)._commands.clear(); + }); + + function getRows() { + return de.queryAll(By.css("bl-row-render")); + } + + function getCells(row: DebugElement) { + return row.queryAll(By.css(".bl-table-cell")); + } + + it("shows all commands with their binding", () => { + const rows = getRows(); + expect(rows.length).toBe(4); + const row0Cells = getCells(rows[0]); + expect(row0Cells[0].nativeElement.textContent).toContain("My foo command"); + expect(row0Cells[1].nativeElement.textContent).toContain("ctrl+f"); + expect(row0Cells[2].nativeElement.textContent).toContain("Default"); + expect(row0Cells[2].nativeElement.textContent).not.toContain("User"); + + const row1Cells = getCells(rows[1]); + expect(row1Cells[0].nativeElement.textContent).toContain("My bar command"); + expect(row1Cells[1].nativeElement.textContent).toContain("ctrl+b"); + expect(row1Cells[2].nativeElement.textContent).toContain("Default"); + expect(row1Cells[2].nativeElement.textContent).not.toContain("User"); + + const row2Cells = getCells(rows[2]); + expect(row2Cells[0].nativeElement.textContent).toContain("My other command"); + expect(row2Cells[1].nativeElement.textContent).toContain("ctrl+b"); + expect(row2Cells[2].nativeElement.textContent).toContain("Default"); + expect(row2Cells[2].nativeElement.textContent).not.toContain("User"); + + const row3Cells = getCells(rows[3]); + expect(row3Cells[0].nativeElement.textContent).toContain("My command override"); + expect(row3Cells[1].nativeElement.textContent).toContain("ctrl+o"); + expect(row3Cells[2].nativeElement.textContent).toContain("User"); + expect(row3Cells[2].nativeElement.textContent).not.toContain("Default"); + }); + + it("filter the rows by description", () => { + updateInput(searchEl, "foo"); + fixture.detectChanges(); + + let rows = getRows(); + expect(rows.length).toBe(1); + + expect(getCells(rows[0])[0].nativeElement.textContent).toContain("My foo command"); + + updateInput(searchEl, "bar"); + fixture.detectChanges(); + + rows = getRows(); + expect(rows.length).toBe(1); + expect(getCells(rows[0])[0].nativeElement.textContent).toContain("My bar command"); + }); + + it("filter the rows by shortcut", () => { + updateInput(searchEl, `"ctrl+o"`); + fixture.detectChanges(); + + let rows = getRows(); + expect(rows.length).toBe(1); + + expect(getCells(rows[0])[0].nativeElement.textContent).toContain("My command override"); + + updateInput(searchEl, `"ctrl+b`); + fixture.detectChanges(); + + rows = getRows(); + expect(rows.length).toBe(2); + expect(getCells(rows[0])[0].nativeElement.textContent).toContain("My bar command"); + expect(getCells(rows[1])[0].nativeElement.textContent).toContain("My other command"); + }); +}); diff --git a/src/@batch-flask/ui/keybindings/keybindings.component.ts b/src/@batch-flask/ui/keybindings/keybindings.component.ts new file mode 100644 index 0000000000..f69dd7abdf --- /dev/null +++ b/src/@batch-flask/ui/keybindings/keybindings.component.ts @@ -0,0 +1,123 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from "@angular/core"; +import { FormControl } from "@angular/forms"; +import { Command, CommandRegistry, KeyBinding, KeyBindingsService } from "@batch-flask/core"; +import { Subject, combineLatest } from "rxjs"; +import { map, startWith, takeUntil } from "rxjs/operators"; +import { TableConfig } from "../table"; + +import "./keybindings.scss"; + +interface DisplayedCommand { + id: string; + description: string; + binding: string; + isDefault: boolean; +} + +interface KeyBindingFilter { + description?: string; + binding?: string; +} + +// Match the following: +// "ctrl+c" +// "ctrl+d +// Will extract the binding(Without the quotes) +const SEARCH_BY_BINDING_REGEX = /"([^"]*)"?/i; + +@Component({ + selector: "bl-keybindings", + templateUrl: "keybindings.html", + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class KeyBindingsComponent implements OnInit, OnDestroy { + public static breadcrumb() { + return { name: "Key bindings" }; + } + + public displayedCommands: DisplayedCommand[] = []; + public search = new FormControl(""); + + public tableConfig: TableConfig = { + activable: false, + }; + + private _destroy = new Subject(); + constructor(private keybindingService: KeyBindingsService, private changeDetector: ChangeDetectorRef) { + + } + + public ngOnInit() { + const commandObs = this.keybindingService.keyBindings.pipe( + map((keybindings) => this._buildCommandList(keybindings)), + ); + + const searchObs = this.search.valueChanges.pipe( + startWith(""), + map((query) => this._processSearch(query)), + ); + combineLatest( + commandObs, + searchObs, + ).pipe( + map(([commands, filter]) => this._filter(commands, filter)), + takeUntil(this._destroy), + ).subscribe((commands) => { + this.displayedCommands = commands; + this.changeDetector.markForCheck(); + }); + } + + public ngOnDestroy() { + this._destroy.next(); + this._destroy.complete(); + } + + private _buildCommandList(keybindings: Map): DisplayedCommand[] { + const commands = CommandRegistry.getCommands(); + const commandBindings = new Map(); + for (const [key, commands] of keybindings.entries()) { + for (const command of commands) { + commandBindings.set(command.id, key); + } + } + return commands.map((command) => { + const binding = commandBindings.get(command.id); + return { + id: command.id, + description: command.description, + binding: binding, + isDefault: binding === command.binding, + }; + }); + } + + private _processSearch(query: string): KeyBindingFilter { + const trimed = query.trim(); + const match = SEARCH_BY_BINDING_REGEX.exec(trimed); + if (match) { + return { + binding: KeyBinding.parse(match[1]).hash, + }; + } + return { + description: query.toLowerCase(), + }; + } + + private _filter(commands: DisplayedCommand[], filter: KeyBindingFilter): DisplayedCommand[] { + return commands.filter((command) => { + if (filter.description && filter.description !== "") { + if (!command.description.toLowerCase().includes(filter.description)) { + return false; + } + } + if (filter.binding && !command.binding.includes(filter.binding)) { + return false; + + } + + return true; + }); + } +} diff --git a/src/@batch-flask/ui/keybindings/keybindings.html b/src/@batch-flask/ui/keybindings/keybindings.html new file mode 100644 index 0000000000..1d4e2d81b8 --- /dev/null +++ b/src/@batch-flask/ui/keybindings/keybindings.html @@ -0,0 +1,28 @@ +
+ + + +
+ +
+ + +
Command
+
{{command.description}}
+
+ + +
Shortcut
+
{{command.binding}}
+
+ + +
Source
+
{{command.isDefault ? "Default" : "User"}}
+
+
+ +
+ No commands match the filter +
+
diff --git a/src/@batch-flask/ui/keybindings/keybindings.module.ts b/src/@batch-flask/ui/keybindings/keybindings.module.ts new file mode 100644 index 0000000000..58eeec2488 --- /dev/null +++ b/src/@batch-flask/ui/keybindings/keybindings.module.ts @@ -0,0 +1,18 @@ +import { CommonModule } from "@angular/common"; +import { NgModule } from "@angular/core"; +import { FormsModule, ReactiveFormsModule } from "@angular/forms"; +import { FormModule } from "../form"; +import { TableModule } from "../table"; +import { KeyBindingsComponent } from "./keybindings.component"; + +const publicComponents = [KeyBindingsComponent]; +const privateComponents = []; + +@NgModule({ + imports: [CommonModule, TableModule, FormsModule, ReactiveFormsModule, FormModule], + declarations: [...publicComponents, ...privateComponents], + exports: publicComponents, + entryComponents: [KeyBindingsComponent], +}) +export class KeyBindingsModule { +} diff --git a/src/@batch-flask/ui/keybindings/keybindings.scss b/src/@batch-flask/ui/keybindings/keybindings.scss new file mode 100644 index 0000000000..cf278c8db9 --- /dev/null +++ b/src/@batch-flask/ui/keybindings/keybindings.scss @@ -0,0 +1,22 @@ +@import "app/styles/variables"; + +bl-keybindings { + height: $contentview-height; + display: flex; + flex-direction: column; + + .header { + padding: 10px; + .search { + width: 100%; + } + } + + .content { + flex: 1; + } + + .no-commands-with-filter { + text-align: center; + } +} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index d25d8893ba..a590436773 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -13,7 +13,7 @@ import { USER_SERVICE, } from "@batch-flask/core"; import { ElectronRendererModule } from "@batch-flask/electron"; -import { BaseModule } from "@batch-flask/ui"; +import { BaseModule, KeyBindingsModule } from "@batch-flask/ui"; import { AppComponent } from "app/app.component"; import { AccountModule } from "app/components/account/account.module"; import { FileModule } from "app/components/file/file.module"; @@ -62,6 +62,7 @@ const modules = [ preloadingStrategy: PreloadAllModules, }), BaseModule, + KeyBindingsModule, HttpClientModule, ...modules, ], diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index b18660fe14..2ebd4557fd 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -1,4 +1,5 @@ import { Routes } from "@angular/router"; +import { KeyBindingsComponent } from "@batch-flask/ui"; import { ActivityMonitorComponent } from "@batch-flask/ui/activity/activity-monitor"; import { RequireActiveBatchAccountGuard } from "app/components/common/guards"; import { ThemeColorsComponent } from "app/components/misc"; @@ -98,4 +99,8 @@ export const routes: Routes = [ path: "standalone/pools/:poolId/graphs", component: PoolStandaloneGraphsComponent, }, + { + path: "keybindings", + component: KeyBindingsComponent, + }, ]; diff --git a/src/app/commands/account.appcmd.ts b/src/app/commands/account.appcmd.ts index f53c05a78c..3ed4a766c1 100644 --- a/src/app/commands/account.appcmd.ts +++ b/src/app/commands/account.appcmd.ts @@ -4,6 +4,7 @@ import { CommandRegistry } from "@batch-flask/core"; CommandRegistry.register({ id: "account.gotoHome", + description: "Navigate to account dashboard", binding: "ctrl+alt+h", execute: (injector: Injector) => { injector.get(Router).navigate(["/accounts"]); diff --git a/src/app/commands/data.appcmd.ts b/src/app/commands/data.appcmd.ts index 4215caa835..5229fec897 100644 --- a/src/app/commands/data.appcmd.ts +++ b/src/app/commands/data.appcmd.ts @@ -4,6 +4,7 @@ import { CommandRegistry } from "@batch-flask/core"; CommandRegistry.register({ id: "data.gotoHome", + description: "Navigate to data", binding: "ctrl+alt+d", execute: (injector: Injector) => { injector.get(Router).navigate(["/data"]); diff --git a/src/app/commands/job.appcmd.ts b/src/app/commands/job.appcmd.ts index fb3d8bf485..3e56ca0027 100644 --- a/src/app/commands/job.appcmd.ts +++ b/src/app/commands/job.appcmd.ts @@ -4,6 +4,7 @@ import { CommandRegistry } from "@batch-flask/core"; CommandRegistry.register({ id: "job.gotoHome", + description: "Navigate to jobs", binding: "ctrl+alt+j", execute: (injector: Injector) => { injector.get(Router).navigate(["/jobs"]); diff --git a/src/app/commands/list.appcmd.ts b/src/app/commands/list.appcmd.ts index fc225373f6..9fb35b2e68 100644 --- a/src/app/commands/list.appcmd.ts +++ b/src/app/commands/list.appcmd.ts @@ -5,6 +5,7 @@ import { log } from "@batch-flask/utils"; CommandRegistry.register({ id: "list.deleteItem", + description: "Delete item in list", binding: "delete", when: (context: CommandContext) => { return context.has("list.focused"); @@ -26,6 +27,7 @@ CommandRegistry.register({ CommandRegistry.register({ id: "list.selectAll", + description: "Select all items in list", binding: "ctrl+a", when: (context: CommandContext) => { return context.has("list.focused"); diff --git a/src/app/commands/pool.appcmd.ts b/src/app/commands/pool.appcmd.ts index 144fd951a5..92c06968ae 100644 --- a/src/app/commands/pool.appcmd.ts +++ b/src/app/commands/pool.appcmd.ts @@ -4,6 +4,7 @@ import { CommandRegistry } from "@batch-flask/core"; CommandRegistry.register({ id: "pool.gotoHome", + description: "Navigate to pools", binding: "ctrl+alt+p", execute: (injector: Injector) => { injector.get(Router).navigate(["/pools"]); diff --git a/src/app/components/layout/main-navigation/profile-button/profile-button.component.spec.ts b/src/app/components/layout/main-navigation/profile-button/profile-button.component.spec.ts index c66e0e08c3..a88919aa1d 100644 --- a/src/app/components/layout/main-navigation/profile-button/profile-button.component.spec.ts +++ b/src/app/components/layout/main-navigation/profile-button/profile-button.component.spec.ts @@ -129,7 +129,7 @@ describe("ProfileButtonComponent", () => { fixture.detectChanges(); expect(contextMenuServiceSpy.openMenu).toHaveBeenCalledOnce(); const items = contextMenuServiceSpy.lastMenu.items; - expect(items.length).toBe(12); + expect(items.length).toBe(13); }); describe("Clicking on the profile", () => { @@ -137,7 +137,7 @@ describe("ProfileButtonComponent", () => { click(clickableEl); expect(contextMenuServiceSpy.openMenu).toHaveBeenCalled(); const items = contextMenuServiceSpy.lastMenu.items; - expect(items.length).toEqual(12); + expect(items.length).toEqual(13); expect(items[0] instanceof ContextMenuItem).toBe(true); expect((items[0] as ContextMenuItem).label).toEqual("Check for updates"); @@ -147,30 +147,33 @@ describe("ProfileButtonComponent", () => { expect(items[2] instanceof ContextMenuItem).toBe(true); expect((items[2] as ContextMenuItem).label).toEqual("profile-button.settings"); - expect(items[3] instanceof MultiContextMenuItem).toBe(true); - expect((items[3] as MultiContextMenuItem).label).toEqual("Language (Preview)"); + expect(items[3] instanceof ContextMenuItem).toBe(true); + expect((items[3] as ContextMenuItem).label).toEqual("profile-button.keybindings"); - expect(items[4] instanceof ContextMenuItem).toBe(true); - expect((items[4] as ContextMenuItem).label).toEqual("profile-button.thirdPartyNotices"); + expect(items[4] instanceof MultiContextMenuItem).toBe(true); + expect((items[4] as MultiContextMenuItem).label).toEqual("Language (Preview)"); expect(items[5] instanceof ContextMenuItem).toBe(true); - expect((items[5] as ContextMenuItem).label).toEqual("profile-button.viewLogs"); + expect((items[5] as ContextMenuItem).label).toEqual("profile-button.thirdPartyNotices"); expect(items[6] instanceof ContextMenuItem).toBe(true); - expect((items[6] as ContextMenuItem).label).toEqual("profile-button.report"); + expect((items[6] as ContextMenuItem).label).toEqual("profile-button.viewLogs"); expect(items[7] instanceof ContextMenuItem).toBe(true); - expect((items[7] as ContextMenuItem).label).toEqual("profile-button.about"); + expect((items[7] as ContextMenuItem).label).toEqual("profile-button.report"); - expect(items[8] instanceof ContextMenuSeparator).toBe(true); + expect(items[8] instanceof ContextMenuItem).toBe(true); + expect((items[8] as ContextMenuItem).label).toEqual("profile-button.about"); - expect(items[9] instanceof ContextMenuItem).toBe(true); - expect((items[9] as ContextMenuItem).label).toEqual("profile-button.viewTheme"); + expect(items[9] instanceof ContextMenuSeparator).toBe(true); - expect(items[10] instanceof ContextMenuSeparator).toBe(true); + expect(items[10] instanceof ContextMenuItem).toBe(true); + expect((items[10] as ContextMenuItem).label).toEqual("profile-button.viewTheme"); - expect(items[11] instanceof ContextMenuItem).toBe(true); - expect((items[11] as ContextMenuItem).label).toEqual("profile-button.logout"); + expect(items[11] instanceof ContextMenuSeparator).toBe(true); + + expect(items[12] instanceof ContextMenuItem).toBe(true); + expect((items[12] as ContextMenuItem).label).toEqual("profile-button.logout"); }); it("check for updates and show update notification when there is one", fakeAsync(() => { diff --git a/src/app/components/layout/main-navigation/profile-button/profile-button.component.ts b/src/app/components/layout/main-navigation/profile-button/profile-button.component.ts index 5b0c79dfec..f4aa3a3def 100644 --- a/src/app/components/layout/main-navigation/profile-button/profile-button.component.ts +++ b/src/app/components/layout/main-navigation/profile-button/profile-button.component.ts @@ -89,6 +89,9 @@ export class ProfileButtonComponent implements OnDestroy, OnInit { const items = [ new ContextMenuSeparator(), new ContextMenuItem({ label: this.i18n.t("profile-button.settings"), click: () => this._goToSettings() }), + new ContextMenuItem({ + label: this.i18n.t("profile-button.keybindings"), click: () => this._goToKeyBindings(), + }), new MultiContextMenuItem({ label: "Language (Preview)", subitems: Object.entries(TranslatedLocales).map(([key, value]) => { return new ContextMenuItem({ label: value, click: () => this._changeLanguage(key as Locale) }); @@ -118,6 +121,10 @@ export class ProfileButtonComponent implements OnDestroy, OnInit { this.router.navigate(["/settings"]); } + private _goToKeyBindings() { + this.router.navigate(["/keybindings"]); + } + private _changeLanguage(locale: Locale) { this.localeService.setLocale(locale); } diff --git a/src/app/components/layout/main-navigation/profile-button/profile-button.i18n.yml b/src/app/components/layout/main-navigation/profile-button/profile-button.i18n.yml index 88af762e90..8e1941347f 100644 --- a/src/app/components/layout/main-navigation/profile-button/profile-button.i18n.yml +++ b/src/app/components/layout/main-navigation/profile-button/profile-button.i18n.yml @@ -1,6 +1,7 @@ profile-button: profile: Profile settings: Settings + keybindings: Keybindings about: About thirdPartyNotices: Third party notices viewLogs: View logs