Browse Source

UI options.

pull/383/head
Sebastian Stehle 7 years ago
parent
commit
53054e0971
  1. 16
      src/Squidex/Areas/Api/Controllers/UI/Models/UISettingsDto.cs
  2. 6
      src/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs
  3. 7
      src/Squidex/Areas/Api/Controllers/UI/UIController.cs
  4. 27
      src/Squidex/Areas/Frontend/Middlewares/IndexExtensions.cs
  5. 15
      src/Squidex/Areas/Frontend/Middlewares/IndexMiddleware.cs
  6. 16
      src/Squidex/Areas/Frontend/Middlewares/WebpackMiddleware.cs
  7. 10
      src/Squidex/app/app.module.ts
  8. 6
      src/Squidex/app/features/apps/pages/apps-page.component.ts
  9. 1
      src/Squidex/app/framework/angular/modals/onboarding-tooltip.component.ts
  10. 29
      src/Squidex/app/framework/configurations.ts
  11. 28
      src/Squidex/app/framework/services/onboarding.service.spec.ts
  12. 13
      src/Squidex/app/framework/services/onboarding.service.ts
  13. 38
      src/Squidex/app/shared/components/geolocation-editor.component.ts
  14. 28
      src/Squidex/app/shared/guards/must-be-authenticated.guard.spec.ts
  15. 13
      src/Squidex/app/shared/guards/must-be-authenticated.guard.ts
  16. 30
      src/Squidex/app/shared/guards/must-be-not-authenticated.guard.spec.ts
  17. 13
      src/Squidex/app/shared/guards/must-be-not-authenticated.guard.ts
  18. 2
      src/Squidex/app/shared/services/ui.service.spec.ts
  19. 3
      src/Squidex/app/shared/services/ui.service.ts
  20. 2
      src/Squidex/app/shared/services/workflows.service.spec.ts
  21. 31
      src/Squidex/appsettings.json

16
src/Squidex/Areas/Api/Controllers/UI/Models/UISettingsDto.cs

