diff --git a/src/Squidex/app/features/content/declarations.ts b/src/Squidex/app/features/content/declarations.ts index f1744a07e..cd59c22c5 100644 --- a/src/Squidex/app/features/content/declarations.ts +++ b/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'; \ No newline at end of file diff --git a/src/Squidex/app/features/content/module.ts b/src/Squidex/app/features/content/module.ts index 0db056f59..a372b141c 100644 --- a/src/Squidex/app/features/content/module.ts +++ b/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 diff --git a/src/Squidex/app/features/content/pages/content/content-field.component.html b/src/Squidex/app/features/content/pages/content/content-field.component.html index 3b96b122a..3b7f60e12 100644 --- a/src/Squidex/app/features/content/pages/content/content-field.component.html +++ b/src/Squidex/app/features/content/pages/content/content-field.component.html @@ -1,10 +1,4 @@
- - - Disabled -
- - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - {{hints}} - - + +
diff --git a/src/Squidex/app/features/content/pages/content/content-page.component.ts b/src/Squidex/app/features/content/pages/content/content-page.component.ts index 617cd242d..f6f475a94 100644 --- a/src/Squidex/app/features/content/pages/content/content-page.component.ts +++ b/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() { diff --git a/src/Squidex/app/features/content/shared/array-editor.component.html b/src/Squidex/app/features/content/shared/array-editor.component.html index f25ca1ed2..34a3d9c2b 100644 --- a/src/Squidex/app/features/content/shared/array-editor.component.html +++ b/src/Squidex/app/features/content/shared/array-editor.component.html @@ -1,133 +1,23 @@
-
- - - Disabled - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - {{hints}} - - + +
- \ No newline at end of file diff --git a/src/Squidex/app/features/content/shared/array-editor.component.ts b/src/Squidex/app/features/content/shared/array-editor.component.ts index 182254745..08686a426 100644 --- a/src/Squidex/app/features/content/shared/array-editor.component.ts +++ b/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; } } \ No newline at end of file diff --git a/src/Squidex/app/features/content/shared/content-item.component.html b/src/Squidex/app/features/content/shared/content-item.component.html index 31123e43e..ef7dfb3e9 100644 --- a/src/Squidex/app/features/content/shared/content-item.component.html +++ b/src/Squidex/app/features/content/shared/content-item.component.html @@ -6,7 +6,7 @@ -
+
@@ -51,7 +51,7 @@
-
+
{{values[i]}}
diff --git a/src/Squidex/app/features/content/shared/content-item.component.ts b/src/Squidex/app/features/content/shared/content-item.component.ts index 798dd5788..68f18d763 100644 --- a/src/Squidex/app/features/content/shared/content-item.component.ts +++ b/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) { diff --git a/src/Squidex/app/features/content/shared/field-editor.component.html b/src/Squidex/app/features/content/shared/field-editor.component.html new file mode 100644 index 000000000..6ba9ecc81 --- /dev/null +++ b/src/Squidex/app/features/content/shared/field-editor.component.html @@ -0,0 +1,124 @@ + + + +Disabled + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + {{hints}} + + \ No newline at end of file diff --git a/src/Squidex/app/features/content/shared/field-editor.component.scss b/src/Squidex/app/features/content/shared/field-editor.component.scss new file mode 100644 index 000000000..8cb38413e --- /dev/null +++ b/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; + } +} \ No newline at end of file diff --git a/src/Squidex/app/features/content/shared/field-editor.component.ts b/src/Squidex/app/features/content/shared/field-editor.component.ts new file mode 100644 index 000000000..ed3e0ee9c --- /dev/null +++ b/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; +} \ No newline at end of file diff --git a/src/Squidex/app/features/schemas/pages/schema/field.component.ts b/src/Squidex/app/features/schemas/pages/schema/field.component.ts index c9c244ee0..1722ce303 100644 --- a/src/Squidex/app/features/schemas/pages/schema/field.component.ts +++ b/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; diff --git a/src/Squidex/app/framework/state.ts b/src/Squidex/app/framework/state.ts index 9ffe7de8f..28ff48cbc 100644 --- a/src/Squidex/app/framework/state.ts +++ b/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 { + 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 { private readonly state = new State({ submitted: false }); @@ -31,10 +59,26 @@ export class Form { ) { } + 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 { if (this.form.valid) { const value = this.form.value; - this.form.disable(); + this.disable(); return value; } else { @@ -54,10 +98,10 @@ export class Form { 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 { 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 { } 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; } } diff --git a/src/Squidex/app/shared/internal.ts b/src/Squidex/app/shared/internal.ts index 69fb2ff8f..02a02f1fa 100644 --- a/src/Squidex/app/shared/internal.ts +++ b/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'; diff --git a/src/Squidex/app/shared/services/schemas.service.ts b/src/Squidex/app/shared/services/schemas.service.ts index e3b868c1e..6deb838c2 100644 --- a/src/Squidex/app/shared/services/schemas.service.ts +++ b/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 { @@ -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 = [{ 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 = [{ properties: {} }]; - } - } } public with(value: Partial): 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 { 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 { @@ -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 { @@ -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 - ) { - 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 - ) { - 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 - ) { - 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 - ) { - 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 - ) { - 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 - ) { - 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 - ) { - super('Default', props); - } - - public formatValue(value: any): string { - if (!value) { - return ''; - } - - return ''; - } - - 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 - ) { - 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 - ) { - 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 - ) { - 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 - ) { - 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> { + public postField(appName: string, schemaName: string, dto: AddFieldDto, parentId: number | undefined, version: Version): Observable> { const url = this.buildUrl(appName, schemaName, parentId, ''); return HTTP.postVersioned(this.http, url, dto, version) diff --git a/src/Squidex/app/shared/services/schemas.types.ts b/src/Squidex/app/shared/services/schemas.types.ts new file mode 100644 index 000000000..080ac7d97 --- /dev/null +++ b/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 { + 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 + ) { + if (props) { + Object.assign(this, props); + } + } + + public abstract accept(visitor: FieldPropertiesVisitor): T; +} + +export class ArrayFieldPropertiesDto extends FieldPropertiesDto { + public readonly fieldType = 'Array'; + + public readonly minItems?: number; + public readonly maxItems?: number; + + constructor( + props?: Partial + ) { + super('Default', props); + } + + public accept(visitor: FieldPropertiesVisitor): 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 + ) { + super('Default', props); + } + + public accept(visitor: FieldPropertiesVisitor): 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 + ) { + super(editor, props); + } + + public accept(visitor: FieldPropertiesVisitor): 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 + ) { + super(editor, props); + } + + public accept(visitor: FieldPropertiesVisitor): T { + return visitor.visitDateTime(this); + } +} + +export class GeolocationFieldPropertiesDto extends FieldPropertiesDto { + public readonly fieldType = 'Geolocation'; + + constructor( + props?: Partial + ) { + super('Default', props); + } + + public accept(visitor: FieldPropertiesVisitor): T { + return visitor.visitGeolocation(this); + } +} + +export class JsonFieldPropertiesDto extends FieldPropertiesDto { + public readonly fieldType = 'Json'; + + constructor( + props?: Partial + ) { + super('Default', props); + } + + public accept(visitor: FieldPropertiesVisitor): 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 + ) { + super(editor, props); + } + + public accept(visitor: FieldPropertiesVisitor): 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 + ) { + super('Default', props); + } + + public accept(visitor: FieldPropertiesVisitor): 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 + ) { + super(editor, props); + } + + public accept(visitor: FieldPropertiesVisitor): 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 + ) { + super('Default', props); + } + + public accept(visitor: FieldPropertiesVisitor): T { + return visitor.visitTags(this); + } +} \ No newline at end of file diff --git a/src/Squidex/app/shared/services/schemas.fields.spec.ts b/src/Squidex/app/shared/state/contents.forms.spec.ts similarity index 71% rename from src/Squidex/app/shared/services/schemas.fields.spec.ts rename to src/Squidex/app/shared/state/contents.forms.spec.ts index 87c4cbad9..6d4eb7952 100644 --- a/src/Squidex/app/shared/services/schemas.fields.spec.ts +++ b/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((field).properties.getDefaultValue(now)).toEqual('2017-10-12'); + expect((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((field).properties.getDefaultValue(now)).toEqual('2017-10-12T16:30:10Z'); + expect((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(''); + expect(FieldFormatter.format(field, {})).toBe(''); }); 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'); }); }); diff --git a/src/Squidex/app/shared/state/contents.forms.ts b/src/Squidex/app/shared/state/contents.forms.ts index 1596ff14d..fdbdb0ab7 100644 --- a/src/Squidex/app/shared/state/contents.forms.ts +++ b/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 { + 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 ''; + } + + public visitNumber(properties: NumberFieldPropertiesDto): string { + return this.value; + } + + public visitString(properties: StringFieldPropertiesDto): string { + return this.value; + } +} + +export class FieldValidatorsFactory implements FieldPropertiesVisitor { + 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 { + 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 { constructor( @@ -29,13 +313,15 @@ export class EditContentForm extends Form { 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 { 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 { } } - 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 { } } - 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 = 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 = 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 { 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 { ) { 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 { 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 }; } } diff --git a/src/Squidex/app/shared/state/schemas.forms.ts b/src/Squidex/app/shared/state/schemas.forms.ts index 18dc57507..5259e5851 100644 --- a/src/Squidex/app/shared/state/schemas.forms.ts +++ b/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'; diff --git a/src/Squidex/app/shared/state/schemas.state.ts b/src/Squidex/app/shared/state/schemas.state.ts index e4b8fe870..fe51c8808 100644 --- a/src/Squidex/app/shared/state/schemas.state.ts +++ b/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 };