Browse Source

Scopes improved and UI for pattern suggestions.

pull/103/head
Sebastian Stehle 8 years ago
parent
commit
4b0693cac4
  1. 10
      src/Squidex.Shared/Identity/SquidexRoles.cs
  2. 2
      src/Squidex/Config/Constants.cs
  3. 2
      src/Squidex/Config/MyUIOptions.cs
  4. 49
      src/Squidex/Config/Swagger/ScopesProcessor.cs
  5. 5
      src/Squidex/Config/Swagger/SwaggerServices.cs
  6. 6
      src/Squidex/Controllers/Api/UI/UIController.cs
  7. 56
      src/Squidex/Controllers/ContentApi/Generator/SchemaSwaggerGenerator.cs
  8. 26
      src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs
  9. 7
      src/Squidex/Pipeline/Swagger/SwaggerHelper.cs
  10. 2
      src/Squidex/Startup.cs
  11. 15
      src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.html
  12. 27
      src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.scss
  13. 45
      src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.ts
  14. 1
      src/Squidex/app/shared/declarations-base.ts
  15. 2
      src/Squidex/app/shared/module.ts
  16. 79
      src/Squidex/app/shared/services/ui.service.spec.ts
  17. 49
      src/Squidex/app/shared/services/ui.service.ts
  18. 7
      src/Squidex/appsettings.json
  19. 4
      tests/Squidex.Domain.Apps.Read.Tests/Contents/GraphQLTests.cs

10
src/Squidex.Shared/Identity/SquidexRoles.cs

@ -10,14 +10,14 @@ namespace Squidex.Shared.Identity
{ {
public static class SquidexRoles public static class SquidexRoles
{ {
public static readonly string Administrator = "ADMINISTRATOR"; public static readonly string Administrator = "administrator";
public static readonly string AppOwner = "APP-OWNER"; public static readonly string AppOwner = "app:owner";
public static readonly string AppEditor = "APP-EDITOR"; public static readonly string AppEditor = "app:editor";
public static readonly string AppReader = "APP-READER"; public static readonly string AppReader = "app:reader";
public static readonly string AppDeveloper = "APP-DEVELOPER"; public static readonly string AppDeveloper = "app:dev";
} }
} }

2
src/Squidex/Config/Constants.cs