@ -3,26 +3,12 @@
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.ComponentModel.DataAnnotations;
// ==========================================================================using System.ComponentModel.DataAnnotations;
namespace Squidex.Areas.Api.Controllers.UI.Models
{
public sealed class UISettingsDto
{
/// <summary>
/// The type of the map control.
/// </summary>
[Required]
public string MapType { get; set; }
/// <summary>
/// The key for the map control.
/// </summary>
[Required]
public string MapKey { get; set; }
/// <summary>
/// True when the user can create apps.
/// </summary>

6
src/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs

@ -15,6 +15,12 @@ namespace Squidex.Areas.Api.Controllers.UI
public MapOptions Map { get; set; }
public bool HideNews { get; set; }
public bool HideOnboarding { get; set; }
public bool RedirectToLogin { get; set; }
public bool OnlyAdminsCanCreateApps { get; set; }
public sealed class MapOptions

7
src/Squidex/Areas/Api/Controllers/UI/UIController.cs

@ -48,14 +48,9 @@ namespace Squidex.Areas.Api.Controllers.UI
{
var result = new UISettingsDto
{
MapType = uiOptions.Map?.Type ?? "OSM",
MapKey = uiOptions.Map?.GoogleMaps?.Key
CanCreateApps = !uiOptions.OnlyAdminsCanCreateApps || Context.Permissions.Includes(CreateAppPermission)
};
var canCreateApps = !uiOptions.OnlyAdminsCanCreateApps || Context.Permissions.Includes(CreateAppPermission);
result.CanCreateApps = canCreateApps;
return Ok(result);
}

27
src/Squidex/Areas/Frontend/Middlewares/IndexExtensions.cs

@ -7,6 +7,11 @@
using System;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Squidex.Areas.Api.Controllers.UI;
using Squidex.Infrastructure.Json;
namespace Squidex.Areas.Frontend.Middlewares
{
@ -26,5 +31,27 @@ namespace Squidex.Areas.Frontend.Middlewares
{
return context.Response.ContentType?.ToLower().Contains("text/html") == true;
}
public static string AdjustHtml(this string html, HttpContext httpContext)
{
var result = html;
if (httpContext.Request.PathBase.HasValue)
{
result = result.Replace("<base href=\"/\">", $"<base href=\"{httpContext.Request.PathBase}/\">");
}
var uiOptions = httpContext.RequestServices.GetService<IOptions<MyUIOptions>>()?.Value;
if (uiOptions != null)
{
var jsonSerializer = httpContext.RequestServices.GetRequiredService<IJsonSerializer>();
var jsonOptions = jsonSerializer.Serialize(uiOptions, false);
result = result.Replace("<body>", $"<body><script>var options = {jsonOptions};</script>");
}
return result;
}
}
}

15
src/Squidex/Areas/Frontend/Middlewares/IndexMiddleware.cs

@ -25,7 +25,7 @@ namespace Squidex.Areas.Frontend.Middlewares
{
var basePath = context.Request.PathBase;
if (context.IsHtmlPath() && basePath.HasValue)
if (context.IsHtmlPath() && context.Response.StatusCode != 304)
{
var responseBuffer = new MemoryStream();
var responseBody = context.Response.Body;
@ -36,24 +36,19 @@ namespace Squidex.Areas.Frontend.Middlewares
context.Response.Body = responseBody;
var response = Encoding.UTF8.GetString(responseBuffer.ToArray());
var html = Encoding.UTF8.GetString(responseBuffer.ToArray());
response = AdjustBase(response, basePath);
html = html.AdjustHtml(context);
context.Response.ContentLength = Encoding.UTF8.GetByteCount(response);
context.Response.ContentLength = Encoding.UTF8.GetByteCount(html);
context.Response.Body = responseBody;
await context.Response.WriteAsync(response);
await context.Response.WriteAsync(html);
}
else
{
await next(context);
}
}
private static string AdjustBase(string response, string baseUrl)
{
return response.Replace("<base href=\"/\">", $"<base href=\"{baseUrl}/\">");
}
}
}

16
src/Squidex/Areas/Frontend/Middlewares/WebpackMiddleware.cs

@ -37,7 +37,7 @@ namespace Squidex.Areas.Frontend.Middlewares
{
var html = await result.Content.ReadAsStringAsync();
html = AdjustBase(html, context.Request.PathBase);
html = html.AdjustHtml(context);
await context.Response.WriteHtmlAsync(html);
}
@ -58,7 +58,7 @@ namespace Squidex.Areas.Frontend.Middlewares
var html = Encoding.UTF8.GetString(responseBuffer.ToArray());
html = AdjustBase(html, context.Request.PathBase);
html = html.AdjustHtml(context);
context.Response.ContentLength = Encoding.UTF8.GetByteCount(html);
context.Response.Body = responseBody;
@ -71,17 +71,5 @@ namespace Squidex.Areas.Frontend.Middlewares
await next(context);
}
}
private static string AdjustBase(string html, PathString baseUrl)
{
if (baseUrl.HasValue)
{
return html.Replace("<base href=\"/\">", $"<base href=\"{baseUrl}/\">");
}
else
{
return html;
}
}
}
}

10
src/Squidex/app/app.module.ts

@ -23,7 +23,8 @@ import {
DecimalSeparatorConfig,
SqxFrameworkModule,
SqxSharedModule,
TitlesConfig
TitlesConfig,
UIOptions
} from './shared';
import { SqxShellModule } from './shell';
@ -49,6 +50,10 @@ export function configApiUrl() {
}
}
export function configUIOptions() {
return new UIOptions(window['options']);
}
export function configTitles() {
return new TitlesConfig({}, undefined, 'Squidex Headless CMS');
}
@ -88,7 +93,8 @@ export function configCurrency() {
{ provide: ApiUrlConfig, useFactory: configApiUrl },
{ provide: CurrencyConfig, useFactory: configCurrency },
{ provide: DecimalSeparatorConfig, useFactory: configDecimalSeparator },
{ provide: TitlesConfig, useFactory: configTitles }
{ provide: TitlesConfig, useFactory: configTitles },
{ provide: UIOptions, useFactory: configUIOptions }
],
entryComponents: [AppComponent]
})

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

