Browse Source

Forms improved.

pull/297/head
Sebastian 8 years ago
parent
commit
ac6cc0ab7a
  1. 1
      src/Squidex/app/features/content/declarations.ts
  2. 2
      src/Squidex/app/features/content/module.ts
  3. 130
      src/Squidex/app/features/content/pages/content/content-field.component.html
  4. 2
      src/Squidex/app/features/content/pages/content/content-page.component.ts
  5. 128
      src/Squidex/app/features/content/shared/array-editor.component.html
  6. 6
      src/Squidex/app/features/content/shared/array-editor.component.ts
  7. 4
      src/Squidex/app/features/content/shared/content-item.component.html
  8. 3
      src/Squidex/app/features/content/shared/content-item.component.ts
  9. 124
      src/Squidex/app/features/content/shared/field-editor.component.html
  10. 14
      src/Squidex/app/features/content/shared/field-editor.component.scss
  11. 39
      src/Squidex/app/features/content/shared/field-editor.component.ts
  12. 3
      src/Squidex/app/features/schemas/pages/schema/field.component.ts
  13. 62
      src/Squidex/app/framework/state.ts
  14. 1
      src/Squidex/app/shared/internal.ts
  15. 683
      src/Squidex/app/shared/services/schemas.service.ts
  16. 315
      src/Squidex/app/shared/services/schemas.types.ts
  17. 105
      src/Squidex/app/shared/state/contents.forms.spec.ts
  18. 399
      src/Squidex/app/shared/state/contents.forms.ts
  19. 2
      src/Squidex/app/shared/state/schemas.forms.ts
  20. 6
      src/Squidex/app/shared/state/schemas.state.ts

1
src/Squidex/app/features/content/declarations.ts

@ -18,4 +18,5 @@ export * from './shared/content-item.component';
export * from './shared/content-status.component';
export * from './shared/contents-selector.component';
export * from './shared/due-time-selector.component';
export * from './shared/field-editor.component';
export * from './shared/references-editor.component';

2
src/Squidex/app/features/content/module.ts

