Browse Source

Default value for arrays (#1024)

* Default value for array and components.

* Fix form group.

* Fix tests
pull/1025/head
Sebastian Stehle 3 years ago
committed by GitHub
parent
commit
e243045617
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 14
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayCalculatedDefaultValue.cs
  2. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayFieldProperties.cs
  3. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ComponentsFieldProperties.cs
  4. 20
      backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/DefaultValueFactory.cs
  5. 5
      backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ArrayFieldPropertiesDto.cs
  6. 5
      backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ComponentsFieldPropertiesDto.cs
  7. 40
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/DefaultValueFactoryTests.cs
  8. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/SchemaState.json
  9. 2
      frontend/src/app/features/schemas/module.ts
  10. 3
      frontend/src/app/features/schemas/pages/schema/fields/forms/field-form-ui.component.html
  11. 11
      frontend/src/app/features/schemas/pages/schema/fields/types/array-ui.component.html
  12. 2
      frontend/src/app/features/schemas/pages/schema/fields/types/array-ui.component.scss
  13. 30
      frontend/src/app/features/schemas/pages/schema/fields/types/array-ui.component.ts
  14. 11
      frontend/src/app/features/schemas/pages/schema/fields/types/components-ui.component.html
  15. 4
      frontend/src/app/features/schemas/pages/schema/fields/types/components-ui.component.ts
  16. 12
      frontend/src/app/framework/angular/forms/extended-form-array.spec.ts
  17. 10
      frontend/src/app/framework/angular/forms/extended-form-array.ts
  18. 18
      frontend/src/app/framework/angular/forms/extended-form-group.spec.ts
  19. 10
      frontend/src/app/framework/angular/forms/extended-form-group.ts
  20. 2
      frontend/src/app/framework/angular/forms/templated-form-array.ts
  21. 2
      frontend/src/app/framework/angular/forms/templated-form-group.ts
  22. 4
      frontend/src/app/shared/services/schemas.types.ts
  23. 20
      frontend/src/app/shared/state/contents.forms.visitors.spec.ts
  24. 20
      frontend/src/app/shared/state/contents.forms.visitors.ts
  25. 2
      frontend/src/app/shared/state/schemas.forms.ts

14
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayCalculatedDefaultValue.cs

@ -0,0 +1,14 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.Schemas;
public enum ArrayCalculatedDefaultValue
{
EmptyArray,
Null
}

2
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayFieldProperties.cs

@ -15,6 +15,8 @@ public sealed record ArrayFieldProperties : FieldProperties
public int? MaxItems { get; init; }
public ArrayCalculatedDefaultValue CalculatedDefaultValue { get; init; }
public ReadonlyList<string>? UniqueFields { get; init; }
public override T Accept<T, TArgs>(IFieldPropertiesVisitor<T, TArgs> visitor, TArgs args)

2
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ComponentsFieldProperties.cs

@ -18,6 +18,8 @@ public sealed record ComponentsFieldProperties : FieldProperties
public ReadonlyList<string>? UniqueFields { get; init; }
public ArrayCalculatedDefaultValue CalculatedDefaultValue { get; init; }
public DomainId SchemaId
{
init

20
backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/DefaultValueFactory.cs

@ -33,6 +33,21 @@ public sealed class DefaultValueFactory : IFieldPropertiesVisitor<JsonValue, Def
public JsonValue Visit(ArrayFieldProperties properties, Args args)
{
if (properties.CalculatedDefaultValue == ArrayCalculatedDefaultValue.Null)
{
return JsonValue.Null;
}
return new JsonArray();
}
public JsonValue Visit(ComponentsFieldProperties properties, Args args)
{
if (properties.CalculatedDefaultValue == ArrayCalculatedDefaultValue.Null)
{
return JsonValue.Null;
}
return new JsonArray();
}
@ -55,11 +70,6 @@ public sealed class DefaultValueFactory : IFieldPropertiesVisitor<JsonValue, Def
return JsonValue.Null;
}
public JsonValue Visit(ComponentsFieldProperties properties, Args args)
{
return new JsonArray();
}
public JsonValue Visit(GeolocationFieldProperties properties, Args args)
{
return JsonValue.Null;

5
backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ArrayFieldPropertiesDto.cs

@ -25,6 +25,11 @@ public sealed class ArrayFieldPropertiesDto : FieldPropertiesDto
/// </summary>
public int? MaxItems { get; set; }
/// <summary>
/// The calculated default value for the field value.
/// </summary>
public ArrayCalculatedDefaultValue CalculatedDefaultValue { get; set; }
/// <summary>
/// The fields that must be unique.
/// </summary>

5
backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ComponentsFieldPropertiesDto.cs

@ -26,6 +26,11 @@ public sealed class ComponentsFieldPropertiesDto : FieldPropertiesDto
/// </summary>
public int? MaxItems { get; set; }
/// <summary>
/// The calculated default value for the field value.
/// </summary>
public ArrayCalculatedDefaultValue CalculatedDefaultValue { get; set; }
/// <summary>
/// The ID of the embedded schemas.
/// </summary>

40
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/DefaultValueFactoryTests.cs

@ -19,6 +19,26 @@ public class DefaultValueFactoryTests
private readonly Instant now = Instant.FromUtc(2017, 10, 12, 16, 30, 10);
private readonly Language language = Language.DE;
[Fact]
public void Should_get_default_value_from_array_field()
{
var field =
Fields.Array(1, "1", Partitioning.Invariant,
new ArrayFieldProperties());
Assert.Equal(new JsonArray(), DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code));
}
[Fact]
public void Should_get_default_value_from_array_field_if_set_to_null()
{
var field =
Fields.Array(1, "1", Partitioning.Invariant,
new ArrayFieldProperties { CalculatedDefaultValue = ArrayCalculatedDefaultValue.Null });
Assert.Equal(JsonValue.Null, DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code));
}
[Fact]
public void Should_get_default_value_from_assets_field()
{
@ -83,6 +103,26 @@ public class DefaultValueFactoryTests
Assert.Equal(JsonValue.Null, DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code));
}
[Fact]
public void Should_get_default_value_from_components_field()
{
var field =
Fields.Components(1, "1", Partitioning.Invariant,
new ComponentsFieldProperties());
Assert.Equal(new JsonArray(), DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code));
}
[Fact]
public void Should_get_default_value_from_components_field_if_set_to_null()
{
var field =
Fields.Components(1, "1", Partitioning.Invariant,
new ComponentsFieldProperties { CalculatedDefaultValue = ArrayCalculatedDefaultValue.Null });
Assert.Equal(JsonValue.Null, DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code));
}
[Fact]
public void Should_get_default_value_from_datetime_field()
{

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/SchemaState.json

@ -130,6 +130,7 @@
"isDisabled": false,
"properties": {
"$type": "ComponentsField",
"calculatedDefaultValue": "EmptyArray",
"schemaId": "62a1d2a8-f08d-4870-8cb9-cb3ba286d56c",
"schemaIds": [
"62a1d2a8-f08d-4870-8cb9-cb3ba286d56c"
@ -267,6 +268,7 @@
"isDisabled": false,
"properties": {
"$type": "ArrayField",
"calculatedDefaultValue": "EmptyArray",
"isRequired": false,
"isRequiredOnPublish": false,
"isHalfWidth": false

2
frontend/src/app/features/schemas/module.ts

@ -9,6 +9,7 @@ import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HelpComponent, HistoryComponent, LoadSchemasGuard, SchemaMustExistGuard, SqxFrameworkModule, SqxSharedModule } from '@app/shared';
import { ArrayValidationComponent, AssetsUIComponent, AssetsValidationComponent, BooleanUIComponent, BooleanValidationComponent, ComponentsUIComponent, ComponentsValidationComponent, ComponentUIComponent, ComponentValidationComponent, DateTimeUIComponent, DateTimeValidationComponent, FieldComponent, FieldFormCommonComponent, FieldFormComponent, FieldFormUIComponent, FieldFormValidationComponent, FieldGroupComponent, FieldListComponent, FieldWizardComponent, GeolocationUIComponent, GeolocationValidationComponent, JsonMoreComponent, JsonUIComponent, JsonValidationComponent, NumberUIComponent, NumberValidationComponent, ReferencesUIComponent, ReferencesValidationComponent, SchemaEditFormComponent, SchemaExportFormComponent, SchemaFieldRulesFormComponent, SchemaFieldsComponent, SchemaFormComponent, SchemaPageComponent, SchemaPreviewUrlsFormComponent, SchemaScriptsFormComponent, SchemasPageComponent, SchemaUIFormComponent, SortableFieldListComponent, StringUIComponent, StringValidationComponent, TagsUIComponent, TagsValidationComponent } from './declarations';
import { ArrayUIComponent } from './pages/schema/fields/types/array-ui.component';
const routes: Routes = [
{
@ -51,6 +52,7 @@ const routes: Routes = [
SchemaMustExistGuard,
],
declarations: [
ArrayUIComponent,
ArrayValidationComponent,
AssetsUIComponent,
AssetsValidationComponent,

3
frontend/src/app/features/schemas/pages/schema/fields/forms/field-form-ui.component.html

@ -13,6 +13,9 @@
</div>
<ng-container [ngSwitch]="field.rawProperties.fieldType">
<ng-container *ngSwitchCase="'Array'">
<sqx-array-ui [fieldForm]="fieldForm" [field]="field" [properties]="field.rawProperties"></sqx-array-ui>
</ng-container>
<ng-container *ngSwitchCase="'Assets'">
<sqx-assets-ui [fieldForm]="fieldForm" [field]="field" [properties]="field.rawProperties"></sqx-assets-ui>
</ng-container>

11
frontend/src/app/features/schemas/pages/schema/fields/types/array-ui.component.html

@ -0,0 +1,11 @@
<div [formGroup]="fieldForm">
<div class="form-group row">
<label class="col-3 col-form-label">{{ 'schemas.field.defaultValue' | sqxTranslate }}</label>
<div class="col-3">
<select class="form-select" formControlName="calculatedDefaultValue">
<option *ngFor="let value of calculatedDefaultValues" [ngValue]="value">{{value}}</option>
</select>
</div>
</div>
</div>

2
frontend/src/app/features/schemas/pages/schema/fields/types/array-ui.component.scss

@ -0,0 +1,2 @@
@import 'mixins';
@import 'vars';

30
frontend/src/app/features/schemas/pages/schema/fields/types/array-ui.component.ts

@ -0,0 +1,30 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component, Input } from '@angular/core';
import { UntypedFormGroup } from '@angular/forms';
import { ArrayFieldPropertiesDto, FieldDto } from '@app/shared';
const CALCULATED_DEFAULT_VALUES: ReadonlyArray<string> = ['EmptyArray', 'Null'];
@Component({
selector: 'sqx-array-ui',
styleUrls: ['array-ui.component.scss'],
templateUrl: 'array-ui.component.html',
})
export class ArrayUIComponent {
@Input({ required: true })
public fieldForm!: UntypedFormGroup;
@Input({ required: true })
public field!: FieldDto;
@Input({ required: true })
public properties!: ArrayFieldPropertiesDto;
public calculatedDefaultValues = CALCULATED_DEFAULT_VALUES;
}

11
frontend/src/app/features/schemas/pages/schema/fields/types/components-ui.component.html

@ -0,0 +1,11 @@
<div [formGroup]="fieldForm">
<div class="form-group row">
<label class="col-3 col-form-label">{{ 'schemas.field.defaultValue' | sqxTranslate }}</label>
<div class="col-3">
<select class="form-select" formControlName="calculatedDefaultValue">
<option *ngFor="let value of calculatedDefaultValues" [ngValue]="value">{{value}}</option>
</select>
</div>
</div>
</div>

4
frontend/src/app/features/schemas/pages/schema/fields/types/components-ui.component.ts

@ -9,6 +9,8 @@ import { Component, Input } from '@angular/core';
import { UntypedFormGroup } from '@angular/forms';
import { FieldDto, ReferencesFieldPropertiesDto } from '@app/shared';
const CALCULATED_DEFAULT_VALUES: ReadonlyArray<string> = ['EmptyArray', 'Null'];
@Component({
selector: 'sqx-components-ui',
styleUrls: ['components-ui.component.scss'],
@ -23,4 +25,6 @@ export class ComponentsUIComponent {
@Input({ required: true })
public properties!: ReferencesFieldPropertiesDto;
public calculatedDefaultValues = CALCULATED_DEFAULT_VALUES;
}

12
frontend/src/app/framework/angular/forms/extended-form-array.spec.ts

@ -41,6 +41,18 @@ describe('UndefinableFormArray', () => {
valueActual: [1],
}];
it('should initialize with undefined', () => {
const control = new UndefinableFormArray();
expect(control.value).toBeUndefined();
});
it('should initialize with empty array', () => {
const control = new UndefinableFormArray([]);
expect(control.value).toEqual([]);
});
it('should provide value even if controls are disabled', () => {
const control = new UndefinableFormArray([
new UntypedFormControl('1'),

10
frontend/src/app/framework/angular/forms/extended-form-array.ts

@ -25,8 +25,8 @@ export class ExtendedFormArray extends UntypedFormArray {
export class UndefinableFormArray extends ExtendedFormArray {
private isUndefined = false;
constructor(controls: AbstractControl[], validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null) {
super(controls, validatorOrOpts, asyncValidator);
constructor(controls?: AbstractControl[], validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null) {
super(controls || [], validatorOrOpts, asyncValidator);
const reduce = (this as any)['_reduceValue'];
@ -37,6 +37,12 @@ export class UndefinableFormArray extends ExtendedFormArray {
return reduce.apply(this);
}
};
if (Types.isUndefined(controls)) {
this.isUndefined = true;
super.reset([], { emitEvent: false });
}
}
public getRawValue() {

18
frontend/src/app/framework/angular/forms/extended-form-group.spec.ts

@ -8,7 +8,7 @@
import { UntypedFormControl, UntypedFormGroup } from '@angular/forms';
import { ExtendedFormGroup, UndefinableFormGroup } from './extended-form-group';
describe('UndefinableFormGroup', () => {
describe('ExtendedFormGroup', () => {
it('should provide value even if controls are disabled', () => {
const control = new ExtendedFormGroup({
test1: new UntypedFormControl('1'),
@ -23,7 +23,7 @@ describe('UndefinableFormGroup', () => {
});
});
describe('ExtendedFormGroup', () => {
describe('UndefinableFormGroup', () => {
const tests = [{
name: 'undefined (on)',
undefinable: true,
@ -41,8 +41,20 @@ describe('ExtendedFormGroup', () => {
valueActual: { field: 1 },
}];
it('should initialize with undefined', () => {
const control = new UndefinableFormGroup();
expect(control.value).toBeUndefined();
});
it('should initialize with empty array', () => {
const control = new UndefinableFormGroup({});
expect(control.value).toEqual({});
});
it('should provide value even if controls are disabled', () => {
const control = new ExtendedFormGroup({
const control = new UndefinableFormGroup({
test1: new UntypedFormControl('1'),
test2: new UntypedFormControl('2'),
});

10
frontend/src/app/framework/angular/forms/extended-form-group.ts

@ -31,8 +31,8 @@ export class ExtendedFormGroup extends UntypedFormGroup {
export class UndefinableFormGroup extends ExtendedFormGroup {
private isUndefined = false;
constructor(controls: { [key: string]: AbstractControl }, validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null) {
super(controls, validatorOrOpts, asyncValidator);
constructor(controls?: { [key: string]: AbstractControl }, validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null) {
super(controls || {}, validatorOrOpts, asyncValidator);
const reduce = (this as any)['_reduceValue'];
@ -43,6 +43,12 @@ export class UndefinableFormGroup extends ExtendedFormGroup {
return reduce.apply(this);
}
};
if (Types.isUndefined(controls)) {
this.isUndefined = true;
super.reset({}, { emitEvent: false });
}
}
public getRawValue() {

2
frontend/src/app/framework/angular/forms/templated-form-array.ts

@ -21,7 +21,7 @@ export class TemplatedFormArray extends UndefinableFormArray {
constructor(public readonly template: FormArrayTemplate,
validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null,
) {
super([], validatorOrOpts, asyncValidator);
super(undefined, validatorOrOpts, asyncValidator);
}
public setValue(value?: any[], options?: { onlySelf?: boolean; emitEvent?: boolean }) {

2
frontend/src/app/framework/angular/forms/templated-form-group.ts

@ -19,7 +19,7 @@ export class TemplatedFormGroup extends UndefinableFormGroup {
constructor(public readonly template: FormGroupTemplate,
validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null,
) {
super({}, validatorOrOpts, asyncValidator);
super(undefined, validatorOrOpts, asyncValidator);
}
public setValue(value?: {}, options?: { onlySelf?: boolean; emitEvent?: boolean }) {

4
frontend/src/app/shared/services/schemas.types.ts

@ -179,6 +179,7 @@ export abstract class FieldPropertiesDto {
export class ArrayFieldPropertiesDto extends FieldPropertiesDto {
public readonly fieldType = 'Array';
public readonly calculatedDefaultValue: 'EmptyArray' | 'Null' = 'EmptyArray';
public readonly maxItems?: number;
public readonly minItems?: number;
public readonly uniqueFields?: ReadonlyArray<string>;
@ -269,6 +270,7 @@ export class ComponentFieldPropertiesDto extends FieldPropertiesDto {
export class ComponentsFieldPropertiesDto extends FieldPropertiesDto {
public readonly fieldType = 'Components';
public readonly calculatedDefaultValue: 'EmptyArray' | 'Null' = 'EmptyArray';
public readonly schemaIds?: ReadonlyArray<string>;
public readonly maxItems?: number;
public readonly minItems?: number;
@ -293,7 +295,7 @@ export const DATETIME_FIELD_EDITORS: ReadonlyArray<DateTimeFieldEditor> = [
export class DateTimeFieldPropertiesDto extends FieldPropertiesDto {
public readonly fieldType = 'DateTime';
public readonly calculatedDefaultValue?: string;
public readonly calculatedDefaultValue?: 'Now' | 'Today';
public readonly defaultValue?: string;
public readonly defaultValues?: DefaultValue<string>;
public readonly format?: string;

20
frontend/src/app/shared/state/contents.forms.visitors.spec.ts

@ -38,8 +38,14 @@ describe('ArrayField', () => {
expect(FieldFormatter.format(field, 1)).toBe('0 Items');
});
it('should return default value as null', () => {
expect(FieldDefaultValue.get(field, 'iv')).toBeNull();
it('should return default value as empty array', () => {
expect(FieldDefaultValue.get(field, 'iv')).toEqual([]);
});
it('should return default value as null when configured', () => {
const field2 = createField({ properties: createProperties('Array', { calculatedDefaultValue: 'Null' }) });
expect(FieldDefaultValue.get(field2, 'iv')).toBeNull();
});
});
@ -130,8 +136,14 @@ describe('ComponentsField', () => {
expect(FieldFormatter.format(field, 1)).toBe('0 Components');
});
it('should return default value as null', () => {
expect(FieldDefaultValue.get(field, 'iv')).toBeNull();
it('should return default value as empty array', () => {
expect(FieldDefaultValue.get(field, 'iv')).toEqual([]);
});
it('should return default value as null when configured', () => {
const field2 = createField({ properties: createProperties('Components', { calculatedDefaultValue: 'Null' }) });
expect(FieldDefaultValue.get(field2, 'iv')).toBeNull();
});
});

20
frontend/src/app/shared/state/contents.forms.visitors.ts

@ -417,8 +417,20 @@ export class FieldDefaultValue implements FieldPropertiesVisitor<any> {
}
}
public visitArray(_: ArrayFieldPropertiesDto): any {
return null;
public visitArray(properties: ArrayFieldPropertiesDto): any {
if (properties.calculatedDefaultValue === 'Null') {
return null;
}
return [];
}
public visitComponents(properties: ComponentsFieldPropertiesDto): any {
if (properties.calculatedDefaultValue === 'Null') {
return null;
}
return [];
}
public visitAssets(properties: AssetsFieldPropertiesDto): any {
@ -433,10 +445,6 @@ export class FieldDefaultValue implements FieldPropertiesVisitor<any> {
return null;
}
public visitComponents(_: ComponentsFieldPropertiesDto): any {
return null;
}
public visitGeolocation(_: GeolocationFieldPropertiesDto): any {
return null;
}

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

@ -249,6 +249,7 @@ export class EditFieldFormVisitor implements FieldPropertiesVisitor<any> {
}
public visitArray() {
this.config['calculatedDefaultValue'] = new UntypedFormControl('EmptyArray');
this.config['maxItems'] = new UntypedFormControl(undefined);
this.config['minItems'] = new UntypedFormControl(undefined);
this.config['uniqueFields'] = new UntypedFormControl(undefined);
@ -288,6 +289,7 @@ export class EditFieldFormVisitor implements FieldPropertiesVisitor<any> {
}
public visitComponents() {
this.config['calculatedDefaultValue'] = new UntypedFormControl('EmptyArray');
this.config['schemaIds'] = new UntypedFormControl(undefined);
this.config['maxItems'] = new UntypedFormControl(undefined);
this.config['minItems'] = new UntypedFormControl(undefined);

Loading…
Cancel
Save