Browse Source

Various UI improvements

pull/327/head
Sebastian 7 years ago
parent
commit
54dc994e8d
  1. 2
      src/Squidex/app/features/administration/guards/user-must-exist.guard.ts
  2. 3
      src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.ts
  3. 8
      src/Squidex/app/features/administration/pages/restore/restore-page.component.ts
  4. 4
      src/Squidex/app/features/content/module.ts
  5. 12
      src/Squidex/app/features/content/pages/content/content-page.component.ts
  6. 35
      src/Squidex/app/features/content/pages/contents/contents-page.component.ts
  7. 1
      src/Squidex/app/features/content/pages/schemas/schemas-page.component.html
  8. 2
      src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts
  9. 6
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts
  10. 2
      src/Squidex/app/framework/angular/forms/date-time-editor.component.ts
  11. 2
      src/Squidex/app/framework/angular/forms/stars.component.ts
  12. 2
      src/Squidex/app/framework/angular/image-source.directive.ts
  13. 2
      src/Squidex/app/framework/angular/modals/modal-view.directive.ts
  14. 22
      src/Squidex/app/framework/angular/routers/router-utils.ts
  15. 5
      src/Squidex/app/shared/components/geolocation-editor.component.ts
  16. 2
      src/Squidex/app/shared/components/schema-category.component.html
  17. 7
      src/Squidex/app/shared/components/schema-category.component.ts
  18. 4
      src/Squidex/app/shared/guards/content-must-exist.guard.spec.ts
  19. 2
      src/Squidex/app/shared/guards/content-must-exist.guard.ts
  20. 4
      src/Squidex/app/shared/guards/must-be-authenticated.guard.spec.ts
  21. 4
      src/Squidex/app/shared/guards/must-be-not-authenticated.guard.spec.ts
  22. 41
      src/Squidex/app/shared/guards/schema-must-exist-published.guard.spec.ts
  23. 10
      src/Squidex/app/shared/guards/schema-must-exist-published.guard.ts
  24. 2
      src/Squidex/app/shared/guards/schema-must-exist.guard.spec.ts
  25. 2
      src/Squidex/app/shared/guards/schema-must-exist.guard.ts
  26. 105
      src/Squidex/app/shared/guards/schema-must-not-be-singleton.guard.spec.ts
  27. 42
      src/Squidex/app/shared/guards/schema-must-not-be-singleton.guard.ts
  28. 1
      src/Squidex/app/shared/internal.ts
  29. 2
      src/Squidex/app/shared/module.ts
  30. 2
      src/Squidex/app/shared/state/contents.forms.ts
  31. 2
      src/Squidex/app/shared/state/contents.state.ts
  32. 4
      src/Squidex/app/shared/state/queries.spec.ts
  33. 25
      tools/GenerateLanguages/GenerateLanguages.sln

2
src/Squidex/app/features/administration/guards/user-must-exist.guard.ts

@ -32,7 +32,7 @@ export class UserMustExistGuard implements CanActivate {
this.router.navigate(['/404']); this.router.navigate(['/404']);
} }
}), }),
map(u => u !== null)); map(u => !!u));
return result; return result;
} }

3
src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.ts

@ -38,8 +38,7 @@ export class EventConsumersPageComponent implements OnDestroy, OnInit {
this.eventConsumersState.load(false, true).pipe(onErrorResumeNext()).subscribe(); this.eventConsumersState.load(false, true).pipe(onErrorResumeNext()).subscribe();
this.timerSubscription = this.timerSubscription =
timer(2000, 2000).pipe( timer(2000, 2000).pipe(switchMap(x => this.eventConsumersState.load(true, true)), onErrorResumeNext())
switchMap(x => this.eventConsumersState.load(true, true)), onErrorResumeNext())
.subscribe(); .subscribe();
} }

8
src/Squidex/app/features/administration/pages/restore/restore-page.component.ts