@ -10,7 +10,7 @@ namespace Squidex.Config
{ {
public static class Constants public static class Constants
{ {
public static readonly string SecurityDefinition = "oauth-client-auth"; public static readonly string SecurityDefinition = "squidex-oauth-auth";
public static readonly string ApiPrefix = "/api"; public static readonly string ApiPrefix = "/api";

2
src/Squidex/Config/MyUIOptions.cs

@ -8,6 +8,8 @@
using System.Collections.Generic; using System.Collections.Generic;
// ReSharper disable CollectionNeverUpdated.Global
namespace Squidex.Config namespace Squidex.Config
{ {
public sealed class MyUIOptions public sealed class MyUIOptions

49
src/Squidex/Config/Swagger/ScopesProcessor.cs

@ -0,0 +1,49 @@
// ==========================================================================
// ScopesProcessor.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using NSwag;
using NSwag.SwaggerGeneration.Processors;
using NSwag.SwaggerGeneration.Processors.Contexts;
using Squidex.Infrastructure.Tasks;
// ReSharper disable InvertIf
namespace Squidex.Config.Swagger
{
public class ScopesProcessor : IOperationProcessor
{
public Task<bool> ProcessAsync(OperationProcessorContext context)
{
if (context.OperationDescription.Operation.Security == null)
{
context.OperationDescription.Operation.Security = new List<SwaggerSecurityRequirement>();
}
var authorizeAttributes =
context.MethodInfo.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Union(
context.MethodInfo.DeclaringType.GetTypeInfo().GetCustomAttributes(true).OfType<AuthorizeAttribute>()).ToArray();
if (authorizeAttributes.Any())
{
var scopes = authorizeAttributes.Where(a => a.Roles != null).SelectMany(a => a.Roles.Split(',')).Distinct().ToList();
context.OperationDescription.Operation.Security.Add(new SwaggerSecurityRequirement
{
{ Constants.SecurityDefinition, scopes }
});
}
return TaskHelper.True;
}
}
}

5
src/Squidex/Config/Swagger/SwaggerServices.cs

@ -42,10 +42,9 @@ namespace Squidex.Config.Swagger
private static SwaggerSettings ConfigureIdentity(this SwaggerSettings settings, MyUrlsOptions urlOptions) private static SwaggerSettings ConfigureIdentity(this SwaggerSettings settings, MyUrlsOptions urlOptions)
{ {
settings.DocumentProcessors.Add( settings.DocumentProcessors.Add(new SecurityDefinitionAppender(Constants.SecurityDefinition, SwaggerHelper.CreateOAuthSchema(urlOptions)));
new SecurityDefinitionAppender(Constants.SecurityDefinition, SwaggerHelper.CreateOAuthSchema(urlOptions)));
settings.OperationProcessors.Add(new OperationSecurityScopeProcessor(Constants.SecurityDefinition)); settings.OperationProcessors.Add(new ScopesProcessor());
return settings; return settings;
} }

6
src/Squidex/Controllers/Api/UI/UIController.cs

@ -43,11 +43,11 @@ namespace Squidex.Controllers.Api.UI
var dto = new UISettingsDto var dto = new UISettingsDto
{ {
RegexSuggestions = RegexSuggestions =
uiOptions?.RegexSuggestions.Where(x => uiOptions.RegexSuggestions?
.Where(x =>
!string.IsNullOrWhiteSpace(x.Key) && !string.IsNullOrWhiteSpace(x.Key) &&
!string.IsNullOrWhiteSpace(x.Value)) !string.IsNullOrWhiteSpace(x.Value))
.Select(x => new UIRegexSuggestionDto { Name = x.Key, Pattern = x.Value }) .Select(x => new UIRegexSuggestionDto { Name = x.Key, Pattern = x.Value }).ToList()
.ToList()
?? new List<UIRegexSuggestionDto>() ?? new List<UIRegexSuggestionDto>()
}; };

56
src/Squidex/Controllers/ContentApi/Generator/SchemaSwaggerGenerator.cs

@ -11,11 +11,13 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using NJsonSchema; using NJsonSchema;
using NSwag; using NSwag;
using Squidex.Config;
using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Read.Contents.JsonSchema; using Squidex.Domain.Apps.Read.Contents.JsonSchema;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Pipeline.Swagger; using Squidex.Pipeline.Swagger;
using Squidex.Shared.Identity;
// ReSharper disable InvertIf // ReSharper disable InvertIf
@ -23,8 +25,10 @@ namespace Squidex.Controllers.ContentApi.Generator
{ {
public sealed class SchemaSwaggerGenerator public sealed class SchemaSwaggerGenerator
{ {
private static readonly string schemaQueryDescription; private static readonly string SchemaQueryDescription;
private static readonly string schemaBodyDescription; private static readonly string SchemaBodyDescription;
private static readonly List<SwaggerSecurityRequirement> EditorSecurity;
private static readonly List<SwaggerSecurityRequirement> ReaderSecurity;
private readonly ContentSchemaBuilder schemaBuilder = new ContentSchemaBuilder(); private readonly ContentSchemaBuilder schemaBuilder = new ContentSchemaBuilder();
private readonly SwaggerDocument document; private readonly SwaggerDocument document;
private readonly JsonSchema4 contentSchema; private readonly JsonSchema4 contentSchema;
@ -36,8 +40,28 @@ namespace Squidex.Controllers.ContentApi.Generator
static SchemaSwaggerGenerator() static SchemaSwaggerGenerator()
{ {
schemaBodyDescription = SwaggerHelper.LoadDocs("schemabody"); SchemaBodyDescription = SwaggerHelper.LoadDocs("schemabody");
schemaQueryDescription = SwaggerHelper.LoadDocs("schemaquery"); SchemaQueryDescription = SwaggerHelper.LoadDocs("schemaquery");
ReaderSecurity = new List<SwaggerSecurityRequirement>
{
new SwaggerSecurityRequirement
{
{
Constants.SecurityDefinition, new[] { SquidexRoles.AppReader }
}
}
};
EditorSecurity = new List<SwaggerSecurityRequirement>
{
new SwaggerSecurityRequirement
{
{
Constants.SecurityDefinition, new[] { SquidexRoles.AppEditor }
}
}
};
} }
public SchemaSwaggerGenerator(SwaggerDocument document, string path, Schema schema, public SchemaSwaggerGenerator(SwaggerDocument document, string path, Schema schema,
@ -89,7 +113,7 @@ namespace Squidex.Controllers.ContentApi.Generator
operation.OperationId = $"Query{schemaKey}Contents"; operation.OperationId = $"Query{schemaKey}Contents";
operation.Summary = $"Queries {schemaName} contents."; operation.Summary = $"Queries {schemaName} contents.";
operation.Description = schemaQueryDescription; operation.Description = SchemaQueryDescription;
operation.AddQueryParameter("$top", JsonObjectType.Number, "Optional number of contents to take."); operation.AddQueryParameter("$top", JsonObjectType.Number, "Optional number of contents to take.");
operation.AddQueryParameter("$skip", JsonObjectType.Number, "Optional number of contents to skip."); operation.AddQueryParameter("$skip", JsonObjectType.Number, "Optional number of contents to skip.");
@ -98,6 +122,8 @@ namespace Squidex.Controllers.ContentApi.Generator
operation.AddQueryParameter("orderby", JsonObjectType.String, "Optional OData order definition."); operation.AddQueryParameter("orderby", JsonObjectType.String, "Optional OData order definition.");
operation.AddResponse("200", $"{schemaName} content retrieved.", CreateContentsSchema(schemaName, contentSchema)); operation.AddResponse("200", $"{schemaName} content retrieved.", CreateContentsSchema(schemaName, contentSchema));
operation.Security = ReaderSecurity;
}); });
} }
@ -109,6 +135,8 @@ namespace Squidex.Controllers.ContentApi.Generator
operation.Summary = $"Get a {schemaName} content."; operation.Summary = $"Get a {schemaName} content.";
operation.AddResponse("200", $"{schemaName} content found.", contentSchema); operation.AddResponse("200", $"{schemaName} content found.", contentSchema);
operation.Security = ReaderSecurity;
}); });
} }
@ -119,10 +147,12 @@ namespace Squidex.Controllers.ContentApi.Generator
operation.OperationId = $"Create{schemaKey}Content"; operation.OperationId = $"Create{schemaKey}Content";
operation.Summary = $"Create a {schemaName} content."; operation.Summary = $"Create a {schemaName} content.";
operation.AddBodyParameter("data", dataSchema, schemaBodyDescription); operation.AddBodyParameter("data", dataSchema, SchemaBodyDescription);
operation.AddQueryParameter("publish", JsonObjectType.Boolean, "Set to true to autopublish content."); operation.AddQueryParameter("publish", JsonObjectType.Boolean, "Set to true to autopublish content.");
operation.AddResponse("201", $"{schemaName} created.", contentSchema); operation.AddResponse("201", $"{schemaName} created.", contentSchema);
operation.Security = EditorSecurity;
}); });
} }
@ -133,9 +163,11 @@ namespace Squidex.Controllers.ContentApi.Generator
operation.OperationId = $"Update{schemaKey}Content"; operation.OperationId = $"Update{schemaKey}Content";
operation.Summary = $"Update a {schemaName} content."; operation.Summary = $"Update a {schemaName} content.";
operation.AddBodyParameter("data", dataSchema, schemaBodyDescription); operation.AddBodyParameter("data", dataSchema, SchemaBodyDescription);
operation.AddResponse("204", $"{schemaName} element updated."); operation.AddResponse("204", $"{schemaName} element updated.");
operation.Security = EditorSecurity;
}); });
} }
@ -146,9 +178,11 @@ namespace Squidex.Controllers.ContentApi.Generator
operation.OperationId = $"Path{schemaKey}Content"; operation.OperationId = $"Path{schemaKey}Content";
operation.Summary = $"Patchs a {schemaName} content."; operation.Summary = $"Patchs a {schemaName} content.";
operation.AddBodyParameter("data", contentSchema, schemaBodyDescription); operation.AddBodyParameter("data", contentSchema, SchemaBodyDescription);
operation.AddResponse("204", $"{schemaName} element updated."); operation.AddResponse("204", $"{schemaName} element updated.");
operation.Security = EditorSecurity;
}); });
} }
@ -160,6 +194,8 @@ namespace Squidex.Controllers.ContentApi.Generator
operation.Summary = $"Publish a {schemaName} content."; operation.Summary = $"Publish a {schemaName} content.";
operation.AddResponse("204", $"{schemaName} element published."); operation.AddResponse("204", $"{schemaName} element published.");
operation.Security = EditorSecurity;
}); });
} }
@ -171,6 +207,8 @@ namespace Squidex.Controllers.ContentApi.Generator
operation.Summary = $"Unpublish a {schemaName} content."; operation.Summary = $"Unpublish a {schemaName} content.";
operation.AddResponse("204", $"{schemaName} element unpublished."); operation.AddResponse("204", $"{schemaName} element unpublished.");
operation.Security = EditorSecurity;
}); });
} }
@ -182,6 +220,8 @@ namespace Squidex.Controllers.ContentApi.Generator
operation.Summary = $"Delete a {schemaName} content."; operation.Summary = $"Delete a {schemaName} content.";
operation.AddResponse("204", $"{schemaName} content deleted."); operation.AddResponse("204", $"{schemaName} content deleted.");
operation.Security = EditorSecurity;
}); });
} }

