From 238d14757435586d80e5b7f5b5bdd33754e57ab9 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 11 Jan 2017 23:04:51 +0100 Subject: [PATCH] Content Handling --- src/Squidex/app/app.module.ts | 44 +-------- .../app/features/content/declarations.ts | 4 +- src/Squidex/app/features/content/module.ts | 61 +++++++++++- .../content/pages/content-page.component.html | 16 ---- .../content/pages/content-page.component.ts | 27 ------ .../pages/content/content-page.component.html | 95 +++++++++++++++++++ .../{ => content}/content-page.component.scss | 0 .../pages/content/content-page.component.ts | 62 ++++++++++++ .../contents/contents-page.component.html | 33 +++++++ .../contents/contents-page.component.scss | 7 ++ .../pages/contents/contents-page.component.ts | 39 ++++++++ .../pages/schemas/schemas-page.component.html | 35 +++++++ .../pages/schemas/schemas-page.component.scss | 83 ++++++++++++++++ .../pages/schemas/schemas-page.component.ts | 76 +++++++++++++++ src/Squidex/app/features/schemas/module.ts | 4 + .../pages/schema/schema-page.component.html | 4 +- .../pages/schema/schema-page.component.ts | 41 ++------ .../schema/types/number-ui.component.html | 2 +- .../app/framework/angular/name.pipe.spec.ts | 6 ++ .../app/framework/angular/name.pipe.ts | 21 +++- src/Squidex/app/framework/module.ts | 26 ++++- src/Squidex/app/shared/declarations.ts | 2 + .../guards/app-must-exist.guard.spec.ts | 15 +-- .../must-be-authenticated.guard.spec.ts | 4 +- .../must-be-not-authenticated.guard.spec.ts | 4 +- .../guards/resolve-published-schema.guard.ts | 55 +++++++++++ .../app/shared/guards/resolve-schema.guard.ts | 55 +++++++++++ src/Squidex/app/shared/module.ts | 46 ++++++++- src/Squidex/app/theme/_bootstrap.scss | 1 + 29 files changed, 728 insertions(+), 140 deletions(-) delete mode 100644 src/Squidex/app/features/content/pages/content-page.component.html delete mode 100644 src/Squidex/app/features/content/pages/content-page.component.ts create mode 100644 src/Squidex/app/features/content/pages/content/content-page.component.html rename src/Squidex/app/features/content/pages/{ => content}/content-page.component.scss (100%) create mode 100644 src/Squidex/app/features/content/pages/content/content-page.component.ts create mode 100644 src/Squidex/app/features/content/pages/contents/contents-page.component.html create mode 100644 src/Squidex/app/features/content/pages/contents/contents-page.component.scss create mode 100644 src/Squidex/app/features/content/pages/contents/contents-page.component.ts create mode 100644 src/Squidex/app/features/content/pages/schemas/schemas-page.component.html create mode 100644 src/Squidex/app/features/content/pages/schemas/schemas-page.component.scss create mode 100644 src/Squidex/app/features/content/pages/schemas/schemas-page.component.ts create mode 100644 src/Squidex/app/shared/guards/resolve-published-schema.guard.ts create mode 100644 src/Squidex/app/shared/guards/resolve-schema.guard.ts diff --git a/src/Squidex/app/app.module.ts b/src/Squidex/app/app.module.ts index 5d6e80929..5219fb0bb 100644 --- a/src/Squidex/app/app.module.ts +++ b/src/Squidex/app/app.module.ts @@ -12,30 +12,11 @@ import { AppComponent } from './app.component'; import { ApiUrlConfig, - AppClientsService, - AppContributorsService, - AppLanguagesService, - AppMustExistGuard, - AppsService, - AppsStoreService, - AuthService, CurrencyConfig, DecimalSeparatorConfig, - HistoryService, - LanguageService, - LocalStoreService, - MessageBus, - MustBeAuthenticatedGuard, - MustBeNotAuthenticatedGuard, - NotificationService, - PanelService, - SchemasService, SqxFrameworkModule, SqxSharedModule, - TitlesConfig, - TitleService, - UsersProviderService, - UsersService + TitlesConfig } from './shared'; import { SqxShellModule } from './shell'; @@ -61,8 +42,8 @@ export function configCurrency() { @NgModule({ imports: [ BrowserModule, - SqxFrameworkModule, - SqxSharedModule, + SqxFrameworkModule.forRoot(), + SqxSharedModule.forRoot(), SqxShellModule, routing ], @@ -70,25 +51,6 @@ export function configCurrency() { AppComponent ], providers: [ - AppClientsService, - AppContributorsService, - AppLanguagesService, - AppsStoreService, - AppsService, - AppMustExistGuard, - AuthService, - HistoryService, - LanguageService, - LocalStoreService, - MessageBus, - MustBeAuthenticatedGuard, - MustBeNotAuthenticatedGuard, - NotificationService, - PanelService, - SchemasService, - TitleService, - UsersProviderService, - UsersService, { provide: ApiUrlConfig, useFactory: configApiUrl }, { provide: CurrencyConfig, useFactory: configCurrency }, { provide: DecimalSeparatorConfig, useFactory: configDecimalSeparator }, diff --git a/src/Squidex/app/features/content/declarations.ts b/src/Squidex/app/features/content/declarations.ts index 648491e92..3f2fdf5d9 100644 --- a/src/Squidex/app/features/content/declarations.ts +++ b/src/Squidex/app/features/content/declarations.ts @@ -5,4 +5,6 @@ * Copyright (c) Sebastian Stehle. All rights reserved */ -export * from './pages/content-page.component'; \ No newline at end of file +export * from './pages/content/content-page.component'; +export * from './pages/contents/contents-page.component'; +export * from './pages/schemas/schemas-page.component'; \ No newline at end of file diff --git a/src/Squidex/app/features/content/module.ts b/src/Squidex/app/features/content/module.ts index 065fb45c6..a9f285510 100644 --- a/src/Squidex/app/features/content/module.ts +++ b/src/Squidex/app/features/content/module.ts @@ -8,16 +8,67 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; -import { SqxFrameworkModule, SqxSharedModule } from 'shared'; +import { + HistoryComponent, + ResolvePublishedSchemaGuard, + SqxFrameworkModule, + SqxSharedModule +} from 'shared'; import { - ContentPageComponent + ContentPageComponent, + ContentsPageComponent, + SchemasPageComponent } from './declarations'; const routes: Routes = [ { path: '', - component: ContentPageComponent + component: SchemasPageComponent, + children: [ + { + path: '' + }, + { + path: ':schemaName', + component: ContentsPageComponent, + resolve: { + schema: ResolvePublishedSchemaGuard + }, + children: [ + { + path: 'new', + component: ContentPageComponent, + resolve: { + schema: ResolvePublishedSchemaGuard + }, + data: { + disableHistory: true + } + }, { + path: 'history', + component: HistoryComponent, + data: { + channel: 'contents.{schemaName}' + } + }, { + path: ':contentId', + component: ContentPageComponent, + resolve: { + schema: ResolvePublishedSchemaGuard + }, + children: [ + { + path: 'history', + component: HistoryComponent, + data: { + channel: 'contents.{schemaName}.{contentId}' + } + } + ] + } + ] + }] } ]; @@ -28,7 +79,9 @@ const routes: Routes = [ RouterModule.forChild(routes) ], declarations: [ - ContentPageComponent + ContentPageComponent, + ContentsPageComponent, + SchemasPageComponent ] }) export class SqxFeatureContentModule { } \ No newline at end of file diff --git a/src/Squidex/app/features/content/pages/content-page.component.html b/src/Squidex/app/features/content/pages/content-page.component.html deleted file mode 100644 index 3c66e1605..000000000 --- a/src/Squidex/app/features/content/pages/content-page.component.html +++ /dev/null @@ -1,16 +0,0 @@ - - -
-
-

Content

- - - - -
- -
-
-
-
-
\ No newline at end of file diff --git a/src/Squidex/app/features/content/pages/content-page.component.ts b/src/Squidex/app/features/content/pages/content-page.component.ts deleted file mode 100644 index a1a0fdc6e..000000000 --- a/src/Squidex/app/features/content/pages/content-page.component.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Squidex Headless CMS - * - * @license - * Copyright (c) Sebastian Stehle. All rights reserved - */ - -import { Component } from '@angular/core'; - -import { - AppComponentBase, - AppsStoreService, - NotificationService, - UsersProviderService - } from 'shared'; - -@Component({ - selector: 'sqx-content-page', - styleUrls: ['./content-page.component.scss'], - templateUrl: './content-page.component.html' -}) -export class ContentPageComponent extends AppComponentBase { - constructor(apps: AppsStoreService, notifications: NotificationService, users: UsersProviderService) { - super(apps, notifications, users); - } -} - diff --git a/src/Squidex/app/features/content/pages/content/content-page.component.html b/src/Squidex/app/features/content/pages/content/content-page.component.html new file mode 100644 index 000000000..483b75aba --- /dev/null +++ b/src/Squidex/app/features/content/pages/content/content-page.component.html @@ -0,0 +1,95 @@ + + +
+
+
+
+ +
+ +

New {{schema|displayName}}

+ + + + +
+ +
+
+
+
+ + +
+
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+ {{field.properties.hints}} +
+
+
+
+
+ +
+
+
+
\ No newline at end of file diff --git a/src/Squidex/app/features/content/pages/content-page.component.scss b/src/Squidex/app/features/content/pages/content/content-page.component.scss similarity index 100% rename from src/Squidex/app/features/content/pages/content-page.component.scss rename to src/Squidex/app/features/content/pages/content/content-page.component.scss diff --git a/src/Squidex/app/features/content/pages/content/content-page.component.ts b/src/Squidex/app/features/content/pages/content/content-page.component.ts new file mode 100644 index 000000000..006e8ac08 --- /dev/null +++ b/src/Squidex/app/features/content/pages/content/content-page.component.ts @@ -0,0 +1,62 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { Component } from '@angular/core'; +import { AbstractControl, FormControl, FormGroup } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; + +import { + AppComponentBase, + AppsStoreService, + NotificationService, + SchemaDetailsDto, + UsersProviderService +} from 'shared'; + +@Component({ + selector: 'sqx-content-page', + styleUrls: ['./content-page.component.scss'], + templateUrl: './content-page.component.html' +}) +export class ContentPageComponent extends AppComponentBase { + public schema: SchemaDetailsDto; + + public contentForm: FormGroup; + + public isNewMode = false; + + constructor(apps: AppsStoreService, notifications: NotificationService, users: UsersProviderService, + private readonly route: ActivatedRoute + ) { + super(apps, notifications, users); + } + + public ngOnInit() { + this.route.params.map(p => p['contentId']).subscribe(contentId => { + this.isNewMode = !contentId; + }); + + this.route.data.map(p => p['schema']).subscribe((schema: SchemaDetailsDto) => { + this.schema = schema; + + this.setupForm(schema); + }); + } + + private setupForm(schema: SchemaDetailsDto) { + const controls: { [key: string]: AbstractControl } = {}; + + for (const field of schema.fields) { + const formControl = new FormControl(); + + controls[field.name] = formControl; + } + + this.contentForm = new FormGroup(controls); + } +} + diff --git a/src/Squidex/app/features/content/pages/contents/contents-page.component.html b/src/Squidex/app/features/content/pages/contents/contents-page.component.html new file mode 100644 index 000000000..67f4bb7cd --- /dev/null +++ b/src/Squidex/app/features/content/pages/contents/contents-page.component.html @@ -0,0 +1,33 @@ + + +
+
+ + +

{{schema|displayName}} Contents

+ + + + +
+ +
+
+
+
+ +
+
+
+ + \ No newline at end of file diff --git a/src/Squidex/app/features/content/pages/contents/contents-page.component.scss b/src/Squidex/app/features/content/pages/contents/contents-page.component.scss new file mode 100644 index 000000000..83df9045e --- /dev/null +++ b/src/Squidex/app/features/content/pages/contents/contents-page.component.scss @@ -0,0 +1,7 @@ +@import '_vars'; +@import '_mixins'; + +.panel { + min-width: 600px; + max-width: 600px; +} \ No newline at end of file diff --git a/src/Squidex/app/features/content/pages/contents/contents-page.component.ts b/src/Squidex/app/features/content/pages/contents/contents-page.component.ts new file mode 100644 index 000000000..307ee8b09 --- /dev/null +++ b/src/Squidex/app/features/content/pages/contents/contents-page.component.ts @@ -0,0 +1,39 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; + +import { + AppComponentBase, + AppsStoreService, + NotificationService, + SchemaDetailsDto, + UsersProviderService +} from 'shared'; + +@Component({ + selector: 'sqx-contents-page', + styleUrls: ['./contents-page.component.scss'], + templateUrl: './contents-page.component.html' +}) +export class ContentsPageComponent extends AppComponentBase implements OnInit { + public schema: SchemaDetailsDto; + + constructor(apps: AppsStoreService, notifications: NotificationService, users: UsersProviderService, + private readonly route: ActivatedRoute + ) { + super(apps, notifications, users); + } + + public ngOnInit() { + this.route.data.map(p => p['schema']).subscribe(schema => { + this.schema = schema; + }); + } +} + diff --git a/src/Squidex/app/features/content/pages/schemas/schemas-page.component.html b/src/Squidex/app/features/content/pages/schemas/schemas-page.component.html new file mode 100644 index 000000000..4de39750e --- /dev/null +++ b/src/Squidex/app/features/content/pages/schemas/schemas-page.component.html @@ -0,0 +1,35 @@ + + +
+
+
+

Schemas

+ + + + +
+ +
+
+ + + +
+
+
+ +
+
+
+
+
+ {{schema|displayName}} +
+
+
+
+
+
+ + \ No newline at end of file diff --git a/src/Squidex/app/features/content/pages/schemas/schemas-page.component.scss b/src/Squidex/app/features/content/pages/schemas/schemas-page.component.scss new file mode 100644 index 000000000..578ff0237 --- /dev/null +++ b/src/Squidex/app/features/content/pages/schemas/schemas-page.component.scss @@ -0,0 +1,83 @@ +@import '_vars'; +@import '_mixins'; + +.panel { + min-width: 280px; + max-width: 280px; +} + +.panel-header { + min-height: 120px; + max-height: 120px; +} + +.subheader { + @include flex-box; + @include flex-flow(row); + margin-top: 2rem; + margin-right: -40px; +} + +.search-form { + & { + @include flex-grow(1); + position: relative; + } + + .form-control { + padding-left: 50px; + } + + .icon-search { + @include absolute(10px, auto, auto, 12px); + color: $color-dark-foreground-selected; + font-size: 1.3rem; + font-weight: lighter; + } +} + +.schemas { + margin-left: -$panel-padding; + margin-right: -$panel-padding; +} + +.schema { + & { + padding-left: $panel-padding; + padding-right: $panel-padding; + } + + &:hover, + &.active { + & { + background: $color-dark-background-selected; + } + + & + .schema > .schema-inner { + border-color: transparent; + } + + .schema-inner { + border-color: transparent; + } + } + + &-inner { + padding-top: 1rem; + padding-bottom: 1rem; + border-top: 1px solid darken($color-dark-foreground, 10%); + } + + &-name { + @include truncate; + color: $color-dark-foreground-selected; + font-size: 1rem; + font-weight: normal; + } + + &:first-child { + .schema-inner { + border: 0; + } + } +} \ No newline at end of file diff --git a/src/Squidex/app/features/content/pages/schemas/schemas-page.component.ts b/src/Squidex/app/features/content/pages/schemas/schemas-page.component.ts new file mode 100644 index 000000000..d26720b77 --- /dev/null +++ b/src/Squidex/app/features/content/pages/schemas/schemas-page.component.ts @@ -0,0 +1,76 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { Component, OnInit } from '@angular/core'; +import { FormControl } from '@angular/forms'; +import { BehaviorSubject, Observable } from 'rxjs'; + +import { + AppComponentBase, + AppsStoreService, + ImmutableArray, + NotificationService, + SchemaDto, + SchemasService, + UsersProviderService +} from 'shared'; + +@Component({ + selector: 'sqx-schemas-page', + styleUrls: ['./schemas-page.component.scss'], + templateUrl: './schemas-page.component.html' +}) +export class SchemasPageComponent extends AppComponentBase implements OnInit { + public schemas = new BehaviorSubject(ImmutableArray.empty()); + public schemasFilter = new FormControl(); + public schemasFiltered = + Observable.of(null) + .merge(this.schemasFilter.valueChanges.debounceTime(100)) + .combineLatest(this.schemas, + (query, schemas) => { + + schemas = schemas.filter(t => t.isPublished); + + if (query && query.length > 0) { + schemas = schemas.filter(t => t.name.indexOf(query) >= 0); + } + + schemas = + schemas.sort((a, b) => { + if (a.name < b.name) { + return -1; + } + if (a.name > b.name) { + return 1; + } + return 0; + }); + + return schemas; + }); + + constructor(apps: AppsStoreService, notifications: NotificationService, users: UsersProviderService, + private readonly schemasService: SchemasService + ) { + super(apps, notifications, users); + } + + public ngOnInit() { + this.load(); + } + + public load() { + this.appName() + .switchMap(app => this.schemasService.getSchemas(app).retry(2)) + .subscribe(dtos => { + this.schemas.next(ImmutableArray.of(dtos)); + }, error => { + this.notifyError(error); + }); + } +} + diff --git a/src/Squidex/app/features/schemas/module.ts b/src/Squidex/app/features/schemas/module.ts index fea1809ae..8b81650a8 100644 --- a/src/Squidex/app/features/schemas/module.ts +++ b/src/Squidex/app/features/schemas/module.ts @@ -10,6 +10,7 @@ import { RouterModule, Routes } from '@angular/router'; import { HistoryComponent, + ResolveSchemaGuard, SqxFrameworkModule, SqxSharedModule } from 'shared'; @@ -39,6 +40,9 @@ const routes: Routes = [ { path: ':schemaName', component: SchemaPageComponent, + resolve: { + schema: ResolveSchemaGuard + }, children: [ { path: 'history', diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.html b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.html index ec41500cb..c75b13583 100644 --- a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.html +++ b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.html @@ -16,8 +16,8 @@

{{schemaProperties|displayName}} -

- + + diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts index 96e7f4742..b358dcbb3 100644 --- a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts +++ b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts @@ -5,10 +5,9 @@ * Copyright (c) Sebastian Stehle. All rights reserved */ -import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; -import { Subscription } from 'rxjs'; import { AddFieldDto, @@ -22,6 +21,7 @@ import { MessageBus, ModalView, NotificationService, + SchemaDetailsDto, SchemasService, UpdateFieldDto, UsersProviderService @@ -38,9 +38,7 @@ import { SchemaUpdated } from './../messages'; fadeAnimation ] }) -export class SchemaPageComponent extends AppComponentBase implements OnDestroy, OnInit { - private routerSubscription: Subscription; - +export class SchemaPageComponent extends AppComponentBase implements OnInit { public fieldTypes: string[] = [ 'string', 'number', @@ -78,30 +76,13 @@ export class SchemaPageComponent extends AppComponentBase implements OnDestroy, super(apps, notifications, users); } - public ngOnDestroy() { - this.routerSubscription.unsubscribe(); - } - public ngOnInit() { - this.routerSubscription = - this.route.params.map(p => p['schemaName']).subscribe(name => { - this.schemaName = name; - - this.reset(); - this.load(); - }); - } - - public load() { - this.appName() - .switchMap(app => this.schemasService.getSchema(app, this.schemaName)).retry(2) - .subscribe(dto => { - this.schemaFields = ImmutableArray.of(dto.fields); - this.schemaProperties = new SchemaPropertiesDto(dto.name, dto.label, dto.hints); - this.isPublished = dto.isPublished; - }, error => { - this.notifyError(error); - }); + this.route.data.map(p => p['schema']).subscribe((schema: SchemaDetailsDto) => { + this.schemaName = schema.name; + this.schemaFields = ImmutableArray.of(schema.fields); + this.schemaProperties = new SchemaPropertiesDto(schema.name, schema.label, schema.hints); + this.isPublished = schema.isPublished; + }); } public publish() { @@ -222,10 +203,6 @@ export class SchemaPageComponent extends AppComponentBase implements OnDestroy, } } - private reset() { - this.schemaFields = ImmutableArray.empty(); - } - public onSchemaSaved(properties: SchemaPropertiesDto) { this.updateProperties(properties); diff --git a/src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.html b/src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.html index 7eed595c4..f88c9472d 100644 --- a/src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.html +++ b/src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.html @@ -45,7 +45,7 @@ -
+
diff --git a/src/Squidex/app/framework/angular/name.pipe.spec.ts b/src/Squidex/app/framework/angular/name.pipe.spec.ts index 3a3fac67d..18c36d138 100644 --- a/src/Squidex/app/framework/angular/name.pipe.spec.ts +++ b/src/Squidex/app/framework/angular/name.pipe.spec.ts @@ -15,6 +15,12 @@ describe('DisplayNamePipe', () => { expect(pipe.transform(undefined)).toBe(''); }); + it('should return value from nested object', () => { + const pipe = new DisplayNamePipe(); + + expect(pipe.transform({ propertes: { label: 'name' } }, 'properties.label')).toBe('name'); + }); + it('should return label if value is valid', () => { const pipe = new DisplayNamePipe(); diff --git a/src/Squidex/app/framework/angular/name.pipe.ts b/src/Squidex/app/framework/angular/name.pipe.ts index 60b9cc644..b4113fd64 100644 --- a/src/Squidex/app/framework/angular/name.pipe.ts +++ b/src/Squidex/app/framework/angular/name.pipe.ts @@ -18,6 +18,25 @@ export class DisplayNamePipe { return ''; } - return StringHelper.firstNonEmpty(value[field1], value[field2]); + return StringHelper.firstNonEmpty(this.valueOf(value, field1), this.valueOf(value, field2)); + } + + private valueOf(o: any, s: string): any { + s = s.replace(/\[(\w+)\]/g, '.$1'); + s = s.replace(/^\./, ''); + + const parts = s.split('.'); + + for (let i = 0, n = parts.length; i < n; ++i) { + let k = parts[i]; + + if (k in o) { + o = o[k]; + } else { + return; + } + } + + return o; } } \ No newline at end of file diff --git a/src/Squidex/app/framework/module.ts b/src/Squidex/app/framework/module.ts index 7b0ff4951..277aa31e3 100644 --- a/src/Squidex/app/framework/module.ts +++ b/src/Squidex/app/framework/module.ts @@ -6,13 +6,14 @@ */ import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; +import { ModuleWithProviders, NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { HttpModule } from '@angular/http'; import { RouterModule } from '@angular/router'; import { AutocompleteComponent, + ClipboardService, CloakDirective, CopyDirective, DayOfWeekPipe, @@ -22,17 +23,23 @@ import { FocusOnChangeDirective, FocusOnInitDirective, FromNowPipe, + LocalStoreService, + MessageBus, ModalViewDirective, MoneyPipe, MonthPipe, + NotificationService, PanelContainerDirective, PanelDirective, + PanelService, ScrollActiveDirective, ShortcutComponent, + ShortcutService, ShortDatePipe, ShortTimePipe, SliderComponent, TagEditorComponent, + TitleService, TitleComponent, UserReportComponent } from './declarations'; @@ -101,4 +108,19 @@ import { ReactiveFormsModule ] }) -export class SqxFrameworkModule { } \ No newline at end of file +export class SqxFrameworkModule { + public static forRoot(): ModuleWithProviders { + return { + ngModule: SqxFrameworkModule, + providers: [ + ClipboardService, + LocalStoreService, + MessageBus, + NotificationService, + PanelService, + ShortcutService, + TitleService + ] + }; + } + } \ No newline at end of file diff --git a/src/Squidex/app/shared/declarations.ts b/src/Squidex/app/shared/declarations.ts index 3be290254..296cee640 100644 --- a/src/Squidex/app/shared/declarations.ts +++ b/src/Squidex/app/shared/declarations.ts @@ -12,6 +12,8 @@ export * from './components/history.component'; export * from './guards/app-must-exist.guard'; export * from './guards/must-be-authenticated.guard'; export * from './guards/must-be-not-authenticated.guard'; +export * from './guards/resolve-published-schema.guard'; +export * from './guards/resolve-schema.guard'; export * from './services/app-contributors.service'; export * from './services/app-clients.service'; diff --git a/src/Squidex/app/shared/guards/app-must-exist.guard.spec.ts b/src/Squidex/app/shared/guards/app-must-exist.guard.spec.ts index cebdbdf3d..dbc130821 100644 --- a/src/Squidex/app/shared/guards/app-must-exist.guard.spec.ts +++ b/src/Squidex/app/shared/guards/app-must-exist.guard.spec.ts @@ -22,11 +22,12 @@ describe('AppMustExistGuard', () => { it('should navigate to 404 page if app is not found', (done) => { appsStore.setup(x => x.selectApp('my-app')) .returns(() => Promise.resolve(false)); - const router = new RouterMockup(); + const route = { params: { appName: 'my-app' } }; + const guard = new AppMustExistGuard(appsStore.object, router); - guard.canActivate( { params: { appName: 'my-app' } }, null) + guard.canActivate(route, null) .then(result => { expect(result).toBeFalsy(); expect(router.lastNavigation).toEqual(['/404']); @@ -38,11 +39,12 @@ describe('AppMustExistGuard', () => { it('should navigate to 404 page if app loading fails', (done) => { appsStore.setup(x => x.selectApp('my-app')) .returns(() => Promise.reject('error')); - const router = new RouterMockup(); + const route = { params: { appName: 'my-app' } }; + const guard = new AppMustExistGuard(appsStore.object, router); - guard.canActivate( { params: { appName: 'my-app' } }, null) + guard.canActivate(route, null) .then(result => { expect(result).toBeFalsy(); expect(router.lastNavigation).toEqual(['/404']); @@ -54,11 +56,12 @@ describe('AppMustExistGuard', () => { it('should return true if app is found', (done) => { appsStore.setup(x => x.selectApp('my-app')) .returns(() => Promise.resolve(true)); - const router = new RouterMockup(); + const route = { params: { appName: 'my-app' } }; + const guard = new AppMustExistGuard(appsStore.object, router); - guard.canActivate( { params: { appName: 'my-app' } }, null) + guard.canActivate(route, null) .then(result => { expect(result).toBeTruthy(); expect(router.lastNavigation).toBeUndefined(); diff --git a/src/Squidex/app/shared/guards/must-be-authenticated.guard.spec.ts b/src/Squidex/app/shared/guards/must-be-authenticated.guard.spec.ts index d138ea96b..09adec8f2 100644 --- a/src/Squidex/app/shared/guards/must-be-authenticated.guard.spec.ts +++ b/src/Squidex/app/shared/guards/must-be-authenticated.guard.spec.ts @@ -22,8 +22,8 @@ describe('MustBeAuthenticatedGuard', () => { it('should navigate to default page if not authenticated', (done) => { authService.setup(x => x.checkLogin()) .returns(() => Promise.resolve(false)); - const router = new RouterMockup(); + const guard = new MustBeAuthenticatedGuard(authService.object, router); guard.canActivate(null, null) @@ -38,8 +38,8 @@ describe('MustBeAuthenticatedGuard', () => { it('should return true if authenticated', (done) => { authService.setup(x => x.checkLogin()) .returns(() => Promise.resolve(true)); - const router = new RouterMockup(); + const guard = new MustBeAuthenticatedGuard(authService.object, router); guard.canActivate(null, null) diff --git a/src/Squidex/app/shared/guards/must-be-not-authenticated.guard.spec.ts b/src/Squidex/app/shared/guards/must-be-not-authenticated.guard.spec.ts index a66047dbc..26991e758 100644 --- a/src/Squidex/app/shared/guards/must-be-not-authenticated.guard.spec.ts +++ b/src/Squidex/app/shared/guards/must-be-not-authenticated.guard.spec.ts @@ -22,8 +22,8 @@ describe('MustBeNotAuthenticatedGuard', () => { it('should navigate to app page if authenticated', (done) => { authService.setup(x => x.checkLogin()) .returns(() => Promise.resolve(true)); - const router = new RouterMockup(); + const guard = new MustBeNotAuthenticatedGuard(authService.object, router); guard.canActivate(null, null) @@ -38,8 +38,8 @@ describe('MustBeNotAuthenticatedGuard', () => { it('should return true if not authenticated', (done) => { authService.setup(x => x.checkLogin()) .returns(() => Promise.resolve(false)); - const router = new RouterMockup(); + const guard = new MustBeNotAuthenticatedGuard(authService.object, router); guard.canActivate(null, null) diff --git a/src/Squidex/app/shared/guards/resolve-published-schema.guard.ts b/src/Squidex/app/shared/guards/resolve-published-schema.guard.ts new file mode 100644 index 000000000..5f2686872 --- /dev/null +++ b/src/Squidex/app/shared/guards/resolve-published-schema.guard.ts @@ -0,0 +1,55 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router'; + +import { SchemaDetailsDto, SchemasService } from './../services/schemas.service'; + +@Injectable() +export class ResolvePublishedSchemaGuard implements Resolve { + constructor( + private readonly schemasService: SchemasService, + private readonly router: Router + ) { + } + + public resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise { + const appName = this.findParameter(route, 'appName'); + const schemaName = this.findParameter(route, 'schemaName'); + + const result = + this.schemasService.getSchema(appName, schemaName).toPromise() + .then(dto => { + if (!dto || !dto.isPublished) { + this.router.navigate(['/404']); + } + + return dto; + }).catch(() => { + this.router.navigate(['/404']); + }); + + return result; + } + + private findParameter(route: ActivatedRouteSnapshot, name: string) { + let result: string; + + while (route) { + result = route.params[name]; + + if (result) { + break; + } + + route = route.parent; + } + + return result; + } +} \ No newline at end of file diff --git a/src/Squidex/app/shared/guards/resolve-schema.guard.ts b/src/Squidex/app/shared/guards/resolve-schema.guard.ts new file mode 100644 index 000000000..81e5ef117 --- /dev/null +++ b/src/Squidex/app/shared/guards/resolve-schema.guard.ts @@ -0,0 +1,55 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router'; + +import { SchemaDetailsDto, SchemasService } from './../services/schemas.service'; + +@Injectable() +export class ResolveSchemaGuard implements Resolve { + constructor( + private readonly schemasService: SchemasService, + private readonly router: Router + ) { + } + + public resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise { + const appName = this.findParameter(route, 'appName'); + const schemaName = this.findParameter(route, 'schemaName'); + + const result = + this.schemasService.getSchema(appName, schemaName).toPromise() + .then(dto => { + if (!dto) { + this.router.navigate(['/404']); + } + + return dto; + }).catch(() => { + this.router.navigate(['/404']); + }); + + return result; + } + + private findParameter(route: ActivatedRouteSnapshot, name: string) { + let result: string; + + while (route) { + result = route.params[name]; + + if (result) { + break; + } + + route = route.parent; + } + + return result; + } +} \ No newline at end of file diff --git a/src/Squidex/app/shared/module.ts b/src/Squidex/app/shared/module.ts index 6d6aafb7c..5f4b5cf26 100644 --- a/src/Squidex/app/shared/module.ts +++ b/src/Squidex/app/shared/module.ts @@ -5,14 +5,30 @@ * Copyright (c) Sebastian Stehle. All rights reserved */ -import { NgModule } from '@angular/core'; +import { ModuleWithProviders, NgModule } from '@angular/core'; import { SqxFrameworkModule } from 'framework'; import { AppFormComponent, + AppClientsService, + AppContributorsService, + AppLanguagesService, + AppsStoreService, + AppsService, + AppMustExistGuard, + AuthService, DashboardLinkDirective, - HistoryComponent + HistoryComponent, + HistoryService, + LanguageService, + MustBeAuthenticatedGuard, + MustBeNotAuthenticatedGuard, + ResolvePublishedSchemaGuard, + ResolveSchemaGuard, + SchemasService, + UsersProviderService, + UsersService } from './declarations'; @NgModule({ @@ -30,4 +46,28 @@ import { HistoryComponent ] }) -export class SqxSharedModule { } \ No newline at end of file +export class SqxSharedModule { + public static forRoot(): ModuleWithProviders { + return { + ngModule: SqxSharedModule, + providers: [ + AppClientsService, + AppContributorsService, + AppLanguagesService, + AppsStoreService, + AppsService, + AppMustExistGuard, + AuthService, + HistoryService, + LanguageService, + MustBeAuthenticatedGuard, + MustBeNotAuthenticatedGuard, + ResolvePublishedSchemaGuard, + ResolveSchemaGuard, + SchemasService, + UsersProviderService, + UsersService + ] + }; + } +} \ No newline at end of file diff --git a/src/Squidex/app/theme/_bootstrap.scss b/src/Squidex/app/theme/_bootstrap.scss index 57dae2d49..33320c710 100644 --- a/src/Squidex/app/theme/_bootstrap.scss +++ b/src/Squidex/app/theme/_bootstrap.scss @@ -161,6 +161,7 @@ select { .form-hint { & { font-size: .8rem; + color: lighten($color-text, 30%); } p {