@ -8,7 +8,7 @@
import { Component, OnDestroy, OnInit } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms'; import { FormBuilder } from '@angular/forms';
import { Subscription, timer } from 'rxjs'; import { Subscription, timer } from 'rxjs';
import { switchMap } from 'rxjs/operators'; import { filter, switchMap } from 'rxjs/operators';
import { import {
AuthService, AuthService,
@ -43,11 +43,9 @@ export class RestorePageComponent implements OnDestroy, OnInit {
public ngOnInit() { public ngOnInit() {
this.timerSubscription = this.timerSubscription =
timer(0, 2000).pipe(switchMap(() => this.backupsService.getRestore())) timer(0, 2000).pipe(switchMap(() => this.backupsService.getRestore()), filter(x => !!x))
.subscribe(dto => { .subscribe(dto => {
if (dto !== null) { this.restoreJob = dto!;
this.restoreJob = dto;
}
}); });
} }

4
src/Squidex/app/features/content/module.ts

@ -15,6 +15,7 @@ import {
ContentMustExistGuard, ContentMustExistGuard,
LoadLanguagesGuard, LoadLanguagesGuard,
SchemaMustExistPublishedGuard, SchemaMustExistPublishedGuard,
SchemaMustNotBeSingletonGuard,
SqxFrameworkModule, SqxFrameworkModule,
SqxSharedModule, SqxSharedModule,
UnsetContentGuard UnsetContentGuard
@ -52,12 +53,13 @@ const routes: Routes = [
{ {
path: '', path: '',
component: ContentsPageComponent, component: ContentsPageComponent,
canActivate: [SchemaMustNotBeSingletonGuard],
canDeactivate: [CanDeactivateGuard] canDeactivate: [CanDeactivateGuard]
}, },
{ {
path: 'new', path: 'new',
component: ContentPageComponent, component: ContentPageComponent,
canActivate: [UnsetContentGuard], canActivate: [SchemaMustNotBeSingletonGuard, UnsetContentGuard],
canDeactivate: [CanDeactivateGuard] canDeactivate: [CanDeactivateGuard]
}, },
{ {

12
src/Squidex/app/features/content/pages/content/content-page.component.ts

@ -8,7 +8,7 @@
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { Observable, of, Subscription } from 'rxjs'; import { Observable, of, Subscription } from 'rxjs';
import { filter, map, onErrorResumeNext, switchMap } from 'rxjs/operators'; import { filter, onErrorResumeNext, switchMap } from 'rxjs/operators';
import { ContentVersionSelected } from './../messages'; import { ContentVersionSelected } from './../messages';
@ -88,19 +88,19 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy,
}); });
this.selectedSchemaSubscription = this.selectedSchemaSubscription =
this.schemasState.selectedSchema.pipe(filter(s => !!s), map(s => s!)) this.schemasState.selectedSchema.pipe(filter(s => !!s))
.subscribe(schema => { .subscribe(schema => {
this.schema = schema; this.schema = schema!;
this.contentForm = new EditContentForm(this.schema, this.languages); this.contentForm = new EditContentForm(this.schema, this.languages);
}); });
this.contentSubscription = this.contentSubscription =
this.contentsState.selectedContent.pipe(filter(c => !!c), map(c => c!)) this.contentsState.selectedContent.pipe(filter(c => !!c))
.subscribe(content => { .subscribe(content => {
this.content = content; this.content = content!;
this.loadContent(content.dataDraft); this.loadContent(this.content.dataDraft);
}); });
this.contentVersionSelectedSubscription = this.contentVersionSelectedSubscription =

35
src/Squidex/app/features/content/pages/contents/contents-page.component.ts

@ -6,8 +6,9 @@
*/ */
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { Router } from '@angular/router';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { onErrorResumeNext, switchMap, tap } from 'rxjs/operators'; import { filter, onErrorResumeNext, switchMap, takeUntil, tap } from 'rxjs/operators';
import { import {
AppLanguageDto, AppLanguageDto,
@ -17,6 +18,7 @@ import {
ImmutableArray, ImmutableArray,
LanguagesState, LanguagesState,
ModalModel, ModalModel,
navigatedToOtherComponent,
Queries, Queries,
SchemaDetailsDto, SchemaDetailsDto,
SchemasState, SchemasState,
@ -59,6 +61,7 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
public readonly contentsState: ContentsState, public readonly contentsState: ContentsState,
private readonly languagesState: LanguagesState, private readonly languagesState: LanguagesState,
private readonly schemasState: SchemasState, private readonly schemasState: SchemasState,
private readonly router: Router,
private readonly uiState: UIState private readonly uiState: UIState
) { ) {
} }
@ -70,8 +73,10 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
} }
public ngOnInit() { public ngOnInit() {
const routeChanged = this.router.events.pipe(filter(navigatedToOtherComponent(this.router)));
this.selectedSchemaSubscription = this.selectedSchemaSubscription =
this.schemasState.selectedSchema this.schemasState.selectedSchema.pipe(takeUntil(routeChanged))
.subscribe(schema => { .subscribe(schema => {
this.resetSelection(); this.resetSelection();
@ -82,7 +87,7 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
}); });
this.contentsSubscription = this.contentsSubscription =
this.contentsState.contents this.contentsState.contents.pipe(takeUntil(routeChanged))
.subscribe(() => { .subscribe(() => {
this.updateSelectionSummary(); this.updateSelectionSummary();
}); });
@ -100,7 +105,7 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
} }
public deleteSelected() { public deleteSelected() {
this.contentsState.deleteMany(this.select()).pipe(onErrorResumeNext()).subscribe(); this.contentsState.deleteMany(this.selectItems()).pipe(onErrorResumeNext()).subscribe();
} }
public delete(content: ContentDto) { public delete(content: ContentDto) {
@ -112,7 +117,7 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
} }
public publishSelected() { public publishSelected() {
this.changeContentItems(this.select(c => c.status !== 'Published'), 'Publish'); this.changeContentItems(this.selectItems(c => c.status !== 'Published'), 'Publish');
} }
public unpublish(content: ContentDto) { public unpublish(content: ContentDto) {
@ -120,7 +125,7 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
} }
public unpublishSelected() { public unpublishSelected() {
this.changeContentItems(this.select(c => c.status === 'Published'), 'Unpublish'); this.changeContentItems(this.selectItems(c => c.status === 'Published'), 'Unpublish');
} }
public archive(content: ContentDto) { public archive(content: ContentDto) {
@ -128,15 +133,15 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
} }
public archiveSelected() { public archiveSelected() {
this.changeContentItems(this.select(), 'Archive'); this.changeContentItems(this.selectItems(), 'Archive');
} }
public restore(content: ContentDto) { public restore(content: ContentDto) {
this.changeContentItems([content], 'Restore'); this.changeContentItems([content], 'Restore');
} }
public restoreSelected(scheduled: boolean) { public restoreSelected() {
this.changeContentItems(this.select(), 'Restore'); this.changeContentItems(this.selectItems(), 'Restore');
} }
public clone(content: ContentDto) { public clone(content: ContentDto) {
@ -185,12 +190,16 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
this.contentsState.search(query).pipe(onErrorResumeNext()).subscribe(); this.contentsState.search(query).pipe(onErrorResumeNext()).subscribe();
} }
public selectLanguage(language: AppLanguageDto) {
this.language = language;
}
public isItemSelected(content: ContentDto): boolean { public isItemSelected(content: ContentDto): boolean {
return !!this.selectedItems[content.id]; return !!this.selectedItems[content.id];
} }
public selectLanguage(language: AppLanguageDto) { private selectItems(predicate?: (content: ContentDto) => boolean) {
this.language = language; return this.contentsState.snapshot.contents.values.filter(c => this.selectedItems[c.id] && (!predicate || predicate(c)));
} }
public selectItem(content: ContentDto, isSelected: boolean) { public selectItem(content: ContentDto, isSelected: boolean) {
@ -215,10 +224,6 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
return content.id; return content.id;
} }
private select(predicate?: (content: ContentDto) => boolean) {
return this.contentsState.snapshot.contents.values.filter(c => this.selectedItems[c.id] && (!predicate || predicate(c)));
}
private resetSelection() { private resetSelection() {
this.selectedItems = {}; this.selectedItems = {};

1
src/Squidex/app/features/content/pages/schemas/schemas-page.component.html

@ -21,6 +21,7 @@
[name]="category" [name]="category"
[schemas]="schemas" [schemas]="schemas"
[schemasFilter]="schemasFilter.valueChanges | async" [schemasFilter]="schemasFilter.valueChanges | async"
[routeSingletonToContent]="true"
[isReadonly]="true"> [isReadonly]="true">
</sqx-schema-category> </sqx-schema-category>
</ng-container> </ng-container>

2
src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts

@ -82,7 +82,7 @@ export class ContentChangedTriggerComponent implements OnInit {
} else { } else {
return null; return null;
} }
}).filter(s => s !== null).map(s => s!)).sortByStringAsc(s => s.schema.name); }).filter(s => !!s).map(s => s!)).sortByStringAsc(s => s.schema.name);
this.schemasToAdd = this.schemasToAdd =
this.schemas.filter(schema => this.schemas.filter(schema =>

6
src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts

@ -10,7 +10,7 @@
import { Component, OnDestroy, OnInit } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { filter, map, onErrorResumeNext } from 'rxjs/operators'; import { filter, onErrorResumeNext } from 'rxjs/operators';
import { import {
AppsState, AppsState,
@ -73,9 +73,9 @@ export class SchemaPageComponent implements OnDestroy, OnInit {
this.patternsState.load().pipe(onErrorResumeNext()).subscribe(); this.patternsState.load().pipe(onErrorResumeNext()).subscribe();
this.selectedSchemaSubscription = this.selectedSchemaSubscription =
this.schemasState.selectedSchema.pipe(filter(s => !!s), map(s => s!)) this.schemasState.selectedSchema.pipe(filter(s => !!s))
.subscribe(schema => { .subscribe(schema => {
this.schema = schema; this.schema = schema!;
this.export(); this.export();
}); });

2
src/Squidex/app/framework/angular/forms/date-time-editor.component.ts

@ -52,7 +52,7 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy,
} }
public get hasValue() { public get hasValue() {
return this.dateValue !== null; return !!this.dateValue;
} }
@ViewChild('dateInput') @ViewChild('dateInput')

2
src/Squidex/app/framework/angular/forms/stars.component.ts

@ -88,7 +88,7 @@ export class StarsComponent implements ControlValueAccessor {
return false; return false;
} }
if (this.value !== null) { if (this.value) {
this.value = null; this.value = null;
this.stars = 0; this.stars = 0;

2
src/Squidex/app/framework/angular/image-source.directive.ts

@ -96,7 +96,7 @@ export class ImageSourceDirective implements OnChanges, OnDestroy, OnInit, After
if (w > 0 && h > 0) { if (w > 0 && h > 0) {
let source = `${this.imageSource}&width=${w}&height=${h}&mode=Crop`; let source = `${this.imageSource}&width=${w}&height=${h}&mode=Crop`;
if (this.loadQuery !== null) { if (this.loadQuery) {
source += `&q=${this.loadQuery}`; source += `&q=${this.loadQuery}`;
} }

2
src/Squidex/app/framework/angular/modals/modal-view.directive.ts

@ -71,7 +71,7 @@ export class ModalViewDirective implements OnChanges, OnDestroy {
} }
private update(isOpen: boolean) { private update(isOpen: boolean) {
if (isOpen === (this.renderedView !== null)) { if (isOpen === (!!this.renderedView)) {
return; return;
} }

22
src/Squidex/app/framework/angular/routers/router-utils.ts

@ -5,7 +5,9 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { ActivatedRoute, ActivatedRouteSnapshot, Data, Params } from '@angular/router'; import { ActivatedRoute, ActivatedRouteSnapshot, Data, Params, Router, RouterEvent, RouterStateSnapshot, RoutesRecognized } from '@angular/router';
import { Types } from './../../utils/types';
export function allData(value: ActivatedRouteSnapshot | ActivatedRoute): Data { export function allData(value: ActivatedRouteSnapshot | ActivatedRoute): Data {
let snapshot: ActivatedRouteSnapshot | null = value['snapshot'] || value; let snapshot: ActivatedRouteSnapshot | null = value['snapshot'] || value;
@ -40,4 +42,22 @@ export function allParams(value: ActivatedRouteSnapshot | ActivatedRoute): Param
} }
return result; return result;
}
export function childComponent(value: RouterStateSnapshot) {
let current = value.root;
while (true) {
if (current.firstChild) {
current = current.firstChild;
} else {
break;
}
}
return current.component;
}
export function navigatedToOtherComponent(router: Router) {
return (e: RouterEvent) => Types.is(e, RoutesRecognized) && childComponent(e.state) !== childComponent(router.routerState.snapshot);
} }

5
src/Squidex/app/shared/components/geolocation-editor.component.ts

@ -144,8 +144,9 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
} }
public updateValueByInput() { public updateValueByInput() {
let updateMap = this.geolocationForm.controls['latitude'].value !== null && let updateMap =
this.geolocationForm.controls['longitude'].value !== null; !!this.geolocationForm.controls['latitude'].value &&
!!this.geolocationForm.controls['longitude'].value;
this.value = this.geolocationForm.value; this.value = this.geolocationForm.value;

2
src/Squidex/app/shared/components/schema-category.component.html

@ -16,7 +16,7 @@
<ul class="nav nav-panel nav-dark nav-dark-bordered flex-column" *ngIf="isOpen" @fade> <ul class="nav nav-panel nav-dark nav-dark-bordered flex-column" *ngIf="isOpen" @fade>
<li class="nav-item" *ngFor="let schema of schemasFiltered; trackBy: trackBySchema" dnd-draggable [dragEnabled]="!isReadonly" [dragData]="schema"> <li class="nav-item" *ngFor="let schema of schemasFiltered; trackBy: trackBySchema" dnd-draggable [dragEnabled]="!isReadonly" [dragData]="schema">
<a class="nav-link" [routerLink]="[schema.name]" routerLinkActive="active"> <a class="nav-link" [routerLink]="schemaRoute(schema)" routerLinkActive="active">
<div class="row" *ngIf="!isReadonly"> <div class="row" *ngIf="!isReadonly">
<div class="col col-4"> <div class="col col-4">
<span class="schema-name schema-name-accent">{{schema.displayName}}</span> <span class="schema-name schema-name-accent">{{schema.displayName}}</span>

7
src/Squidex/app/shared/components/schema-category.component.ts

@ -36,6 +36,9 @@ export class SchemaCategoryComponent implements OnInit, OnChanges {
@Input() @Input()
public isReadonly: boolean; public isReadonly: boolean;
@Input()
public routeSingletonToContent = false;
@Input() @Input()
public schemasFilter: string; public schemasFilter: string;
@ -100,6 +103,10 @@ export class SchemaCategoryComponent implements OnInit, OnChanges {
this.schemasState.changeCategory(schema, this.name).pipe(onErrorResumeNext()).subscribe(); this.schemasState.changeCategory(schema, this.name).pipe(onErrorResumeNext()).subscribe();
} }
public schemaRoute(schema: SchemaDto) {
return schema.isSingleton && this.routeSingletonToContent ? [schema.name, schema.id] : [schema.name];
}
public trackBySchema(index: number, schema: SchemaDto) { public trackBySchema(index: number, schema: SchemaDto) {
return schema.id; return schema.id;
} }

4
src/Squidex/app/shared/guards/content-must-exist.guard.spec.ts

@ -7,7 +7,7 @@
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { IMock, Mock, Times } from 'typemoq'; import { IMock, It, Mock, Times } from 'typemoq';
import { ContentDto } from './../services/contents.service'; import { ContentDto } from './../services/contents.service';
import { ContentsState } from './../state/contents.state'; import { ContentsState } from './../state/contents.state';
@ -42,7 +42,7 @@ describe('ContentMustExistGuard', () => {
expect(result!).toBeTruthy(); expect(result!).toBeTruthy();
contentsState.verify(x => x.select('123'), Times.once()); router.verify(x => x.navigate(It.isAny()), Times.never());
}); });
it('should load content and return false when not found', () => { it('should load content and return false when not found', () => {

2
src/Squidex/app/shared/guards/content-must-exist.guard.ts

@ -32,7 +32,7 @@ export class ContentMustExistGuard implements CanActivate {
this.router.navigate(['/404']); this.router.navigate(['/404']);
} }
}), }),
map(u => u !== null)); map(u => !!u));
return result; return result;
} }