26
src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs

@ -20,7 +20,6 @@ using Squidex.Domain.Apps.Read.Apps;
using Squidex.Domain.Apps.Read.Schemas; using Squidex.Domain.Apps.Read.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Pipeline.Swagger; using Squidex.Pipeline.Swagger;
using Squidex.Shared.Identity;
// ReSharper disable InvertIf // ReSharper disable InvertIf
// ReSharper disable SuggestBaseTypeForParameter // ReSharper disable SuggestBaseTypeForParameter
@ -55,37 +54,12 @@ namespace Squidex.Controllers.ContentApi.Generator
swaggerGenerator = new SwaggerGenerator(schemaGenerator, settings, schemaResolver); swaggerGenerator = new SwaggerGenerator(schemaGenerator, settings, schemaResolver);
GenerateSchemasOperations(schemas, app); GenerateSchemasOperations(schemas, app);
GenerateSecurityRequirements();
await GenerateDefaultErrorsAsync(); await GenerateDefaultErrorsAsync();
return document; return document;
} }
private void GenerateSecurityRequirements()
{
var securityRequirements = new List<SwaggerSecurityRequirement>
{
new SwaggerSecurityRequirement
{
{
Constants.SecurityDefinition,
new List<string>
{
SquidexRoles.AppOwner,
SquidexRoles.AppDeveloper,
SquidexRoles.AppEditor
}
}
}
};
foreach (var operation in document.Paths.Values.SelectMany(x => x.Values))
{
operation.Security = securityRequirements;
}
}
private void GenerateSchemasOperations(IEnumerable<ISchemaEntity> schemas, IAppEntity app) private void GenerateSchemasOperations(IEnumerable<ISchemaEntity> schemas, IAppEntity app)
{ {
var appBasePath = $"/content/{app.Name}"; var appBasePath = $"/content/{app.Name}";

7
src/Squidex/Pipeline/Swagger/SwaggerHelper.cs

@ -94,9 +94,10 @@ namespace Squidex.Pipeline.Swagger
Scopes = new Dictionary<string, string> Scopes = new Dictionary<string, string>
{ {
{ Constants.ApiScope, "Read and write access to the API" }, { Constants.ApiScope, "Read and write access to the API" },
{ SquidexRoles.AppOwner, "You get this scope / role when you are owner of the app you are accessing." }, { SquidexRoles.AppOwner, "App contributor with Owner permission." },
{ SquidexRoles.AppEditor, "You get this scope / role when you are owner of the app you are accessing or when the subject is a client." }, { SquidexRoles.AppEditor, "Client (writer) or App contributor with Editor permission." },
{ SquidexRoles.AppDeveloper, "You get this scope / role when you are owner of the app you are accessing." } { SquidexRoles.AppReader, "Client (readonly) or App contributor with Editor permission." },
{ SquidexRoles.AppDeveloper, "App contributor with Developer permission." },
}, },
Description = securityDescription Description = securityDescription
}; };

