mirror of https://github.com/Squidex/squidex.git
61 changed files with 1052 additions and 182 deletions
@ -1 +1,15 @@ |
|||||
<router-outlet></router-outlet> |
<nav class="navbar navbar-fixed-top navbar-dark bg-primary bg-faded"> |
||||
|
<span class="navbar-brand">Squidex</span> |
||||
|
|
||||
|
<div class="float-xs-left apps-menu"> |
||||
|
<sqx-apps-menu></sqx-apps-menu> |
||||
|
</div> |
||||
|
|
||||
|
<div class="float-xs-left search-form"> |
||||
|
<sqx-search-form></sqx-search-form> |
||||
|
</div> |
||||
|
</nav> |
||||
|
|
||||
|
<main> |
||||
|
<router-outlet></router-outlet> |
||||
|
</main> |
||||
|
|||||
@ -1,8 +1,13 @@ |
|||||
|
@import 'theme/mixins.scss'; |
||||
|
|
||||
|
.navbar { |
||||
|
@include box-shadow(0, 4px, 4px, 0.2px); |
||||
|
} |
||||
|
|
||||
|
.search-form { |
||||
|
margin-left: 15px; |
||||
|
} |
||||
|
|
||||
main { |
main { |
||||
padding: 1em; |
margin-top: 54px |
||||
font-family: Arial, Helvetica, sans-serif; |
|
||||
font-size: 1.1rem; |
|
||||
text-align: center; |
|
||||
margin-top: 50px; |
|
||||
display: block; |
|
||||
} |
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
<content> |
||||
|
<div class="apps-empty"> |
||||
|
<h3 class="apps-empty-headline">You are not collaborating to any app yet</h3> |
||||
|
|
||||
|
<button class="apps-empty-button btn btn-success" (click)="modalDialog.show()">Create App</button> |
||||
|
</div> |
||||
|
</content> |
||||
|
|
||||
|
<div class="modal" [(sqxModalView)]="modalDialog" [@fade]="modalDialog.isOpenChanges | async"> |
||||
|
<div class="modal-dialog"> |
||||
|
<div class="modal-content"> |
||||
|
<div class="modal-header"> |
||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close" (click)="modalDialog.hide()"> |
||||
|
<span aria-hidden="true">×</span> |
||||
|
</button> |
||||
|
|
||||
|
<h4 class="modal-title">Create App</h4> |
||||
|
</div> |
||||
|
|
||||
|
<div class="modal-body"> |
||||
|
<sqx-app-form |
||||
|
(onCreated)="modalDialog.hide()" |
||||
|
(onCancelled)="modalDialog.hide()"></sqx-app-form> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
@ -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(); |
||||
|
} |
||||
@ -1,3 +0,0 @@ |
|||||
<main> |
|
||||
<h1>Hello from Angular 2 App with Webpack!</h1> |
|
||||
</main> |
|
||||
@ -0,0 +1,31 @@ |
|||||
|
<form [formGroup]="createForm" (ngSubmit)="submit()"> |
||||
|
<div class="form-group"> |
||||
|
<label for="app-name">Name</label> |
||||
|
|
||||
|
<div class="errors-box" *ngIf="createForm.get('name').invalid && createForm.get('name').dirty" [@fade]> |
||||
|
<div class="errors"> |
||||
|
<span *ngIf="createForm.get('name').hasError('required')"> |
||||
|
Name is required. |
||||
|
</span> |
||||
|
<span *ngIf="createForm.get('name').hasError('maxlength')"> |
||||
|
Name can not have more than 40 characters. |
||||
|
</span> |
||||
|
<span *ngIf="createForm.get('name').hasError('pattern')"> |
||||
|
Name can contain lower case letters (a-z), numbers and dashes only. |
||||
|
</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<input type="text" class="form-control" id="app-name" formControlName="name" /> |
||||
|
|
||||
|
<span class="form-hint"> |
||||
|
The app name becomes part of the api url, e.g, https://<b>{{appName | async}}</b>.squidex.io/.<br /> |
||||
|
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. |
||||
|
</span> |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-group"> |
||||
|
<button type="reset" class="btn btn-link" (click)="cancel()">Cancel</button> |
||||
|
<button type="submit" class="btn btn-success">Create</button> |
||||
|
</div> |
||||
|
</form> |
||||
@ -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<string>; |
||||
|
|
||||
|
@Ng2.Input() |
||||
|
public showClose = false; |
||||
|
|
||||
|
@Ng2.Output() |
||||
|
public onCreated = new Ng2.EventEmitter(); |
||||
|
|
||||
|
@Ng2.Output() |
||||
|
public onCancelled = new Ng2.EventEmitter(); |
||||
|
|
||||
|
public creating = new Ng2.EventEmitter<boolean>(); |
||||
|
|
||||
|
public creationError = new Ng2.EventEmitter<any>(); |
||||
|
|
||||
|
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(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,41 @@ |
|||||
|
<ul class="nav navbar-nav" *ngIf="apps"> |
||||
|
<li class="nav-item dropdown"> |
||||
|
<span class="nav-link dropdown-toggle" id="app-name" (click)="modalMenu.toggle()">My App with a really very long name</span> |
||||
|
|
||||
|
<div class="dropdown-menu" [(sqxModalView)]="modalMenu"> |
||||
|
<a class="dropdown-item" href="#">Action</a> |
||||
|
<a class="dropdown-item" href="#">Another action</a> |
||||
|
<a class="dropdown-item" href="#">Something else here</a> |
||||
|
|
||||
|
<div class="dropdown-divider"></div> |
||||
|
|
||||
|
<a class="dropdown-item" routerLink="/apps">All apps</a> |
||||
|
|
||||
|
<div class="dropdown-divider"></div> |
||||
|
|
||||
|
<div class="drodown-button"> |
||||
|
<button class="btn btn-block btn-success" id="app-create" (click)="createApp()">Create App</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</li> |
||||
|
</ul> |
||||
|
|
||||
|
<div class="modal ng-animate" [(sqxModalView)]="modalDialog" [@fade]="(modalDialog.isOpenChanges | async)"> |
||||
|
<div class="modal-dialog" role="document"> |
||||
|
<div class="modal-content"> |
||||
|
<div class="modal-header"> |
||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close" (click)="modalDialog.hide()"> |
||||
|
<span aria-hidden="true">×</span> |
||||
|
</button> |
||||
|
|
||||
|
<h4 class="modal-title">Create App</h4> |
||||
|
</div> |
||||
|
|
||||
|
<div class="modal-body"> |
||||
|
<sqx-app-form |
||||
|
(onCreated)="modalDialog.hide()" |
||||
|
(onCancelled)="modalDialog.hide()"></sqx-app-form> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -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(); |
||||
|
} |
||||
|
} |
||||
@ -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'; |
||||
@ -0,0 +1,10 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Sebastian Stehle. All rights reserved |
||||
|
*/ |
||||
|
|
||||
|
export * from './declarations'; |
||||
|
|
||||
|
export * from './layout.module'; |
||||
@ -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 { } |
||||
@ -0,0 +1,3 @@ |
|||||
|
<form class="form-inline"> |
||||
|
<input class="form-control search" type="text" /> |
||||
|
</form> |
||||
@ -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%); |
||||
|
} |
||||
|
} |
||||
@ -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)) |
||||
|
] |
||||
|
); |
||||
|
} |
||||
|
|
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
@ -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<boolean>; |
||||
|
|
||||
|
public get isOpenChanges(): Observable<boolean> { |
||||
|
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); |
||||
|
} |
||||
|
} |
||||
@ -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<AppsService>; |
||||
|
let authService: TypeMoq.Mock<AuthService>; |
||||
|
|
||||
|
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(); |
||||
|
}); |
||||
|
}); |
||||
@ -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<AppDto[]>(null); |
||||
|
|
||||
|
public get appsChanges(): Observable<AppDto[]> { |
||||
|
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<any> { |
||||
|
return this.appService.postApp(appToCreate).do(app => { |
||||
|
if (this.lastApps && app) { |
||||
|
this.apps$.next(this.lastApps.concat([app])); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -1,69 +1,50 @@ |
|||||
@import '_mixins.scss'; |
@import '_mixins.scss'; |
||||
@import '_vars.scss'; |
@import '_vars.scss'; |
||||
|
|
||||
.nav-icon { |
.form-hint { |
||||
& { |
font-size: 0.8rem; |
||||
margin-top: -0.5rem; |
|
||||
text-align: center; |
|
||||
text-decoration: none; |
|
||||
cursor: default; |
|
||||
} |
|
||||
|
|
||||
span { |
|
||||
display: block; |
|
||||
font-size: 0.8em; |
|
||||
font-weight: normal; |
|
||||
cursor: default; |
|
||||
} |
|
||||
} |
} |
||||
|
|
||||
.navbar-light .navbar-nav .nav-link, .btn-icon { |
.ng-invalid.ng-dirty { |
||||
& { |
border-color: $accent-error; |
||||
color: $nav-text-color; |
} |
||||
} |
|
||||
|
|
||||
&:hover, &:focus { |
|
||||
text-decoration: none; |
|
||||
} |
|
||||
|
|
||||
&:hover { |
|
||||
color: black; |
|
||||
} |
|
||||
|
|
||||
&.disabled { |
.ng-invalid.ng-dirty :focus, .ng-invalid.ng-dirty :hover { |
||||
color: lighten($nav-text-color, 55%); |
border-color: $accent-error-dark; |
||||
} |
|
||||
} |
} |
||||
|
|
||||
.navbar-light .navbar-nav .nav-link.btn-blue, .btn-icon.btn-blue, .btn-blue { |
.errors { |
||||
&:hover { |
&-box { |
||||
color: $accent-normal; |
position: relative; |
||||
} |
} |
||||
|
|
||||
&:focus { |
&:after { |
||||
color: $accent-dark |
@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 { |
.modal-content { |
||||
padding-left: 0.4rem; |
@include box-shadow(0px, 6px, 16px, 0.2px); |
||||
padding-right: 0.4rem; |
|
||||
cursor: default; |
|
||||
} |
|
||||
|
|
||||
.navbar-nav .nav-item + .nav-nomargin { |
|
||||
margin-left: 0; |
|
||||
margin-right: -1rem; |
|
||||
} |
} |
||||
|
|
||||
.nav-separator { |
.modal-content, .modal-header { |
||||
margin-top: .425rem; |
border: 0; |
||||
margin-bottom: .425rem; |
|
||||
margin-left: 1.2em; |
|
||||
margin-right: 1.2em; |
|
||||
display: block; |
|
||||
} |
} |
||||
@ -1,4 +1,10 @@ |
|||||
$nav-text-color: #333; |
$nav-text-color: #333; |
||||
|
|
||||
$accent-normal: blue; |
$accent-blue: #438CEF; |
||||
$accent-dark: darkblue; |
$accent-blue-dark: #3F83DF; |
||||
|
|
||||
|
$accent-green: #4CC159; |
||||
|
$accent-green-dark: #47B353; |
||||
|
|
||||
|
$accent-error: red; |
||||
|
$accent-error-dark: darken(red, 5%); |
||||
@ -0,0 +1,7 @@ |
|||||
|
@import '_mixins.scss'; |
||||
|
@import '_vars.scss'; |
||||
|
|
||||
|
$fa-font-path: "~font-awesome/fonts"; |
||||
|
|
||||
|
$brand-primary: $accent-blue; |
||||
|
$brand-success: $accent-green; |
||||
Loading…
Reference in new issue