4
src/Squidex/app/shared/guards/must-be-authenticated.guard.spec.ts

@ -7,7 +7,7 @@
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { IMock, Mock, Times } from 'typemoq'; import { IMock, It, Mock, Times } from 'typemoq';
import { AuthService } from '@app/shared'; import { AuthService } from '@app/shared';
@ -52,5 +52,7 @@ describe('MustBeAuthenticatedGuard', () => {
}); });
expect(result!).toBeTruthy(); expect(result!).toBeTruthy();
router.verify(x => x.navigate(It.isAny()), Times.never());
}); });
}); });

4
src/Squidex/app/shared/guards/must-be-not-authenticated.guard.spec.ts

@ -7,7 +7,7 @@
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { IMock, Mock, Times } from 'typemoq'; import { IMock, It, Mock, Times } from 'typemoq';
import { AuthService } from '@app/shared'; import { AuthService } from '@app/shared';
@ -52,5 +52,7 @@ describe('MustNotBeAuthenticatedGuard', () => {
}); });
expect(result!).toBeTruthy(); expect(result!).toBeTruthy();
router.verify(x => x.navigate(It.isAny()), Times.never());
}); });
}); });

41
src/Squidex/app/shared/guards/schema-must-exist-published.guard.spec.ts

@ -5,9 +5,9 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Router, RouterStateSnapshot } from '@angular/router'; import { Router } from '@angular/router';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { IMock, Mock, Times } from 'typemoq'; import { IMock, It, Mock, Times } from 'typemoq';
import { SchemaDetailsDto } from './../services/schemas.service'; import { SchemaDetailsDto } from './../services/schemas.service';
import { SchemasState } from './../state/schemas.state'; import { SchemasState } from './../state/schemas.state';
@ -21,7 +21,6 @@ describe('SchemaMustExistPublishedGuard', () => {
}; };
let schemasState: IMock<SchemasState>; let schemasState: IMock<SchemasState>;
let state: RouterStateSnapshot = <any>{ url: 'current-url' };
let router: IMock<Router>; let router: IMock<Router>;
let schemaGuard: SchemaMustExistPublishedGuard; let schemaGuard: SchemaMustExistPublishedGuard;
@ -37,13 +36,13 @@ describe('SchemaMustExistPublishedGuard', () => {
let result: boolean; let result: boolean;
schemaGuard.canActivate(route, state).subscribe(x => { schemaGuard.canActivate(route).subscribe(x => {
result = x; result = x;
}).unsubscribe(); }).unsubscribe();
expect(result!).toBeTruthy(); expect(result!).toBeTruthy();
schemasState.verify(x => x.select('123'), Times.once()); router.verify(x => x.navigate(It.isAny()), Times.never());
}); });
it('should load schema and return false when not found', () => { it('should load schema and return false when not found', () => {
@ -52,7 +51,7 @@ describe('SchemaMustExistPublishedGuard', () => {
let result: boolean; let result: boolean;
schemaGuard.canActivate(route, state).subscribe(x => { schemaGuard.canActivate(route).subscribe(x => {
result = x; result = x;
}).unsubscribe(); }).unsubscribe();
@ -60,34 +59,4 @@ describe('SchemaMustExistPublishedGuard', () => {
router.verify(x => x.navigate(['/404']), Times.once()); router.verify(x => x.navigate(['/404']), Times.once());
}); });
it('should load schema and return false when not found', () => {
schemasState.setup(x => x.select('123'))
.returns(() => of(null));
let result: boolean;
schemaGuard.canActivate(route, state).subscribe(x => {
result = x;
}).unsubscribe();
expect(result!).toBeFalsy();
router.verify(x => x.navigate(['/404']), Times.once());
});
it('should redirect to content when singleton', () => {
schemasState.setup(x => x.select('123'))
.returns(() => of(<SchemaDetailsDto>{ isSingleton: true, id: 'schema-id' }));
let result: boolean;
schemaGuard.canActivate(route, state).subscribe(x => {
result = x;
}).unsubscribe();
expect(result!).toBeFalsy();
router.verify(x => x.navigate([state.url, 'schema-id']), Times.once());
});
}); });

