Browse Source

Content form fixed and tested.

pull/414/head
Sebastian Stehle 7 years ago
parent
commit
0587ad3555
  1. 10
      src/Squidex/app/features/content/pages/content/content-page.component.ts
  2. 6
      src/Squidex/app/shared/services/schemas.service.ts
  3. 33
      src/Squidex/app/shared/services/schemas.types.ts
  4. 396
      src/Squidex/app/shared/state/contents.forms.spec.ts
  5. 221
      src/Squidex/app/shared/state/contents.forms.ts

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

@ -99,7 +99,7 @@ export class ContentPageComponent extends ResourceOwner implements CanComponentD
if (schema) { if (schema) {
this.schema = schema!; this.schema = schema!;
this.contentForm = new EditContentForm(this.schema, this.languages); this.contentForm = new EditContentForm(this.languages, this.schema);
} }
})); }));
@ -272,18 +272,14 @@ export class ContentPageComponent extends ResourceOwner implements CanComponentD
this.contentsState.loadVersion(this.content, version) this.contentsState.loadVersion(this.content, version)
.subscribe(dto => { .subscribe(dto => {
if (compare) { if (compare) {
if (this.contentFormCompare === null) { this.contentFormCompare = new EditContentForm(this.languages, this.schema);
this.contentFormCompare = new EditContentForm(this.schema, this.languages);
}
this.contentFormCompare.load(dto.payload); this.contentFormCompare.load(dto.payload);
this.contentFormCompare.setEnabled(false); this.contentFormCompare.setEnabled(false);
this.loadContent(this.content.dataDraft, false); this.loadContent(this.content.dataDraft, false);
} else { } else {
if (this.contentFormCompare) { this.contentFormCompare = null;
this.contentFormCompare = null;
}
this.loadContent(dto.payload, false); this.loadContent(dto.payload, false);
} }

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

@ -80,9 +80,9 @@ export class SchemaDto {
} }
export class SchemaDetailsDto extends SchemaDto { export class SchemaDetailsDto extends SchemaDto {
public listFields: RootFieldDto[]; public readonly listFields: RootFieldDto[];
public listFieldsEditable: RootFieldDto[]; public readonly listFieldsEditable: RootFieldDto[];
public referenceFields: RootFieldDto[]; public readonly referenceFields: RootFieldDto[];
constructor(links: ResourceLinks, id: string, name: string, category: string, constructor(links: ResourceLinks, id: string, name: string, category: string,
properties: SchemaPropertiesDto, properties: SchemaPropertiesDto,

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

@ -57,54 +57,47 @@ export const fieldTypes: { type: FieldType, description: string }[] = [
export const fieldInvariant = 'iv'; export const fieldInvariant = 'iv';
export function createProperties(fieldType: FieldType, values: Object | null = null): FieldPropertiesDto { export function createProperties(fieldType: FieldType, values?: any): FieldPropertiesDto {
let properties: FieldPropertiesDto; let properties: FieldPropertiesDto;
switch (fieldType) { switch (fieldType) {
case 'Array': case 'Array':
properties = new ArrayFieldPropertiesDto(); properties = new ArrayFieldPropertiesDto(values);
break; break;
case 'Assets': case 'Assets':
properties = new AssetsFieldPropertiesDto(); properties = new AssetsFieldPropertiesDto(values);
break; break;
case 'Boolean': case 'Boolean':
properties = new BooleanFieldPropertiesDto('Checkbox'); properties = new BooleanFieldPropertiesDto('Checkbox', values);
break; break;
case 'DateTime': case 'DateTime':
properties = new DateTimeFieldPropertiesDto('DateTime'); properties = new DateTimeFieldPropertiesDto('DateTime', values);
break; break;
case 'Geolocation': case 'Geolocation':
properties = new GeolocationFieldPropertiesDto(); properties = new GeolocationFieldPropertiesDto(values);
break; break;
case 'Json': case 'Json':
properties = new JsonFieldPropertiesDto(); properties = new JsonFieldPropertiesDto(values);
break; break;
case 'Number': case 'Number':
properties = new NumberFieldPropertiesDto('Input'); properties = new NumberFieldPropertiesDto('Input', values);
break; break;
case 'References': case 'References':
properties = new ReferencesFieldPropertiesDto('List'); properties = new ReferencesFieldPropertiesDto('List', values);
break; break;
case 'String': case 'String':
properties = new StringFieldPropertiesDto('Input'); properties = new StringFieldPropertiesDto('Input', values);
break; break;
case 'Tags': case 'Tags':
properties = new TagsFieldPropertiesDto('Tags'); properties = new TagsFieldPropertiesDto(values);
break;
case 'Tags':
properties = new TagsFieldPropertiesDto('Tags');
break; break;
case 'UI': case 'UI':
properties = new UIFieldPropertiesDto(); properties = new UIFieldPropertiesDto(values);
break; break;
default: default:
throw 'Invalid properties type'; throw 'Invalid properties type';
} }
if (values) {
Object.assign(properties, values);
}
return properties; return properties;
} }
@ -387,7 +380,7 @@ export class TagsFieldPropertiesDto extends FieldPropertiesDto {
return false; return false;
} }
constructor(editor: string, constructor(
props?: Partial<TagsFieldPropertiesDto> props?: Partial<TagsFieldPropertiesDto>
) { ) {
super('Tags', props); super('Tags', props);

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

@ -5,71 +5,89 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { AbstractControl, FormArray } from '@angular/forms';
import { import {
AppLanguageDto,
ArrayFieldPropertiesDto, ArrayFieldPropertiesDto,
AssetsFieldPropertiesDto, AssetsFieldPropertiesDto,
BooleanFieldPropertiesDto, BooleanFieldPropertiesDto,
createProperties,
DateTime, DateTime,
DateTimeFieldPropertiesDto, DateTimeFieldPropertiesDto,
EditContentForm,
FieldDefaultValue, FieldDefaultValue,
FieldFormatter, FieldFormatter,
FieldPropertiesDto, FieldPropertiesDto,
FieldValidatorsFactory, FieldsValidators,
GeolocationFieldPropertiesDto, GeolocationFieldPropertiesDto,
getContentValue, getContentValue,
HtmlValue, HtmlValue,
ImmutableArray,
JsonFieldPropertiesDto, JsonFieldPropertiesDto,
LanguageDto, LanguageDto,
NestedFieldDto,
NumberFieldPropertiesDto, NumberFieldPropertiesDto,
PartitionConfig,
ReferencesFieldPropertiesDto, ReferencesFieldPropertiesDto,
RootFieldDto, RootFieldDto,
SchemaDetailsDto, SchemaDetailsDto,
SchemaPropertiesDto, SchemaPropertiesDto,
StringFieldPropertiesDto, StringFieldPropertiesDto,
TagsFieldPropertiesDto TagsFieldPropertiesDto,
Version
} from '@app/shared/internal'; } from '@app/shared/internal';
import { TestValues } from './_test-helpers';
const {
modified,
modifier,
creation,
creator
} = TestValues;
describe('SchemaDetailsDto', () => { describe('SchemaDetailsDto', () => {
it('should return label as display name', () => { it('should return label as display name', () => {
const schema = createSchema(new SchemaPropertiesDto('Label'), 1, []); const schema = createSchema({ properties: new SchemaPropertiesDto('Label') });
expect(schema.displayName).toBe('Label'); expect(schema.displayName).toBe('Label');
}); });
it('should return name as display name if label is undefined', () => { it('should return name as display name if label is undefined', () => {
const schema = createSchema(new SchemaPropertiesDto(undefined), 1, []); const schema = createSchema({ properties: new SchemaPropertiesDto(undefined) });
expect(schema.displayName).toBe('schema1'); expect(schema.displayName).toBe('schema1');
}); });
it('should return name as display name label is empty', () => { it('should return name as display name label is empty', () => {
const schema = createSchema(new SchemaPropertiesDto(''), 1, []); const schema = createSchema({ properties: new SchemaPropertiesDto('') });
expect(schema.displayName).toBe('schema1'); expect(schema.displayName).toBe('schema1');
}); });
it('should return configured fields as list fields if no schema field are declared', () => { it('should return configured fields as list fields if no schema field are declared', () => {
const field1 = createField(new ArrayFieldPropertiesDto({ isListField: true }), 1); const field1 = createField({ properties: new ArrayFieldPropertiesDto({ isListField: true }) });
const field2 = createField(new ArrayFieldPropertiesDto({ isListField: false }), 2); const field2 = createField({ properties: new ArrayFieldPropertiesDto({ isListField: false }), id: 2 });
const field3 = createField(new ArrayFieldPropertiesDto({ isListField: true }), 3); const field3 = createField({ properties: new ArrayFieldPropertiesDto({ isListField: true }), id: 3 });
const schema = createSchema(new SchemaPropertiesDto(''), 1, [field1, field2, field3]); const schema = createSchema({ properties: new SchemaPropertiesDto(''), fields: [field1, field2, field3] });
expect(schema.listFields).toEqual([field1, field3]); expect(schema.listFields).toEqual([field1, field3]);
}); });
it('should return first fields as list fields if no schema field is declared', () => { it('should return first fields as list fields if no schema field is declared', () => {
const field1 = createField(new ArrayFieldPropertiesDto(), 1); const field1 = createField({ properties: new ArrayFieldPropertiesDto() });
const field2 = createField(new ArrayFieldPropertiesDto(), 2); const field2 = createField({ properties: new ArrayFieldPropertiesDto(), id: 2 });
const field3 = createField(new ArrayFieldPropertiesDto(), 3); const field3 = createField({ properties: new ArrayFieldPropertiesDto(), id: 3 });
const schema = createSchema(new SchemaPropertiesDto(''), 1, [field1, field2, field3]); const schema = createSchema({ properties: new SchemaPropertiesDto(''), fields: [field1, field2, field3] });
expect(schema.listFields).toEqual([field1]); expect(schema.listFields).toEqual([field1]);
}); });
it('should return empty list fields if fields is empty', () => { it('should return empty list fields if fields is empty', () => {
const schema = createSchema(new SchemaPropertiesDto(), 1, []); const schema = createSchema({ properties: new SchemaPropertiesDto() });
expect(schema.listFields[0].fieldId).toEqual(-1); expect(schema.listFields[0].fieldId).toEqual(-1);
}); });
@ -77,53 +95,53 @@ describe('SchemaDetailsDto', () => {
describe('FieldDto', () => { describe('FieldDto', () => {
it('should return label as display name', () => { it('should return label as display name', () => {
const field = createField(new AssetsFieldPropertiesDto({ label: 'Label' }), 1); const field = createField({ properties: new AssetsFieldPropertiesDto({ label: 'Label' }) });
expect(field.displayName).toBe('Label'); expect(field.displayName).toBe('Label');
}); });
it('should return name as display name if label is null', () => { it('should return name as display name if label is null', () => {
const field = createField(new AssetsFieldPropertiesDto(), 1); const field = createField({ properties: new AssetsFieldPropertiesDto() });
expect(field.displayName).toBe('field1'); expect(field.displayName).toBe('field1');
}); });
it('should return name as display name label is empty', () => { it('should return name as display name label is empty', () => {
const field = createField(new AssetsFieldPropertiesDto({ label: '' }), 1); const field = createField({ properties: new AssetsFieldPropertiesDto({ label: '' }) });
expect(field.displayName).toBe('field1'); expect(field.displayName).toBe('field1');
}); });
it('should return placeholder as display placeholder', () => { it('should return placeholder as display placeholder', () => {
const field = createField(new AssetsFieldPropertiesDto({ placeholder: 'Placeholder' }), 1); const field = createField({ properties: new AssetsFieldPropertiesDto({ placeholder: 'Placeholder' }) });
expect(field.displayPlaceholder).toBe('Placeholder'); expect(field.displayPlaceholder).toBe('Placeholder');
}); });
it('should return empty as display placeholder if placeholder is null', () => { it('should return empty as display placeholder if placeholder is null', () => {
const field = createField(new AssetsFieldPropertiesDto()); const field = createField({ properties: new AssetsFieldPropertiesDto() });
expect(field.displayPlaceholder).toBe(''); expect(field.displayPlaceholder).toBe('');
}); });
it('should return localizable if partitioning is language', () => { it('should return localizable if partitioning is language', () => {
const field = createField(new AssetsFieldPropertiesDto(), 1, 'language'); const field = createField({ properties: new AssetsFieldPropertiesDto(), partitioning: 'language' });
expect(field.isLocalizable).toBeTruthy(); expect(field.isLocalizable).toBeTruthy();
}); });
it('should not return localizable if partitioning is invarient', () => { it('should not return localizable if partitioning is invarient', () => {
const field = createField(new AssetsFieldPropertiesDto(), 1, 'invariant'); const field = createField({ properties: new AssetsFieldPropertiesDto(), partitioning: 'invariant' });
expect(field.isLocalizable).toBeFalsy(); expect(field.isLocalizable).toBeFalsy();
}); });
}); });
describe('ArrayField', () => { describe('ArrayField', () => {
const field = createField(new ArrayFieldPropertiesDto({ isRequired: true, minItems: 1, maxItems: 5 })); const field = createField({ properties: new ArrayFieldPropertiesDto({ isRequired: true, minItems: 1, maxItems: 5 }) });
it('should create validators', () => { it('should create validators', () => {
expect(FieldValidatorsFactory.createValidators(field, false).length).toBe(2); expect(FieldsValidators.create(field, false).length).toBe(2);
}); });
it('should format to empty string if null', () => { it('should format to empty string if null', () => {
@ -144,10 +162,10 @@ describe('ArrayField', () => {
}); });
describe('AssetsField', () => { describe('AssetsField', () => {
const field = createField(new AssetsFieldPropertiesDto({ isRequired: true, minItems: 1, maxItems: 5 })); const field = createField({ properties: new AssetsFieldPropertiesDto({ isRequired: true, minItems: 1, maxItems: 5 }) });
it('should create validators', () => { it('should create validators', () => {
expect(FieldValidatorsFactory.createValidators(field, false).length).toBe(3); expect(FieldsValidators.create(field, false).length).toBe(3);
}); });
it('should format to empty string if null', () => { it('should format to empty string if null', () => {
@ -168,10 +186,10 @@ describe('AssetsField', () => {
}); });
describe('TagsField', () => { describe('TagsField', () => {
const field = createField(new TagsFieldPropertiesDto('Tags', { isRequired: true, minItems: 1, maxItems: 5 })); const field = createField({ properties: new TagsFieldPropertiesDto({ isRequired: true, minItems: 1, maxItems: 5 }) });
it('should create validators', () => { it('should create validators', () => {
expect(FieldValidatorsFactory.createValidators(field, false).length).toBe(2); expect(FieldsValidators.create(field, false).length).toBe(2);
}); });
it('should format to empty string if null', () => { it('should format to empty string if null', () => {
@ -192,10 +210,10 @@ describe('TagsField', () => {
}); });
describe('BooleanField', () => { describe('BooleanField', () => {
const field = createField(new BooleanFieldPropertiesDto('Checkbox', { isRequired: true })); const field = createField({ properties: new BooleanFieldPropertiesDto('Checkbox', { isRequired: true }) });
it('should create validators', () => { it('should create validators', () => {
expect(FieldValidatorsFactory.createValidators(field, false).length).toBe(1); expect(FieldsValidators.create(field, false).length).toBe(1);
}); });
it('should format to empty string if null', () => { it('should format to empty string if null', () => {
@ -211,7 +229,7 @@ describe('BooleanField', () => {
}); });
it('should return default value for default properties', () => { it('should return default value for default properties', () => {
const field2 = createField(new BooleanFieldPropertiesDto('Checkbox', { defaultValue: true })); const field2 = createField({ properties: new BooleanFieldPropertiesDto('Checkbox', { defaultValue: true }) });
expect(FieldDefaultValue.get(field2)).toBeTruthy(); expect(FieldDefaultValue.get(field2)).toBeTruthy();
}); });
@ -219,10 +237,10 @@ describe('BooleanField', () => {
describe('DateTimeField', () => { describe('DateTimeField', () => {
const now = DateTime.parseISO_UTC('2017-10-12T16:30:10Z'); const now = DateTime.parseISO_UTC('2017-10-12T16:30:10Z');
const field = createField(new DateTimeFieldPropertiesDto('DateTime', { isRequired: true })); const field = createField({ properties: new DateTimeFieldPropertiesDto('DateTime', { isRequired: true }) });
it('should create validators', () => { it('should create validators', () => {
expect(FieldValidatorsFactory.createValidators(field, false).length).toBe(1); expect(FieldsValidators.create(field, false).length).toBe(1);
}); });
it('should format to empty string if null', () => { it('should format to empty string if null', () => {
@ -234,41 +252,41 @@ describe('DateTimeField', () => {
}); });
it('should format to date', () => { it('should format to date', () => {
const dateField = createField(new DateTimeFieldPropertiesDto('Date')); const dateField = createField({ properties: new DateTimeFieldPropertiesDto('Date') });
expect(FieldFormatter.format(dateField, '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', () => { it('should format to date time', () => {
const field2 = createField(new DateTimeFieldPropertiesDto('DateTime')); const field2 = createField({ properties: new DateTimeFieldPropertiesDto('DateTime') });
expect(FieldFormatter.format(field2, '2017-12-12T16:00:00Z')).toBe('2017-12-12 16:00:00'); expect(FieldFormatter.format(field2, '2017-12-12T16:00:00Z')).toBe('2017-12-12 16:00:00');
}); });
it('should return default for DateFieldProperties', () => { it('should return default for DateFieldProperties', () => {
const field2 = createField(new DateTimeFieldPropertiesDto('DateTime', { defaultValue: '2017-10-12T16:00:00Z' })); const field2 = createField({ properties: new DateTimeFieldPropertiesDto('DateTime', { defaultValue: '2017-10-12T16:00:00Z' }) });
expect(FieldDefaultValue.get(field2)).toEqual('2017-10-12T16:00:00Z'); expect(FieldDefaultValue.get(field2)).toEqual('2017-10-12T16:00:00Z');
}); });
it('should return calculated date when Today for DateFieldProperties', () => { it('should return calculated date when Today for DateFieldProperties', () => {
const field2 = createField(new DateTimeFieldPropertiesDto('DateTime', { calculatedDefaultValue: 'Today' })); const field2 = createField({ properties: new DateTimeFieldPropertiesDto('DateTime', { calculatedDefaultValue: 'Today' }) });
expect(FieldDefaultValue.get(field2, now)).toEqual('2017-10-12'); expect(FieldDefaultValue.get(field2, now)).toEqual('2017-10-12');
}); });
it('should return calculated date when Now for DateFieldProperties', () => { it('should return calculated date when Now for DateFieldProperties', () => {
const field2 = createField(new DateTimeFieldPropertiesDto('DateTime', { calculatedDefaultValue: 'Now' })); const field2 = createField({ properties: new DateTimeFieldPropertiesDto('DateTime', { calculatedDefaultValue: 'Now' }) });
expect(FieldDefaultValue.get(field2, now)).toEqual('2017-10-12T16:30:10Z'); expect(FieldDefaultValue.get(field2, now)).toEqual('2017-10-12T16:30:10Z');
}); });
}); });
describe('GeolocationField', () => { describe('GeolocationField', () => {
const field = createField(new GeolocationFieldPropertiesDto({ isRequired: true })); const field = createField({ properties: new GeolocationFieldPropertiesDto({ isRequired: true }) });
it('should create validators', () => { it('should create validators', () => {
expect(FieldValidatorsFactory.createValidators(field, false).length).toBe(1); expect(FieldsValidators.create(field, false).length).toBe(1);
}); });
it('should format to empty string if null', () => { it('should format to empty string if null', () => {
@ -285,10 +303,10 @@ describe('GeolocationField', () => {
}); });
describe('JsonField', () => { describe('JsonField', () => {
const field = createField(new JsonFieldPropertiesDto({ isRequired: true })); const field = createField({ properties: new JsonFieldPropertiesDto({ isRequired: true }) });
it('should create validators', () => { it('should create validators', () => {
expect(FieldValidatorsFactory.createValidators(field, false).length).toBe(1); expect(FieldsValidators.create(field, false).length).toBe(1);
}); });
it('should format to empty string if null', () => { it('should format to empty string if null', () => {
@ -305,10 +323,10 @@ describe('JsonField', () => {
}); });
describe('NumberField', () => { describe('NumberField', () => {
const field = createField(new NumberFieldPropertiesDto('Input', { isRequired: true, minValue: 1, maxValue: 6, allowedValues: [1, 3] })); const field = createField({ properties: new NumberFieldPropertiesDto('Input', { isRequired: true, minValue: 1, maxValue: 6, allowedValues: [1, 3] }) });
it('should create validators', () => { it('should create validators', () => {
expect(FieldValidatorsFactory.createValidators(field, false).length).toBe(3); expect(FieldsValidators.create(field, false).length).toBe(3);
}); });
it('should format to empty string if null', () => { it('should format to empty string if null', () => {
@ -320,47 +338,47 @@ describe('NumberField', () => {
}); });
it('should format to stars if html allowed', () => { it('should format to stars if html allowed', () => {
const field2 = createField(new NumberFieldPropertiesDto('Stars')); const field2 = createField({ properties: new NumberFieldPropertiesDto('Stars') });
expect(FieldFormatter.format(field2, 3)).toEqual(new HtmlValue('&#9733; &#9733; &#9733; ')); expect(FieldFormatter.format(field2, 3)).toEqual(new HtmlValue('&#9733; &#9733; &#9733; '));
}); });
it('should format to short star view for many stars', () => { it('should format to short star view for many stars', () => {
const field2 = createField(new NumberFieldPropertiesDto('Stars')); const field2 = createField({ properties: new NumberFieldPropertiesDto('Stars') });
expect(FieldFormatter.format(field2, 42)).toEqual(new HtmlValue('&#9733; 42')); expect(FieldFormatter.format(field2, 42)).toEqual(new HtmlValue('&#9733; 42'));
}); });
it('should format to short star view for no stars', () => { it('should format to short star view for no stars', () => {
const field2 = createField(new NumberFieldPropertiesDto('Stars')); const field2 = createField({ properties: new NumberFieldPropertiesDto('Stars') });
expect(FieldFormatter.format(field2, 0)).toEqual(new HtmlValue('&#9733; 0')); expect(FieldFormatter.format(field2, 0)).toEqual(new HtmlValue('&#9733; 0'));
}); });
it('should format to short star view for negative stars', () => { it('should format to short star view for negative stars', () => {
const field2 = createField(new NumberFieldPropertiesDto('Stars')); const field2 = createField({ properties: new NumberFieldPropertiesDto('Stars') });
expect(FieldFormatter.format(field2, -13)).toEqual(new HtmlValue('&#9733; -13')); expect(FieldFormatter.format(field2, -13)).toEqual(new HtmlValue('&#9733; -13'));
}); });
it('should not format to stars if html not allowed', () => { it('should not format to stars if html not allowed', () => {
const field2 = createField(new NumberFieldPropertiesDto('Stars')); const field2 = createField({ properties: new NumberFieldPropertiesDto('Stars') });
expect(FieldFormatter.format(field2, 3, false)).toEqual('3'); expect(FieldFormatter.format(field2, 3, false)).toEqual('3');
}); });
it('should return default value for default properties', () => { it('should return default value for default properties', () => {
const field2 = createField(new NumberFieldPropertiesDto('Input', { defaultValue: 13 })); const field2 = createField({ properties: new NumberFieldPropertiesDto('Input', { defaultValue: 13 }) });
expect(FieldDefaultValue.get(field2)).toEqual(13); expect(FieldDefaultValue.get(field2)).toEqual(13);
}); });
}); });
describe('ReferencesField', () => { describe('ReferencesField', () => {
const field = createField(new ReferencesFieldPropertiesDto('List', { isRequired: true, minItems: 1, maxItems: 5 })); const field = createField({ properties: new ReferencesFieldPropertiesDto('List', { isRequired: true, minItems: 1, maxItems: 5 }) });
it('should create validators', () => { it('should create validators', () => {
expect(FieldValidatorsFactory.createValidators(field, false).length).toBe(3); expect(FieldsValidators.create(field, false).length).toBe(3);
}); });
it('should format to empty string if null', () => { it('should format to empty string if null', () => {
@ -381,10 +399,10 @@ describe('ReferencesField', () => {
}); });
describe('StringField', () => { describe('StringField', () => {
const field = createField(new StringFieldPropertiesDto('Input', { isRequired: true, pattern: 'pattern', minLength: 1, maxLength: 5, allowedValues: ['a', 'b'] })); const field = createField({ properties: new StringFieldPropertiesDto('Input', { isRequired: true, pattern: 'pattern', minLength: 1, maxLength: 5, allowedValues: ['a', 'b'] }) });
it('should create validators', () => { it('should create validators', () => {
expect(FieldValidatorsFactory.createValidators(field, false).length).toBe(4); expect(FieldsValidators.create(field, false).length).toBe(4);
}); });
it('should format to empty string if null', () => { it('should format to empty string if null', () => {
@ -396,7 +414,7 @@ describe('StringField', () => {
}); });
it('should return default value for default properties', () => { it('should return default value for default properties', () => {
const field2 = createField(new StringFieldPropertiesDto('Input', { defaultValue: 'MyDefault' })); const field2 = createField({ properties: new StringFieldPropertiesDto('Input', { defaultValue: 'MyDefault' }) });
expect(FieldDefaultValue.get(field2)).toEqual('MyDefault'); expect(FieldDefaultValue.get(field2)).toEqual('MyDefault');
}); });
@ -404,8 +422,8 @@ describe('StringField', () => {
describe('GetContentValue', () => { describe('GetContentValue', () => {
const language = new LanguageDto('en', 'English'); const language = new LanguageDto('en', 'English');
const fieldInvariant = createField(new NumberFieldPropertiesDto('Input'), 1, 'invariant'); const fieldInvariant = createField({ properties: new NumberFieldPropertiesDto('Input'), partitioning: 'invariant' });
const fieldLocalized = createField(new NumberFieldPropertiesDto('Input')); const fieldLocalized = createField({ properties: new NumberFieldPropertiesDto('Input') });
it('should resolve invariant field from references value', () => { it('should resolve invariant field from references value', () => {
const content: any = { const content: any = {
@ -498,10 +516,268 @@ describe('GetContentValue', () => {
}); });
}); });
function createSchema(properties: SchemaPropertiesDto, index = 1, fields: RootFieldDto[]) { describe('ContentForm', () => {
return new SchemaDetailsDto({}, 'id' + index, 'schema' + index, '', properties, false, true, null!, null!, null!, null!, null!, fields); const languages = ImmutableArray.of([
new AppLanguageDto({}, 'en', 'English', true, false, []),
new AppLanguageDto({}, 'de', 'English', false, true, [])
]);
const complexSchema = createSchema({ fields: [
createField({ id: 1, properties: createProperties('String'), partitioning: 'invariant' }),
createField({ id: 2, properties: createProperties('String'), isDisabled: true }),
createField({ id: 3, properties: createProperties('String', { isRequired: true }) }),
createField({ id: 4, properties: createProperties('Array'), partitioning: 'invariant', nested: [
createNestedField({ id: 41, properties: createProperties('String') }),
createNestedField({ id: 42, properties: createProperties('String', { defaultValue: 'Default' }), isDisabled: true })
]})
]});
describe('should resolve partitions', () => {
const partitions = new PartitionConfig(languages);
it('should return invariant partitions', () => {
const result = partitions.getAll(createField({ id: 3, properties: createProperties('String'), partitioning: 'invariant' }));
expect(result).toEqual([
{ key: 'iv', isOptional: false }
]);
});
it('should return language partitions', () => {
const result = partitions.getAll(createField({ id: 3, properties: createProperties('String') }));
expect(result).toEqual([
{ key: 'en', isOptional: false },
{ key: 'de', isOptional: true }
]);
});
it('should return partition for language', () => {
const result = partitions.get(languages.at(1));
expect(result).toEqual({ key: 'de', isOptional: true });
});
it('should return partition for no language', () => {
const result = partitions.get();
expect(result).toEqual({ key: 'iv', isOptional: false });
});
});
describe('with complex form', () => {
it('should not enabled disabled fields', () => {
const contentForm = createForm([
createField({ id: 1, properties: createProperties('String') }),
createField({ id: 2, properties: createProperties('String'), isDisabled: true })
]);
expectForm(contentForm.form, 'field1', { disabled: false });
expectForm(contentForm.form, 'field2', { disabled: true });
});
it('should not create required validator for optional language', () => {
const contentForm = createForm([
createField({ id: 3, properties: createProperties('String', { isRequired: true }) })
]);
expectForm(contentForm.form, 'field3.en', { invalid: true });
expectForm(contentForm.form, 'field3.de', { invalid: false });
});
it('should load with array and not enable disabled nested fields', () => {
const contentForm = createForm([
createField({ id: 4, properties: createProperties('Array'), partitioning: 'invariant', nested: [
createNestedField({ id: 41, properties: createProperties('String') }),
createNestedField({ id: 42, properties: createProperties('String'), isDisabled: true })
]})
]);
contentForm.load({
field4: {
iv: [{
nested41: 'Text'
}]
}
});
const nestedForm = contentForm.form.get('field4.iv') as FormArray;
const nestedItem = nestedForm.get([0])!;
expect(nestedForm.controls.length).toBe(1);
expectForm(nestedItem, 'nested41', { disabled: false, value: 'Text' });
expectForm(nestedItem, 'nested42', { disabled: true, value: null });
});
it('should add array item', () => {
const contentForm = createForm([
createField({ id: 4, properties: createProperties('Array'), partitioning: 'invariant', nested: [
createNestedField({ id: 41, properties: createProperties('String') }),
createNestedField({ id: 42, properties: createProperties('String', { defaultValue: 'Default' }), isDisabled: true })
]})
]);
contentForm.arrayItemInsert(complexSchema.fields[3], languages[0]);
const nestedForm = contentForm.form.get('field4.iv') as FormArray;
const nestedItem = nestedForm.get([0])!;
expect(nestedForm.controls.length).toBe(1);
expectForm(nestedItem, 'nested41', { disabled: false, value: null });
expectForm(nestedItem, 'nested42', { disabled: true, value: 'Default' });
});
it('should remove array item', () => {
const contentForm = createForm([
createField({ id: 4, properties: createProperties('Array'), partitioning: 'invariant', nested: [
createNestedField({ id: 41, properties: createProperties('String') })
]})
]);
contentForm.arrayItemInsert(complexSchema.fields[3], languages[0]);
contentForm.arrayItemRemove(complexSchema.fields[3], languages[0], 0);
const nestedForm = contentForm.form.get('field4.iv') as FormArray;
expect(nestedForm.controls.length).toBe(0);
});
it('should not array item if field has no nested fields', () => {
const contentForm = createForm([
createField({ id: 4, properties: createProperties('Array'), partitioning: 'invariant' })
]);
const nestedForm = contentForm.form.get('field4.iv') as FormArray;
expect(nestedForm.controls.length).toBe(0);
});
function expectForm(parent: AbstractControl, path: string, test: { invalid?: boolean, disabled?: boolean, value?: any }) {
const form = parent.get(path);
if (form) {
for (let key in test) {
if (test.hasOwnProperty(key)) {
const a = form[key];
const e = test[key];
expect(a).toBe(e, `Expected ${key} of ${path} to be <${e}>, but found <${a}>.`);
}
}
} else {
expect(form).not.toBeNull(`Expected to find form ${path}, but form not found.`);
}
}
});
describe('for new content', () => {
let simpleForm: EditContentForm;
beforeEach(() => {
simpleForm = createForm([
createField({ id: 1, properties: createProperties('String'), partitioning: 'invariant' })
]);
});
it('should not be an unsaved change when nothing has changed', () => {
expect(simpleForm.hasChanged()).toBeFalsy();
});
it('should be an unsaved change when value has changed but not saved', () => {
simpleForm.form.setValue({ field1: { iv: 'Change' }});
expect(simpleForm.hasChanged()).toBeTruthy();
});
it('should not be an unsaved change when value has changed and saved', () => {
simpleForm.form.setValue({ field1: { iv: 'Change' }});
simpleForm.submit();
simpleForm.submitCompleted();
expect(simpleForm.hasChanged()).toBeFalsy();
});
});
describe('for editing content', () => {
let simpleForm: EditContentForm;
beforeEach(() => {
simpleForm = createForm([
createField({ id: 1, properties: createProperties('String'), partitioning: 'invariant' })
]);
simpleForm.load({ field1: { iv: 'Initial' } }, true);
});
it('should not be an unsaved change when nothing has changed', () => {
simpleForm.load({ field1: { iv: 'Initial' } }, true);
expect(simpleForm.hasChanged()).toBeFalsy();
});
it('should be an unsaved change when value has changed but not saved', () => {
simpleForm.form.setValue({ field1: { iv: 'Change' }});
expect(simpleForm.hasChanged()).toBeTruthy();
});
it('should be an unsaved change when value has been loaded but not saved', () => {
simpleForm.load({ field1: { iv: 'Prev' } });
expect(simpleForm.hasChanged()).toBeTruthy();
});
it('should not be an unsaved change when value has changed and saved', () => {
simpleForm.form.setValue({ field1: { iv: 'Change' }});
simpleForm.submit();
simpleForm.submitCompleted();
expect(simpleForm.hasChanged()).toBeFalsy();
});
it('should not be an unsaved change when value has been loaded but not saved', () => {
simpleForm.load({ field1: { iv: 'Prev' } });
simpleForm.submit();
simpleForm.submitCompleted();
expect(simpleForm.hasChanged()).toBeFalsy();
});
});
function createForm(fields: RootFieldDto[]) {
return new EditContentForm(languages,
createSchema({ fields }));
}
});
type SchemaValues = { properties?: SchemaPropertiesDto; id?: number; fields?: RootFieldDto[]; };
function createSchema({ properties, id, fields }: SchemaValues = {}) {
id = id || 1;
return new SchemaDetailsDto({},
`schema${1}`,
`schema${1}`,
'category',
properties || new SchemaPropertiesDto(), false, true,
creation,
creator,
modified,
modifier,
new Version('1'),
fields);
}
type FieldValues = { properties: FieldPropertiesDto; id?: number; partitioning?: string; isDisabled?: boolean, nested?: NestedFieldDto[] };
function createField({ properties, id, partitioning, isDisabled, nested }: FieldValues) {
id = id || 1;
return new RootFieldDto({}, id, `field${id}`, properties, partitioning || 'language', false, false, isDisabled, nested);
} }
function createField(properties: FieldPropertiesDto, index = 1, partitioning = 'language') { function createNestedField({ properties, id, isDisabled }: FieldValues) {
return new RootFieldDto({}, index, 'field' + index, properties, partitioning); id = id || 1;
return new NestedFieldDto({}, id, `nested${id}`, properties, 0, false, false, isDisabled);
} }

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

@ -110,7 +110,7 @@ export function getContentValue(content: ContentDto, language: LanguageDto, fiel
} }
export class FieldFormatter implements FieldPropertiesVisitor<FieldValue> { export class FieldFormatter implements FieldPropertiesVisitor<FieldValue> {
constructor( private constructor(
private readonly value: any, private readonly value: any,
private readonly allowHtml: boolean private readonly allowHtml: boolean
) { ) {
@ -208,14 +208,14 @@ export class FieldFormatter implements FieldPropertiesVisitor<FieldValue> {
} }
} }
export class FieldValidatorsFactory implements FieldPropertiesVisitor<ValidatorFn[]> { export class FieldsValidators implements FieldPropertiesVisitor<ValidatorFn[]> {
constructor( private constructor(
private readonly isOptional: boolean private readonly isOptional: boolean
) { ) {
} }
public static createValidators(field: FieldDto, isOptional: boolean) { public static create(field: FieldDto, isOptional: boolean) {
const validators = field.properties.accept(new FieldValidatorsFactory(isOptional)); const validators = field.properties.accept(new FieldsValidators(isOptional));
if (field.properties.isRequired && !isOptional) { if (field.properties.isRequired && !isOptional) {
validators.push(Validators.required); validators.push(Validators.required);
@ -332,11 +332,15 @@ export class FieldValidatorsFactory implements FieldPropertiesVisitor<ValidatorF
} }
export class FieldDefaultValue implements FieldPropertiesVisitor<any> { export class FieldDefaultValue implements FieldPropertiesVisitor<any> {
constructor( private constructor(
private readonly now?: DateTime private readonly now?: DateTime
) { ) {
} }
public static get(field: FieldDto, now?: DateTime) {
return field.properties.accept(new FieldDefaultValue(now));
}
public visitDateTime(properties: DateTimeFieldPropertiesDto): any { public visitDateTime(properties: DateTimeFieldPropertiesDto): any {
const now = this.now || DateTime.now(); const now = this.now || DateTime.now();
@ -349,10 +353,6 @@ export class FieldDefaultValue implements FieldPropertiesVisitor<any> {
} }
} }
public static get(field: FieldDto, now?: DateTime) {
return field.properties.accept(new FieldDefaultValue(now));
}
public visitArray(_: ArrayFieldPropertiesDto): any { public visitArray(_: ArrayFieldPropertiesDto): any {
return null; return null;
} }
@ -394,46 +394,65 @@ export class FieldDefaultValue implements FieldPropertiesVisitor<any> {
} }
} }
const NO_EMIT = { emitEvent: false };
const NO_EMIT_SELF = { emitEvent: false, onlySelf: true };
type Partition = { key: string, isOptional: boolean };
export class PartitionConfig {
private readonly invariant: Partition[] = [{ key: fieldInvariant, isOptional: false }];
private readonly languages: Partition[];
constructor(languages: ImmutableArray<AppLanguageDto>) {
this.languages = languages.values.map(l => this.get(l));
}
public get(language?: AppLanguageDto) {
if (!language) {
return this.invariant[0];
}
return { key: language.iso2Code, isOptional: language.isOptional };
}
public getAll(field: RootFieldDto) {
return field.isLocalizable ? this.languages : this.invariant;
}
}
export class EditContentForm extends Form<FormGroup, any> { export class EditContentForm extends Form<FormGroup, any> {
private readonly partitions: PartitionConfig;
private initialData: any; private initialData: any;
public value = value$(this.form); public value = value$(this.form);
constructor( constructor(languages: ImmutableArray<AppLanguageDto>,
private readonly schema: SchemaDetailsDto, private readonly schema: SchemaDetailsDto
private readonly languages: ImmutableArray<AppLanguageDto>
) { ) {
super(new FormGroup({})); super(new FormGroup({}));
this.partitions = new PartitionConfig(languages);
for (const field of schema.fields) { for (const field of schema.fields) {
if (field.properties.isContentField) { if (field.properties.isContentField) {
const fieldForm = new FormGroup({}); const fieldForm = new FormGroup({});
const fieldDefault = FieldDefaultValue.get(field); const fieldDefault = FieldDefaultValue.get(field);
const createControl = (isOptional: boolean) => { for (let { key, isOptional } of this.partitions.getAll(field)) {
const validators = FieldValidatorsFactory.createValidators(field, isOptional); const fieldValidators = FieldsValidators.create(field, isOptional);
if (field.isArray) { if (field.isArray) {
return new FormArray([], validators); fieldForm.setControl(key, new FormArray([], fieldValidators));
} else { } else {
return new FormControl(fieldDefault, validators); fieldForm.setControl(key, new FormControl(fieldDefault, fieldValidators));
} }
};
if (field.isLocalizable) {
for (let language of this.languages.values) {
fieldForm.setControl(language.iso2Code, createControl(language.isOptional));
}
} else {
fieldForm.setControl(fieldInvariant, createControl(false));
} }
this.form.setControl(field.name, fieldForm); this.form.setControl(field.name, fieldForm);
} }
} }
this.initialData = this.form.getRawValue(); this.extractPrevData();
this.enable(); this.enable();
} }
@ -444,85 +463,90 @@ export class EditContentForm extends Form<FormGroup, any> {
} }
public arrayItemRemove(field: RootFieldDto, language: AppLanguageDto, index: number) { public arrayItemRemove(field: RootFieldDto, language: AppLanguageDto, index: number) {
this.findArrayItemForm(field, language).removeAt(index); const partitionForm = this.findArrayItemForm(field, language);
if (partitionForm) {
this.removeItem(partitionForm, index);
}
} }
public arrayItemInsert(field: RootFieldDto, language: AppLanguageDto, source?: FormGroup) { public arrayItemInsert(field: RootFieldDto, language: AppLanguageDto, source?: FormGroup) {
if (field.nested.length > 0) { const partitionForm = this.findArrayItemForm(field, language);
const formControl = this.findArrayItemForm(field, language);
this.addArrayItem(field, language, formControl, source); if (partitionForm && field.nested.length > 0) {
this.addArrayItem(partitionForm, field, this.partitions.get(language), source);
} }
} }
private addArrayItem(field: RootFieldDto, language: AppLanguageDto | null, partitionForm: FormArray, source?: FormGroup) { private removeItem(partitionForm: FormArray, index: number) {
const itemForm = new FormGroup({}); partitionForm.removeAt(index);
}
let isOptional = field.isLocalizable && !!language && language.isOptional;
for (let nested of field.nested) { private addArrayItem(partitionForm: FormArray, field: RootFieldDto, partition: Partition, source?: FormGroup) {
if (nested.properties.isContentField) { const itemForm = new FormGroup({});
const nestedValidators = FieldValidatorsFactory.createValidators(nested, isOptional);
let value = FieldDefaultValue.get(nested); for (let nestedField of field.nested) {
if (nestedField.properties.isContentField) {
let value = FieldDefaultValue.get(nestedField);
if (source) { if (source) {
const sourceField = source.get(nested.name); const sourceField = source.get(nestedField.name);
if (sourceField) { if (sourceField) {
value = sourceField.value; value = sourceField.value;
} }
} }
itemForm.setControl(nested.name, new FormControl(value, nestedValidators)); const nestedValidators = FieldsValidators.create(nestedField, partition.isOptional);
const nestedForm = new FormControl(value, nestedValidators);
if (nestedField.isDisabled) {
nestedForm.disable(NO_EMIT);
}
itemForm.setControl(nestedField.name, nestedForm);
} }
} }
partitionForm.push(itemForm); partitionForm.push(itemForm);
} }
private findArrayItemForm(field: RootFieldDto, language: AppLanguageDto): FormArray { private findArrayItemForm(field: RootFieldDto, language: AppLanguageDto): FormArray | null {
const fieldForm = this.form.get(field.name)!; const fieldForm = this.form.get(field.name);
if (field.isLocalizable) { if (!fieldForm) {
return <FormArray>fieldForm.get(language.iso2Code)!; return null;
} else if (field.isLocalizable) {
return fieldForm.get(language.iso2Code) as FormArray;
} else { } else {
return <FormArray>fieldForm.get(fieldInvariant); return fieldForm.get(fieldInvariant) as FormArray;
} }
} }
public load(value: any, isInitial?: boolean) { public load(value: any, isInitial?: boolean) {
for (let field of this.schema.fields) { for (let field of this.schema.fields) {
if (field.isArray && field.nested.length > 0) { if (field.isArray && field.nested.length > 0) {
const fieldForm = <FormGroup>this.form.get(field.name); const fieldForm = this.form.get(field.name) as FormGroup;
if (!fieldForm) { if (fieldForm) {
continue; const fieldValue = value ? value[field.name] || {} : {};
}
const fieldValue = value ? value[field.name] || {} : {}; for (let partition of this.partitions.getAll(field)) {
const { key, isOptional } = partition;
const addControls = (key: string, language: AppLanguageDto | null) => { const partitionValidators = FieldsValidators.create(field, isOptional);
const partitionValidators = FieldValidatorsFactory.createValidators(field, !!language && language.isOptional); const partitionForm = new FormArray([], partitionValidators);
const partitionForm = new FormArray([], partitionValidators);
const partitionValue = fieldValue[key]; const partitionValue = fieldValue[key];
if (Types.isArray(partitionValue)) { if (Types.isArray(partitionValue)) {
for (let i = 0; i < partitionValue.length; i++) { for (let i = 0; i < partitionValue.length; i++) {
this.addArrayItem(field, language, partitionForm); this.addArrayItem(partitionForm, field, partition);
}
} }
}
fieldForm.setControl(key, partitionForm); fieldForm.setControl(key, partitionForm);
};
if (field.isLocalizable) {
for (let language of this.languages.values) {
addControls(language.iso2Code, language);
} }
} else {
addControls(fieldInvariant, null);
} }
} }
} }
@ -530,54 +554,61 @@ export class EditContentForm extends Form<FormGroup, any> {
super.load(value); super.load(value);
if (isInitial) { if (isInitial) {
this.initialData = this.form.getRawValue(); this.extractPrevData();
} }
} }
public disable() { public submitCompleted(options?: { newValue?: any, noReset?: boolean }) {
this.form.disable({ emitEvent: false }); super.submitCompleted(options);
this.extractPrevData();
}
protected disable() {
this.form.disable(NO_EMIT);
} }
protected enable() { protected enable() {
if (this.schema.fields.length === 0) { this.form.enable(NO_EMIT_SELF);
this.form.enable({ emitEvent: false });
return;
}
for (const field of this.schema.fields) { for (const field of this.schema.fields) {
const fieldForm = this.form.get(field.name); const fieldForm = this.form.get(field.name);
if (!fieldForm) { if (fieldForm) {
continue; if (field.isArray) {
} fieldForm.enable(NO_EMIT_SELF);
if (field.isArray) { for (let partitionForm of formControls(fieldForm)) {
fieldForm.enable({ emitEvent: false }); partitionForm.enable(NO_EMIT_SELF);
for (let partitionForm of formControls(fieldForm)) { for (let itemForm of formControls(partitionForm)) {
for (let itemForm of formControls(partitionForm)) { itemForm.enable(NO_EMIT_SELF);
for (let nested of field.nested) {
const nestedForm = itemForm.get(nested.name);
if (!nestedForm) { for (let nestedField of field.nested) {
continue; const nestedForm = itemForm.get(nestedField.name);
}
if (nested.isDisabled) { if (nestedForm) {
nestedForm.disable({ emitEvent: false }); if (nestedField.isDisabled) {
} else { nestedForm.disable(NO_EMIT);
nestedForm.enable({ emitEvent: false }); } else {
nestedForm.enable(NO_EMIT);
}
}
} }
} }
} }
} else if (field.isDisabled) {
fieldForm.disable(NO_EMIT);
} else {
fieldForm.enable(NO_EMIT);
} }
} else if (field.isDisabled) {
fieldForm.disable({ emitEvent: false });
} else {
fieldForm.enable({ emitEvent: false });
} }
} }
} }
private extractPrevData() {
this.initialData = this.form.getRawValue();
}
} }
export class PatchContentForm extends Form<FormGroup, any> { export class PatchContentForm extends Form<FormGroup, any> {
@ -588,7 +619,7 @@ export class PatchContentForm extends Form<FormGroup, any> {
super(new FormGroup({})); super(new FormGroup({}));
for (let field of this.schema.listFieldsEditable) { for (let field of this.schema.listFieldsEditable) {
const validators = FieldValidatorsFactory.createValidators(field, this.language.isOptional); const validators = FieldsValidators.create(field, this.language.isOptional);
this.form.setControl(field.name, new FormControl(undefined, validators)); this.form.setControl(field.name, new FormControl(undefined, validators));
} }

Loading…
Cancel
Save