Browse Source

References

pull/65/head
Sebastian Stehle 9 years ago
parent
commit
8977c5bb33
  1. 2
      src/Squidex.Core/Schemas/AssetsField.cs
  2. 30
      src/Squidex.Core/Schemas/AssetsFieldProperties.cs
  3. 2
      src/Squidex.Core/Schemas/ReferencesField.cs
  4. 29
      src/Squidex.Core/Schemas/ReferencesFieldProperties.cs
  5. 16
      src/Squidex.Core/Schemas/Validators/AssetsValidator.cs
  6. 16
      src/Squidex.Core/Schemas/Validators/ReferencesValidator.cs
  7. 2
      src/Squidex.Read.MongoDb/Contents/Visitors/FindExtensions.cs
  8. 10
      src/Squidex/Controllers/Api/Schemas/Models/AssetsFieldPropertiesDto.cs
  9. 10
      src/Squidex/Controllers/Api/Schemas/Models/ReferencesFieldPropertiesDto.cs
  10. 29
      src/Squidex/app/features/content/pages/content/content-page.component.ts
  11. 22
      src/Squidex/app/features/content/pages/contents/contents-page.component.html
  12. 2
      src/Squidex/app/features/content/shared/content-item.component.html
  13. 62
      src/Squidex/app/features/content/shared/content-item.component.ts
  14. 8
      src/Squidex/app/features/content/shared/references-editor.component.html
  15. 13
      src/Squidex/app/features/schemas/pages/schema/types/assets-validation.component.html
  16. 10
      src/Squidex/app/features/schemas/pages/schema/types/assets-validation.component.scss
  17. 14
      src/Squidex/app/features/schemas/pages/schema/types/assets-validation.component.ts
  18. 14
      src/Squidex/app/features/schemas/pages/schema/types/references-validation.component.html
  19. 10
      src/Squidex/app/features/schemas/pages/schema/types/references-validation.component.scss
  20. 6
      src/Squidex/app/features/schemas/pages/schema/types/references-validation.component.ts
  21. 2
      src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.scss
  22. 4
      src/Squidex/app/framework/angular/control-errors.component.ts
  23. 175
      src/Squidex/app/shared/services/schemas.fields.spec.ts
  24. 226
      src/Squidex/app/shared/services/schemas.service.ts
  25. 19
      tests/Squidex.Core.Tests/Schemas/AssetsFieldPropertiesTests.cs
  26. 22
      tests/Squidex.Core.Tests/Schemas/AssetsFieldTests.cs
  27. 19
      tests/Squidex.Core.Tests/Schemas/ReferencesFieldPropertiesTests.cs
  28. 22
      tests/Squidex.Core.Tests/Schemas/ReferencesFieldTests.cs
  29. 4
      tests/Squidex.Core.Tests/Schemas/SchemaTests.cs

2
src/Squidex.Core/Schemas/AssetsField.cs

@ -32,7 +32,7 @@ namespace Squidex.Core.Schemas
protected override IEnumerable<IValidator> CreateValidators()
{
yield return new AssetsValidator(Properties.IsRequired);
yield return new AssetsValidator(Properties.IsRequired, Properties.MinItems, Properties.MaxItems);
}
public IEnumerable<Guid> GetReferencedIds(JToken value)

30
src/Squidex.Core/Schemas/AssetsFieldProperties.cs

@ -15,6 +15,31 @@ namespace Squidex.Core.Schemas
[TypeName("AssetsField")]
public sealed class AssetsFieldProperties : FieldProperties
{
private int? minItems;
private int? maxItems;
public int? MinItems
{
get { return minItems; }
set
{
ThrowIfFrozen();
minItems = value;
}
}
public int? MaxItems
{
get { return maxItems; }
set
{
ThrowIfFrozen();
maxItems = value;
}
}
public override JToken GetDefaultValue()
{
return new JArray();
@ -22,7 +47,10 @@ namespace Squidex.Core.Schemas
protected override IEnumerable<ValidationError> ValidateCore()
{
yield break;
if (MaxItems.HasValue && MinItems.HasValue && MinItems.Value >= MaxItems.Value)
{
yield return new ValidationError("Max items must be greater than min items", nameof(MinItems), nameof(MaxItems));
}
}
}
}

2
src/Squidex.Core/Schemas/ReferencesField.cs

@ -34,7 +34,7 @@ namespace Squidex.Core.Schemas
{
if (Properties.SchemaId != Guid.Empty)
{
yield return new ReferencesValidator(Properties.IsRequired, Properties.SchemaId);
yield return new ReferencesValidator(Properties.IsRequired, Properties.SchemaId, Properties.MinItems, Properties.MaxItems);
}
}

29
src/Squidex.Core/Schemas/ReferencesFieldProperties.cs