10
src/Squidex/app/shared/guards/schema-must-exist-published.guard.ts

@ -6,7 +6,7 @@
*/ */
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router'; import { ActivatedRouteSnapshot, CanActivate, Router } from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map, tap } from 'rxjs/operators'; import { map, tap } from 'rxjs/operators';
@ -22,7 +22,7 @@ export class SchemaMustExistPublishedGuard implements CanActivate {
) { ) {
} }
public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> { public canActivate(route: ActivatedRouteSnapshot): Observable<boolean> {
const schemaName = allParams(route)['schemaName']; const schemaName = allParams(route)['schemaName'];
const result = const result =
@ -31,12 +31,8 @@ export class SchemaMustExistPublishedGuard implements CanActivate {
if (!dto || !dto.isPublished) { if (!dto || !dto.isPublished) {
this.router.navigate(['/404']); this.router.navigate(['/404']);
} }
if (dto && dto.isSingleton && state.url.indexOf(dto.id) < 0) {
this.router.navigate([state.url, dto.id]);
}
}), }),
map(s => s !== null && s.isPublished)); map(s => !!s && s.isPublished));
return result; return result;
} }

2
src/Squidex/app/shared/guards/schema-must-exist.guard.spec.ts

@ -41,8 +41,6 @@ describe('SchemaMustExistGuard', () => {
}).unsubscribe(); }).unsubscribe();
expect(result!).toBeTruthy(); expect(result!).toBeTruthy();
schemasState.verify(x => x.select('123'), Times.once());
}); });
it('should load schema and return false when not found', () => { it('should load schema and return false when not found', () => {

2
src/Squidex/app/shared/guards/schema-must-exist.guard.ts

@ -32,7 +32,7 @@ export class SchemaMustExistGuard implements CanActivate {
this.router.navigate(['/404']); this.router.navigate(['/404']);
} }
}), }),
map(s => s !== null)); map(s => !!s));
return result; return result;
} }

