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']);
}
}),
map(u => u !== null));
map(u => !!u));
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.timerSubscription =
timer(2000, 2000).pipe(
switchMap(x => this.eventConsumersState.load(true, true)), onErrorResumeNext())
timer(2000, 2000).pipe(switchMap(x => this.eventConsumersState.load(true, true)), onErrorResumeNext())
.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 { FormBuilder } from '@angular/forms';
import { Subscription, timer } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { filter, switchMap } from 'rxjs/operators';
import {
AuthService,
@ -43,11 +43,9 @@ export class RestorePageComponent implements OnDestroy, OnInit {
public ngOnInit() {
this.timerSubscription =
timer(0, 2000).pipe(switchMap(() => this.backupsService.getRestore()))
timer(0, 2000).pipe(switchMap(() => this.backupsService.getRestore()), filter(x => !!x))
.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,
LoadLanguagesGuard,
SchemaMustExistPublishedGuard,
SchemaMustNotBeSingletonGuard,
SqxFrameworkModule,
SqxSharedModule,
UnsetContentGuard
@ -52,12 +53,13 @@ const routes: Routes = [
{
path: '',
component: ContentsPageComponent,
canActivate: [SchemaMustNotBeSingletonGuard],
canDeactivate: [CanDeactivateGuard]
},
{
path: 'new',
component: ContentPageComponent,
canActivate: [UnsetContentGuard],
canActivate: [SchemaMustNotBeSingletonGuard, UnsetContentGuard],
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 { ActivatedRoute, Router } from '@angular/router';
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';
@ -88,19 +88,19 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy,
});
this.selectedSchemaSubscription =
this.schemasState.selectedSchema.pipe(filter(s => !!s), map(s => s!))
this.schemasState.selectedSchema.pipe(filter(s => !!s))
.subscribe(schema => {
this.schema = schema;
this.schema = schema!;
this.contentForm = new EditContentForm(this.schema, this.languages);
});
this.contentSubscription =
this.contentsState.selectedContent.pipe(filter(c => !!c), map(c => c!))
this.contentsState.selectedContent.pipe(filter(c => !!c))
.subscribe(content => {
this.content = content;
this.content = content!;
this.loadContent(content.dataDraft);
this.loadContent(this.content.dataDraft);
});
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 { Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { onErrorResumeNext, switchMap, tap } from 'rxjs/operators';
import { filter, onErrorResumeNext, switchMap, takeUntil, tap } from 'rxjs/operators';
import {
AppLanguageDto,
@ -17,6 +18,7 @@ import {
ImmutableArray,
LanguagesState,
ModalModel,
navigatedToOtherComponent,
Queries,
SchemaDetailsDto,
SchemasState,
@ -59,6 +61,7 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
public readonly contentsState: ContentsState,
private readonly languagesState: LanguagesState,
private readonly schemasState: SchemasState,
private readonly router: Router,
private readonly uiState: UIState
) {
}
@ -70,8 +73,10 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
}
public ngOnInit() {
const routeChanged = this.router.events.pipe(filter(navigatedToOtherComponent(this.router)));
this.selectedSchemaSubscription =
this.schemasState.selectedSchema
this.schemasState.selectedSchema.pipe(takeUntil(routeChanged))
.subscribe(schema => {
this.resetSelection();
@ -82,7 +87,7 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
});
this.contentsSubscription =
this.contentsState.contents
this.contentsState.contents.pipe(takeUntil(routeChanged))
.subscribe(() => {
this.updateSelectionSummary();
});
@ -100,7 +105,7 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
}
public deleteSelected() {
this.contentsState.deleteMany(this.select()).pipe(onErrorResumeNext()).subscribe();
this.contentsState.deleteMany(this.selectItems()).pipe(onErrorResumeNext()).subscribe();
}
public delete(content: ContentDto) {
@ -112,7 +117,7 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
}
public publishSelected() {
this.changeContentItems(this.select(c => c.status !== 'Published'), 'Publish');
this.changeContentItems(this.selectItems(c => c.status !== 'Published'), 'Publish');
}
public unpublish(content: ContentDto) {
@ -120,7 +125,7 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
}
public unpublishSelected() {
this.changeContentItems(this.select(c => c.status === 'Published'), 'Unpublish');
this.changeContentItems(this.selectItems(c => c.status === 'Published'), 'Unpublish');
}
public archive(content: ContentDto) {
@ -128,15 +133,15 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
}
public archiveSelected() {
this.changeContentItems(this.select(), 'Archive');
this.changeContentItems(this.selectItems(), 'Archive');
}
public restore(content: ContentDto) {
this.changeContentItems([content], 'Restore');
}
public restoreSelected(scheduled: boolean) {
this.changeContentItems(this.select(), 'Restore');
public restoreSelected() {
this.changeContentItems(this.selectItems(), 'Restore');
}
public clone(content: ContentDto) {
@ -185,12 +190,16 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
this.contentsState.search(query).pipe(onErrorResumeNext()).subscribe();
}
public selectLanguage(language: AppLanguageDto) {
this.language = language;
}
public isItemSelected(content: ContentDto): boolean {
return !!this.selectedItems[content.id];
}
public selectLanguage(language: AppLanguageDto) {
this.language = language;
private selectItems(predicate?: (content: ContentDto) => boolean) {
return this.contentsState.snapshot.contents.values.filter(c => this.selectedItems[c.id] && (!predicate || predicate(c)));
}
public selectItem(content: ContentDto, isSelected: boolean) {
@ -215,10 +224,6 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
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() {
this.selectedItems = {};

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

@ -21,6 +21,7 @@
[name]="category"
[schemas]="schemas"
[schemasFilter]="schemasFilter.valueChanges | async"
[routeSingletonToContent]="true"
[isReadonly]="true">
</sqx-schema-category>
</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 {
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.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 { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { filter, map, onErrorResumeNext } from 'rxjs/operators';
import { filter, onErrorResumeNext } from 'rxjs/operators';
import {
AppsState,
@ -73,9 +73,9 @@ export class SchemaPageComponent implements OnDestroy, OnInit {
this.patternsState.load().pipe(onErrorResumeNext()).subscribe();
this.selectedSchemaSubscription =
this.schemasState.selectedSchema.pipe(filter(s => !!s), map(s => s!))
this.schemasState.selectedSchema.pipe(filter(s => !!s))
.subscribe(schema => {
this.schema = schema;
this.schema = schema!;
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() {
return this.dateValue !== null;
return !!this.dateValue;
}
@ViewChild('dateInput')

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

@ -88,7 +88,7 @@ export class StarsComponent implements ControlValueAccessor {
return false;
}
if (this.value !== null) {
if (this.value) {
this.value = null;
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) {
let source = `${this.imageSource}&width=${w}&height=${h}&mode=Crop`;
if (this.loadQuery !== null) {
if (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) {
if (isOpen === (this.renderedView !== null)) {
if (isOpen === (!!this.renderedView)) {
return;
}

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

@ -5,7 +5,9 @@
* 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 {
let snapshot: ActivatedRouteSnapshot | null = value['snapshot'] || value;
@ -41,3 +43,21 @@ export function allParams(value: ActivatedRouteSnapshot | ActivatedRoute): Param
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() {
let updateMap = this.geolocationForm.controls['latitude'].value !== null &&
this.geolocationForm.controls['longitude'].value !== null;
let updateMap =
!!this.geolocationForm.controls['latitude'].value &&
!!this.geolocationForm.controls['longitude'].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>
<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="col col-4">
<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()
public isReadonly: boolean;
@Input()
public routeSingletonToContent = false;
@Input()
public schemasFilter: string;
@ -100,6 +103,10 @@ export class SchemaCategoryComponent implements OnInit, OnChanges {
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) {
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 { of } from 'rxjs';
import { IMock, Mock, Times } from 'typemoq';
import { IMock, It, Mock, Times } from 'typemoq';
import { ContentDto } from './../services/contents.service';
import { ContentsState } from './../state/contents.state';
@ -42,7 +42,7 @@ describe('ContentMustExistGuard', () => {
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', () => {

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

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

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

@ -7,7 +7,7 @@
import { Router } from '@angular/router';
import { of } from 'rxjs';
import { IMock, Mock, Times } from 'typemoq';
import { IMock, It, Mock, Times } from 'typemoq';
import { AuthService } from '@app/shared';
@ -52,5 +52,7 @@ describe('MustBeAuthenticatedGuard', () => {
});
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 { of } from 'rxjs';
import { IMock, Mock, Times } from 'typemoq';
import { IMock, It, Mock, Times } from 'typemoq';
import { AuthService } from '@app/shared';
@ -52,5 +52,7 @@ describe('MustNotBeAuthenticatedGuard', () => {
});
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.
*/
import { Router, RouterStateSnapshot } from '@angular/router';
import { Router } from '@angular/router';
import { of } from 'rxjs';
import { IMock, Mock, Times } from 'typemoq';
import { IMock, It, Mock, Times } from 'typemoq';
import { SchemaDetailsDto } from './../services/schemas.service';
import { SchemasState } from './../state/schemas.state';
@ -21,7 +21,6 @@ describe('SchemaMustExistPublishedGuard', () => {
};
let schemasState: IMock<SchemasState>;
let state: RouterStateSnapshot = <any>{ url: 'current-url' };
let router: IMock<Router>;
let schemaGuard: SchemaMustExistPublishedGuard;
@ -37,13 +36,13 @@ describe('SchemaMustExistPublishedGuard', () => {
let result: boolean;
schemaGuard.canActivate(route, state).subscribe(x => {
schemaGuard.canActivate(route).subscribe(x => {
result = x;
}).unsubscribe();
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', () => {
@ -52,7 +51,7 @@ describe('SchemaMustExistPublishedGuard', () => {
let result: boolean;
schemaGuard.canActivate(route, state).subscribe(x => {
schemaGuard.canActivate(route).subscribe(x => {
result = x;
}).unsubscribe();
@ -60,34 +59,4 @@ describe('SchemaMustExistPublishedGuard', () => {
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 { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
import { ActivatedRouteSnapshot, CanActivate, Router } from '@angular/router';
import { Observable } from 'rxjs';
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 result =
@ -31,12 +31,8 @@ export class SchemaMustExistPublishedGuard implements CanActivate {
if (!dto || !dto.isPublished) {
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;
}

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

@ -41,8 +41,6 @@ describe('SchemaMustExistGuard', () => {
}).unsubscribe();
expect(result!).toBeTruthy();
schemasState.verify(x => x.select('123'), Times.once());
});
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']);
}
}),
map(s => s !== null));
map(s => !!s));
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/schema-must-exist-published.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-content.guard';

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

@ -64,6 +64,7 @@ import {
SchemaCategoryComponent,
SchemaMustExistGuard,
SchemaMustExistPublishedGuard,
SchemaMustNotBeSingletonGuard,
SchemasService,
SchemasState,
SearchFormComponent,
@ -180,6 +181,7 @@ export class SqxSharedModule {
RulesState,
SchemaMustExistGuard,
SchemaMustExistPublishedGuard,
SchemaMustNotBeSingletonGuard,
SchemasService,
SchemasState,
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) {
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) {
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> {
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();
}

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

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