@ -16,8 +16,32 @@ namespace Squidex.Core.Schemas
[TypeName("References")]
public sealed class ReferencesFieldProperties : FieldProperties
{
private int? minItems;
private int? maxItems;
private Guid schemaId;
public int? MinItems
{
get { return minItems; }
set
{
ThrowIfFrozen();
minItems = value;
}
}
public int? MaxItems
{
get { return maxItems; }
set
{
ThrowIfFrozen();
maxItems = value;
}
}
public Guid SchemaId
{
get { return schemaId; }
@ -36,7 +60,10 @@ namespace Squidex.Core.Schemas
protected override IEnumerable<ValidationError> ValidateCore()
{
yield break;
if (MaxItems.HasValue && MinItems.HasValue && MinItems.Value >= MaxItems.Value)
{
yield return new ValidationError("Max items must be greater than min items", nameof(MinItems), nameof(MaxItems));
}
}
}
}

16
src/Squidex.Core/Schemas/Validators/AssetsValidator.cs

@ -14,10 +14,14 @@ namespace Squidex.Core.Schemas.Validators
public sealed class AssetsValidator : IValidator
{
private readonly bool isRequired;
private readonly int? minItems;
private readonly int? maxItems;
public AssetsValidator(bool isRequired)
public AssetsValidator(bool isRequired, int? minItems = null, int? maxItems = null)
{
this.isRequired = isRequired;
this.minItems = minItems;
this.maxItems = maxItems;
}
public async Task ValidateAsync(object value, ValidationContext context, Action<string> addError)
@ -34,6 +38,16 @@ namespace Squidex.Core.Schemas.Validators
return;
}
if (minItems.HasValue && assets.AssetIds.Count < minItems.Value)
{
addError($"<FIELD> must have at least {minItems} asset(s)");
}
if (maxItems.HasValue && assets.AssetIds.Count > maxItems.Value)
{
addError($"<FIELD> must have not more than {maxItems} asset(s)");
}
var invalidIds = await context.GetInvalidAssetIdsAsync(assets.AssetIds);
foreach (var invalidId in invalidIds)

16
src/Squidex.Core/Schemas/Validators/ReferencesValidator.cs

@ -15,11 +15,15 @@ namespace Squidex.Core.Schemas.Validators
{
private readonly bool isRequired;
private readonly Guid schemaId;
private readonly int? minItems;
private readonly int? maxItems;
public ReferencesValidator(bool isRequired, Guid schemaId)
public ReferencesValidator(bool isRequired, Guid schemaId, int? minItems = null, int? maxItems = null)
{
this.isRequired = isRequired;
this.schemaId = schemaId;
this.minItems = minItems;
this.maxItems = maxItems;
}
public async Task ValidateAsync(object value, ValidationContext context, Action<string> addError)
@ -36,6 +40,16 @@ namespace Squidex.Core.Schemas.Validators
return;
}
if (minItems.HasValue && references.ContentIds.Count < minItems.Value)
{
addError($"<FIELD> must have at least {minItems} reference(s)");
}
if (maxItems.HasValue && references.ContentIds.Count > maxItems.Value)
{
addError($"<FIELD> must have not more than {maxItems} reference(s)");
}
var invalidIds = await context.GetInvalidContentIdsAsync(references.ContentIds, schemaId);
foreach (var invalidId in invalidIds)

2
src/Squidex.Read.MongoDb/Contents/Visitors/FindExtensions.cs

@ -80,7 +80,7 @@ namespace Squidex.Read.MongoDb.Contents.Visitors
if (ids != null && ids.Count > 0)
{
Filter.In(x => x.Id, ids);
filters.Add(Filter.In(x => x.Id, ids));
}
var filter = FilterBuilder.Build(query, schema);

10
src/Squidex/Controllers/Api/Schemas/Models/AssetsFieldPropertiesDto.cs

