From f281ecb9d73c2cf0b37236cd6dee27392b1b538b Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 2 Nov 2016 18:53:00 +0100 Subject: [PATCH] Temp version --- src/Squidex/app/app.component.html | 16 +- src/Squidex/app/app.component.scss | 17 ++- src/Squidex/app/app.component.spec.ts | 10 +- src/Squidex/app/app.component.ts | 6 +- src/Squidex/app/app.module.ts | 17 ++- src/Squidex/app/app.routes.ts | 8 +- .../components/apps/apps-page.component.html | 27 ++++ .../components/apps/apps-page.component.scss | 17 +++ .../components/apps/apps-page.component.ts | 22 +++ .../app/components/apps/apps.component.html | 3 - .../app/components/apps/apps.module.ts | 12 +- .../app/components/apps/declarations.ts | 2 +- src/Squidex/app/components/apps/index.ts | 3 +- src/Squidex/app/components/index.ts | 1 + .../components/layout/app-form.component.html | 31 ++++ .../components/layout/app-form.component.scss | 0 .../components/layout/app-form.component.ts | 91 +++++++++++ .../layout/apps-menu.component.html | 41 +++++ .../layout/apps-menu.component.scss | 33 ++++ .../components/layout/apps-menu.component.ts | 54 +++++++ .../app/components/layout/declarations.ts | 10 ++ src/Squidex/app/components/layout/index.ts | 10 ++ .../app/components/layout/layout.module.ts | 33 ++++ .../layout/search-form.component.html | 3 + .../layout/search-form.component.scss | 16 ++ .../search-form.component.ts} | 7 +- .../app/components/login/declarations.ts | 2 +- src/Squidex/app/components/login/index.ts | 3 +- ...mponent.html => login-page.component.html} | 0 ...n.component.ts => login-page.component.ts} | 2 +- .../app/components/login/login.module.ts | 10 +- .../app/framework/angular/animations.ts | 26 ++++ .../app/framework/angular/cloak.directive.ts | 4 +- .../angular/color-picker.component.ts | 2 +- .../framework/angular/drag-model.directive.ts | 2 +- .../framework/angular/image-drop.directive.ts | 2 +- .../framework/angular/modal-view.directive.ts | 69 +++++++++ .../framework/angular/shortcut.component.ts | 2 +- .../app/framework/angular/slider.component.ts | 2 +- .../framework/angular/spinner.component.ts | 2 +- .../angular/user-report.component.ts | 2 +- src/Squidex/app/framework/declarations.ts | 28 +++- src/Squidex/app/framework/framework.module.ts | 17 ++- src/Squidex/app/framework/index.ts | 26 +--- .../services/clipboard.service.spec.ts | 2 +- .../framework/services/clipboard.service.ts | 10 +- .../framework/services/drag.service.spec.ts | 2 +- .../app/framework/services/drag.service.ts | 2 +- .../app/framework/utils/modal-view.spec.ts | 65 ++++++++ src/Squidex/app/framework/utils/modal-view.ts | 38 +++++ src/Squidex/app/shared/index.ts | 2 + .../services/apps-store.service.spec.ts | 144 ++++++++++++++++++ .../app/shared/services/apps-store.service.ts | 67 ++++++++ .../app/shared/services/apps.service.ts | 20 ++- .../app/shared/services/auth.service.ts | 82 ++++++---- src/Squidex/app/theme/_bootstrap.scss | 83 ++++------ src/Squidex/app/theme/_vars.scss | 10 +- src/Squidex/app/theme/_vendor-overrides.scss | 7 + src/Squidex/app/theme/vendor.scss | 2 +- src/Squidex/app/vendor.ts | 5 +- src/Squidex/wwwroot/index.html | 2 +- 61 files changed, 1052 insertions(+), 182 deletions(-) create mode 100644 src/Squidex/app/components/apps/apps-page.component.html create mode 100644 src/Squidex/app/components/apps/apps-page.component.scss create mode 100644 src/Squidex/app/components/apps/apps-page.component.ts delete mode 100644 src/Squidex/app/components/apps/apps.component.html create mode 100644 src/Squidex/app/components/layout/app-form.component.html create mode 100644 src/Squidex/app/components/layout/app-form.component.scss create mode 100644 src/Squidex/app/components/layout/app-form.component.ts create mode 100644 src/Squidex/app/components/layout/apps-menu.component.html create mode 100644 src/Squidex/app/components/layout/apps-menu.component.scss create mode 100644 src/Squidex/app/components/layout/apps-menu.component.ts create mode 100644 src/Squidex/app/components/layout/declarations.ts create mode 100644 src/Squidex/app/components/layout/index.ts create mode 100644 src/Squidex/app/components/layout/layout.module.ts create mode 100644 src/Squidex/app/components/layout/search-form.component.html create mode 100644 src/Squidex/app/components/layout/search-form.component.scss rename src/Squidex/app/components/{apps/apps.component.ts => layout/search-form.component.ts} (67%) rename src/Squidex/app/components/login/{login.component.html => login-page.component.html} (100%) rename src/Squidex/app/components/login/{login.component.ts => login-page.component.ts} (92%) create mode 100644 src/Squidex/app/framework/angular/animations.ts create mode 100644 src/Squidex/app/framework/angular/modal-view.directive.ts create mode 100644 src/Squidex/app/framework/utils/modal-view.spec.ts create mode 100644 src/Squidex/app/framework/utils/modal-view.ts create mode 100644 src/Squidex/app/shared/services/apps-store.service.spec.ts create mode 100644 src/Squidex/app/shared/services/apps-store.service.ts create mode 100644 src/Squidex/app/theme/_vendor-overrides.scss diff --git a/src/Squidex/app/app.component.html b/src/Squidex/app/app.component.html index 16227ef88..157c6ce2d 100644 --- a/src/Squidex/app/app.component.html +++ b/src/Squidex/app/app.component.html @@ -1 +1,15 @@ - \ No newline at end of file + + +
+ +
diff --git a/src/Squidex/app/app.component.scss b/src/Squidex/app/app.component.scss index 040e5e425..01319f4c0 100644 --- a/src/Squidex/app/app.component.scss +++ b/src/Squidex/app/app.component.scss @@ -1,8 +1,13 @@ +@import 'theme/mixins.scss'; + +.navbar { + @include box-shadow(0, 4px, 4px, 0.2px); +} + +.search-form { + margin-left: 15px; +} + main { - padding: 1em; - font-family: Arial, Helvetica, sans-serif; - font-size: 1.1rem; - text-align: center; - margin-top: 50px; - display: block; + margin-top: 54px } \ No newline at end of file diff --git a/src/Squidex/app/app.component.spec.ts b/src/Squidex/app/app.component.spec.ts index d6fc9c607..0cc8bde5a 100644 --- a/src/Squidex/app/app.component.spec.ts +++ b/src/Squidex/app/app.component.spec.ts @@ -3,6 +3,10 @@ import { TestBed } from '@angular/core/testing'; import { RouterModule, provideRoutes } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; +import { SqxLayoutModule } from './components/layout'; + +import { AppsStoreService } from './shared'; + import { AppComponent } from './app.component'; describe('App', () => { @@ -13,10 +17,12 @@ describe('App', () => { ], imports: [ RouterModule, - RouterTestingModule + RouterTestingModule, + SqxLayoutModule ], providers: [ - provideRoutes([]) + provideRoutes([]), + { provide: AppsStoreService, useValue: new AppsStoreService(null, null) } ] }); }); diff --git a/src/Squidex/app/app.component.ts b/src/Squidex/app/app.component.ts index 9ed0b28d8..92f567eaf 100644 --- a/src/Squidex/app/app.component.ts +++ b/src/Squidex/app/app.component.ts @@ -8,8 +8,8 @@ import * as Ng2 from '@angular/core'; @Ng2.Component({ - selector: 'my-app', - template, - styles + selector: 'sqx-app', + styles, + template }) export class AppComponent { } \ No newline at end of file diff --git a/src/Squidex/app/app.module.ts b/src/Squidex/app/app.module.ts index d5d3c3b23..01b21d2b1 100644 --- a/src/Squidex/app/app.module.ts +++ b/src/Squidex/app/app.module.ts @@ -19,15 +19,20 @@ import { } from './framework'; import { + AppsStoreService, + AppsService, AuthGuard, AuthService, } from './shared'; import { - MyAppModule, - MyLoginModule + SqxAppModule, + SqxLayoutModule, + SqxLoginModule } from './components'; +import { SqxFrameworkModule } from './framework'; + import { routing } from './app.routes'; const baseUrl = window.location.protocol + '//' + window.location.host + '/'; @@ -35,14 +40,18 @@ const baseUrl = window.location.protocol + '//' + window.location.host + '/'; @Ng2.NgModule({ imports: [ Ng2Browser.BrowserModule, - MyAppModule, - MyLoginModule, + SqxAppModule, + SqxLayoutModule, + SqxLoginModule, + SqxFrameworkModule, routing ], declarations: [ AppComponent ], providers: [ + AppsStoreService, + AppsService, AuthGuard, AuthService, { provide: ApiUrlConfig, useValue: new ApiUrlConfig(baseUrl) }, diff --git a/src/Squidex/app/app.routes.ts b/src/Squidex/app/app.routes.ts index 513aeafa4..b74d4f669 100644 --- a/src/Squidex/app/app.routes.ts +++ b/src/Squidex/app/app.routes.ts @@ -9,8 +9,8 @@ import * as Ng2 from '@angular/core'; import * as Ng2Router from '@angular/router'; import { - AppsComponent, - LoginComponent + AppsPageComponent, + LoginPageComponent } from './components'; import { @@ -25,12 +25,12 @@ export const routes: Ng2Router.Routes = [ }, { path: 'apps', - component: AppsComponent, + component: AppsPageComponent, canActivate: [AuthGuard] }, { path: 'login', - component: LoginComponent + component: LoginPageComponent } ]; diff --git a/src/Squidex/app/components/apps/apps-page.component.html b/src/Squidex/app/components/apps/apps-page.component.html new file mode 100644 index 000000000..26449ea61 --- /dev/null +++ b/src/Squidex/app/components/apps/apps-page.component.html @@ -0,0 +1,27 @@ + +
+

You are not collaborating to any app yet

+ + +
+
+ + \ No newline at end of file diff --git a/src/Squidex/app/components/apps/apps-page.component.scss b/src/Squidex/app/components/apps/apps-page.component.scss new file mode 100644 index 000000000..3b5bb8840 --- /dev/null +++ b/src/Squidex/app/components/apps/apps-page.component.scss @@ -0,0 +1,17 @@ +@import '../../theme/_vars.scss'; +@import '../../theme/_mixins.scss'; + +content { + padding: 20px; +} + +.apps-empty { + & { + text-align: center; + } + + &-headline { + margin-top: 100px; + margin-bottom: 20px; + } +} \ No newline at end of file diff --git a/src/Squidex/app/components/apps/apps-page.component.ts b/src/Squidex/app/components/apps/apps-page.component.ts new file mode 100644 index 000000000..3a4bb1aff --- /dev/null +++ b/src/Squidex/app/components/apps/apps-page.component.ts @@ -0,0 +1,22 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import * as Ng2 from '@angular/core'; + +import { fadeAnimation, ModalView } from './../../framework'; + +@Ng2.Component({ + selector: 'sqx-apps-page', + styles, + template, + animations: [ + fadeAnimation() + ] +}) +export class AppsPageComponent { + public modalDialog = new ModalView(); +} \ No newline at end of file diff --git a/src/Squidex/app/components/apps/apps.component.html b/src/Squidex/app/components/apps/apps.component.html deleted file mode 100644 index aa8f55823..000000000 --- a/src/Squidex/app/components/apps/apps.component.html +++ /dev/null @@ -1,3 +0,0 @@ -
-

Hello from Angular 2 App with Webpack!

-
\ No newline at end of file diff --git a/src/Squidex/app/components/apps/apps.module.ts b/src/Squidex/app/components/apps/apps.module.ts index f74f3f618..fbeeb6850 100644 --- a/src/Squidex/app/components/apps/apps.module.ts +++ b/src/Squidex/app/components/apps/apps.module.ts @@ -7,18 +7,20 @@ import * as Ng2 from '@angular/core'; -import { FrameworkModule } from './../../framework'; +import { SqxFrameworkModule } from './../../framework'; +import { SqxLayoutModule } from './../layout'; import { - AppsComponent + AppsPageComponent } from './declarations'; @Ng2.NgModule({ imports: [ - FrameworkModule + SqxFrameworkModule, + SqxLayoutModule ], declarations: [ - AppsComponent + AppsPageComponent ] }) -export class MyAppModule { } \ No newline at end of file +export class SqxAppModule { } \ No newline at end of file diff --git a/src/Squidex/app/components/apps/declarations.ts b/src/Squidex/app/components/apps/declarations.ts index cb1bc8d45..b8ee4800a 100644 --- a/src/Squidex/app/components/apps/declarations.ts +++ b/src/Squidex/app/components/apps/declarations.ts @@ -5,4 +5,4 @@ * Copyright (c) Sebastian Stehle. All rights reserved */ -export * from './apps.component'; \ No newline at end of file +export * from './apps-page.component'; \ No newline at end of file diff --git a/src/Squidex/app/components/apps/index.ts b/src/Squidex/app/components/apps/index.ts index 46ecc5b9e..8b961c614 100644 --- a/src/Squidex/app/components/apps/index.ts +++ b/src/Squidex/app/components/apps/index.ts @@ -5,5 +5,6 @@ * Copyright (c) Sebastian Stehle. All rights reserved */ -export * from './apps.component'; +export * from './declarations'; + export * from './apps.module'; \ No newline at end of file diff --git a/src/Squidex/app/components/index.ts b/src/Squidex/app/components/index.ts index a856e83bb..61702b4e3 100644 --- a/src/Squidex/app/components/index.ts +++ b/src/Squidex/app/components/index.ts @@ -6,4 +6,5 @@ */ export * from './apps'; +export * from './layout'; export * from './login'; \ No newline at end of file diff --git a/src/Squidex/app/components/layout/app-form.component.html b/src/Squidex/app/components/layout/app-form.component.html new file mode 100644 index 000000000..53c42f3cc --- /dev/null +++ b/src/Squidex/app/components/layout/app-form.component.html @@ -0,0 +1,31 @@ +
+
+ + +
+
+ + Name is required. + + + Name can not have more than 40 characters. + + + Name can contain lower case letters (a-z), numbers and dashes only. + +
+
+ + + + + The app name becomes part of the api url, e.g, https://{{appName | async}}.squidex.io/.
+ It must contain lower case letters (a-z), numbers and dashes only, and cannot be longer than 40 characters. The name cannot be changed later. +
+
+ +
+ + +
+
\ No newline at end of file diff --git a/src/Squidex/app/components/layout/app-form.component.scss b/src/Squidex/app/components/layout/app-form.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/Squidex/app/components/layout/app-form.component.ts b/src/Squidex/app/components/layout/app-form.component.ts new file mode 100644 index 000000000..41e90f31e --- /dev/null +++ b/src/Squidex/app/components/layout/app-form.component.ts @@ -0,0 +1,91 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import * as Ng2 from '@angular/core'; +import * as Ng2Forms from '@angular/forms'; + +import { Observable } from 'rxjs'; + +import { fadeAnimation } from './../../framework'; + +import { AppCreateDto, AppsStoreService } from './../../shared'; + +const FALLBACK_NAME = 'my-app'; + +@Ng2.Component({ + selector: 'sqx-app-form', + styles, + template, + animations: [ + fadeAnimation() + ] +}) +export class AppFormComponent implements Ng2.OnInit { + public createForm: Ng2Forms.FormGroup; + + public appName: Observable; + + @Ng2.Input() + public showClose = false; + + @Ng2.Output() + public onCreated = new Ng2.EventEmitter(); + + @Ng2.Output() + public onCancelled = new Ng2.EventEmitter(); + + public creating = new Ng2.EventEmitter(); + + public creationError = new Ng2.EventEmitter(); + + constructor( + private readonly appsStore: AppsStoreService, + private readonly formBuilder: Ng2Forms.FormBuilder + ) { + } + + public ngOnInit() { + this.createForm = this.formBuilder.group({ + name: ['', + [ + Ng2Forms.Validators.required, + Ng2Forms.Validators.maxLength(40), + Ng2Forms.Validators.pattern('[a-z0-9]+(\-[a-z0-9]+)*'), + ]] + }); + + this.appName = this.createForm.controls['name'].valueChanges.map(name => name || FALLBACK_NAME).publishBehavior(FALLBACK_NAME).refCount(); + } + + public submit() { + if (this.createForm.valid) { + this.createForm.disable(); + this.creating.emit(true); + + const dto = new AppCreateDto(this.createForm.controls['name'].value); + + this.appsStore.createApp(dto) + .finally(() => { + this.reset(); + }) + .subscribe(() => { + this.onCreated.emit(); + }, error => { + this.creationError.emit(error); + }); + } + } + + private reset() { + this.createForm.enable(); + this.creating.emit(false); + } + + public cancel() { + this.onCancelled.emit(); + } +} \ No newline at end of file diff --git a/src/Squidex/app/components/layout/apps-menu.component.html b/src/Squidex/app/components/layout/apps-menu.component.html new file mode 100644 index 000000000..d61412a7f --- /dev/null +++ b/src/Squidex/app/components/layout/apps-menu.component.html @@ -0,0 +1,41 @@ + + + \ No newline at end of file diff --git a/src/Squidex/app/components/layout/apps-menu.component.scss b/src/Squidex/app/components/layout/apps-menu.component.scss new file mode 100644 index 000000000..d6e18b3eb --- /dev/null +++ b/src/Squidex/app/components/layout/apps-menu.component.scss @@ -0,0 +1,33 @@ +@import '../../theme/_vars.scss'; +@import '../../theme/_mixins.scss'; + +.navbar-dark .navbar-nav .nav-link { + color: white; +} + +.nav-link { + @include truncate(); +} + +.drodown-button { + padding: 3px 1.5rem; +} + +#app-name { + & { + padding-right: 15px; + @include transition(opacity 0.4 ease); + @include opacity(0.95); + color: white; + cursor: pointer; + width: 200px; + } + + &:hover { + @include opacity(1); + } + + &:after { + @include absolute(50%, 0px, auto, auto); + } +} \ No newline at end of file diff --git a/src/Squidex/app/components/layout/apps-menu.component.ts b/src/Squidex/app/components/layout/apps-menu.component.ts new file mode 100644 index 000000000..da8e8ff11 --- /dev/null +++ b/src/Squidex/app/components/layout/apps-menu.component.ts @@ -0,0 +1,54 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import * as Ng2 from '@angular/core'; + +import { + AppDto, + AppsStoreService +} from './../../shared'; + +import { fadeAnimation, ModalView } from './../../framework'; + +@Ng2.Component({ + selector: 'sqx-apps-menu', + styles, + template, + animations: [ + fadeAnimation() + ] +}) +export class AppsMenuComponent implements Ng2.OnInit, Ng2.OnDestroy { + private subscription: any | null = null; + + public modalMenu = new ModalView(); + public modalDialog = new ModalView(); + + public apps: AppDto[] | null = null; + + constructor( + private readonly appsStore: AppsStoreService + ) { + } + + public ngOnInit() { + this.subscription = this.appsStore.appsChanges.subscribe(apps => { + this.apps = apps; + }); + } + + public ngOnDestroy() { + if (this.subscription) { + this.subscription.unsubscribe(); + } + } + + public createApp() { + this.modalMenu.hide(); + this.modalDialog.show(); + } +} \ No newline at end of file diff --git a/src/Squidex/app/components/layout/declarations.ts b/src/Squidex/app/components/layout/declarations.ts new file mode 100644 index 000000000..924233228 --- /dev/null +++ b/src/Squidex/app/components/layout/declarations.ts @@ -0,0 +1,10 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +export * from './app-form.component'; +export * from './apps-menu.component'; +export * from './search-form.component'; \ No newline at end of file diff --git a/src/Squidex/app/components/layout/index.ts b/src/Squidex/app/components/layout/index.ts new file mode 100644 index 000000000..3f66f638f --- /dev/null +++ b/src/Squidex/app/components/layout/index.ts @@ -0,0 +1,10 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +export * from './declarations'; + +export * from './layout.module'; \ No newline at end of file diff --git a/src/Squidex/app/components/layout/layout.module.ts b/src/Squidex/app/components/layout/layout.module.ts new file mode 100644 index 000000000..5c0ecfdf6 --- /dev/null +++ b/src/Squidex/app/components/layout/layout.module.ts @@ -0,0 +1,33 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import * as Ng2 from '@angular/core'; + +import { SqxFrameworkModule } from './../../framework'; + +import { + AppFormComponent, + AppsMenuComponent, + SearchFormComponent +} from './declarations'; + +@Ng2.NgModule({ + imports: [ + SqxFrameworkModule + ], + declarations: [ + AppFormComponent, + AppsMenuComponent, + SearchFormComponent, + ], + exports: [ + AppFormComponent, + AppsMenuComponent, + SearchFormComponent, + ] +}) +export class SqxLayoutModule { } \ No newline at end of file diff --git a/src/Squidex/app/components/layout/search-form.component.html b/src/Squidex/app/components/layout/search-form.component.html new file mode 100644 index 000000000..3559a01f1 --- /dev/null +++ b/src/Squidex/app/components/layout/search-form.component.html @@ -0,0 +1,3 @@ +
+ +
\ No newline at end of file diff --git a/src/Squidex/app/components/layout/search-form.component.scss b/src/Squidex/app/components/layout/search-form.component.scss new file mode 100644 index 000000000..b703e4d4b --- /dev/null +++ b/src/Squidex/app/components/layout/search-form.component.scss @@ -0,0 +1,16 @@ +@import '../../theme/_vars.scss'; +@import '../../theme/_mixins.scss'; + +.search { + & { + @include transition(background 0.4s ease); + color: white; + background: $accent-blue-dark; + border-color: $accent-blue-dark; + border-width: 1px; + } + + &:focus { + background: darken($accent-blue-dark, 5%); + } +} \ No newline at end of file diff --git a/src/Squidex/app/components/apps/apps.component.ts b/src/Squidex/app/components/layout/search-form.component.ts similarity index 67% rename from src/Squidex/app/components/apps/apps.component.ts rename to src/Squidex/app/components/layout/search-form.component.ts index 84f9fd40e..212e8d4b6 100644 --- a/src/Squidex/app/components/apps/apps.component.ts +++ b/src/Squidex/app/components/layout/search-form.component.ts @@ -1,4 +1,4 @@ -/* +/* * Squidex Headless CMS * * @license @@ -8,9 +8,10 @@ import * as Ng2 from '@angular/core'; @Ng2.Component({ - selector: 'apps', + selector: 'sqx-search-form', + styles, template }) -export class AppsComponent { +export class SearchFormComponent { } \ No newline at end of file diff --git a/src/Squidex/app/components/login/declarations.ts b/src/Squidex/app/components/login/declarations.ts index c3fdc28a2..830720b45 100644 --- a/src/Squidex/app/components/login/declarations.ts +++ b/src/Squidex/app/components/login/declarations.ts @@ -5,4 +5,4 @@ * Copyright (c) Sebastian Stehle. All rights reserved */ -export * from './login.component'; \ No newline at end of file +export * from './login-page.component'; \ No newline at end of file diff --git a/src/Squidex/app/components/login/index.ts b/src/Squidex/app/components/login/index.ts index 9451cbeda..4ec27fcf1 100644 --- a/src/Squidex/app/components/login/index.ts +++ b/src/Squidex/app/components/login/index.ts @@ -5,5 +5,6 @@ * Copyright (c) Sebastian Stehle. All rights reserved */ -export * from './login.component'; +export * from './declarations'; + export * from './login.module'; \ No newline at end of file diff --git a/src/Squidex/app/components/login/login.component.html b/src/Squidex/app/components/login/login-page.component.html similarity index 100% rename from src/Squidex/app/components/login/login.component.html rename to src/Squidex/app/components/login/login-page.component.html diff --git a/src/Squidex/app/components/login/login.component.ts b/src/Squidex/app/components/login/login-page.component.ts similarity index 92% rename from src/Squidex/app/components/login/login.component.ts rename to src/Squidex/app/components/login/login-page.component.ts index a0bedcfd1..d569945bf 100644 --- a/src/Squidex/app/components/login/login.component.ts +++ b/src/Squidex/app/components/login/login-page.component.ts @@ -14,7 +14,7 @@ import { AuthService } from './../../shared'; selector: 'login', template }) -export class LoginComponent implements Ng2.OnInit { +export class LoginPageComponent implements Ng2.OnInit { public showFailedError = false; constructor( diff --git a/src/Squidex/app/components/login/login.module.ts b/src/Squidex/app/components/login/login.module.ts index 4ebbe7d3f..add4240f6 100644 --- a/src/Squidex/app/components/login/login.module.ts +++ b/src/Squidex/app/components/login/login.module.ts @@ -7,18 +7,18 @@ import * as Ng2 from '@angular/core'; -import { FrameworkModule } from './../../framework'; +import { SqxFrameworkModule } from './../../framework'; import { - LoginComponent + LoginPageComponent } from './declarations'; @Ng2.NgModule({ imports: [ - FrameworkModule + SqxFrameworkModule ], declarations: [ - LoginComponent + LoginPageComponent ] }) -export class MyLoginModule { } \ No newline at end of file +export class SqxLoginModule { } \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/animations.ts b/src/Squidex/app/framework/angular/animations.ts new file mode 100644 index 000000000..9c08d3350 --- /dev/null +++ b/src/Squidex/app/framework/angular/animations.ts @@ -0,0 +1,26 @@ + +import * as Ng2 from '@angular/core'; + +export const fadeAnimation = (name = 'fade', timing = '200ms'): Ng2.AnimationEntryMetadata => { + return Ng2.trigger( + name, [ + Ng2.transition(':enter', [ + Ng2.style({ opacity: 0 }), + Ng2.animate(timing, Ng2.style({ opacity: 1 })) + ]), + Ng2.transition(':leave', [ + Ng2.style({ 'opacity': 1 }), + Ng2.animate(timing, Ng2.style({ opacity: 0 })) + ]), + Ng2.state('true', + Ng2.style({ opacity: 1 }) + ), + Ng2.state('false', + Ng2.style({ opacity: 0 }) + ), + Ng2.transition('1 => 0', Ng2.animate(timing)), + Ng2.transition('0 => 1', Ng2.animate(timing)) + ] + ); +} + \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/cloak.directive.ts b/src/Squidex/app/framework/angular/cloak.directive.ts index 359f6668c..6b6e6398b 100644 --- a/src/Squidex/app/framework/angular/cloak.directive.ts +++ b/src/Squidex/app/framework/angular/cloak.directive.ts @@ -8,12 +8,12 @@ import * as Ng2 from '@angular/core'; @Ng2.Directive({ - selector: '.gp-cloak' + selector: '.sqx-cloak' }) export class CloakDirective implements Ng2.OnInit { constructor(private readonly element: Ng2.ElementRef) { } public ngOnInit() { - this.element.nativeElement.classList.remove('gp-cloak'); + this.element.nativeElement.classList.remove('sqx-cloak'); } } \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/color-picker.component.ts b/src/Squidex/app/framework/angular/color-picker.component.ts index 0020b36a8..70b8d94b5 100644 --- a/src/Squidex/app/framework/angular/color-picker.component.ts +++ b/src/Squidex/app/framework/angular/color-picker.component.ts @@ -16,7 +16,7 @@ import { } from './../utils/color-palette'; @Ng2.Component({ - selector: 'gp-color-picker', + selector: 'sqx-color-picker', styles, template }) diff --git a/src/Squidex/app/framework/angular/drag-model.directive.ts b/src/Squidex/app/framework/angular/drag-model.directive.ts index a918dcfc4..59fe9212a 100644 --- a/src/Squidex/app/framework/angular/drag-model.directive.ts +++ b/src/Squidex/app/framework/angular/drag-model.directive.ts @@ -77,7 +77,7 @@ export class DragModelDirective { let dropCandidate: Element | null = document.elementFromPoint(event.clientX, event.clientY); - while (dropCandidate && (dropCandidate.classList && !dropCandidate.classList.contains('gp-drop'))) { + while (dropCandidate && (dropCandidate.classList && !dropCandidate.classList.contains('sqx-drop'))) { dropCandidate = dropCandidate.parentNode as Element; } diff --git a/src/Squidex/app/framework/angular/image-drop.directive.ts b/src/Squidex/app/framework/angular/image-drop.directive.ts index 1caa7d347..8b057f77b 100644 --- a/src/Squidex/app/framework/angular/image-drop.directive.ts +++ b/src/Squidex/app/framework/angular/image-drop.directive.ts @@ -11,7 +11,7 @@ import { DragService } from './../services/drag.service'; import { Vec2 } from './../utils/vec2'; @Ng2.Directive({ - selector: '.gp-image-drop' + selector: '.sqx-image-drop' }) export class ImageDropDirective { constructor( diff --git a/src/Squidex/app/framework/angular/modal-view.directive.ts b/src/Squidex/app/framework/angular/modal-view.directive.ts new file mode 100644 index 000000000..4e76e2985 --- /dev/null +++ b/src/Squidex/app/framework/angular/modal-view.directive.ts @@ -0,0 +1,69 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import * as Ng2 from '@angular/core'; + +import { ModalView } from './../utils/modal-view'; + +@Ng2.Directive({ + selector: '[sqxModalView]' +}) +export class ModalViewDirective implements Ng2.OnChanges { + private subscription: any | null; + private isEnabled = true; + + @Ng2.Input('sqxModalView') + public modalView: ModalView; + + constructor( + private readonly elementRef: Ng2.ElementRef, + private readonly renderer: Ng2.Renderer, + ) { + } + + @Ng2.HostListener('document:click', ['$event', '$event.target']) + public clickOutside(event: MouseEvent, targetElement: HTMLElement) { + if (!targetElement) { + return; + } + + const clickedInside = this.elementRef.nativeElement.contains(targetElement); + + if (!clickedInside && this.modalView && this.isEnabled) { + this.modalView.hide(); + } + } + + public ngOnChanges() { + if (this.subscription) { + this.subscription.unsubscribe(); + this.subscription = null; + } + + if (this.modalView) { + this.subscription = this.modalView.isOpenChanges.subscribe(isOpen => { + if (this.isEnabled) { + if (isOpen) { + this.renderer.setElementStyle(this.elementRef.nativeElement, 'display', 'block'); + } else { + this.renderer.setElementStyle(this.elementRef.nativeElement, 'display', 'none'); + } + + this.updateEnabled(); + } + }); + } + } + + private updateEnabled() { + this.isEnabled = false; + + setTimeout(() => { + this.isEnabled = true; + }, 500); + } +} \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/shortcut.component.ts b/src/Squidex/app/framework/angular/shortcut.component.ts index e947b97c1..52e1dcef5 100644 --- a/src/Squidex/app/framework/angular/shortcut.component.ts +++ b/src/Squidex/app/framework/angular/shortcut.component.ts @@ -10,7 +10,7 @@ import * as Ng2 from '@angular/core'; import { ShortcutService } from './../services/shortcut.service'; @Ng2.Component({ - selector: 'gp-shortcut', + selector: 'sqx-shortcut', template: '' }) export class ShortcutComponent implements Ng2.OnInit, Ng2.OnDestroy { diff --git a/src/Squidex/app/framework/angular/slider.component.ts b/src/Squidex/app/framework/angular/slider.component.ts index 6ee092926..99cdf5f43 100644 --- a/src/Squidex/app/framework/angular/slider.component.ts +++ b/src/Squidex/app/framework/angular/slider.component.ts @@ -8,7 +8,7 @@ import * as Ng2 from '@angular/core'; @Ng2.Component({ - selector: 'gp-slider', + selector: 'sqx-slider', styles, template }) diff --git a/src/Squidex/app/framework/angular/spinner.component.ts b/src/Squidex/app/framework/angular/spinner.component.ts index 8517df0a7..431dd1c8d 100644 --- a/src/Squidex/app/framework/angular/spinner.component.ts +++ b/src/Squidex/app/framework/angular/spinner.component.ts @@ -10,7 +10,7 @@ import * as Ng2 from '@angular/core'; declare var Spinner: any; @Ng2.Component({ - selector: 'gp-spinner', + selector: 'sqx-spinner', template: '' }) export class SpinnerComponent { diff --git a/src/Squidex/app/framework/angular/user-report.component.ts b/src/Squidex/app/framework/angular/user-report.component.ts index 27bf2f611..54e83a24f 100644 --- a/src/Squidex/app/framework/angular/user-report.component.ts +++ b/src/Squidex/app/framework/angular/user-report.component.ts @@ -10,7 +10,7 @@ import * as Ng2 from '@angular/core'; import { UserReportConfig } from './../configurations'; @Ng2.Component({ - selector: 'gp-user-report', + selector: 'sqx-user-report', template: '' }) export class UserReportComponent implements Ng2.OnInit { diff --git a/src/Squidex/app/framework/declarations.ts b/src/Squidex/app/framework/declarations.ts index 58bcedfaf..207d46e55 100644 --- a/src/Squidex/app/framework/declarations.ts +++ b/src/Squidex/app/framework/declarations.ts @@ -5,16 +5,42 @@ * Copyright (c) Sebastian Stehle. All rights reserved */ +export * from './angular/action'; +export * from './angular/animations'; +export * from './angular/validators'; export * from './angular/cloak.directive'; export * from './angular/color-picker.component'; export * from './angular/date-time.pipes'; export * from './angular/drag-model.directive'; export * from './angular/focus-on-change.directive'; export * from './angular/image-drop.directive'; +export * from './angular/modal-view.directive'; export * from './angular/money.pipe'; export * from './angular/shortcut.component'; export * from './angular/slider.component'; export * from './angular/spinner.component'; export * from './angular/user-report.component'; +export * from './configurations'; + +export * from './services/clipboard.service'; export * from './services/drag.service'; -export * from './services/title.service'; \ No newline at end of file +export * from './services/local-store.service'; +export * from './services/shortcut.service'; +export * from './services/title.service'; + +export * from './plattform'; + +export * from './utils/color'; +export * from './utils/color-palette'; +export * from './utils/date-helper'; +export * from './utils/date-time'; +export * from './utils/duration'; +export * from './utils/immutable-id-map'; +export * from './utils/immutable-list'; +export * from './utils/immutable-object'; +export * from './utils/immutable-set'; +export * from './utils/math-helper'; +export * from './utils/modal-view'; +export * from './utils/rotation'; +export * from './utils/vec2'; +export * from './utils/rect2'; \ No newline at end of file diff --git a/src/Squidex/app/framework/framework.module.ts b/src/Squidex/app/framework/framework.module.ts index cd9909f30..3e2482e16 100644 --- a/src/Squidex/app/framework/framework.module.ts +++ b/src/Squidex/app/framework/framework.module.ts @@ -16,10 +16,9 @@ import { ColorPickerComponent, DayOfWeekPipe, DayPipe, - DragModelDirective, DurationPipe, FocusOnChangeDirective, - ImageDropDirective, + ModalViewDirective, MoneyPipe, MonthPipe, ShortcutComponent, @@ -32,18 +31,20 @@ import { @Ng2.NgModule({ imports: [ + Ng2Http.HttpModule, Ng2Forms.FormsModule, - Ng2Common.CommonModule + Ng2Forms.ReactiveFormsModule, + Ng2Common.CommonModule, + Ng2Router.RouterModule ], declarations: [ CloakDirective, ColorPickerComponent, DayOfWeekPipe, DayPipe, - DragModelDirective, DurationPipe, FocusOnChangeDirective, - ImageDropDirective, + ModalViewDirective, MoneyPipe, MonthPipe, ShortcutComponent, @@ -51,7 +52,7 @@ import { ShortTimePipe, SliderComponent, SpinnerComponent, - UserReportComponent + UserReportComponent, ], exports: [ CloakDirective, @@ -60,6 +61,7 @@ import { DayPipe, DurationPipe, FocusOnChangeDirective, + ModalViewDirective, MoneyPipe, MonthPipe, ShortcutComponent, @@ -70,8 +72,9 @@ import { UserReportComponent, Ng2Http.HttpModule, Ng2Forms.FormsModule, + Ng2Forms.ReactiveFormsModule, Ng2Common.CommonModule, Ng2Router.RouterModule ] }) -export class FrameworkModule { } \ No newline at end of file +export class SqxFrameworkModule { } \ No newline at end of file diff --git a/src/Squidex/app/framework/index.ts b/src/Squidex/app/framework/index.ts index b5a8af614..7b39fb2ac 100644 --- a/src/Squidex/app/framework/index.ts +++ b/src/Squidex/app/framework/index.ts @@ -5,26 +5,6 @@ * Copyright (c) Sebastian Stehle. All rights reserved */ -export * from './angular/action'; -export * from './angular/validators'; -export * from './configurations'; -export * from './framework.module'; -export * from './plattform'; -export * from './services/clipboard.service'; -export * from './services/drag.service'; -export * from './services/local-store.service'; -export * from './services/shortcut.service'; -export * from './services/title.service'; -export * from './utils/color'; -export * from './utils/color-palette'; -export * from './utils/date-helper'; -export * from './utils/date-time'; -export * from './utils/duration'; -export * from './utils/immutable-id-map'; -export * from './utils/immutable-list'; -export * from './utils/immutable-object'; -export * from './utils/immutable-set'; -export * from './utils/math-helper'; -export * from './utils/rotation'; -export * from './utils/vec2'; -export * from './utils/rect2'; \ No newline at end of file +export * from './declarations'; + +export * from './framework.module'; \ No newline at end of file diff --git a/src/Squidex/app/framework/services/clipboard.service.spec.ts b/src/Squidex/app/framework/services/clipboard.service.spec.ts index 7a3ba0fc4..3c80cfaf5 100644 --- a/src/Squidex/app/framework/services/clipboard.service.spec.ts +++ b/src/Squidex/app/framework/services/clipboard.service.spec.ts @@ -40,7 +40,7 @@ describe('ShortcutService', () => { let text = ''; - clipboardService.text.subscribe(t => { + clipboardService.textChanges.subscribe(t => { text = t; }); diff --git a/src/Squidex/app/framework/services/clipboard.service.ts b/src/Squidex/app/framework/services/clipboard.service.ts index 8d5ee5c92..ff3675ca2 100644 --- a/src/Squidex/app/framework/services/clipboard.service.ts +++ b/src/Squidex/app/framework/services/clipboard.service.ts @@ -15,16 +15,16 @@ export const ClipboardServiceFactory = () => { @Ng2.Injectable() export class ClipboardService { - private textInstance = new BehaviorSubject(''); + private readonly text$ = new BehaviorSubject(''); - public get text(): Observable { - return this.textInstance; + public get textChanges(): Observable { + return this.text$; } public selectText(): string { let result = ''; - this.textInstance.subscribe(t => { + this.text$.subscribe(t => { result = t; }).unsubscribe(); @@ -32,6 +32,6 @@ export class ClipboardService { } public setText(text: any) { - this.textInstance.next(text); + this.text$.next(text); } } \ No newline at end of file diff --git a/src/Squidex/app/framework/services/drag.service.spec.ts b/src/Squidex/app/framework/services/drag.service.spec.ts index 158bf554f..418a51dd7 100644 --- a/src/Squidex/app/framework/services/drag.service.spec.ts +++ b/src/Squidex/app/framework/services/drag.service.spec.ts @@ -29,7 +29,7 @@ describe('DragService', () => { const dragService = new DragService(); - dragService.drop.subscribe(e => { + dragService.onDrop.subscribe(e => { emittedEvent = e; }); diff --git a/src/Squidex/app/framework/services/drag.service.ts b/src/Squidex/app/framework/services/drag.service.ts index 7807ff1b5..80f7ce015 100644 --- a/src/Squidex/app/framework/services/drag.service.ts +++ b/src/Squidex/app/framework/services/drag.service.ts @@ -21,7 +21,7 @@ export const DragServiceFactory = () => { export class DragService { private readonly dropEvent = new Subject(); - public get drop(): Observable { + public get onDrop(): Observable { return this.dropEvent; } diff --git a/src/Squidex/app/framework/utils/modal-view.spec.ts b/src/Squidex/app/framework/utils/modal-view.spec.ts new file mode 100644 index 000000000..7214110f0 --- /dev/null +++ b/src/Squidex/app/framework/utils/modal-view.spec.ts @@ -0,0 +1,65 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { ModalView } from './../'; + +describe('ModalView', () => { + it('should have initial true value', () => { + const dialog = new ModalView(true); + + checkValue(dialog, true); + }); + + it('should have initial false value', () => { + const dialog = new ModalView(false); + + checkValue(dialog, false); + }); + + it('should become open after show', () => { + const dialog = new ModalView(false); + + dialog.show(); + + checkValue(dialog, true); + }); + + it('should become open after toggle', () => { + const dialog = new ModalView(false); + + dialog.toggle(); + + checkValue(dialog, true); + }); + + it('should become closed after hide', () => { + const dialog = new ModalView(true); + + dialog.hide(); + + checkValue(dialog, false); + }); + + it('should become closed after toggle', () => { + const dialog = new ModalView(true); + + dialog.toggle(); + + checkValue(dialog, false); + }); + + function checkValue(dialog: ModalView, expected: boolean) { + let result: boolean | null = null; + + dialog.isOpenChanges.subscribe(value => { + result = value; + }).unsubscribe(); + + expect(result).toBe(expected); + } +}); + diff --git a/src/Squidex/app/framework/utils/modal-view.ts b/src/Squidex/app/framework/utils/modal-view.ts new file mode 100644 index 000000000..440d05a5c --- /dev/null +++ b/src/Squidex/app/framework/utils/modal-view.ts @@ -0,0 +1,38 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { BehaviorSubject, Observable } from 'rxjs'; + +export class ModalView { + private readonly isOpen$: BehaviorSubject; + + public get isOpenChanges(): Observable { + return this.isOpen$.distinctUntilChanged(); + } + + constructor(isOpen = false) { + this.isOpen$ = new BehaviorSubject(isOpen); + } + + public show() { + this.isOpen$.next(true); + } + + public hide() { + this.isOpen$.next(false); + } + + public toggle() { + let value = false; + + this.isOpenChanges.subscribe(v => { + value = v; + }).unsubscribe(); + + this.isOpen$.next(!value); + } +} \ No newline at end of file diff --git a/src/Squidex/app/shared/index.ts b/src/Squidex/app/shared/index.ts index 118e5a6f8..e4c1a1998 100644 --- a/src/Squidex/app/shared/index.ts +++ b/src/Squidex/app/shared/index.ts @@ -6,4 +6,6 @@ */ export * from './guards/auth.guard'; +export * from './services/apps-store.service'; +export * from './services/apps.service'; export * from './services/auth.service'; \ No newline at end of file diff --git a/src/Squidex/app/shared/services/apps-store.service.spec.ts b/src/Squidex/app/shared/services/apps-store.service.spec.ts new file mode 100644 index 000000000..1406f39e0 --- /dev/null +++ b/src/Squidex/app/shared/services/apps-store.service.spec.ts @@ -0,0 +1,144 @@ +import * as TypeMoq from 'typemoq'; + +import { Observable } from 'rxjs'; + +import { + AppCreateDto, + AppDto, + AppsStoreService, + AppsService, + AuthService +} from './../'; + +describe('AppsStoreService', () => { + const oldApps = [new AppDto('id', 'name', null, null)]; + const newApp = new AppDto('id', 'new-name', null, null); + + let appsService: TypeMoq.Mock; + let authService: TypeMoq.Mock; + + beforeEach(() => { + appsService = TypeMoq.Mock.ofType(AppsService); + authService = TypeMoq.Mock.ofType(AuthService); + }); + + it('should load when authenticated once', () => { + authService.setup(x => x.isAuthenticatedChanges) + .returns(() => Observable.of(true)) + .verifiable(TypeMoq.Times.once()); + + appsService.setup(x => x.getApps()) + .returns(() => Observable.of(oldApps)) + .verifiable(TypeMoq.Times.once()); + + const store = new AppsStoreService(authService.object, appsService.object); + + let result1: AppDto[]; + let result2: AppDto[]; + + store.appsChanges.subscribe(x => { + result1 = x; + }).unsubscribe(); + + store.appsChanges.subscribe(x => { + result2 = x; + }).unsubscribe(); + + expect(result1).toEqual(oldApps); + expect(result2).toEqual(oldApps); + + appsService.verifyAll(); + }); + + it('should reload value from apps-service when called', () => { + authService.setup(x => x.isAuthenticated) + .returns(() => true) + .verifiable(TypeMoq.Times.once()); + + authService.setup(x => x.isAuthenticatedChanges) + .returns(() => Observable.of(true)) + .verifiable(TypeMoq.Times.once()); + + appsService.setup(x => x.getApps()) + .returns(() => Observable.of(oldApps)) + .verifiable(TypeMoq.Times.exactly(2)); + + const store = new AppsStoreService(authService.object, appsService.object); + + let result1: AppDto[]; + let result2: AppDto[]; + + store.appsChanges.subscribe(x => { + result1 = x; + }).unsubscribe(); + + store.reload(); + + store.appsChanges.subscribe(x => { + result2 = x; + }).unsubscribe(); + + expect(result1).toEqual(oldApps); + expect(result2).toEqual(oldApps); + + appsService.verifyAll(); + }); + + it('should add app to cache when created', () => { + authService.setup(x => x.isAuthenticatedChanges) + .returns(() => Observable.of(true)) + .verifiable(TypeMoq.Times.once()); + + appsService.setup(x => x.getApps()) + .returns(() => Observable.of(oldApps)) + .verifiable(TypeMoq.Times.once()); + + appsService.setup(x => x.postApp(TypeMoq.It.isAny())) + .returns(() => Observable.of(newApp)) + .verifiable(TypeMoq.Times.once()); + + const store = new AppsStoreService(authService.object, appsService.object); + + let result1: AppDto[]; + let result2: AppDto[]; + + store.appsChanges.subscribe(x => { + result1 = x; + }).unsubscribe(); + + store.createApp(new AppCreateDto('new-name')).subscribe(x => { }); + + store.appsChanges.subscribe(x => { + result2 = x; + }).unsubscribe(); + + expect(result1).toEqual(oldApps); + expect(JSON.stringify(result2)).toEqual(JSON.stringify(oldApps.concat([newApp]))); + + appsService.verifyAll(); + }); + + it('should not add app to cache when cache is null', () => { + authService.setup(x => x.isAuthenticatedChanges) + .returns(() => Observable.of(false)) + .verifiable(TypeMoq.Times.once()); + + appsService.setup(x => x.postApp(TypeMoq.It.isAny())) + .returns(() => Observable.of(newApp)) + .verifiable(TypeMoq.Times.once()); + + const store = new AppsStoreService(authService.object, appsService.object); + + let result: AppDto[]; + + store.createApp(new AppCreateDto('new-name')).subscribe(x => { }); + + store.appsChanges.subscribe(x => { + result = x; + }).unsubscribe(); + + expect(result).toBeNull(); + + appsService.verifyAll(); + }); +}); \ No newline at end of file diff --git a/src/Squidex/app/shared/services/apps-store.service.ts b/src/Squidex/app/shared/services/apps-store.service.ts new file mode 100644 index 000000000..e5c43c074 --- /dev/null +++ b/src/Squidex/app/shared/services/apps-store.service.ts @@ -0,0 +1,67 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import * as Ng2 from '@angular/core'; + +import { BehaviorSubject, Observable } from 'rxjs'; + +import { + AppCreateDto, + AppDto, + AppsService +} from './apps.service'; + +import { AuthService } from './auth.service'; + +@Ng2.Injectable() +export class AppsStoreService { + private lastApps: AppDto[] = null; + private readonly apps$ = new BehaviorSubject(null); + + public get appsChanges(): Observable { + return this.apps$; + } + + constructor( + private readonly authService: AuthService, + private readonly appService: AppsService + ) { + if (!authService || !appService) { + return; + } + + this.apps$.subscribe(apps => { + this.lastApps = apps; + }); + + this.authService.isAuthenticatedChanges.subscribe(isAuthenticated => { + if (isAuthenticated) { + this.load(); + } + }); + } + + public reload() { + if (this.authService.isAuthenticated) { + this.load(); + } + } + + private load() { + this.appService.getApps().subscribe(apps => { + this.apps$.next(apps); + }); + } + + public createApp(appToCreate: AppCreateDto): Observable { + return this.appService.postApp(appToCreate).do(app => { + if (this.lastApps && app) { + this.apps$.next(this.lastApps.concat([app])); + } + }); + } +} \ No newline at end of file diff --git a/src/Squidex/app/shared/services/apps.service.ts b/src/Squidex/app/shared/services/apps.service.ts index ee2aa5581..63d1685cd 100644 --- a/src/Squidex/app/shared/services/apps.service.ts +++ b/src/Squidex/app/shared/services/apps.service.ts @@ -14,17 +14,17 @@ import { AuthService } from './auth.service'; export class AppDto { constructor( - private readonly id: string, - private readonly name: string, - private readonly created: DateTime, - private readonly lastModified: DateTime + public readonly id: string, + public readonly name: string, + public readonly created: DateTime, + public readonly lastModified: DateTime ) { } } export class AppCreateDto { constructor( - private readonly name: string + public readonly name: string ) { } } @@ -53,7 +53,13 @@ export class AppsService { }); } - public postApp(app: AppCreateDto): Observable { - return this.authService.authPost(this.apiUrl.buildUrl('api/apps'), app); + public postApp(appToCreate: AppCreateDto): Observable { + const now = DateTime.now(); + + return this.authService.authPost(this.apiUrl.buildUrl('api/apps'), appToCreate) + .map(response => response.json()) + .map(response => { + return new AppDto(response.id, appToCreate.name, now, now); + }); } } \ No newline at end of file diff --git a/src/Squidex/app/shared/services/auth.service.ts b/src/Squidex/app/shared/services/auth.service.ts index 7df98f2b6..6a03d0f3c 100644 --- a/src/Squidex/app/shared/services/auth.service.ts +++ b/src/Squidex/app/shared/services/auth.service.ts @@ -14,13 +14,14 @@ import { UserManager } from 'oidc-client'; -import { Observable } from 'rxjs'; +import { BehaviorSubject, Observable } from 'rxjs'; import { ApiUrlConfig } from './../../framework'; @Ng2.Injectable() export class AuthService { private readonly userManager: UserManager; + private readonly isAuthenticatedChanged$ = new BehaviorSubject(false); private currentUser: User | null = null; private checkLoginPromise: Promise; @@ -32,6 +33,38 @@ export class AuthService { return !!this.currentUser; } + public get isAuthenticatedChanges(): Observable { + return this.isAuthenticatedChanged$; + } + + constructor(apiUrl: ApiUrlConfig, + private readonly http: Ng2Http.Http, + ) { + Log.logger = console; + + if (apiUrl) { + this.userManager = new UserManager({ + client_id: 'squidex-frontend', + scope: 'squidex-api openid profile ', + response_type: 'id_token token', + silent_redirect_uri: apiUrl.buildUrl('identity-server/client-callback-silent/'), + popup_redirect_uri: apiUrl.buildUrl('identity-server/client-callback-popup/'), + authority: apiUrl.buildUrl('identity-server/'), + automaticSilentRenew: true + }); + + this.userManager.events.addUserLoaded(user => { + this.onAuthenticated(user); + }); + + this.userManager.events.addUserUnloaded(() => { + this.onDeauthenticated(); + }); + + this.checkLogin(); + } + } + public checkLogin(): Promise { if (this.checkLoginPromise) { return this.checkLoginPromise; @@ -39,7 +72,7 @@ export class AuthService { return Promise.resolve(true); } else { this.checkLoginPromise = - this.checkState(this.userManager.getUser()) + this.checkState(this.userManager.signinSilent()) .then(result => { return result || this.checkState(this.userManager.signinSilent()); }); @@ -48,31 +81,6 @@ export class AuthService { } } - constructor(apiUrl: ApiUrlConfig, - private readonly http: Ng2Http.Http, - ) { - Log.logger = console; - - this.userManager = new UserManager({ - client_id: 'squidex-frontend', - scope: 'squidex-api openid profile ', - response_type: 'id_token token', - silent_redirect_uri: apiUrl.buildUrl('identity-server/client-callback-silent/'), - popup_redirect_uri: apiUrl.buildUrl('identity-server/client-callback-popup/'), - authority: apiUrl.buildUrl('identity-server/') - }); - - this.userManager.events.addUserLoaded(user => { - this.currentUser = user; - }); - - this.userManager.events.addUserUnloaded(() => { - this.currentUser = null; - }); - - this.checkLogin(); - } - public logout(): Observable { return Observable.fromPromise(this.userManager.signoutRedirectCallback()); } @@ -87,12 +95,24 @@ export class AuthService { return Observable.fromPromise(userPromise); } + private onAuthenticated(user: User) { + this.currentUser = user; + + this.isAuthenticatedChanged$.next(true); + } + + private onDeauthenticated() { + this.currentUser = null; + + this.isAuthenticatedChanged$.next(false); + } + private checkState(promise: Promise): Promise { const resultPromise = promise .then(user => { if (user) { - this.currentUser = user; + this.onAuthenticated(user); } return !!this.currentUser; }).catch((err) => { @@ -128,11 +148,13 @@ export class AuthService { private setRequestOptions(options?: Ng2Http.RequestOptions) { if (!options) { options = new Ng2Http.RequestOptions(); + } + if (!options.headers) { + options.headers = new Ng2Http.Headers(); options.headers.append('Content-Type', 'application/json'); } - - options.headers.append('Authorization', '${this.user.token_type} {this.user.access_token}'); + options.headers.append('Authorization', `${this.currentUser.token_type} ${this.currentUser.access_token}`); return options; } diff --git a/src/Squidex/app/theme/_bootstrap.scss b/src/Squidex/app/theme/_bootstrap.scss index 64a90dcf0..274f0a945 100644 --- a/src/Squidex/app/theme/_bootstrap.scss +++ b/src/Squidex/app/theme/_bootstrap.scss @@ -1,69 +1,50 @@ @import '_mixins.scss'; @import '_vars.scss'; -.nav-icon { - & { - margin-top: -0.5rem; - text-align: center; - text-decoration: none; - cursor: default; - } - - span { - display: block; - font-size: 0.8em; - font-weight: normal; - cursor: default; - } +.form-hint { + font-size: 0.8rem; } -.navbar-light .navbar-nav .nav-link, .btn-icon { - & { - color: $nav-text-color; - } - - &:hover, &:focus { - text-decoration: none; - } - - &:hover { - color: black; - } +.ng-invalid.ng-dirty { + border-color: $accent-error; +} - &.disabled { - color: lighten($nav-text-color, 55%); - } +.ng-invalid.ng-dirty :focus, .ng-invalid.ng-dirty :hover { + border-color: $accent-error-dark; } -.navbar-light .navbar-nav .nav-link.btn-blue, .btn-icon.btn-blue, .btn-blue { - &:hover { - color: $accent-normal; +.errors { + &-box { + position: relative; } - &:focus { - color: $accent-dark + &:after { + @include absolute(2rem, auto, auto, 0.6rem); + content: ''; + height: 0; + border-style: solid; + border-width: 0.4rem; + border-color: $accent-error transparent transparent transparent; + width: 0; } - &.disabled { - color: lighten($nav-text-color, 55%); + & { + @include absolute(-2.4rem, 0px, auto, 0px); + @include border-radius(2px); + color: white; + cursor: none; + font-size: 0.9rem; + font-weight: normal; + line-height: 2rem; + padding: 0 0.4rem; + background: $accent-error; } } -.btn-icon { - padding-left: 0.4rem; - padding-right: 0.4rem; - cursor: default; -} - -.navbar-nav .nav-item + .nav-nomargin { - margin-left: 0; - margin-right: -1rem; +.modal-content { + @include box-shadow(0px, 6px, 16px, 0.2px); } -.nav-separator { - margin-top: .425rem; - margin-bottom: .425rem; - margin-left: 1.2em; - margin-right: 1.2em; - display: block; +.modal-content, .modal-header { + border: 0; } \ No newline at end of file diff --git a/src/Squidex/app/theme/_vars.scss b/src/Squidex/app/theme/_vars.scss index 08896bc36..9f6b7004d 100644 --- a/src/Squidex/app/theme/_vars.scss +++ b/src/Squidex/app/theme/_vars.scss @@ -1,4 +1,10 @@ $nav-text-color: #333; -$accent-normal: blue; -$accent-dark: darkblue; \ No newline at end of file +$accent-blue: #438CEF; +$accent-blue-dark: #3F83DF; + +$accent-green: #4CC159; +$accent-green-dark: #47B353; + +$accent-error: red; +$accent-error-dark: darken(red, 5%); \ No newline at end of file diff --git a/src/Squidex/app/theme/_vendor-overrides.scss b/src/Squidex/app/theme/_vendor-overrides.scss new file mode 100644 index 000000000..1a97cb2dc --- /dev/null +++ b/src/Squidex/app/theme/_vendor-overrides.scss @@ -0,0 +1,7 @@ +@import '_mixins.scss'; +@import '_vars.scss'; + +$fa-font-path: "~font-awesome/fonts"; + +$brand-primary: $accent-blue; +$brand-success: $accent-green; \ No newline at end of file diff --git a/src/Squidex/app/theme/vendor.scss b/src/Squidex/app/theme/vendor.scss index 07031f251..bbd65bbfb 100644 --- a/src/Squidex/app/theme/vendor.scss +++ b/src/Squidex/app/theme/vendor.scss @@ -1,4 +1,4 @@ -$fa-font-path: "~font-awesome/fonts"; +@import '_vendor-overrides.scss'; // Font Awesome @import './../../node_modules/font-awesome/scss/font-awesome.scss'; diff --git a/src/Squidex/app/vendor.ts b/src/Squidex/app/vendor.ts index 3b16330ab..41b7a3bb8 100644 --- a/src/Squidex/app/vendor.ts +++ b/src/Squidex/app/vendor.ts @@ -18,4 +18,7 @@ import '@angular/router'; import 'oidc-client'; // RxJS -import 'rxjs'; \ No newline at end of file +import 'rxjs'; + +// Bootstrap +import 'theme/vendor.scss'; \ No newline at end of file diff --git a/src/Squidex/wwwroot/index.html b/src/Squidex/wwwroot/index.html index bc47aa5fc..b83038cba 100644 --- a/src/Squidex/wwwroot/index.html +++ b/src/Squidex/wwwroot/index.html @@ -7,6 +7,6 @@ - Loading... + Loading... \ No newline at end of file