Browse Source

Tests

pull/1/head
Sebastian 10 years ago
parent
commit
17396976e0
  1. 5
      src/Squidex.Core/Schemas/NamedElementProperties.cs
  2. 23
      src/Squidex.Core/Schemas/NumberFieldProperties.cs
  3. 24
      src/Squidex.Core/Schemas/StringFieldProperties.cs
  4. 4
      src/Squidex.Infrastructure/CQRS/Commands/DefaultDomainObjectRepository.cs
  5. 4
      src/Squidex.Infrastructure/CQRS/Events/EventDataFormatter.cs
  6. 16
      src/Squidex.Infrastructure/EnumExtensions.cs
  7. 10
      src/Squidex.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs
  8. 6
      src/Squidex/app/features/content/pages/content/content-page.component.html
  9. 7
      src/Squidex/app/features/content/pages/content/content-page.component.ts
  10. 4
      src/Squidex/app/features/schemas/pages/schema/field.component.html
  11. 4
      src/Squidex/app/features/schemas/pages/schema/field.component.ts
  12. 4
      src/Squidex/app/features/schemas/pages/schema/schema-edit-form.component.html
  13. 4
      src/Squidex/app/features/schemas/pages/schema/schema-edit-form.component.ts
  14. 2
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.html
  15. 5
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts
  16. 2
      src/Squidex/app/features/schemas/pages/schema/types/boolean-ui.component.html
  17. 2
      src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.html
  18. 2
      src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.html
  19. 2
      src/Squidex/app/features/schemas/pages/schemas/schema-form.component.html
  20. 4
      src/Squidex/app/features/schemas/pages/schemas/schema-form.component.ts
  21. 2
      src/Squidex/app/features/settings/pages/clients/client.component.html
  22. 2
      src/Squidex/app/features/settings/pages/clients/clients-page.component.html
  23. 92
      src/Squidex/app/framework/angular/control-errors.component.ts
  24. 14
      src/Squidex/app/framework/angular/validators.ts
  25. 2
      src/Squidex/app/shared/components/app-form.component.html
  26. 6
      src/Squidex/app/shared/components/app-form.component.ts
  27. 8
      tests/Squidex.Core.Tests/Schemas/NumberFieldPropertiesTests.cs
  28. 29
      tests/Squidex.Core.Tests/Schemas/StringFieldPropertiesTests.cs
  29. 168
      tests/Squidex.Infrastructure.Tests/CQRS/Commands/DefaultDomainObjectRepositoryTests.cs

5
src/Squidex.Core/Schemas/NamedElementProperties.cs

@ -47,6 +47,11 @@ namespace Squidex.Core.Schemas
} }
} }
public bool ShouldSerializeIsFrozen()
{
return false;
}
public void Freeze() public void Freeze()
{ {
IsFrozen = true; IsFrozen = true;

23
src/Squidex.Core/Schemas/NumberFieldProperties.cs

@ -93,27 +93,22 @@ namespace Squidex.Core.Schemas
yield return new ValidationError("Max value must be greater than min value", nameof(MinValue), nameof(MaxValue)); yield return new ValidationError("Max value must be greater than min value", nameof(MinValue), nameof(MaxValue));
} }
if (AllowedValues != null && AllowedValues.Count > 0 && (MinValue.HasValue || MaxValue.HasValue)) if (DefaultValue.HasValue && MinValue.HasValue && DefaultValue.Value < MinValue.Value)
{
yield return new ValidationError("Either or allowed values or range can be defined",
nameof(AllowedValues),
nameof(MinValue),
nameof(MaxValue));
}
if (!DefaultValue.HasValue)
{ {
yield break; yield return new ValidationError("Default value must be greater than min value", nameof(DefaultValue));
} }
if (MinValue.HasValue && DefaultValue.Value < MinValue.Value) if (DefaultValue.HasValue && MaxValue.HasValue && DefaultValue.Value > MaxValue.Value)
{ {
yield return new ValidationError("Default value must be greater than min value", nameof(DefaultValue)); yield return new ValidationError("Default value must be less than max value", nameof(DefaultValue));
} }
if (MaxValue.HasValue && DefaultValue.Value > MaxValue.Value) if (AllowedValues != null && AllowedValues.Count > 0 && (MinValue.HasValue || MaxValue.HasValue))
{ {
yield return new ValidationError("Default value must be less than max value", nameof(DefaultValue)); yield return new ValidationError("Either allowed values or min and max value can be defined",
nameof(AllowedValues),
nameof(MinValue),
nameof(MaxValue));
} }
} }
} }

24
src/Squidex.Core/Schemas/StringFieldProperties.cs

@ -6,9 +6,7 @@
// All rights reserved. // All rights reserved.
// ========================================================================== // ==========================================================================
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text.RegularExpressions;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using System.Collections.Immutable; using System.Collections.Immutable;
// ReSharper disable ObjectCreationAsStatement // ReSharper disable ObjectCreationAsStatement
@ -108,25 +106,17 @@ namespace Squidex.Core.Schemas
yield return new ValidationError("Max length must be greater than min length", nameof(MinLength), nameof(MaxLength)); yield return new ValidationError("Max length must be greater than min length", nameof(MinLength), nameof(MaxLength));
} }
if (Pattern == null) if (Pattern != null && !Pattern.IsValidRegex())
{ {
yield break; yield return new ValidationError("Pattern is not a valid expression", nameof(Pattern));
}
var isValidPattern = true;
try
{
new Regex(Pattern);
}
catch (ArgumentException)
{
isValidPattern = false;
} }
if (!isValidPattern) if (AllowedValues != null && AllowedValues.Count > 0 && (MinLength.HasValue || MaxLength.HasValue))
{ {
yield return new ValidationError("Pattern is not a valid expression", nameof(Pattern)); yield return new ValidationError("Either allowed values or min and max length can be defined",
nameof(AllowedValues),
nameof(MinLength),
nameof(MaxLength));
} }
} }
} }