@ -15,6 +15,16 @@ namespace Squidex.Controllers.Api.Schemas.Models
[JsonSchema("Assets")]
public sealed class AssetsFieldPropertiesDto : FieldPropertiesDto
{
/// <summary>
/// The minimum allowed items for the field value.
/// </summary>
public int? MinItems { get; set; }
/// <summary>
/// The maximum allowed items for the field value.
/// </summary>
public int? MaxItems { get; set; }
public override FieldProperties ToProperties()
{
return SimpleMapper.Map(this, new AssetsFieldProperties());

10
src/Squidex/Controllers/Api/Schemas/Models/ReferencesFieldPropertiesDto.cs

@ -16,6 +16,16 @@ namespace Squidex.Controllers.Api.Schemas.Models
[JsonSchema("References")]
public sealed class ReferencesFieldPropertiesDto : FieldPropertiesDto
{
/// <summary>
/// The minimum allowed items for the field value.
/// </summary>
public int? MinItems { get; set; }
/// <summary>
/// The maximum allowed items for the field value.
/// </summary>
public int? MaxItems { get; set; }
/// <summary>
/// The id of the referenced schema.
/// </summary>

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

@ -6,7 +6,7 @@
*/
import { Component, OnDestroy, OnInit } from '@angular/core';
import { AbstractControl, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms';
import { AbstractControl, FormControl, FormGroup } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { Observable, Subject, Subscription } from 'rxjs';
@ -28,10 +28,7 @@ import {
ModalView,
MessageBus,
NotificationService,
NumberFieldPropertiesDto,
SchemaDetailsDto,
StringFieldPropertiesDto,
ValidatorsEx,
Version
} from 'shared';
@ -192,34 +189,14 @@ export class ContentPageComponent extends AppComponentBase implements CanCompone
const controls: { [key: string]: AbstractControl } = {};
for (const field of schema.fields) {
const validators: ValidatorFn[] = [];
if (field.properties.isRequired) {
validators.push(Validators.required);
}
if (field.properties instanceof NumberFieldPropertiesDto) {
validators.push(ValidatorsEx.between(field.properties.minValue, field.properties.maxValue));
}
if (field.properties instanceof StringFieldPropertiesDto) {
if (field.properties.minLength) {
validators.push(Validators.minLength(field.properties.minLength));
}
if (field.properties.maxLength) {
validators.push(Validators.maxLength(field.properties.maxLength));
}
if (field.properties.pattern) {
validators.push(ValidatorsEx.pattern(field.properties.pattern, field.properties.patternMessage));
}
}
const group = new FormGroup({});
if (field.partitioning === 'language') {
for (let language of this.languages) {
group.addControl(language.iso2Code, new FormControl(undefined, validators));
group.addControl(language.iso2Code, new FormControl(undefined, field.createValidators()));
}
} else {
group.addControl('iv', new FormControl(undefined, validators));
group.addControl('iv', new FormControl(undefined, field.createValidators()));
}
controls[field.name] = group;

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

@ -25,7 +25,13 @@
</button>
</div>
<h3 class="panel-title">Contents</h3>
<h3 class="panel-title" *ngIf="!isReadOnly">
Contents
</h3>
<h3 class="panel-title" *ngIf="isReadOnly">
References
</h3>
</div>
<a class="panel-close" sqxParentLink>
@ -39,7 +45,7 @@
<colgroup>
<col *ngFor="let field of contentFields" [style.width]="columnWidth + '%'" />
<col style="width: 180px" />
<col style="width: 50px" />
<col style="width: 70px" />
<col style="width: 80px" *ngIf="!isReadOnly" />
</colgroup>
@ -62,10 +68,9 @@
<tbody *ngIf="!isReadOnly">
<ng-template ngFor let-content [ngForOf]="contentItems">
<tr [routerLink]="[content.id]" routerLinkActive="active" class="content"
[sqxContent]="content"
<tr [sqxContent]="content" [routerLink]="[content.id]" routerLinkActive="active"
[language]="languageSelected"
[fields]="contentFields"
[schemaFields]="contentFields"
[schema]="schema"
(unpublishing)="unpublishContent(content)"
(publishing)="publishContent(content)"
@ -76,10 +81,11 @@
<tbody *ngIf="isReadOnly">
<ng-template ngFor let-content [ngForOf]="contentItems">
<tr [sqxContent]="content" isReadOnly="true" dnd-draggable [dragData]="dropData(content)"
<tr [sqxContent]="content" dnd-draggable [dragData]="dropData(content)"
[language]="languageSelected"
[fields]="contentFields"
[schema]="schema"></tr>
[schemaFields]="contentFields"
[schema]="schema"
isReadOnly="true"></tr>
<tr class="spacer"></tr>
</ng-template>
</tbody>

2
src/Squidex/app/features/content/shared/content-item.component.html

@ -29,7 +29,7 @@
</div>
</div>
</td>
<td *ngIf="isReadOnly">
<td *ngIf="isReference">
<button type="button" class="btn btn-link btn-danger" (click)="deleting.emit(); $event.stopPropagation()">
<i class="icon-bin2"></i>
</button>

62
src/Squidex/app/features/content/shared/content-item.component.ts

@ -12,7 +12,6 @@ import {
AppLanguageDto,
AppsStoreService,
ContentDto,
DateTime,
fadeAnimation,
FieldDto,
ModalView,
@ -41,10 +40,10 @@ export class ContentItemComponent extends AppComponentBase implements OnInit, On
public deleting = new EventEmitter<ContentDto>();
@Input()
public fields: FieldDto[];
public language: AppLanguageDto;
@Input()
public language: AppLanguageDto;
public schemaFields: FieldDto[];
@Input()
public schema: SchemaDto;
@ -52,6 +51,9 @@ export class ContentItemComponent extends AppComponentBase implements OnInit, On
@Input()
public isReadOnly = false;
@Input()
public isReference = false;
@Input('sqxContent')
public content: ContentDto;
@ -72,57 +74,25 @@ export class ContentItemComponent extends AppComponentBase implements OnInit, On
private updateValues() {
this.values = [];
for (let field of this.fields) {
this.values.push(this.getValue(field));
if (this.schemaFields) {
for (let field of this.schemaFields) {
this.values.push(this.getValue(field));
}
}
}
private getValue(field: FieldDto): any {
const contentField = this.content.data[field.name];
if (!contentField) {
return '';
}
const properties = field.properties;
let value: any;
if (field.partitioning === 'language') {
value = contentField[this.language.iso2Code];
} else {
value = contentField['iv'];
}
if (value) {
if (properties.fieldType === 'Json') {
value = '<Json />';
} else if (properties.fieldType === 'Geolocation') {
value = `${value.longitude}, ${value.latitude}`;
} else if (properties.fieldType === 'Boolean') {
value = value ? '✔' : '-';
}else if (properties.fieldType === 'Assets') {
try {
value = `${value.length} Asset(s)`;
} catch (ex) {
value = '0 Asset(s)';
}
} else if (properties.fieldType === 'DateTime') {
try {
const parsed = DateTime.parseISO_UTC(value);
if (properties['editor'] === 'Date') {
value = parsed.toStringFormat('YYYY-MM-DD');
} else {
value = parsed.toStringFormat('YYYY-MM-DD hh:mm:ss');
}
} catch (ex) {
value = value;
}
if (contentField) {
if (field.partitioning === 'language') {
return field.formatValue(contentField[this.language.iso2Code]);
} else {
return field.formatValue(contentField['iv']);
}
} else {
return '';
}
return value;
}
}

8
src/Squidex/app/features/content/shared/references-editor.component.html

@ -19,11 +19,13 @@
<tbody dnd-sortable-container [sortableData]="contentItems.mutableValues">
<ng-template ngFor let-content let-i="index" [ngForOf]="contentItems">
<tr [sqxContent]="content" isReadOnly="true" dnd-sortable [sortableIndex]="i" (sorted)="onContentsSorted($event)"
<tr [sqxContent]="content" dnd-sortable [sortableIndex]="i" (sorted)="onContentsSorted($event)"
[language]="languageSelected"
[fields]="contentFields"
[schemaFields]="contentFields"
[schema]="schema"
(deleting)="onContentRemoving(content)"></tr>
(deleting)="onContentRemoving(content)"
isReadOnly="true"
isReference="true"></tr>
<tr class="spacer"></tr>
</ng-template>
</tbody>

13
src/Squidex/app/features/schemas/pages/schema/types/assets-validation.component.html

@ -6,4 +6,17 @@
<input type="checkbox" class="form-check-input" id="field-required" formControlName="isRequired" />
</div>
</div>
<div class="form-group row">
<label class="col col-3 col-form-label">Items</label>
<div class="col col-3 minlength-col">
<input type="number" class="form-control" id="field-min-items" formControlName="minItems" placeholder="Min Items" />
<label class="col-form-label minitems-label">-</label>
</div>
<div class="col col-3">
<input type="number" class="form-control" id="field-max-items" formControlName="maxItems" placeholder="Max Items" />
</div>
</div>
</div>

10
src/Squidex/app/features/schemas/pages/schema/types/assets-validation.component.scss

@ -1,6 +1,16 @@
@import '_vars';
@import '_mixins';
.minitems {
&-col {
position: relative;
}
&-label {
@include absolute(0, -.2rem, auto, auto);
}
}
.form-check-input {
margin: 0;
}

14
src/Squidex/app/features/schemas/pages/schema/types/assets-validation.component.ts

@ -5,8 +5,8 @@
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { AssetsFieldPropertiesDto } from 'shared';
@ -16,10 +16,18 @@ import { AssetsFieldPropertiesDto } from 'shared';
templateUrl: 'assets-validation.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AssetsValidationComponent {
export class AssetsValidationComponent implements OnInit {
@Input()
public editForm: FormGroup;
@Input()
public properties: AssetsFieldPropertiesDto;
public ngOnInit() {
this.editForm.setControl('maxItems',
new FormControl(this.properties.maxItems));
this.editForm.setControl('minItems',
new FormControl(this.properties.minItems));
}
}

14
src/Squidex/app/features/schemas/pages/schema/types/references-validation.component.html

@ -8,6 +8,7 @@
</select>
</div>
</div>
<div class="form-group row">
<label class="col col-3 col-form-checkbox-label" for="field-required">Required</label>
@ -15,5 +16,18 @@
<input type="checkbox" class="form-check-input" id="field-required" formControlName="isRequired" />
</div>
</div>
<div class="form-group row">
<label class="col col-3 col-form-label">Items</label>
<div class="col col-3 minlength-col">
<input type="number" class="form-control" id="field-min-items" formControlName="minItems" placeholder="Min Items" />
<label class="col-form-label minitems-label">-</label>
</div>
<div class="col col-3">
<input type="number" class="form-control" id="field-max-items" formControlName="maxItems" placeholder="Max Items" />
</div>
</div>
</div>

10
src/Squidex/app/features/schemas/pages/schema/types/references-validation.component.scss

@ -1,6 +1,16 @@
@import '_vars';
@import '_mixins';
.minitems {
&-col {
position: relative;
}
&-label {
@include absolute(0, -.2rem, auto, auto);
}
}
.form-check-input {
margin: 0;
}

6
src/Squidex/app/features/schemas/pages/schema/types/references-validation.component.ts

@ -27,6 +27,12 @@ export class ReferencesValidationComponent implements OnInit {
public schemas: SchemaDto[];
public ngOnInit() {
this.editForm.setControl('maxItems',
new FormControl(this.properties.maxItems));
this.editForm.setControl('minItems',
new FormControl(this.properties.minItems));
this.editForm.setControl('schemaId',
new FormControl(this.properties.schemaId, [
Validators.required

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

@ -7,7 +7,7 @@
}
&-label {
@include absolute(0, -2rem, auto, auto);
@include absolute(0, -.2rem, auto, auto);
}
}

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

@ -16,8 +16,8 @@ const DEFAULT_ERRORS: { [key: string]: string } = {
patternmessage: '{message}',
minvalue: '{field} must be larger than {minValue}.',
maxvalue: '{field} must be smaller than {maxValue}.',
minlength: '{field} must have more than {requiredLength} characters.',
maxlength: '{field} cannot have more than {requiredLength} characters.',
minlength: '{field} must have a length of more than {requiredLength}.',
maxlength: '{field} must have a length of less than {requiredLength}.',
match: '{message}',
validdatetime: '{field} is not a valid date time',
validnumber: '{field} is not a valid number.',

175
src/Squidex/app/shared/services/schemas.fields.spec.ts

@ -0,0 +1,175 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import {
AssetsFieldPropertiesDto,
BooleanFieldPropertiesDto,
DateTimeFieldPropertiesDto,
FieldDto,
FieldPropertiesDto,
GeolocationFieldPropertiesDto,
JsonFieldPropertiesDto,
NumberFieldPropertiesDto,
ReferencesFieldPropertiesDto,
StringFieldPropertiesDto
} from './../';
describe('AssetsField', () => {
const field = createField(new AssetsFieldPropertiesDto(null, null, null, true, false, 1, 1));
it('should create validators', () => {
expect(field.createValidators().length).toBe(3);
});
it('should format to empty string if null', () => {
expect(field.formatValue(null)).toBe('');
});
it('should format to asset count', () => {
expect(field.formatValue([1, 2, 3])).toBe('3 Asset(s)');
});
it('should return zero formatting if other type', () => {
expect(field.formatValue(1)).toBe('0 Assets');
});
});
describe('BooleanField', () => {
const field = createField(new BooleanFieldPropertiesDto(null, null, null, true, false, 'Checkbox'));
it('should create validators', () => {
expect(field.createValidators().length).toBe(1);
});
it('should format to empty string if null', () => {
expect(field.formatValue(null)).toBe('');
});
it('should format to checkmark if true', () => {
expect(field.formatValue(true)).toBe('✔');
});
it('should format to minus if false', () => {
expect(field.formatValue(false)).toBe('-');
});
});
describe('DateTimeField', () => {
const field = createField(new DateTimeFieldPropertiesDto(null, null, null, true, false, 'Date'));
it('should create validators', () => {
expect(field.createValidators().length).toBe(1);
});
it('should format to empty string if null', () => {
expect(field.formatValue(null)).toBe('');
});
it('should format to input if parsing failed', () => {
expect(field.formatValue(true)).toBe(true);
});
it('should format to date', () => {
const dateField = createField(new DateTimeFieldPropertiesDto(null, null, null, true, false, 'Date'));
expect(dateField.formatValue('2017-12-12T16:00:00Z')).toBe('2017-12-12');
});
it('should format to date', () => {
const dateTimeField = createField(new DateTimeFieldPropertiesDto(null, null, null, true, false, 'DateTime'));
expect(dateTimeField.formatValue('2017-12-12T16:00:00Z').substr(0, 10)).toBe('2017-12-12');
});
});
describe('GeolocationField', () => {
const field = createField(new GeolocationFieldPropertiesDto(null, null, null, true, false, 'Default'));
it('should create validators', () => {
expect(field.createValidators().length).toBe(1);
});
it('should format to empty string if null', () => {
expect(field.formatValue(null)).toBe('');
});
it('should format to latitude and longitude', () => {
expect(field.formatValue({ latitude: 42, longitude: 3.14 })).toBe('3.14, 42');
});
});
describe('JsonField', () => {
const field = createField(new JsonFieldPropertiesDto(null, null, null, true, false));
it('should create validators', () => {
expect(field.createValidators().length).toBe(1);
});
it('should format to empty string if null', () => {
expect(field.formatValue(null)).toBe('');
});
it('should format to constant', () => {
expect(field.formatValue({})).toBe('<Json />');
});
});
describe('NumberField', () => {
const field = createField(new NumberFieldPropertiesDto(null, null, null, true, false, 'Input', undefined, 3, 1, [1, 2, 3]));
it('should create validators', () => {
expect(field.createValidators().length).toBe(4);
});
it('should format to empty string if null', () => {
expect(field.formatValue(null)).toBe('');
});
it('should format to number', () => {
expect(field.formatValue(42)).toBe(42);
});
});
describe('ReferencesField', () => {
const field = createField(new ReferencesFieldPropertiesDto(null, null, null, true, false, 1, 1));
it('should create validators', () => {
expect(field.createValidators().length).toBe(3);
});
it('should format to empty string if null', () => {
expect(field.formatValue(null)).toBe('');
});
it('should format to asset count', () => {
expect(field.formatValue([1, 2, 3])).toBe('3 Reference(s)');
});
it('should return zero formatting if other type', () => {
expect(field.formatValue(1)).toBe('0 References');
});
});
describe('NumberField', () => {
const field = createField(new StringFieldPropertiesDto(null, null, null, true, false, 'Input', undefined, 'pattern', undefined, 3, 1, ['1', '2']));
it('should create validators', () => {
expect(field.createValidators().length).toBe(5);
});
it('should format to empty string if null', () => {
expect(field.formatValue(null)).toBe('');
});
it('should format to string', () => {
expect(field.formatValue('hello')).toBe('hello');
});
});
function createField(properties: FieldPropertiesDto) {
return new FieldDto(1, 'field1', false, false, 'languages', properties);
}

226
src/Squidex/app/shared/services/schemas.service.ts

@ -6,6 +6,7 @@
*/
import { Injectable } from '@angular/core';
import { ValidatorFn, Validators } from '@angular/forms';
import { Observable } from 'rxjs';
import 'framework/angular/http-extensions';
@ -14,6 +15,7 @@ import {
ApiUrlConfig,
DateTime,
EntityCreatedDto,
ValidatorsEx,
Version
} from 'framework';
@ -110,6 +112,14 @@ export class FieldDto {
public readonly properties: FieldPropertiesDto
) {
}
public formatValue(value: any): string {
return this.properties.formatValue(value);
}
public createValidators(): ValidatorFn[] {
return this.properties.createValidators();
}
}
export abstract class FieldPropertiesDto {
@ -122,6 +132,10 @@ export abstract class FieldPropertiesDto {
public readonly isListField: boolean
) {
}
public abstract formatValue(value: any): string;
public abstract createValidators(): ValidatorFn[];
}
export class StringFieldPropertiesDto extends FieldPropertiesDto {
@ -138,6 +152,40 @@ export class StringFieldPropertiesDto extends FieldPropertiesDto {
) {
super('String', label, hints, placeholder, isRequired, isListField);
}
public formatValue(value: any): string {
if (!value) {
return '';
}
return value;
}
public createValidators(): ValidatorFn[] {
const validators: ValidatorFn[] = [];
if (this.isRequired) {
validators.push(Validators.required);
}
if (this.minLength) {
validators.push(Validators.minLength(this.minLength));
}
if (this.maxLength) {
validators.push(Validators.maxLength(this.maxLength));
}
if (this.pattern && this.pattern.length > 0) {
validators.push(ValidatorsEx.pattern(this.pattern, this.patternMessage));
}
if (this.allowedValues && this.allowedValues.length > 0) {
validators.push(ValidatorsEx.validValues(this.allowedValues));
}
return validators;
}
}
export class NumberFieldPropertiesDto extends FieldPropertiesDto {
@ -152,6 +200,36 @@ export class NumberFieldPropertiesDto extends FieldPropertiesDto {
) {
super('Number', label, hints, placeholder, isRequired, isListField);
}
public formatValue(value: any): string {
if (!value) {
return '';
}
return value;
}
public createValidators(): ValidatorFn[] {
const validators: ValidatorFn[] = [];
if (this.isRequired) {
validators.push(Validators.required);
}
if (this.minValue) {
validators.push(Validators.min(this.minValue));
}
if (this.maxValue) {
validators.push(Validators.max(this.maxValue));
}
if (this.allowedValues && this.allowedValues.length > 0) {
validators.push(ValidatorsEx.validValues(this.allowedValues));
}
return validators;
}
}
export class DateTimeFieldPropertiesDto extends FieldPropertiesDto {
@ -166,6 +244,34 @@ export class DateTimeFieldPropertiesDto extends FieldPropertiesDto {
) {
super('DateTime', label, hints, placeholder, isRequired, isListField);
}
public formatValue(value: any): string {
if (!value) {
return '';
}
try {
const parsed = DateTime.parseISO_UTC(value);
if (this.editor === 'Date') {
return parsed.toStringFormat('YYYY-MM-DD');
} else {
return parsed.toStringFormat('YYYY-MM-DD HH:mm:ss');
}
} catch (ex) {
return value;
}
}
public createValidators(): ValidatorFn[] {
const validators: ValidatorFn[] = [];
if (this.isRequired) {
validators.push(Validators.required);
}
return validators;
}
}
export class BooleanFieldPropertiesDto extends FieldPropertiesDto {
@ -177,6 +283,24 @@ export class BooleanFieldPropertiesDto extends FieldPropertiesDto {
) {
super('Boolean', label, hints, placeholder, isRequired, isListField);
}
public formatValue(value: any): string {
if (value === null || value === undefined) {
return '';
}
return value ? '✔' : '-';
}
public createValidators(): ValidatorFn[] {
const validators: ValidatorFn[] = [];
if (this.isRequired) {
validators.push(Validators.required);
}
return validators;
}
}
export class GeolocationFieldPropertiesDto extends FieldPropertiesDto {
@ -187,25 +311,107 @@ export class GeolocationFieldPropertiesDto extends FieldPropertiesDto {
) {
super('Geolocation', label, hints, placeholder, isRequired, isListField);
}
public formatValue(value: any): string {
if (!value) {
return '';
}
return `${value.longitude}, ${value.latitude}`;
}
public createValidators(): ValidatorFn[] {
const validators: ValidatorFn[] = [];
if (this.isRequired) {
validators.push(Validators.required);
}
return validators;
}
}
export class ReferencesFieldPropertiesDto extends FieldPropertiesDto {
constructor(label: string | null, hints: string | null, placeholder: string | null,
isRequired: boolean,
isListField: boolean,
public readonly minItems?: number,
public readonly maxItems?: number,
public readonly schemaId?: string
) {
super('References', label, hints, placeholder, isRequired, isListField);
}
public formatValue(value: any): string {
if (!value) {
return '';
}
if (value.length) {
return `${value.length} Reference(s)`;
} else {
return '0 References';
}
}
public createValidators(): ValidatorFn[] {
const validators: ValidatorFn[] = [];
if (this.isRequired) {
validators.push(Validators.required);
}
if (this.minItems) {
validators.push(Validators.minLength(this.minItems));
}
if (this.maxItems) {
validators.push(Validators.maxLength(this.maxItems));
}
return validators;
}
}
export class AssetsFieldPropertiesDto extends FieldPropertiesDto {
constructor(label: string | null, hints: string | null, placeholder: string | null,
isRequired: boolean,
isListField: boolean
isListField: boolean,
public readonly minItems?: number,
public readonly maxItems?: number
) {
super('Assets', label, hints, placeholder, isRequired, isListField);
}
public formatValue(value: any): string {
if (!value) {
return '';
}
if (value.length) {
return `${value.length} Asset(s)`;
} else {
return '0 Assets';
}
}
public createValidators(): ValidatorFn[] {
const validators: ValidatorFn[] = [];
if (this.isRequired) {
validators.push(Validators.required);
}
if (this.minItems) {
validators.push(Validators.minLength(this.minItems));
}
if (this.maxItems) {
validators.push(Validators.maxLength(this.maxItems));
}
return validators;
}
}
export class JsonFieldPropertiesDto extends FieldPropertiesDto {
@ -215,6 +421,24 @@ export class JsonFieldPropertiesDto extends FieldPropertiesDto {
) {
super('Json', label, hints, placeholder, isRequired, isListField);
}
public formatValue(value: any): string {
if (!value) {
return '';
}
return '<Json />';
}
public createValidators(): ValidatorFn[] {
const validators: ValidatorFn[] = [];
if (this.isRequired) {
validators.push(Validators.required);
}
return validators;
}
}
export class SchemaPropertiesDto {

19
tests/Squidex.Core.Tests/Schemas/AssetsFieldPropertiesTests.cs

@ -7,14 +7,33 @@
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using FluentAssertions;
using Squidex.Infrastructure;
using Xunit;
namespace Squidex.Core.Schemas
{
public class AssetsFieldPropertiesTests
{
private readonly List<ValidationError> errors = new List<ValidationError>();
[Fact]
public void Should_add_error_if_min_greater_than_max()
{
var sut = new AssetsFieldProperties { MinItems = 10, MaxItems = 5 };
sut.Validate(errors);
errors.ShouldBeEquivalentTo(
new List<ValidationError>
{
new ValidationError("Max items must be greater than min items", "MinItems", "MaxItems")
});
}
[Fact]
public void Should_set_or_freeze_sut()
{

22
tests/Squidex.Core.Tests/Schemas/AssetsFieldTests.cs

@ -91,6 +91,28 @@ namespace Squidex.Core.Schemas
new[] { "<FIELD> is not a valid value" });
}
[Fact]
public async Task Should_add_errors_if_value_has_not_enough_items()
{
var sut = new AssetsField(1, "my-asset", Partitioning.Invariant, new AssetsFieldProperties { MinItems = 3 });
await sut.ValidateAsync(CreateValue(Guid.NewGuid(), Guid.NewGuid()), errors);
errors.ShouldBeEquivalentTo(
new[] { "<FIELD> must have at least 3 asset(s)" });
}
[Fact]
public async Task Should_add_errors_if_value_has_too_much_items()
{
var sut = new AssetsField(1, "my-asset", Partitioning.Invariant, new AssetsFieldProperties { MaxItems = 1 });
await sut.ValidateAsync(CreateValue(Guid.NewGuid(), Guid.NewGuid()), errors);
errors.ShouldBeEquivalentTo(
new[] { "<FIELD> must have not more than 1 asset(s)" });
}
[Fact]
public async Task Should_add_errors_if_asset_are_not_valid()
{

19
tests/Squidex.Core.Tests/Schemas/ReferencesFieldPropertiesTests.cs

@ -7,14 +7,33 @@
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using FluentAssertions;
using Squidex.Infrastructure;
using Xunit;
namespace Squidex.Core.Schemas
{
public class ReferencesFieldPropertiesTests
{
private readonly List<ValidationError> errors = new List<ValidationError>();
[Fact]
public void Should_add_error_if_min_greater_than_max()
{
var sut = new ReferencesFieldProperties { MinItems = 10, MaxItems = 5 };
sut.Validate(errors);
errors.ShouldBeEquivalentTo(
new List<ValidationError>
{
new ValidationError("Max items must be greater than min items", "MinItems", "MaxItems")
});
}
[Fact]
public void Should_set_or_freeze_sut()
{

22
tests/Squidex.Core.Tests/Schemas/ReferencesFieldTests.cs

@ -92,6 +92,28 @@ namespace Squidex.Core.Schemas
new[] { "<FIELD> is not a valid value" });
}
[Fact]
public async Task Should_add_errors_if_value_has_not_enough_items()
{
var sut = new ReferencesField(1, "my-refs", Partitioning.Invariant, new ReferencesFieldProperties { SchemaId = schemaId, MinItems = 3 });
await sut.ValidateAsync(CreateValue(Guid.NewGuid(), Guid.NewGuid()), errors);
errors.ShouldBeEquivalentTo(
new[] { "<FIELD> must have at least 3 reference(s)" });
}
[Fact]
public async Task Should_add_errors_if_value_has_too_much_items()
{
var sut = new ReferencesField(1, "my-refs", Partitioning.Invariant, new ReferencesFieldProperties { SchemaId = schemaId, MaxItems = 1 });
await sut.ValidateAsync(CreateValue(Guid.NewGuid(), Guid.NewGuid()), errors);
errors.ShouldBeEquivalentTo(
new[] { "<FIELD> must have not more than 1 reference(s)" });
}
[Fact]
public async Task Should_add_errors_if_reference_are_not_valid()
{

4
tests/Squidex.Core.Tests/Schemas/SchemaTests.cs

@ -332,7 +332,9 @@ namespace Squidex.Core.Schemas
.AddOrUpdateField(new DateTimeField(8, "my-date", Partitioning.Invariant,
new DateTimeFieldProperties { Editor = DateTimeFieldEditor.Date }))
.AddOrUpdateField(new GeolocationField(9, "my-geolocation", Partitioning.Invariant,
new GeolocationFieldProperties()));
new GeolocationFieldProperties()))
.AddOrUpdateField(new ReferencesField(10, "my-references", Partitioning.Invariant,
new ReferencesFieldProperties()));
return schema;
}

Loading…
Cancel
Save