105
src/Squidex/app/shared/guards/schema-must-not-be-singleton.guard.spec.ts

@ -0,0 +1,105 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Router, RouterStateSnapshot, UrlSegment } from '@angular/router';
import { of } from 'rxjs';
import { IMock, It, Mock, Times } from 'typemoq';
import { SchemaDetailsDto } from './../services/schemas.service';
import { SchemasState } from './../state/schemas.state';
import { SchemaMustNotBeSingletonGuard } from './schema-must-not-be-singleton.guard';
describe('SchemaMustNotBeSingletonGuard', () => {
const route: any = {
params: {
schemaName: '123'
},
url: [
new UrlSegment('schemas', {}),
new UrlSegment('name', {}),
new UrlSegment('new', {})
]
};
let schemasState: IMock<SchemasState>;
let router: IMock<Router>;
let schemaGuard: SchemaMustNotBeSingletonGuard;
beforeEach(() => {
router = Mock.ofType<Router>();
schemasState = Mock.ofType<SchemasState>();
schemaGuard = new SchemaMustNotBeSingletonGuard(schemasState.object, router.object);
});
it('should subscribe to schema and return true when not singleton', () => {
const state: RouterStateSnapshot = <any>{ url: 'schemas/name/' };
schemasState.setup(x => x.selectedSchema)
.returns(() => of(<SchemaDetailsDto>{ id: '123', isSingleton: false }));
let result: boolean;
schemaGuard.canActivate(route, state).subscribe(x => {
result = x;
}).unsubscribe();
expect(result!).toBeTruthy();
router.verify(x => x.navigate(It.isAny()), Times.never());
});
it('should subscribe to schema and return false when not found', () => {
const state: RouterStateSnapshot = <any>{ url: 'schemas/name/' };
schemasState.setup(x => x.selectedSchema)
.returns(() => of(null));
let result: boolean;
schemaGuard.canActivate(route, state).subscribe(x => {
result = x;
}).unsubscribe();
expect(result!).toBeFalsy();
router.verify(x => x.navigate(It.isAny()), Times.never());
});
it('should redirect to content when singleton', () => {
const state: RouterStateSnapshot = <any>{ url: 'schemas/name/' };
schemasState.setup(x => x.selectedSchema)
.returns(() => of(<SchemaDetailsDto>{ id: '123', isSingleton: true }));
let result: boolean;
schemaGuard.canActivate(route, state).subscribe(x => {
result = x;
}).unsubscribe();
expect(result!).toBeFalsy();
router.verify(x => x.navigate([state.url, '123']), Times.once());
});
it('should redirect to content when singleton on new page', () => {
const state: RouterStateSnapshot = <any>{ url: 'schemas/name/new/' };
schemasState.setup(x => x.selectedSchema)
.returns(() => of(<SchemaDetailsDto>{ id: '123', isSingleton: true }));
let result: boolean;
schemaGuard.canActivate(route, state).subscribe(x => {
result = x;
}).unsubscribe();
expect(result!).toBeFalsy();
router.verify(x => x.navigate(['schemas/name/', '123']), Times.once());
});
});

