Browse Source

More tags editors and allowed values.

pull/335/head
Sebastian Stehle 8 years ago
parent
commit
9c98560d35
  1. 16
      src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldEditor.cs
  2. 5
      src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldProperties.cs
  3. 8
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AllowedValuesValidator.cs
  4. 9
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidatorsFactory.cs
  5. 12
      src/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs
  6. 6
      src/Squidex/Areas/Api/Controllers/Schemas/Models/Converters/FieldPropertiesDtoFactory.cs
  7. 23
      src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/TagsFieldPropertiesDto.cs
  8. 14
      src/Squidex/app/features/content/shared/field-editor.component.html
  9. 59
      src/Squidex/app/features/schemas/pages/schema/types/tags-ui.component.html
  10. 20
      src/Squidex/app/features/schemas/pages/schema/types/tags-ui.component.ts
  11. 9
      src/Squidex/app/framework/angular/forms/checkbox-group.component.html
  12. 12
      src/Squidex/app/framework/angular/forms/checkbox-group.component.scss
  13. 79
      src/Squidex/app/framework/angular/forms/checkbox-group.component.ts
  14. 3
      src/Squidex/app/framework/angular/forms/control-errors.component.ts
  15. 26
      src/Squidex/app/framework/angular/forms/dropdown.component.html
  16. 38
      src/Squidex/app/framework/angular/forms/dropdown.component.scss
  17. 149
      src/Squidex/app/framework/angular/forms/dropdown.component.ts
  18. 24
      src/Squidex/app/framework/angular/forms/validators.spec.ts
  19. 20
      src/Squidex/app/framework/angular/forms/validators.ts
  20. 2
      src/Squidex/app/framework/declarations.ts
  21. 6
      src/Squidex/app/framework/module.ts
  22. 7
      src/Squidex/app/shared/services/schemas.types.ts
  23. 2
      src/Squidex/app/shared/state/contents.forms.spec.ts
  24. 6
      src/Squidex/app/shared/state/contents.forms.ts
  25. 18
      src/Squidex/app/theme/icomoon/demo.html
  26. BIN
      src/Squidex/app/theme/icomoon/fonts/icomoon.eot
  27. 1
      src/Squidex/app/theme/icomoon/fonts/icomoon.svg
  28. BIN
      src/Squidex/app/theme/icomoon/fonts/icomoon.ttf
  29. BIN
      src/Squidex/app/theme/icomoon/fonts/icomoon.woff
  30. 68
      src/Squidex/app/theme/icomoon/icons/control-Checkboxes.svg
  31. 2
      src/Squidex/app/theme/icomoon/selection.json
  32. 13
      src/Squidex/app/theme/icomoon/style.css
  33. 12
      tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/TagsFieldTests.cs
  34. 28
      tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/TagsFieldPropertiesTests.cs

16
src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldEditor.cs

@ -0,0 +1,16 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.Schemas
{
public enum TagsFieldEditor
{
Tags,
Checkboxes,
Dropdown
}
}

5
src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldProperties.cs

@ -6,16 +6,21 @@
// ==========================================================================
using Squidex.Infrastructure;
using System.Collections.Immutable;
namespace Squidex.Domain.Apps.Core.Schemas
{
[TypeName("TagsField")]
public sealed class TagsFieldProperties : FieldProperties
{
public ImmutableList<string> AllowedValues { get; set; }
public int? MinItems { get; set; }
public int? MaxItems { get; set; }
public TagsFieldEditor Editor { get; set; }
public TagsFieldNormalization Normalization { get; set; }
public override T Accept<T>(IFieldPropertiesVisitor<T> visitor)

8
src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AllowedValuesValidator.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Infrastructure;
@ -14,9 +15,14 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
{
public sealed class AllowedValuesValidator<T> : IValidator
{
private readonly T[] allowedValues;
private readonly IEnumerable<T> allowedValues;
public AllowedValuesValidator(params T[] allowedValues)
: this((IEnumerable<T>)allowedValues)
{
}
public AllowedValuesValidator(IEnumerable<T> allowedValues)
{
Guard.NotNull(allowedValues, nameof(allowedValues));

9
src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidatorsFactory.cs

@ -109,7 +109,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
if (field.Properties.AllowedValues != null)
{
yield return new AllowedValuesValidator<double>(field.Properties.AllowedValues.ToArray());
yield return new AllowedValuesValidator<double>(field.Properties.AllowedValues);
}
}
@ -145,7 +145,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
if (field.Properties.AllowedValues != null)
{
yield return new AllowedValuesValidator<string>(field.Properties.AllowedValues.ToArray());
yield return new AllowedValuesValidator<string>(field.Properties.AllowedValues);
}
}
@ -156,6 +156,11 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
yield return new CollectionValidator(field.Properties.IsRequired, field.Properties.MinItems, field.Properties.MaxItems);
}
if (field.Properties.AllowedValues != null)
{
yield return new CollectionItemValidator(new AllowedValuesValidator<string>(field.Properties.AllowedValues));
}
yield return new CollectionItemValidator(new RequiredStringValidator());
}
}

