Browse Source

Temp version

pull/1/head
Sebastian 9 years ago
parent
commit
f281ecb9d7
  1. 16
      src/Squidex/app/app.component.html
  2. 17
      src/Squidex/app/app.component.scss
  3. 10
      src/Squidex/app/app.component.spec.ts
  4. 6
      src/Squidex/app/app.component.ts
  5. 17
      src/Squidex/app/app.module.ts
  6. 8
      src/Squidex/app/app.routes.ts
  7. 27
      src/Squidex/app/components/apps/apps-page.component.html
  8. 17
      src/Squidex/app/components/apps/apps-page.component.scss
  9. 22
      src/Squidex/app/components/apps/apps-page.component.ts
  10. 3
      src/Squidex/app/components/apps/apps.component.html
  11. 12
      src/Squidex/app/components/apps/apps.module.ts
  12. 2
      src/Squidex/app/components/apps/declarations.ts
  13. 3
      src/Squidex/app/components/apps/index.ts
  14. 1
      src/Squidex/app/components/index.ts
  15. 31
      src/Squidex/app/components/layout/app-form.component.html
  16. 0
      src/Squidex/app/components/layout/app-form.component.scss
  17. 91
      src/Squidex/app/components/layout/app-form.component.ts
  18. 41
      src/Squidex/app/components/layout/apps-menu.component.html
  19. 33
      src/Squidex/app/components/layout/apps-menu.component.scss
  20. 54
      src/Squidex/app/components/layout/apps-menu.component.ts
  21. 10
      src/Squidex/app/components/layout/declarations.ts
  22. 10
      src/Squidex/app/components/layout/index.ts
  23. 33
      src/Squidex/app/components/layout/layout.module.ts
  24. 3
      src/Squidex/app/components/layout/search-form.component.html
  25. 16
      src/Squidex/app/components/layout/search-form.component.scss
  26. 7
      src/Squidex/app/components/layout/search-form.component.ts
  27. 2
      src/Squidex/app/components/login/declarations.ts
  28. 3
      src/Squidex/app/components/login/index.ts
  29. 0
      src/Squidex/app/components/login/login-page.component.html
  30. 2
      src/Squidex/app/components/login/login-page.component.ts
  31. 10
      src/Squidex/app/components/login/login.module.ts
  32. 26
      src/Squidex/app/framework/angular/animations.ts
  33. 4
      src/Squidex/app/framework/angular/cloak.directive.ts
  34. 2
      src/Squidex/app/framework/angular/color-picker.component.ts
  35. 2
      src/Squidex/app/framework/angular/drag-model.directive.ts
  36. 2
      src/Squidex/app/framework/angular/image-drop.directive.ts
  37. 69
      src/Squidex/app/framework/angular/modal-view.directive.ts
  38. 2
      src/Squidex/app/framework/angular/shortcut.component.ts
  39. 2
      src/Squidex/app/framework/angular/slider.component.ts
  40. 2
      src/Squidex/app/framework/angular/spinner.component.ts
  41. 2
      src/Squidex/app/framework/angular/user-report.component.ts
  42. 26
      src/Squidex/app/framework/declarations.ts
  43. 17
      src/Squidex/app/framework/framework.module.ts
  44. 24
      src/Squidex/app/framework/index.ts
  45. 2
      src/Squidex/app/framework/services/clipboard.service.spec.ts
  46. 10
      src/Squidex/app/framework/services/clipboard.service.ts
  47. 2
      src/Squidex/app/framework/services/drag.service.spec.ts
  48. 2
      src/Squidex/app/framework/services/drag.service.ts
  49. 65
      src/Squidex/app/framework/utils/modal-view.spec.ts
  50. 38
      src/Squidex/app/framework/utils/modal-view.ts
  51. 2
      src/Squidex/app/shared/index.ts
  52. 144
      src/Squidex/app/shared/services/apps-store.service.spec.ts
  53. 67
      src/Squidex/app/shared/services/apps-store.service.ts
  54. 20
      src/Squidex/app/shared/services/apps.service.ts
  55. 64
      src/Squidex/app/shared/services/auth.service.ts
  56. 83
      src/Squidex/app/theme/_bootstrap.scss
  57. 10
      src/Squidex/app/theme/_vars.scss
  58. 7
      src/Squidex/app/theme/_vendor-overrides.scss
  59. 2
      src/Squidex/app/theme/vendor.scss
  60. 3
      src/Squidex/app/vendor.ts
  61. 2
      src/Squidex/wwwroot/index.html

16
src/Squidex/app/app.component.html

@ -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>

17
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
}

10
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) }
]
});
});

6
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 { }

17
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) },

8
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
}
];

27
src/Squidex/app/components/apps/apps-page.component.html

@ -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">&times;</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>

17
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;
}
}

22
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();
}

3
src/Squidex/app/components/apps/apps.component.html

@ -1,3 +0,0 @@
<main>
<h1>Hello from Angular 2 App with Webpack!</h1>
</main>

12
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 { }
export class SqxAppModule { }

2
src/Squidex/app/components/apps/declarations.ts

@ -5,4 +5,4 @@
* Copyright (c) Sebastian Stehle. All rights reserved
*/
export * from './apps.component';
export * from './apps-page.component';