42
src/Squidex/app/shared/guards/schema-must-not-be-singleton.guard.ts

@ -0,0 +1,42 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { map, take, tap } from 'rxjs/operators';
import { SchemasState } from './../state/schemas.state';
@Injectable()
export class SchemaMustNotBeSingletonGuard implements CanActivate {
constructor(
private readonly schemasState: SchemasState,
private readonly router: Router
) {
}
public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
const result =
this.schemasState.selectedSchema.pipe(
take(1),
tap(dto => {
if (dto && dto.isSingleton) {
if (state.url.indexOf('/new') >= 0) {
const parentUrl = state.url.slice(0, state.url.indexOf(route.url[route.url.length - 1].path));
this.router.navigate([parentUrl, dto.id]);
} else {
this.router.navigate([state.url, dto.id]);
}
}
}),
map(s => !!s && !s.isSingleton));
return result;
}
}

1
src/Squidex/app/shared/internal.ts

@ -13,6 +13,7 @@ export * from './guards/must-be-authenticated.guard';
export * from './guards/must-be-not-authenticated.guard'; export * from './guards/must-be-not-authenticated.guard';
export * from './guards/schema-must-exist-published.guard'; export * from './guards/schema-must-exist-published.guard';
export * from './guards/schema-must-exist.guard'; export * from './guards/schema-must-exist.guard';
export * from './guards/schema-must-not-be-singleton.guard';
export * from './guards/unset-app.guard'; export * from './guards/unset-app.guard';
export * from './guards/unset-content.guard'; export * from './guards/unset-content.guard';

