From a327c1d6d7bf0076a35f07145cf39453f240089f Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 29 Mar 2019 10:36:58 -0700 Subject: [PATCH 1/6] Show bindings and filter --- .../command-registry/command-registry.ts | 1 + .../keybindings/keybindings.service.spec.ts | 3 + .../keybindings/keybindings.service.ts | 22 ++-- src/@batch-flask/ui/index.ts | 1 + src/@batch-flask/ui/keybindings/index.ts | 2 + .../ui/keybindings/keybindings.component.ts | 118 ++++++++++++++++++ .../ui/keybindings/keybindings.html | 28 +++++ .../ui/keybindings/keybindings.module.ts | 18 +++ .../ui/keybindings/keybindings.scss | 22 ++++ src/app/app.module.ts | 3 +- src/app/app.routes.ts | 5 + src/app/commands/account.appcmd.ts | 1 + src/app/commands/data.appcmd.ts | 1 + src/app/commands/job.appcmd.ts | 1 + src/app/commands/list.appcmd.ts | 2 + src/app/commands/pool.appcmd.ts | 1 + .../profile-button.component.ts | 7 ++ .../profile-button/profile-button.i18n.yml | 1 + 18 files changed, 228 insertions(+), 9 deletions(-) create mode 100644 src/@batch-flask/ui/keybindings/index.ts create mode 100644 src/@batch-flask/ui/keybindings/keybindings.component.ts create mode 100644 src/@batch-flask/ui/keybindings/keybindings.html create mode 100644 src/@batch-flask/ui/keybindings/keybindings.module.ts create mode 100644 src/@batch-flask/ui/keybindings/keybindings.scss 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..ef80d1b22e 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,15 +56,18 @@ 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); + if (map.has(binding.hash)) { + map.get(binding.hash)!.push(command); } else { - this._keyBindings.set(binding.hash, [command]); + map.set(binding.hash, [command]); } } + + this._keyBindings.next(map); } } 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.ts b/src/@batch-flask/ui/keybindings/keybindings.component.ts new file mode 100644 index 0000000000..f5826e2c84 --- /dev/null +++ b/src/@batch-flask/ui/keybindings/keybindings.component.ts @@ -0,0 +1,118 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from "@angular/core"; +import { FormControl } from "@angular/forms"; +import { Command, CommandRegistry, KeyBindingsService } from "@batch-flask/core"; +import { Subject, combineLatest } from "rxjs"; +import { filter, map, startWith, takeUntil } from "rxjs/operators"; + +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(""); + 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); + console.log("MAtch", match); + if (match) { + return { + binding: match[1], + }; + } + 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 !== 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..ec536462af --- /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.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 From 1617e1e19b5861a48c5c593f9caf0a434c4cbca5 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 29 Mar 2019 10:54:18 -0700 Subject: [PATCH 2/6] Tweaks --- .../commands/keybindings/keybindings.service.ts | 13 ++++++------- .../ui/keybindings/keybindings.component.ts | 13 +++++++++---- src/@batch-flask/ui/keybindings/keybindings.html | 2 +- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/@batch-flask/core/commands/keybindings/keybindings.service.ts b/src/@batch-flask/core/commands/keybindings/keybindings.service.ts index ef80d1b22e..a0e269907d 100644 --- a/src/@batch-flask/core/commands/keybindings/keybindings.service.ts +++ b/src/@batch-flask/core/commands/keybindings/keybindings.service.ts @@ -59,7 +59,7 @@ export class KeyBindingsService { const map = new Map(); const commands = CommandRegistry.getCommands(); for (const command of commands) { - const binding = parseKeyBinding(command.binding); + const binding = KeyBinding.parse(command.binding); if (map.has(binding.hash)) { map.get(binding.hash)!.push(command); } else { @@ -71,16 +71,15 @@ export class KeyBindingsService { } } -export function parseKeyBinding(value: string): KeyBinding { - const keys = value.replace(/ /g, "").toLowerCase().split("+"); - return new KeyBinding(keys); -} - 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/keybindings/keybindings.component.ts b/src/@batch-flask/ui/keybindings/keybindings.component.ts index f5826e2c84..5f821e60c2 100644 --- a/src/@batch-flask/ui/keybindings/keybindings.component.ts +++ b/src/@batch-flask/ui/keybindings/keybindings.component.ts @@ -1,8 +1,9 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from "@angular/core"; import { FormControl } from "@angular/forms"; -import { Command, CommandRegistry, KeyBindingsService } from "@batch-flask/core"; +import { Command, CommandRegistry, KeyBinding, KeyBindingsService } from "@batch-flask/core"; import { Subject, combineLatest } from "rxjs"; -import { filter, map, startWith, takeUntil } from "rxjs/operators"; +import { map, startWith, takeUntil } from "rxjs/operators"; +import { TableConfig } from "../table"; import "./keybindings.scss"; @@ -36,6 +37,11 @@ export class KeyBindingsComponent implements OnInit, OnDestroy { public displayedCommands: DisplayedCommand[] = []; public search = new FormControl(""); + + public tableConfig: TableConfig = { + activable: false, + }; + private _destroy = new Subject(); constructor(private keybindingService: KeyBindingsService, private changeDetector: ChangeDetectorRef) { @@ -89,10 +95,9 @@ export class KeyBindingsComponent implements OnInit, OnDestroy { private _processSearch(query: string): KeyBindingFilter { const trimed = query.trim(); const match = SEARCH_BY_BINDING_REGEX.exec(trimed); - console.log("MAtch", match); if (match) { return { - binding: match[1], + binding: KeyBinding.parse(match[1]).hash, }; } return { diff --git a/src/@batch-flask/ui/keybindings/keybindings.html b/src/@batch-flask/ui/keybindings/keybindings.html index ec536462af..1d4e2d81b8 100644 --- a/src/@batch-flask/ui/keybindings/keybindings.html +++ b/src/@batch-flask/ui/keybindings/keybindings.html @@ -5,7 +5,7 @@
- +
Command
{{command.description}}
From 9d346eef19302fbdaae0dcc185b9deb8407b2d50 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 29 Mar 2019 11:02:11 -0700 Subject: [PATCH 3/6] Tweaks --- src/@batch-flask/ui/keybindings/keybindings.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/@batch-flask/ui/keybindings/keybindings.component.ts b/src/@batch-flask/ui/keybindings/keybindings.component.ts index 5f821e60c2..f69dd7abdf 100644 --- a/src/@batch-flask/ui/keybindings/keybindings.component.ts +++ b/src/@batch-flask/ui/keybindings/keybindings.component.ts @@ -112,7 +112,7 @@ export class KeyBindingsComponent implements OnInit, OnDestroy { return false; } } - if (filter.binding && command.binding !== filter.binding) { + if (filter.binding && !command.binding.includes(filter.binding)) { return false; } From 018a0e4b30a688dcdd3ac2e9d2cd6d3c1e272df3 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 29 Mar 2019 11:29:55 -0700 Subject: [PATCH 4/6] Tweaks --- karma.conf.js | 2 +- .../core/commands/command-registry/command-registry.spec.ts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) 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, From c7334790190d6cd16950c1a97eaf774364c22a0c Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 29 Mar 2019 11:30:15 -0700 Subject: [PATCH 5/6] Added specs --- .../keybindings/keybindings.component.spec.ts | 166 ++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 src/@batch-flask/ui/keybindings/keybindings.component.spec.ts 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"); + }); +}); From 9b3d91a57c8ae19b1a4f8748aa8428e3e077ebfc Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 29 Mar 2019 12:26:19 -0700 Subject: [PATCH 6/6] Updated specs --- .../profile-button.component.spec.ts | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) 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(() => {