2
src/Squidex/Startup.cs

@ -76,7 +76,7 @@ namespace Squidex
Configuration.GetSection("urls")); Configuration.GetSection("urls"));
services.Configure<MyIdentityOptions>( services.Configure<MyIdentityOptions>(
Configuration.GetSection("identity")); Configuration.GetSection("identity"));
services.Configure<MyUrlsOptions>( services.Configure<MyUIOptions>(
Configuration.GetSection("ui")); Configuration.GetSection("ui"));
services.Configure<MyUsageOptions>( services.Configure<MyUsageOptions>(
Configuration.GetSection("usage")); Configuration.GetSection("usage"));

15
src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.html

@ -24,7 +24,20 @@
<label class="col col-3 col-form-label" for="field-pattern">Pattern</label> <label class="col col-3 col-form-label" for="field-pattern">Pattern</label>
<div class="col col-6"> <div class="col col-6">
<input type="text" class="form-control" id="field-pattern" formControlName="pattern" placeholder="Regex Pattern" /> <input type="text" class="form-control" id="field-pattern" formControlName="pattern" placeholder="Regex Pattern" #patternInput
autocomplete="off"
autocorrect="off"
autocapitalize="off"
(focus)="regexSuggestionsModal.show()" (blur)="regexSuggestionsModal.hide()" />
<div *ngIf="regexSuggestions.length > 0 && (regexSuggestionsModal.isOpen | async) && (showPatternSuggestions | async)" [sqxModalTarget]="patternInput" class="control-dropdown">
<h4>Suggestions</h4>
<div *ngFor="let suggestion of regexSuggestions" class="control-dropdown-item control-dropdown-item-selectable" (mousedown)="setPattern(suggestion.pattern)">
<div class="truncate">{{suggestion.name}}</div>
<div class="truncate text-muted">{{suggestion.pattern}}</div>
</div>
</div>
</div> </div>
</div> </div>
<div class="form-group row" *ngIf="showPatternMessage | async"> <div class="form-group row" *ngIf="showPatternMessage | async">