4
src/Squidex.Infrastructure/CQRS/Commands/DefaultDomainObjectRepository.cs

@ -49,8 +49,6 @@ namespace Squidex.Infrastructure.CQRS.Commands
var streamName = nameResolver.GetStreamName(typeof(TDomainObject), id); var streamName = nameResolver.GetStreamName(typeof(TDomainObject), id);
var domainObject = (TDomainObject)factory.CreateNew(typeof(TDomainObject), id);
var events = await eventStore.GetEventsAsync(streamName).ToList(); var events = await eventStore.GetEventsAsync(streamName).ToList();
if (events.Count == 0) if (events.Count == 0)
@ -58,6 +56,8 @@ namespace Squidex.Infrastructure.CQRS.Commands
throw new DomainObjectNotFoundException(id.ToString(), typeof(TDomainObject)); throw new DomainObjectNotFoundException(id.ToString(), typeof(TDomainObject));
} }
var domainObject = (TDomainObject)factory.CreateNew(typeof(TDomainObject), id);
foreach (var eventData in events) foreach (var eventData in events)
{ {
var envelope = formatter.Parse(eventData); var envelope = formatter.Parse(eventData);

4
src/Squidex.Infrastructure/CQRS/Events/EventDataFormatter.cs

@ -22,7 +22,7 @@ namespace Squidex.Infrastructure.CQRS.Events
this.serializerSettings = serializerSettings ?? new JsonSerializerSettings(); this.serializerSettings = serializerSettings ?? new JsonSerializerSettings();
} }
public Envelope<IEvent> Parse(EventData eventData) public virtual Envelope<IEvent> Parse(EventData eventData)
{ {
var headers = ReadJson<PropertiesBag>(eventData.Metadata); var headers = ReadJson<PropertiesBag>(eventData.Metadata);
@ -34,7 +34,7 @@ namespace Squidex.Infrastructure.CQRS.Events
return envelope; return envelope;
} }
public EventData ToEventData(Envelope<IEvent> envelope, Guid commitId) public virtual EventData ToEventData(Envelope<IEvent> envelope, Guid commitId)
{ {
var eventType = TypeNameRegistry.GetName(envelope.Payload.GetType()); var eventType = TypeNameRegistry.GetName(envelope.Payload.GetType());

16
src/Squidex.Infrastructure/EnumExtensions.cs

@ -7,6 +7,8 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Text.RegularExpressions;
// ReSharper disable ObjectCreationAsStatement
namespace Squidex.Infrastructure namespace Squidex.Infrastructure
{ {
@ -23,5 +25,19 @@ namespace Squidex.Infrastructure
return false; return false;
} }
} }
public static bool IsValidRegex(this string value)
{
try
{
new Regex(value);
return true;
}
catch (ArgumentException)
{
return false;
}
}
} }
} }

10
src/Squidex.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs

@ -29,6 +29,8 @@ namespace Squidex.Read.Schemas.Services.Implementations
private sealed class CacheItem private sealed class CacheItem
{ {
public ISchemaEntityWithSchema Entity; public ISchemaEntityWithSchema Entity;
public string Name;
} }
public CachingSchemaProvider(IMemoryCache cache, ISchemaRepository repository) public CachingSchemaProvider(IMemoryCache cache, ISchemaRepository repository)
@ -48,7 +50,7 @@ namespace Squidex.Read.Schemas.Services.Implementations
{ {
var entity = await repository.FindSchemaAsync(schemaId); var entity = await repository.FindSchemaAsync(schemaId);
cacheItem = new CacheItem { Entity = entity }; cacheItem = new CacheItem { Entity = entity, Name = entity?.Name };
Cache.Set(cacheKey, cacheItem, CacheDuration); Cache.Set(cacheKey, cacheItem, CacheDuration);
@ -72,7 +74,7 @@ namespace Squidex.Read.Schemas.Services.Implementations
{ {
var entity = await repository.FindSchemaAsync(appId, name); var entity = await repository.FindSchemaAsync(appId, name);
cacheItem = new CacheItem { Entity = entity }; cacheItem = new CacheItem { Entity = entity, Name = name };
Cache.Set(cacheKey, cacheItem, CacheDuration); Cache.Set(cacheKey, cacheItem, CacheDuration);
@ -94,9 +96,9 @@ namespace Squidex.Read.Schemas.Services.Implementations
var cacheItem = Cache.Get<CacheItem>(cacheKey); var cacheItem = Cache.Get<CacheItem>(cacheKey);
if (cacheItem?.Entity != null) if (cacheItem?.Name != null)
{ {
Cache.Remove(BuildNameCacheKey(@event.Headers.AppId(), cacheItem.Entity.Name)); Cache.Remove(BuildNameCacheKey(@event.Headers.AppId(), cacheItem.Name));
} }
Cache.Remove(cacheKey); Cache.Remove(cacheKey);

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

@ -1,6 +1,6 @@
<sqx-title message="{app} | Content" parameter1="app" value1="{{appName() | async}}"></sqx-title> <sqx-title message="{app} | Content" parameter1="app" value1="{{appName() | async}}"></sqx-title>
<form [formGroup]="contentForm" (ngSubmit)="saveSchema()"> <form [formGroup]="contentForm" (ngSubmit)="saveContent()">
<div class="panel panel-light" > <div class="panel panel-light" >
<div class="panel-header"> <div class="panel-header">
<div class="float-xs-right"> <div class="float-xs-right">
@ -22,6 +22,8 @@
<div class="table-items-row"> <div class="table-items-row">
<label>{{field|displayName:'properties.label':'name'}}</label> <label>{{field|displayName:'properties.label':'name'}}</label>
<sqx-control-errors [for]="field.name" fieldName="{{field|displayName:'properties.label':'name'}}" [submitted]="contentFormSubmitted"></sqx-control-errors>
<div> <div>
<div [ngSwitch]="field.properties.fieldType"> <div [ngSwitch]="field.properties.fieldType">
<div *ngSwitchCase="'number'"> <div *ngSwitchCase="'number'">
@ -44,7 +46,7 @@
<div *ngSwitchCase="'string'"> <div *ngSwitchCase="'string'">
<div [ngSwitch]="field.properties.editor"> <div [ngSwitch]="field.properties.editor">
<div *ngSwitchCase="'Input'"> <div *ngSwitchCase="'Input'">
<input class="form-control" type="number" [formControlName]="field.name"> <input class="form-control" type="text" [formControlName]="field.name">
</div> </div>
<div *ngSwitchCase="'Dropdown'"> <div *ngSwitchCase="'Dropdown'">
<select class="form-control" [formControlName]="field.name"> <select class="form-control" [formControlName]="field.name">

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

@ -28,6 +28,7 @@ import {
export class ContentPageComponent extends AppComponentBase { export class ContentPageComponent extends AppComponentBase {
public schema: SchemaDetailsDto; public schema: SchemaDetailsDto;
public contentFormSubmitted = false;
public contentForm: FormGroup; public contentForm: FormGroup;
public isNewMode = false; public isNewMode = false;
@ -50,6 +51,10 @@ export class ContentPageComponent extends AppComponentBase {
}); });
} }
public saveContent() {
this.contentFormSubmitted = true;
}
private setupForm(schema: SchemaDetailsDto) { private setupForm(schema: SchemaDetailsDto) {
const controls: { [key: string]: AbstractControl } = {}; const controls: { [key: string]: AbstractControl } = {};
@ -74,7 +79,7 @@ export class ContentPageComponent extends AppComponentBase {
} }
} }
controls[field.name] = new FormControl(validators); controls[field.name] = new FormControl(undefined, validators);
} }
this.contentForm = new FormGroup(controls); this.contentForm = new FormGroup(controls);

4
src/Squidex/app/features/schemas/pages/schema/field.component.html

@ -86,7 +86,7 @@
<label for="field-label" class="col-xs-3 col-form-label">Label</label> <label for="field-label" class="col-xs-3 col-form-label">Label</label>
<div class="col-xs-6"> <div class="col-xs-6">
<sqx-control-errors for="label" [submitted]="editForm.touched"></sqx-control-errors> <sqx-control-errors for="label" [submitted]="editFormSubmitted"></sqx-control-errors>
<input type="text" class="form-control" id="field-label" maxlength="100" formControlName="label" /> <input type="text" class="form-control" id="field-label" maxlength="100" formControlName="label" />
@ -100,7 +100,7 @@
<label for="field-hints" class="col-xs-3 col-form-label">Hints</label> <label for="field-hints" class="col-xs-3 col-form-label">Hints</label>
<div class="col-xs-6"> <div class="col-xs-6">
<sqx-control-errors for="hints" [submitted]="editForm.touched"></sqx-control-errors> <sqx-control-errors for="hints" [submitted]="editFormSubmitted"></sqx-control-errors>
<input type="text" class="form-control" id="field-hints" maxlength="100" formControlName="hints" /> <input type="text" class="form-control" id="field-hints" maxlength="100" formControlName="hints" />

4
src/Squidex/app/features/schemas/pages/schema/field.component.ts