@ -29,6 +29,7 @@ import {
ContentsSelectorComponent,
ContentStatusComponent,
DueTimeSelectorComponent,
FieldEditorComponent,
ReferencesEditorComponent,
SchemasPageComponent,
SearchFormComponent
@ -95,6 +96,7 @@ const routes: Routes = [
ContentsPageComponent,
ContentsSelectorComponent,
DueTimeSelectorComponent,
FieldEditorComponent,
ReferencesEditorComponent,
SchemasPageComponent,
SearchFormComponent

130
src/Squidex/app/features/content/pages/content/content-field.component.html

@ -1,10 +1,4 @@
<div class="table-items-row" [class.invalid]="fieldForm.invalid">
<label>
{{field.displayName}} <span class="field-required" [class.hidden]="!field.properties.isRequired">*</span>
</label>
<span class="field-disabled" *ngIf="field.isDisabled">Disabled</span>
<ng-container *ngIf="field.isLocalizable && languages.length > 1">
<div class="languages-buttons" #buttonLanguages>
<sqx-language-selector size="sm"
@ -19,121 +13,11 @@
</sqx-onboarding-tooltip>
</ng-container>
<sqx-control-errors [for]="selectedFormControl" [fieldName]="field.displayName" [submitted]="form.submitted | async"></sqx-control-errors>
<div>
<ng-container *ngIf="field.properties.editorUrl">
<sqx-iframe-editor [url]="field.properties.editorUrl" [formControl]="selectedFormControl"></sqx-iframe-editor>
</ng-container>
<ng-container *ngIf="!field.properties.editorUrl">
<ng-container [ngSwitch]="field.properties.fieldType">
<ng-container *ngSwitchCase="'Number'">
<ng-container [ngSwitch]="field.properties['editor']">
<ng-container *ngSwitchCase="'Input'">
<input class="form-control" type="number" [formControl]="selectedFormControl" [placeholder]="field.displayPlaceholder" />
</ng-container>
<ng-container *ngSwitchCase="'Stars'">
<sqx-stars [formControl]="selectedFormControl" [maximumStars]="field.properties['maxValue']"></sqx-stars>
</ng-container>
<ng-container *ngSwitchCase="'Dropdown'">
<select class="form-control" [formControl]="selectedFormControl">
<option [ngValue]="null"></option>
<option *ngFor="let value of field.properties['allowedValues']" [ngValue]="value">{{value}}</option>
</select>
</ng-container>
<ng-container *ngSwitchCase="'Radio'">
<ng-container class="form-check form-check-inline" *ngFor="let value of field.properties['allowedValues']">
<input class="form-check-input" type="radio" [value]="value" [formControl]="selectedFormControl" />
<label class="form-check-label">
{{value}}
</label>
</ng-container>
</ng-container>
</ng-container>
</ng-container>
<ng-container *ngSwitchCase="'String'">
<ng-container [ngSwitch]="field.properties['editor']">
<ng-container *ngSwitchCase="'Input'">
<input class="form-control" type="text" [formControl]="selectedFormControl" [placeholder]="field.displayPlaceholder" />
</ng-container>
<ng-container *ngSwitchCase="'Slug'">
<input class="form-control" type="text" [formControl]="selectedFormControl" [placeholder]="field.displayPlaceholder" sqxTransformInput="Slugify" />
</ng-container>
<ng-container *ngSwitchCase="'TextArea'">
<textarea class="form-control" [formControl]="selectedFormControl" rows="5" [placeholder]="field.displayPlaceholder"></textarea>
</ng-container>
<ng-container *ngSwitchCase="'RichText'">
<sqx-rich-editor [formControl]="selectedFormControl"></sqx-rich-editor>
</ng-container>
<ng-container *ngSwitchCase="'Markdown'">
<sqx-markdown-editor [formControl]="selectedFormControl"></sqx-markdown-editor>
</ng-container>
<ng-container *ngSwitchCase="'Dropdown'">
<select class="form-control" [formControl]="selectedFormControl">
<option [ngValue]="null"></option>
<option *ngFor="let value of field.properties['allowedValues']" [ngValue]="value">{{value}}</option>
</select>
</ng-container>
<ng-container *ngSwitchCase="'Radio'">
<ng-container class="form-check form-check-inline" *ngFor="let value of field.properties['allowedValues']">
<input class="form-check-input" type="radio" value="{{value}}" [formControl]="selectedFormControl" />
<label class="form-check-label">
{{value}}
</label>
</ng-container>
</ng-container>
</ng-container>
</ng-container>
<ng-container *ngSwitchCase="'Boolean'">
<ng-container [ngSwitch]="field.properties['editor']">
<ng-container *ngSwitchCase="'Toggle'">
<sqx-toggle [formControl]="selectedFormControl"></sqx-toggle>
</ng-container>
<ng-container *ngSwitchCase="'Checkbox'">
<input type="checkbox" [formControl]="selectedFormControl" sqxIndeterminateValue />
</ng-container>
</ng-container>
</ng-container>
<ng-container *ngSwitchCase="'DateTime'">
<sqx-date-time-editor enforceTime="true" [mode]="field.properties['editor']" [formControl]="selectedFormControl"></sqx-date-time-editor>
</ng-container>
<ng-container *ngSwitchCase="'Geolocation'">
<sqx-geolocation-editor [formControl]="selectedFormControl"></sqx-geolocation-editor>
</ng-container>
<ng-container *ngSwitchCase="'Json'">
<sqx-json-editor [formControl]="selectedFormControl"></sqx-json-editor>
</ng-container>
<ng-container *ngSwitchCase="'Assets'">
<sqx-assets-editor [formControl]="selectedFormControl"></sqx-assets-editor>
</ng-container>
<ng-container *ngSwitchCase="'Tags'">
<sqx-tag-editor [formControl]="selectedFormControl"></sqx-tag-editor>
</ng-container>
<ng-container *ngSwitchCase="'Array'">
<sqx-array-editor
[arrayControl]="selectedFormControl"
[form]="form"
[field]="field"
[language]="language"
[languages]="languages">
</sqx-array-editor>
</ng-container>
<ng-container *ngSwitchCase="'References'">
<sqx-references-editor
[formControl]="selectedFormControl"
[language]="language"
[languages]="languages"
[schemaId]="field.properties['schemaId']">
</sqx-references-editor>
</ng-container>
</ng-container>
</ng-container>
</div>
<ng-container *ngIf="field.properties['hints']; let hints">
<small class="form-text text-muted" *ngIf="hints.length > 0">
{{hints}}
</small>
</ng-container>
<sqx-field-editor
[form]="form"
[field]="field"
[language]="language"
[languages]="languages"
[control]="selectedFormControl">
</sqx-field-editor>
</div>

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

@ -171,7 +171,7 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy,
}
private loadContent(data: any) {
this.contentForm.loadData(data, this.content && this.content.status === 'Archived');
this.contentForm.loadContent(data, this.content && this.content.status === 'Archived');
}
public discardChanges() {

128
src/Squidex/app/features/content/shared/array-editor.component.html

@ -1,133 +1,23 @@
<div class="array-container" *ngIf="arrayControl.controls.length > 0">
<div class="array-item" *ngFor="let nestedForm of arrayControl.controls; let i = index">
<button type="button" class="btn btn-link btn-danger array-item-remove" (click)="removeItem(i)">
<button type="button" class="btn btn-link btn-danger array-item-remove" (click)="removeItem(i); $event.preventDefault()">
<i class="icon-bin2"></i>
</button>
<div class="form-group" *ngFor="let nestedField of field.nested">
<ng-container *ngIf="nestedForm.get(nestedField.name); let nestedFieldForm">
<label>
{{nestedField.displayName}} <span class="field-required" [class.hidden]="!nestedField.properties.isRequired">*</span>
</label>
<span class="field-disabled" *ngIf="nestedField.isDisabled">Disabled</span>
<sqx-control-errors [for]="nestedFieldForm" [fieldName]="nestedField.displayName" [submitted]="form.submitted | async"></sqx-control-errors>
<div>
<ng-container *ngIf="nestedField.properties.editorUrl">
<sqx-iframe-editor [url]="nestedField.properties.editorUrl" [formControl]="nestedFieldForm"></sqx-iframe-editor>
</ng-container>
<ng-container *ngIf="!nestedField.properties.editorUrl">
<ng-container [ngSwitch]="nestedField.properties.fieldType">
<ng-container *ngSwitchCase="'Number'">
<ng-container [ngSwitch]="nestedField.properties['editor']">
<ng-container *ngSwitchCase="'Input'">
<input class="form-control" type="number" [formControl]="nestedFieldForm" [placeholder]="nestedField.displayPlaceholder" />
</ng-container>
<ng-container *ngSwitchCase="'Stars'">
<sqx-stars [formControl]="nestedFieldForm" [maximumStars]="nestedField.properties['maxValue']"></sqx-stars>
</ng-container>
<ng-container *ngSwitchCase="'Dropdown'">
<select class="form-control" [formControl]="nestedFieldForm">
<option [ngValue]="null"></option>
<option *ngFor="let value of nestedField.properties['allowedValues']" [ngValue]="value">{{value}}</option>
</select>
</ng-container>
<ng-container *ngSwitchCase="'Radio'">
<ng-container class="form-check form-check-inline" *ngFor="let value of nestedField.properties['allowedValues']">
<input class="form-check-input" type="radio" [value]="value" [formControl]="nestedFieldForm" />
<label class="form-check-label">
{{value}}
</label>
</ng-container>
</ng-container>
</ng-container>
</ng-container>
<ng-container *ngSwitchCase="'String'">
<ng-container [ngSwitch]="nestedField.properties['editor']">
<ng-container *ngSwitchCase="'Input'">
<input class="form-control" type="text" [formControl]="nestedFieldForm" [placeholder]="nestedField.displayPlaceholder" />
</ng-container>
<ng-container *ngSwitchCase="'Slug'">
<input class="form-control" type="text" [formControl]="nestedFieldForm" [placeholder]="nestedField.displayPlaceholder" sqxTransformInput="Slugify" />
</ng-container>
<ng-container *ngSwitchCase="'TextArea'">
<textarea class="form-control" [formControl]="nestedFieldForm" rows="5" [placeholder]="nestedField.displayPlaceholder"></textarea>
</ng-container>
<ng-container *ngSwitchCase="'RichText'">
<sqx-rich-editor [formControl]="nestedFieldForm"></sqx-rich-editor>
</ng-container>
<ng-container *ngSwitchCase="'Markdown'">
<sqx-markdown-editor [formControl]="nestedFieldForm"></sqx-markdown-editor>
</ng-container>
<ng-container *ngSwitchCase="'Dropdown'">
<select class="form-control" [formControl]="nestedFieldForm">
<option [ngValue]="null"></option>
<option *ngFor="let value of nestedField.properties['allowedValues']" [ngValue]="value">{{value}}</option>
</select>
</ng-container>
<ng-container *ngSwitchCase="'Radio'">
<ng-container class="form-check form-check-inline" *ngFor="let value of nestedField.properties['allowedValues']">
<input class="form-check-input" type="radio" value="{{value}}" [formControl]="nestedFieldForm" />
<label class="form-check-label">
{{value}}
</label>
</ng-container>
</ng-container>
</ng-container>
</ng-container>
<ng-container *ngSwitchCase="'Boolean'">
<ng-container [ngSwitch]="nestedField.properties['editor']">
<ng-container *ngSwitchCase="'Toggle'">
<sqx-toggle [formControl]="nestedFieldForm"></sqx-toggle>
</ng-container>
<ng-container *ngSwitchCase="'Checkbox'">
<input type="checkbox" [formControl]="nestedFieldForm" sqxIndeterminateValue />
</ng-container>
</ng-container>
</ng-container>
<ng-container *ngSwitchCase="'DateTime'">
<sqx-date-time-editor enforceTime="true" [mode]="nestedField.properties['editor']" [formControl]="nestedFieldForm"></sqx-date-time-editor>
</ng-container>
<ng-container *ngSwitchCase="'Geolocation'">
<sqx-geolocation-editor [formControl]="nestedFieldForm"></sqx-geolocation-editor>
</ng-container>
<ng-container *ngSwitchCase="'Json'">
<sqx-json-editor [formControl]="nestedFieldForm"></sqx-json-editor>
</ng-container>
<ng-container *ngSwitchCase="'Assets'">
<sqx-assets-editor [formControl]="nestedFieldForm"></sqx-assets-editor>
</ng-container>
<ng-container *ngSwitchCase="'Tags'">
<sqx-tag-editor [formControl]="nestedFieldForm"></sqx-tag-editor>
</ng-container>
<ng-container *ngSwitchCase="'Array'">
<sqx-array-editor [formControl]="nestedFieldForm" [field]="field"></sqx-array-editor>
</ng-container>
<ng-container *ngSwitchCase="'References'">
<sqx-references-editor
[formControl]="nestedFieldForm"
[language]="language"
[languages]="languages"
[schemaId]="nestedField.properties['schemaId']">
</sqx-references-editor>
</ng-container>
</ng-container>
</ng-container>
</div>
<ng-container *ngIf="nestedField.properties['hints']; let hints">
<small class="form-text text-muted" *ngIf="hints.length > 0">
{{hints}}
</small>
</ng-container>
<sqx-field-editor
[form]="form"
[field]="nestedField"
[language]="language"
[languages]="languages"
[control]="nestedFieldForm">
</sqx-field-editor>
</ng-container>
</div>
</div>
</div>
<button class="btn btn-success" (click)="addItem()">
<button class="btn btn-success" (click)="addItem(); $event.preventDefault()">
Add Item
</button>

6
src/Squidex/app/features/content/shared/array-editor.component.ts

@ -5,8 +5,6 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
// tslint:disable:prefer-for-of
import { Component, Input } from '@angular/core';
import { FormArray } from '@angular/forms';
@ -40,13 +38,9 @@ export class ArrayEditorComponent {
public removeItem(index: number) {
this.form.removeArrayItem(this.field, this.language, index);
return false;
}
public addItem() {
this.form.insertArrayItem(this.field, this.language);
return false;
}
}

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

@ -6,7 +6,7 @@
</td>
<td class="cell-auto" *ngFor="let field of schema.listFields; let i = index" (click)="shouldStop($event)">
<div *ngIf="field.properties['inlineEditable'] && !isReadOnly" [formGroup]="patchForm.form" (click)="$event.stopPropagation()">
<div *ngIf="field.isInlineEditable && !isReadOnly" [formGroup]="patchForm.form" (click)="$event.stopPropagation()">
<div [ngSwitch]="field.properties.fieldType">
<div *ngSwitchCase="'Number'">
<div [ngSwitch]="field.properties['editor']">
@ -51,7 +51,7 @@
</div>
</div>
</div>
<div *ngIf="!field.properties['inlineEditable'] || isReadOnly" class="truncate">
<div *ngIf="!field.isInlineEditable || isReadOnly" class="truncate">
{{values[i]}}
</div>
</td>

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

@ -12,6 +12,7 @@ import {
ContentDto,
ContentsState,
fadeAnimation,
FieldFormatter,
fieldInvariant,
ModalView,
PatchContentForm,
@ -123,7 +124,7 @@ export class ContentItemComponent implements OnChanges {
if (Types.isUndefined(value)) {
this.values.push('');
} else {
this.values.push(field.formatValue(value));
this.values.push(FieldFormatter.format(field, value));
}
if (this.patchForm) {

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

@ -0,0 +1,124 @@
<ng-container *ngIf="field">
<label>
{{field.displayName}} <span class="field-required" [class.hidden]="!field.properties.isRequired">*</span>
</label>
<span class="field-disabled" *ngIf="field.isDisabled">Disabled</span>
<sqx-control-errors [for]="control" [fieldName]="field.displayName" [submitted]="form.submitted | async"></sqx-control-errors>
<div>
<ng-container *ngIf="field.properties.editorUrl; else noEditor">
<sqx-iframe-editor [url]="field.properties.editorUrl" [formControl]="control"></sqx-iframe-editor>
</ng-container>
<ng-template #noEditor>
<ng-container [ngSwitch]="field.properties.fieldType">
<ng-container *ngSwitchCase="'Number'">
<ng-container [ngSwitch]="field.properties['editor']">
<ng-container *ngSwitchCase="'Input'">
<input class="form-control" type="number" [formControl]="control" [placeholder]="field.displayPlaceholder" />
</ng-container>
<ng-container *ngSwitchCase="'Stars'">
<sqx-stars [formControl]="control" [maximumStars]="field.properties['maxValue']"></sqx-stars>
</ng-container>
<ng-container *ngSwitchCase="'Dropdown'">
<select class="form-control" [formControl]="control">
<option [ngValue]="null"></option>
<option *ngFor="let value of field.properties['allowedValues']" [ngValue]="value">{{value}}</option>
</select>
</ng-container>
<ng-container *ngSwitchCase="'Radio'">
<ng-container class="form-check form-check-inline" *ngFor="let value of field.properties['allowedValues']">
<input class="form-check-input" type="radio" [value]="value" [formControl]="control" />
<label class="form-check-label">
{{value}}
</label>
</ng-container>
</ng-container>
</ng-container>
</ng-container>
<ng-container *ngSwitchCase="'String'">
<ng-container [ngSwitch]="field.properties['editor']">
<ng-container *ngSwitchCase="'Input'">
<input class="form-control" type="text" [formControl]="control" [placeholder]="field.displayPlaceholder" />
</ng-container>
<ng-container *ngSwitchCase="'Slug'">
<input class="form-control" type="text" [formControl]="control" [placeholder]="field.displayPlaceholder" sqxTransformInput="Slugify" />
</ng-container>
<ng-container *ngSwitchCase="'TextArea'">
<textarea class="form-control" [formControl]="control" rows="5" [placeholder]="field.displayPlaceholder"></textarea>
</ng-container>
<ng-container *ngSwitchCase="'RichText'">
<sqx-rich-editor [formControl]="control"></sqx-rich-editor>
</ng-container>
<ng-container *ngSwitchCase="'Markdown'">
<sqx-markdown-editor [formControl]="control"></sqx-markdown-editor>
</ng-container>
<ng-container *ngSwitchCase="'Dropdown'">
<select class="form-control" [formControl]="control">
<option [ngValue]="null"></option>
<option *ngFor="let value of field.properties['allowedValues']" [ngValue]="value">{{value}}</option>
</select>
</ng-container>
<ng-container *ngSwitchCase="'Radio'">
<ng-container class="form-check form-check-inline" *ngFor="let value of field.properties['allowedValues']">
<input class="form-check-input" type="radio" value="{{value}}" [formControl]="control" />
<label class="form-check-label">
{{value}}
</label>
</ng-container>
</ng-container>
</ng-container>
</ng-container>
<ng-container *ngSwitchCase="'Boolean'">
<ng-container [ngSwitch]="field.properties['editor']">
<ng-container *ngSwitchCase="'Toggle'">
<sqx-toggle [formControl]="control"></sqx-toggle>
</ng-container>
<ng-container *ngSwitchCase="'Checkbox'">
<input type="checkbox" [formControl]="control" sqxIndeterminateValue />
</ng-container>
</ng-container>
</ng-container>
<ng-container *ngSwitchCase="'DateTime'">
<sqx-date-time-editor enforceTime="true" [mode]="field.properties['editor']" [formControl]="control"></sqx-date-time-editor>
</ng-container>
<ng-container *ngSwitchCase="'Geolocation'">
<sqx-geolocation-editor [formControl]="control"></sqx-geolocation-editor>
</ng-container>
<ng-container *ngSwitchCase="'Json'">
<sqx-json-editor [formControl]="control"></sqx-json-editor>
</ng-container>
<ng-container *ngSwitchCase="'Assets'">
<sqx-assets-editor [formControl]="control"></sqx-assets-editor>
</ng-container>
<ng-container *ngSwitchCase="'Tags'">
<sqx-tag-editor [formControl]="control"></sqx-tag-editor>
</ng-container>
<ng-container *ngSwitchCase="'Array'">
<sqx-array-editor
[arrayControl]="control"
[form]="form"
[field]="field"
[language]="language"
[languages]="languages">
</sqx-array-editor>
</ng-container>
<ng-container *ngSwitchCase="'References'">
<sqx-references-editor
[formControl]="control"
[language]="language"
[languages]="languages"
[schemaId]="field.properties['schemaId']">
</sqx-references-editor>
</ng-container>
</ng-container>
</ng-template>
</div>
<ng-container *ngIf="field.properties.hints; let hints">
<small class="form-text text-muted" *ngIf="hints.length > 0">
{{hints}}
</small>
</ng-container>

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

@ -0,0 +1,14 @@
@import '_vars';
@import '_mixins';
.field {
&-required {
color: $color-theme-error;
}
&-disabled {
color: $color-border-dark;
font-size: .8rem;
font-weight: normal;
}
}

39
src/Squidex/app/features/content/shared/field-editor.component.ts

@ -0,0 +1,39 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { FormControl } from '@angular/forms';
import {
AppLanguageDto,
EditContentForm,
FieldDto,
ImmutableArray
} from '@app/shared';
@Component({
selector: 'sqx-field-editor',
styleUrls: ['./field-editor.component.scss'],
templateUrl: './field-editor.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class FieldEditorComponent {
@Input()
public form: EditContentForm;
@Input()
public field: FieldDto;
@Input()
public control: FormControl;
@Input()
public language: AppLanguageDto;
@Input()
public languages: ImmutableArray<AppLanguageDto>;
}

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

@ -9,7 +9,6 @@ import { Component, Input, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import {
AnyFieldDto,
AppPatternDto,
createProperties,
EditFieldForm,
@ -33,7 +32,7 @@ import {
})
export class FieldComponent implements OnInit {
@Input()
public field: AnyFieldDto;
public field: NestedFieldDto | RootFieldDto;
@Input()
public schema: SchemaDetailsDto;

62
src/Squidex/app/framework/state.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { AbstractControl } from '@angular/forms';
import { AbstractControl, FormArray, FormGroup } from '@angular/forms';
import { BehaviorSubject, Observable } from 'rxjs';
import { ErrorDto } from './utils/error';
@ -17,6 +17,34 @@ export interface FormState {
error?: string;
}
export class Lazy<T> {
private valueSet = false;
private valueField: T;
public get value(): T {
if (!this.valueSet) {
this.valueField = this.factory();
this.valueSet = true;
}
return this.valueField;
}
constructor(
private readonly factory: () => T
) {
}
}
export const formControls = (form: AbstractControl): AbstractControl[] => {
if (Types.is(form, FormGroup)) {
return Object.values(form.controls);
} else if (Types.is(form, FormArray)) {
return form.controls;
} else {
return [];
}
};
export class Form<T extends AbstractControl> {
private readonly state = new State<FormState>({ submitted: false });
@ -31,10 +59,26 @@ export class Form<T extends AbstractControl> {
) {
}
protected disable() {
this.form.disable();
}
protected enable() {
this.form.enable();
}
protected reset(value: any) {
this.form.reset(value);
}
protected setValue(value: any) {
this.form.reset(value, { emitEvent: true });
}
public load(value: any) {
this.state.next({ submitted: false, error: null });
this.form.reset(value, { emitEvent: true });
this.setValue(value);
}
public submit(): any | null {
@ -43,7 +87,7 @@ export class Form<T extends AbstractControl> {
if (this.form.valid) {
const value = this.form.value;
this.form.disable();
this.disable();
return value;
} else {
@ -54,10 +98,10 @@ export class Form<T extends AbstractControl> {
public submitCompleted(newValue?: any) {
this.state.next({ submitted: false, error: null });
this.form.enable();
this.enable();
if (newValue) {
this.form.reset(newValue);
this.reset(newValue);
} else {
this.form.markAsPristine();
}
@ -66,7 +110,7 @@ export class Form<T extends AbstractControl> {
public submitFailed(error?: string | ErrorDto) {
this.state.next({ submitted: false, error: this.getError(error) });
this.form.enable();
this.enable();
}
private getError(error?: string | ErrorDto) {
@ -79,10 +123,6 @@ export class Form<T extends AbstractControl> {
}
export class Model {
protected onCreated() {
return;
}
protected clone(update: ((v: any) => object) | object): any {
let values: object;
if (Types.isFunction(update)) {
@ -93,8 +133,6 @@ export class Model {
const clone = Object.assign(Object.create(Object.getPrototypeOf(this)), this, values);
clone.onCreated();
return clone;
}
}

1
src/Squidex/app/shared/internal.ts

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

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

@ -7,7 +7,6 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { ValidatorFn, Validators } from '@angular/forms';
import { Observable } from 'rxjs';
import '@app/framework/angular/http/http-extensions';
@ -17,96 +16,21 @@ import {
ApiUrlConfig,
DateTime,
HTTP,
Lazy,
Model,
StringHelper,
ValidatorsEx,
Version,
Versioned
} from '@app/framework';
export const fieldTypes = [
{
type: 'String',
description: 'Titles, names, paragraphs.'
}, {
type: 'Assets',
description: 'Images, videos, documents.'
}, {
type: 'Boolean',
description: 'Yes or no, true or false.'
}, {
type: 'DateTime',
description: 'Events date, opening hours.'
}, {
type: 'Geolocation',
description: 'Coordinates: latitude and longitude.'
}, {
type: 'Json',
description: 'Data in JSON format, for developers.'
}, {
type: 'Number',
description: 'ID, order number, rating, quantity.'
}, {
type: 'References',
description: 'Links to other content items.'
}, {
type: 'Tags',
description: 'Special format for tags.'
}, {
type: 'Array',
description: 'List of embedded objects.'
}
];
export const fieldInvariant = 'iv';
export function createProperties(fieldType: string, values: Object | null = null): FieldPropertiesDto {
let properties: FieldPropertiesDto;
switch (fieldType) {
case 'Array':
properties = new ArrayFieldPropertiesDto();
break;
case 'Assets':
properties = new AssetsFieldPropertiesDto();
break;
case 'Boolean':
properties = new BooleanFieldPropertiesDto('Checkbox');
break;
case 'DateTime':
properties = new DateTimeFieldPropertiesDto('DateTime');
break;
case 'Geolocation':
properties = new GeolocationFieldPropertiesDto();
break;
case 'Json':
properties = new JsonFieldPropertiesDto();
break;
case 'Number':
properties = new NumberFieldPropertiesDto('Input');
break;
case 'References':
properties = new ReferencesFieldPropertiesDto();
break;
case 'String':
properties = new StringFieldPropertiesDto('Input');
break;
case 'Tags':
properties = new TagsFieldPropertiesDto();
break;
default:
throw 'Invalid properties type';
}
if (values) {
Object.assign(properties, values);
}
return properties;
}
import { createProperties, FieldPropertiesDto } from './schemas.types';
export class SchemaDto extends Model {
public displayName: string;
private readonly displayNameValue = new Lazy((() => StringHelper.firstNonEmpty(this.properties.label, this.name)));
public get displayName() {
return this.displayNameValue.value;
}
constructor(
public readonly id: string,
@ -121,12 +45,6 @@ export class SchemaDto extends Model {
public readonly version: Version
) {
super();
this.onCreated();
}
public onCreated() {
this.displayName = StringHelper.firstNonEmpty(this.properties.label, this.name);
}
public with(value: Partial<SchemaDto>): SchemaDto {
@ -135,7 +53,28 @@ export class SchemaDto extends Model {
}
export class SchemaDetailsDto extends SchemaDto {
public listFields: RootFieldDto[];
private inlineEditableFieldsValue = new Lazy(() => this.listFields.filter(x => x.isInlineEditable));
private listFieldsValue = new Lazy(() => {
let fields = this.fields.filter(x => x.properties.isListField);
if (fields.length === 0 && this.fields.length > 0) {
fields = [this.fields[0]];
}
if (fields.length === 0) {
fields = [<any>{ properties: {} }];
}
return fields;
});
public get inlineEditableFields() {
return this.inlineEditableFieldsValue.value;
}
public get listFields() {
return this.listFieldsValue.value;
}
constructor(id: string, name: string, category: string, properties: SchemaPropertiesDto, isPublished: boolean, created: DateTime, createdBy: string, lastModified: DateTime, lastModifiedBy: string, version: Version,
public readonly fields: RootFieldDto[],
@ -146,24 +85,6 @@ export class SchemaDetailsDto extends SchemaDto {
public readonly scriptChange?: string
) {
super(id, name, category, properties, isPublished, created, createdBy, lastModified, lastModifiedBy, version);
this.onCreated();
}
public onCreated() {
super.onCreated();
if (this.fields) {
this.listFields = this.fields.filter(x => x.properties.isListField);
if (this.listFields.length === 0 && this.fields.length > 0) {
this.listFields = [this.fields[0]];
}
if (this.listFields.length === 0) {
this.listFields = [<any>{ properties: {} }];
}
}
}
public with(value: Partial<SchemaDetailsDto>): SchemaDetailsDto {
@ -172,8 +93,17 @@ export class SchemaDetailsDto extends SchemaDto {
}
export class FieldDto extends Model {
public displayName: string;
public displayPlaceholder: string;
public get isInlineEditable(): boolean {
return !this.isDisabled && this.properties['inlineEditable'] === true;
}
public get displayName() {
return StringHelper.firstNonEmpty(this.properties.label, this.name);
}
public get displayPlaceholder() {
return this.properties.placeholder || '';
}
constructor(
public readonly fieldId: number,
@ -186,30 +116,19 @@ export class FieldDto extends Model {
super();
}
public onCreated() {
this.displayName = StringHelper.firstNonEmpty(this.properties.label, this.name);
this.displayPlaceholder = this.properties.placeholder || '';
}
public formatValue(value: any): string {
return this.properties.formatValue(value);
}
public createValidators(isOptional: boolean): ValidatorFn[] {
return this.properties.createValidators(isOptional);
}
public defaultValue(): any {
return this.properties.getDefaultValue();
}
public with(value: Partial<FieldDto>): FieldDto {
return this.clone(value);
}
}
export class RootFieldDto extends FieldDto {
public readonly isLocalizable = this.partitioning === 'language';
public get isLocalizable() {
return this.partitioning === 'language';
}
public get isArray() {
return this.properties.fieldType === 'Array';
}
constructor(fieldId: number, name: string, properties: FieldPropertiesDto,
public readonly partitioning: string,
@ -219,8 +138,6 @@ export class RootFieldDto extends FieldDto {
public readonly nested: NestedFieldDto[] = []
) {
super(fieldId, name, properties, isLocked, isHidden, isDisabled);
this.onCreated();
}
public with(value: Partial<RootFieldDto>): RootFieldDto {
@ -236,8 +153,6 @@ export class NestedFieldDto extends FieldDto {
isDisabled: boolean = false
) {
super(fieldId, name, properties, isLocked, isHidden, isDisabled);
this.onCreated();
}
public with(value: Partial<NestedFieldDto>): NestedFieldDto {
@ -245,482 +160,6 @@ export class NestedFieldDto extends FieldDto {
}
}
export type AnyFieldDto = RootFieldDto | NestedFieldDto;
export abstract class FieldPropertiesDto {
public abstract fieldType: string;
public readonly editorUrl?: string;
public readonly label?: string;
public readonly hints?: string;
public readonly placeholder?: string;
public readonly isRequired: boolean = false;
public readonly isListField: boolean = false;
constructor(public readonly editor: string,
props?: Partial<FieldPropertiesDto>
) {
if (props) {
Object.assign(this, props);
}
}
public abstract formatValue(value: any): string;
public abstract createValidators(isOptional: boolean): ValidatorFn[];
public getDefaultValue(): any {
return null;
}
}
export class ArrayFieldPropertiesDto extends FieldPropertiesDto {
public readonly fieldType = 'Array';
public readonly minItems?: number;
public readonly maxItems?: number;
constructor(
props?: Partial<ArrayFieldPropertiesDto>
) {
super('Default', props);
}
public formatValue(value: any): string {
if (!value) {
return '';
}
if (value.length) {
return `${value.length} Items(s)`;
} else {
return '0 Items';
}
}
public createValidators(isOptional: boolean): ValidatorFn[] {
const validators: ValidatorFn[] = [];
if (this.isRequired && !isOptional) {
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 {
public readonly fieldType = 'Assets';
public readonly minItems?: number;
public readonly maxItems?: number;
public readonly minSize?: number;
public readonly maxSize?: number;
public readonly allowedExtensions?: string[];
public readonly mustBeImage?: boolean;
public readonly minWidth?: number;
public readonly maxWidth?: number;
public readonly minHeight?: number;
public readonly maxHeight?: number;
public readonly aspectWidth?: number;
public readonly aspectHeight?: number;
constructor(
props?: Partial<AssetsFieldPropertiesDto>
) {
super('Default', props);
}
public formatValue(value: any): string {
if (!value) {
return '';
}
if (value.length) {
return `${value.length} Asset(s)`;
} else {
return '0 Assets';
}
}
public createValidators(isOptional: boolean): ValidatorFn[] {
const validators: ValidatorFn[] = [];
if (this.isRequired && !isOptional) {
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 BooleanFieldPropertiesDto extends FieldPropertiesDto {
public readonly fieldType = 'Boolean';
public readonly inlineEditable: boolean = false;
public readonly defaultValue?: boolean;
constructor(editor: string,
props?: Partial<BooleanFieldPropertiesDto>
) {
super(editor, props);
}
public formatValue(value: any): string {
if (value === null || value === undefined) {
return '';
}
return value ? 'Yes' : 'No';
}
public createValidators(isOptional: boolean): ValidatorFn[] {
const validators: ValidatorFn[] = [];
if (this.isRequired && !isOptional) {
validators.push(Validators.required);
}
return validators;
}
public getDefaultValue(): any {
return this.defaultValue;
}
}
export class DateTimeFieldPropertiesDto extends FieldPropertiesDto {
public readonly fieldType = 'DateTime';
public readonly defaultValue?: string;
public readonly maxValue?: string;
public readonly minValue?: string;
public readonly calculatedDefaultValue?: string;
constructor(editor: string,
props?: Partial<DateTimeFieldPropertiesDto>
) {
super(editor, props);
}
public formatValue(value: any): string {
if (!value) {
return '';
}
try {
const parsed = DateTime.parseISO_UTC(value);
if (this.editor === 'Date') {
return parsed.toUTCStringFormat('YYYY-MM-DD');
} else {
return parsed.toUTCStringFormat('YYYY-MM-DD HH:mm:ss');
}
} catch (ex) {
return value;
}
}
public createValidators(isOptional: boolean): ValidatorFn[] {
const validators: ValidatorFn[] = [];
if (this.isRequired && !isOptional) {
validators.push(Validators.required);
}
return validators;
}
public getDefaultValue(now?: DateTime): any {
now = now || DateTime.now();
if (this.calculatedDefaultValue === 'Now') {
return now.toUTCStringFormat('YYYY-MM-DDTHH:mm:ss') + 'Z';
} else if (this.calculatedDefaultValue === 'Today') {
return now.toUTCStringFormat('YYYY-MM-DD');
} else {
return this.defaultValue;
}
}
}
export class GeolocationFieldPropertiesDto extends FieldPropertiesDto {
public readonly fieldType = 'Geolocation';
constructor(
props?: Partial<GeolocationFieldPropertiesDto>
) {
super('Default', props);
}
public formatValue(value: any): string {
if (!value) {
return '';
}
return `${value.longitude}, ${value.latitude}`;
}
public createValidators(isOptional: boolean): ValidatorFn[] {
const validators: ValidatorFn[] = [];
if (this.isRequired && !isOptional) {
validators.push(Validators.required);
}
return validators;
}
}
export class JsonFieldPropertiesDto extends FieldPropertiesDto {
public readonly fieldType = 'Json';
constructor(
props?: Partial<JsonFieldPropertiesDto>
) {
super('Default', props);
}
public formatValue(value: any): string {
if (!value) {
return '';
}
return '<Json />';
}
public createValidators(isOptional: boolean): ValidatorFn[] {
const validators: ValidatorFn[] = [];
if (this.isRequired && !isOptional) {
validators.push(Validators.required);
}
return validators;
}
}
export class NumberFieldPropertiesDto extends FieldPropertiesDto {
public readonly fieldType = 'Number';
public readonly inlineEditable: boolean = false;
public readonly defaultValue?: number;
public readonly maxValue?: number;
public readonly minValue?: number;
public readonly allowedValues?: number[];
constructor(editor: string,
props?: Partial<NumberFieldPropertiesDto>
) {
super(editor, props);
}
public formatValue(value: any): string {
if (!value) {
return '';
}
return value;
}
public createValidators(isOptional: boolean): ValidatorFn[] {
const validators: ValidatorFn[] = [];
if (this.isRequired && !isOptional) {
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) {
const values: (number | null)[] = this.allowedValues;
if (this.isRequired && !isOptional) {
validators.push(ValidatorsEx.validValues(values));
} else {
validators.push(ValidatorsEx.validValues(values.concat([null])));
}
}
return validators;
}
public getDefaultValue(): any {
return this.defaultValue;
}
}
export class ReferencesFieldPropertiesDto extends FieldPropertiesDto {
public readonly fieldType = 'References';
public readonly minItems?: number;
public readonly maxItems?: number;
public readonly schemaId?: string;
constructor(
props?: Partial<ReferencesFieldPropertiesDto>
) {
super('Default', props);
}
public formatValue(value: any): string {
if (!value) {
return '';
}
if (value.length) {
return `${value.length} Reference(s)`;
} else {
return '0 References';
}
}
public createValidators(isOptional: boolean): ValidatorFn[] {
const validators: ValidatorFn[] = [];
if (this.isRequired && !isOptional) {
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 StringFieldPropertiesDto extends FieldPropertiesDto {
public readonly fieldType = 'String';
public readonly inlineEditable = false;
public readonly defaultValue?: string;
public readonly pattern?: string;
public readonly patternMessage?: string;
public readonly minLength?: number;
public readonly maxLength?: number;
public readonly allowedValues?: string[];
constructor(editor: string,
props?: Partial<StringFieldPropertiesDto>
) {
super(editor, props);
}
public formatValue(value: any): string {
if (!value) {
return '';
}
return value;
}
public createValidators(isOptional: false): ValidatorFn[] {
const validators: ValidatorFn[] = [];
if (this.isRequired && !isOptional) {
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) {
const values: (string | null)[] = this.allowedValues;
if (this.isRequired && !isOptional) {
validators.push(ValidatorsEx.validValues(values));
} else {
validators.push(ValidatorsEx.validValues(values.concat([null])));
}
}
return validators;
}
public getDefaultValue(): any {
return this.defaultValue;
}
}
export class TagsFieldPropertiesDto extends FieldPropertiesDto {
public readonly fieldType = 'Tags';
public readonly minItems?: number;
public readonly maxItems?: number;
constructor(
props?: Partial<TagsFieldPropertiesDto>
) {
super('Default', props);
}
public formatValue(value: any): string {
if (!value) {
return '';
}
if (value.length) {
return value.join(', ');
} else {
return '';
}
}
public createValidators(isOptional: boolean): ValidatorFn[] {
const validators: ValidatorFn[] = [];
if (this.isRequired && !isOptional) {
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 SchemaPropertiesDto {
constructor(
public readonly label?: string,
@ -729,26 +168,27 @@ export class SchemaPropertiesDto {
}
}
export class UpdateSchemaDto {
export class AddFieldDto {
constructor(
public readonly label?: string,
public readonly hints?: string
public readonly name: string,
public readonly partitioning: string,
public readonly properties: FieldPropertiesDto
) {
}
}
export class UpdateSchemaCategoryDto {
export class CreateSchemaDto {
constructor(
public readonly name?: string
public readonly name: string,
public readonly fields?: RootFieldDto[],
public readonly properties?: SchemaPropertiesDto
) {
}
}
export class AddFieldDto {
export class UpdateSchemaCategoryDto {
constructor(
public readonly name: string,
public readonly partitioning: string,
public readonly properties: FieldPropertiesDto
public readonly name?: string
) {
}
}
@ -760,11 +200,10 @@ export class UpdateFieldDto {
}
}
export class CreateSchemaDto {
export class UpdateSchemaDto {
constructor(
public readonly name: string,
public readonly fields?: RootFieldDto[],
public readonly properties?: SchemaPropertiesDto
public readonly label?: string,
public readonly hints?: string
) {
}
}
@ -970,7 +409,7 @@ export class SchemasService {
.pretifyError('Failed to change category. Please reload.');
}
public postField(appName: string, schemaName: string, dto: AddFieldDto, parentId: number | undefined, version: Version): Observable<Versioned<AnyFieldDto>> {
public postField(appName: string, schemaName: string, dto: AddFieldDto, parentId: number | undefined, version: Version): Observable<Versioned<RootFieldDto | NestedFieldDto>> {
const url = this.buildUrl(appName, schemaName, parentId, '');
return HTTP.postVersioned<any>(this.http, url, dto, version)

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

@ -0,0 +1,315 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
export const fieldTypes = [
{
type: 'String',
description: 'Titles, names, paragraphs.'
}, {
type: 'Assets',
description: 'Images, videos, documents.'
}, {
type: 'Boolean',
description: 'Yes or no, true or false.'
}, {
type: 'DateTime',
description: 'Events date, opening hours.'
}, {
type: 'Geolocation',
description: 'Coordinates: latitude and longitude.'
}, {
type: 'Json',
description: 'Data in JSON format, for developers.'
}, {
type: 'Number',
description: 'ID, order number, rating, quantity.'
}, {
type: 'References',
description: 'Links to other content items.'
}, {
type: 'Tags',
description: 'Special format for tags.'
}, {
type: 'Array',
description: 'List of embedded objects.'
}
];
export const fieldInvariant = 'iv';
export function createProperties(fieldType: string, values: Object | null = null): FieldPropertiesDto {
let properties: FieldPropertiesDto;
switch (fieldType) {
case 'Array':
properties = new ArrayFieldPropertiesDto();
break;
case 'Assets':
properties = new AssetsFieldPropertiesDto();
break;
case 'Boolean':
properties = new BooleanFieldPropertiesDto('Checkbox');
break;
case 'DateTime':
properties = new DateTimeFieldPropertiesDto('DateTime');
break;
case 'Geolocation':
properties = new GeolocationFieldPropertiesDto();
break;
case 'Json':
properties = new JsonFieldPropertiesDto();
break;
case 'Number':
properties = new NumberFieldPropertiesDto('Input');
break;
case 'References':
properties = new ReferencesFieldPropertiesDto();
break;
case 'String':
properties = new StringFieldPropertiesDto('Input');
break;
case 'Tags':
properties = new TagsFieldPropertiesDto();
break;
default:
throw 'Invalid properties type';
}
if (values) {
Object.assign(properties, values);
}
return properties;
}
export interface FieldPropertiesVisitor<T> {
visitArray(properties: ArrayFieldPropertiesDto): T;
visitAssets(properties: AssetsFieldPropertiesDto): T;
visitBoolean(properties: BooleanFieldPropertiesDto): T;
visitDateTime(properties: DateTimeFieldPropertiesDto): T;
visitGeolocation(properties: GeolocationFieldPropertiesDto): T;
visitJson(properties: JsonFieldPropertiesDto): T;
visitNumber(properties: NumberFieldPropertiesDto): T;
visitReferences(properties: ReferencesFieldPropertiesDto): T;
visitString(properties: StringFieldPropertiesDto): T;
visitTags(properties: TagsFieldPropertiesDto): T;
}
export abstract class FieldPropertiesDto {
public abstract fieldType: string;
public readonly editorUrl?: string;
public readonly label?: string;
public readonly hints?: string;
public readonly placeholder?: string;
public readonly isRequired: boolean = false;
public readonly isListField: boolean = false;
constructor(public readonly editor: string,
props?: Partial<FieldPropertiesDto>
) {
if (props) {
Object.assign(this, props);
}
}
public abstract accept<T>(visitor: FieldPropertiesVisitor<T>): T;
}
export class ArrayFieldPropertiesDto extends FieldPropertiesDto {
public readonly fieldType = 'Array';
public readonly minItems?: number;
public readonly maxItems?: number;
constructor(
props?: Partial<ArrayFieldPropertiesDto>
) {
super('Default', props);
}
public accept<T>(visitor: FieldPropertiesVisitor<T>): T {
return visitor.visitArray(this);
}
}
export class AssetsFieldPropertiesDto extends FieldPropertiesDto {
public readonly fieldType = 'Assets';
public readonly minItems?: number;
public readonly maxItems?: number;
public readonly minSize?: number;
public readonly maxSize?: number;
public readonly allowedExtensions?: string[];
public readonly mustBeImage?: boolean;
public readonly minWidth?: number;
public readonly maxWidth?: number;
public readonly minHeight?: number;
public readonly maxHeight?: number;
public readonly aspectWidth?: number;
public readonly aspectHeight?: number;
constructor(
props?: Partial<AssetsFieldPropertiesDto>
) {
super('Default', props);
}
public accept<T>(visitor: FieldPropertiesVisitor<T>): T {
return visitor.visitAssets(this);
}
}
export class BooleanFieldPropertiesDto extends FieldPropertiesDto {
public readonly fieldType = 'Boolean';
public readonly inlineEditable: boolean = false;
public readonly defaultValue?: boolean;
constructor(editor: string,
props?: Partial<BooleanFieldPropertiesDto>
) {
super(editor, props);
}
public accept<T>(visitor: FieldPropertiesVisitor<T>): T {
return visitor.visitBoolean(this);
}
}
export class DateTimeFieldPropertiesDto extends FieldPropertiesDto {
public readonly fieldType = 'DateTime';
public readonly defaultValue?: string;
public readonly maxValue?: string;
public readonly minValue?: string;
public readonly calculatedDefaultValue?: string;
constructor(editor: string,
props?: Partial<DateTimeFieldPropertiesDto>
) {
super(editor, props);
}
public accept<T>(visitor: FieldPropertiesVisitor<T>): T {
return visitor.visitDateTime(this);
}
}
export class GeolocationFieldPropertiesDto extends FieldPropertiesDto {
public readonly fieldType = 'Geolocation';
constructor(
props?: Partial<GeolocationFieldPropertiesDto>
) {
super('Default', props);
}
public accept<T>(visitor: FieldPropertiesVisitor<T>): T {
return visitor.visitGeolocation(this);
}
}
export class JsonFieldPropertiesDto extends FieldPropertiesDto {
public readonly fieldType = 'Json';
constructor(
props?: Partial<JsonFieldPropertiesDto>
) {
super('Default', props);
}
public accept<T>(visitor: FieldPropertiesVisitor<T>): T {
return visitor.visitJson(this);
}
}
export class NumberFieldPropertiesDto extends FieldPropertiesDto {
public readonly fieldType = 'Number';
public readonly inlineEditable: boolean = false;
public readonly defaultValue?: number;
public readonly maxValue?: number;
public readonly minValue?: number;
public readonly allowedValues?: number[];
constructor(editor: string,
props?: Partial<NumberFieldPropertiesDto>
) {
super(editor, props);
}
public accept<T>(visitor: FieldPropertiesVisitor<T>): T {
return visitor.visitNumber(this);
}
}
export class ReferencesFieldPropertiesDto extends FieldPropertiesDto {
public readonly fieldType = 'References';
public readonly minItems?: number;
public readonly maxItems?: number;
public readonly schemaId?: string;
constructor(
props?: Partial<ReferencesFieldPropertiesDto>
) {
super('Default', props);
}
public accept<T>(visitor: FieldPropertiesVisitor<T>): T {
return visitor.visitReferences(this);
}
}
export class StringFieldPropertiesDto extends FieldPropertiesDto {
public readonly fieldType = 'String';
public readonly inlineEditable = false;
public readonly defaultValue?: string;
public readonly pattern?: string;
public readonly patternMessage?: string;
public readonly minLength?: number;
public readonly maxLength?: number;
public readonly allowedValues?: string[];
constructor(editor: string,
props?: Partial<StringFieldPropertiesDto>
) {
super(editor, props);
}
public accept<T>(visitor: FieldPropertiesVisitor<T>): T {
return visitor.visitString(this);
}
}
export class TagsFieldPropertiesDto extends FieldPropertiesDto {
public readonly fieldType = 'Tags';
public readonly minItems?: number;
public readonly maxItems?: number;
constructor(
props?: Partial<TagsFieldPropertiesDto>
) {
super('Default', props);
}
public accept<T>(visitor: FieldPropertiesVisitor<T>): T {
return visitor.visitTags(this);
}
}

105
src/Squidex/app/shared/services/schemas.fields.spec.ts → src/Squidex/app/shared/state/contents.forms.spec.ts

@ -12,7 +12,10 @@ import {
AssetsFieldPropertiesDto,
BooleanFieldPropertiesDto,
DateTimeFieldPropertiesDto,
FieldDefaultValue,
FieldFormatter,
FieldPropertiesDto,
FieldValidatorsFactory,
GeolocationFieldPropertiesDto,
JsonFieldPropertiesDto,
NumberFieldPropertiesDto,
@ -118,23 +121,23 @@ describe('ArrayField', () => {
const field = createField(new ArrayFieldPropertiesDto({ isRequired: true, minItems: 1, maxItems: 5 }));
it('should create validators', () => {
expect(field.createValidators(false).length).toBe(3);
expect(FieldValidatorsFactory.createValidators(field, false).length).toBe(3);
});
it('should format to empty string if null', () => {
expect(field.formatValue(null)).toBe('');
expect(FieldFormatter.format(field, null)).toBe('');
});
it('should format to asset count', () => {
expect(field.formatValue([1, 2, 3])).toBe('3 Items(s)');
expect(FieldFormatter.format(field, [1, 2, 3])).toBe('3 Items(s)');
});
it('should return zero formatting if other type', () => {
expect(field.formatValue(1)).toBe('0 Items');
expect(FieldFormatter.format(field, 1)).toBe('0 Items');
});
it('should return null for default properties', () => {
expect(field.defaultValue()).toBeNull();
expect(FieldDefaultValue.get(field)).toBeNull();
});
});
@ -142,23 +145,23 @@ describe('AssetsField', () => {
const field = createField(new AssetsFieldPropertiesDto({ isRequired: true, minItems: 1, maxItems: 5 }));
it('should create validators', () => {
expect(field.createValidators(false).length).toBe(3);
expect(FieldValidatorsFactory.createValidators(field, false).length).toBe(3);
});
it('should format to empty string if null', () => {
expect(field.formatValue(null)).toBe('');
expect(FieldFormatter.format(field, null)).toBe('');
});
it('should format to asset count', () => {
expect(field.formatValue([1, 2, 3])).toBe('3 Asset(s)');
expect(FieldFormatter.format(field, [1, 2, 3])).toBe('3 Asset(s)');
});
it('should return zero formatting if other type', () => {
expect(field.formatValue(1)).toBe('0 Assets');
expect(FieldFormatter.format(field, 1)).toBe('0 Assets');
});
it('should return null for default properties', () => {
expect(field.defaultValue()).toBeNull();
expect(FieldDefaultValue.get(field)).toBeNull();
});
});
@ -166,23 +169,23 @@ describe('TagsField', () => {
const field = createField(new TagsFieldPropertiesDto({ isRequired: true, minItems: 1, maxItems: 5 }));
it('should create validators', () => {
expect(field.createValidators(false).length).toBe(3);
expect(FieldValidatorsFactory.createValidators(field, false).length).toBe(3);
});
it('should format to empty string if null', () => {
expect(field.formatValue(null)).toBe('');
expect(FieldFormatter.format(field, null)).toBe('');
});
it('should format to asset count', () => {
expect(field.formatValue(['hello', 'squidex', 'cms'])).toBe('hello, squidex, cms');
expect(FieldFormatter.format(field, ['hello', 'squidex', 'cms'])).toBe('hello, squidex, cms');
});
it('should return zero formatting if other type', () => {
expect(field.formatValue(1)).toBe('');
expect(FieldFormatter.format(field, 1)).toBe('');
});
it('should return null for default properties', () => {
expect(field.defaultValue()).toBeNull();
expect(FieldDefaultValue.get(field)).toBeNull();
});
});
@ -190,25 +193,25 @@ describe('BooleanField', () => {
const field = createField(new BooleanFieldPropertiesDto('Checkbox', { isRequired: true }));
it('should create validators', () => {
expect(field.createValidators(false).length).toBe(1);
expect(FieldValidatorsFactory.createValidators(field, false).length).toBe(1);
});
it('should format to empty string if null', () => {
expect(field.formatValue(null)).toBe('');
expect(FieldFormatter.format(field, null)).toBe('');
});
it('should format to Yes if true', () => {
expect(field.formatValue(true)).toBe('Yes');
expect(FieldFormatter.format(field, true)).toBe('Yes');
});
it('should format to No if false', () => {
expect(field.formatValue(false)).toBe('No');
expect(FieldFormatter.format(field, false)).toBe('No');
});
it('should return default value for default properties', () => {
Object.assign(field.properties, { defaultValue: true });
expect(field.defaultValue()).toBeTruthy();
expect(FieldDefaultValue.get(field)).toBeTruthy();
});
});
@ -217,45 +220,45 @@ describe('DateTimeField', () => {
const field = createField(new DateTimeFieldPropertiesDto('DateTime', { isRequired: true }));
it('should create validators', () => {
expect(field.createValidators(false).length).toBe(1);
expect(FieldValidatorsFactory.createValidators(field, false).length).toBe(1);
});
it('should format to empty string if null', () => {
expect(field.formatValue(null)).toBe('');
expect(FieldFormatter.format(field, null)).toBe('');
});
it('should format to input if parsing failed', () => {
expect(field.formatValue(true)).toBe(true);
expect(FieldFormatter.format(field, true)).toBe(true);
});
it('should format to date', () => {
const dateField = createField(new DateTimeFieldPropertiesDto('Date'));
expect(dateField.formatValue('2017-12-12T16:00:00Z')).toBe('2017-12-12');
expect(FieldFormatter.format(dateField, '2017-12-12T16:00:00Z')).toBe('2017-12-12');
});
it('should format to date time', () => {
const dateTimeField = createField(new DateTimeFieldPropertiesDto('DateTime'));
expect(dateTimeField.formatValue('2017-12-12T16:00:00Z')).toBe('2017-12-12 16:00:00');
expect(FieldFormatter.format(dateTimeField, '2017-12-12T16:00:00Z')).toBe('2017-12-12 16:00:00');
});
it('should return default for DateFieldProperties', () => {
Object.assign(field.properties, { defaultValue: '2017-10-12T16:00:00Z' });
expect(field.defaultValue()).toEqual('2017-10-12T16:00:00Z');
expect(FieldDefaultValue.get(field)).toEqual('2017-10-12T16:00:00Z');
});
it('should return calculated date when Today for DateFieldProperties', () => {
Object.assign(field.properties, { calculatedDefaultValue: 'Today' });
Object.assign(field.properties, { calculatedFieldDefaultValue: 'Today' });
expect((<any>field).properties.getDefaultValue(now)).toEqual('2017-10-12');
expect((<any>field).properties.getFieldDefaultValue(now)).toEqual('2017-10-12');
});
it('should return calculated date when Now for DateFieldProperties', () => {
Object.assign(field.properties, { calculatedDefaultValue: 'Now' });
Object.assign(field.properties, { calculatedFieldDefaultValue: 'Now' });
expect((<any>field).properties.getDefaultValue(now)).toEqual('2017-10-12T16:30:10Z');
expect((<any>field).properties.getFieldDefaultValue(now)).toEqual('2017-10-12T16:30:10Z');
});
});
@ -263,19 +266,19 @@ describe('GeolocationField', () => {
const field = createField(new GeolocationFieldPropertiesDto({ isRequired: true }));
it('should create validators', () => {
expect(field.createValidators(false).length).toBe(1);
expect(FieldValidatorsFactory.createValidators(field, false).length).toBe(1);
});
it('should format to empty string if null', () => {
expect(field.formatValue(null)).toBe('');
expect(FieldFormatter.format(field, null)).toBe('');
});
it('should format to latitude and longitude', () => {
expect(field.formatValue({ latitude: 42, longitude: 3.14 })).toBe('3.14, 42');
expect(FieldFormatter.format(field, { latitude: 42, longitude: 3.14 })).toBe('3.14, 42');
});
it('should return null for default properties', () => {
expect(field.defaultValue()).toBeNull();
expect(FieldDefaultValue.get(field)).toBeNull();
});
});
@ -283,19 +286,19 @@ describe('JsonField', () => {
const field = createField(new JsonFieldPropertiesDto({ isRequired: true }));
it('should create validators', () => {
expect(field.createValidators(false).length).toBe(1);
expect(FieldValidatorsFactory.createValidators(field, false).length).toBe(1);
});
it('should format to empty string if null', () => {
expect(field.formatValue(null)).toBe('');
expect(FieldFormatter.format(field, null)).toBe('');
});
it('should format to constant', () => {
expect(field.formatValue({})).toBe('<Json />');
expect(FieldFormatter.format(field, {})).toBe('<Json />');
});
it('should return null for default properties', () => {
expect(field.defaultValue()).toBeNull();
expect(FieldDefaultValue.get(field)).toBeNull();
});
});
@ -303,21 +306,21 @@ describe('NumberField', () => {
const field = createField(new NumberFieldPropertiesDto('Input', { isRequired: true, minValue: 1, maxValue: 6, allowedValues: [1, 3] }));
it('should create validators', () => {
expect(field.createValidators(false).length).toBe(4);
expect(FieldValidatorsFactory.createValidators(field, false).length).toBe(4);
});
it('should format to empty string if null', () => {
expect(field.formatValue(null)).toBe('');
expect(FieldFormatter.format(field, null)).toBe('');
});
it('should format to number', () => {
expect(field.formatValue(42)).toBe(42);
expect(FieldFormatter.format(field, 42)).toBe(42);
});
it('should return default value for default properties', () => {
Object.assign(field.properties, { defaultValue: 13 });
expect(field.defaultValue()).toEqual(13);
expect(FieldDefaultValue.get(field)).toEqual(13);
});
});
@ -325,23 +328,23 @@ describe('ReferencesField', () => {
const field = createField(new ReferencesFieldPropertiesDto({ isRequired: true, minItems: 1, maxItems: 5 }));
it('should create validators', () => {
expect(field.createValidators(false).length).toBe(3);
expect(FieldValidatorsFactory.createValidators(field, false).length).toBe(3);
});
it('should format to empty string if null', () => {
expect(field.formatValue(null)).toBe('');
expect(FieldFormatter.format(field, null)).toBe('');
});
it('should format to asset count', () => {
expect(field.formatValue([1, 2, 3])).toBe('3 Reference(s)');
expect(FieldFormatter.format(field, [1, 2, 3])).toBe('3 Reference(s)');
});
it('should return zero formatting if other type', () => {
expect(field.formatValue(1)).toBe('0 References');
expect(FieldFormatter.format(field, 1)).toBe('0 References');
});
it('should return null for default properties', () => {
expect(field.defaultValue()).toBeNull();
expect(FieldDefaultValue.get(field)).toBeNull();
});
});
@ -349,21 +352,21 @@ describe('StringField', () => {
const field = createField(new StringFieldPropertiesDto('Input', { isRequired: true, pattern: 'pattern', minLength: 1, maxLength: 5, allowedValues: ['a', 'b'] }));
it('should create validators', () => {
expect(field.createValidators(false).length).toBe(5);
expect(FieldValidatorsFactory.createValidators(field, false).length).toBe(5);
});
it('should format to empty string if null', () => {
expect(field.formatValue(null)).toBe('');
expect(FieldFormatter.format(field, null)).toBe('');
});
it('should format to string', () => {
expect(field.formatValue('hello')).toBe('hello');
expect(FieldFormatter.format(field, 'hello')).toBe('hello');
});
it('should return default value for default properties', () => {
Object.assign(field.properties, { defaultValue: 'MyDefault' });
expect(field.defaultValue()).toEqual('MyDefault');
expect(FieldDefaultValue.get(field)).toEqual('MyDefault');
});
});

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

@ -8,17 +8,301 @@
// tslint:disable:prefer-for-of
import { FormArray, FormControl, FormGroup } from '@angular/forms';
import { FormArray, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms';
import {
ErrorDto,
DateTime,
Form,
formControls,
ImmutableArray,
Types
Types,
ValidatorsEx
} from '@app/framework';
import { AppLanguageDto } from './../services/app-languages.service';
import { fieldInvariant, RootFieldDto, SchemaDetailsDto } from './../services/schemas.service';
import { FieldDto, RootFieldDto, SchemaDetailsDto } from './../services/schemas.service';
import {
ArrayFieldPropertiesDto,
AssetsFieldPropertiesDto,
BooleanFieldPropertiesDto,
DateTimeFieldPropertiesDto,
fieldInvariant,
FieldPropertiesVisitor,
GeolocationFieldPropertiesDto,
JsonFieldPropertiesDto,
NumberFieldPropertiesDto,
ReferencesFieldPropertiesDto,
StringFieldPropertiesDto,
TagsFieldPropertiesDto
} from './../services/schemas.types';
export class FieldFormatter implements FieldPropertiesVisitor<string> {
constructor(
private readonly value: any
) {
}
public static format(field: FieldDto, value: any) {
if (!value) {
return '';
}
return field.properties.accept(new FieldFormatter(value));
}
public visitDateTime(properties: DateTimeFieldPropertiesDto): string {
try {
const parsed = DateTime.parseISO_UTC(this.value);
if (properties.editor === 'Date') {
return parsed.toUTCStringFormat('YYYY-MM-DD');
} else {
return parsed.toUTCStringFormat('YYYY-MM-DD HH:mm:ss');
}
} catch (ex) {
return this.value;
}
}
public visitArray(properties: ArrayFieldPropertiesDto): string {
if (this.value.length) {
return `${this.value.length} Item(s)`;
} else {
return '0 Items';
}
}
public visitAssets(properties: AssetsFieldPropertiesDto): string {
if (this.value.length) {
return `${this.value.length} Asset(s)`;
} else {
return '0 Assets';
}
}
public visitReferences(properties: ReferencesFieldPropertiesDto): string {
if (this.value.length) {
return `${this.value.length} Reference(s)`;
} else {
return '0 References';
}
}
public visitTags(properties: TagsFieldPropertiesDto): string {
if (this.value.length) {
return this.value.join(', ');
} else {
return '';
}
}
public visitBoolean(properties: BooleanFieldPropertiesDto): string {
return this.value ? 'Yes' : 'No';
}
public visitGeolocation(properties: GeolocationFieldPropertiesDto): string {
return `${this.value.longitude}, ${this.value.latitude}`;
}
public visitJson(properties: JsonFieldPropertiesDto): string {
return '<Json />';
}
public visitNumber(properties: NumberFieldPropertiesDto): string {
return this.value;
}
public visitString(properties: StringFieldPropertiesDto): string {
return this.value;
}
}
export class FieldValidatorsFactory implements FieldPropertiesVisitor<ValidatorFn[]> {
constructor(
private readonly isOptional: boolean
) {
}
public static createValidators(field: FieldDto, isOptional: boolean) {
const validators = field.properties.accept(new FieldValidatorsFactory(isOptional));
if (field.properties.isRequired && !isOptional) {
validators.push(Validators.required);
}
return validators;
}
public visitNumber(properties: NumberFieldPropertiesDto): ValidatorFn[] {
const validators: ValidatorFn[] = [];
if (properties.minValue) {
validators.push(Validators.min(properties.minValue));
}
if (properties.maxValue) {
validators.push(Validators.max(properties.maxValue));
}
if (properties.allowedValues && properties.allowedValues.length > 0) {
const values: (number | null)[] = properties.allowedValues;
if (properties.isRequired && !this.isOptional) {
validators.push(ValidatorsEx.validValues(values));
} else {
validators.push(ValidatorsEx.validValues(values.concat([null])));
}
}
return validators;
}
public visitString(properties: StringFieldPropertiesDto): ValidatorFn[] {
const validators: ValidatorFn[] = [];
if (properties.minLength) {
validators.push(Validators.minLength(properties.minLength));
}
if (properties.maxLength) {
validators.push(Validators.maxLength(properties.maxLength));
}
if (properties.pattern && properties.pattern.length > 0) {
validators.push(ValidatorsEx.pattern(properties.pattern, properties.patternMessage));
}
if (properties.allowedValues && properties.allowedValues.length > 0) {
const values: (string | null)[] = properties.allowedValues;
if (properties.isRequired && !this.isOptional) {
validators.push(ValidatorsEx.validValues(values));
} else {
validators.push(ValidatorsEx.validValues(values.concat([null])));
}
}
return validators;
}
public visitArray(properties: ArrayFieldPropertiesDto): ValidatorFn[] {
const validators: ValidatorFn[] = [];
if (properties.minItems) {
validators.push(Validators.minLength(properties.minItems));
}
if (properties.maxItems) {
validators.push(Validators.maxLength(properties.maxItems));
}
return validators;
}
public visitAssets(properties: AssetsFieldPropertiesDto): ValidatorFn[] {
const validators: ValidatorFn[] = [];
if (properties.minItems) {
validators.push(Validators.minLength(properties.minItems));
}
if (properties.maxItems) {
validators.push(Validators.maxLength(properties.maxItems));
}
return validators;
}
public visitReferences(properties: ReferencesFieldPropertiesDto): ValidatorFn[] {
const validators: ValidatorFn[] = [];
if (properties.minItems) {
validators.push(Validators.minLength(properties.minItems));
}
if (properties.maxItems) {
validators.push(Validators.maxLength(properties.maxItems));
}
return validators;
}
public visitTags(properties: TagsFieldPropertiesDto): ValidatorFn[] {
const validators: ValidatorFn[] = [];
if (properties.minItems) {
validators.push(Validators.minLength(properties.minItems));
}
if (properties.maxItems) {
validators.push(Validators.maxLength(properties.maxItems));
}
return validators;
}
public visitBoolean(properties: BooleanFieldPropertiesDto): ValidatorFn[] {
return [];
}
public visitDateTime(properties: DateTimeFieldPropertiesDto): ValidatorFn[] {
return [];
}
public visitGeolocation(properties: GeolocationFieldPropertiesDto): ValidatorFn[] {
return [];
}
public visitJson(properties: JsonFieldPropertiesDto): ValidatorFn[] {
return [];
}
}
export class FieldDefaultValue implements FieldPropertiesVisitor<any> {
public static get(field: FieldDto) {
return field.properties.accept(new FieldDefaultValue());
}
public visitArray(properties: ArrayFieldPropertiesDto): any {
return null;
}
public visitAssets(properties: AssetsFieldPropertiesDto): any {
return null;
}
public visitBoolean(properties: BooleanFieldPropertiesDto): any {
return properties.defaultValue;
}
public visitDateTime(properties: DateTimeFieldPropertiesDto): any {
return null;
}
public visitGeolocation(properties: GeolocationFieldPropertiesDto): any {
return null;
}
public visitJson(properties: JsonFieldPropertiesDto): any {
return null;
}
public visitNumber(properties: NumberFieldPropertiesDto): any {
return properties.defaultValue;
}
public visitReferences(properties: ReferencesFieldPropertiesDto): any {
return null;
}
public visitString(properties: StringFieldPropertiesDto): any {
return properties.defaultValue;
}
public visitTags(properties: TagsFieldPropertiesDto): any {
return null;
}
}
export class EditContentForm extends Form<FormGroup> {
constructor(
@ -29,13 +313,15 @@ export class EditContentForm extends Form<FormGroup> {
for (const field of schema.fields) {
const fieldForm = new FormGroup({});
const fieldDefault = field.defaultValue();
const fieldDefault = FieldDefaultValue.get(field);
const createControl = (isOptional: boolean) => {
if (field.properties.fieldType === 'Array') {
return new FormArray([], field.createValidators(isOptional));
const validators = FieldValidatorsFactory.createValidators(field, isOptional);
if (field.isArray) {
return new FormArray([], validators);
} else {
return new FormControl(fieldDefault, field.createValidators(isOptional));
return new FormControl(fieldDefault, validators);
}
};
@ -50,7 +336,7 @@ export class EditContentForm extends Form<FormGroup> {
this.form.setControl(field.name, fieldForm);
}
this.enableContentForm();
this.enable();
}
public removeArrayItem(field: RootFieldDto, language: AppLanguageDto, index: number) {
@ -65,18 +351,19 @@ export class EditContentForm extends Form<FormGroup> {
}
}
private addArrayItem(field: RootFieldDto, language: AppLanguageDto | null, formControl: FormArray) {
const formItem = new FormGroup({});
private addArrayItem(field: RootFieldDto, language: AppLanguageDto | null, partitionForm: FormArray) {
const itemForm = new FormGroup({});
let isOptional = field.isLocalizable && language !== null && language.isOptional;
for (let nested of field.nested) {
const nestedDefault = field.defaultValue();
const nestedValidators = FieldValidatorsFactory.createValidators(nested, isOptional);
const nestedDefault = FieldDefaultValue.get(nested);
formItem.setControl(nested.name, new FormControl(nestedDefault, nested.createValidators(isOptional)));
itemForm.setControl(nested.name, new FormControl(nestedDefault, nestedValidators));
}
formControl.push(formItem);
partitionForm.push(itemForm);
}
private findArrayItemForm(field: RootFieldDto, language: AppLanguageDto): FormArray {
@ -89,23 +376,16 @@ export class EditContentForm extends Form<FormGroup> {
}
}
public submitCompleted(newValue?: any) {
super.submitCompleted(newValue);
this.enableContentForm();
}
public submitFailed(error?: string | ErrorDto) {
super.submitFailed(error);
public loadContent(value: any, isArchive: boolean) {
for (let field of this.schema.fields) {
if (field.isArray && field.nested.length > 0) {
const fieldForm = <FormGroup>this.form.get(field.name);
this.enableContentForm();
}
if (!fieldForm) {
continue;
}
public loadData(value: any, isArchive: boolean) {
for (let field of this.schema.fields) {
if (field.properties.fieldType === 'Array' && field.nested.length > 0) {
const fieldValue = value ? value[field.name] || {} : {};
const fieldForm = <FormGroup>this.form.get(field.name)!;
const addControls = (key: string, language: AppLanguageDto | null) => {
const languageValue = fieldValue[key];
@ -133,24 +413,45 @@ export class EditContentForm extends Form<FormGroup> {
super.load(value);
if (isArchive) {
this.form.disable();
this.disable();
} else {
this.enableContentForm();
this.enable();
}
}
private enableContentForm() {
protected enable() {
if (this.schema.fields.length === 0) {
this.form.enable();
} else {
for (const field of this.schema.fields) {
const fieldForm = this.form.controls[field.name];
return;
}
if (field.isDisabled) {
fieldForm.disable();
} else {
fieldForm.enable();
for (const field of this.schema.fields) {
const fieldForm = this.form.get(field.name);
if (!fieldForm) {
continue;
}
if (field.properties.fieldType === 'Array') {
for (let partitionForm of formControls(fieldForm)) {
for (let nested of field.nested) {
const nestedForm = partitionForm.get(nested.name);
if (!nestedForm) {
continue;
}
if (nested.isDisabled) {
nestedForm.disable();
} else {
nestedForm.enable();
}
}
}
} else if (field.isDisabled) {
fieldForm.disable();
} else {
fieldForm.enable();
}
}
}
@ -163,10 +464,10 @@ export class PatchContentForm extends Form<FormGroup> {
) {
super(new FormGroup({}));
for (let field of this.schema.listFields) {
if (field.properties && field.properties['inlineEditable']) {
this.form.setControl(field.name, new FormControl(undefined, field.createValidators(this.language.isOptional)));
}
for (let field of this.schema.inlineEditableFields) {
const validators = FieldValidatorsFactory.createValidators(field, this.language.isOptional);
this.form.setControl(field.name, new FormControl(undefined, validators));
}
}
@ -176,15 +477,13 @@ export class PatchContentForm extends Form<FormGroup> {
if (result) {
const request = {};
for (let field of this.schema.listFields) {
if (field.properties['inlineEditable']) {
const value = result[field.name];
for (let field of this.schema.inlineEditableFields) {
const value = result[field.name];
if (field.isLocalizable) {
request[field.name] = { [this.language.iso2Code]: value };
} else {
request[field.name] = { iv: value };
}
if (field.isLocalizable) {
request[field.name] = { [this.language.iso2Code]: value };
} else {
request[field.name] = { iv: value };
}
}

2
src/Squidex/app/shared/state/schemas.forms.ts

@ -9,7 +9,7 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Form, ValidatorsEx } from '@app/framework';
import { createProperties } from './../services/schemas.service';
import { createProperties } from './../services/schemas.types';
const FALLBACK_NAME = 'my-schema';

6
src/Squidex/app/shared/state/schemas.state.ts

@ -24,10 +24,8 @@ import { AppsState } from './apps.state';
import {
AddFieldDto,
AnyFieldDto,
CreateSchemaDto,
FieldDto,
FieldPropertiesDto,
NestedFieldDto,
RootFieldDto,
SchemaDetailsDto,
@ -40,6 +38,10 @@ import {
UpdateSchemaScriptsDto
} from './../services/schemas.service';
import { FieldPropertiesDto } from './../services/schemas.types';
type AnyFieldDto = NestedFieldDto | RootFieldDto;
interface Snapshot {
categories: { [name: string]: boolean };

Loading…
Cancel
Save