27
src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.scss

@ -11,6 +11,33 @@
} }
} }
.control-dropdown {
& {
max-width: 285px;
min-height: 220px;
}
h4 {
padding: .5rem 0 0 .5rem;
}
&-item {
& {
font-size: .85rem;
}
&:hover {
.text-muted {
color: $color-dark-foreground !important;
}
}
}
}
.form-check-input { .form-check-input {
margin: 0; margin: 0;
} }
.truncate {
@include truncate;
}

45
src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.ts

@ -9,7 +9,12 @@ import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms'; import { FormControl, FormGroup } from '@angular/forms';
import { Observable, Subscription } from 'rxjs'; import { Observable, Subscription } from 'rxjs';
import { StringFieldPropertiesDto } from 'shared'; import {
ModalView,
StringFieldPropertiesDto,
UIRegexSuggestionDto,
UIService
} from 'shared';
@Component({ @Component({
selector: 'sqx-string-validation', selector: 'sqx-string-validation',
@ -18,6 +23,7 @@ import { StringFieldPropertiesDto } from 'shared';
}) })
export class StringValidationComponent implements OnDestroy, OnInit { export class StringValidationComponent implements OnDestroy, OnInit {
private patternSubscription: Subscription; private patternSubscription: Subscription;
private uiSettingsSubscription: Subscription;
@Input() @Input()
public editForm: FormGroup; public editForm: FormGroup;
@ -27,9 +33,20 @@ export class StringValidationComponent implements OnDestroy, OnInit {
public showDefaultValue: Observable<boolean>; public showDefaultValue: Observable<boolean>;
public showPatternMessage: Observable<boolean>; public showPatternMessage: Observable<boolean>;
public showPatternSuggestions: Observable<boolean>;
public regexSuggestions: UIRegexSuggestionDto[] = [];
public regexSuggestionsModal = new ModalView(false, false);
constructor(
private readonly uiService: UIService
) {
}
public ngOnDestroy() { public ngOnDestroy() {
this.patternSubscription.unsubscribe(); this.patternSubscription.unsubscribe();
this.uiSettingsSubscription.unsubscribe();
} }
public ngOnInit() { public ngOnInit() {
@ -58,11 +75,27 @@ export class StringValidationComponent implements OnDestroy, OnInit {
.startWith('') .startWith('')
.map(x => x && x.trim().length > 0); .map(x => x && x.trim().length > 0);
this.showPatternSuggestions =
this.editForm.controls['pattern'].valueChanges
.startWith('')
.map(x => !x || x.trim().length === 0);
this.uiSettingsSubscription =
this.uiService.getSettings()
.subscribe(settings => {
this.regexSuggestions = settings.regexSuggestions;
});
this.patternSubscription = this.patternSubscription =
this.editForm.controls['pattern'].valueChanges.subscribe((value: string) => { this.editForm.controls['pattern'].valueChanges
if (!value || value.length === 0) { .subscribe((value: string) => {
this.editForm.controls['patternMessage'].setValue(undefined); if (!value || value.length === 0) {
} this.editForm.controls['patternMessage'].setValue(undefined);
}); }
});
}
public setPattern(pattern: string) {
this.editForm.controls['pattern'].setValue(pattern);
} }
} }

1
src/Squidex/app/shared/declarations-base.ts

@ -31,6 +31,7 @@ export * from './services/history.service';
export * from './services/languages.service'; export * from './services/languages.service';
export * from './services/plans.service'; export * from './services/plans.service';
export * from './services/schemas.service'; export * from './services/schemas.service';
export * from './services/ui.service';
export * from './services/usages.service'; export * from './services/usages.service';
export * from './services/users-provider.service'; export * from './services/users-provider.service';
export * from './services/users.service'; export * from './services/users.service';

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

@ -44,6 +44,7 @@ import {
ResolveSchemaGuard, ResolveSchemaGuard,
SchemasService, SchemasService,
ResolveUserGuard, ResolveUserGuard,
UIService,
UsagesService, UsagesService,
UserDtoPicture, UserDtoPicture,
UserEmailPipe, UserEmailPipe,
@ -129,6 +130,7 @@ export class SqxSharedModule {
ResolveSchemaGuard, ResolveSchemaGuard,
ResolveUserGuard, ResolveUserGuard,
SchemasService, SchemasService,
UIService,
UsagesService, UsagesService,
UserManagementService, UserManagementService,
UsersProviderService, UsersProviderService,

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

@ -0,0 +1,79 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { inject, TestBed } from '@angular/core/testing';
import {
ApiUrlConfig,
UIService,
UISettingsDto
} from './../';
describe('UIService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
HttpClientTestingModule
],
providers: [
UIService,
{ provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') }
]
});
});
afterEach(inject([HttpTestingController], (httpMock: HttpTestingController) => {
httpMock.verify();
}));
it('should make get request to get settings',
inject([UIService, HttpTestingController], (uiService: UIService, httpMock: HttpTestingController) => {
let settings1: UISettingsDto | null = null;
let settings2: UISettingsDto | null = null;
uiService.getSettings().subscribe(result => {
settings1 = result;
});
const response: UISettingsDto = { regexSuggestions: [] };
const req = httpMock.expectOne('http://service/p/api/ui/settings');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush(response);
uiService.getSettings().subscribe(result => {
settings2 = result;
});
expect(settings1).toEqual(response);
expect(settings2).toEqual(response);
}));
it('should return default settings when error occurs',
inject([UIService, HttpTestingController], (uiService: UIService, httpMock: HttpTestingController) => {
let settings: UISettingsDto | null = null;
uiService.getSettings().subscribe(result => {
settings = result;
});
const req = httpMock.expectOne('http://service/p/api/ui/settings');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
req.error(new ErrorEvent('500'));
expect(settings.regexSuggestions).toEqual([]);
}));
});

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

