From 7fc7ed2ba661d8d1f4fad7c428d2fc256026da2b Mon Sep 17 00:00:00 2001 From: seokju-na Date: Thu, 6 Dec 2018 22:30:44 +0900 Subject: [PATCH 01/20] Update theme service to use workspace database Add theme updating stream that can be consumed by component. Move ThemeService from 'ui/' to 'shared/' --- src/browser/app/app.component.ts | 8 ++-- src/browser/shared/index.ts | 1 + src/browser/shared/shared.module.ts | 2 + src/browser/shared/theme.service.ts | 44 +++++++++++++++++++ src/browser/shared/workspace-database.ts | 4 +- src/browser/ui/style/color-and-theme.ts | 8 ++++ src/browser/ui/style/index.ts | 2 - src/browser/ui/style/style.module.ts | 13 ------ src/browser/ui/style/theme.service.ts | 35 --------------- src/browser/ui/ui.module.ts | 2 - .../wizard-choosing.component.spec.ts | 8 +++- .../wizard-choosing.component.ts | 28 +++++------- src/browser/wizard/wizard.component.spec.ts | 8 ++-- src/browser/wizard/wizard.component.ts | 8 ++-- 14 files changed, 87 insertions(+), 84 deletions(-) create mode 100644 src/browser/shared/theme.service.ts delete mode 100644 src/browser/ui/style/style.module.ts delete mode 100644 src/browser/ui/style/theme.service.ts diff --git a/src/browser/app/app.component.ts b/src/browser/app/app.component.ts index 5ef5845c..136a1e99 100644 --- a/src/browser/app/app.component.ts +++ b/src/browser/app/app.component.ts @@ -5,8 +5,8 @@ import { filter, map } from 'rxjs/operators'; import { NoteFinderComponent } from '../note/note-collection'; import { NoteCollectionService } from '../note/note-collection/note-collection.service'; import { ChangeViewModeAction, NoteEditorViewModes } from '../note/note-editor'; -import { MenuEvent, MenuService, WORKSPACE_DATABASE, WorkspaceDatabase } from '../shared'; -import { Themes, ThemeService } from '../ui/style'; +import { MenuEvent, MenuService, ThemeService, WORKSPACE_DATABASE, WorkspaceDatabase } from '../shared'; +import { defaultTheme, Themes } from '../ui/style'; import { VcsManagerComponent, VcsService } from '../vcs'; import { AppLayoutSidenavOutlet, ToggleSidenavPanelAction } from './app-layout'; import { AppStateWithFeatures } from './app.state'; @@ -65,9 +65,9 @@ export class AppComponent implements OnInit { ) { const _theme = workspaceDB.cachedInfo ? workspaceDB.cachedInfo.theme as Themes - : ThemeService.defaultTheme; + : defaultTheme; - theme.setTheme(_theme); + theme.applyThemeToHtml(_theme); workspaceDB.update({ theme: _theme }); } diff --git a/src/browser/shared/index.ts b/src/browser/shared/index.ts index b754e389..c8abcd8f 100644 --- a/src/browser/shared/index.ts +++ b/src/browser/shared/index.ts @@ -5,3 +5,4 @@ export * from './workspace.service'; export * from './git.service'; export * from './menu.service'; export * from './native-dialog'; +export * from './theme.service'; diff --git a/src/browser/shared/shared.module.ts b/src/browser/shared/shared.module.ts index 3635a811..63c4e980 100644 --- a/src/browser/shared/shared.module.ts +++ b/src/browser/shared/shared.module.ts @@ -6,6 +6,7 @@ import { FsService } from './fs.service'; import { GitService } from './git.service'; import { MenuService } from './menu.service'; import { NativeDialog } from './native-dialog'; +import { ThemeService } from './theme.service'; import { WorkspaceDatabaseProvider } from './workspace-database'; import { WorkspaceService } from './workspace.service'; @@ -23,6 +24,7 @@ import { WorkspaceService } from './workspace.service'; GitService, MenuService, NativeDialog, + ThemeService, ], exports: [ ConfirmDialogModule, diff --git a/src/browser/shared/theme.service.ts b/src/browser/shared/theme.service.ts new file mode 100644 index 00000000..59af0018 --- /dev/null +++ b/src/browser/shared/theme.service.ts @@ -0,0 +1,44 @@ +import { Inject, Injectable, OnDestroy } from '@angular/core'; +import { from, Subject, Subscription } from 'rxjs'; +import { debounceTime, distinctUntilChanged, switchMap, tap } from 'rxjs/operators'; +import { WORKSPACE_DATABASE, WorkspaceDatabase } from './workspace-database'; +import { Themes } from '../ui/style'; + + +@Injectable() +export class ThemeService implements OnDestroy { + readonly setTheme = new Subject(); + private readonly setThemeSubscription: Subscription; + + constructor( + @Inject(WORKSPACE_DATABASE) private workspaceDB: WorkspaceDatabase, + ) { + this.setThemeSubscription = this.setTheme.asObservable().pipe( + distinctUntilChanged(), + debounceTime(50), + tap(theme => this.applyThemeToHtml(theme)), + switchMap(theme => from(this.workspaceDB.update({ theme }))), + ).subscribe(); + } + + private _currentTheme: Themes | null = null; + + get currentTheme(): Themes | null { + return this._currentTheme; + } + + ngOnDestroy(): void { + this.setThemeSubscription.unsubscribe(); + } + + applyThemeToHtml(theme: Themes): void { + const elem: HTMLElement = document.getElementsByTagName('html')[0]; + + if (this._currentTheme && elem.classList.contains(this._currentTheme)) { + elem.classList.remove(this._currentTheme); + } + + this._currentTheme = theme; + elem.classList.add(this._currentTheme); + } +} diff --git a/src/browser/shared/workspace-database.ts b/src/browser/shared/workspace-database.ts index 0e3c0981..e4a45dec 100644 --- a/src/browser/shared/workspace-database.ts +++ b/src/browser/shared/workspace-database.ts @@ -2,7 +2,7 @@ import { InjectionToken, Provider } from '@angular/core'; import Dexie from 'dexie'; import { Database } from '../../core/database'; import { WorkspaceInfo } from '../../core/workspace'; -import { ThemeService } from '../ui/style'; +import { defaultTheme } from '../ui/style'; export const WORKSPACE_INFO_ID = 1; @@ -28,7 +28,7 @@ export class WorkspaceDatabase extends Database { if (!info) { info = { id: WORKSPACE_INFO_ID, - theme: ThemeService.defaultTheme, + theme: defaultTheme, }; await this.info.add(info); diff --git a/src/browser/ui/style/color-and-theme.ts b/src/browser/ui/style/color-and-theme.ts index aac140b4..540af7ef 100644 --- a/src/browser/ui/style/color-and-theme.ts +++ b/src/browser/ui/style/color-and-theme.ts @@ -1,3 +1,6 @@ +import { remote } from 'electron'; + + export type ColorTheme = 'normal' | 'primary' | 'warn'; @@ -5,3 +8,8 @@ export enum Themes { BASIC_LIGHT_THEME = 'BasicLightTheme', BASIC_DARK_THEME = 'BasicDarkTheme', } + + +export const defaultTheme = remote.systemPreferences.isDarkMode() + ? Themes.BASIC_DARK_THEME + : Themes.BASIC_LIGHT_THEME; diff --git a/src/browser/ui/style/index.ts b/src/browser/ui/style/index.ts index 417be88b..e032d2b1 100644 --- a/src/browser/ui/style/index.ts +++ b/src/browser/ui/style/index.ts @@ -1,3 +1 @@ -export * from './style.module'; export * from './color-and-theme'; -export * from './theme.service'; diff --git a/src/browser/ui/style/style.module.ts b/src/browser/ui/style/style.module.ts deleted file mode 100644 index ef8a2238..00000000 --- a/src/browser/ui/style/style.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { ThemeService } from './theme.service'; - - -@NgModule({ - imports: [CommonModule], - providers: [ - ThemeService, - ], -}) -export class StyleModule { -} diff --git a/src/browser/ui/style/theme.service.ts b/src/browser/ui/style/theme.service.ts deleted file mode 100644 index c3d33aca..00000000 --- a/src/browser/ui/style/theme.service.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { DOCUMENT } from '@angular/common'; -import { Inject, Injectable } from '@angular/core'; -import { remote } from 'electron'; -import { Themes } from './color-and-theme'; - - -const { systemPreferences } = remote; - - -@Injectable() -export class ThemeService { - static readonly defaultTheme = systemPreferences.isDarkMode() - ? Themes.BASIC_DARK_THEME - : Themes.BASIC_LIGHT_THEME; - - constructor(@Inject(DOCUMENT) private document: any) { - } - - private _currentTheme: Themes | null = null; - - get currentTheme(): Themes | null { - return this._currentTheme; - } - - setTheme(theme: Themes): void { - const elem: HTMLElement = this.document.getElementsByTagName('html')[0]; - - if (this._currentTheme && elem.classList.contains(this._currentTheme)) { - elem.classList.remove(this._currentTheme); - } - - this._currentTheme = theme; - elem.classList.add(this._currentTheme); - } -} diff --git a/src/browser/ui/ui.module.ts b/src/browser/ui/ui.module.ts index 3aa35fa3..91561bbc 100644 --- a/src/browser/ui/ui.module.ts +++ b/src/browser/ui/ui.module.ts @@ -18,7 +18,6 @@ import { RadioModule } from './radio'; import { ResizableModule } from './resizable'; import { ScrollingModule } from './scrolling'; import { SpinnerModule } from './spinner'; -import { StyleModule } from './style'; import { TabsModule } from './tabs'; import { TextFieldModule } from './text-field'; import { TitleBarModule } from './title-bar'; @@ -48,7 +47,6 @@ const UI_MODULES = [ SpinnerModule, TooltipModule, RadioModule, - StyleModule, ButtonToggleModule, LineModule, TextFieldModule, diff --git a/src/browser/wizard/wizard-choosing/wizard-choosing.component.spec.ts b/src/browser/wizard/wizard-choosing/wizard-choosing.component.spec.ts index 206da5e7..8a5b439d 100644 --- a/src/browser/wizard/wizard-choosing/wizard-choosing.component.spec.ts +++ b/src/browser/wizard/wizard-choosing/wizard-choosing.component.spec.ts @@ -7,11 +7,11 @@ import { of, throwError } from 'rxjs'; import { fastTestSetup, NoopComponent, NoopModule, sample, sampleWithout } from '../../../../test/helpers'; import { MockDialog } from '../../../../test/mocks/browser'; import { WorkspaceError, WorkspaceErrorCodes, WorkspaceInfo } from '../../../core/workspace'; -import { SharedModule, WORKSPACE_DATABASE, WorkspaceDatabase, WorkspaceService } from '../../shared'; +import { SharedModule, ThemeService, WORKSPACE_DATABASE, WorkspaceDatabase, WorkspaceService } from '../../shared'; import { ConfirmDialogComponent, ConfirmDialogData } from '../../shared/confirm-dialog'; import { ButtonToggleComponent } from '../../ui/button-toggle'; import { Dialog } from '../../ui/dialog'; -import { Themes, ThemeService } from '../../ui/style'; +import { Themes } from '../../ui/style'; import { UiModule } from '../../ui/ui.module'; import { WizardChoosingComponent } from './wizard-choosing.component'; @@ -79,6 +79,10 @@ describe('browser.wizard.wizardChoosing.WizardChoosingComponent', () => { component = fixture.componentInstance; }); + afterEach(async () => { + await workspaceDB.info.clear(); + }); + describe('create new workspace', () => { it('should init workspace when click create new workspace button.', fakeAsync(() => { fixture.detectChanges(); diff --git a/src/browser/wizard/wizard-choosing/wizard-choosing.component.ts b/src/browser/wizard/wizard-choosing/wizard-choosing.component.ts index ae888218..f2572ae2 100644 --- a/src/browser/wizard/wizard-choosing/wizard-choosing.component.ts +++ b/src/browser/wizard/wizard-choosing/wizard-choosing.component.ts @@ -1,12 +1,11 @@ -import { Component, Inject, OnInit } from '@angular/core'; +import { Component, OnDestroy, OnInit } from '@angular/core'; import { FormControl } from '@angular/forms'; -import { from } from 'rxjs'; -import { debounceTime, distinctUntilChanged, switchMap, tap } from 'rxjs/operators'; +import { Subscription } from 'rxjs'; import { environment } from '../../../core/environment'; import { WorkspaceError } from '../../../core/workspace'; -import { WORKSPACE_DATABASE, WorkspaceDatabase, WorkspaceService } from '../../shared'; +import { ThemeService, WorkspaceService } from '../../shared'; import { ConfirmDialog } from '../../shared/confirm-dialog'; -import { Themes, ThemeService } from '../../ui/style'; +import { Themes } from '../../ui/style'; @Component({ @@ -14,7 +13,7 @@ import { Themes, ThemeService } from '../../ui/style'; templateUrl: './wizard-choosing.component.html', styleUrls: ['./wizard-choosing.component.scss'], }) -export class WizardChoosingComponent implements OnInit { +export class WizardChoosingComponent implements OnInit, OnDestroy { readonly appVersion = environment.version; readonly themeFormControl = new FormControl(); @@ -23,25 +22,22 @@ export class WizardChoosingComponent implements OnInit { { name: 'Dark Theme', value: Themes.BASIC_DARK_THEME }, ]; + private themeSetSubscription = Subscription.EMPTY; + constructor( private theme: ThemeService, private workspace: WorkspaceService, - @Inject(WORKSPACE_DATABASE) private workspaceDB: WorkspaceDatabase, private confirmDialog: ConfirmDialog, ) { } ngOnInit(): void { this.themeFormControl.patchValue(this.theme.currentTheme); - this.themeFormControl.valueChanges.pipe( - tap(theme => this.theme.setTheme(theme)), - distinctUntilChanged(), - // Saving value in database cost IO. We need to throttle events. - debounceTime(50), - switchMap(theme => - from(this.workspaceDB.update({ theme })), - ), - ).subscribe(); + this.themeSetSubscription = this.themeFormControl.valueChanges.subscribe(this.theme.setTheme); + } + + ngOnDestroy(): void { + this.themeSetSubscription.unsubscribe(); } createNewWorkspace(): void { diff --git a/src/browser/wizard/wizard.component.spec.ts b/src/browser/wizard/wizard.component.spec.ts index d75f936b..d5cd6f2a 100644 --- a/src/browser/wizard/wizard.component.spec.ts +++ b/src/browser/wizard/wizard.component.spec.ts @@ -1,8 +1,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { fastTestSetup, sample } from '../../../test/helpers'; -import { SharedModule, WORKSPACE_DATABASE, WorkspaceDatabase } from '../shared'; -import { Themes, ThemeService } from '../ui/style'; +import { SharedModule, ThemeService, WORKSPACE_DATABASE, WorkspaceDatabase } from '../shared'; +import { Themes } from '../ui/style'; import { UiModule } from '../ui/ui.module'; import { WizardComponent } from './wizard.component'; @@ -42,7 +42,7 @@ describe('browser.wizard.WizardComponent', () => { }); it('should set and update theme form workspace database cache if it\'s exists.', () => { - spyOn(theme, 'setTheme'); + spyOn(theme, 'applyThemeToHtml'); spyOn(workspaceDB, 'update'); const _theme = sample(Themes); @@ -50,7 +50,7 @@ describe('browser.wizard.WizardComponent', () => { createFixture(); - expect(theme.setTheme).toHaveBeenCalledWith(_theme); + expect(theme.applyThemeToHtml).toHaveBeenCalledWith(_theme); expect(workspaceDB.update).toHaveBeenCalledWith({ theme: _theme }); }); }); diff --git a/src/browser/wizard/wizard.component.ts b/src/browser/wizard/wizard.component.ts index 53c1341e..333f10b2 100644 --- a/src/browser/wizard/wizard.component.ts +++ b/src/browser/wizard/wizard.component.ts @@ -1,6 +1,6 @@ import { Component, Inject } from '@angular/core'; -import { WORKSPACE_DATABASE, WorkspaceDatabase } from '../shared'; -import { Themes, ThemeService } from '../ui/style'; +import { ThemeService, WORKSPACE_DATABASE, WorkspaceDatabase } from '../shared'; +import { defaultTheme, Themes } from '../ui/style'; @Component({ @@ -15,9 +15,9 @@ export class WizardComponent { ) { const _theme = workspaceDB.cachedInfo ? workspaceDB.cachedInfo.theme as Themes - : ThemeService.defaultTheme; + : defaultTheme; - theme.setTheme(_theme); + theme.applyThemeToHtml(_theme); workspaceDB.update({ theme: _theme }); } } From e5af881f16f59df816fa42ab1cbb269860f87a71 Mon Sep 17 00:00:00 2001 From: seokju-na Date: Mon, 10 Dec 2018 21:05:17 +0900 Subject: [PATCH 02/20] Update host element display property Took 3 hours 24 minutes --- src/browser/ui/tabs/tab-group.component.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/src/browser/ui/tabs/tab-group.component.scss b/src/browser/ui/tabs/tab-group.component.scss index 909efd3b..3e86b859 100644 --- a/src/browser/ui/tabs/tab-group.component.scss +++ b/src/browser/ui/tabs/tab-group.component.scss @@ -3,6 +3,7 @@ .TabGroup { position: relative; + display: block; &__list { margin: 0; From 0933a23af0c814e50a719756267c3680515d1004 Mon Sep 17 00:00:00 2001 From: seokju-na Date: Tue, 11 Dec 2018 03:22:38 +0900 Subject: [PATCH 03/20] #39 Add settings registry --- src/browser/settings/settings-context.ts | 8 +++ .../settings/settings-registry.spec.ts | 55 +++++++++++++++++++ src/browser/settings/settings-registry.ts | 25 +++++++++ 3 files changed, 88 insertions(+) create mode 100644 src/browser/settings/settings-context.ts create mode 100644 src/browser/settings/settings-registry.spec.ts create mode 100644 src/browser/settings/settings-registry.ts diff --git a/src/browser/settings/settings-context.ts b/src/browser/settings/settings-context.ts new file mode 100644 index 00000000..6862a9e3 --- /dev/null +++ b/src/browser/settings/settings-context.ts @@ -0,0 +1,8 @@ +import { Type } from '@angular/core'; + + +export class SettingsContext { + id: string; + tabName: string; + component: Type; +} diff --git a/src/browser/settings/settings-registry.spec.ts b/src/browser/settings/settings-registry.spec.ts new file mode 100644 index 00000000..47095fea --- /dev/null +++ b/src/browser/settings/settings-registry.spec.ts @@ -0,0 +1,55 @@ +import { TestBed } from '@angular/core/testing'; +import { fastTestSetup, NoopComponent, NoopModule } from '../../../test/helpers'; +import { SettingsContext } from './settings-context'; +import { SETTINGS_REGISTRATION, SettingsRegistry } from './settings-registry'; + + +describe('browser.settings.SettingsRegistry', () => { + let registry: SettingsRegistry; + const registration: SettingsContext[] = [ + { + id: 'settings-1', + tabName: 'Settings1', + component: NoopComponent, + }, + { + id: 'settings-2', + tabName: 'Settings2', + component: NoopComponent, + }, + { + id: 'settings-3', + tabName: 'Settings3', + component: NoopComponent, + }, + ]; + + fastTestSetup(); + + beforeAll(() => { + TestBed.configureTestingModule({ + imports: [ + NoopModule, + ], + providers: [ + { provide: SETTINGS_REGISTRATION, useValue: registration }, + SettingsRegistry, + ], + }); + }); + + beforeEach(() => { + registry = TestBed.get(SettingsRegistry); + }); + + afterEach(() => { + registry.ngOnDestroy(); + }); + + describe('getSettings', () => { + it('should return settings by sorted order.', () => { + const contexts = registry.getSettings(); + expect(contexts).toEqual(registration); + }); + }); +}); diff --git a/src/browser/settings/settings-registry.ts b/src/browser/settings/settings-registry.ts new file mode 100644 index 00000000..0ec9f6d3 --- /dev/null +++ b/src/browser/settings/settings-registry.ts @@ -0,0 +1,25 @@ +import { Inject, Injectable, InjectionToken, OnDestroy } from '@angular/core'; +import { SettingsContext } from './settings-context'; + + +export const SETTINGS_REGISTRATION = new InjectionToken[]>('SettingsRegistration'); + + +@Injectable() +export class SettingsRegistry implements OnDestroy { + readonly settingsMap = new Map>(); + + constructor(@Inject(SETTINGS_REGISTRATION) registration: SettingsContext[]) { + registration.forEach((context) => { + this.settingsMap.set(context.id, context); + }); + } + + ngOnDestroy(): void { + this.settingsMap.clear(); + } + + getSettings(): SettingsContext[] { + return [...this.settingsMap.values()]; + } +} From 3c2075a9321f1f140baa7393fdb3b10c33f2ebb4 Mon Sep 17 00:00:00 2001 From: seokju-na Date: Tue, 11 Dec 2018 03:23:10 +0900 Subject: [PATCH 04/20] #39 Add settings module --- src/browser/app.scss | 3 + src/browser/settings/_all-theme.scss | 5 + src/browser/settings/index.ts | 5 + .../_settings-dialog-theme.scss | 11 ++ .../settings-dialog/settings-dialog-data.ts | 3 + .../settings-dialog.component.html | 17 +++ .../settings-dialog.component.scss | 14 +++ .../settings-dialog.component.spec.ts | 108 ++++++++++++++++++ .../settings-dialog.component.ts | 45 ++++++++ .../settings-dialog/settings-dialog.ts | 25 ++++ src/browser/settings/settings.module.ts | 23 ++++ 11 files changed, 259 insertions(+) create mode 100644 src/browser/settings/_all-theme.scss create mode 100644 src/browser/settings/index.ts create mode 100644 src/browser/settings/settings-dialog/_settings-dialog-theme.scss create mode 100644 src/browser/settings/settings-dialog/settings-dialog-data.ts create mode 100644 src/browser/settings/settings-dialog/settings-dialog.component.html create mode 100644 src/browser/settings/settings-dialog/settings-dialog.component.scss create mode 100644 src/browser/settings/settings-dialog/settings-dialog.component.spec.ts create mode 100644 src/browser/settings/settings-dialog/settings-dialog.component.ts create mode 100644 src/browser/settings/settings-dialog/settings-dialog.ts create mode 100644 src/browser/settings/settings.module.ts diff --git a/src/browser/app.scss b/src/browser/app.scss index 380aaa3e..024e9e90 100644 --- a/src/browser/app.scss +++ b/src/browser/app.scss @@ -10,6 +10,7 @@ @import "app/all-theme"; @import "note/all-theme"; @import "vcs/all-theme"; +@import "settings/all-theme"; @include ui-core(); @@ -18,6 +19,7 @@ @include gd-ui-all-theme($basic-light-theme); @include gd-note-all-theme($basic-light-theme); @include gd-vcs-all-theme($basic-light-theme); + @include gd-settings-all-theme($basic-light-theme); } .BasicDarkTheme { @@ -25,4 +27,5 @@ @include gd-ui-all-theme($basic-dark-theme); @include gd-note-all-theme($basic-dark-theme); @include gd-vcs-all-theme($basic-dark-theme); + @include gd-settings-all-theme($basic-dark-theme); } diff --git a/src/browser/settings/_all-theme.scss b/src/browser/settings/_all-theme.scss new file mode 100644 index 00000000..48cef7d9 --- /dev/null +++ b/src/browser/settings/_all-theme.scss @@ -0,0 +1,5 @@ +@import "./settings-dialog/settings-dialog-theme"; + +@mixin gd-settings-all-theme($theme) { + @include gd-settings-dialog-theme($theme); +} diff --git a/src/browser/settings/index.ts b/src/browser/settings/index.ts new file mode 100644 index 00000000..386ad3b0 --- /dev/null +++ b/src/browser/settings/index.ts @@ -0,0 +1,5 @@ +export * from './settings.module'; +export * from './settings-context'; +export * from './settings-registry'; +export * from './settings-dialog/settings-dialog.component'; +export * from './settings-dialog/settings-dialog'; diff --git a/src/browser/settings/settings-dialog/_settings-dialog-theme.scss b/src/browser/settings/settings-dialog/_settings-dialog-theme.scss new file mode 100644 index 00000000..33c8af6c --- /dev/null +++ b/src/browser/settings/settings-dialog/_settings-dialog-theme.scss @@ -0,0 +1,11 @@ +@import "../../ui/style/theming"; + +@mixin gd-settings-dialog-theme($theme) { + $foreground: map-get($theme, foreground); + + .SettingsDialog { + &__content > gd-tab-group { + border-bottom: 1px solid gd-color($foreground, divider); + } + } +} diff --git a/src/browser/settings/settings-dialog/settings-dialog-data.ts b/src/browser/settings/settings-dialog/settings-dialog-data.ts new file mode 100644 index 00000000..94262c55 --- /dev/null +++ b/src/browser/settings/settings-dialog/settings-dialog-data.ts @@ -0,0 +1,3 @@ +export interface SettingsDialogData { + initialSettingId?: string; +} diff --git a/src/browser/settings/settings-dialog/settings-dialog.component.html b/src/browser/settings/settings-dialog/settings-dialog.component.html new file mode 100644 index 00000000..26c399c2 --- /dev/null +++ b/src/browser/settings/settings-dialog/settings-dialog.component.html @@ -0,0 +1,17 @@ + +

{{ title }}

+ +
+ + + +
+ +
+ +
+
+
+
diff --git a/src/browser/settings/settings-dialog/settings-dialog.component.scss b/src/browser/settings/settings-dialog/settings-dialog.component.scss new file mode 100644 index 00000000..c71c05a9 --- /dev/null +++ b/src/browser/settings/settings-dialog/settings-dialog.component.scss @@ -0,0 +1,14 @@ +@import "../../ui/style/spacing"; + +.SettingsDialog { + &__content { + padding: 0 !important; + } + + &__container { + padding: $spacing; + } + + &__tabContent { + } +} diff --git a/src/browser/settings/settings-dialog/settings-dialog.component.spec.ts b/src/browser/settings/settings-dialog/settings-dialog.component.spec.ts new file mode 100644 index 00000000..e7b0fae4 --- /dev/null +++ b/src/browser/settings/settings-dialog/settings-dialog.component.spec.ts @@ -0,0 +1,108 @@ +import { NgModule } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { fastTestSetup, NoopComponent } from '../../../../test/helpers'; +import { MockDialog, MockDialogRef } from '../../../../test/mocks/browser'; +import { DialogRef } from '../../ui/dialog'; +import { UiModule } from '../../ui/ui.module'; +import { SettingsContext } from '../settings-context'; +import { SETTINGS_REGISTRATION, SettingsRegistry } from '../settings-registry'; +import { SettingsDialogData } from './settings-dialog-data'; +import { SettingsDialogComponent } from './settings-dialog.component'; + + +describe('browser.settings.SettingsDialogComponent', () => { + let component: SettingsDialogComponent; + let fixture: ComponentFixture; + + let mockDialogRef: MockDialogRef; + + let registry: SettingsRegistry; + const registration: SettingsContext[] = [ + { + id: 'settings-1', + tabName: 'Settings1', + component: NoopComponent, + }, + { + id: 'settings-2', + tabName: 'Settings2', + component: NoopComponent, + }, + { + id: 'settings-3', + tabName: 'Settings3', + component: NoopComponent, + }, + ]; + + @NgModule({ + declarations: [NoopComponent], + entryComponents: [NoopComponent], + exports: [NoopComponent], + }) + class NoopModuleWithEntries { + } + + fastTestSetup(); + + beforeAll(async () => { + mockDialogRef = new MockDialogRef(new MockDialog(), SettingsDialogComponent); + + await TestBed + .configureTestingModule({ + imports: [ + UiModule, + NoopModuleWithEntries, + ], + providers: [ + SettingsRegistry, + { provide: SETTINGS_REGISTRATION, useValue: registration }, + { provide: DialogRef, useValue: mockDialogRef }, + ], + declarations: [ + SettingsDialogComponent, + ], + }) + .compileComponents(); + }); + + beforeEach(() => { + registry = TestBed.get(SettingsRegistry); + + fixture = TestBed.createComponent(SettingsDialogComponent); + component = fixture.componentInstance; + component.data = {}; + }); + + afterEach(() => { + registry.ngOnDestroy(); + }); + + it('should select first tab by default.', () => { + fixture.detectChanges(); + expect(component.tabControl.activateTab.value).toEqual('settings-1'); + }); + + it('should select initial setting id if it provided.', () => { + component.data = { initialSettingId: 'settings-3' }; + fixture.detectChanges(); + + expect(component.tabControl.activateTab.value).toEqual('settings-3'); + }); + + it('should close dialog when click close button.', () => { + const closeCallback = jasmine.createSpy('close callback'); + const subscription = mockDialogRef.afterClosed().subscribe(closeCallback); + + fixture.detectChanges(); + + const closeButtonEl = fixture.debugElement.query( + By.css('button#settings-dialog-close-button'), + ).nativeElement as HTMLButtonElement; + closeButtonEl.click(); + + expect(closeCallback).toHaveBeenCalled(); + subscription.unsubscribe(); + }); +}); diff --git a/src/browser/settings/settings-dialog/settings-dialog.component.ts b/src/browser/settings/settings-dialog/settings-dialog.component.ts new file mode 100644 index 00000000..720854df --- /dev/null +++ b/src/browser/settings/settings-dialog/settings-dialog.component.ts @@ -0,0 +1,45 @@ +import { Component, Inject, Injector, OnInit, Optional } from '@angular/core'; +import { __DARWIN__ } from '../../../libs/platform'; +import { DIALOG_DATA, DialogRef } from '../../ui/dialog'; +import { TabControl } from '../../ui/tabs/tab-control'; +import { SettingsContext } from '../settings-context'; +import { SettingsRegistry } from '../settings-registry'; +import { SettingsDialogData } from './settings-dialog-data'; + + +@Component({ + selector: 'gd-settings-dialog', + templateUrl: './settings-dialog.component.html', + styleUrls: ['./settings-dialog.component.scss'], +}) +export class SettingsDialogComponent implements OnInit { + readonly title = __DARWIN__ ? 'Preferences' : 'Settings'; + readonly settingContexts: SettingsContext[]; + readonly tabControl: TabControl; + + constructor( + public _injector: Injector, + @Optional() @Inject(DIALOG_DATA) public data: SettingsDialogData, + private registry: SettingsRegistry, + private dialogRef: DialogRef, + ) { + this.settingContexts = this.registry.getSettings(); + this.tabControl = new TabControl(this.settingContexts.map(context => ({ + id: context.id, + name: context.tabName, + value: context.id, + }))); + } + + ngOnInit(): void { + if (this.data && this.data.initialSettingId) { + this.tabControl.selectTabByValue(this.data.initialSettingId); + } else { + this.tabControl.selectFirstTab(); + } + } + + closeThisDialog(): void { + this.dialogRef.close(); + } +} diff --git a/src/browser/settings/settings-dialog/settings-dialog.ts b/src/browser/settings/settings-dialog/settings-dialog.ts new file mode 100644 index 00000000..ff666821 --- /dev/null +++ b/src/browser/settings/settings-dialog/settings-dialog.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@angular/core'; +import { Dialog, DialogRef } from '../../ui/dialog'; +import { SettingsDialogData } from './settings-dialog-data'; +import { SettingsDialogComponent } from './settings-dialog.component'; + + +@Injectable() +export class SettingsDialog { + constructor(private dialog: Dialog) { + } + + open(data?: SettingsDialogData): DialogRef { + return this.dialog.open( + SettingsDialogComponent, + { + width: '500px', + maxHeight: '75vh', + disableBackdropClickClose: true, + data, + }, + ); + } +} diff --git a/src/browser/settings/settings.module.ts b/src/browser/settings/settings.module.ts new file mode 100644 index 00000000..68ccc305 --- /dev/null +++ b/src/browser/settings/settings.module.ts @@ -0,0 +1,23 @@ +import { NgModule } from '@angular/core'; +import { UiModule } from '../ui/ui.module'; +import { SettingsDialog } from './settings-dialog/settings-dialog'; +import { SettingsDialogComponent } from './settings-dialog/settings-dialog.component'; +import { SettingsRegistry } from './settings-registry'; + + +@NgModule({ + imports: [ + UiModule, + ], + declarations: [SettingsDialogComponent], + entryComponents: [ + SettingsDialogComponent, + ], + providers: [ + SettingsDialog, + SettingsRegistry, + ], + exports: [SettingsDialogComponent], +}) +export class SettingsModule { +} From 1bbe9768c1a2f61e8559ea15ce139a6318e01b65 Mon Sep 17 00:00:00 2001 From: seokju-na Date: Tue, 11 Dec 2018 03:25:17 +0900 Subject: [PATCH 05/20] Add git set remote repository method Took 1 hour 15 minutes --- src/browser/shared/git.service.ts | 8 ++++ src/core/git.ts | 9 +++++ src/main-process/services/git.service.spec.ts | 38 +++++++++++++++++-- src/main-process/services/git.service.ts | 32 +++++++++++++++- 4 files changed, 83 insertions(+), 4 deletions(-) diff --git a/src/browser/shared/git.service.ts b/src/browser/shared/git.service.ts index 4085f60e..06777df1 100644 --- a/src/browser/shared/git.service.ts +++ b/src/browser/shared/git.service.ts @@ -7,6 +7,7 @@ import { GitFindRemoteOptions, GitGetHistoryOptions, GitGetHistoryResult, + GitSetRemoteOptions, GitSyncWithRemoteOptions, GitSyncWithRemoteResult, } from '../../core/git'; @@ -85,6 +86,13 @@ export class GitService implements OnDestroy { )); } + setRemote(options: GitSetRemoteOptions): Observable { + return from(this.ipcClient.performAction( + 'setRemote', + options, + )); + } + syncWithRemote(options: GitSyncWithRemoteOptions): Observable { return from(this.ipcClient.performAction( 'syncWithRemote', diff --git a/src/core/git.ts b/src/core/git.ts index dbb8b3c1..96437c23 100644 --- a/src/core/git.ts +++ b/src/core/git.ts @@ -74,6 +74,7 @@ export function parseGitRemoteUrl(url: string): GitRemoteUrl | null { export enum GitErrorCodes { AUTHENTICATION_FAIL = 'git.authenticationFail', REMOTE_NOT_FOUND = 'git.remoteNotFound', + REMOTE_ALREADY_EXISTS = 'git.remoteAlreadyExists', MERGE_CONFLICTED = 'git.mergeConflicted', NETWORK_ERROR = 'git.networkError', } @@ -84,6 +85,7 @@ export enum GitErrorCodes { export const gitErrorRegexes: { [key: string]: RegExp } = { [GitErrorCodes.AUTHENTICATION_FAIL]: /authentication required/, [GitErrorCodes.REMOTE_NOT_FOUND]: /remote '.*' does not exist/, + [GitErrorCodes.REMOTE_ALREADY_EXISTS]: /remote '.*' already exists/, [GitErrorCodes.NETWORK_ERROR]: /curl error: Could not resolve host:/, }; @@ -198,6 +200,13 @@ export interface GitFindRemoteOptions { } +export interface GitSetRemoteOptions { + workspaceDirPath: string; + remoteName: string; + remoteUrl: string; +} + + export interface GitSyncWithRemoteOptions { workspaceDirPath: string; remoteName: string; diff --git a/src/main-process/services/git.service.spec.ts b/src/main-process/services/git.service.spec.ts index e954999c..fd1ce06a 100644 --- a/src/main-process/services/git.service.spec.ts +++ b/src/main-process/services/git.service.spec.ts @@ -231,11 +231,14 @@ describe('mainProcess.services.GitService', () => { }); }); - // FIXME LATER : Travis CI fails in this test. Ignore for while. - xdescribe('isRemoteExists', () => { + describe('isRemoteExists', () => { + beforeEach(async () => { + await makeTmpPath(true); + }); + it('should return \'false\' if repository has not remote.', async () => { const result = await git.isRemoteExists({ - workspaceDirPath: getFixturePath('origin-not-exists'), + workspaceDirPath: tmpPath, remoteName: 'origin', }); @@ -243,6 +246,35 @@ describe('mainProcess.services.GitService', () => { }); }); + describe('setRemote', () => { + beforeEach(async () => { + await makeTmpPath(true); + }); + + it('should remove exists remote and set new remote.', async () => { + const remoteName = 'origin'; + const prevRemoteUrl = 'previous_remote_url'; + const nextRemoteUrl = 'next_remote_url'; + + // Set previous remote... + const repo = await _git.Repository.open(tmpPath); + await _git.Remote.create(repo, remoteName, prevRemoteUrl); + + await git.setRemote({ + workspaceDirPath: tmpPath, + remoteName, + remoteUrl: nextRemoteUrl, + }); + + // Check changed remote + const remote = await repo.getRemote(remoteName); + expect(remote.url()).to.equals(nextRemoteUrl); + + remote.free(); + repo.free(); + }); + }); + /** * NOTE: This tests require network connection. * If your network is slow, this test is likely to be timed out. diff --git a/src/main-process/services/git.service.ts b/src/main-process/services/git.service.ts index 1dda7d28..68b23c38 100644 --- a/src/main-process/services/git.service.ts +++ b/src/main-process/services/git.service.ts @@ -11,8 +11,10 @@ import { GitFindRemoteOptions, GitGetHistoryOptions, GitGetHistoryResult, - GitMergeConflictedError, GitNetworkError, + GitMergeConflictedError, + GitNetworkError, GitRemoteNotFoundError, + GitSetRemoteOptions, GitSyncWithRemoteOptions, GitSyncWithRemoteResult, } from '../../core/git'; @@ -225,6 +227,34 @@ export class GitService extends Service { } } + @IpcActionHandler('setRemote') + async setRemote(options: GitSetRemoteOptions): Promise { + const { remoteName, remoteUrl } = options; + const repository = await this.openRepository(options.workspaceDirPath); + + // If remote already exists, delete first. + try { + await this.git.Remote.lookup(repository, remoteName); + await this.git.Remote.delete(repository, remoteName); + } catch (error) { + const message = error.message ? error.message : ''; + + // Only remote not found error accepted. Other should be thrown. + if (!gitErrorRegexes[GitErrorCodes.REMOTE_NOT_FOUND].test(message)) { + repository.free(); + throw error; + } + } + + try { + await this.git.Remote.create(repository, remoteName, remoteUrl); + } catch (error) { + throw error; + } finally { + repository.free(); + } + } + @IpcActionHandler('syncWithRemote') async syncWithRemote(options: GitSyncWithRemoteOptions): Promise { const repository = await this.openRepository(options.workspaceDirPath); From fccd31e38ff51fc4b53d8089244030685c196860 Mon Sep 17 00:00:00 2001 From: seokju-na Date: Tue, 11 Dec 2018 03:28:52 +0900 Subject: [PATCH 06/20] Update vcs item background color Took 4 minutes --- .../note/note-shared/note-vcs-item/_note-vcs-item-theme.scss | 3 +++ .../note-shared/note-vcs-item/note-vcs-item.component.scss | 2 ++ .../vcs/vcs-view/base-vcs-item/_base-vcs-item-theme.scss | 5 +++++ 3 files changed, 10 insertions(+) diff --git a/src/browser/note/note-shared/note-vcs-item/_note-vcs-item-theme.scss b/src/browser/note/note-shared/note-vcs-item/_note-vcs-item-theme.scss index 13bf160f..93b71952 100644 --- a/src/browser/note/note-shared/note-vcs-item/_note-vcs-item-theme.scss +++ b/src/browser/note/note-shared/note-vcs-item/_note-vcs-item-theme.scss @@ -3,9 +3,12 @@ @import "./note-vcs-item-sizes"; @mixin gd-note-vcs-item-theme($theme) { + $background: map-get($theme, background); $foreground: map-get($theme, foreground); .NoteVcsItem { + background: gd-color($background, background-highlight); + &__panel { // border-top: 1px solid gd-color($foreground, divider); } diff --git a/src/browser/note/note-shared/note-vcs-item/note-vcs-item.component.scss b/src/browser/note/note-shared/note-vcs-item/note-vcs-item.component.scss index fde27117..2e081acd 100644 --- a/src/browser/note/note-shared/note-vcs-item/note-vcs-item.component.scss +++ b/src/browser/note/note-shared/note-vcs-item/note-vcs-item.component.scss @@ -3,6 +3,8 @@ @import "./note-vcs-item-sizes"; .NoteVcsItem { + display: block; + &__head { padding: 0 $spacing-half; width: 100%; diff --git a/src/browser/vcs/vcs-view/base-vcs-item/_base-vcs-item-theme.scss b/src/browser/vcs/vcs-view/base-vcs-item/_base-vcs-item-theme.scss index b67c4729..c1644e31 100644 --- a/src/browser/vcs/vcs-view/base-vcs-item/_base-vcs-item-theme.scss +++ b/src/browser/vcs/vcs-view/base-vcs-item/_base-vcs-item-theme.scss @@ -1,4 +1,9 @@ +@import "../../../ui/style/theming"; + @mixin gd-vcs-base-vcs-item-theme($theme) { + $background: map-get($theme, background); + .BaseVcsItem { + background: gd-color($background, background-highlight); } } From f9d699173977213fbccaaf12b3adbb93b038d456 Mon Sep 17 00:00:00 2001 From: seokju-na Date: Tue, 11 Dec 2018 03:29:22 +0900 Subject: [PATCH 07/20] Add set remote repository method Took 29 seconds --- src/browser/vcs/vcs.service.spec.ts | 27 ++++++++++++++++++++++++++- src/browser/vcs/vcs.service.ts | 13 ++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/browser/vcs/vcs.service.spec.ts b/src/browser/vcs/vcs.service.spec.ts index d44eb442..e99b0120 100644 --- a/src/browser/vcs/vcs.service.spec.ts +++ b/src/browser/vcs/vcs.service.spec.ts @@ -3,7 +3,7 @@ import { fakeAsync, flush, TestBed } from '@angular/core/testing'; import { Observable, of } from 'rxjs'; import { fastTestSetup } from '../../../test/helpers'; import { VcsAccountDummy } from '../../core/dummies'; -import { GitFindRemoteOptions, GitSyncWithRemoteOptions } from '../../core/git'; +import { GitFindRemoteOptions, GitSetRemoteOptions, GitSyncWithRemoteOptions } from '../../core/git'; import { VcsAccount, VcsAuthenticationInfo, VcsAuthenticationTypes, VcsRemoteRepository } from '../../core/vcs'; import { toPromise } from '../../libs/rx'; import { GitService, SharedModule, WORKSPACE_DEFAULT_CONFIG, WorkspaceConfig } from '../shared'; @@ -245,6 +245,31 @@ describe('browser.vcs.VcsService', () => { }); }); + describe('setRemoteRepository', () => { + it('should save fetch account to account database and call set remote method ' + + 'from git service.', fakeAsync(() => { + const fetchAccount = accountDummy.create(); + const remoteUrl = 'https://github.com/seokju-na/geeks-diary.git'; + + spyOn(accountDB, 'setRepositoryFetchAccountAs').and.callFake(() => Promise.resolve(null)); + spyOn(git, 'setRemote').and.returnValue(of(null)); + + const callback = jasmine.createSpy('set remote repository'); + const subscription = vcs.setRemoteRepository(fetchAccount, remoteUrl).subscribe(callback); + + flush(); + + expect(accountDB.setRepositoryFetchAccountAs).toHaveBeenCalledWith(fetchAccount); + expect(git.setRemote).toHaveBeenCalledWith({ + workspaceDirPath: workspaceConfig.rootDirPath, + remoteName: 'origin', + remoteUrl, + } as GitSetRemoteOptions); + + subscription.unsubscribe(); + })); + }); + describe('syncRepository', () => { it('should call sync method with fetch account.', () => { const fetchAccount = accountDummy.create(); diff --git a/src/browser/vcs/vcs.service.ts b/src/browser/vcs/vcs.service.ts index 960102cb..44270d90 100644 --- a/src/browser/vcs/vcs.service.ts +++ b/src/browser/vcs/vcs.service.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@angular/core'; -import { from, Observable, of } from 'rxjs'; +import { from, Observable, of, zip } from 'rxjs'; import { filter, map, mapTo, switchMap, tap } from 'rxjs/operators'; import { GitGetHistoryOptions, GitSyncWithRemoteResult } from '../../core/git'; import { VcsAccount, VcsCommitItem, VcsFileChange } from '../../core/vcs'; @@ -167,6 +167,17 @@ export class VcsService { return (fetchAccount !== undefined) && isRemoteExists; } + setRemoteRepository(fetchAccount: VcsAccount, remoteUrl: string): Observable { + return zip( + from(this.accountDB.setRepositoryFetchAccountAs(fetchAccount)), + this.git.setRemote({ + workspaceDirPath: this.workspace.configs.rootDirPath, + remoteName: 'origin', + remoteUrl, + }), + ).pipe(mapTo(null)); + } + syncRepository(): Observable { return from(this.accountDB.getRepositoryFetchAccount()).pipe( filter(fetchAccount => fetchAccount !== undefined), From cee050ad0fdd0a78c96641308631b2d8489d87c6 Mon Sep 17 00:00:00 2001 From: seokju-na Date: Tue, 11 Dec 2018 18:12:16 +0900 Subject: [PATCH 08/20] Add app general settings component --- .../general-settings.component.html | 11 +++ .../general-settings.component.scss | 7 ++ .../general-settings.component.spec.ts | 80 +++++++++++++++++++ .../general-settings.component.ts | 33 ++++++++ 4 files changed, 131 insertions(+) create mode 100644 src/browser/app/app-settings/general-settings/general-settings.component.html create mode 100644 src/browser/app/app-settings/general-settings/general-settings.component.scss create mode 100644 src/browser/app/app-settings/general-settings/general-settings.component.spec.ts create mode 100644 src/browser/app/app-settings/general-settings/general-settings.component.ts diff --git a/src/browser/app/app-settings/general-settings/general-settings.component.html b/src/browser/app/app-settings/general-settings/general-settings.component.html new file mode 100644 index 00000000..345592f6 --- /dev/null +++ b/src/browser/app/app-settings/general-settings/general-settings.component.html @@ -0,0 +1,11 @@ +
+ +
+ + + {{ option.name }} + + +
+
diff --git a/src/browser/app/app-settings/general-settings/general-settings.component.scss b/src/browser/app/app-settings/general-settings/general-settings.component.scss new file mode 100644 index 00000000..8dc3d840 --- /dev/null +++ b/src/browser/app/app-settings/general-settings/general-settings.component.scss @@ -0,0 +1,7 @@ +@import "../../../ui/form-field/form-field-sizes"; + +.GeneralSettings { + &__formFieldSizedBox { + height: $form-control-size; + } +} diff --git a/src/browser/app/app-settings/general-settings/general-settings.component.spec.ts b/src/browser/app/app-settings/general-settings/general-settings.component.spec.ts new file mode 100644 index 00000000..5ff9f2bd --- /dev/null +++ b/src/browser/app/app-settings/general-settings/general-settings.component.spec.ts @@ -0,0 +1,80 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { Subject } from 'rxjs'; +import { fastTestSetup } from '../../../../../test/helpers'; +import { SharedModule, ThemeService } from '../../../shared'; +import { ButtonToggleComponent } from '../../../ui/button-toggle'; +import { Themes } from '../../../ui/style'; +import { UiModule } from '../../../ui/ui.module'; +import { GeneralSettingsComponent } from './general-settings.component'; + + +describe('browser.app.appSettings.GeneralSettingsComponent', () => { + let fixture: ComponentFixture; + let component: GeneralSettingsComponent; + + let theme: ThemeService; + + const getThemeButtonToggles = (): ButtonToggleComponent[] => + fixture.debugElement + .queryAll(By.directive(ButtonToggleComponent)) + .map(toggle => toggle.componentInstance as ButtonToggleComponent); + + fastTestSetup(); + + beforeAll(async () => { + await TestBed + .configureTestingModule({ + imports: [ + SharedModule, + UiModule, + ], + declarations: [ + GeneralSettingsComponent, + ], + }) + .compileComponents(); + }); + + beforeEach(() => { + theme = TestBed.get(ThemeService); + + fixture = TestBed.createComponent(GeneralSettingsComponent); + component = fixture.componentInstance; + }); + + describe('theme', () => { + it('should set theme form control with current theme.', () => { + spyOnProperty(theme, 'currentTheme', 'get').and.returnValue(Themes.BASIC_DARK_THEME); + fixture.detectChanges(); + + expect(component.themeFormControl.value).toEqual(Themes.BASIC_DARK_THEME); + }); + + it('should render theme options as button toggles.', () => { + fixture.detectChanges(); + + const allThemes = Object.values(Themes); + + getThemeButtonToggles().forEach((toggle) => { + expect(allThemes.includes(toggle.value)).toBe(true); + }); + }); + + it('should call set theme update when form control value changes.', () => { + const setTheme = new Subject(); + const callback = jasmine.createSpy('callback'); + const subscription = setTheme.asObservable().subscribe(callback); + + (theme as any).setTheme = setTheme; + + fixture.detectChanges(); + + const buttonToggle = getThemeButtonToggles().find(toggle => toggle.value === Themes.BASIC_LIGHT_THEME); + buttonToggle._onButtonClick(); + + expect(callback).toHaveBeenCalledWith(Themes.BASIC_LIGHT_THEME); + subscription.unsubscribe(); + }); + }); +}); diff --git a/src/browser/app/app-settings/general-settings/general-settings.component.ts b/src/browser/app/app-settings/general-settings/general-settings.component.ts new file mode 100644 index 00000000..817cdcab --- /dev/null +++ b/src/browser/app/app-settings/general-settings/general-settings.component.ts @@ -0,0 +1,33 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { FormControl } from '@angular/forms'; +import { Subscription } from 'rxjs'; +import { ThemeService } from '../../../shared'; +import { Themes } from '../../../ui/style'; + + +@Component({ + selector: 'gd-general-settings', + templateUrl: './general-settings.component.html', + styleUrls: ['./general-settings.component.scss'], +}) +export class GeneralSettingsComponent implements OnInit, OnDestroy { + readonly themeFormControl = new FormControl(); + readonly themeOptions = [ + { name: 'Light Theme', value: Themes.BASIC_LIGHT_THEME }, + { name: 'Dark Theme', value: Themes.BASIC_DARK_THEME }, + ]; + + private themeUpdateSubscription = Subscription.EMPTY; + + constructor(private theme: ThemeService) { + } + + ngOnInit(): void { + this.themeFormControl.setValue(this.theme.currentTheme); + this.themeUpdateSubscription = this.themeFormControl.valueChanges.subscribe(this.theme.setTheme); + } + + ngOnDestroy(): void { + this.themeUpdateSubscription.unsubscribe(); + } +} From 40a66783265bb1610342951b52874e30ad65aa61 Mon Sep 17 00:00:00 2001 From: seokju-na Date: Tue, 11 Dec 2018 18:13:02 +0900 Subject: [PATCH 09/20] Refactor vcs error code name --- src/core/vcs.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/core/vcs.ts b/src/core/vcs.ts index b6dac8c3..ddd03a5f 100644 --- a/src/core/vcs.ts +++ b/src/core/vcs.ts @@ -146,8 +146,8 @@ export interface VcsFileChange { export enum VcsErrorCodes { - AUTHENTICATE_ERROR = 'AUTHENTICATE_ERROR', - REPOSITORY_NOT_EXISTS = 'REPOSITORY_NOT_EXISTS', + AUTHENTICATE_ERROR = 'vcs.authenticateError', + REPOSITORY_NOT_EXISTS = 'vcs.repositoryNotExists', } @@ -169,5 +169,6 @@ export class VcsRepositoryNotExistsError extends Error { } -export type VcsError = VcsAuthenticateError +export type VcsError = + VcsAuthenticateError | VcsRepositoryNotExistsError; From cd374be6d990bc3ac9a84247c4abab7b8c863854 Mon Sep 17 00:00:00 2001 From: seokju-na Date: Tue, 11 Dec 2018 18:13:56 +0900 Subject: [PATCH 10/20] Add git get remote url method --- src/browser/shared/git.service.ts | 7 ++++++ src/main-process/services/git.service.spec.ts | 22 +++++++++++++++++++ src/main-process/services/git.service.ts | 12 ++++++++++ 3 files changed, 41 insertions(+) diff --git a/src/browser/shared/git.service.ts b/src/browser/shared/git.service.ts index 06777df1..5270e83a 100644 --- a/src/browser/shared/git.service.ts +++ b/src/browser/shared/git.service.ts @@ -86,6 +86,13 @@ export class GitService implements OnDestroy { )); } + getRemoteUrl(options: GitFindRemoteOptions): Observable { + return from(this.ipcClient.performAction( + 'getRemoteUrl', + options, + )); + } + setRemote(options: GitSetRemoteOptions): Observable { return from(this.ipcClient.performAction( 'setRemote', diff --git a/src/main-process/services/git.service.spec.ts b/src/main-process/services/git.service.spec.ts index fd1ce06a..3a600347 100644 --- a/src/main-process/services/git.service.spec.ts +++ b/src/main-process/services/git.service.spec.ts @@ -246,6 +246,28 @@ describe('mainProcess.services.GitService', () => { }); }); + describe('getRemoteUrl', () => { + beforeEach(async () => { + await makeTmpPath(true); + }); + + it('should return url of remote.', async () => { + // First set remote.. + const remoteName = 'my_remote'; + const remoteUrl = 'some_remote_url'; + + const repo = await _git.Repository.open(tmpPath); + await _git.Remote.create(repo, remoteName, remoteUrl); + + const result = await git.getRemoteUrl({ + workspaceDirPath: tmpPath, + remoteName, + }); + + expect(result).to.equals(remoteUrl); + }); + }); + describe('setRemote', () => { beforeEach(async () => { await makeTmpPath(true); diff --git a/src/main-process/services/git.service.ts b/src/main-process/services/git.service.ts index 68b23c38..e0a678bc 100644 --- a/src/main-process/services/git.service.ts +++ b/src/main-process/services/git.service.ts @@ -227,6 +227,18 @@ export class GitService extends Service { } } + @IpcActionHandler('getRemoteUrl') + async getRemoteUrl(options: GitFindRemoteOptions): Promise { + const repository = await this.openRepository(options.workspaceDirPath); + const remote = await repository.getRemote(options.remoteName); + const remoteUrl = remote.url(); + + remote.free(); + repository.free(); + + return remoteUrl; + } + @IpcActionHandler('setRemote') async setRemote(options: GitSetRemoteOptions): Promise { const { remoteName, remoteUrl } = options; From ea2fe2e6ef05b9afcc458983752a37f5ea37a29c Mon Sep 17 00:00:00 2001 From: seokju-na Date: Tue, 11 Dec 2018 18:14:58 +0900 Subject: [PATCH 11/20] Add methods: get fetch account, find remote, get remote url --- src/browser/vcs/vcs.service.spec.ts | 66 ++++++++++++++++++++++++++++- src/browser/vcs/vcs.service.ts | 55 ++++++++++++++++++------ 2 files changed, 107 insertions(+), 14 deletions(-) diff --git a/src/browser/vcs/vcs.service.spec.ts b/src/browser/vcs/vcs.service.spec.ts index e99b0120..b26efa2b 100644 --- a/src/browser/vcs/vcs.service.spec.ts +++ b/src/browser/vcs/vcs.service.spec.ts @@ -1,9 +1,14 @@ import { HttpClientTestingModule } from '@angular/common/http/testing'; import { fakeAsync, flush, TestBed } from '@angular/core/testing'; -import { Observable, of } from 'rxjs'; +import { Observable, of, throwError } from 'rxjs'; import { fastTestSetup } from '../../../test/helpers'; import { VcsAccountDummy } from '../../core/dummies'; -import { GitFindRemoteOptions, GitSetRemoteOptions, GitSyncWithRemoteOptions } from '../../core/git'; +import { + GitFindRemoteOptions, + GitRemoteNotFoundError, + GitSetRemoteOptions, + GitSyncWithRemoteOptions, +} from '../../core/git'; import { VcsAccount, VcsAuthenticationInfo, VcsAuthenticationTypes, VcsRemoteRepository } from '../../core/vcs'; import { toPromise } from '../../libs/rx'; import { GitService, SharedModule, WORKSPACE_DEFAULT_CONFIG, WorkspaceConfig } from '../shared'; @@ -245,6 +250,63 @@ describe('browser.vcs.VcsService', () => { }); }); + describe('getRepositoryFetchAccount', () => { + it('should return fetch account if its exists.', fakeAsync(() => { + const fetchAccount = accountDummy.create(); + spyOn(accountDB, 'getRepositoryFetchAccount').and.callFake(() => Promise.resolve(fetchAccount)); + + const callback = jasmine.createSpy('get repository fetch account callback'); + const subscription = vcs.getRepositoryFetchAccount().subscribe(callback); + flush(); + + expect(callback).toHaveBeenCalledWith(fetchAccount); + subscription.unsubscribe(); + })); + + it('should return null if fetch account is not exists.', fakeAsync(() => { + spyOn(accountDB, 'getRepositoryFetchAccount').and.callFake(() => Promise.resolve(undefined)); + + const callback = jasmine.createSpy('get repository fetch account callback'); + const subscription = vcs.getRepositoryFetchAccount().subscribe(callback); + flush(); + + expect(callback).toHaveBeenCalledWith(null); + subscription.unsubscribe(); + })); + }); + + describe('getRemoteRepositoryUrl', () => { + it('should return null if remote not found.', () => { + spyOn(git, 'getRemoteUrl').and.returnValue(throwError(new GitRemoteNotFoundError())); + + const callback = jasmine.createSpy('get remote repository url callback'); + const subscription = vcs.getRemoteRepositoryUrl().subscribe(callback); + + expect(git.getRemoteUrl).toHaveBeenCalledWith({ + workspaceDirPath: workspaceConfig.rootDirPath, + remoteName: 'origin', + } as GitFindRemoteOptions); + expect(callback).toHaveBeenCalledWith(null); + + subscription.unsubscribe(); + }); + + it('should return remote url.', () => { + spyOn(git, 'getRemoteUrl').and.returnValue(of('remote_repository_url')); + + const callback = jasmine.createSpy('get remote repository url callback'); + const subscription = vcs.getRemoteRepositoryUrl().subscribe(callback); + + expect(git.getRemoteUrl).toHaveBeenCalledWith({ + workspaceDirPath: workspaceConfig.rootDirPath, + remoteName: 'origin', + } as GitFindRemoteOptions); + expect(callback).toHaveBeenCalledWith('remote_repository_url'); + + subscription.unsubscribe(); + }); + }); + describe('setRemoteRepository', () => { it('should save fetch account to account database and call set remote method ' + 'from git service.', fakeAsync(() => { diff --git a/src/browser/vcs/vcs.service.ts b/src/browser/vcs/vcs.service.ts index 44270d90..c696ac2e 100644 --- a/src/browser/vcs/vcs.service.ts +++ b/src/browser/vcs/vcs.service.ts @@ -1,8 +1,8 @@ import { Inject, Injectable } from '@angular/core'; -import { from, Observable, of, zip } from 'rxjs'; -import { filter, map, mapTo, switchMap, tap } from 'rxjs/operators'; -import { GitGetHistoryOptions, GitSyncWithRemoteResult } from '../../core/git'; -import { VcsAccount, VcsCommitItem, VcsFileChange } from '../../core/vcs'; +import { from, Observable, of, throwError, zip } from 'rxjs'; +import { catchError, filter, map, mapTo, switchMap, tap } from 'rxjs/operators'; +import { GitError, GitErrorCodes, GitGetHistoryOptions, GitSyncWithRemoteResult } from '../../core/git'; +import { VcsAccount, VcsAuthenticationInfo, VcsCommitItem, VcsFileChange, VcsRemoteRepository } from '../../core/vcs'; import { toPromise } from '../../libs/rx'; import { GitService, WorkspaceService } from '../shared'; import { VCS_ACCOUNT_DATABASE, VcsAccountDatabase } from './vcs-account-database'; @@ -22,8 +22,14 @@ export class VcsCloneRepositoryOption { @Injectable() export class VcsService { - _removeProvider: VcsRemoteProvider | null = null; + // Git commit history. + private _commitHistoryFetchingSize: number = 50; + + get commitHistoryFetchingSize(): number { + return this._commitHistoryFetchingSize; + } + _removeProvider: VcsRemoteProvider | null = null; private nextCommitHistoryFetchingOptions: GitGetHistoryOptions | null = null; constructor( @@ -34,13 +40,6 @@ export class VcsService { ) { } - // Git commit history. - private _commitHistoryFetchingSize: number = 50; - - get commitHistoryFetchingSize(): number { - return this._commitHistoryFetchingSize; - } - setRemoveProvider(type: VcsRemoteProviderType): this { this._removeProvider = this.remoteProviderFactory.create(type); return this; @@ -167,6 +166,38 @@ export class VcsService { return (fetchAccount !== undefined) && isRemoteExists; } + getRepositoryFetchAccount(): Observable { + return from(this.accountDB.getRepositoryFetchAccount()).pipe( + map(fetchAccount => fetchAccount ? fetchAccount : null), + ); + } + + getRemoteRepositoryUrl(): Observable { + return this.git.getRemoteUrl({ + workspaceDirPath: this.workspace.configs.rootDirPath, + remoteName: 'origin', + }).pipe( + catchError((error) => { + // If remote not exists, return null. + // Otherwise throw error because its unexpected error. + if ((error as GitError).code === GitErrorCodes.REMOTE_NOT_FOUND) { + return of(null); + } else { + return throwError(error); + } + }), + ); + } + + findRemoteRepository(remoteUrl: string, authentication?: VcsAuthenticationInfo): Observable { + this.checkIfRemoteProviderIsProvided(); + + return this._removeProvider.findRepository( + remoteUrl, + authentication, + ); + } + setRemoteRepository(fetchAccount: VcsAccount, remoteUrl: string): Observable { return zip( from(this.accountDB.setRepositoryFetchAccountAs(fetchAccount)), From e3faca11d7346c81eb11bee57de2abcab1181976 Mon Sep 17 00:00:00 2001 From: seokju-na Date: Tue, 11 Dec 2018 18:15:14 +0900 Subject: [PATCH 12/20] Add vcs settings component --- .../vcs/vcs-settings/_vcs-settings-theme.scss | 15 + .../vcs-settings/vcs-settings.component.html | 44 +++ .../vcs-settings/vcs-settings.component.scss | 12 + .../vcs-settings.component.spec.ts | 358 ++++++++++++++++++ .../vcs-settings/vcs-settings.component.ts | 171 +++++++++ 5 files changed, 600 insertions(+) create mode 100644 src/browser/vcs/vcs-settings/_vcs-settings-theme.scss create mode 100644 src/browser/vcs/vcs-settings/vcs-settings.component.html create mode 100644 src/browser/vcs/vcs-settings/vcs-settings.component.scss create mode 100644 src/browser/vcs/vcs-settings/vcs-settings.component.spec.ts create mode 100644 src/browser/vcs/vcs-settings/vcs-settings.component.ts diff --git a/src/browser/vcs/vcs-settings/_vcs-settings-theme.scss b/src/browser/vcs/vcs-settings/_vcs-settings-theme.scss new file mode 100644 index 00000000..e21cd0d3 --- /dev/null +++ b/src/browser/vcs/vcs-settings/_vcs-settings-theme.scss @@ -0,0 +1,15 @@ +@import "../../ui/style/theming"; + +@mixin gd-vcs-settings-theme($theme) { + .VcsSettings { + &__saveRemoteResultMessage { + &--success { + color: map-get($color-green, 700); + } + + &--fail { + color: map-get($color-red, 700); + } + } + } +} diff --git a/src/browser/vcs/vcs-settings/vcs-settings.component.html b/src/browser/vcs/vcs-settings/vcs-settings.component.html new file mode 100644 index 00000000..421ae1df --- /dev/null +++ b/src/browser/vcs/vcs-settings/vcs-settings.component.html @@ -0,0 +1,44 @@ +
+ + + + + + + + + + + + + + + + + Remote url required + Invalid remote url format + Repository not exists + + + +
+ + {{ saveRemoteResultMessage }} + +
+
diff --git a/src/browser/vcs/vcs-settings/vcs-settings.component.scss b/src/browser/vcs/vcs-settings/vcs-settings.component.scss new file mode 100644 index 00000000..afaf95bf --- /dev/null +++ b/src/browser/vcs/vcs-settings/vcs-settings.component.scss @@ -0,0 +1,12 @@ +@import "../../ui/style/spacing"; + +.VcsSettings { + &__saveRemoteButton { + } + + &__saveRemoteResultMessage { + display: inline-flex; + align-items: center; + padding: 0 $spacing-half; + } +} diff --git a/src/browser/vcs/vcs-settings/vcs-settings.component.spec.ts b/src/browser/vcs/vcs-settings/vcs-settings.component.spec.ts new file mode 100644 index 00000000..93d75b19 --- /dev/null +++ b/src/browser/vcs/vcs-settings/vcs-settings.component.spec.ts @@ -0,0 +1,358 @@ +import { DebugElement } from '@angular/core'; +import { ComponentFixture, fakeAsync, flush, flushMicrotasks, TestBed, tick } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { of, throwError } from 'rxjs'; +import { + createDummies, + dispatchMouseEvent, + expectDom, + fastTestSetup, + getVisibleErrorAt, + typeInElement, +} from '../../../../test/helpers'; +import { MockDialog } from '../../../../test/mocks/browser'; +import { VcsAccountDummy } from '../../../core/dummies'; +import { VcsRepositoryNotExistsError } from '../../../core/vcs'; +import { GitService, SharedModule } from '../../shared'; +import { Dialog } from '../../ui/dialog'; +import { MenuItem } from '../../ui/menu'; +import { UiModule } from '../../ui/ui.module'; +import { VCS_ACCOUNT_DATABASE, VcsAccountDatabase, VcsAccountDatabaseProvider } from '../vcs-account-database'; +import { GithubAccountsDialogComponent } from '../vcs-remote'; +import { VcsService } from '../vcs.service'; +import { VcsSettingsComponent } from './vcs-settings.component'; +import Spy = jasmine.Spy; + + +describe('browser.vcs.vcsSettings.VcsSettingsComponent', () => { + let component: VcsSettingsComponent; + let fixture: ComponentFixture; + + let mockDialog: MockDialog; + let accountDB: VcsAccountDatabase; + let vcs: VcsService; + let git: GitService; + + const accountDummy = new VcsAccountDummy(); + + function ignoreAccountsGetting(): void { + component['loadAccounts'] = jasmine.createSpy('private method'); + } + + function ignoreFetchAccountGetting(): void { + component['getFetchAccountAndPatchFormValueIfExists'] = + jasmine.createSpy('getFetchAccountAndPatchFormValueIfExists private method'); + } + + function ignoreRemoteUrlGetting(): void { + component['getRemoteUrlAndPatchFormValueIfExists'] = + jasmine.createSpy('getRemoteUrlAndPatchFormValueIfExists private method'); + } + + const getAccountSelectEl = (): HTMLElement => + fixture.debugElement.query(By.css('#vcs-remote-setting-github-account-select')).nativeElement; + + const getRemoteUrlFormFieldDe = (): DebugElement => + fixture.debugElement.query(By.css('#vcs-remote-setting-url-form-field')); + + const getRemoteUrlInputEl = (): HTMLInputElement => + fixture.debugElement.query(By.css('#vcs-remote-setting-url-input')).nativeElement as HTMLInputElement; + + const getSaveRemoteButtonEl = (): HTMLButtonElement => + fixture.debugElement.query(By.css('.VcsSettings__saveRemoteButton')).nativeElement as HTMLButtonElement; + + const getSaveRemoteResultMessageEl = (): HTMLElement => + fixture.debugElement.query(By.css('.VcsSettings__saveRemoteResultMessage')).nativeElement as HTMLElement; + + fastTestSetup(); + + beforeAll(async () => { + mockDialog = new MockDialog(); + vcs = jasmine.createSpyObj('vcs', [ + 'setRemoveProvider', + 'isRemoteRepositoryUrlValid', + 'findRemoteRepository', + 'setRemoteRepository', + 'getRepositoryFetchAccount', + 'getRemoteRepositoryUrl', + ]); + + (vcs.setRemoveProvider as Spy).and.returnValue(vcs); + + await TestBed + .configureTestingModule({ + imports: [ + UiModule, + SharedModule, + ], + providers: [ + VcsAccountDatabaseProvider, + { provide: VcsService, useValue: vcs }, + ], + declarations: [ + VcsSettingsComponent, + ], + }) + .overrideComponent(VcsSettingsComponent, { + set: { + providers: [{ provide: Dialog, useValue: mockDialog }], + }, + }) + .compileComponents(); + }); + + beforeEach(() => { + accountDB = TestBed.get(VCS_ACCOUNT_DATABASE); + git = TestBed.get(GitService); + + fixture = TestBed.createComponent(VcsSettingsComponent); + component = fixture.componentInstance; + }); + + afterEach(async () => { + await accountDB.accounts.clear(); + }); + + describe('remote setting', () => { + it('should get all accounts from account database on ngOnInit.', fakeAsync(() => { + ignoreFetchAccountGetting(); + ignoreRemoteUrlGetting(); + + const accounts = createDummies(accountDummy, 10); + spyOn(accountDB, 'getAllAccounts').and.returnValue(Promise.resolve(accounts)); + + // Expect to fetch accounts. + fixture.detectChanges(); + flush(); + fixture.detectChanges(); + + expect(component.accountMenuItems).toEqual(accounts.map(account => ({ + id: account.email, + label: `${account.name} <${account.email}>`, + } as MenuItem))); + })); + + // FIXME LATER + xit('should open github accounts dialog when click \'add account\' button. After dialog ' + + 'closed, should reload all accounts from account database.', fakeAsync(() => { + const prevAccounts = createDummies(accountDummy, 5); + spyOn(accountDB, 'getAllAccounts').and.returnValue(Promise.resolve(prevAccounts)); + + fixture.detectChanges(); + flushMicrotasks(); + fixture.detectChanges(); + + expect(component.accountMenuItems).toEqual(prevAccounts.map(account => ({ + id: account.email, + label: `${account.name} <${account.email}>`, + } as MenuItem))); + + // First, we should open menu... + const menuTriggerEl = getAccountSelectEl(); + dispatchMouseEvent(menuTriggerEl, 'mousedown'); + menuTriggerEl.click(); + tick(); + fixture.detectChanges(); + + // Click add account button + const addAccountButtonEl = document.querySelector('#add-github-account-button') as HTMLButtonElement; + console.log(addAccountButtonEl); + addAccountButtonEl.click(); + tick(500); + fixture.detectChanges(); + + const githubAccountsDialogRef = mockDialog.getByComponent( + GithubAccountsDialogComponent, + ); + expect(githubAccountsDialogRef).toBeDefined(); + + const nextAccounts = [...prevAccounts, ...createDummies(accountDummy, 5)]; + (accountDB.getAllAccounts as jasmine.Spy).and.returnValue(Promise.resolve(nextAccounts)); + + githubAccountsDialogRef.close(); + flushMicrotasks(); + fixture.detectChanges(); + + expect(component.accountMenuItems).toEqual(nextAccounts.map(account => ({ + id: account.email, + label: `${account.name} <${account.email}>`, + } as MenuItem))); + })); + + it('should save remote button is disabled if github account is not selected.', () => { + ignoreFetchAccountGetting(); + ignoreRemoteUrlGetting(); + ignoreAccountsGetting(); + + fixture.detectChanges(); + + (vcs.isRemoteRepositoryUrlValid as Spy).and.returnValue(true); + + // Github Account: invalid, Remote URL: valid + typeInElement('https://github.com/seokju-na/geeks-diary', getRemoteUrlInputEl()); + component.remoteSettingForm.get('githubAccount').patchValue(null); + fixture.detectChanges(); + + expectDom(getSaveRemoteButtonEl()).toBeDisabled(); + }); + + it('should save remote button is disabled if remote url is not input.', () => { + ignoreFetchAccountGetting(); + ignoreRemoteUrlGetting(); + ignoreAccountsGetting(); + + fixture.detectChanges(); + + // Github Account: valid, Remote URL: invalid + component.remoteSettingForm.get('githubAccount').patchValue(accountDummy.create()); + typeInElement('', getRemoteUrlInputEl()); + fixture.detectChanges(); + + expectDom(getSaveRemoteButtonEl()).toBeDisabled(); + }); + + it('should save remote button is disabled if remote url is not valid.', () => { + ignoreFetchAccountGetting(); + ignoreRemoteUrlGetting(); + ignoreAccountsGetting(); + + fixture.detectChanges(); + + (vcs.isRemoteRepositoryUrlValid as Spy).and.returnValue(false); + + // Github Account: valid, Remote URL: invalid + component.remoteSettingForm.get('githubAccount').patchValue(accountDummy.create()); + typeInElement('not_valid_url', getRemoteUrlInputEl()); + fixture.detectChanges(); + + expectDom(getSaveRemoteButtonEl()).toBeDisabled(); + }); + + it('should save remote button is not disabled if github account is selected and ' + + 'remote url is valid.', () => { + ignoreFetchAccountGetting(); + ignoreRemoteUrlGetting(); + ignoreAccountsGetting(); + + fixture.detectChanges(); + + (vcs.isRemoteRepositoryUrlValid as Spy).and.returnValue(true); + + component.remoteSettingForm.get('githubAccount').patchValue(accountDummy.create()); + typeInElement('https://github.com/seokju-na/geeks-diary', getRemoteUrlInputEl()); + fixture.detectChanges(); + + expectDom(getSaveRemoteButtonEl()).not.toBeDisabled(); + }); + + it('should show repository not exists error if repository not exists in remote provider ' + + 'when submit remote setting form.', fakeAsync(() => { + ignoreFetchAccountGetting(); + ignoreRemoteUrlGetting(); + ignoreAccountsGetting(); + fixture.detectChanges(); + + const fetchAccount = accountDummy.create(); + const remoteUrl = 'https://github.com/seokju-na/geeks-diary.git'; + + component.remoteSettingForm.patchValue({ + githubAccount: fetchAccount, + remoteUrl, + }); + fixture.detectChanges(); + + (vcs.findRemoteRepository as Spy).and.returnValue(throwError(new VcsRepositoryNotExistsError())); + + getSaveRemoteButtonEl().click(); + flush(); + fixture.detectChanges(); + + expect(vcs.findRemoteRepository).toHaveBeenCalledWith(remoteUrl, fetchAccount.authentication); + expect(getVisibleErrorAt(getRemoteUrlFormFieldDe()).errorName).toEqual('repositoryNotExists'); + })); + + it('should show success message if set remote repository success when submit ' + + 'remote settings form.', fakeAsync(() => { + ignoreFetchAccountGetting(); + ignoreRemoteUrlGetting(); + ignoreAccountsGetting(); + fixture.detectChanges(); + + const fetchAccount = accountDummy.create(); + const remoteUrl = 'https://github.com/seokju-na/geeks-diary.git'; + + component.remoteSettingForm.patchValue({ + githubAccount: fetchAccount, + remoteUrl, + }); + fixture.detectChanges(); + + (vcs.findRemoteRepository as Spy).and.returnValue(of(null)); + (vcs.setRemoteRepository as Spy).and.returnValue(of(null)); + + getSaveRemoteButtonEl().click(); + flush(); + fixture.detectChanges(); + + expect(vcs.setRemoteRepository).toHaveBeenCalledWith(fetchAccount, remoteUrl); + + expectDom(getSaveRemoteResultMessageEl()) + .toContainClasses('VcsSettings__saveRemoteResultMessage--success'); + expectDom(getSaveRemoteResultMessageEl()) + .toContainText(component.saveRemoteResultMessage); + })); + + it('should show fail message if set remote repository fail when submit ' + + 'remote setting form', fakeAsync(() => { + ignoreFetchAccountGetting(); + ignoreRemoteUrlGetting(); + ignoreAccountsGetting(); + fixture.detectChanges(); + + const fetchAccount = accountDummy.create(); + const remoteUrl = 'https://github.com/seokju-na/geeks-diary.git'; + + component.remoteSettingForm.patchValue({ + githubAccount: fetchAccount, + remoteUrl, + }); + fixture.detectChanges(); + + (vcs.findRemoteRepository as Spy).and.returnValue(of(null)); + (vcs.setRemoteRepository as Spy).and.returnValue(throwError(new Error('Some Error'))); + + getSaveRemoteButtonEl().click(); + flush(); + fixture.detectChanges(); + + expect(vcs.setRemoteRepository).toHaveBeenCalledWith(fetchAccount, remoteUrl); + + expectDom(getSaveRemoteResultMessageEl()) + .toContainClasses('VcsSettings__saveRemoteResultMessage--fail'); + expectDom(getSaveRemoteResultMessageEl()) + .toContainText(component.saveRemoteResultMessage); + })); + + it('should set github account if fetch repository account is exists on ngOnInit.', () => { + ignoreRemoteUrlGetting(); + ignoreAccountsGetting(); + + const fetchAccount = accountDummy.create(); + (vcs.getRepositoryFetchAccount as Spy).and.returnValue(of(fetchAccount)); + fixture.detectChanges(); + + expect(component.remoteSettingForm.get('githubAccount').value).toEqual(fetchAccount); + }); + + it('should set remote url if remote url is exists on ngOnInit.', () => { + ignoreFetchAccountGetting(); + ignoreAccountsGetting(); + + const remoteUrl = 'https://github.com/seokju-na/geeks-diary.git'; + (vcs.getRemoteRepositoryUrl as Spy).and.returnValue(of(remoteUrl)); + fixture.detectChanges(); + + expect(component.remoteSettingForm.get('remoteUrl').value).toEqual(remoteUrl); + }); + }); +}); diff --git a/src/browser/vcs/vcs-settings/vcs-settings.component.ts b/src/browser/vcs/vcs-settings/vcs-settings.component.ts new file mode 100644 index 00000000..b79ce529 --- /dev/null +++ b/src/browser/vcs/vcs-settings/vcs-settings.component.ts @@ -0,0 +1,171 @@ +import { Component, Inject, OnInit } from '@angular/core'; +import { FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms'; +import { take } from 'rxjs/operators'; +import { VcsAccount, VcsRepositoryNotExistsError } from '../../../core/vcs'; +import { toPromise } from '../../../libs/rx'; +import { WorkspaceService } from '../../shared'; +import { Dialog } from '../../ui/dialog'; +import { MenuItem } from '../../ui/menu'; +import { VCS_ACCOUNT_DATABASE, VcsAccountDatabase } from '../vcs-account-database'; +import { GithubAccountsDialogComponent } from '../vcs-remote'; +import { VcsService } from '../vcs.service'; + + +@Component({ + selector: 'gd-vcs-settings', + templateUrl: './vcs-settings.component.html', + styleUrls: ['./vcs-settings.component.scss'], + providers: [Dialog], +}) +export class VcsSettingsComponent implements OnInit { + private _saveRemoteProcessing = false; + + get saveRemoteProcessing(): boolean { + return this._saveRemoteProcessing; + } + + get saveRemoteResultMessage(): string { + switch (this.saveRemoteResult) { + case 'success': + return 'Save remote complete'; + case 'fail': + return 'Fail to save remote'; + } + } + + readonly remoteSettingForm = new FormGroup({ + githubAccount: new FormControl('', [Validators.required]), + remoteUrl: new FormControl(''), + }); + + saveRemoteResult: 'success' | 'fail'; + accountMenuItems: MenuItem[] = []; + private accounts: VcsAccount[] = []; + + constructor( + private vcs: VcsService, + private workspace: WorkspaceService, + private dialog: Dialog, + @Inject(VCS_ACCOUNT_DATABASE) private accountDB: VcsAccountDatabase, + ) { + } + + readonly accountMenuConvertFn = (value: VcsAccount): MenuItem => ({ + id: value.email, + label: `${value.name} <${value.email}>`, + }); + + ngOnInit(): void { + this.vcs.setRemoveProvider('github'); + + this.buildRemoteUrlFormControl(); + this.getFetchAccountAndPatchFormValueIfExists(); + this.getRemoteUrlAndPatchFormValueIfExists(); + this.loadAccounts(); + } + + addAccount(): void { + // Wait for next tick, because before open the dialog, we should + // release trigger focuses first. + setTimeout(() => { + this.dialog.open( + GithubAccountsDialogComponent, + { + width: '480px', + disableBackdropClickClose: true, + }, + ).afterClosed().subscribe(() => this.loadAccounts()); + }, 0); + } + + selectAccount(item: MenuItem): void { + const account = this.accounts.find(account => account.email === item.id); + this.remoteSettingForm.get('githubAccount').patchValue(account); + } + + async saveRemote(): Promise { + if (this._saveRemoteProcessing) { + return; + } + + this._saveRemoteProcessing = true; + this.saveRemoteResult = null; + const { githubAccount, remoteUrl } = this.remoteSettingForm.value; + + // Check remote if repository exists. + try { + await toPromise(this.vcs.findRemoteRepository( + remoteUrl as string, + (githubAccount as VcsAccount).authentication, + )); + } catch (error) { + // Display error message for remote url input. + if (error instanceof VcsRepositoryNotExistsError) { + this.remoteSettingForm.get('remoteUrl').setErrors({ repositoryNotExists: true }); + } else { + // TODO(@seokju-na): How can we handle other errors...? + } + + this.saveRemoteResult = 'fail'; + this._saveRemoteProcessing = false; + return; + } + + // Set remote repository. + try { + await toPromise(this.vcs.setRemoteRepository( + githubAccount as VcsAccount, + remoteUrl as string, + )); + + this.saveRemoteResult = 'success'; + } catch (error) { + this.saveRemoteResult = 'fail'; + } finally { + this._saveRemoteProcessing = false; + } + } + + private buildRemoteUrlFormControl(): void { + const remoteUrlFormatValidator: ValidatorFn = (control) => { + if (control.value === '' || control.value.length === 0) { + return null; + } + + return this.vcs.isRemoteRepositoryUrlValid(control.value) + ? null + : { invalidFormat: true }; + }; + + this.remoteSettingForm.get('remoteUrl').setValidators([ + Validators.required, + remoteUrlFormatValidator, + ]); + } + + private getFetchAccountAndPatchFormValueIfExists(): void { + this.vcs.getRepositoryFetchAccount().pipe(take(1)).subscribe((fetchAccount) => { + if (fetchAccount) { + this.remoteSettingForm.get('githubAccount').patchValue(fetchAccount); + } + }); + } + + private getRemoteUrlAndPatchFormValueIfExists(): void { + this.vcs.getRemoteRepositoryUrl().pipe(take(1)).subscribe((remoteUrl) => { + if (remoteUrl) { + this.remoteSettingForm.get('remoteUrl').patchValue(remoteUrl); + } + }); + } + + private async loadAccounts(): Promise { + const accounts = await this.accountDB.getAllAccounts(); + + this.accounts = accounts; + this.accountMenuItems = accounts.map(account => ({ + id: account.email, + label: `${account.name} <${account.email}>`, + })); + } +} From 9d61cfc0bb805fb2b6e18f2cf79fb96f43c1a33d Mon Sep 17 00:00:00 2001 From: seokju-na Date: Tue, 11 Dec 2018 18:15:43 +0900 Subject: [PATCH 13/20] Add vcs settings --- src/browser/vcs/_all-theme.scss | 2 ++ src/browser/vcs/vcs-settings/_all-theme.scss | 5 +++++ src/browser/vcs/vcs-settings/index.ts | 3 +++ .../vcs/vcs-settings/vcs-settings.module.ts | 15 +++++++++++++++ src/browser/vcs/vcs-settings/vcs-settings.ts | 11 +++++++++++ src/browser/vcs/vcs.module.ts | 3 +++ 6 files changed, 39 insertions(+) create mode 100644 src/browser/vcs/vcs-settings/_all-theme.scss create mode 100644 src/browser/vcs/vcs-settings/index.ts create mode 100644 src/browser/vcs/vcs-settings/vcs-settings.module.ts create mode 100644 src/browser/vcs/vcs-settings/vcs-settings.ts diff --git a/src/browser/vcs/_all-theme.scss b/src/browser/vcs/_all-theme.scss index dc40ea3d..949f6bbb 100644 --- a/src/browser/vcs/_all-theme.scss +++ b/src/browser/vcs/_all-theme.scss @@ -2,10 +2,12 @@ @import "./vcs-manager-theme"; @import "./vcs-local/all-theme"; @import "./vcs-remote/all-theme"; +@import "./vcs-settings/all-theme"; @mixin gd-vcs-all-theme($theme) { @include gd-vcs-view-all-theme($theme); @include gd-vcs-manager-theme($theme); @include gd-vcs-local-all-theme($theme); @include gd-vcs-remote-all-theme($theme); + @include gd-vcs-settings-all-theme($theme); } diff --git a/src/browser/vcs/vcs-settings/_all-theme.scss b/src/browser/vcs/vcs-settings/_all-theme.scss new file mode 100644 index 00000000..573cee13 --- /dev/null +++ b/src/browser/vcs/vcs-settings/_all-theme.scss @@ -0,0 +1,5 @@ +@import "./_vcs-settings-theme"; + +@mixin gd-vcs-settings-all-theme($theme) { + @include gd-vcs-settings-theme($theme); +} diff --git a/src/browser/vcs/vcs-settings/index.ts b/src/browser/vcs/vcs-settings/index.ts new file mode 100644 index 00000000..c53c0eb9 --- /dev/null +++ b/src/browser/vcs/vcs-settings/index.ts @@ -0,0 +1,3 @@ +export * from './vcs-settings.component'; +export * from './vcs-settings.module'; +export * from './vcs-settings'; diff --git a/src/browser/vcs/vcs-settings/vcs-settings.module.ts b/src/browser/vcs/vcs-settings/vcs-settings.module.ts new file mode 100644 index 00000000..a1f59924 --- /dev/null +++ b/src/browser/vcs/vcs-settings/vcs-settings.module.ts @@ -0,0 +1,15 @@ +import { NgModule } from '@angular/core'; +import { UiModule } from '../../ui/ui.module'; +import { VcsSettingsComponent } from './vcs-settings.component'; + + +@NgModule({ + imports: [ + UiModule, + ], + declarations: [VcsSettingsComponent], + entryComponents: [VcsSettingsComponent], + exports: [VcsSettingsComponent], +}) +export class VcsSettingsModule { +} diff --git a/src/browser/vcs/vcs-settings/vcs-settings.ts b/src/browser/vcs/vcs-settings/vcs-settings.ts new file mode 100644 index 00000000..70042e19 --- /dev/null +++ b/src/browser/vcs/vcs-settings/vcs-settings.ts @@ -0,0 +1,11 @@ +import { SettingsContext } from '../../settings'; +import { VcsSettingsComponent } from './vcs-settings.component'; + + +export const VCS_SETTINGS_ID = 'settings.vcs'; + +export const vcsSettingsContext: SettingsContext = { + id: VCS_SETTINGS_ID, + tabName: 'Version Control', + component: VcsSettingsComponent, +}; diff --git a/src/browser/vcs/vcs.module.ts b/src/browser/vcs/vcs.module.ts index 7198c49e..2b83e3c9 100644 --- a/src/browser/vcs/vcs.module.ts +++ b/src/browser/vcs/vcs.module.ts @@ -6,6 +6,7 @@ import { VcsAccountDatabaseProvider } from './vcs-account-database'; import { VcsLocalModule } from './vcs-local'; import { VcsManagerComponent } from './vcs-manager.component'; import { VcsRemoteModule } from './vcs-remote'; +import { VcsSettingsModule } from './vcs-settings'; import { VcsViewModule } from './vcs-view'; import { VcsEffects } from './vcs.effects'; import { vcsReducerMap } from './vcs.reducer'; @@ -18,6 +19,7 @@ import { VcsService } from './vcs.service'; VcsRemoteModule, VcsViewModule, VcsLocalModule, + VcsSettingsModule, StoreModule.forFeature('vcs', vcsReducerMap), EffectsModule.forFeature([VcsEffects]), ], @@ -34,6 +36,7 @@ import { VcsService } from './vcs.service'; exports: [ VcsRemoteModule, VcsViewModule, + VcsSettingsModule, VcsManagerComponent, ], }) From 4e57fe5b682130f4f4ace70dc8db584f22274c82 Mon Sep 17 00:00:00 2001 From: seokju-na Date: Tue, 11 Dec 2018 18:16:21 +0900 Subject: [PATCH 14/20] Add app settings --- .../app/app-settings/app-settings.module.ts | 39 +++++++++++++++++++ src/browser/app/app-settings/index.ts | 2 + src/browser/app/app.module.ts | 4 ++ 3 files changed, 45 insertions(+) create mode 100644 src/browser/app/app-settings/app-settings.module.ts create mode 100644 src/browser/app/app-settings/index.ts diff --git a/src/browser/app/app-settings/app-settings.module.ts b/src/browser/app/app-settings/app-settings.module.ts new file mode 100644 index 00000000..b69d94b9 --- /dev/null +++ b/src/browser/app/app-settings/app-settings.module.ts @@ -0,0 +1,39 @@ +import { NgModule } from '@angular/core'; +import { SETTINGS_REGISTRATION, SettingsContext } from '../../settings'; +import { UiModule } from '../../ui/ui.module'; +import { vcsSettingsContext } from '../../vcs/vcs-settings'; +import { GeneralSettingsComponent } from './general-settings/general-settings.component'; + + +const generalSettingsContext: SettingsContext = { + id: 'settings.general', + tabName: 'General', + component: GeneralSettingsComponent, +}; + + +@NgModule({ + imports: [ + UiModule, + ], + declarations: [ + GeneralSettingsComponent, + ], + entryComponents: [ + GeneralSettingsComponent, + ], + providers: [ + { + provide: SETTINGS_REGISTRATION, + useValue: [ + generalSettingsContext, + vcsSettingsContext, + ] as SettingsContext[], + }, + ], + exports: [ + GeneralSettingsComponent, + ], +}) +export class AppSettingsModule { +} diff --git a/src/browser/app/app-settings/index.ts b/src/browser/app/app-settings/index.ts new file mode 100644 index 00000000..0c8c8871 --- /dev/null +++ b/src/browser/app/app-settings/index.ts @@ -0,0 +1,2 @@ +export * from './app-settings.module'; +export * from './general-settings/general-settings.component'; diff --git a/src/browser/app/app.module.ts b/src/browser/app/app.module.ts index 3c13d461..156bb841 100644 --- a/src/browser/app/app.module.ts +++ b/src/browser/app/app.module.ts @@ -5,6 +5,7 @@ import { EffectsModule } from '@ngrx/effects'; import { StoreModule } from '@ngrx/store'; import { StoreDevtoolsModule } from '@ngrx/store-devtools'; import { NoteModule } from '../note'; +import { SettingsModule } from '../settings'; import { UiModule } from '../ui/ui.module'; import { VcsModule } from '../vcs'; import { @@ -15,6 +16,7 @@ import { AppVcsItemFactoriesProvider, } from './app-configs'; import { AppLayoutModule } from './app-layout'; +import { AppSettingsModule } from './app-settings'; import { AppComponent } from './app.component'; import { appReducer } from './app.reducer'; @@ -28,8 +30,10 @@ import { appReducer } from './app.reducer'; StoreDevtoolsModule.instrument(), EffectsModule.forRoot([]), AppLayoutModule, + AppSettingsModule, NoteModule, VcsModule, + SettingsModule, ], providers: [ AppVcsItemFactoriesProvider, From 23f4c8c5f15ac3c6e6a28f6e629fee30d467c917 Mon Sep 17 00:00:00 2001 From: seokju-na Date: Tue, 11 Dec 2018 18:17:39 +0900 Subject: [PATCH 15/20] Add settings button on app layout sidenav --- .../app-layout-sidenav/app-layout-sidenav.component.html | 4 ++++ .../app-layout-sidenav/app-layout-sidenav.component.scss | 1 + .../app-layout-sidenav/app-layout-sidenav.component.ts | 6 ++++++ 3 files changed, 11 insertions(+) diff --git a/src/browser/app/app-layout/app-layout-sidenav/app-layout-sidenav.component.html b/src/browser/app/app-layout/app-layout-sidenav/app-layout-sidenav.component.html index 35cf2e18..484f81a9 100644 --- a/src/browser/app/app-layout/app-layout-sidenav/app-layout-sidenav.component.html +++ b/src/browser/app/app-layout/app-layout-sidenav/app-layout-sidenav.component.html @@ -16,6 +16,10 @@ + + diff --git a/src/browser/app/app-layout/app-layout-sidenav/app-layout-sidenav.component.scss b/src/browser/app/app-layout/app-layout-sidenav/app-layout-sidenav.component.scss index b7fc17ca..1e0442b5 100644 --- a/src/browser/app/app-layout/app-layout-sidenav/app-layout-sidenav.component.scss +++ b/src/browser/app/app-layout/app-layout-sidenav/app-layout-sidenav.component.scss @@ -17,6 +17,7 @@ justify-content: space-between; width: $app-layout-sidenav-nav-size; height: 100%; + padding-bottom: $spacing; } &__tabList { diff --git a/src/browser/app/app-layout/app-layout-sidenav/app-layout-sidenav.component.ts b/src/browser/app/app-layout/app-layout-sidenav/app-layout-sidenav.component.ts index 71873fa8..06d3ed80 100644 --- a/src/browser/app/app-layout/app-layout-sidenav/app-layout-sidenav.component.ts +++ b/src/browser/app/app-layout/app-layout-sidenav/app-layout-sidenav.component.ts @@ -2,6 +2,7 @@ import { Component, Injector, Input } from '@angular/core'; import { select, Store } from '@ngrx/store'; import { Observable } from 'rxjs'; import { share } from 'rxjs/operators'; +import { SettingsDialog } from '../../../settings'; import { AppState } from '../../app.state'; import { ToggleSidenavPanelAction } from '../app-layout.actions'; import { AppLayoutSidenavOutlet } from '../app-layout.state'; @@ -28,10 +29,15 @@ export class AppLayoutSidenavComponent { constructor( private store: Store, public _injector: Injector, + private settingsDialog: SettingsDialog, ) { } toggleServicePanel(outletId: string): void { this.store.dispatch(new ToggleSidenavPanelAction({ outletId })); } + + openSettingsDialog(): void { + this.settingsDialog.open(); + } } From 24e4b7e70bff6d031dd4e9b79107729f55a185e2 Mon Sep 17 00:00:00 2001 From: seokju-na Date: Tue, 11 Dec 2018 18:22:34 +0900 Subject: [PATCH 16/20] Fix lint error --- src/browser/vcs/vcs-settings/vcs-settings.component.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/browser/vcs/vcs-settings/vcs-settings.component.ts b/src/browser/vcs/vcs-settings/vcs-settings.component.ts index b79ce529..cda345c9 100644 --- a/src/browser/vcs/vcs-settings/vcs-settings.component.ts +++ b/src/browser/vcs/vcs-settings/vcs-settings.component.ts @@ -50,11 +50,14 @@ export class VcsSettingsComponent implements OnInit { ) { } + /* tslint:disable */ readonly accountMenuConvertFn = (value: VcsAccount): MenuItem => ({ id: value.email, label: `${value.name} <${value.email}>`, }); + /* tslint:enable */ + ngOnInit(): void { this.vcs.setRemoveProvider('github'); @@ -79,8 +82,8 @@ export class VcsSettingsComponent implements OnInit { } selectAccount(item: MenuItem): void { - const account = this.accounts.find(account => account.email === item.id); - this.remoteSettingForm.get('githubAccount').patchValue(account); + const fetchAccount = this.accounts.find(account => account.email === item.id); + this.remoteSettingForm.get('githubAccount').patchValue(fetchAccount); } async saveRemote(): Promise { From 073b7074a5da2b70b4afb83e2bf0f7df93e3bed6 Mon Sep 17 00:00:00 2001 From: seokju-na Date: Wed, 12 Dec 2018 01:02:44 +0900 Subject: [PATCH 17/20] Update only one settings dialog can be opened --- .../settings-dialog/settings-dialog.ts | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/src/browser/settings/settings-dialog/settings-dialog.ts b/src/browser/settings/settings-dialog/settings-dialog.ts index ff666821..96c25534 100644 --- a/src/browser/settings/settings-dialog/settings-dialog.ts +++ b/src/browser/settings/settings-dialog/settings-dialog.ts @@ -1,16 +1,28 @@ -import { Injectable } from '@angular/core'; +import { Injectable, OnDestroy } from '@angular/core'; +import { Subscription } from 'rxjs'; import { Dialog, DialogRef } from '../../ui/dialog'; import { SettingsDialogData } from './settings-dialog-data'; import { SettingsDialogComponent } from './settings-dialog.component'; @Injectable() -export class SettingsDialog { +export class SettingsDialog implements OnDestroy { + private dialogRef: DialogRef | null = null; + private dialogCloseSubscription = Subscription.EMPTY; + constructor(private dialog: Dialog) { } + ngOnDestroy(): void { + this.dialogCloseSubscription.unsubscribe(); + } + open(data?: SettingsDialogData): DialogRef { - return this.dialog.open( SettingsDialogComponent, @@ -21,5 +33,16 @@ export class SettingsDialog { data, }, ); + + // If subscription exists, unsubscribe first. + if (this.dialogCloseSubscription) { + this.dialogCloseSubscription.unsubscribe(); + } + + this.dialogCloseSubscription = this.dialogRef.afterClosed().subscribe(() => { + this.dialogRef = null; + }); + + return this.dialogRef; } } From ee1aa05948179b864757ef47341513c8dc8e2250 Mon Sep 17 00:00:00 2001 From: seokju-na Date: Wed, 12 Dec 2018 01:02:58 +0900 Subject: [PATCH 18/20] Add open settings menu event --- src/browser/shared/menu.service.ts | 3 +++ src/main-process/services/menu.service.ts | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/browser/shared/menu.service.ts b/src/browser/shared/menu.service.ts index c85d708b..83152a2b 100644 --- a/src/browser/shared/menu.service.ts +++ b/src/browser/shared/menu.service.ts @@ -14,6 +14,7 @@ export enum MenuEvent { CHANGE_EDITOR_VIEW_MODE_TO_EDITOR_ONLY = '[MenuEvent] Change editor view mode to editor only', CHANGE_EDITOR_VIEW_MODE_TO_PREVIEW_ONLY = '[MenuEvent] Change editor view mode to preview only', CHANGE_EDITOR_VIEW_MODE_TO_SHOW_BOTH = '[MenuEvent] Change editor view mode to show both', + OPEN_SETTINGS = '[MenuEvent] Open settings', } @@ -53,6 +54,8 @@ export class MenuService implements OnDestroy { return MenuEvent.CHANGE_EDITOR_VIEW_MODE_TO_PREVIEW_ONLY; case 'changeEditorViewModeToShowBoth': return MenuEvent.CHANGE_EDITOR_VIEW_MODE_TO_SHOW_BOTH; + case 'showSettings': + return MenuEvent.OPEN_SETTINGS; default: return null; } diff --git a/src/main-process/services/menu.service.ts b/src/main-process/services/menu.service.ts index c00d1b5f..ea4b0e91 100644 --- a/src/main-process/services/menu.service.ts +++ b/src/main-process/services/menu.service.ts @@ -70,7 +70,7 @@ export class MenuService extends Service { label: 'Preferences…', id: 'preferences', accelerator: 'CmdOrCtrl+,', - click: sendMessageOnClick('showPreferences'), + click: sendMessageOnClick('showSettings'), }, separator, { From 7561964311661c10a98f2eebc404a633f94d0028 Mon Sep 17 00:00:00 2001 From: seokju-na Date: Wed, 12 Dec 2018 01:03:12 +0900 Subject: [PATCH 19/20] Fix padding --- .../app-layout-sidenav/app-layout-sidenav.component.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/app/app-layout/app-layout-sidenav/app-layout-sidenav.component.scss b/src/browser/app/app-layout/app-layout-sidenav/app-layout-sidenav.component.scss index 1e0442b5..c04ac277 100644 --- a/src/browser/app/app-layout/app-layout-sidenav/app-layout-sidenav.component.scss +++ b/src/browser/app/app-layout/app-layout-sidenav/app-layout-sidenav.component.scss @@ -17,7 +17,7 @@ justify-content: space-between; width: $app-layout-sidenav-nav-size; height: 100%; - padding-bottom: $spacing; + padding-bottom: $spacing-half; } &__tabList { From 807811de6df5a0c6b1c5c632c87e55e170022a18 Mon Sep 17 00:00:00 2001 From: seokju-na Date: Wed, 12 Dec 2018 01:03:36 +0900 Subject: [PATCH 20/20] Add menu event listener for open settings dialog --- .../app-layout-sidenav.component.html | 3 +- .../app-layout-sidenav.component.spec.ts | 43 +++++++++++++++++++ .../app-layout-sidenav.component.ts | 14 ++++-- 3 files changed, 56 insertions(+), 4 deletions(-) diff --git a/src/browser/app/app-layout/app-layout-sidenav/app-layout-sidenav.component.html b/src/browser/app/app-layout/app-layout-sidenav/app-layout-sidenav.component.html index 484f81a9..8709bb66 100644 --- a/src/browser/app/app-layout/app-layout-sidenav/app-layout-sidenav.component.html +++ b/src/browser/app/app-layout/app-layout-sidenav/app-layout-sidenav.component.html @@ -17,7 +17,8 @@ - diff --git a/src/browser/app/app-layout/app-layout-sidenav/app-layout-sidenav.component.spec.ts b/src/browser/app/app-layout/app-layout-sidenav/app-layout-sidenav.component.spec.ts index fee55ca4..52009579 100644 --- a/src/browser/app/app-layout/app-layout-sidenav/app-layout-sidenav.component.spec.ts +++ b/src/browser/app/app-layout/app-layout-sidenav/app-layout-sidenav.component.spec.ts @@ -1,8 +1,12 @@ import { Component, NgModule } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { combineReducers, StoreModule } from '@ngrx/store'; +import { Subject } from 'rxjs'; import { fastTestSetup } from '../../../../../test/helpers'; +import { SettingsDialog } from '../../../settings'; +import { MenuEvent, MenuService } from '../../../shared'; import { AppLayoutModule } from '../app-layout.module'; import { appLayoutReducer } from '../app-layout.reducer'; import { AppLayoutSidenavOutlet } from '../app-layout.state'; @@ -53,6 +57,11 @@ describe('browser.app.appLayout.AppLayoutSidenavComponent', () => { let fixture: ComponentFixture; let component: AppLayoutSidenavComponent; + let settingsDialog: SettingsDialog; + let menu: MenuService; + + let menuMessages: Subject; + const outlets: AppLayoutSidenavOutlet[] = [ { id: 'outlet-1', @@ -75,17 +84,51 @@ describe('browser.app.appLayout.AppLayoutSidenavComponent', () => { fastTestSetup(); beforeAll(async () => { + settingsDialog = jasmine.createSpyObj('settingsDialog', [ + 'open', + ]); + menu = jasmine.createSpyObj('menu', [ + 'onMessage', + ]); + await TestBed .configureTestingModule({ imports: [TestAppLayoutSidenavModule], + providers: [ + { provide: SettingsDialog, useValue: settingsDialog }, + { provide: MenuService, useValue: menu }, + ], }) .compileComponents(); }); beforeEach(() => { + menuMessages = new Subject(); + (menu.onMessage as jasmine.Spy).and.callFake(() => menuMessages.asObservable()); + fixture = TestBed.createComponent(AppLayoutSidenavComponent); component = fixture.componentInstance; component.outlets = outlets; fixture.detectChanges(); }); + + describe('toggle panel', () => { + }); + + describe('settings', () => { + it('should open settings dialog when click settings button.', () => { + const settingsButtonEl = fixture.debugElement.query( + By.css('.AppLayoutSidenav__settingsButton'), + ).nativeElement as HTMLButtonElement; + + settingsButtonEl.click(); + + expect(settingsDialog.open).toHaveBeenCalled(); + }); + + it('should open settings dialog when menu event called.', () => { + menuMessages.next(MenuEvent.OPEN_SETTINGS); + expect(settingsDialog.open).toHaveBeenCalled(); + }); + }); }); diff --git a/src/browser/app/app-layout/app-layout-sidenav/app-layout-sidenav.component.ts b/src/browser/app/app-layout/app-layout-sidenav/app-layout-sidenav.component.ts index 06d3ed80..4a8f8a6d 100644 --- a/src/browser/app/app-layout/app-layout-sidenav/app-layout-sidenav.component.ts +++ b/src/browser/app/app-layout/app-layout-sidenav/app-layout-sidenav.component.ts @@ -1,8 +1,9 @@ -import { Component, Injector, Input } from '@angular/core'; +import { Component, Injector, Input, OnInit } from '@angular/core'; import { select, Store } from '@ngrx/store'; import { Observable } from 'rxjs'; -import { share } from 'rxjs/operators'; +import { filter, share } from 'rxjs/operators'; import { SettingsDialog } from '../../../settings'; +import { MenuEvent, MenuService } from '../../../shared'; import { AppState } from '../../app.state'; import { ToggleSidenavPanelAction } from '../app-layout.actions'; import { AppLayoutSidenavOutlet } from '../app-layout.state'; @@ -13,7 +14,7 @@ import { AppLayoutSidenavOutlet } from '../app-layout.state'; templateUrl: './app-layout-sidenav.component.html', styleUrls: ['./app-layout-sidenav.component.scss'], }) -export class AppLayoutSidenavComponent { +export class AppLayoutSidenavComponent implements OnInit { @Input() outlets: AppLayoutSidenavOutlet[]; readonly showPanel: Observable = this.store.pipe( @@ -30,9 +31,16 @@ export class AppLayoutSidenavComponent { private store: Store, public _injector: Injector, private settingsDialog: SettingsDialog, + private menu: MenuService, ) { } + ngOnInit(): void { + this.menu.onMessage().pipe( + filter(event => event === MenuEvent.OPEN_SETTINGS), + ).subscribe(() => this.openSettingsDialog()); + } + toggleServicePanel(outletId: string): void { this.store.dispatch(new ToggleSidenavPanelAction({ outletId })); }