@ -54,6 +54,7 @@ export class FieldComponent implements OnInit {
return this.field.properties.label && this.field.properties.label.length > 0 ? this.field.properties.label : this.field.name; return this.field.properties.label && this.field.properties.label.length > 0 ? this.field.properties.label : this.field.name;
} }
public editFormSubmitted = false;
public editForm: FormGroup = public editForm: FormGroup =
this.formBuilder.group({ this.formBuilder.group({
label: ['', label: ['',
@ -77,7 +78,7 @@ export class FieldComponent implements OnInit {
} }
public save() { public save() {
this.editForm.markAsTouched(); this.editFormSubmitted = true;
if (this.editForm.valid) { if (this.editForm.valid) {
const properties = createProperties(this.field.properties['fieldType'], this.editForm.value); const properties = createProperties(this.field.properties['fieldType'], this.editForm.value);
@ -107,6 +108,7 @@ export class FieldComponent implements OnInit {
} }
private resetForm() { private resetForm() {
this.editFormSubmitted = false;
this.editForm.reset(this.field.properties); this.editForm.reset(this.field.properties);
this.isEditing = false; this.isEditing = false;

4
src/Squidex/app/features/schemas/pages/schema/schema-edit-form.component.html

@ -8,7 +8,7 @@
<div class="form-group"> <div class="form-group">
<label for="schema-label">Label</label> <label for="schema-label">Label</label>
<sqx-control-errors for="label" [submitted]="editForm.touched"></sqx-control-errors> <sqx-control-errors for="label" [submitted]="editFormSubmitted"></sqx-control-errors>
<input type="text" class="form-control" id="schema-label" formControlName="label" /> <input type="text" class="form-control" id="schema-label" formControlName="label" />
</div> </div>
@ -16,7 +16,7 @@
<div class="form-group"> <div class="form-group">
<label for="schema-hints">Hints</label> <label for="schema-hints">Hints</label>
<sqx-control-errors for="hints" [submitted]="editForm.touched"></sqx-control-errors> <sqx-control-errors for="hints" [submitted]="editFormSubmitted"></sqx-control-errors>
<textarea type="text" class="form-control" id="schema-hints" formControlName="hints" rows="4"></textarea> <textarea type="text" class="form-control" id="schema-hints" formControlName="hints" rows="4"></textarea>
</div> </div>

4
src/Squidex/app/features/schemas/pages/schema/schema-edit-form.component.ts

@ -34,6 +34,7 @@ export class SchemaEditFormComponent implements OnInit {
@Input() @Input()
public appName: string; public appName: string;
public editFormSubmitted = false;
public editForm: FormGroup = public editForm: FormGroup =
this.formBuilder.group({ this.formBuilder.group({
name: '', name: '',
@ -59,7 +60,7 @@ export class SchemaEditFormComponent implements OnInit {
} }
public saveSchema() { public saveSchema() {
this.editForm.markAsTouched(); this.editFormSubmitted = true;
if (this.editForm.valid) { if (this.editForm.valid) {
this.editForm.disable(); this.editForm.disable();
@ -78,6 +79,7 @@ export class SchemaEditFormComponent implements OnInit {
} }
public reset() { public reset() {
this.editFormSubmitted = false;
this.editForm.reset(); this.editForm.reset();
} }

2
src/Squidex/app/features/schemas/pages/schema/schema-page.component.html

@ -45,7 +45,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<sqx-control-errors for="name" [submitted]="addFieldForm.touched"></sqx-control-errors> <sqx-control-errors for="name" [submitted]="addFieldFormSubmitted"></sqx-control-errors>
<input type="text" class="form-control" formControlName="name" maxlength="40" placeholder="Enter field name" /> <input type="text" class="form-control" formControlName="name" maxlength="40" placeholder="Enter field name" />
</div> </div>

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

@ -54,6 +54,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit {
public isPublished: boolean; public isPublished: boolean;
public addFieldFormSubmitted = false;
public addFieldForm: FormGroup = public addFieldForm: FormGroup =
this.formBuilder.group({ this.formBuilder.group({
type: ['string', type: ['string',
@ -171,7 +172,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit {
} }
public addField() { public addField() {
this.addFieldForm.markAsTouched(); this.addFieldFormSubmitted = true;
if (this.addFieldForm.valid) { if (this.addFieldForm.valid) {
this.addFieldForm.disable(); this.addFieldForm.disable();
@ -183,6 +184,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit {
const reset = () => { const reset = () => {
this.addFieldForm.get('name').reset(); this.addFieldForm.get('name').reset();
this.addFieldForm.enable(); this.addFieldForm.enable();
this.addFieldFormSubmitted = false;
}; };
this.appName() this.appName()
@ -205,6 +207,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit {
} }
public resetFieldForm() { public resetFieldForm() {
this.addFieldFormSubmitted = false;
this.addFieldForm.reset(); this.addFieldForm.reset();
} }

2
src/Squidex/app/features/schemas/pages/schema/types/boolean-ui.component.html

@ -3,7 +3,7 @@
<label for="field-placeholder" class="col-xs-3 col-form-label">Placeholder</label> <label for="field-placeholder" class="col-xs-3 col-form-label">Placeholder</label>
<div class="col-xs-6"> <div class="col-xs-6">
<sqx-control-errors for="placeholder" [submitted]="editForm.touched"></sqx-control-errors> <sqx-control-errors for="placeholder" [submitted]="editFormSubmitted"></sqx-control-errors>
<input type="text" class="form-control" id="field-placeholder" maxlength="100" formControlName="placeholder" /> <input type="text" class="form-control" id="field-placeholder" maxlength="100" formControlName="placeholder" />

2
src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.html

@ -3,7 +3,7 @@
<label for="field-placeholder" class="col-xs-3 col-form-label">Placeholder</label> <label for="field-placeholder" class="col-xs-3 col-form-label">Placeholder</label>
<div class="col-xs-6"> <div class="col-xs-6">
<sqx-control-errors for="placeholder" [submitted]="editForm.touched"></sqx-control-errors> <sqx-control-errors for="placeholder" [submitted]="editFormSubmitted"></sqx-control-errors>
<input type="text" class="form-control" id="field-placeholder" maxlength="100" formControlName="placeholder" /> <input type="text" class="form-control" id="field-placeholder" maxlength="100" formControlName="placeholder" />

2
src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.html

@ -3,7 +3,7 @@
<label for="field-input-placeholder" class="col-xs-3 col-form-label">Placeholder</label> <label for="field-input-placeholder" class="col-xs-3 col-form-label">Placeholder</label>
<div class="col-xs-6"> <div class="col-xs-6">
<sqx-control-errors for="placeholder" [submitted]="editForm.touched"></sqx-control-errors> <sqx-control-errors for="placeholder" [submitted]="editFormSubmitted"></sqx-control-errors>
<input type="text" class="form-control" id="field-input-placeholder" maxlength="100" formControlName="placeholder" /> <input type="text" class="form-control" id="field-input-placeholder" maxlength="100" formControlName="placeholder" />

2
src/Squidex/app/features/schemas/pages/schemas/schema-form.component.html

@ -8,7 +8,7 @@
<div class="form-group"> <div class="form-group">
<label for="schema-name">Name</label> <label for="schema-name">Name</label>
<sqx-control-errors for="name" [submitted]="createForm.touched"></sqx-control-errors> <sqx-control-errors for="name" [submitted]="createFormSubmitted"></sqx-control-errors>
<input type="text" class="form-control" id="schema-name" formControlName="name" /> <input type="text" class="form-control" id="schema-name" formControlName="name" />

4
src/Squidex/app/features/schemas/pages/schemas/schema-form.component.ts

@ -40,6 +40,7 @@ export class SchemaFormComponent {
public cancelled = new EventEmitter(); public cancelled = new EventEmitter();
public creationError = ''; public creationError = '';
public createFormSubmitted = false;
public createForm: FormGroup = public createForm: FormGroup =
this.formBuilder.group({ this.formBuilder.group({
name: ['', name: ['',
@ -62,7 +63,7 @@ export class SchemaFormComponent {
} }
public createSchema() { public createSchema() {
this.createForm.markAsTouched(); this.createFormSubmitted = true;
if (this.createForm.valid) { if (this.createForm.valid) {
this.createForm.disable(); this.createForm.disable();
@ -85,6 +86,7 @@ export class SchemaFormComponent {
public reset() { public reset() {
this.creationError = ''; this.creationError = '';
this.createForm.reset(); this.createForm.reset();
this.createFormSubmitted = false;
} }
public cancel() { public cancel() {

2
src/Squidex/app/features/settings/pages/clients/client.component.html

@ -15,7 +15,7 @@
<div class="client-header"> <div class="client-header">
<form *ngIf="isRenaming" class="form-inline" [formGroup]="renameForm" (ngSubmit)="rename()"> <form *ngIf="isRenaming" class="form-inline" [formGroup]="renameForm" (ngSubmit)="rename()">
<div class="form-group"> <div class="form-group">
<sqx-control-errors for="name" [submitted]="renameForm.touched"></sqx-control-errors> <sqx-control-errors for="name" [submitted]="renameFormSubmitted"></sqx-control-errors>
<input type="text" class="form-control client-name enabled" formControlName="name" maxlength="20" sqxFocusOnInit (keydown)="onKeyDown($event.keyCode)" /> <input type="text" class="form-control client-name enabled" formControlName="name" maxlength="20" sqxFocusOnInit (keydown)="onKeyDown($event.keyCode)" />
</div> </div>

2
src/Squidex/app/features/settings/pages/clients/clients-page.component.html

@ -22,7 +22,7 @@
<div class="table-items-footer"> <div class="table-items-footer">
<form class="form-inline" [formGroup]="addClientForm" (ngSubmit)="attachClient()"> <form class="form-inline" [formGroup]="addClientForm" (ngSubmit)="attachClient()">
<div class="form-group"> <div class="form-group">
<sqx-control-errors for="name" [submitted]="addClientForm.touched"></sqx-control-errors> <sqx-control-errors for="name" [submitted]="addClientFormSubmitted"></sqx-control-errors>
<input type="text" class="form-control" formControlName="name" maxlength="40" placeholder="Enter client name" /> <input type="text" class="form-control" formControlName="name" maxlength="40" placeholder="Enter client name" />
</div> </div>

92
src/Squidex/app/framework/angular/control-errors.component.ts

@ -5,35 +5,32 @@
* Copyright (c) Sebastian Stehle. All rights reserved * Copyright (c) Sebastian Stehle. All rights reserved
*/ */
import { ChangeDetectorRef, ChangeDetectionStrategy, Component, Host, Input, OnChanges, OnInit, OnDestroy, Optional } from '@angular/core'; import { Component, Host, Input, OnChanges, OnInit, Optional } from '@angular/core';
import { AbstractControl, FormGroupDirective } from '@angular/forms'; import { AbstractControl, FormGroupDirective } from '@angular/forms';
import { Subscription } from 'rxjs';
import { fadeAnimation } from './animations'; import { fadeAnimation } from './animations';
const DEFAULT_ERRORS: { [key: string]: string } = { const DEFAULT_ERRORS: { [key: string]: string } = {
required: '{field} is required.', required: '{field} is required.',
pattern: '{field} does not follow the pattern.', pattern: '{field} does not follow the pattern.',
patternMessage: '{message}', patternmessage: '{message}',
minValue: '{field} must be larget than {minValue}.', minvalue: '{field} must be larger than {minValue}.',
maxValue: '{field} must be larget than {maxValue}.', maxvalue: '{field} must be smaller than {maxValue}.',
minLength: '{field} must have more than {minLength} characters.', minlength: '{field} must have more than {requiredLength} characters.',
maxLength: '{field} cannot have more than {maxLength} characters.', maxlength: '{field} cannot have more than {requiredLength} characters.',
validNumber: '{field} is not a valid number.', validnumber: '{field} is not a valid number.',
validValues: '{field} is not a valid value.' validvalues: '{field} is not a valid value.'
}; };
@Component({ @Component({
selector: 'sqx-control-errors', selector: 'sqx-control-errors',
styleUrls: ['./control-errors.component.scss'], styleUrls: ['./control-errors.component.scss'],
templateUrl: './control-errors.component.html', templateUrl: './control-errors.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
animations: [ animations: [
fadeAnimation fadeAnimation
] ]
}) })
export class ControlErrorsComponent implements OnChanges, OnInit, OnDestroy { export class ControlErrorsComponent implements OnChanges, OnInit {
private formSubscription: Subscription;
private displayFieldName: string; private displayFieldName: string;
private control: AbstractControl; private control: AbstractControl;
@ -49,51 +46,22 @@ export class ControlErrorsComponent implements OnChanges, OnInit, OnDestroy {
@Input() @Input()
public submitted: boolean; public submitted: boolean;
public errorMessages: string[]; public get errorMessages(): string[] {
constructor(@Optional() @Host() private readonly form: FormGroupDirective,
private readonly changeDetector: ChangeDetectorRef
) {
if (!this.form) {
throw new Error('control-errors must be used with a parent formGroup directive');
}
}
public ngOnChanges() {
if (this.fieldName) {
this.displayFieldName = this.fieldName;
} else if (this.for) {
this.displayFieldName = this.for.substr(0, 1).toUpperCase() + this.for.substr(1);
}
this.update();
}
public ngOnDestroy() {
this.formSubscription.unsubscribe();
}
public ngOnInit() {
this.control = this.form.form.controls[this.for];
this.formSubscription =
this.form.form.statusChanges.merge(this.control.statusChanges)
.subscribe(_ => {
this.update();
});
}
private update() {
if (!this.control) { if (!this.control) {
return; return null;
} }
if (this.control.invalid && (this.control.touched || this.form.form.touched)) { if (this.control.invalid && (this.control.touched || this.submitted)) {
const errors: string[] = []; const errors: string[] = [];
for (let key in <any>this.control.errors) { for (let key in <any>this.control.errors) {
if (this.control.errors.hasOwnProperty(key)) { if (this.control.errors.hasOwnProperty(key)) {
let message: string = (this.errors ? this.errors[key] : null) || DEFAULT_ERRORS[key]; let message: string = (this.errors ? this.errors[key] : null) || DEFAULT_ERRORS[key];
if (!message) {
continue;
}
let properties = this.control.errors[key]; let properties = this.control.errors[key];
for (let property in properties) { for (let property in properties) {
@ -108,11 +76,29 @@ export class ControlErrorsComponent implements OnChanges, OnInit, OnDestroy {
} }
} }
this.errorMessages = errors.length > 0 ? errors : null; return errors.length > 0 ? errors : null;
} else { }
this.errorMessages = null;
return null;
}
constructor(
@Optional() @Host() private readonly formGroupDirective: FormGroupDirective
) {
if (!this.formGroupDirective) {
throw new Error('control-errors must be used with a parent formGroup directive');
}
}
public ngOnChanges() {
if (this.fieldName) {
this.displayFieldName = this.fieldName;
} else if (this.for) {
this.displayFieldName = this.for.substr(0, 1).toUpperCase() + this.for.substr(1);
} }
}
this.changeDetector.markForCheck(); public ngOnInit() {
this.control = this.formGroupDirective.form.controls[this.for];
} }
} }

14
src/Squidex/app/framework/angular/validators.ts

@ -33,7 +33,7 @@ export class ValidatorsEx {
if (!regex.test(n)) { if (!regex.test(n)) {
if (message) { if (message) {
return { patternMessage: { requiredPattern: regexStr, actualValue: n, message } }; return { patternmessage: { requiredPattern: regexStr, actualValue: n, message } };
} else { } else {
return { pattern: { requiredPattern: regexStr, actualValue: n } }; return { pattern: { requiredPattern: regexStr, actualValue: n } };
} }
@ -44,15 +44,19 @@ export class ValidatorsEx {
} }
public static between(minValue: number | undefined, maxValue: number | undefined) { public static between(minValue: number | undefined, maxValue: number | undefined) {
if (!minValue || !maxValue) {
return Validators.nullValidator;
}
return (control: AbstractControl): { [key: string]: any } => { return (control: AbstractControl): { [key: string]: any } => {
const n: number = control.value; const n: number = control.value;
if (typeof n !== 'number') { if (typeof n !== 'number') {
return { validNumber: false }; return { validnumber: false };
} else if (minValue && n < minValue) { } else if (minValue && n < minValue) {
return { minValue: { minValue, actualValue: n } }; return { minvalue: { minValue, actualValue: n } };
} else if (maxValue && n > maxValue) { } else if (maxValue && n > maxValue) {
return { maxValue: { maxValue, actualValue: n } }; return { maxvalue: { maxValue, actualValue: n } };
} }
return {}; return {};
@ -64,7 +68,7 @@ export class ValidatorsEx {
const n: T = control.value; const n: T = control.value;
if (values.indexOf(n) < 0) { if (values.indexOf(n) < 0) {
return { validValues: false }; return { validvalues: false };
} }
return {}; return {};

2
src/Squidex/app/shared/components/app-form.component.html

@ -8,7 +8,7 @@
<div class="form-group"> <div class="form-group">
<label for="app-name">Name</label> <label for="app-name">Name</label>
<sqx-control-errors for="name" [submitted]="createForm.touched"></sqx-control-errors> <sqx-control-errors for="name" [submitted]="createFormSubmitted"></sqx-control-errors>
<input type="text" class="form-control" id="app-name" formControlName="name" /> <input type="text" class="form-control" id="app-name" formControlName="name" />

6
src/Squidex/app/shared/components/app-form.component.ts

@ -29,6 +29,7 @@ export class AppFormComponent {
public cancelled = new EventEmitter(); public cancelled = new EventEmitter();
public creationError = ''; public creationError = '';
public createFormSubmitted = false;
public createForm: FormGroup = public createForm: FormGroup =
this.formBuilder.group({ this.formBuilder.group({
name: ['', name: ['',
@ -50,7 +51,7 @@ export class AppFormComponent {
} }
public createApp() { public createApp() {
this.createForm.markAsTouched(); this.createFormSubmitted = true;
if (this.createForm.valid) { if (this.createForm.valid) {
this.createForm.disable(); this.createForm.disable();
@ -69,8 +70,9 @@ export class AppFormComponent {
} }
private reset() { private reset() {
this.createForm.enable();
this.creationError = ''; this.creationError = '';
this.createForm.enable();
this.createFormSubmitted = false;
} }
public cancel() { public cancel() {

8
tests/Squidex.Core.Tests/Schemas/NumberFieldPropertiesTests.cs

@ -79,7 +79,7 @@ namespace Squidex.Core.Schemas
} }
[Fact] [Fact]
public void Should_add_error_if_allowed_values_and_max_is_specified() public void Should_add_error_if_allowed_values_and_max_value_is_specified()
{ {
var sut = new NumberFieldProperties { MaxValue = 10, AllowedValues = ImmutableList.Create<double>(4) }; var sut = new NumberFieldProperties { MaxValue = 10, AllowedValues = ImmutableList.Create<double>(4) };
@ -88,12 +88,12 @@ namespace Squidex.Core.Schemas
errors.ShouldBeEquivalentTo( errors.ShouldBeEquivalentTo(
new List<ValidationError> new List<ValidationError>
{ {
new ValidationError("Either or allowed values or range can be defined", "AllowedValues", "MinValue", "MaxValue") new ValidationError("Either allowed values or min and max value can be defined", "AllowedValues", "MinValue", "MaxValue")
}); });
} }
[Fact] [Fact]
public void Should_add_error_if_allowed_values_and_min_is_specified() public void Should_add_error_if_allowed_values_and_min_value_is_specified()
{ {
var sut = new NumberFieldProperties { MinValue = 10, AllowedValues = ImmutableList.Create<double>(4) }; var sut = new NumberFieldProperties { MinValue = 10, AllowedValues = ImmutableList.Create<double>(4) };
@ -102,7 +102,7 @@ namespace Squidex.Core.Schemas
errors.ShouldBeEquivalentTo( errors.ShouldBeEquivalentTo(
new List<ValidationError> new List<ValidationError>
{ {
new ValidationError("Either or allowed values or range can be defined", "AllowedValues", "MinValue", "MaxValue") new ValidationError("Either allowed values or min and max value can be defined", "AllowedValues", "MinValue", "MaxValue")
}); });
} }

29
tests/Squidex.Core.Tests/Schemas/StringFieldPropertiesTests.cs

@ -8,6 +8,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using FluentAssertions; using FluentAssertions;
@ -34,6 +35,34 @@ namespace Squidex.Core.Schemas
}); });
} }
[Fact]
public void Should_add_error_if_allowed_values_and_max_value_is_specified()
{
var sut = new StringFieldProperties { MinLength = 10, AllowedValues = ImmutableList.Create("4") };
sut.Validate(errors);
errors.ShouldBeEquivalentTo(
new List<ValidationError>
{
new ValidationError("Either allowed values or min and max length can be defined", "AllowedValues", "MinLength", "MaxLength")
});
}
[Fact]
public void Should_add_error_if_allowed_values_and_min_value_is_specified()
{
var sut = new StringFieldProperties { MaxLength = 10, AllowedValues = ImmutableList.Create("4") };
sut.Validate(errors);
errors.ShouldBeEquivalentTo(
new List<ValidationError>
{
new ValidationError("Either allowed values or min and max length can be defined", "AllowedValues", "MinLength", "MaxLength")
});
}
[Fact] [Fact]
public void Should_add_error_if_radio_button_has_no_allowed_values() public void Should_add_error_if_radio_button_has_no_allowed_values()
{ {

168
tests/Squidex.Infrastructure.Tests/CQRS/Commands/DefaultDomainObjectRepositoryTests.cs

@ -0,0 +1,168 @@
// ==========================================================================
// DefaultDomainObjectRepositoryTests.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Moq;
using Squidex.Infrastructure.CQRS.Events;
using Xunit;
using System.Collections.Generic;
using System.Linq;
// ReSharper disable PrivateFieldCanBeConvertedToLocalVariable
namespace Squidex.Infrastructure.CQRS.Commands
{
public class DefaultDomainObjectRepositoryTests
{
private readonly Mock<IDomainObjectFactory> factory = new Mock<IDomainObjectFactory>();
private readonly Mock<IEventStore> eventStore = new Mock<IEventStore>();
private readonly Mock<IEventPublisher> eventPublisher = new Mock<IEventPublisher>();
private readonly Mock<IStreamNameResolver> streamNameResolver = new Mock<IStreamNameResolver>();
private readonly Mock<EventDataFormatter> eventDataFormatter = new Mock<EventDataFormatter>(null);
private readonly string streamName = Guid.NewGuid().ToString();
private readonly Guid aggregateId = Guid.NewGuid();
private readonly MyDomainObject domainObject;
private readonly DefaultDomainObjectRepository sut;
public DefaultDomainObjectRepositoryTests()
{
domainObject = new MyDomainObject(aggregateId, 123);
streamNameResolver.Setup(x => x.GetStreamName(It.IsAny<Type>(), aggregateId)).Returns(streamName);
factory.Setup(x => x.CreateNew(typeof(MyDomainObject), aggregateId)).Returns(domainObject);
sut = new DefaultDomainObjectRepository(factory.Object, eventStore.Object, eventPublisher.Object, streamNameResolver.Object, eventDataFormatter.Object);
}
public sealed class MyEvent : IEvent
{
}
public sealed class MyDomainObject : DomainObject
{
private readonly List<IEvent> appliedEvents = new List<IEvent>();
public List<IEvent> AppliedEvents
{
get { return appliedEvents; }
}
public MyDomainObject(Guid id, int version) : base(id, version)
{
}
public void AddEvent(IEvent @event)
{
RaiseEvent(@event);
}
protected override void DispatchEvent(Envelope<IEvent> @event)
{
appliedEvents.Add(@event.Payload);
}
}
[Fact]
public async Task Should_throw_exception_when_event_store_returns_no_events()
{
eventStore.Setup(x => x.GetEventsAsync(streamName)).Returns(Observable.Empty<EventData>());
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.GetByIdAsync<MyDomainObject>(aggregateId));
}
[Fact]
public async Task Should_apply_domain_objects_to_event()
{
var eventData1 = new EventData();
var eventData2 = new EventData();
var event1 = new MyEvent();
var event2 = new MyEvent();
eventStore.Setup(x => x.GetEventsAsync(streamName)).Returns(new[] { eventData1, eventData2 }.ToObservable());
eventDataFormatter.Setup(x => x.Parse(eventData1)).Returns(new Envelope<IEvent>(event1));
eventDataFormatter.Setup(x => x.Parse(eventData2)).Returns(new Envelope<IEvent>(event2));
var result = await sut.GetByIdAsync<MyDomainObject>(aggregateId);
Assert.Equal(result.AppliedEvents, new[] { event1, event2 });
}
[Fact]
public async Task Should_throw_exception_if_final_version_does_not_match_to_expected()
{
var eventData1 = new EventData();
var eventData2 = new EventData();
var event1 = new MyEvent();
var event2 = new MyEvent();
eventStore.Setup(x => x.GetEventsAsync(streamName)).Returns(new[] { eventData1, eventData2 }.ToObservable());
eventDataFormatter.Setup(x => x.Parse(eventData1)).Returns(new Envelope<IEvent>(event1));
eventDataFormatter.Setup(x => x.Parse(eventData2)).Returns(new Envelope<IEvent>(event2));
await Assert.ThrowsAsync<DomainObjectVersionException>(() => sut.GetByIdAsync<MyDomainObject>(aggregateId, 200));
}
[Fact]
public async Task Should_append_events_and_publish()
{
var commitId = Guid.NewGuid();
var event1 = new MyEvent();
var event2 = new MyEvent();
var eventData1 = new EventData();
var eventData2 = new EventData();
eventDataFormatter.Setup(x => x.ToEventData(It.Is<Envelope<IEvent>>(e => e.Payload == event1), commitId)).Returns(eventData1);
eventDataFormatter.Setup(x => x.ToEventData(It.Is<Envelope<IEvent>>(e => e.Payload == event2), commitId)).Returns(eventData2);
eventStore.Setup(x => x.AppendEventsAsync(commitId, streamName, 122, It.Is<IEnumerable<EventData>>(e => e.Count() == 2))).Returns(Task.FromResult(true)).Verifiable();
domainObject.AddEvent(event1);
domainObject.AddEvent(event2);
await sut.SaveAsync(domainObject, domainObject.GetUncomittedEvents(), commitId);
eventPublisher.Verify(x => x.Publish(eventData1));
eventPublisher.Verify(x => x.Publish(eventData2));
eventStore.VerifyAll();
}
[Fact]
public async Task Should_throw_exception_on_version_mismatch()
{
var commitId = Guid.NewGuid();
var event1 = new MyEvent();
var event2 = new MyEvent();
var eventData1 = new EventData();
var eventData2 = new EventData();
eventDataFormatter.Setup(x => x.ToEventData(It.Is<Envelope<IEvent>>(e => e.Payload == event1), commitId)).Returns(eventData1);
eventDataFormatter.Setup(x => x.ToEventData(It.Is<Envelope<IEvent>>(e => e.Payload == event2), commitId)).Returns(eventData2);
eventStore.Setup(x => x.AppendEventsAsync(commitId, streamName, 122, new List<EventData> { eventData1, eventData2 })).Throws(new WrongEventVersionException(1, 2)).Verifiable();
domainObject.AddEvent(event1);
domainObject.AddEvent(event2);
await Assert.ThrowsAsync<DomainObjectVersionException>(() => sut.SaveAsync(domainObject, domainObject.GetUncomittedEvents(), commitId));
eventStore.VerifyAll();
}
}
}
Loading…
Cancel
Save