2
src/Squidex/app/shared/module.ts

@ -64,6 +64,7 @@ import {
SchemaCategoryComponent, SchemaCategoryComponent,
SchemaMustExistGuard, SchemaMustExistGuard,
SchemaMustExistPublishedGuard, SchemaMustExistPublishedGuard,
SchemaMustNotBeSingletonGuard,
SchemasService, SchemasService,
SchemasState, SchemasState,
SearchFormComponent, SearchFormComponent,
@ -180,6 +181,7 @@ export class SqxSharedModule {
RulesState, RulesState,
SchemaMustExistGuard, SchemaMustExistGuard,
SchemaMustExistPublishedGuard, SchemaMustExistPublishedGuard,
SchemaMustNotBeSingletonGuard,
SchemasService, SchemasService,
SchemasState, SchemasState,
UIService, UIService,

2
src/Squidex/app/shared/state/contents.forms.ts

@ -379,7 +379,7 @@ export class EditContentForm extends Form<FormGroup> {
private addArrayItem(field: RootFieldDto, language: AppLanguageDto | null, partitionForm: FormArray) { private addArrayItem(field: RootFieldDto, language: AppLanguageDto | null, partitionForm: FormArray) {
const itemForm = new FormGroup({}); const itemForm = new FormGroup({});
let isOptional = field.isLocalizable && language !== null && language.isOptional; let isOptional = field.isLocalizable && !!language && language.isOptional;
for (let nested of field.nested) { for (let nested of field.nested) {
const nestedValidators = FieldValidatorsFactory.createValidators(nested, isOptional); const nestedValidators = FieldValidatorsFactory.createValidators(nested, isOptional);

2
src/Squidex/app/shared/state/contents.state.ts

@ -273,7 +273,7 @@ export abstract class ContentsStateBase extends State<Snapshot> {
} }
public init(): Observable<any> { public init(): Observable<any> {
this.next(s => ({ ...s, contentsPager: new Pager(0), contentsQuery: '', isArchive: false, isLoaded: false })); this.next(s => ({ contents: ImmutableArray.of(), contentsPager: new Pager(0) }));
return this.loadInternal(); return this.loadInternal();
} }

4
src/Squidex/app/shared/state/queries.spec.ts

@ -68,12 +68,16 @@ describe('Queries', () => {
it('should forward add call to state', () => { it('should forward add call to state', () => {
queries.add('key3', 'filter3'); queries.add('key3', 'filter3');
expect(true).toBeTruthy();
uiState.verify(x => x.set('schemas.my-schema.queries.key3', 'filter3'), Times.once()); uiState.verify(x => x.set('schemas.my-schema.queries.key3', 'filter3'), Times.once());
}); });
it('should forward remove call to state', () => { it('should forward remove call to state', () => {
queries.remove('key3'); queries.remove('key3');
expect(true).toBeTruthy();
uiState.verify(x => x.remove('schemas.my-schema.queries.key3'), Times.once()); uiState.verify(x => x.remove('schemas.my-schema.queries.key3'), Times.once());
}); });
}); });

25
tools/GenerateLanguages/GenerateLanguages.sln

@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.27703.2042
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GenerateLanguages", "GenerateLanguages.csproj", "{8421C72C-A305-4CDA-9413-715B4A095F56}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{8421C72C-A305-4CDA-9413-715B4A095F56}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8421C72C-A305-4CDA-9413-715B4A095F56}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8421C72C-A305-4CDA-9413-715B4A095F56}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8421C72C-A305-4CDA-9413-715B4A095F56}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {25A5F1D2-5109-48B1-B161-8F846145BEFB}
EndGlobalSection
EndGlobal
Loading…
Cancel
Save