Browse Source

Content form fixed and tested.

pull/414/head
Sebastian Stehle 6 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) {
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)
.subscribe(dto => {
if (compare) {
if (this.contentFormCompare === null) {
this.contentFormCompare = new EditContentForm(this.schema, this.languages);
}
this.contentFormCompare = new EditContentForm(this.languages, this.schema);
this.contentFormCompare.load(dto.payload);
this.contentFormCompare.setEnabled(false);
this.loadContent(this.content.dataDraft, false);
} else {
if (this.contentFormCompare) {
this.contentFormCompare = null;
}
this.contentFormCompare = null;
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 {
public listFields: RootFieldDto[];
public listFieldsEditable: RootFieldDto[];
public referenceFields: RootFieldDto[];
public readonly listFields: RootFieldDto[];
public readonly listFieldsEditable: RootFieldDto[];
public readonly referenceFields: RootFieldDto[];
constructor(links: ResourceLinks, id: string, name: string, category: string,
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 function createProperties(fieldType: FieldType, values: Object | null = null): FieldPropertiesDto {
export function createProperties(fieldType: FieldType, values?: any): FieldPropertiesDto {
let properties: FieldPropertiesDto;
switch (fieldType) {
case 'Array':
properties = new ArrayFieldPropertiesDto();
properties = new ArrayFieldPropertiesDto(values);
break;
case 'Assets':
properties = new AssetsFieldPropertiesDto();
properties = new AssetsFieldPropertiesDto(values);
break;
case 'Boolean':
properties = new BooleanFieldPropertiesDto('Checkbox');
properties = new BooleanFieldPropertiesDto('Checkbox', values);
break;
case 'DateTime':
properties = new DateTimeFieldPropertiesDto('DateTime');
properties = new DateTimeFieldPropertiesDto('DateTime', values);
break;
case 'Geolocation':
properties = new GeolocationFieldPropertiesDto();
properties = new GeolocationFieldPropertiesDto(values);
break;
case 'Json':
properties = new JsonFieldPropertiesDto();
properties = new JsonFieldPropertiesDto(values);
break;
case 'Number':
properties = new NumberFieldPropertiesDto('Input');
properties = new NumberFieldPropertiesDto('Input', values);
break;
case 'References':
properties = new ReferencesFieldPropertiesDto('List');
properties = new ReferencesFieldPropertiesDto('List', values);
break;
case 'String':
properties = new StringFieldPropertiesDto('Input');
properties = new StringFieldPropertiesDto('Input', values);
break;
case 'Tags':
properties = new TagsFieldPropertiesDto('Tags');
break;
case 'Tags':
properties = new TagsFieldPropertiesDto('Tags');
properties = new TagsFieldPropertiesDto(values);
break;
case 'UI':
properties = new UIFieldPropertiesDto();
properties = new UIFieldPropertiesDto(values);
break;
default:
throw 'Invalid properties type';
}
if (values) {
Object.assign(properties, values);
}
return properties;
}
@ -387,7 +380,7 @@ export class TagsFieldPropertiesDto extends FieldPropertiesDto {
return false;
}
constructor(editor: string,
constructor(
props?: Partial<TagsFieldPropertiesDto>
) {
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.
*/
import { AbstractControl, FormArray } from '@angular/forms';
import {
AppLanguageDto,
ArrayFieldPropertiesDto,
AssetsFieldPropertiesDto,
BooleanFieldPropertiesDto,
createProperties,
DateTime,
DateTimeFieldPropertiesDto,
EditContentForm,
FieldDefaultValue,
FieldFormatter,
FieldPropertiesDto,
FieldValidatorsFactory,
FieldsValidators,
GeolocationFieldPropertiesDto,
getContentValue,
HtmlValue,
ImmutableArray,
JsonFieldPropertiesDto,
LanguageDto,
NestedFieldDto,
NumberFieldPropertiesDto,
PartitionConfig,
ReferencesFieldPropertiesDto,
RootFieldDto,
SchemaDetailsDto,
SchemaPropertiesDto,
StringFieldPropertiesDto,
TagsFieldPropertiesDto
TagsFieldPropertiesDto,
Version
} from '@app/shared/internal';
import { TestValues } from './_test-helpers';
const {
modified,
modifier,
creation,
creator
} = TestValues;
describe('SchemaDetailsDto', () => {
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');
});
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');
});
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');
});
it('should return configured fields as list fields if no schema field are declared', () => {
const field1 = createField(new ArrayFieldPropertiesDto({ isListField: true }), 1);
const field2 = createField(new ArrayFieldPropertiesDto({ isListField: false }), 2);
const field3 = createField(new ArrayFieldPropertiesDto({ isListField: true }), 3);
const field1 = createField({ properties: new ArrayFieldPropertiesDto({ isListField: true }) });
const field2 = createField({ properties: new ArrayFieldPropertiesDto({ isListField: false }), id: 2 });
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]);
});
it('should return first fields as list fields if no schema field is declared', () => {
const field1 = createField(new ArrayFieldPropertiesDto(), 1);
const field2 = createField(new ArrayFieldPropertiesDto(), 2);
const field3 = createField(new ArrayFieldPropertiesDto(), 3);
const field1 = createField({ properties: new ArrayFieldPropertiesDto() });
const field2 = createField({ properties: new ArrayFieldPropertiesDto(), id: 2 });
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]);
});
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);
});
@ -77,53 +95,53 @@ describe('SchemaDetailsDto', () => {
describe('FieldDto', () => {
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');
});
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');
});
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');
});
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');
});
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('');
});
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();
});
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();
});
});
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', () => {
expect(FieldValidatorsFactory.createValidators(field, false).length).toBe(2);
expect(FieldsValidators.create(field, false).length).toBe(2);
});
it('should format to empty string if null', () => {
@ -144,10 +162,10 @@ describe('ArrayField', () => {
});
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', () => {
expect(FieldValidatorsFactory.createValidators(field, false).length).toBe(3);
expect(FieldsValidators.create(field, false).length).toBe(3);
});
it('should format to empty string if null', () => {
@ -168,10 +186,10 @@ describe('AssetsField', () => {
});
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', () => {
expect(FieldValidatorsFactory.createValidators(field, false).length).toBe(2);
expect(FieldsValidators.create(field, false).length).toBe(2);
});
it('should format to empty string if null', () => {
@ -192,10 +210,10 @@ describe('TagsField', () => {
});
describe('BooleanField', () => {
const field = createField(new BooleanFieldPropertiesDto('Checkbox', { isRequired: true }));
const field = createField({ properties: new BooleanFieldPropertiesDto('Checkbox', { isRequired: true }) });
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', () => {
@ -211,7 +229,7 @@ describe('BooleanField', () => {
});
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();
});
@ -219,10 +237,10 @@ describe('BooleanField', () => {
describe('DateTimeField', () => {
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', () => {
expect(FieldValidatorsFactory.createValidators(field, false).length).toBe(1);
expect(FieldsValidators.create(field, false).length).toBe(1);
});
it('should format to empty string if null', () => {
@ -234,41 +252,41 @@ describe('DateTimeField', () => {
});
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');
});
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');
});
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');
});
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');
});
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');
});
});
describe('GeolocationField', () => {
const field = createField(new GeolocationFieldPropertiesDto({ isRequired: true }));
const field = createField({ properties: new GeolocationFieldPropertiesDto({ isRequired: true }) });
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', () => {
@ -285,10 +303,10 @@ describe('GeolocationField', () => {
});
describe('JsonField', () => {
const field = createField(new JsonFieldPropertiesDto({ isRequired: true }));
const field = createField({ properties: new JsonFieldPropertiesDto({ isRequired: true }) });
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', () => {
@ -305,10 +323,10 @@ describe('JsonField', () => {
});
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', () => {
expect(FieldValidatorsFactory.createValidators(field, false).length).toBe(3);
expect(FieldsValidators.create(field, false).length).toBe(3);
});
it('should format to empty string if null', () => {
@ -320,47 +338,47 @@ describe('NumberField', () => {
});
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; '));
});
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'));
});
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'));
});
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'));
});
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');
});
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);
});
});
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', () => {
expect(FieldValidatorsFactory.createValidators(field, false).length).toBe(3);
expect(FieldsValidators.create(field, false).length).toBe(3);
});
it('should format to empty string if null', () => {
@ -381,10 +399,10 @@ describe('ReferencesField', () => {
});
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', () => {
expect(FieldValidatorsFactory.createValidators(field, false).length).toBe(4);
expect(FieldsValidators.create(field, false).length).toBe(4);
});
it('should format to empty string if null', () => {
@ -396,7 +414,7 @@ describe('StringField', () => {
});
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');
});
@ -404,8 +422,8 @@ describe('StringField', () => {
describe('GetContentValue', () => {
const language = new LanguageDto('en', 'English');
const fieldInvariant = createField(new NumberFieldPropertiesDto('Input'), 1, 'invariant');
const fieldLocalized = createField(new NumberFieldPropertiesDto('Input'));
const fieldInvariant = createField({ properties: new NumberFieldPropertiesDto('Input'), partitioning: 'invariant' });
const fieldLocalized = createField({ properties: new NumberFieldPropertiesDto('Input') });
it('should resolve invariant field from references value', () => {
const content: any = {
@ -498,10 +516,268 @@ describe('GetContentValue', () => {
});
});
function createSchema(properties: SchemaPropertiesDto, index = 1, fields: RootFieldDto[]) {
return new SchemaDetailsDto({}, 'id' + index, 'schema' + index, '', properties, false, true, null!, null!, null!, null!, null!, fields);
describe('ContentForm', () => {
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') {
return new RootFieldDto({}, index, 'field' + index, properties, partitioning);
function createNestedField({ properties, id, isDisabled }: FieldValues) {
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> {
constructor(
private constructor(
private readonly value: any,
private readonly allowHtml: boolean
) {
@ -208,14 +208,14 @@ export class FieldFormatter implements FieldPropertiesVisitor<FieldValue> {
}
}
export class FieldValidatorsFactory implements FieldPropertiesVisitor<ValidatorFn[]> {
constructor(
export class FieldsValidators implements FieldPropertiesVisitor<ValidatorFn[]> {
private constructor(
private readonly isOptional: boolean
) {
}
public static createValidators(field: FieldDto, isOptional: boolean) {
const validators = field.properties.accept(new FieldValidatorsFactory(isOptional));
public static create(field: FieldDto, isOptional: boolean) {
const validators = field.properties.accept(new FieldsValidators(isOptional));
if (field.properties.isRequired && !isOptional) {
validators.push(Validators.required);
@ -332,11 +332,15 @@ export class FieldValidatorsFactory implements FieldPropertiesVisitor<ValidatorF
}
export class FieldDefaultValue implements FieldPropertiesVisitor<any> {
constructor(
private constructor(
private readonly now?: DateTime
) {
}
public static get(field: FieldDto, now?: DateTime) {
return field.properties.accept(new FieldDefaultValue(now));
}
public visitDateTime(properties: DateTimeFieldPropertiesDto): any {
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 {
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> {
private readonly partitions: PartitionConfig;
private initialData: any;
public value = value$(this.form);
constructor(
private readonly schema: SchemaDetailsDto,
private readonly languages: ImmutableArray<AppLanguageDto>
constructor(languages: ImmutableArray<AppLanguageDto>,
private readonly schema: SchemaDetailsDto
) {
super(new FormGroup({}));
this.partitions = new PartitionConfig(languages);
for (const field of schema.fields) {
if (field.properties.isContentField) {
const fieldForm = new FormGroup({});
const fieldDefault = FieldDefaultValue.get(field);
const createControl = (isOptional: boolean) => {
const validators = FieldValidatorsFactory.createValidators(field, isOptional);
for (let { key, isOptional } of this.partitions.getAll(field)) {
const fieldValidators = FieldsValidators.create(field, isOptional);
if (field.isArray) {
return new FormArray([], validators);
fieldForm.setControl(key, new FormArray([], fieldValidators));
} 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.initialData = this.form.getRawValue();
this.extractPrevData();
this.enable();
}
@ -444,85 +463,90 @@ export class EditContentForm extends Form<FormGroup, any> {
}
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) {
if (field.nested.length > 0) {
const formControl = this.findArrayItemForm(field, language);
const partitionForm = 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) {
const itemForm = new FormGroup({});
let isOptional = field.isLocalizable && !!language && language.isOptional;
private removeItem(partitionForm: FormArray, index: number) {
partitionForm.removeAt(index);
}
for (let nested of field.nested) {
if (nested.properties.isContentField) {
const nestedValidators = FieldValidatorsFactory.createValidators(nested, isOptional);
private addArrayItem(partitionForm: FormArray, field: RootFieldDto, partition: Partition, source?: FormGroup) {
const itemForm = new FormGroup({});
let value = FieldDefaultValue.get(nested);
for (let nestedField of field.nested) {
if (nestedField.properties.isContentField) {
let value = FieldDefaultValue.get(nestedField);
if (source) {
const sourceField = source.get(nested.name);
const sourceField = source.get(nestedField.name);
if (sourceField) {
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);
}
private findArrayItemForm(field: RootFieldDto, language: AppLanguageDto): FormArray {
const fieldForm = this.form.get(field.name)!;
private findArrayItemForm(field: RootFieldDto, language: AppLanguageDto): FormArray | null {
const fieldForm = this.form.get(field.name);
if (field.isLocalizable) {
return <FormArray>fieldForm.get(language.iso2Code)!;
if (!fieldForm) {
return null;
} else if (field.isLocalizable) {
return fieldForm.get(language.iso2Code) as FormArray;
} else {
return <FormArray>fieldForm.get(fieldInvariant);
return fieldForm.get(fieldInvariant) as FormArray;
}
}
public load(value: any, isInitial?: boolean) {
for (let field of this.schema.fields) {
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) {
continue;
}
if (fieldForm) {
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 = FieldValidatorsFactory.createValidators(field, !!language && language.isOptional);
const partitionForm = new FormArray([], partitionValidators);
const partitionValidators = FieldsValidators.create(field, isOptional);
const partitionForm = new FormArray([], partitionValidators);
const partitionValue = fieldValue[key];
const partitionValue = fieldValue[key];
if (Types.isArray(partitionValue)) {
for (let i = 0; i < partitionValue.length; i++) {
this.addArrayItem(field, language, partitionForm);
if (Types.isArray(partitionValue)) {
for (let i = 0; i < partitionValue.length; i++) {
this.addArrayItem(partitionForm, field, partition);
}
}
}
fieldForm.setControl(key, partitionForm);
};
if (field.isLocalizable) {
for (let language of this.languages.values) {
addControls(language.iso2Code, language);
fieldForm.setControl(key, partitionForm);
}
} else {
addControls(fieldInvariant, null);
}
}
}
@ -530,54 +554,61 @@ export class EditContentForm extends Form<FormGroup, any> {
super.load(value);
if (isInitial) {
this.initialData = this.form.getRawValue();
this.extractPrevData();
}
}
public disable() {
this.form.disable({ emitEvent: false });
public submitCompleted(options?: { newValue?: any, noReset?: boolean }) {
super.submitCompleted(options);
this.extractPrevData();
}
protected disable() {
this.form.disable(NO_EMIT);
}
protected enable() {
if (this.schema.fields.length === 0) {
this.form.enable({ emitEvent: false });
return;
}
this.form.enable(NO_EMIT_SELF);
for (const field of this.schema.fields) {
const fieldForm = this.form.get(field.name);
if (!fieldForm) {
continue;
}
if (fieldForm) {
if (field.isArray) {
fieldForm.enable(NO_EMIT_SELF);
if (field.isArray) {
fieldForm.enable({ emitEvent: false });
for (let partitionForm of formControls(fieldForm)) {
partitionForm.enable(NO_EMIT_SELF);
for (let partitionForm of formControls(fieldForm)) {
for (let itemForm of formControls(partitionForm)) {
for (let nested of field.nested) {
const nestedForm = itemForm.get(nested.name);
for (let itemForm of formControls(partitionForm)) {
itemForm.enable(NO_EMIT_SELF);
if (!nestedForm) {
continue;
}
for (let nestedField of field.nested) {
const nestedForm = itemForm.get(nestedField.name);
if (nested.isDisabled) {
nestedForm.disable({ emitEvent: false });
} else {
nestedForm.enable({ emitEvent: false });
if (nestedForm) {
if (nestedField.isDisabled) {
nestedForm.disable(NO_EMIT);
} 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> {
@ -588,7 +619,7 @@ export class PatchContentForm extends Form<FormGroup, any> {
super(new FormGroup({}));
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));
}

Loading…
Cancel
Save