@ -17,6 +17,7 @@ import {
LocalStoreService,
NewsService,
OnboardingService,
UIOptions,
UIState
} from '@app/shared';
@ -40,7 +41,8 @@ export class AppsPageComponent implements OnInit {
public readonly uiState: UIState,
private readonly localStore: LocalStoreService,
private readonly newsService: NewsService,
private readonly onboardingService: OnboardingService
private readonly onboardingService: OnboardingService,
private readonly uiOptions: UIOptions
) {
}
@ -52,7 +54,7 @@ export class AppsPageComponent implements OnInit {
if (shouldShowOnboarding && apps.length === 0) {
this.onboardingService.disable('dialog');
this.onboardingDialog.show();
} else {
} else if (!this.uiOptions.get('hideNews')) {
const newsVersion = this.localStore.getInt('squidex.news.version');
this.newsService.getFeatures(newsVersion)

1
src/Squidex/app/framework/angular/modals/onboarding-tooltip.component.ts

@ -45,6 +45,7 @@ export class OnboardingTooltipComponent extends StatefulComponent implements OnD
private readonly renderer: Renderer2
) {
super(changeDetector, {});
}
public ngOnDestroy() {

29
src/Squidex/app/framework/configurations.ts

@ -5,6 +5,35 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
export class UIOptions {
constructor(
private readonly value: any
) {
}
public get(path: string) {
if (!path) {
return undefined;
}
let value = this.value;
if (value) {
const parts = path.split('.');
for (let part of parts) {
value = value[part];
if (!value) {
break;
}
}
}
return value;
}
}
export class ApiUrlConfig {
public readonly value: string;

28
src/Squidex/app/framework/services/onboarding.service.spec.ts

@ -7,6 +7,8 @@
import { OnboardingService, OnboardingServiceFactory } from './onboarding.service';
import { UIOptions } from './../configurations';
class LocalStoreMock {
private store = {};
@ -27,46 +29,52 @@ describe('OnboardingService', () => {
});
it('should instantiate from factory', () => {
const onboardingService = OnboardingServiceFactory(<any>localStore);
const onboardingService = OnboardingServiceFactory(new UIOptions({}), <any>localStore);
expect(onboardingService).toBeDefined();
});
it('should instantiate', () => {
const onboardingService = new OnboardingService(<any>localStore);
const onboardingService = new OnboardingService(new UIOptions({}), <any>localStore);
expect(onboardingService).toBeDefined();
});
it('should return true when value not in store', () => {
it('should show when value not in store', () => {
localStore.set('squidex.onboarding.disable.feature-a1', '0');
const onboardingService = new OnboardingService(<any>localStore);
const onboardingService = new OnboardingService(new UIOptions({}), <any>localStore);
expect(onboardingService.shouldShow('feature-a2')).toBeTruthy();
});
it('should return false when value in store', () => {
it('should not show when value in store', () => {
localStore.set('squidex.onboarding.disable.feature-b1', '1');
const onboardingService = new OnboardingService(<any>localStore);
const onboardingService = new OnboardingService(new UIOptions({}), <any>localStore);
expect(onboardingService.shouldShow('feature-b1')).toBeFalsy();
});
it('should return false when disabled', () => {
const onboardingService = new OnboardingService(<any>localStore);
it('should not show when disabled', () => {
const onboardingService = new OnboardingService(new UIOptions({}), <any>localStore);
onboardingService.disable('feature-c1');
expect(onboardingService.shouldShow('feature-c1')).toBeFalsy();
});
it('should return false when all disabled', () => {
const onboardingService = new OnboardingService(<any>localStore);
it('should not show when all disabled', () => {
const onboardingService = new OnboardingService(new UIOptions({}), <any>localStore);
onboardingService.disableAll();
expect(onboardingService.shouldShow('feature-d1')).toBeFalsy();
});
it('should not show when disabled by setting', () => {
const onboardingService = new OnboardingService(new UIOptions({ hideOnboarding: true }), <any>localStore);
expect(onboardingService.shouldShow('feature-d1')).toBeFalsy();
});
});

13
src/Squidex/app/framework/services/onboarding.service.ts

@ -12,15 +12,20 @@ import { Injectable } from '@angular/core';
import { LocalStoreService } from './local-store.service';
export const OnboardingServiceFactory = (localStore: LocalStoreService) => {
return new OnboardingService(localStore);
import { UIOptions } from './../configurations';
export const OnboardingServiceFactory = (uiOptions: UIOptions, localStore: LocalStoreService) => {
return new OnboardingService(uiOptions, localStore);
};
@Injectable()
export class OnboardingService {
constructor(
private readonly disabled: boolean;
constructor(uiOptions: UIOptions,
private readonly localStore: LocalStoreService
) {
this.disabled = uiOptions.get('hideOnboardingTooltips');
}
public disableAll() {
@ -32,7 +37,7 @@ export class OnboardingService {
}
public shouldShow(key: string) {
return this.shouldShowKey(key) && this.shouldShowKey('all');
return !this.disabled && this.shouldShowKey(key) && this.shouldShowKey('all');
}
private shouldShowKey(key: string) {

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

@ -12,7 +12,7 @@ import {
ResourceLoaderService,
StatefulControlComponent,
Types,
UIState,
UIOptions,
ValidatorsEx
} from '@app/shared/internal';
@ -28,10 +28,6 @@ interface Geolocation {
longitude: number;
}
interface State {
isGoogleMaps: boolean;
}
@Component({
selector: 'sqx-geolocation-editor',
styleUrls: ['./geolocation-editor.component.scss'],
@ -39,7 +35,8 @@ interface State {
providers: [SQX_GEOLOCATION_EDITOR_CONTROL_VALUE_ACCESSOR],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class GeolocationEditorComponent extends StatefulControlComponent<State, Geolocation> implements AfterViewInit {
export class GeolocationEditorComponent extends StatefulControlComponent<any, Geolocation> implements AfterViewInit {
private readonly isGoogleMaps: boolean;
private marker: any;
private map: any;
private value: Geolocation | null = null;
@ -73,11 +70,11 @@ export class GeolocationEditorComponent extends StatefulControlComponent<State,
constructor(changeDetector: ChangeDetectorRef,
private readonly resourceLoader: ResourceLoaderService,
private readonly formBuilder: FormBuilder,
private readonly uiState: UIState
private readonly uiOptions: UIOptions
) {
super(changeDetector, {
isGoogleMaps: false
});
super(changeDetector, {});
this.isGoogleMaps = uiOptions.get('map.type');
}
public writeValue(obj: any) {
@ -154,18 +151,11 @@ export class GeolocationEditorComponent extends StatefulControlComponent<State,
}
public ngAfterViewInit() {
this.uiState.settings
.subscribe(settings => {
const isGoogleMaps = settings.mapType === 'GoogleMaps';
this.next(s => ({ ...s, isGoogleMaps }));
if (!this.snapshot.isGoogleMaps) {
this.ngAfterViewInitOSM();
} else {
this.ngAfterViewInitGoogle(settings.mapKey);
}
});
if (!this.isGoogleMaps) {
this.ngAfterViewInitOSM();
} else {
this.ngAfterViewInitGoogle(this.uiOptions.get('map.googleMaps.key'));
}
}
private ngAfterViewInitOSM() {
@ -224,11 +214,11 @@ export class GeolocationEditorComponent extends StatefulControlComponent<State,
}
});
this.map.addListener('bounds_changed', (event: any) => {
this.map.addListener('bounds_changed', () => {
searchBox.setBounds(this.map.getBounds());
});
searchBox.addListener('places_changed', (event: any) => {
searchBox.addListener('places_changed', () => {
let places = searchBox.getPlaces();
if (places.length === 1) {

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

@ -9,24 +9,25 @@ import { Router } from '@angular/router';
import { of } from 'rxjs';
import { IMock, It, Mock, Times } from 'typemoq';
import { AuthService } from '@app/shared';
import { AuthService, UIOptions } from '@app/shared';
import { MustBeAuthenticatedGuard } from './must-be-authenticated.guard';
describe('MustBeAuthenticatedGuard', () => {
let router: IMock<Router>;
let authService: IMock<AuthService>;
let authGuard: MustBeAuthenticatedGuard;
let uiOptions = new UIOptions({ map: { type: 'OSM' } });
let uiOptionsRedirect = new UIOptions({ map: { type: 'OSM' }, redirectToLogin: true });
beforeEach(() => {
router = Mock.ofType<Router>();
authService = Mock.ofType<AuthService>();
authGuard = new MustBeAuthenticatedGuard(authService.object, router.object);
});
it('should navigate to default page if not authenticated', () => {
const authGuard = new MustBeAuthenticatedGuard(uiOptions, authService.object, router.object);
authService.setup(x => x.userChanges)
.returns(() => of(null));
@ -42,6 +43,8 @@ describe('MustBeAuthenticatedGuard', () => {
});
it('should return true if authenticated', () => {
const authGuard = new MustBeAuthenticatedGuard(uiOptions, authService.object, router.object);
authService.setup(x => x.userChanges)
.returns(() => of(<any>{}));
@ -55,4 +58,21 @@ describe('MustBeAuthenticatedGuard', () => {
router.verify(x => x.navigate(It.isAny()), Times.never());
});
it('should login redirect if redirect enabled', () => {
const authGuard = new MustBeAuthenticatedGuard(uiOptionsRedirect, authService.object, router.object);
authService.setup(x => x.userChanges)
.returns(() => of(null));
let result: boolean;
authGuard.canActivate().subscribe(x => {
result = x;
});
expect(result!).toBeFalsy();
authService.verify(x => x.loginRedirect(), Times.once());
});
});

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

@ -10,14 +10,19 @@ import { CanActivate, Router } from '@angular/router';
import { Observable } from 'rxjs';
import { map, take, tap } from 'rxjs/operators';
import { UIOptions } from '@app/framework';
import { AuthService } from './../services/auth.service';
@Injectable()
export class MustBeAuthenticatedGuard implements CanActivate {
constructor(
private readonly redirect: boolean;
constructor(uiOptions: UIOptions,
private readonly authService: AuthService,
private readonly router: Router
) {
this.redirect = uiOptions.get('redirectToLogin');
}
public canActivate(): Observable<boolean> {
@ -25,7 +30,11 @@ export class MustBeAuthenticatedGuard implements CanActivate {
take(1),
tap(user => {
if (!user) {
this.router.navigate(['']);
if (this.redirect) {
this.authService.loginRedirect();
} else {
this.router.navigate(['']);
}
}
}),
map(user => !!user));

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

@ -9,24 +9,25 @@ import { Router } from '@angular/router';
import { of } from 'rxjs';
import { IMock, It, Mock, Times } from 'typemoq';
import { AuthService } from '@app/shared';
import { AuthService, UIOptions } from '@app/shared';
import { MustBeNotAuthenticatedGuard } from './must-be-not-authenticated.guard';
describe('MustNotBeAuthenticatedGuard', () => {
describe('MustBeNotAuthenticatedGuard', () => {
let router: IMock<Router>;
let authService: IMock<AuthService>;
let authGuard: MustBeNotAuthenticatedGuard;
let uiOptions = new UIOptions({ map: { type: 'OSM' } });
let uiOptionsRedirect = new UIOptions({ map: { type: 'OSM' }, redirectToLogin: true });
beforeEach(() => {
router = Mock.ofType<Router>();
authService = Mock.ofType<AuthService>();
authGuard = new MustBeNotAuthenticatedGuard(authService.object, router.object);
});
it('should navigate to app page if authenticated', () => {
const authGuard = new MustBeNotAuthenticatedGuard(uiOptions, authService.object, router.object);
authService.setup(x => x.userChanges)
.returns(() => of(<any>{}));
@ -42,6 +43,8 @@ describe('MustNotBeAuthenticatedGuard', () => {
});
it('should return true if not authenticated', () => {
const authGuard = new MustBeNotAuthenticatedGuard(uiOptions, authService.object, router.object);
authService.setup(x => x.userChanges)
.returns(() => of(null));
@ -55,4 +58,21 @@ describe('MustNotBeAuthenticatedGuard', () => {
router.verify(x => x.navigate(It.isAny()), Times.never());
});
it('should login redirect and return false if redirect enabled', () => {
const authGuard = new MustBeNotAuthenticatedGuard(uiOptionsRedirect, authService.object, router.object);
authService.setup(x => x.userChanges)
.returns(() => of(null));
let result: boolean;
authGuard.canActivate().subscribe(x => {
result = x;
});
expect(result!).toBeFalsy();
authService.verify(x => x.loginRedirect(), Times.once());
});
});

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

@ -10,24 +10,31 @@ import { CanActivate, Router } from '@angular/router';
import { Observable } from 'rxjs';
import { map, take, tap } from 'rxjs/operators';
import { UIOptions } from '@app/framework';
import { AuthService } from './../services/auth.service';
@Injectable()
export class MustBeNotAuthenticatedGuard implements CanActivate {
constructor(
private readonly redirect: boolean;
constructor(uiOptions: UIOptions,
private readonly authService: AuthService,
private readonly router: Router
) {
this.redirect = uiOptions.get('redirectToLogin');
}
public canActivate(): Observable<boolean> {
return this.authService.userChanges.pipe(
take(1),
tap(user => {
if (user) {
if (this.redirect) {
this.authService.loginRedirect();
} else if (user) {
this.router.navigate(['app']);
}
}),
map(user => !user));
map(user => !user && !this.redirect));
}
}

2
src/Squidex/app/shared/services/ui.service.spec.ts

@ -40,7 +40,7 @@ describe('UIService', () => {
settings = result;
});
const response: UISettingsDto = { mapType: 'OSM', mapKey: '', canCreateApps: true };
const response: UISettingsDto = { canCreateApps: true };
const req = httpMock.expectOne('http://service/p/api/ui/settings');

3
src/Squidex/app/shared/services/ui.service.ts

@ -13,9 +13,6 @@ import { catchError } from 'rxjs/operators';
import { ApiUrlConfig } from '@app/framework';
export interface UISettingsDto {
readonly mapType: string;
readonly mapKey?: string;
readonly canCreateApps: boolean;
}

2
src/Squidex/app/shared/services/workflows.service.spec.ts

@ -147,7 +147,7 @@ describe('Workflow', () => {
it('should create empty workflow', () => {
const workflow = new WorkflowDto();
expect(workflow.initial);
expect(workflow.initial).not.toBeDefined();
});
it('should add step to workflow', () => {

31
src/Squidex/appsettings.json

@ -65,7 +65,12 @@
*/
"key": "AIzaSyB_Z8l3nwUxZhMJykiDUJy6bSHXXlwcYMg"
}
}
},
/*
* Redirect to login automatically.
*/
"redirectTopLogin": false
},
"email": {
@ -73,29 +78,29 @@
/*
* The host name to your email server.
*/
"server": "",
/*
"server": "",
/*
* The sender email address.
*/
"sender": "hello@squidex.io",
/*
"sender": "hello@squidex.io",
/*
* The username to authenticate to your email server.
*/
"username": "",
/*
"username": "",
/*
* The password to authenticate to your email server.
*/
"password": "",
/*
"password": "",
/*
* Always use SSL if possible.
*/
"enableSsl": true,
/*
"enableSsl": true,
/*
* The port to your email server.
*/
"port": 465
},
"port": 465
},
"notifications": {
/*
* The email subject when a new user is added as contributor.

Loading…
Cancel
Save