@ -0,0 +1,49 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import 'framework/angular/http-extensions';
import { ApiUrlConfig } from 'framework';
export interface UISettingsDto {
regexSuggestions: UIRegexSuggestionDto[];
}
export interface UIRegexSuggestionDto {
name: string; pattern: string;
}
@Injectable()
export class UIService {
private settings: UISettingsDto;
constructor(
private readonly http: HttpClient,
private readonly apiUrl: ApiUrlConfig
) {
}
public getSettings(): Observable<UISettingsDto> {
if (this.settings) {
return Observable.of(this.settings);
} else {
const url = this.apiUrl.buildUrl(`api/ui/settings`);
return this.http.get<UISettingsDto>(url)
.catch(error => {
return Observable.of({ regexSuggestions: [] });
})
.do(settings => {
this.settings = settings;
});
}
}
}

7
src/Squidex/appsettings.json

@ -7,10 +7,17 @@
}, },
"ui": { "ui": {
/*
* Regex suggestions for the UI
*/
"regexSuggestions": { "regexSuggestions": {
// Regex for emails.
"Email": "^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\\\\.[a-zA-Z0-9-]+)*$", "Email": "^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\\\\.[a-zA-Z0-9-]+)*$",
// Regex for phone numbers.
"Phone": "^\\(*\\+*[1-9]{0,3}\\)*-*[1-9]{0,3}[-. /]*\\(*[2-9]\\d{2}\\)*[-. /]*\\d{3}[-. /]*\\d{4} *e*x*t*\\.* *\\d{0,4}$", "Phone": "^\\(*\\+*[1-9]{0,3}\\)*-*[1-9]{0,3}[-. /]*\\(*[2-9]\\d{2}\\)*[-. /]*\\d{3}[-. /]*\\d{4} *e*x*t*\\.* *\\d{0,4}$",
// Regex for slugs (e.g. hello-world).
"Slug": "^[a-z0-9]+(\\\\-[a-z0-9]+)*$", "Slug": "^[a-z0-9]+(\\\\-[a-z0-9]+)*$",
// Regex for urls.
"Url": "^(?:http(s)?:\\/\\/)?[\\w.-]+(?:\\.[\\w\\.-]+)+[\\w\\-\\._~:/?#[\\]@!\\$&'\\(\\)\\*\\+,;=.]+$" "Url": "^(?:http(s)?:\\/\\/)?[\\w.-]+(?:\\.[\\w\\.-]+)+[\\w\\-\\._~:/?#[\\]@!\\$&'\\(\\)\\*\\+,;=.]+$"
} }
}, },

4
tests/Squidex.Domain.Apps.Read.Tests/Contents/GraphQLTests.cs

@ -35,8 +35,8 @@ namespace Squidex.Domain.Apps.Read.Contents
{ {
public class GraphQLTests public class GraphQLTests
{ {
private static readonly Guid schemaId = Guid.NewGuid(); private readonly Guid schemaId = Guid.NewGuid();
private static readonly Guid appId = Guid.NewGuid(); private readonly Guid appId = Guid.NewGuid();
private readonly Schema schema = private readonly Schema schema =
Schema.Create("my-schema", new SchemaProperties()) Schema.Create("my-schema", new SchemaProperties())

Loading…
Cancel
Save