12
src/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs

@ -243,6 +243,18 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
public IEnumerable<ValidationError> Visit(TagsFieldProperties properties)
{
if (!properties.Editor.IsEnumValue())
{
yield return new ValidationError("Editor is not a valid value.",
nameof(properties.Editor));
}
if ((properties.Editor == TagsFieldEditor.Checkboxes || properties.Editor == TagsFieldEditor.Dropdown) && (properties.AllowedValues == null || properties.AllowedValues.Count == 0))
{
yield return new ValidationError("Chekboxes or dropdown list need allowed values.",
nameof(properties.AllowedValues));
}
if (properties.MaxItems.HasValue && properties.MinItems.HasValue && properties.MinItems.Value >= properties.MaxItems.Value)
{
yield return new ValidationError("Max items must be greater than min items.",

6
src/Squidex/Areas/Api/Controllers/Schemas/Models/Converters/FieldPropertiesDtoFactory.cs

@ -57,7 +57,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Converters
public FieldPropertiesDto Visit(TagsFieldProperties properties)
{
return SimpleMapper.Map(properties, new TagsFieldPropertiesDto());
var result = SimpleMapper.Map(properties, new TagsFieldPropertiesDto());
result.AllowedValues = properties.AllowedValues?.ToArray();
return result;
}
public FieldPropertiesDto Visit(AssetsFieldProperties properties)

23
src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/TagsFieldPropertiesDto.cs

@ -5,6 +5,9 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Immutable;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using NJsonSchema.Annotations;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure.Reflection;
@ -24,9 +27,27 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields
/// </summary>
public int? MaxItems { get; set; }
/// <summary>
/// The allowed values for the field value.
/// </summary>
public string[] AllowedValues { get; set; }
/// <summary>
/// The editor that is used to manage this field.
/// </summary>
[JsonConverter(typeof(StringEnumConverter))]
public TagsFieldEditor Editor { get; set; }
public override FieldProperties ToProperties()
{
return SimpleMapper.Map(this, new TagsFieldProperties());
var result = SimpleMapper.Map(this, new TagsFieldProperties());
if (AllowedValues != null)
{
result.AllowedValues = ImmutableList.Create(AllowedValues);
}
return result;
}
}
}

14
src/Squidex/app/features/content/shared/field-editor.component.html

@ -104,7 +104,19 @@
<sqx-assets-editor [formControl]="control"></sqx-assets-editor>
</ng-container>
<ng-container *ngSwitchCase="'Tags'">
<sqx-tag-editor [formControl]="control"></sqx-tag-editor>
<ng-container [ngSwitch]="field.properties['editor']">
<ng-container *ngSwitchCase="'Tags'">
<sqx-tag-editor [formControl]="control" [placeholder]="field.displayPlaceholder" [suggestions]="field.properties['allowedValues']"></sqx-tag-editor>
</ng-container>
<ng-container *ngSwitchCase="'Checkboxes'">
<sqx-checkbox-group [formControl]="control" [values]="field.properties['allowedValues']"></sqx-checkbox-group>
</ng-container>
<ng-container *ngSwitchCase="'Dropdown'">
<select multiple class="form-control" [formControl]="control">
<option *ngFor="let value of field.properties['allowedValues']" [ngValue]="value">{{value}}</option>
</select>
</ng-container>
</ng-container>
</ng-container>
<ng-container *ngSwitchCase="'Array'">
<sqx-array-editor

59
src/Squidex/app/features/schemas/pages/schema/types/tags-ui.component.html

@ -1,3 +1,58 @@
<div [formGroup]="editForm">
<span>Nothing to setup</span>
<div>
<div class="form-group row">
<label class="col-3 col-form-label" for="{{field.fieldId}}_editorUrl">Editor Url</label>
<div class="col-6">
<input type="text" class="form-control" id="{{field.fieldId}}_editorUrl" formControlName="editorUrl" />
<small class="form-text text-muted">
Url to your plugin if you use a custom editor.
</small>
</div>
</div>
<div class="form-group row">
<label class="col-3 col-form-label" for="{{field.fieldId}}_fieldPlaceholder">Placeholder</label>
<div class="col-6">
<input type="text" class="form-control" id="{{field.fieldId}}_fieldPlaceholder" maxlength="100" formControlName="placeholder" />
<small class="form-text text-muted">
Define the placeholder for the input control.
</small>
</div>
</div>
<div class="form-group row">
<label class="col-3 col-form-label">Editor</label>
<div class="col-9">
<label class="btn btn-radio" [class.active]="editForm.controls['editor'].value === 'Tags'">
<input type="radio" class="radio-input" formControlName="editor" value="Tags" />
<i class="icon-control-Input"></i>
<span class="radio-label">Tags</span>
</label>
<label class="btn btn-radio" [class.active]="editForm.controls['editor'].value === 'Dropdown'">
<input type="radio" class="radio-input" formControlName="editor" value="Dropdown" />
<i class="icon-control-Dropdown"></i>
<span class="radio-label" clas>Dropdown</span>
</label>
<label class="btn btn-radio" [class.active]="editForm.controls['editor'].value === 'Checkboxes'">
<input type="radio" class="radio-input" formControlName="editor" value="Checkboxes" />
<i class="icon-control-Checkboxes"></i>
<span class="radio-label">Checkboxes</span>
</label>
</div>
</div>
<div class="form-group row">
<label class="col-3 col-form-label">Allowed Values</label>
<div class="col-6">
<sqx-tag-editor formControlName="allowedValues"></sqx-tag-editor>
</div>
</div>
</div>

20
src/Squidex/app/features/schemas/pages/schema/types/tags-ui.component.ts

@ -5,17 +5,17 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component, Input } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { Component, Input, OnInit } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { AssetsFieldPropertiesDto, FieldDto } from '@app/shared';
import { FieldDto, TagsFieldPropertiesDto } from '@app/shared';
@Component({
selector: 'sqx-tags-ui',
styleUrls: ['tags-ui.component.scss'],
templateUrl: 'tags-ui.component.html'
})
export class TagsUIComponent {
export class TagsUIComponent implements OnInit {
@Input()
public editForm: FormGroup;
@ -23,5 +23,15 @@ export class TagsUIComponent {
public field: FieldDto;
@Input()
public properties: AssetsFieldPropertiesDto;
public properties: TagsFieldPropertiesDto;
public ngOnInit() {
this.editForm.setControl('editor',
new FormControl(this.properties.editor, [
Validators.required
]));
this.editForm.setControl('allowedValues',
new FormControl(this.properties.allowedValues));
}
}

9
src/Squidex/app/framework/angular/forms/checkbox-group.component.html

@ -0,0 +1,9 @@
<span class="form-check" *ngFor="let value of values">
<input type="checkbox" class="form-check-input" id="{{control}}{{value}}"
(blur)="blur()"
(change)="check($event.target.checked, value)"
[checked]="isChecked(value)"
[disabled]="isDisabled">
<label class="form-check-label" for="{{control}}{{value}}">{{value}}</label>
</span>

12
src/Squidex/app/framework/angular/forms/checkbox-group.component.scss

@ -0,0 +1,12 @@
@import '_mixins';
@import '_vars';
.form-check {
display: inline-block;
margin-left: 0;
margin-right: 1rem;
}
.form-check-input {
margin-top: .4rem;
}

79
src/Squidex/app/framework/angular/forms/checkbox-group.component.ts

@ -0,0 +1,79 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, Input } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Types } from '@app/framework/internal';
import { MathHelper } from '@app/shared';
export const SQX_CHECKBOX_GROUP_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => CheckboxGroupComponent), multi: true
};
@Component({
selector: 'sqx-checkbox-group',
styleUrls: ['./checkbox-group.component.scss'],
templateUrl: './checkbox-group.component.html',
providers: [SQX_CHECKBOX_GROUP_CONTROL_VALUE_ACCESSOR],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CheckboxGroupComponent implements ControlValueAccessor {
private callChange = (v: any) => { /* NOOP */ };
private callTouched = () => { /* NOOP */ };
private checkedValues: string[] = [];
@Input()
public values: string[] = [];
public isDisabled = false;
public control = MathHelper.guid();
constructor(
private readonly changeDetector: ChangeDetectorRef
) {
}
public writeValue(obj: any) {
this.checkedValues = Types.isArrayOfString(obj) ? obj.filter(x => this.values.indexOf(x) >= 0) : [];
this.changeDetector.markForCheck();
}
public setDisabledState(isDisabled: boolean): void {
this.isDisabled = isDisabled;
this.changeDetector.markForCheck();
}
public registerOnChange(fn: any) {
this.callChange = fn;
}
public registerOnTouched(fn: any) {
this.callTouched = fn;
}
public blur() {
this.callTouched();
}
public check(isChecked: boolean, value: string) {
if (isChecked) {
this.checkedValues = [value, ...this.checkedValues];
} else {
this.checkedValues = this.checkedValues.filter(x => x !== value);
}
this.callChange(this.checkedValues);
}
public isChecked(value: string) {
return this.checkedValues.indexOf(value) >= 0;
}
}

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

@ -22,7 +22,8 @@ const DEFAULT_ERRORS: { [key: string]: string } = {
match: '{message}',
validdatetime: '{field} is not a valid date time',
validnumber: '{field} is not a valid number.',
validvalues: '{field} is not a valid value.'
validvalues: '{field} is not a valid value.',
validarrayvalues: '{field} contains an invalid value.'
};
@Component({

26
src/Squidex/app/framework/angular/forms/dropdown.component.html

@ -1,26 +0,0 @@
<span>
<div class="selection">
<input type="text" class="form-control" [disabled]="isDisabled" (click)="open()" readonly (keydown)="onKeyDown($event)" #input
autocomplete="off"
autocorrect="off"
autocapitalize="off">
<div class="control-dropdown-item" *ngIf="selectedItem">
<ng-container *ngIf="!selectionTemplate">{{selectedItem}}</ng-container>
<ng-template *ngIf="selectionTemplate" [sqxTemplateWrapper]="selectionTemplate" [item]="selectedItem"></ng-template>
</div>
<i class="icon-caret-down"></i>
</div>
<div class="items-container">
<div class="control-dropdown" #container *sqxModalView="dropdown" [sqxModalTarget]="input" position="bottomLeft">
<div *ngFor="let item of items; let i = index;" class="control-dropdown-item control-dropdown-item-selectable" [class.active]="i === selectedIndex" (mousedown)="selectIndexAndClose(i)" [sqxScrollActive]="i === selectedIndex" [container]="container">
<ng-container *ngIf="!itemTemplate">{{item}}</ng-container>
<ng-template *ngIf="itemTemplate" [sqxTemplateWrapper]="itemTemplate" [item]="item" [index]="i"></ng-template>
</div>
</div>
</div>
</span>

38
src/Squidex/app/framework/angular/forms/dropdown.component.scss

@ -1,38 +0,0 @@
@import '_mixins';
@import '_vars';
$color-input-disabled: #eef1f4;
.form-control {
& {
width: 100%;
}
&[readonly] {
background: $color-input-background;
}
&:disabled {
background: $color-input-disabled;
}
}
.selection {
& {
position: relative;
overflow: hidden;
}
.control-dropdown-item {
@include absolute(0, 1rem, 0, 0);
pointer-events: none;
}
.icon-caret-down {
@include absolute(30%, .4rem, auto, auto);
cursor: pointer;
font-size: .9rem;
font-weight: normal;
pointer-events: none;
}
}

149
src/Squidex/app/framework/angular/forms/dropdown.component.ts

@ -1,149 +0,0 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { AfterContentInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChildren, forwardRef, Input, QueryList, TemplateRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
const KEY_ENTER = 13;
const KEY_ESCAPE = 27;
const KEY_UP = 38;
const KEY_DOWN = 40;
import { ModalModel } from '@app/framework/internal';
export const SQX_DROPDOWN_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => DropdownComponent), multi: true
};
@Component({
selector: 'sqx-dropdown',
styleUrls: ['./dropdown.component.scss'],
templateUrl: './dropdown.component.html',
providers: [SQX_DROPDOWN_CONTROL_VALUE_ACCESSOR],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DropdownComponent implements AfterContentInit, ControlValueAccessor {
private callChange = (v: any) => { /* NOOP */ };
private callTouched = () => { /* NOOP */ };
@Input()
public items: any[] = [];
@ContentChildren(TemplateRef)
public templates: QueryList<any>;
public dropdown = new ModalModel();
public selectedItem: any;
public selectedIndex = -1;
public selectionTemplate: TemplateRef<any>;
public itemTemplate: TemplateRef<any>;
public isDisabled = false;
constructor(
private readonly changeDetector: ChangeDetectorRef
) {
}
public ngAfterContentInit() {
if (this.templates.length === 1) {
this.itemTemplate = this.selectionTemplate = this.templates.first;
} else {
this.templates.forEach(template => {
if (template.name === 'selection') {
this.selectionTemplate = template;
} else {
this.itemTemplate = template;
}
});
}
}
public writeValue(obj: any) {
this.selectIndex(this.items && obj ? this.items.indexOf(obj) : 0);
this.changeDetector.markForCheck();
}
public setDisabledState(isDisabled: boolean): void {
this.isDisabled = isDisabled;
this.changeDetector.markForCheck();
}
public registerOnChange(fn: any) {
this.callChange = fn;
}
public registerOnTouched(fn: any) {
this.callTouched = fn;
}
public onKeyDown(event: KeyboardEvent) {
switch (event.keyCode) {
case KEY_UP:
this.up();
return false;
case KEY_DOWN:
this.down();
return false;
case KEY_ESCAPE:
case KEY_ENTER:
if (this.dropdown.isOpen) {
this.close();
return false;
}
}
return true;
}
public open() {
this.dropdown.show();
this.callTouched();
}
public selectIndexAndClose(selectedIndex: number) {
this.selectIndex(selectedIndex);
this.close();
}
private close() {
this.dropdown.hide();
}
private up() {
this.selectIndex(this.selectedIndex - 1);
}
private down() {
this.selectIndex(this.selectedIndex + 1);
}
private selectIndex(selectedIndex: number) {
if (selectedIndex < 0) {
selectedIndex = 0;
}
const items = this.items || [];
if (selectedIndex >= items.length) {
selectedIndex = items.length - 1;
}
const value = items[selectedIndex];
if (value !== this.selectedItem) {
this.selectedIndex = selectedIndex;
this.selectedItem = value;
this.callChange(value);
}
}
}

24
src/Squidex/app/framework/angular/forms/validators.spec.ts

@ -101,6 +101,30 @@ describe('ValidatorsEx.validValues', () => {
});
});
describe('ValidatorsEx.validArrayValues', () => {
it('should return null validator if values not defined', () => {
const validator = ValidatorsEx.validArrayValues(null!);
expect(validator).toBe(Validators.nullValidator);
});
it('should return null if value is in allowed values', () => {
const input = new FormControl([10, 20]);
const error = ValidatorsEx.validArrayValues([10, 20, 30])(input);
expect(error).toBeNull();
});
it('should return error if value is not in allowed values', () => {
const input = new FormControl([50]);
const error = <any>ValidatorsEx.validArrayValues([10, 20, 30])(input);
expect(error.validarrayvalues).toBeDefined();
});
});
describe('ValidatorsEx.match', () => {
it('should revalidate if other control changes', () => {
const validator = ValidatorsEx.match('password', 'Passwords are not the same.');

20
src/Squidex/app/framework/angular/forms/validators.ts

@ -125,6 +125,26 @@ export module ValidatorsEx {
};
}
export function validArrayValues<T>(values: T[]): ValidatorFn {
if (!values) {
return Validators.nullValidator;
}
return (control: AbstractControl) => {
const ns: T[] = control.value;
if (ns) {
for (let n of ns) {
if (values.indexOf(n) < 0) {
return { validarrayvalues: false };
}
}
}
return null;
};
}
export function noop(): ValidatorFn {
return () => {
return null;

2
src/Squidex/app/framework/declarations.ts

@ -6,12 +6,12 @@
*/
export * from './angular/forms/autocomplete.component';
export * from './angular/forms/checkbox-group.component';
export * from './angular/forms/code-editor.component';
export * from './angular/forms/confirm-click.directive';
export * from './angular/forms/control-errors.component';
export * from './angular/forms/copy.directive';
export * from './angular/forms/date-time-editor.component';
export * from './angular/forms/dropdown.component';
export * from './angular/forms/file-drop.directive';
export * from './angular/forms/focus-on-init.directive';
export * from './angular/forms/form-error.component';

6
src/Squidex/app/framework/module.ts

@ -15,6 +15,7 @@ import {
AutocompleteComponent,
CachingInterceptor,
CanDeactivateGuard,
CheckboxGroupComponent,
ClipboardService,
CodeEditorComponent,
ConfirmClickDirective,
@ -28,7 +29,6 @@ import {
DialogRendererComponent,
DialogService,
DisplayNamePipe,
DropdownComponent,
DurationPipe,
FileDropDirective,
FileSizePipe,
@ -92,6 +92,7 @@ import {
],
declarations: [
AutocompleteComponent,
CheckboxGroupComponent,
ConfirmClickDirective,
ControlErrorsComponent,
CodeEditorComponent,
@ -103,7 +104,6 @@ import {
DayPipe,
DialogRendererComponent,
DisplayNamePipe,
DropdownComponent,
DurationPipe,
FileDropDirective,
FileSizePipe,
@ -152,6 +152,7 @@ import {
],
exports: [
AutocompleteComponent,
CheckboxGroupComponent,
CodeEditorComponent,
CommonModule,
ConfirmClickDirective,
@ -164,7 +165,6 @@ import {
DayPipe,
DialogRendererComponent,
DisplayNamePipe,
DropdownComponent,
DurationPipe,
FileDropDirective,
FileSizePipe,

7
src/Squidex/app/shared/services/schemas.types.ts

@ -73,7 +73,7 @@ export function createProperties(fieldType: string, values: Object | null = null
properties = new StringFieldPropertiesDto('Input');
break;
case 'Tags':
properties = new TagsFieldPropertiesDto();
properties = new TagsFieldPropertiesDto('Tags');
break;
default:
throw 'Invalid properties type';
@ -302,11 +302,12 @@ export class TagsFieldPropertiesDto extends FieldPropertiesDto {
public readonly minItems?: number;
public readonly maxItems?: number;
public readonly allowedValues?: string[];
constructor(
constructor(editor: string,
props?: Partial<TagsFieldPropertiesDto>
) {
super('Default', props);
super('Tags', props);
}
public accept<T>(visitor: FieldPropertiesVisitor<T>): T {

2
src/Squidex/app/shared/state/contents.forms.spec.ts

@ -166,7 +166,7 @@ describe('AssetsField', () => {
});
describe('TagsField', () => {
const field = createField(new TagsFieldPropertiesDto({ isRequired: true, minItems: 1, maxItems: 5 }));
const field = createField(new TagsFieldPropertiesDto('Tags', { isRequired: true, minItems: 1, maxItems: 5 }));
it('should create validators', () => {
expect(FieldValidatorsFactory.createValidators(field, false).length).toBe(3);

6
src/Squidex/app/shared/state/contents.forms.ts

@ -250,6 +250,12 @@ export class FieldValidatorsFactory implements FieldPropertiesVisitor<ValidatorF
validators.push(Validators.maxLength(properties.maxItems));
}
if (properties.allowedValues && properties.allowedValues.length > 0) {
const values: (string | null)[] = properties.allowedValues;
validators.push(ValidatorsEx.validArrayValues(values));
}
return validators;
}

18
src/Squidex/app/theme/icomoon/demo.html

@ -9,7 +9,7 @@
<link rel="stylesheet" href="style.css"></head>
<body>
<div class="bgc1 clearfix">
<h1 class="mhmm mvm"><span class="fgc1">Font Name:</span> icomoon <small class="fgc1">(Glyphs:&nbsp;101)</small></h1>
<h1 class="mhmm mvm"><span class="fgc1">Font Name:</span> icomoon <small class="fgc1">(Glyphs:&nbsp;102)</small></h1>
</div>
<div class="clearfix mhl ptl">
<h1 class="mvm mtn fgc1">Grid Size: 24</h1>
@ -1372,6 +1372,22 @@
</div>
<div class="clearfix mhl ptl">
<h1 class="mvm mtn fgc1">Grid Size: 14</h1>
<div class="glyph fs6">
<div class="clearfix bshadow0 pbs">
<span class="icon-control-Checkboxes">
</span>
<span class="mls"> icon-control-Checkboxes</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e962" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe962;" class="unitRight size1of2 talign-right" />
</fieldset>
<div class="fs0 bshadow0 clearfix hidden-true">
<span class="unit pvs fgc1">liga: </span>
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs6">
<div class="clearfix bshadow0 pbs">
<span class="icon-control-Html">

BIN
src/Squidex/app/theme/icomoon/fonts/icomoon.eot

Binary file not shown.

1
src/Squidex/app/theme/icomoon/fonts/icomoon.svg

@ -105,6 +105,7 @@
<glyph unicode="&#xe95f;" glyph-name="comments" d="M854 256.667v512h-684v-598l86 86h598zM854 852.667c46 0 84-38 84-84v-512c0-46-38-86-84-86h-598l-170-170v768c0 46 38 84 84 84h684z" />
<glyph unicode="&#xe960;" glyph-name="control-Html" d="M159.073 285.257l-159.073 134.055 159.073 133.819 36.818-37.29-117.062-96.057 117.062-97.237zM493.247 414.828q0-33.042-9.441-57.115-9.205-24.073-25.961-40.122-16.521-15.813-39.178-23.601-22.657-7.552-49.327-7.552-26.197 0-48.855 4.012-22.421 3.776-42.954 10.385v323.338h57.587v-78.356l-2.36-47.203q12.981 16.757 30.21 26.905 17.465 10.149 41.774 10.149 21.241 0 37.762-8.496t27.614-24.309q11.329-15.577 17.229-37.998 5.9-22.185 5.9-50.035zM432.828 412.468q0 19.825-2.832 33.75t-8.26 22.893q-5.192 8.969-12.981 12.981-7.552 4.248-17.465 4.248-14.633 0-28.086-11.801-13.217-11.801-28.086-32.098v-104.79q6.844-2.596 16.757-4.248 10.149-1.652 20.533-1.652 13.689 0 24.781 5.664 11.329 5.664 19.117 16.049 8.024 10.385 12.273 25.253 4.248 15.105 4.248 33.75zM700.682 437.249q0.472 13.453-1.416 22.893-1.652 9.441-5.664 15.577-3.776 6.136-9.441 8.968t-12.981 2.832q-12.745 0-26.433-10.621-13.453-10.385-29.738-34.458v-151.756h-59.003v239.789h52.159l2.124-34.93q5.9 9.205 13.217 16.521 7.552 7.316 16.521 12.509 9.205 5.428 20.297 8.26t24.309 2.832q18.173 0 32.098-6.372 14.161-6.136 23.601-18.409 9.677-12.273 14.161-30.918 4.72-18.409 4.012-42.718zM864.927 553.132l159.073-133.819-159.073-134.055-36.582 37.29 116.826 96.293-116.826 97.001z" />
<glyph unicode="&#xe961;" glyph-name="drag2" d="M170 298.667v86h684v-86h-684zM854 554.667v-86h-684v86h684z" />
<glyph unicode="&#xe962;" glyph-name="control-Checkboxes" d="M384 771.657h614.4c14.157 0 25.6 11.443 25.6 25.6s-11.443 25.6-25.6 25.6h-614.4c-14.131 0-25.6-11.443-25.6-25.6s11.469-25.6 25.6-25.6zM998.4 464.457h-614.4c-14.131 0-25.6-11.443-25.6-25.6s11.469-25.6 25.6-25.6h614.4c14.157 0 25.6 11.443 25.6 25.6s-11.443 25.6-25.6 25.6zM998.4 106.057h-614.4c-38.406-15.539-22.811-37.543 0-51.2h614.4c14.157 0 25.6 11.443 25.6 25.6s-11.443 25.6-25.6 25.6zM0 950.857v-307.2h307.2v307.2zM47.4 903.457h212.4v-212.4h-212.4zM0 234.057v-307.2h307.2v307.2zM47.4 186.657h212.4v-212.4h-212.4zM0 592.457v-307.2h307.2v307.2zM47.4 545.057h212.4v-212.4h-212.4zM89.6 861.257h128v-128h-128v128z" />
<glyph unicode="&#xe9ca;" glyph-name="earth" d="M512 960c-282.77 0-512-229.23-512-512s229.23-512 512-512 512 229.23 512 512-229.23 512-512 512zM512-0.002c-62.958 0-122.872 13.012-177.23 36.452l233.148 262.29c5.206 5.858 8.082 13.422 8.082 21.26v96c0 17.674-14.326 32-32 32-112.99 0-232.204 117.462-233.374 118.626-6 6.002-14.14 9.374-22.626 9.374h-128c-17.672 0-32-14.328-32-32v-192c0-12.122 6.848-23.202 17.69-28.622l110.31-55.156v-187.886c-116.052 80.956-192 215.432-192 367.664 0 68.714 15.49 133.806 43.138 192h116.862c8.488 0 16.626 3.372 22.628 9.372l128 128c6 6.002 9.372 14.14 9.372 22.628v77.412c40.562 12.074 83.518 18.588 128 18.588 70.406 0 137.004-16.26 196.282-45.2-4.144-3.502-8.176-7.164-12.046-11.036-36.266-36.264-56.236-84.478-56.236-135.764s19.97-99.5 56.236-135.764c36.434-36.432 85.218-56.264 135.634-56.26 3.166 0 6.342 0.080 9.518 0.236 13.814-51.802 38.752-186.656-8.404-372.334-0.444-1.744-0.696-3.488-0.842-5.224-81.324-83.080-194.7-134.656-320.142-134.656z" />
<glyph unicode="&#xf00a;" glyph-name="grid" d="M292.571 237.714v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM292.571 530.286v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM658.286 237.714v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM292.571 822.857v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM658.286 530.286v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM1024 237.714v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM658.286 822.857v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM1024 530.286v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM1024 822.857v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857z" />
<glyph unicode="&#xf0c9;" glyph-name="list1" horiz-adv-x="878" d="M877.714 182.857v-73.143c0-20-16.571-36.571-36.571-36.571h-804.571c-20 0-36.571 16.571-36.571 36.571v73.143c0 20 16.571 36.571 36.571 36.571h804.571c20 0 36.571-16.571 36.571-36.571zM877.714 475.428v-73.143c0-20-16.571-36.571-36.571-36.571h-804.571c-20 0-36.571 16.571-36.571 36.571v73.143c0 20 16.571 36.571 36.571 36.571h804.571c20 0 36.571-16.571 36.571-36.571zM877.714 768v-73.143c0-20-16.571-36.571-36.571-36.571h-804.571c-20 0-36.571 16.571-36.571 36.571v73.143c0 20 16.571 36.571 36.571 36.571h804.571c20 0 36.571-16.571 36.571-36.571z" />

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 81 KiB

BIN
src/Squidex/app/theme/icomoon/fonts/icomoon.ttf

Binary file not shown.

BIN
src/Squidex/app/theme/icomoon/fonts/icomoon.woff

Binary file not shown.

68
src/Squidex/app/theme/icomoon/icons/control-Checkboxes.svg

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Layer_1"
x="0px"
y="0px"
width="40px"
height="40px"
viewBox="0 0 40 40"
enable-background="new 0 0 40 40"
xml:space="preserve"
inkscape:version="0.92.3 (2405546, 2018-03-11)"
sodipodi:docname="control-Checkboxes.svg"><metadata
id="metadata13"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs
id="defs11" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="2560"
inkscape:window-height="1017"
id="namedview9"
showgrid="false"
inkscape:zoom="5.9"
inkscape:cx="-77.668252"
inkscape:cy="4.4313497"
inkscape:window-x="1912"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="Layer_1" /><path
d="M 15,7 H 39 C 39.553,7 40,6.553 40,6 40,5.447 39.553,5 39,5 H 15 c -0.552,0 -1,0.447 -1,1 0,0.553 0.448,1 1,1 z M 39,19 H 15 c -0.552,0 -1,0.447 -1,1 0,0.553 0.448,1 1,1 h 24 c 0.553,0 1,-0.447 1,-1 0,-0.553 -0.447,-1 -1,-1 z m 0,14 H 15 c -1.500231,0.607011 -0.891047,1.466536 0,2 h 24 c 0.553,0 1,-0.447 1,-1 0,-0.553 -0.447,-1 -1,-1 z"
id="path7"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="sssssssssssssssccsss" /><path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.85125196;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="M 0,0 V 0.92578125 12 H 12 V 0 Z M 1.8515625,1.8515625 H 10.148438 V 10.148438 H 1.8515625 Z"
id="rect4515"
inkscape:connector-curvature="0" /><path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.85125196;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="M 0,28 V 28.925781 40 H 12 V 28 Z m 1.8515625,1.851562 h 8.2968755 v 8.296876 H 1.8515625 Z"
id="rect4515-1"
inkscape:connector-curvature="0" /><path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.85125196;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="M 0,14 V 14.925781 26 H 12 V 14 Z m 1.8515625,1.851562 h 8.2968755 v 8.296876 H 1.8515625 Z"
id="rect4515-1-3"
inkscape:connector-curvature="0" /><rect
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.52624989;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect4549"
width="5"
height="5"
x="3.4999998"
y="3.4999979" /></svg>

After

Width:  |  Height:  |  Size: 6.4 KiB

2
src/Squidex/app/theme/icomoon/selection.json

File diff suppressed because one or more lines are too long

13
src/Squidex/app/theme/icomoon/style.css

@ -1,10 +1,10 @@
@font-face {
font-family: 'icomoon';
src: url('fonts/icomoon.eot?wza1pz');
src: url('fonts/icomoon.eot?wza1pz#iefix') format('embedded-opentype'),
url('fonts/icomoon.ttf?wza1pz') format('truetype'),
url('fonts/icomoon.woff?wza1pz') format('woff'),
url('fonts/icomoon.svg?wza1pz#icomoon') format('svg');
src: url('fonts/icomoon.eot?l0yskd');
src: url('fonts/icomoon.eot?l0yskd#iefix') format('embedded-opentype'),
url('fonts/icomoon.ttf?l0yskd') format('truetype'),
url('fonts/icomoon.woff?l0yskd') format('woff'),
url('fonts/icomoon.svg?l0yskd#icomoon') format('svg');
font-weight: normal;
font-style: normal;
}
@ -276,6 +276,9 @@
.icon-user:before {
content: "\e928";
}
.icon-control-Checkboxes:before {
content: "\e962";
}
.icon-control-Html:before {
content: "\e960";
}

12
tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/TagsFieldTests.cs

@ -6,6 +6,7 @@
// ==========================================================================
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using FluentAssertions;
@ -102,6 +103,17 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
new[] { "Must have not more than 1 item(s)." });
}
[Fact]
public async Task Should_add_errors_if_value_contains_an_not_allowed_values()
{
var sut = Field(new TagsFieldProperties { AllowedValues = ImmutableList.Create("tag-2", "tag-3") });
await sut.ValidateAsync(CreateValue("tag-1", "tag-2", null), errors);
errors.Should().BeEquivalentTo(
new[] { "[1]: Not an allowed value." });
}
private static JToken CreateValue(params string[] ids)
{
return ids == null ? JValue.CreateNull() : (JToken)new JArray(ids.OfType<object>().ToArray());

28
tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/TagsFieldPropertiesTests.cs

@ -29,5 +29,33 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards.FieldProperties
new ValidationError("Max items must be greater than min items.", "MinItems", "MaxItems")
});
}
[Fact]
public void Should_add_error_if_radio_button_has_no_allowed_values()
{
var sut = new TagsFieldProperties { Editor = TagsFieldEditor.Checkboxes };
var errors = FieldPropertiesValidator.Validate(sut).ToList();
errors.Should().BeEquivalentTo(
new List<ValidationError>
{
new ValidationError("Checkboxes or dropdown list need allowed values.", "AllowedValues")
});
}
[Fact]
public void Should_add_error_if_editor_is_not_valid()
{
var sut = new TagsFieldProperties { Editor = (TagsFieldEditor)123 };
var errors = FieldPropertiesValidator.Validate(sut).ToList();
errors.Should().BeEquivalentTo(
new List<ValidationError>
{
new ValidationError("Editor is not a valid value.", "Editor")
});
}
}
}

Loading…
Cancel
Save