3
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';

1
src/Squidex/app/components/index.ts

@ -6,4 +6,5 @@
*/
export * from './apps';
export * from './layout';
export * from './login';

31
src/Squidex/app/components/layout/app-form.component.html

@ -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
src/Squidex/app/components/layout/app-form.component.scss

91
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<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();
}
}

41
src/Squidex/app/components/layout/apps-menu.component.html

@ -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">&times;</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>

33
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);
}
}

54
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();
}
}

10
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';

10
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';

33
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 { }

3
src/Squidex/app/components/layout/search-form.component.html

@ -0,0 +1,3 @@
<form class="form-inline">
<input class="form-control search" type="text" />
</form>

16
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%);
}
}

7
src/Squidex/app/components/apps/apps.component.ts → 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 {
}

2
src/Squidex/app/components/login/declarations.ts

@ -5,4 +5,4 @@
* Copyright (c) Sebastian Stehle. All rights reserved
*/
export * from './login.component';
export * from './login-page.component';

3
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';

0
src/Squidex/app/components/login/login.component.html → src/Squidex/app/components/login/login-page.component.html

2
src/Squidex/app/components/login/login.component.ts → 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(

10
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 { }
export class SqxLoginModule { }

26
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))
]
);
}

4
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');
}
}

2
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
})

2
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;
}

2
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(

69
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);
}
}

2
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 {

2
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
})

2
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 {

2
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 {

26
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/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';

17
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 { }
export class SqxFrameworkModule { }

24
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 './declarations';
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';

2
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;
});

10
src/Squidex/app/framework/services/clipboard.service.ts

@ -15,16 +15,16 @@ export const ClipboardServiceFactory = () => {
@Ng2.Injectable()
export class ClipboardService {
private textInstance = new BehaviorSubject<string>('');
private readonly text$ = new BehaviorSubject<string>('');
public get text(): Observable<string> {
return this.textInstance;
public get textChanges(): Observable<string> {
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);
}
}

2
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;
});

2
src/Squidex/app/framework/services/drag.service.ts

@ -21,7 +21,7 @@ export const DragServiceFactory = () => {
export class DragService {
private readonly dropEvent = new Subject<DropEvent>();
public get drop(): Observable<DropEvent> {
public get onDrop(): Observable<DropEvent> {
return this.dropEvent;
}

65
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);
}
});

38
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<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);
}
}

2
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';

144
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<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();
});
});

67
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<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]));
}
});
}
}

20
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<any> {
return this.authService.authPost(this.apiUrl.buildUrl('api/apps'), app);
public postApp(appToCreate: AppCreateDto): Observable<AppDto> {
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);
});
}
}

64
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<boolean>(false);
private currentUser: User | null = null;
private checkLoginPromise: Promise<boolean>;
@ -32,20 +33,8 @@ export class AuthService {
return !!this.currentUser;
}
public checkLogin(): Promise<boolean> {
if (this.checkLoginPromise) {
return this.checkLoginPromise;
} else if (this.currentUser) {
return Promise.resolve(true);
} else {
this.checkLoginPromise =
this.checkState(this.userManager.getUser())
.then(result => {
return result || this.checkState(this.userManager.signinSilent());
});
return this.checkLoginPromise;
}
public get isAuthenticatedChanges(): Observable<boolean> {
return this.isAuthenticatedChanged$;
}
constructor(apiUrl: ApiUrlConfig,
@ -53,25 +42,44 @@ export class AuthService {
) {
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/')
authority: apiUrl.buildUrl('identity-server/'),
automaticSilentRenew: true
});
this.userManager.events.addUserLoaded(user => {
this.currentUser = user;
this.onAuthenticated(user);
});
this.userManager.events.addUserUnloaded(() => {
this.currentUser = null;
this.onDeauthenticated();
});
this.checkLogin();
}
}
public checkLogin(): Promise<boolean> {
if (this.checkLoginPromise) {
return this.checkLoginPromise;
} else if (this.currentUser) {
return Promise.resolve(true);
} else {
this.checkLoginPromise =
this.checkState(this.userManager.signinSilent())
.then(result => {
return result || this.checkState(this.userManager.signinSilent());
});
return this.checkLoginPromise;
}
}
public logout(): Observable<any> {
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<User>): Promise<boolean> {
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;
}

83
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;
}

10
src/Squidex/app/theme/_vars.scss

@ -1,4 +1,10 @@
$nav-text-color: #333;
$accent-normal: blue;
$accent-dark: darkblue;
$accent-blue: #438CEF;
$accent-blue-dark: #3F83DF;
$accent-green: #4CC159;
$accent-green-dark: #47B353;
$accent-error: red;
$accent-error-dark: darken(red, 5%);

7
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;

2
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';

3
src/Squidex/app/vendor.ts

@ -19,3 +19,6 @@ import 'oidc-client';
// RxJS
import 'rxjs';
// Bootstrap
import 'theme/vendor.scss';

2
src/Squidex/wwwroot/index.html

@ -7,6 +7,6 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<my-app>Loading...</my-app>
<sqx-app>Loading...</sqx-app>
</body>
</html>
Loading…
Cancel
Save