Browse Source

Refactoring/forms (#800)

* Get rid of formbuilder

* Temp

* Cleanup forms.

* More tests.

* More improvements.

* Cleanup.
pull/802/head
Sebastian Stehle 4 years ago
committed by GitHub
parent
commit
d41b63837d
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      backend/i18n/frontend_en.json
  2. 1
      backend/i18n/frontend_it.json
  3. 1
      backend/i18n/frontend_nl.json
  4. 1
      backend/i18n/frontend_zh.json
  5. 1
      backend/i18n/source/frontend_en.json
  6. 4
      frontend/app/features/administration/pages/restore/restore-page.component.ts
  7. 4
      frontend/app/features/administration/pages/users/user-page.component.ts
  8. 40
      frontend/app/features/administration/state/users.forms.ts
  9. 4
      frontend/app/features/assets/pages/asset-tag-dialog.component.ts
  10. 4
      frontend/app/features/content/pages/content/editor/content-field.component.ts
  11. 2
      frontend/app/features/content/pages/content/editor/field-copy-button.component.ts
  12. 4
      frontend/app/features/content/shared/forms/array-item.component.html
  13. 57
      frontend/app/features/content/shared/forms/array-item.component.ts
  14. 2
      frontend/app/features/content/shared/forms/field-editor.component.html
  15. 59
      frontend/app/features/content/shared/forms/iframe-editor.component.ts
  16. 4
      frontend/app/features/rules/pages/rule/rule-page.component.ts
  17. 4
      frontend/app/features/schemas/pages/schema/common/schema-edit-form.component.ts
  18. 4
      frontend/app/features/schemas/pages/schema/export/schema-export-form.component.ts
  19. 6
      frontend/app/features/schemas/pages/schema/fields/field-wizard.component.ts
  20. 4
      frontend/app/features/schemas/pages/schema/fields/field.component.ts
  21. 4
      frontend/app/features/schemas/pages/schema/preview/schema-preview-urls-form.component.ts
  22. 4
      frontend/app/features/schemas/pages/schema/rules/schema-field-rules-form.component.ts
  23. 4
      frontend/app/features/schemas/pages/schema/scripts/schema-scripts-form.component.ts
  24. 4
      frontend/app/features/schemas/pages/schemas/schema-form.component.ts
  25. 5
      frontend/app/features/schemas/pages/schemas/schemas-page.component.ts
  26. 4
      frontend/app/features/settings/pages/asset-scripts/asset-scripts-page.component.ts
  27. 4
      frontend/app/features/settings/pages/clients/client-add-form.component.ts
  28. 4
      frontend/app/features/settings/pages/contributors/contributor-add-form.component.ts
  29. 4
      frontend/app/features/settings/pages/contributors/import-contributors-dialog.component.ts
  30. 4
      frontend/app/features/settings/pages/languages/language-add-form.component.ts
  31. 4
      frontend/app/features/settings/pages/languages/language.component.ts
  32. 4
      frontend/app/features/settings/pages/more/more-page.component.ts
  33. 4
      frontend/app/features/settings/pages/roles/role-add-form.component.ts
  34. 4
      frontend/app/features/settings/pages/roles/role.component.ts
  35. 4
      frontend/app/features/settings/pages/settings/settings-page.component.ts
  36. 4
      frontend/app/features/settings/pages/workflows/workflow-add-form.component.ts
  37. 4
      frontend/app/framework/angular/forms/editable-title.component.html
  38. 25
      frontend/app/framework/angular/forms/editable-title.component.ts
  39. 128
      frontend/app/framework/angular/forms/extended-form-array.spec.ts
  40. 38
      frontend/app/framework/angular/forms/extended-form-array.ts
  41. 110
      frontend/app/framework/angular/forms/extended-form-group.spec.ts
  42. 44
      frontend/app/framework/angular/forms/extended-form-group.ts
  43. 12
      frontend/app/framework/angular/forms/forms-helper.ts
  44. 4
      frontend/app/framework/angular/forms/model.ts
  45. 2
      frontend/app/framework/angular/forms/templated-form-array.ts
  46. 2
      frontend/app/framework/angular/forms/templated-form-group.ts
  47. 89
      frontend/app/framework/angular/forms/undefinable-form-array.spec.ts
  48. 71
      frontend/app/framework/angular/forms/undefinable-form-group.spec.ts
  49. 8
      frontend/app/framework/declarations.ts
  50. 4
      frontend/app/shared/components/app-form.component.ts
  51. 4
      frontend/app/shared/components/assets/asset-dialog.component.ts
  52. 4
      frontend/app/shared/components/assets/asset-folder-dialog.component.ts
  53. 4
      frontend/app/shared/components/comments/comments.component.ts
  54. 21
      frontend/app/shared/components/forms/geolocation-editor.component.ts
  55. 8
      frontend/app/shared/components/search/search-form.component.ts
  56. 98
      frontend/app/shared/state/apps.forms.ts
  57. 3
      frontend/app/shared/state/assets.forms.spec.ts
  58. 110
      frontend/app/shared/state/assets.forms.ts
  59. 32
      frontend/app/shared/state/backups.forms.ts
  60. 34
      frontend/app/shared/state/clients.forms.ts
  61. 14
      frontend/app/shared/state/comments.form.ts
  62. 11
      frontend/app/shared/state/contents.forms-helpers.ts
  63. 126
      frontend/app/shared/state/contents.forms.spec.ts
  64. 106
      frontend/app/shared/state/contents.forms.ts
  65. 46
      frontend/app/shared/state/contributors.forms.ts
  66. 46
      frontend/app/shared/state/languages.forms.ts
  67. 44
      frontend/app/shared/state/roles.forms.ts
  68. 41
      frontend/app/shared/state/rules.forms.ts
  69. 416
      frontend/app/shared/state/schemas.forms.ts
  70. 22
      frontend/app/shared/state/workflows.forms.ts

1
backend/i18n/frontend_en.json

@ -408,6 +408,7 @@
"contents.changeStatusTo": "Change content item(s) to {action}", "contents.changeStatusTo": "Change content item(s) to {action}",
"contents.changeStatusToImmediately": "Set to {action} immediately.", "contents.changeStatusToImmediately": "Set to {action} immediately.",
"contents.changeStatusToLater": "Set to {action} at a later point date and time.", "contents.changeStatusToLater": "Set to {action} at a later point date and time.",
"contents.componentInvalid": "Invalid component, schema has been deleted.",
"contents.componentNoSchema": "Add at least one schema to set component.", "contents.componentNoSchema": "Add at least one schema to set component.",
"contents.componentsNoSchema": "Add at least one schema to add components.", "contents.componentsNoSchema": "Add at least one schema to add components.",
"contents.contentNotValid": "Content element not valid, please check the field with the red bar on the left in all languages (if localizable).", "contents.contentNotValid": "Content element not valid, please check the field with the red bar on the left in all languages (if localizable).",

1
backend/i18n/frontend_it.json

@ -408,6 +408,7 @@
"contents.changeStatusTo": "Cambia gli elementi del contenuto in {action}", "contents.changeStatusTo": "Cambia gli elementi del contenuto in {action}",
"contents.changeStatusToImmediately": "Imposta {action} immediatamente.", "contents.changeStatusToImmediately": "Imposta {action} immediatamente.",
"contents.changeStatusToLater": "Imposta {action} ad una data e ora successiva.", "contents.changeStatusToLater": "Imposta {action} ad una data e ora successiva.",
"contents.componentInvalid": "Invalid component, schema has been deleted.",
"contents.componentNoSchema": "Add at least one schema to set component.", "contents.componentNoSchema": "Add at least one schema to set component.",
"contents.componentsNoSchema": "Add at least one schema to add components.", "contents.componentsNoSchema": "Add at least one schema to add components.",
"contents.contentNotValid": "Un elemento del contenuto non è valido, verifica il campo con la barra rossa per tutte le lingue impostate (se presenti).", "contents.contentNotValid": "Un elemento del contenuto non è valido, verifica il campo con la barra rossa per tutte le lingue impostate (se presenti).",

1
backend/i18n/frontend_nl.json

@ -408,6 +408,7 @@
"contents.changeStatusTo": "Verander inhoud item(s) in {action}", "contents.changeStatusTo": "Verander inhoud item(s) in {action}",
"contents.changeStatusToImmediately": "Zet onmiddellijk op {action}.", "contents.changeStatusToImmediately": "Zet onmiddellijk op {action}.",
"contents.changeStatusToLater": "Zet op {action} op een latere datum en tijd.", "contents.changeStatusToLater": "Zet op {action} op een latere datum en tijd.",
"contents.componentInvalid": "Invalid component, schema has been deleted.",
"contents.componentNoSchema": "Add at least one schema to set component.", "contents.componentNoSchema": "Add at least one schema to set component.",
"contents.componentsNoSchema": "Add at least one schema to add components.", "contents.componentsNoSchema": "Add at least one schema to add components.",
"contents.contentNotValid": "Inhoudselement niet geldig, controleer het veld met de rode balk aan de linkerkant in alle talen (indien lokaliseerbaar).", "contents.contentNotValid": "Inhoudselement niet geldig, controleer het veld met de rode balk aan de linkerkant in alle talen (indien lokaliseerbaar).",

1
backend/i18n/frontend_zh.json

@ -408,6 +408,7 @@
"contents.changeStatusTo": "将内容项更改为 {action}", "contents.changeStatusTo": "将内容项更改为 {action}",
"contents.changeStatusToImmediately": "立即设置为 {action}。", "contents.changeStatusToImmediately": "立即设置为 {action}。",
"contents.changeStatusToLater": "在稍后的日期和时间设置为 {action}。", "contents.changeStatusToLater": "在稍后的日期和时间设置为 {action}。",
"contents.componentInvalid": "Invalid component, schema has been deleted.",
"contents.componentNoSchema": "添加至少一个Schemas来设置组件。", "contents.componentNoSchema": "添加至少一个Schemas来设置组件。",
"contents.componentsNoSchema": "添加至少一个Schemas来添加组件。", "contents.componentsNoSchema": "添加至少一个Schemas来添加组件。",
"contents.contentNotValid": "内容元素无效,请用所有语言(如果可本地化)检查左侧带有红色条的字段。", "contents.contentNotValid": "内容元素无效,请用所有语言(如果可本地化)检查左侧带有红色条的字段。",

1
backend/i18n/source/frontend_en.json

@ -408,6 +408,7 @@
"contents.changeStatusTo": "Change content item(s) to {action}", "contents.changeStatusTo": "Change content item(s) to {action}",
"contents.changeStatusToImmediately": "Set to {action} immediately.", "contents.changeStatusToImmediately": "Set to {action} immediately.",
"contents.changeStatusToLater": "Set to {action} at a later point date and time.", "contents.changeStatusToLater": "Set to {action} at a later point date and time.",
"contents.componentInvalid": "Invalid component, schema has been deleted.",
"contents.componentNoSchema": "Add at least one schema to set component.", "contents.componentNoSchema": "Add at least one schema to set component.",
"contents.componentsNoSchema": "Add at least one schema to add components.", "contents.componentsNoSchema": "Add at least one schema to add components.",
"contents.contentNotValid": "Content element not valid, please check the field with the red bar on the left in all languages (if localizable).", "contents.contentNotValid": "Content element not valid, please check the field with the red bar on the left in all languages (if localizable).",

4
frontend/app/features/administration/pages/restore/restore-page.component.ts

@ -6,7 +6,6 @@
*/ */
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { AuthService, BackupsService, DialogService, RestoreForm, switchSafe } from '@app/shared'; import { AuthService, BackupsService, DialogService, RestoreForm, switchSafe } from '@app/shared';
import { timer } from 'rxjs'; import { timer } from 'rxjs';
@ -16,7 +15,7 @@ import { timer } from 'rxjs';
templateUrl: './restore-page.component.html', templateUrl: './restore-page.component.html',
}) })
export class RestorePageComponent { export class RestorePageComponent {
public restoreForm = new RestoreForm(this.formBuilder); public restoreForm = new RestoreForm();
public restoreJob = public restoreJob =
timer(0, 2000).pipe(switchSafe(() => this.backupsService.getRestore())); timer(0, 2000).pipe(switchSafe(() => this.backupsService.getRestore()));
@ -25,7 +24,6 @@ export class RestorePageComponent {
public readonly authState: AuthService, public readonly authState: AuthService,
private readonly backupsService: BackupsService, private readonly backupsService: BackupsService,
private readonly dialogs: DialogService, private readonly dialogs: DialogService,
private readonly formBuilder: FormBuilder,
) { ) {
} }

4
frontend/app/features/administration/pages/users/user-page.component.ts

@ -6,7 +6,6 @@
*/ */
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { CreateUserDto, UserDto, UserForm, UsersState } from '@app/features/administration/internal'; import { CreateUserDto, UserDto, UserForm, UsersState } from '@app/features/administration/internal';
import { ResourceOwner } from '@app/shared'; import { ResourceOwner } from '@app/shared';
@ -20,11 +19,10 @@ export class UserPageComponent extends ResourceOwner implements OnInit {
public isEditable = false; public isEditable = false;
public user?: UserDto | null; public user?: UserDto | null;
public userForm = new UserForm(this.formBuilder); public userForm = new UserForm();
constructor( constructor(
public readonly usersState: UsersState, public readonly usersState: UsersState,
private readonly formBuilder: FormBuilder,
private readonly route: ActivatedRoute, private readonly route: ActivatedRoute,
private readonly router: Router, private readonly router: Router,
) { ) {

40
frontend/app/features/administration/state/users.forms.ts

@ -5,39 +5,31 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormControl, Validators } from '@angular/forms';
import { Form, ValidatorsEx } from '@app/shared'; import { Form, ExtendedFormGroup, ValidatorsEx } from '@app/shared';
import { UpdateUserDto, UserDto } from './../services/users.service'; import { UpdateUserDto, UserDto } from './../services/users.service';
export class UserForm extends Form<FormGroup, UpdateUserDto, UserDto> { export class UserForm extends Form<ExtendedFormGroup, UpdateUserDto, UserDto> {
constructor( constructor() {
formBuilder: FormBuilder, super(new ExtendedFormGroup({
) { email: new FormControl('', [
super(formBuilder.group({
email: ['',
[
Validators.email, Validators.email,
Validators.required, Validators.required,
Validators.maxLength(100), Validators.maxLength(100),
], ]),
], displayName: new FormControl('', [
displayName: ['',
[
Validators.required, Validators.required,
Validators.maxLength(100), Validators.maxLength(100),
], ]),
], password: new FormControl('',
password: ['',
[
Validators.required, Validators.required,
], ),
], passwordConfirm: new FormControl('',
passwordConfirm: ['',
[
ValidatorsEx.match('password', 'i18n:users.passwordConfirmValidationMessage'), ValidatorsEx.match('password', 'i18n:users.passwordConfirmValidationMessage'),
], ),
], permissions: new FormControl('',
permissions: [''], Validators.nullValidator,
),
})); }));
} }

4
frontend/app/features/assets/pages/asset-tag-dialog.component.ts

@ -6,7 +6,6 @@
*/ */
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { AssetsState, RenameAssetTagForm } from '@app/shared/internal'; import { AssetsState, RenameAssetTagForm } from '@app/shared/internal';
@Component({ @Component({
@ -21,11 +20,10 @@ export class AssetTagDialogComponent implements OnInit {
@Input() @Input()
public tagName: string; public tagName: string;
public editForm = new RenameAssetTagForm(this.formBuilder); public editForm = new RenameAssetTagForm();
constructor( constructor(
private readonly assetsState: AssetsState, private readonly assetsState: AssetsState,
private readonly formBuilder: FormBuilder,
) { ) {
} }

4
frontend/app/features/content/pages/content/editor/content-field.component.ts

@ -91,12 +91,12 @@ export class ContentFieldComponent implements OnChanges {
public copy() { public copy() {
if (this.formModel && this.formModelCompare) { if (this.formModel && this.formModelCompare) {
if (this.showAllControls) { if (this.showAllControls) {
this.formModel.setValue(this.formModelCompare.getRawValue()); this.formModel.setValue(this.formModelCompare.form.value);
} else { } else {
const target = this.formModel.get(this.language.iso2Code); const target = this.formModel.get(this.language.iso2Code);
if (target) { if (target) {
target.setValue(this.formModelCompare.get(this.language.iso2Code)?.getRawValue()); target.setValue(this.formModelCompare.get(this.language.iso2Code)?.form.value);
} }
} }
} }

2
frontend/app/features/content/pages/content/editor/field-copy-button.component.ts

@ -49,7 +49,7 @@ export class FieldCopyButtonComponent implements OnChanges {
public copy() { public copy() {
if (this.copySource && this.copyTargets?.length > 0) { if (this.copySource && this.copyTargets?.length > 0) {
const source = this.formModel.get(this.copySource).getRawValue(); const source = this.formModel.get(this.copySource).form.value;
for (const target of this.copyTargets) { for (const target of this.copyTargets) {
if (target !== this.copySource) { if (target !== this.copySource) {

4
frontend/app/features/content/shared/forms/array-item.component.html

@ -54,5 +54,9 @@
[languages]="languages"> [languages]="languages">
</sqx-component-section> </sqx-component-section>
</div> </div>
<sqx-form-hint *ngIf="isInvalidComponent | async">
{{ 'contents.componentInvalid' | sqxTranslate }}
</sqx-form-hint>
</div> </div>
</div> </div>

57
frontend/app/features/content/shared/forms/array-item.component.ts

@ -7,7 +7,8 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, OnInit, Output, QueryList, SimpleChanges, ViewChildren } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, OnInit, Output, QueryList, SimpleChanges, ViewChildren } from '@angular/core';
import { AppLanguageDto, ComponentForm, EditContentForm, FieldDto, FieldFormatter, FieldSection, invalid$, ObjectFormBase, RootFieldDto, StatefulComponent, Types, valueProjection$ } from '@app/shared'; import { AppLanguageDto, ComponentForm, EditContentForm, FieldDto, FieldFormatter, FieldSection, invalid$, ObjectFormBase, RootFieldDto, StatefulComponent, Types, valueProjection$ } from '@app/shared';
import { Observable } from 'rxjs'; import { Observable, of } from 'rxjs';
import { map } from 'rxjs/operators';
import { ComponentSectionComponent } from './component-section.component'; import { ComponentSectionComponent } from './component-section.component';
interface State { interface State {
@ -69,6 +70,7 @@ export class ArrayItemComponent extends StatefulComponent<State> implements OnCh
public isCollapsed = false; public isCollapsed = false;
public isInvalid: Observable<boolean>; public isInvalid: Observable<boolean>;
public isInvalidComponent: Observable<boolean>;
public title: Observable<string>; public title: Observable<string>;
@ -87,34 +89,16 @@ export class ArrayItemComponent extends StatefulComponent<State> implements OnCh
if (changes['formModel']) { if (changes['formModel']) {
this.isInvalid = invalid$(this.formModel.form); this.isInvalid = invalid$(this.formModel.form);
this.title = valueProjection$(this.formModel.form, x => this.getTitle(x)); if (Types.is(this.formModel, ComponentForm)) {
this.isInvalidComponent = this.formModel.schemaChanges.pipe(map(x => !x));
} else {
this.isInvalidComponent = of(false);
} }
}
private getTitle(value: any) {
const values: string[] = [];
if (Types.is(this.formModel, ComponentForm) && this.formModel.schema) {
values.push(this.formModel.schema.displayName);
}
if (Types.is(this.formModel.field, RootFieldDto)) {
for (const field of this.formModel.field.nested) {
const fieldValue = value[field.name];
if (fieldValue) {
const formatted = FieldFormatter.format(field, fieldValue);
if (formatted) { this.title = valueProjection$(this.formModel.form, () => getTitle(this.formModel));
values.push(formatted);
}
}
} }
} }
return values.join(', ');
}
public collapse() { public collapse() {
this.next({ isCollapsed: true }); this.next({ isCollapsed: true });
} }
@ -149,3 +133,28 @@ export class ArrayItemComponent extends StatefulComponent<State> implements OnCh
return section.separator?.fieldId; return section.separator?.fieldId;
} }
} }
function getTitle(formModel: ObjectFormBase) {
const value = formModel.form.value;
const values: string[] = [];
if (Types.is(formModel, ComponentForm) && formModel.schema) {
values.push(formModel.schema.displayName);
}
if (Types.is(formModel.field, RootFieldDto)) {
for (const field of formModel.field.nested) {
const fieldValue = value[field.name];
if (fieldValue) {
const formatted = FieldFormatter.format(field, fieldValue);
if (formatted) {
values.push(formatted);
}
}
}
}
return values.join(', ');
}

2
frontend/app/features/content/shared/forms/field-editor.component.html

@ -11,7 +11,7 @@
<ng-container *ngIf="field.properties.editorUrl; else noEditor"> <ng-container *ngIf="field.properties.editorUrl; else noEditor">
<sqx-iframe-editor [url]="field.properties.editorUrl" #editor <sqx-iframe-editor [url]="field.properties.editorUrl" #editor
[context]="formContext" [context]="formContext"
[formControl]="$any(fieldForm)" [formControlBinding]="$any(fieldForm)"
[formValue]="form.valueChanges | async" [formValue]="form.valueChanges | async"
[formIndex]="index" [formIndex]="index"
[language]="language?.iso2Code"> [language]="language?.iso2Code">

59
frontend/app/features/content/shared/forms/iframe-editor.component.ts

@ -5,33 +5,27 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, HostListener, Input, OnChanges, OnDestroy, Renderer2, SimpleChanges, ViewChild } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, HostListener, Input, OnChanges, OnDestroy, Renderer2, SimpleChanges, ViewChild } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { AbstractControl } from '@angular/forms';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { DialogModel, DialogService, StatefulControlComponent, Types } from '@app/framework'; import { DialogModel, DialogService, disabled$, StatefulComponent, Types, value$ } from '@app/framework';
import { AppsState, AssetDto, computeEditorUrl } from '@app/shared'; import { AppsState, AssetDto, computeEditorUrl } from '@app/shared';
export const SQX_IFRAME_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => IFrameEditorComponent), multi: true,
};
interface State { interface State {
// True, when the editor is shown as fullscreen. // True, when the editor is shown as fullscreen.
isFullscreen: boolean; isFullscreen: boolean;
} }
@Component({ @Component({
selector: 'sqx-iframe-editor[context][formValue]', selector: 'sqx-iframe-editor[context][formValue][formControlBinding]',
styleUrls: ['./iframe-editor.component.scss'], styleUrls: ['./iframe-editor.component.scss'],
templateUrl: './iframe-editor.component.html', templateUrl: './iframe-editor.component.html',
providers: [
SQX_IFRAME_EDITOR_CONTROL_VALUE_ACCESSOR,
],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class IFrameEditorComponent extends StatefulControlComponent<State, any> implements OnChanges, OnDestroy { export class IFrameEditorComponent extends StatefulComponent<State> implements OnChanges, OnDestroy {
private value: any; private value: any;
private isInitialized = false; private isInitialized = false;
private isDisabled = false;
private assetsCorrelationId: any; private assetsCorrelationId: any;
@ViewChild('iframe', { static: false }) @ViewChild('iframe', { static: false })
@ -55,9 +49,12 @@ export class IFrameEditorComponent extends StatefulControlComponent<State, any>
@Input() @Input()
public language?: string | null; public language?: string | null;
@Input()
public formControlBinding: AbstractControl;
@Input() @Input()
public set disabled(value: boolean | undefined | null) { public set disabled(value: boolean | undefined | null) {
this.setDisabledState(value === true); this.updatedisabled(value === true);
} }
@Input() @Input()
@ -81,11 +78,12 @@ export class IFrameEditorComponent extends StatefulControlComponent<State, any>
} }
public ngOnDestroy() { public ngOnDestroy() {
super.ngOnDestroy();
this.toggleFullscreen(false); this.toggleFullscreen(false);
} }
public ngOnChanges(changes: SimpleChanges) { public ngOnChanges(changes: SimpleChanges) {
if (this.iframe?.nativeElement) {
if (changes['formValue']) { if (changes['formValue']) {
this.sendFormValue(); this.sendFormValue();
} }
@ -97,6 +95,23 @@ export class IFrameEditorComponent extends StatefulControlComponent<State, any>
if (changes['formIndex']) { if (changes['formIndex']) {
this.sendMoved(); this.sendMoved();
} }
if (changes['formControlBinding']) {
this.unsubscribeAll();
const control = this.formControlBinding;
if (control) {
this.own(value$(control)
.subscribe(value => {
this.updateValue(value);
}));
this.own(disabled$(control)
.subscribe(isDisabled => {
this.updatedisabled(isDisabled);
}));
}
} }
} }
@ -135,10 +150,10 @@ export class IFrameEditorComponent extends StatefulControlComponent<State, any>
if (!Types.equals(this.value, value)) { if (!Types.equals(this.value, value)) {
this.value = value; this.value = value;
this.callChange(value); this.formControlBinding?.reset(value);
} }
} else if (type === 'touched') { } else if (type === 'touched') {
this.callTouched(); this.formControlBinding?.markAsTouched();
} else if (type === 'notifyInfo') { } else if (type === 'notifyInfo') {
const { text } = event.data; const { text } = event.data;
@ -182,15 +197,21 @@ export class IFrameEditorComponent extends StatefulControlComponent<State, any>
this.assetsDialog.hide(); this.assetsDialog.hide();
} }
public writeValue(obj: any) { public updateValue(obj: any) {
if (!Types.equals(obj, this.value)) {
this.value = obj; this.value = obj;
this.sendValue(); this.sendValue();
} }
}
public updatedisabled(isDisabled: boolean) {
if (isDisabled !== this.isDisabled) {
this.isDisabled === isDisabled;
public onDisabled() {
this.sendDisabled(); this.sendDisabled();
} }
}
public reset() { public reset() {
this.sendInit(); this.sendInit();
@ -209,7 +230,7 @@ export class IFrameEditorComponent extends StatefulControlComponent<State, any>
} }
private sendDisabled() { private sendDisabled() {
this.sendMessage('disabled', { isDisabled: this.snapshot.isDisabled }); this.sendMessage('disabled', { isDisabled: this.isDisabled });
} }
private sendFormValue() { private sendFormValue() {

4
frontend/app/features/rules/pages/rule/rule-page.component.ts

@ -6,7 +6,6 @@
*/ */
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { ActionForm, ALL_TRIGGERS, ResourceOwner, RuleDto, RuleElementDto, RulesService, RulesState, SchemasState, TriggerForm } from '@app/shared'; import { ActionForm, ALL_TRIGGERS, ResourceOwner, RuleDto, RuleElementDto, RulesService, RulesState, SchemasState, TriggerForm } from '@app/shared';
@ -45,7 +44,6 @@ export class RulePageComponent extends ResourceOwner implements OnInit {
public readonly rulesState: RulesState, public readonly rulesState: RulesState,
public readonly rulesService: RulesService, public readonly rulesService: RulesService,
public readonly schemasState: SchemasState, public readonly schemasState: SchemasState,
private readonly formBuilder: FormBuilder,
private readonly route: ActivatedRoute, private readonly route: ActivatedRoute,
private readonly router: Router, private readonly router: Router,
) { ) {
@ -97,7 +95,7 @@ export class RulePageComponent extends ResourceOwner implements OnInit {
} }
public selectTrigger(type: string, values = {}) { public selectTrigger(type: string, values = {}) {
const form = new TriggerForm(this.formBuilder, type); const form = new TriggerForm(type);
form.setEnabled(this.isEditable); form.setEnabled(this.isEditable);
form.load(values); form.load(values);

4
frontend/app/features/schemas/pages/schema/common/schema-edit-form.component.ts

@ -6,7 +6,6 @@
*/ */
import { Component, Input, OnChanges } from '@angular/core'; import { Component, Input, OnChanges } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { EditSchemaForm, SchemaDto, SchemasState } from '@app/shared'; import { EditSchemaForm, SchemaDto, SchemasState } from '@app/shared';
@Component({ @Component({
@ -18,12 +17,11 @@ export class SchemaEditFormComponent implements OnChanges {
@Input() @Input()
public schema: SchemaDto; public schema: SchemaDto;
public fieldForm = new EditSchemaForm(this.formBuilder); public fieldForm = new EditSchemaForm();
public isEditable?: boolean | null; public isEditable?: boolean | null;
constructor( constructor(
private readonly formBuilder: FormBuilder,
private readonly schemasState: SchemasState, private readonly schemasState: SchemasState,
) { ) {
} }

4
frontend/app/features/schemas/pages/schema/export/schema-export-form.component.ts

@ -6,7 +6,6 @@
*/ */
import { Component, Input, OnChanges } from '@angular/core'; import { Component, Input, OnChanges } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { SchemaDto, SchemasState, SynchronizeSchemaForm } from '@app/shared'; import { SchemaDto, SchemasState, SynchronizeSchemaForm } from '@app/shared';
@Component({ @Component({
@ -18,12 +17,11 @@ export class SchemaExportFormComponent implements OnChanges {
@Input() @Input()
public schema: SchemaDto; public schema: SchemaDto;
public synchronizeForm = new SynchronizeSchemaForm(this.formBuilder); public synchronizeForm = new SynchronizeSchemaForm();
public isEditable = false; public isEditable = false;
constructor( constructor(
private readonly formBuilder: FormBuilder,
private readonly schemasState: SchemasState, private readonly schemasState: SchemasState,
) { ) {
} }

6
frontend/app/features/schemas/pages/schema/fields/field-wizard.component.ts

@ -6,7 +6,6 @@
*/ */
import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'; import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { AddFieldForm, AppSettingsDto, createProperties, EditFieldForm, FieldDto, fieldTypes, LanguagesState, RootFieldDto, SchemaDto, SchemasState, Types } from '@app/shared'; import { AddFieldForm, AppSettingsDto, createProperties, EditFieldForm, FieldDto, fieldTypes, LanguagesState, RootFieldDto, SchemaDto, SchemasState, Types } from '@app/shared';
const DEFAULT_FIELD = { name: '', partitioning: 'invariant', properties: createProperties('String') }; const DEFAULT_FIELD = { name: '', partitioning: 'invariant', properties: createProperties('String') };
@ -39,12 +38,11 @@ export class FieldWizardComponent implements OnInit {
public fieldTypes = fieldTypes; public fieldTypes = fieldTypes;
public field: FieldDto; public field: FieldDto;
public addFieldForm = new AddFieldForm(this.formBuilder); public addFieldForm = new AddFieldForm();
public editForm?: EditFieldForm; public editForm?: EditFieldForm;
constructor( constructor(
private readonly formBuilder: FormBuilder,
private readonly schemasState: SchemasState, private readonly schemasState: SchemasState,
public readonly languagesState: LanguagesState, public readonly languagesState: LanguagesState,
) { ) {
@ -76,7 +74,7 @@ export class FieldWizardComponent implements OnInit {
this.nameInput.nativeElement.focus(); this.nameInput.nativeElement.focus();
} }
} else if (edit) { } else if (edit) {
this.editForm = new EditFieldForm(this.formBuilder, this.field.properties); this.editForm = new EditFieldForm(this.field.properties);
this.editForm.load(this.field.properties); this.editForm.load(this.field.properties);
} else { } else {
this.emitComplete(); this.emitComplete();

4
frontend/app/features/schemas/pages/schema/fields/field.component.ts

@ -7,7 +7,6 @@
import { CdkDragDrop } from '@angular/cdk/drag-drop'; import { CdkDragDrop } from '@angular/cdk/drag-drop';
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { AppSettingsDto, createProperties, DialogModel, EditFieldForm, fadeAnimation, LanguageDto, ModalModel, NestedFieldDto, RootFieldDto, SchemaDto, SchemasState, sorted } from '@app/shared'; import { AppSettingsDto, createProperties, DialogModel, EditFieldForm, fadeAnimation, LanguageDto, ModalModel, NestedFieldDto, RootFieldDto, SchemaDto, SchemasState, sorted } from '@app/shared';
@Component({ @Component({
@ -50,7 +49,6 @@ export class FieldComponent implements OnChanges {
public addFieldDialog = new DialogModel(); public addFieldDialog = new DialogModel();
constructor( constructor(
private readonly formBuilder: FormBuilder,
private readonly schemasState: SchemasState, private readonly schemasState: SchemasState,
) { ) {
this.trackByFieldFn = this.trackByField.bind(this); this.trackByFieldFn = this.trackByField.bind(this);
@ -60,7 +58,7 @@ export class FieldComponent implements OnChanges {
if (changes['field']) { if (changes['field']) {
this.isEditable = this.field.canUpdate; this.isEditable = this.field.canUpdate;
this.editForm = new EditFieldForm(this.formBuilder, this.field.properties); this.editForm = new EditFieldForm(this.field.properties);
this.editForm.load(this.field.properties); this.editForm.load(this.field.properties);
} }
} }

4
frontend/app/features/schemas/pages/schema/preview/schema-preview-urls-form.component.ts

@ -6,7 +6,6 @@
*/ */
import { Component, Input, OnChanges } from '@angular/core'; import { Component, Input, OnChanges } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { ConfigurePreviewUrlsForm, SchemaDto, SchemasState } from '@app/shared'; import { ConfigurePreviewUrlsForm, SchemaDto, SchemasState } from '@app/shared';
@Component({ @Component({
@ -18,12 +17,11 @@ export class SchemaPreviewUrlsFormComponent implements OnChanges {
@Input() @Input()
public schema: SchemaDto; public schema: SchemaDto;
public editForm = new ConfigurePreviewUrlsForm(this.formBuilder); public editForm = new ConfigurePreviewUrlsForm();
public isEditable = false; public isEditable = false;
constructor( constructor(
private readonly formBuilder: FormBuilder,
private readonly schemasState: SchemasState, private readonly schemasState: SchemasState,
) { ) {
} }

4
frontend/app/features/schemas/pages/schema/rules/schema-field-rules-form.component.ts

@ -6,7 +6,6 @@
*/ */
import { Component, Input, OnChanges } from '@angular/core'; import { Component, Input, OnChanges } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { ConfigureFieldRulesForm, FIELD_RULE_ACTIONS, SchemaDto, SchemasState } from '@app/shared'; import { ConfigureFieldRulesForm, FIELD_RULE_ACTIONS, SchemaDto, SchemasState } from '@app/shared';
@Component({ @Component({
@ -18,7 +17,7 @@ export class SchemaFieldRulesFormComponent implements OnChanges {
@Input() @Input()
public schema: SchemaDto; public schema: SchemaDto;
public editForm = new ConfigureFieldRulesForm(this.formBuilder); public editForm = new ConfigureFieldRulesForm();
public fieldNames: ReadonlyArray<string>; public fieldNames: ReadonlyArray<string>;
public fieldActions = FIELD_RULE_ACTIONS; public fieldActions = FIELD_RULE_ACTIONS;
@ -26,7 +25,6 @@ export class SchemaFieldRulesFormComponent implements OnChanges {
public isEditable = false; public isEditable = false;
constructor( constructor(
private readonly formBuilder: FormBuilder,
private readonly schemasState: SchemasState, private readonly schemasState: SchemasState,
) { ) {
} }

4
frontend/app/features/schemas/pages/schema/scripts/schema-scripts-form.component.ts

@ -6,7 +6,6 @@
*/ */
import { Component, Input, OnChanges } from '@angular/core'; import { Component, Input, OnChanges } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { AppsState, EditSchemaScriptsForm, SchemaCompletions, SchemaDto, SchemasService, SchemasState } from '@app/shared'; import { AppsState, EditSchemaScriptsForm, SchemaCompletions, SchemaDto, SchemasService, SchemasState } from '@app/shared';
import { EMPTY, Observable } from 'rxjs'; import { EMPTY, Observable } from 'rxjs';
@ -22,13 +21,12 @@ export class SchemaScriptsFormComponent implements OnChanges {
public schemaScript = 'query'; public schemaScript = 'query';
public schemaCompletions: Observable<SchemaCompletions> = EMPTY; public schemaCompletions: Observable<SchemaCompletions> = EMPTY;
public editForm = new EditSchemaScriptsForm(this.formBuilder); public editForm = new EditSchemaScriptsForm();
public isEditable = false; public isEditable = false;
constructor( constructor(
private readonly appsState: AppsState, private readonly appsState: AppsState,
private readonly formBuilder: FormBuilder,
private readonly schemasState: SchemasState, private readonly schemasState: SchemasState,
private readonly schemasService: SchemasService, private readonly schemasService: SchemasService,
) { ) {

4
frontend/app/features/schemas/pages/schemas/schema-form.component.ts

@ -6,7 +6,6 @@
*/ */
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { ApiUrlConfig, AppsState, CreateSchemaForm, SchemaDto, SchemasState } from '@app/shared'; import { ApiUrlConfig, AppsState, CreateSchemaForm, SchemaDto, SchemasState } from '@app/shared';
@Component({ @Component({
@ -24,7 +23,7 @@ export class SchemaFormComponent implements OnInit {
@Input() @Input()
public import: any; public import: any;
public createForm = new CreateSchemaForm(this.formBuilder); public createForm = new CreateSchemaForm();
public showImport = false; public showImport = false;
@ -32,7 +31,6 @@ export class SchemaFormComponent implements OnInit {
public readonly apiUrl: ApiUrlConfig, public readonly apiUrl: ApiUrlConfig,
public readonly appsState: AppsState, public readonly appsState: AppsState,
public readonly schemasState: SchemasState, public readonly schemasState: SchemasState,
private readonly formBuilder: FormBuilder,
) { ) {
} }

5
frontend/app/features/schemas/pages/schemas/schemas-page.component.ts

@ -6,7 +6,7 @@
*/ */
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormControl } from '@angular/forms'; import { FormControl } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { CreateCategoryForm, DialogModel, getCategoryTree, MessageBus, ResourceOwner, SchemaCategory, SchemaDto, SchemasState, value$ } from '@app/shared'; import { CreateCategoryForm, DialogModel, getCategoryTree, MessageBus, ResourceOwner, SchemaCategory, SchemaDto, SchemasState, value$ } from '@app/shared';
import { combineLatest } from 'rxjs'; import { combineLatest } from 'rxjs';
@ -20,7 +20,7 @@ import { SchemaCloning } from './../messages';
}) })
export class SchemasPageComponent extends ResourceOwner implements OnInit { export class SchemasPageComponent extends ResourceOwner implements OnInit {
public addSchemaDialog = new DialogModel(); public addSchemaDialog = new DialogModel();
public addCategoryForm = new CreateCategoryForm(this.formBuilder); public addCategoryForm = new CreateCategoryForm();
public schemasFilter = new FormControl(); public schemasFilter = new FormControl();
@ -37,7 +37,6 @@ export class SchemasPageComponent extends ResourceOwner implements OnInit {
constructor( constructor(
public readonly schemasState: SchemasState, public readonly schemasState: SchemasState,
private readonly formBuilder: FormBuilder,
private readonly messageBus: MessageBus, private readonly messageBus: MessageBus,
private readonly route: ActivatedRoute, private readonly route: ActivatedRoute,
private readonly router: Router, private readonly router: Router,

4
frontend/app/features/settings/pages/asset-scripts/asset-scripts-page.component.ts

@ -6,7 +6,6 @@
*/ */
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { AppsState, AssetCompletions, AssetScriptsState, AssetsService, EditAssetScriptsForm, ResourceOwner } from '@app/shared'; import { AppsState, AssetCompletions, AssetScriptsState, AssetsService, EditAssetScriptsForm, ResourceOwner } from '@app/shared';
import { EMPTY, Observable } from 'rxjs'; import { EMPTY, Observable } from 'rxjs';
@ -19,13 +18,12 @@ export class AssetScriptsPageComponent extends ResourceOwner implements OnInit {
public assetScript = 'create'; public assetScript = 'create';
public assetCompletions: Observable<AssetCompletions> = EMPTY; public assetCompletions: Observable<AssetCompletions> = EMPTY;
public editForm = new EditAssetScriptsForm(this.formBuilder); public editForm = new EditAssetScriptsForm();
public isEditable = false; public isEditable = false;
constructor( constructor(
private readonly appsState: AppsState, private readonly appsState: AppsState,
private readonly formBuilder: FormBuilder,
private readonly assetScriptsState: AssetScriptsState, private readonly assetScriptsState: AssetScriptsState,
private readonly assetsService: AssetsService, private readonly assetsService: AssetsService,
) { ) {

4
frontend/app/features/settings/pages/clients/client-add-form.component.ts

@ -6,7 +6,6 @@
*/ */
import { ChangeDetectionStrategy, Component } from '@angular/core'; import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { AddClientForm, ClientsState } from '@app/shared'; import { AddClientForm, ClientsState } from '@app/shared';
@Component({ @Component({
@ -16,11 +15,10 @@ import { AddClientForm, ClientsState } from '@app/shared';
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class ClientAddFormComponent { export class ClientAddFormComponent {
public addClientForm = new AddClientForm(this.formBuilder); public addClientForm = new AddClientForm();
constructor( constructor(
private readonly clientsState: ClientsState, private readonly clientsState: ClientsState,
private readonly formBuilder: FormBuilder,
) { ) {
} }

4
frontend/app/features/settings/pages/contributors/contributor-add-form.component.ts

@ -6,7 +6,6 @@
*/ */
import { Component, Injectable, Input, OnChanges } from '@angular/core'; import { Component, Injectable, Input, OnChanges } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { AssignContributorForm, AutocompleteSource, ContributorsState, DialogModel, DialogService, RoleDto, UsersService } from '@app/shared'; import { AssignContributorForm, AutocompleteSource, ContributorsState, DialogModel, DialogService, RoleDto, UsersService } from '@app/shared';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { withLatestFrom } from 'rxjs/operators'; import { withLatestFrom } from 'rxjs/operators';
@ -48,7 +47,7 @@ export class ContributorAddFormComponent implements OnChanges {
@Input() @Input()
public roles: ReadonlyArray<RoleDto>; public roles: ReadonlyArray<RoleDto>;
public assignContributorForm = new AssignContributorForm(this.formBuilder); public assignContributorForm = new AssignContributorForm();
public importDialog = new DialogModel(); public importDialog = new DialogModel();
@ -56,7 +55,6 @@ export class ContributorAddFormComponent implements OnChanges {
public readonly contributorsState: ContributorsState, public readonly contributorsState: ContributorsState,
public readonly usersDataSource: UsersDataSource, public readonly usersDataSource: UsersDataSource,
private readonly dialogs: DialogService, private readonly dialogs: DialogService,
private readonly formBuilder: FormBuilder,
) { ) {
} }

4
frontend/app/features/settings/pages/contributors/import-contributors-dialog.component.ts

@ -6,7 +6,6 @@
*/ */
import { Component, EventEmitter, Input, Output } from '@angular/core'; import { Component, EventEmitter, Input, Output } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { ContributorsState, ErrorDto, ImportContributorsForm, RoleDto } from '@app/shared'; import { ContributorsState, ErrorDto, ImportContributorsForm, RoleDto } from '@app/shared';
import { EMPTY, of } from 'rxjs'; import { EMPTY, of } from 'rxjs';
import { catchError, mergeMap, tap } from 'rxjs/operators'; import { catchError, mergeMap, tap } from 'rxjs/operators';
@ -30,12 +29,11 @@ export class ImportContributorsDialogComponent {
@Input() @Input()
public roles: ReadonlyArray<RoleDto>; public roles: ReadonlyArray<RoleDto>;
public importForm = new ImportContributorsForm(this.formBuilder); public importForm = new ImportContributorsForm();
public importStatus: ReadonlyArray<ImportStatus> = []; public importStatus: ReadonlyArray<ImportStatus> = [];
public importStage: 'Start' | 'Change' | 'Wait' = 'Start'; public importStage: 'Start' | 'Change' | 'Wait' = 'Start';
constructor( constructor(
private readonly formBuilder: FormBuilder,
private readonly contributorsState: ContributorsState, private readonly contributorsState: ContributorsState,
) { ) {
} }

4
frontend/app/features/settings/pages/languages/language-add-form.component.ts

@ -6,7 +6,6 @@
*/ */
import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { AddLanguageForm, LanguageDto, LanguagesState } from '@app/shared'; import { AddLanguageForm, LanguageDto, LanguagesState } from '@app/shared';
@Component({ @Component({
@ -19,11 +18,10 @@ export class LanguageAddFormComponent implements OnChanges {
@Input() @Input()
public newLanguages: ReadonlyArray<LanguageDto>; public newLanguages: ReadonlyArray<LanguageDto>;
public addLanguageForm = new AddLanguageForm(this.formBuilder); public addLanguageForm = new AddLanguageForm();
constructor( constructor(
private readonly languagesState: LanguagesState, private readonly languagesState: LanguagesState,
private readonly formBuilder: FormBuilder,
) { ) {
} }

4
frontend/app/features/settings/pages/languages/language.component.ts

@ -7,7 +7,6 @@
import { CdkDragDrop } from '@angular/cdk/drag-drop'; import { CdkDragDrop } from '@angular/cdk/drag-drop';
import { Component, Input, OnChanges } from '@angular/core'; import { Component, Input, OnChanges } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { AppLanguageDto, EditLanguageForm, LanguageDto, LanguagesState, sorted } from '@app/shared'; import { AppLanguageDto, EditLanguageForm, LanguageDto, LanguagesState, sorted } from '@app/shared';
@Component({ @Component({
@ -30,10 +29,9 @@ export class LanguageComponent implements OnChanges {
public isEditing?: boolean | null; public isEditing?: boolean | null;
public isEditable = false; public isEditable = false;
public editForm = new EditLanguageForm(this.formBuilder); public editForm = new EditLanguageForm();
constructor( constructor(
private readonly formBuilder: FormBuilder,
private readonly languagesState: LanguagesState, private readonly languagesState: LanguagesState,
) { ) {
} }

4
frontend/app/features/settings/pages/more/more-page.component.ts

@ -6,7 +6,6 @@
*/ */
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { AppDto, AppsState, defined, ResourceOwner, Types, UpdateAppForm } from '@app/shared'; import { AppDto, AppsState, defined, ResourceOwner, Types, UpdateAppForm } from '@app/shared';
@ -25,11 +24,10 @@ export class MorePageComponent extends ResourceOwner implements OnInit {
public uploading = false; public uploading = false;
public uploadProgress = 10; public uploadProgress = 10;
public updateForm = new UpdateAppForm(this.formBuilder); public updateForm = new UpdateAppForm();
constructor( constructor(
private readonly appsState: AppsState, private readonly appsState: AppsState,
private readonly formBuilder: FormBuilder,
private readonly router: Router, private readonly router: Router,
) { ) {
super(); super();

4
frontend/app/features/settings/pages/roles/role-add-form.component.ts

@ -6,7 +6,6 @@
*/ */
import { ChangeDetectionStrategy, Component } from '@angular/core'; import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { AddRoleForm, RolesState } from '@app/shared'; import { AddRoleForm, RolesState } from '@app/shared';
@Component({ @Component({
@ -16,11 +15,10 @@ import { AddRoleForm, RolesState } from '@app/shared';
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class RoleAddFormComponent { export class RoleAddFormComponent {
public addRoleForm = new AddRoleForm(this.formBuilder); public addRoleForm = new AddRoleForm();
constructor( constructor(
private readonly rolesState: RolesState, private readonly rolesState: RolesState,
private readonly formBuilder: FormBuilder,
) { ) {
} }

4
frontend/app/features/settings/pages/roles/role.component.ts

@ -6,7 +6,6 @@
*/ */
import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges, ViewChild } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges, ViewChild } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { AddPermissionForm, AutocompleteComponent, AutocompleteSource, EditRoleForm, RoleDto, RolesState, SchemaDto, Settings } from '@app/shared'; import { AddPermissionForm, AutocompleteComponent, AutocompleteSource, EditRoleForm, RoleDto, RolesState, SchemaDto, Settings } from '@app/shared';
const DESCRIPTIONS = { const DESCRIPTIONS = {
@ -64,12 +63,11 @@ export class RoleComponent implements OnChanges {
public isEditing = false; public isEditing = false;
public isEditable = false; public isEditable = false;
public addPermissionForm = new AddPermissionForm(this.formBuilder); public addPermissionForm = new AddPermissionForm();
public editForm = new EditRoleForm(); public editForm = new EditRoleForm();
constructor( constructor(
private readonly formBuilder: FormBuilder,
private readonly rolesState: RolesState, private readonly rolesState: RolesState,
) { ) {
} }

4
frontend/app/features/settings/pages/settings/settings-page.component.ts

@ -6,7 +6,6 @@
*/ */
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { AppSettingsDto, AppsState, EditAppSettingsForm, ResourceOwner } from '@app/shared'; import { AppSettingsDto, AppsState, EditAppSettingsForm, ResourceOwner } from '@app/shared';
@Component({ @Component({
@ -17,12 +16,11 @@ import { AppSettingsDto, AppsState, EditAppSettingsForm, ResourceOwner } from '@
export class SettingsPageComponent extends ResourceOwner implements OnInit { export class SettingsPageComponent extends ResourceOwner implements OnInit {
public isEditable = false; public isEditable = false;
public editForm = new EditAppSettingsForm(this.formBuilder); public editForm = new EditAppSettingsForm();
public editingSettings: AppSettingsDto; public editingSettings: AppSettingsDto;
constructor( constructor(
private readonly appsState: AppsState, private readonly appsState: AppsState,
private readonly formBuilder: FormBuilder,
) { ) {
super(); super();
} }

4
frontend/app/features/settings/pages/workflows/workflow-add-form.component.ts

@ -6,7 +6,6 @@
*/ */
import { ChangeDetectionStrategy, Component } from '@angular/core'; import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { AddWorkflowForm, WorkflowsState } from '@app/shared'; import { AddWorkflowForm, WorkflowsState } from '@app/shared';
@Component({ @Component({
@ -16,11 +15,10 @@ import { AddWorkflowForm, WorkflowsState } from '@app/shared';
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class WorkflowAddFormComponent { export class WorkflowAddFormComponent {
public addWorkflowForm = new AddWorkflowForm(this.formBuilder); public addWorkflowForm = new AddWorkflowForm();
constructor( constructor(
private readonly workflowsState: WorkflowsState, private readonly workflowsState: WorkflowsState,
private readonly formBuilder: FormBuilder,
) { ) {
} }

4
frontend/app/framework/angular/forms/editable-title.component.html

@ -1,11 +1,11 @@
<div class="title"> <div class="title">
<form *ngIf="renaming; else noRenaming" [formGroup]="renameForm" (ngSubmit)="rename()"> <form *ngIf="renaming; else noRenaming" (ngSubmit)="rename()">
<div class="row g-0"> <div class="row g-0">
<div class="col"> <div class="col">
<div class="form-group me-2"> <div class="form-group me-2">
<sqx-control-errors for="name"></sqx-control-errors> <sqx-control-errors for="name"></sqx-control-errors>
<input type="text" class="form-control" formControlName="name" [maxLength]="maxLength" sqxFocusOnInit (keydown)="onKeyDown($event)" spellcheck="false"> <input type="text" class="form-control" [formControl]="renameForm" [maxLength]="maxLength" sqxFocusOnInit (keydown)="onKeyDown($event)" spellcheck="false">
</div> </div>
</div> </div>
<div class="col-auto"> <div class="col-auto">

25
frontend/app/framework/angular/forms/editable-title.component.ts

@ -6,7 +6,7 @@
*/ */
import { Component, EventEmitter, Input, Output } from '@angular/core'; import { Component, EventEmitter, Input, Output } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms'; import { FormControl, Validators } from '@angular/forms';
import { Keys } from '@app/framework/internal'; import { Keys } from '@app/framework/internal';
@Component({ @Component({
@ -37,22 +37,11 @@ export class EditableTitleComponent {
Validators.required : Validators.required :
Validators.nullValidator; Validators.nullValidator;
this.renameForm.controls['name'].setValidators(validator); this.renameForm.setValidators(validator);
} }
public renaming = false; public renaming = false;
public renameForm = this.formBuilder.group({ public renameForm = new FormControl();
name: ['',
[
Validators.required,
],
],
});
constructor(
private readonly formBuilder: FormBuilder,
) {
}
public onKeyDown(event: KeyboardEvent) { public onKeyDown(event: KeyboardEvent) {
if (Keys.isEscape(event)) { if (Keys.isEscape(event)) {
@ -65,7 +54,7 @@ export class EditableTitleComponent {
return; return;
} }
this.renameForm.setValue({ name: this.name || '' }); this.renameForm.setValue(this.name || '');
this.renaming = !this.renaming; this.renaming = !this.renaming;
} }
@ -75,10 +64,10 @@ export class EditableTitleComponent {
} }
if (this.renameForm.valid) { if (this.renameForm.valid) {
const value = this.renameForm.value; const name = this.renameForm.value;
this.nameChange.emit(value.name); this.nameChange.emit(name);
this.name = value.name; this.name = name;
this.renaming = false; this.renaming = false;
} }

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

@ -0,0 +1,128 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { FormArray, FormControl } from '@angular/forms';
import { ExtendedFormArray, UndefinableFormArray } from './extended-form-array';
describe('ExtendedFormArray', () => {
it('should provide value even if controls are disabled', () => {
const control = new ExtendedFormArray([
new FormControl('1'),
new FormControl('2'),
]);
expect(control.value).toEqual(['1', '2']);
assertValue(control, ['1', '2'], () => {
control.controls[0].disable();
});
});
});
describe('UndefinableFormArray', () => {
const tests = [{
name: 'undefined (on)',
undefinable: true,
valueExpected: undefined,
valueActual: undefined,
}, {
name: 'defined (on)',
undefinable: true,
valueExpected: [1],
valueActual: [1],
}, {
name: 'defined (off)',
undefinable: false,
valueExpected: [1],
valueActual: [1],
}];
it('should provide value even if controls are disabled', () => {
const control = new UndefinableFormArray([
new FormControl('1'),
new FormControl('2'),
]);
expect(control.value).toEqual(['1', '2']);
assertValue(control, ['1', '2'], () => {
control.controls[0].disable();
});
});
tests.forEach(x => {
it(`should set value as <${x.name}>`, () => {
const control = buildControl(x.undefinable);
assertValue(control, x.valueExpected, () => {
control.setValue(x.valueActual as any);
});
});
});
tests.forEach(x => {
it(`should patch value as <${x.name}>`, () => {
const control = buildControl(x.undefinable);
assertValue(control, x.valueExpected, () => {
control.patchValue(x.valueActual as any);
});
});
});
tests.forEach(x => {
it(`should reset value as <${x.name}>`, () => {
const control = buildControl(x.undefinable);
assertValue(control, x.valueExpected, () => {
control.reset(x.valueActual as any);
});
});
});
it('should reset value back after push', () => {
const control = new UndefinableFormArray([]);
assertValue(control, ['1'], () => {
control.setValue(undefined);
control.push(new FormControl('1'));
});
});
it('should reset value back after insert', () => {
const control = new UndefinableFormArray([]);
assertValue(control, ['1'], () => {
control.setValue(undefined);
control.insert(0, new FormControl('1'));
});
});
function buildControl(undefinable: boolean) {
return undefinable ?
new UndefinableFormArray([
new FormControl(''),
]) :
new ExtendedFormArray([
new FormControl(''),
]);
}
});
function assertValue(control: FormArray, expected: any, action: () => void) {
let currentValue: any;
control.valueChanges.subscribe(value => {
currentValue = value;
});
action();
expect(currentValue).toEqual(expected);
expect(control.getRawValue()).toEqual(expected);
expect(control.value).toEqual(expected);
}

38
frontend/app/framework/angular/forms/undefinable-form-array.ts → frontend/app/framework/angular/forms/extended-form-array.ts

@ -5,11 +5,24 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { EventEmitter } from '@angular/core';
import { AbstractControl, AbstractControlOptions, AsyncValidatorFn, FormArray, ValidatorFn } from '@angular/forms'; import { AbstractControl, AbstractControlOptions, AsyncValidatorFn, FormArray, ValidatorFn } from '@angular/forms';
import { Types } from '@app/framework/internal'; import { Types } from '@app/framework/internal';
export class UndefinableFormArray extends FormArray { export class ExtendedFormArray extends FormArray {
constructor(controls: AbstractControl[], validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null) {
super(controls, validatorOrOpts, asyncValidator);
this['_reduceValue'] = () => {
return this.controls.map(x => x.value);
};
this['_updateValue'] = () => {
(this as { value: any }).value = this['_reduceValue']();
};
}
}
export class UndefinableFormArray extends ExtendedFormArray {
private isUndefined = false; private isUndefined = false;
constructor(controls: AbstractControl[], validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null) { constructor(controls: AbstractControl[], validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null) {
@ -79,25 +92,4 @@ export class UndefinableFormArray extends FormArray {
this.clear({ emitEvent: false }); this.clear({ emitEvent: false });
} }
} }
public updateValueAndValidity(opts: { onlySelf?: boolean; emitEvent?: boolean } = {}) {
super.updateValueAndValidity({ emitEvent: false, onlySelf: true });
if (this.isUndefined) {
this.unsetValue();
}
if (opts.emitEvent !== false) {
(this.valueChanges as EventEmitter<any>).emit(this.value);
(this.statusChanges as EventEmitter<string>).emit(this.status);
}
if (this.parent && !opts.onlySelf) {
this.parent.updateValueAndValidity(opts);
}
}
private unsetValue() {
(this as { value: any }).value = undefined;
}
} }

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

@ -0,0 +1,110 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { FormControl, FormGroup } from '@angular/forms';
import { ExtendedFormGroup, UndefinableFormGroup } from './extended-form-group';
describe('UndefinableFormGroup', () => {
it('should provide value even if controls are disabled', () => {
const control = new ExtendedFormGroup({
test1: new FormControl('1'),
test2: new FormControl('2'),
});
expect(control.value).toEqual({ test1: '1', test2: '2' });
assertValue(control, { test1: '1', test2: '2' }, () => {
control.controls['test1'].disable();
});
});
});
describe('ExtendedFormGroup', () => {
const tests = [{
name: 'undefined (on)',
undefinable: true,
valueExpected: undefined,
valueActual: undefined,
}, {
name: 'defined (on)',
undefinable: true,
valueExpected: { field: 1 },
valueActual: { field: 1 },
}, {
name: 'defined (off)',
undefinable: false,
valueExpected: { field: 1 },
valueActual: { field: 1 },
}];
it('should provide value even if controls are disabled', () => {
const control = new ExtendedFormGroup({
test1: new FormControl('1'),
test2: new FormControl('2'),
});
expect(control.value).toEqual({ test1: '1', test2: '2' });
assertValue(control, { test1: '1', test2: '2' }, () => {
control.controls['test1'].disable();
});
});
tests.forEach(x => {
it(`should set value as <${x.name}>`, () => {
const control = buildControl(x.undefinable);
assertValue(control, x.valueExpected, () => {
control.setValue(x.valueActual as any);
});
});
});
tests.forEach(x => {
it(`should patch value as <${x.name}>`, () => {
const control = buildControl(x.undefinable);
assertValue(control, x.valueExpected, () => {
control.patchValue(x.valueActual as any);
});
});
});
tests.forEach(x => {
it(`should reset value as <${x.name}>`, () => {
const control = buildControl(x.undefinable);
assertValue(control, x.valueExpected, () => {
control.reset(x.valueActual);
});
});
});
function buildControl(undefinable: boolean) {
return undefinable ?
new UndefinableFormGroup({
field: new FormControl(),
}) :
new ExtendedFormGroup({
field: new FormControl(),
});
}
});
function assertValue(control: FormGroup, expected: any, action: () => void) {
let currentValue: any;
control.valueChanges.subscribe(value => {
currentValue = value;
});
action();
expect(currentValue).toEqual(expected);
expect(control.getRawValue()).toEqual(expected);
expect(control.value).toEqual(expected);
}

44
frontend/app/framework/angular/forms/undefinable-form-group.ts → frontend/app/framework/angular/forms/extended-form-group.ts

@ -5,11 +5,30 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { EventEmitter } from '@angular/core';
import { AbstractControl, AbstractControlOptions, AsyncValidatorFn, FormGroup, ValidatorFn } from '@angular/forms'; import { AbstractControl, AbstractControlOptions, AsyncValidatorFn, FormGroup, ValidatorFn } from '@angular/forms';
import { Types } from '@app/framework/internal'; import { Types } from '@app/framework/internal';
export class UndefinableFormGroup extends FormGroup { export class ExtendedFormGroup extends FormGroup {
constructor(controls: { [key: string]: AbstractControl }, validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null) {
super(controls, validatorOrOpts, asyncValidator);
this['_reduceValue'] = () => {
const result = {};
for (const [key, control] of Object.entries(this.controls)) {
result[key] = control.value;
}
return result;
};
this['_updateValue'] = () => {
(this as { value: any }).value = this['_reduceValue']();
};
}
}
export class UndefinableFormGroup extends ExtendedFormGroup {
private isUndefined = false; private isUndefined = false;
constructor(controls: { [key: string]: AbstractControl }, validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null) { constructor(controls: { [key: string]: AbstractControl }, validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null) {
@ -63,25 +82,4 @@ export class UndefinableFormGroup extends FormGroup {
private checkUndefined(value?: {}) { private checkUndefined(value?: {}) {
this.isUndefined = Types.isUndefined(value); this.isUndefined = Types.isUndefined(value);
} }
public updateValueAndValidity(opts: { onlySelf?: boolean; emitEvent?: boolean } = {}) {
super.updateValueAndValidity({ emitEvent: false, onlySelf: true });
if (this.isUndefined) {
this.unsetValue();
}
if (opts.emitEvent !== false) {
(this.valueChanges as EventEmitter<any>).emit(this.value);
(this.statusChanges as EventEmitter<string>).emit(this.status);
}
if (this.parent && !opts.onlySelf) {
this.parent.updateValueAndValidity(opts);
}
}
private unsetValue() {
(this as { value: any }).value = undefined;
}
} }

12
frontend/app/framework/angular/forms/forms-helper.ts

@ -96,7 +96,7 @@ export function invalid$(form: AbstractControl): Observable<boolean> {
} }
export function value$<T = any>(form: AbstractControl): Observable<T> { export function value$<T = any>(form: AbstractControl): Observable<T> {
return form.valueChanges.pipe(map(() => getRawValue(form)), startWith(getRawValue(form)), distinctUntilChanged()); return form.valueChanges.pipe(startWith(form.value), distinctUntilChanged());
} }
export function valueProjection$<T = any>(form: AbstractControl, projection: (value: any) => T): Observable<T> { export function valueProjection$<T = any>(form: AbstractControl, projection: (value: any) => T): Observable<T> {
@ -159,16 +159,6 @@ function isValid(value: any) {
return !Types.isNull(value) && !Types.isUndefined(value); return !Types.isNull(value) && !Types.isUndefined(value);
} }
export function getRawValue(form: AbstractControl): any {
if (Types.is(form, FormGroup)) {
return form.getRawValue();
} else if (Types.is(form, FormArray)) {
return form.getRawValue();
} else {
return form.value;
}
}
export function hasNonCustomError(form: AbstractControl) { export function hasNonCustomError(form: AbstractControl) {
if (form.errors) { if (form.errors) {
for (const key in form.errors) { for (const key in form.errors) {

4
frontend/app/framework/angular/forms/model.ts

@ -9,7 +9,7 @@ import { AbstractControl, ValidatorFn } from '@angular/forms';
import { ErrorDto, Types } from '@app/framework/internal'; import { ErrorDto, Types } from '@app/framework/internal';
import { State } from './../../state'; import { State } from './../../state';
import { ErrorValidator } from './error-validator'; import { ErrorValidator } from './error-validator';
import { addValidator, getRawValue, hasNonCustomError, updateAll } from './forms-helper'; import { addValidator, hasNonCustomError, updateAll } from './forms-helper';
export interface FormState { export interface FormState {
// The number of submits. // The number of submits.
@ -92,7 +92,7 @@ export class Form<T extends AbstractControl, TOut, TIn = TOut> {
this.form.markAllAsTouched(); this.form.markAllAsTouched();
if (!hasNonCustomError(this.form)) { if (!hasNonCustomError(this.form)) {
const value = this.transformSubmit(getRawValue(this.form)); const value = this.transformSubmit(this.form.value);
if (value) { if (value) {
this.disable(); this.disable();

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

@ -7,7 +7,7 @@
import { AbstractControl, AbstractControlOptions, AsyncValidatorFn, ValidatorFn } from '@angular/forms'; import { AbstractControl, AbstractControlOptions, AsyncValidatorFn, ValidatorFn } from '@angular/forms';
import { Types } from '@app/framework/internal'; import { Types } from '@app/framework/internal';
import { UndefinableFormArray } from './undefinable-form-array'; import { UndefinableFormArray } from './extended-form-array';
export interface FormArrayTemplate { export interface FormArrayTemplate {
createControl(value: any, initialValue?: any): AbstractControl; createControl(value: any, initialValue?: any): AbstractControl;

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

@ -7,7 +7,7 @@
import { AbstractControlOptions, AsyncValidatorFn, FormGroup, ValidatorFn } from '@angular/forms'; import { AbstractControlOptions, AsyncValidatorFn, FormGroup, ValidatorFn } from '@angular/forms';
import { Types } from '@app/framework/internal'; import { Types } from '@app/framework/internal';
import { UndefinableFormGroup } from './undefinable-form-group'; import { UndefinableFormGroup } from './extended-form-group';
export interface FormGroupTemplate { export interface FormGroupTemplate {
setControls(form: FormGroup, value: any): void; setControls(form: FormGroup, value: any): void;

89
frontend/app/framework/angular/forms/undefinable-form-array.spec.ts

@ -1,89 +0,0 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { FormArray, FormControl } from '@angular/forms';
import { UndefinableFormArray } from './undefinable-form-array';
describe('UndefinableFormArray', () => {
const tests = [{
name: 'undefined',
value: undefined,
}, {
name: 'defined',
value: ['1'],
}];
tests.forEach(x => {
it(`should set value as <${x.name}>`, () => {
const control =
new UndefinableFormArray([
new FormControl(''),
]);
assertValue(control, x.value, () => {
control.setValue(x.value);
});
});
});
tests.forEach(x => {
it(`should patch value as <${x.name}>`, () => {
const control =
new UndefinableFormArray([
new FormControl(''),
]);
assertValue(control, x.value, () => {
control.patchValue(x.value);
});
});
});
tests.forEach(x => {
it(`should reset value as <${x.name}>`, () => {
const control =
new UndefinableFormArray([
new FormControl(''),
]);
assertValue(control, x.value, () => {
control.reset(x.value);
});
});
});
it('should reset value back after push', () => {
const control = new UndefinableFormArray([]);
assertValue(control, ['1'], () => {
control.setValue(undefined);
control.push(new FormControl('1'));
});
});
it('should reset value back after insert', () => {
const control = new UndefinableFormArray([]);
assertValue(control, ['1'], () => {
control.setValue(undefined);
control.insert(0, new FormControl('1'));
});
});
function assertValue(control: FormArray, expected: any, action: () => void) {
let currentValue: any;
control.valueChanges.subscribe(value => {
currentValue = value;
});
action();
expect(currentValue).toEqual(expected);
expect(control.getRawValue()).toEqual(expected);
}
});

71
frontend/app/framework/angular/forms/undefinable-form-group.spec.ts

@ -1,71 +0,0 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { FormControl, FormGroup } from '@angular/forms';
import { UndefinableFormGroup } from './undefinable-form-group';
describe('UndefinableFormGroup', () => {
const tests = [{
name: 'undefined',
value: undefined,
}, {
name: 'defined',
value: { field: ['1'] },
}];
tests.forEach(x => {
it(`should set value as <${x.name}>`, () => {
const control =
new UndefinableFormGroup({
field: new FormControl(),
});
assertValue(control, x.value, () => {
control.setValue(x.value);
});
});
});
tests.forEach(x => {
it(`should patch value as <${x.name}>`, () => {
const control =
new UndefinableFormGroup({
field: new FormControl(),
});
assertValue(control, x.value, () => {
control.patchValue(x.value);
});
});
});
tests.forEach(x => {
it(`should reset value as <${x.name}>`, () => {
const control =
new UndefinableFormGroup({
field: new FormControl(),
});
assertValue(control, x.value, () => {
control.reset(x.value);
});
});
});
function assertValue(control: FormGroup, expected: any, action: () => void) {
let currentValue: any;
control.valueChanges.subscribe(value => {
currentValue = value;
});
action();
expect(currentValue).toEqual(expected);
expect(control.getRawValue()).toEqual(expected);
}
});

8
frontend/app/framework/declarations.ts

@ -22,6 +22,8 @@ export * from './angular/forms/editors/localized-input.component';
export * from './angular/forms/editors/stars.component'; export * from './angular/forms/editors/stars.component';
export * from './angular/forms/editors/tag-editor.component'; export * from './angular/forms/editors/tag-editor.component';
export * from './angular/forms/editors/toggle.component'; export * from './angular/forms/editors/toggle.component';
export * from './angular/forms/extended-form-array';
export * from './angular/forms/extended-form-group';
export * from './angular/forms/file-drop.directive'; export * from './angular/forms/file-drop.directive';
export * from './angular/forms/focus-on-init.directive'; export * from './angular/forms/focus-on-init.directive';
export * from './angular/forms/form-alert.component'; export * from './angular/forms/form-alert.component';
@ -33,17 +35,15 @@ export * from './angular/forms/model';
export * from './angular/forms/progress-bar.component'; export * from './angular/forms/progress-bar.component';
export * from './angular/forms/templated-form-array'; export * from './angular/forms/templated-form-array';
export * from './angular/forms/transform-input.directive'; export * from './angular/forms/transform-input.directive';
export * from './angular/forms/undefinable-form-array';
export * from './angular/forms/undefinable-form-group';
export * from './angular/forms/validators'; export * from './angular/forms/validators';
export * from './angular/hover-background.directive'; export * from './angular/hover-background.directive';
export * from './angular/http/caching.interceptor'; export * from './angular/http/caching.interceptor';
export * from './angular/http/http-extensions'; export * from './angular/http/http-extensions';
export * from './angular/http/loading.interceptor'; export * from './angular/http/loading.interceptor';
export * from './angular/image-source.directive'; export * from './angular/image-source.directive';
export * from './angular/layout.component';
export * from './angular/layout-container.directive';
export * from './angular/language-selector.component'; export * from './angular/language-selector.component';
export * from './angular/layout-container.directive';
export * from './angular/layout.component';
export * from './angular/list-view.component'; export * from './angular/list-view.component';
export * from './angular/modals/dialog-renderer.component'; export * from './angular/modals/dialog-renderer.component';
export * from './angular/modals/modal-dialog.component'; export * from './angular/modals/modal-dialog.component';

4
frontend/app/shared/components/app-form.component.ts

@ -6,7 +6,6 @@
*/ */
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { ApiUrlConfig, AppsState, CreateAppForm } from '@app/shared/internal'; import { ApiUrlConfig, AppsState, CreateAppForm } from '@app/shared/internal';
@Component({ @Component({
@ -22,12 +21,11 @@ export class AppFormComponent {
@Input() @Input()
public template = ''; public template = '';
public createForm = new CreateAppForm(this.formBuilder); public createForm = new CreateAppForm();
constructor( constructor(
public readonly apiUrl: ApiUrlConfig, public readonly apiUrl: ApiUrlConfig,
private readonly appsStore: AppsState, private readonly appsStore: AppsState,
private readonly formBuilder: FormBuilder,
) { ) {
} }

4
frontend/app/shared/components/assets/asset-dialog.component.ts

@ -6,7 +6,6 @@
*/ */
import { ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, Output, QueryList, ViewChildren } from '@angular/core'; import { ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, Output, QueryList, ViewChildren } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { AnnotateAssetDto, AnnotateAssetForm, AppsState, AssetDto, AssetsState, AssetUploaderState, AuthService, DialogService, Types, UploadCanceled } from '@app/shared/internal'; import { AnnotateAssetDto, AnnotateAssetForm, AppsState, AssetDto, AssetsState, AssetUploaderState, AuthService, DialogService, Types, UploadCanceled } from '@app/shared/internal';
import { AssetsService } from '@app/shared/services/assets.service'; import { AssetsService } from '@app/shared/services/assets.service';
import { AssetPathItem, ROOT_ITEM } from '@app/shared/state/assets.state'; import { AssetPathItem, ROOT_ITEM } from '@app/shared/state/assets.state';
@ -53,7 +52,7 @@ export class AssetDialogComponent implements OnChanges {
public selectedTab = 0; public selectedTab = 0;
public annotateForm = new AnnotateAssetForm(this.formBuilder); public annotateForm = new AnnotateAssetForm();
public get isImage() { public get isImage() {
return this.asset.type === 'Image'; return this.asset.type === 'Image';
@ -74,7 +73,6 @@ export class AssetDialogComponent implements OnChanges {
private readonly assetsService: AssetsService, private readonly assetsService: AssetsService,
private readonly changeDetector: ChangeDetectorRef, private readonly changeDetector: ChangeDetectorRef,
private readonly dialogs: DialogService, private readonly dialogs: DialogService,
private readonly formBuilder: FormBuilder,
public readonly authService: AuthService, public readonly authService: AuthService,
) { ) {
} }

4
frontend/app/shared/components/assets/asset-folder-dialog.component.ts

@ -6,7 +6,6 @@
*/ */
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { AssetFolderDto, AssetsState, RenameAssetFolderForm } from '@app/shared/internal'; import { AssetFolderDto, AssetsState, RenameAssetFolderForm } from '@app/shared/internal';
@Component({ @Component({
@ -21,11 +20,10 @@ export class AssetFolderDialogComponent implements OnInit {
@Input() @Input()
public assetFolder: AssetFolderDto; public assetFolder: AssetFolderDto;
public editForm = new RenameAssetFolderForm(this.formBuilder); public editForm = new RenameAssetFolderForm();
constructor( constructor(
private readonly assetsState: AssetsState, private readonly assetsState: AssetsState,
private readonly formBuilder: FormBuilder,
) { ) {
} }

4
frontend/app/shared/components/comments/comments.component.ts

@ -6,7 +6,6 @@
*/ */
import { ChangeDetectorRef, Component, ElementRef, Input, OnChanges, QueryList, ViewChild, ViewChildren } from '@angular/core'; import { ChangeDetectorRef, Component, ElementRef, Input, OnChanges, QueryList, ViewChild, ViewChildren } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { switchSafe } from '@app/framework'; import { switchSafe } from '@app/framework';
import { AppsState, AuthService, CommentDto, CommentsService, CommentsState, ContributorsState, DialogService, ResourceOwner, UpsertCommentForm } from '@app/shared/internal'; import { AppsState, AuthService, CommentDto, CommentsService, CommentsState, ContributorsState, DialogService, ResourceOwner, UpsertCommentForm } from '@app/shared/internal';
@ -31,7 +30,7 @@ export class CommentsComponent extends ResourceOwner implements OnChanges {
public commentsUrl: string; public commentsUrl: string;
public commentsState: CommentsState; public commentsState: CommentsState;
public commentForm = new UpsertCommentForm(this.formBuilder); public commentForm = new UpsertCommentForm();
public mentionUsers = this.contributorsState.contributors; public mentionUsers = this.contributorsState.contributors;
public mentionConfig: MentionConfig = { dropUp: true, labelKey: 'contributorEmail' }; public mentionConfig: MentionConfig = { dropUp: true, labelKey: 'contributorEmail' };
@ -44,7 +43,6 @@ export class CommentsComponent extends ResourceOwner implements OnChanges {
private readonly contributorsState: ContributorsState, private readonly contributorsState: ContributorsState,
private readonly changeDetector: ChangeDetectorRef, private readonly changeDetector: ChangeDetectorRef,
private readonly dialogs: DialogService, private readonly dialogs: DialogService,
private readonly formBuilder: FormBuilder,
private readonly router: Router, private readonly router: Router,
) { ) {
super(); super();

21
frontend/app/shared/components/forms/geolocation-editor.component.ts

@ -6,8 +6,8 @@
*/ */
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, Input, ViewChild } from '@angular/core'; import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, Input, ViewChild } from '@angular/core';
import { FormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms'; import { FormControl, NG_VALUE_ACCESSOR } from '@angular/forms';
import { LocalStoreService, ResourceLoaderService, Settings, StatefulControlComponent, Types, UIOptions, ValidatorsEx } from '@app/shared/internal'; import { LocalStoreService, ResourceLoaderService, Settings, StatefulControlComponent, Types, UIOptions, ExtendedFormGroup, ValidatorsEx } from '@app/shared/internal';
declare const L: any; declare const L: any;
declare const google: any; declare const google: any;
@ -54,19 +54,13 @@ export class GeolocationEditorComponent extends StatefulControlComponent<State,
} }
public geolocationForm = public geolocationForm =
this.formBuilder.group({ new ExtendedFormGroup({
latitude: [ latitude: new FormControl('',
'',
[
ValidatorsEx.between(-90, 90), ValidatorsEx.between(-90, 90),
], ),
], longitude: new FormControl('',
longitude: [
'',
[
ValidatorsEx.between(-180, 180), ValidatorsEx.between(-180, 180),
], ),
],
}); });
@ViewChild('editor', { static: false }) @ViewChild('editor', { static: false })
@ -78,7 +72,6 @@ export class GeolocationEditorComponent extends StatefulControlComponent<State,
constructor(changeDetector: ChangeDetectorRef, constructor(changeDetector: ChangeDetectorRef,
private readonly localStore: LocalStoreService, private readonly localStore: LocalStoreService,
private readonly resourceLoader: ResourceLoaderService, private readonly resourceLoader: ResourceLoaderService,
private readonly formBuilder: FormBuilder,
private readonly uiOptions: UIOptions, private readonly uiOptions: UIOptions,
) { ) {
super(changeDetector, { super(changeDetector, {

8
frontend/app/shared/components/search/search-form.component.ts

@ -6,7 +6,6 @@
*/ */
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { DialogModel, equalsQuery, hasFilter, LanguageDto, Queries, Query, QueryModel, SaveQueryForm, Types } from '@app/shared/internal'; import { DialogModel, equalsQuery, hasFilter, LanguageDto, Queries, Query, QueryModel, SaveQueryForm, Types } from '@app/shared/internal';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
@ -53,17 +52,12 @@ export class SearchFormComponent implements OnChanges {
public saveKey: Observable<string | undefined>; public saveKey: Observable<string | undefined>;
public saveQueryDialog = new DialogModel(); public saveQueryDialog = new DialogModel();
public saveQueryForm = new SaveQueryForm(this.formBuilder); public saveQueryForm = new SaveQueryForm();
public searchDialog = new DialogModel(false); public searchDialog = new DialogModel(false);
public hasFilter: boolean; public hasFilter: boolean;
constructor(
private readonly formBuilder: FormBuilder,
) {
}
public ngOnChanges(changes: SimpleChanges) { public ngOnChanges(changes: SimpleChanges) {
if (changes['query'] || changes['queries']) { if (changes['query'] || changes['queries']) {
this.updateSaveKey(); this.updateSaveKey();

98
frontend/app/shared/state/apps.forms.ts

@ -7,99 +7,99 @@
/* eslint-disable no-useless-escape */ /* eslint-disable no-useless-escape */
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormControl, Validators } from '@angular/forms';
import { Form, TemplatedFormArray, ValidatorsEx } from '@app/framework'; import { Form, TemplatedFormArray, ExtendedFormGroup, ValidatorsEx } from '@app/framework';
import { AppDto, AppSettingsDto, CreateAppDto, UpdateAppDto, UpdateAppSettingsDto } from './../services/apps.service'; import { AppDto, AppSettingsDto, CreateAppDto, UpdateAppDto, UpdateAppSettingsDto } from './../services/apps.service';
export class CreateAppForm extends Form<FormGroup, CreateAppDto> { export class CreateAppForm extends Form<ExtendedFormGroup, CreateAppDto> {
constructor(formBuilder: FormBuilder) { constructor() {
super(formBuilder.group({ super(new ExtendedFormGroup({
name: ['', name: new FormControl('', [
[
Validators.required, Validators.required,
Validators.maxLength(40), Validators.maxLength(40),
ValidatorsEx.pattern('[a-z0-9]+(\-[a-z0-9]+)*', 'i18n:apps.appNameValidationMessage'), ValidatorsEx.pattern('[a-z0-9]+(\-[a-z0-9]+)*', 'i18n:apps.appNameValidationMessage'),
], ]),
],
})); }));
} }
} }
export class UpdateAppForm extends Form<FormGroup, UpdateAppDto, AppDto> { export class UpdateAppForm extends Form<ExtendedFormGroup, UpdateAppDto, AppDto> {
constructor(formBuilder: FormBuilder) { constructor() {
super(formBuilder.group({ super(new ExtendedFormGroup({
label: ['', label: new FormControl('',
[
Validators.maxLength(40), Validators.maxLength(40),
], ),
], description: new FormControl('',
description: '', Validators.nullValidator,
),
})); }));
} }
} }
export class EditAppSettingsForm extends Form<FormGroup, UpdateAppSettingsDto, AppSettingsDto> { export class EditAppSettingsForm extends Form<ExtendedFormGroup, UpdateAppSettingsDto, AppSettingsDto> {
public get patterns() { public get patterns() {
return this.form.controls['patterns']! as TemplatedFormArray; return this.form.controls['patterns'] as TemplatedFormArray;
} }
public get patternsControls(): ReadonlyArray<FormGroup> { public get patternsControls(): ReadonlyArray<ExtendedFormGroup> {
return this.patterns.controls as any; return this.patterns.controls as any;
} }
public get editors() { public get editors() {
return this.form.controls['editors']! as TemplatedFormArray; return this.form.controls['editors'] as TemplatedFormArray;
} }
public get editorsControls(): ReadonlyArray<FormGroup> { public get editorsControls(): ReadonlyArray<ExtendedFormGroup> {
return this.editors.controls as any; return this.editors.controls as any;
} }
constructor(formBuilder: FormBuilder) { constructor() {
super(formBuilder.group({ super(new ExtendedFormGroup({
patterns: new TemplatedFormArray(new PatternTemplate(formBuilder)), patterns: new TemplatedFormArray(
hideScheduler: false, PatternTemplate.INSTANCE,
hideDateTimeButtons: false, ),
editors: new TemplatedFormArray(new EditorTemplate(formBuilder)), hideScheduler: new FormControl(false,
Validators.nullValidator,
),
hideDateTimeButtons: new FormControl(false,
Validators.nullValidator,
),
editors: new TemplatedFormArray(
EditorTemplate.INSTANCE,
),
})); }));
} }
} }
class PatternTemplate { class PatternTemplate {
constructor(private readonly formBuilder: FormBuilder) {} public static readonly INSTANCE = new PatternTemplate();
public createControl() { public createControl() {
return this.formBuilder.group({ return new FormControl({
name: ['', name: new FormControl('',
[
Validators.required, Validators.required,
], ),
], regex: new FormControl('',
regex: ['',
[
Validators.required, Validators.required,
], ),
], message: new FormControl('',
message: '', Validators.nullValidator,
),
}); });
} }
} }
class EditorTemplate { class EditorTemplate {
constructor(private readonly formBuilder: FormBuilder) {} public static readonly INSTANCE = new EditorTemplate();
public createControl() { public createControl() {
return this.formBuilder.group({ return new FormControl({
name: ['', name: new FormControl('',
[
Validators.required, Validators.required,
], ),
], url: new FormControl('',
url: ['',
[
Validators.required, Validators.required,
], ),
],
}); });
} }
} }

3
frontend/app/shared/state/assets.forms.spec.ts

@ -5,7 +5,6 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { FormBuilder } from '@angular/forms';
import { AnnotateAssetForm } from './assets.forms'; import { AnnotateAssetForm } from './assets.forms';
describe('AnnotateAssetForm', () => { describe('AnnotateAssetForm', () => {
@ -28,7 +27,7 @@ describe('AnnotateAssetForm', () => {
}; };
beforeEach(() => { beforeEach(() => {
form = new AnnotateAssetForm(new FormBuilder()); form = new AnnotateAssetForm();
}); });
it('shoulde remove extension if loading asset file name', () => { it('shoulde remove extension if loading asset file name', () => {

110
frontend/app/shared/state/assets.forms.ts

@ -5,43 +5,37 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormControl, Validators } from '@angular/forms';
import { Form, Mutable, TemplatedFormArray, Types } from '@app/framework'; import { Form, Mutable, TemplatedFormArray, Types, ExtendedFormGroup } from '@app/framework';
import slugify from 'slugify'; import slugify from 'slugify';
import { AnnotateAssetDto, AssetDto, AssetFolderDto, RenameAssetFolderDto, RenameAssetTagDto } from './../services/assets.service'; import { AnnotateAssetDto, AssetDto, AssetFolderDto, RenameAssetFolderDto, RenameAssetTagDto } from './../services/assets.service';
export class AnnotateAssetForm extends Form<FormGroup, AnnotateAssetDto, AssetDto> { export class AnnotateAssetForm extends Form<ExtendedFormGroup, AnnotateAssetDto, AssetDto> {
public get metadata() { public get metadata() {
return this.form.get('metadata')! as TemplatedFormArray; return this.form.controls['metadata'] as TemplatedFormArray;
} }
public get metadataControls(): ReadonlyArray<FormGroup> { public get metadataControls(): ReadonlyArray<ExtendedFormGroup> {
return this.metadata.controls as any; return this.metadata.controls as any;
} }
constructor(formBuilder: FormBuilder) { constructor() {
super(formBuilder.group({ super(new ExtendedFormGroup({
isProtected: [false, isProtected: new FormControl(false,
[
Validators.nullValidator, Validators.nullValidator,
], ),
], fileName: new FormControl('',
fileName: ['',
[
Validators.required, Validators.required,
], ),
], slug: new FormControl('',
slug: ['',
[
Validators.required, Validators.required,
], ),
], tags: new FormControl([],
tags: [[],
[
Validators.nullValidator, Validators.nullValidator,
], ),
], metadata: new TemplatedFormArray(
metadata: new TemplatedFormArray(new MetadataTemplate(formBuilder)), MetadataTemplate.INSTANCE,
),
})); }));
} }
@ -149,7 +143,7 @@ export class AnnotateAssetForm extends Form<FormGroup, AnnotateAssetDto, AssetDt
} }
public generateSlug(asset: AssetDto) { public generateSlug(asset: AssetDto) {
const fileName = this.form.get('fileName')!.value; const fileName = this.form.controls['fileName'].value;
if (fileName) { if (fileName) {
let slug = slugify(fileName, { lower: true }); let slug = slugify(fileName, { lower: true });
@ -162,58 +156,64 @@ export class AnnotateAssetForm extends Form<FormGroup, AnnotateAssetDto, AssetDt
} }
} }
this.form.get('slug')!.setValue(slug); this.form.controls['slug'].setValue(slug);
} }
} }
} }
class MetadataTemplate { class MetadataTemplate {
constructor(private readonly formBuilder: FormBuilder) {} public static readonly INSTANCE = new MetadataTemplate();
public createControl() { public createControl() {
return this.formBuilder.group({ return new ExtendedFormGroup({
name: ['', name: new FormControl('',
[
Validators.required, Validators.required,
], ),
], value: new FormControl('',
value: [''], Validators.nullValidator,
),
}); });
} }
} }
export class EditAssetScriptsForm extends Form<FormGroup, {}, object> { export class EditAssetScriptsForm extends Form<ExtendedFormGroup, {}, object> {
constructor(formBuilder: FormBuilder) { constructor() {
super(formBuilder.group({ super(new ExtendedFormGroup({
annotate: '', annotate: new FormControl('',
create: '', Validators.nullValidator,
delete: '', ),
move: '', create: new FormControl('',
update: '', Validators.nullValidator,
),
delete: new FormControl('',
Validators.nullValidator,
),
move: new FormControl('',
Validators.nullValidator,
),
update: new FormControl('',
Validators.nullValidator,
),
})); }));
} }
} }
export class RenameAssetFolderForm extends Form<FormGroup, RenameAssetFolderDto, AssetFolderDto> { export class RenameAssetFolderForm extends Form<ExtendedFormGroup, RenameAssetFolderDto, AssetFolderDto> {
constructor(formBuilder: FormBuilder) { constructor() {
super(formBuilder.group({ super(new ExtendedFormGroup({
folderName: ['', folderName: new FormControl('',
[
Validators.required, Validators.required,
], ),
],
})); }));
} }
} }
export class RenameAssetTagForm extends Form<FormGroup, RenameAssetTagDto, RenameAssetTagDto> { export class RenameAssetTagForm extends Form<ExtendedFormGroup, RenameAssetTagDto, RenameAssetTagDto> {
constructor(formBuilder: FormBuilder) { constructor() {
super(formBuilder.group({ super(new ExtendedFormGroup({
tagName: ['', tagName: new FormControl('',
[
Validators.required, Validators.required,
], ),
],
})); }));
} }
} }

32
frontend/app/shared/state/backups.forms.ts

@ -7,26 +7,28 @@
/* eslint-disable no-useless-escape */ /* eslint-disable no-useless-escape */
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormControl, Validators } from '@angular/forms';
import { Form, hasNoValue$, ValidatorsEx } from '@app/framework'; import { Form, hasNoValue$, ExtendedFormGroup, ValidatorsEx } from '@app/framework';
import { StartRestoreDto } from './../services/backups.service'; import { StartRestoreDto } from './../services/backups.service';
export class RestoreForm extends Form<FormGroup, StartRestoreDto> { export class RestoreForm extends Form<ExtendedFormGroup, StartRestoreDto> {
public hasNoUrl = hasNoValue$(this.form.controls['url']); public get url() {
return this.form.controls['url'];
}
public hasNoUrl = hasNoValue$(this.url);
constructor(formBuilder: FormBuilder) { constructor() {
super(formBuilder.group({ super(
name: ['', new ExtendedFormGroup({
[ name: new FormControl('', [
Validators.maxLength(40), Validators.maxLength(40),
ValidatorsEx.pattern('[a-z0-9]+(\-[a-z0-9]+)*', 'i18n:apps.appNameValidationMessage'), ValidatorsEx.pattern('[a-z0-9]+(\-[a-z0-9]+)*', 'i18n:apps.appNameValidationMessage'),
], ]),
], url: new FormControl('',
url: ['',
[
Validators.required, Validators.required,
], ),
], }),
})); );
} }
} }

34
frontend/app/shared/state/clients.forms.ts

@ -7,33 +7,33 @@
/* eslint-disable no-useless-escape */ /* eslint-disable no-useless-escape */
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormControl, Validators } from '@angular/forms';
import { Form, hasNoValue$, ValidatorsEx } from '@app/framework'; import { Form, hasNoValue$, ExtendedFormGroup, ValidatorsEx } from '@app/framework';
import { ClientDto, CreateClientDto, UpdateClientDto } from './../services/clients.service'; import { ClientDto, CreateClientDto, UpdateClientDto } from './../services/clients.service';
export class RenameClientForm extends Form<FormGroup, UpdateClientDto, ClientDto> { export class RenameClientForm extends Form<ExtendedFormGroup, UpdateClientDto, ClientDto> {
constructor(formBuilder: FormBuilder) { constructor() {
super(formBuilder.group({ super(new ExtendedFormGroup({
name: ['', name: new FormControl('',
[
Validators.required, Validators.required,
], ),
],
})); }));
} }
} }
export class AddClientForm extends Form<FormGroup, CreateClientDto> { export class AddClientForm extends Form<ExtendedFormGroup, CreateClientDto> {
public hasNoId = hasNoValue$(this.form.controls['id']); public get id() {
return this.form.controls['id'];
}
public hasNoId = hasNoValue$(this.id);
constructor(formBuilder: FormBuilder) { constructor() {
super(formBuilder.group({ super(new ExtendedFormGroup({
id: ['', id: new FormControl('', [
[
Validators.maxLength(40), Validators.maxLength(40),
ValidatorsEx.pattern('[a-z0-9]+(\-[a-z0-9]+)*', 'i18n:clients.clientIdValidationMessage'), ValidatorsEx.pattern('[a-z0-9]+(\-[a-z0-9]+)*', 'i18n:clients.clientIdValidationMessage'),
], ]),
],
})); }));
} }
} }

14
frontend/app/shared/state/comments.form.ts

@ -5,14 +5,16 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { FormBuilder, FormGroup } from '@angular/forms'; import { FormControl, Validators } from '@angular/forms';
import { Form } from '@app/framework'; import { Form, ExtendedFormGroup } from '@app/framework';
import { UpsertCommentDto } from './../services/comments.service'; import { UpsertCommentDto } from './../services/comments.service';
export class UpsertCommentForm extends Form<FormGroup, UpsertCommentDto> { export class UpsertCommentForm extends Form<ExtendedFormGroup, UpsertCommentDto> {
constructor(formBuilder: FormBuilder) { constructor() {
super(formBuilder.group({ super(new ExtendedFormGroup({
text: '', text: new FormControl('',
Validators.nullValidator,
),
})); }));
} }
} }

11
frontend/app/shared/state/contents.forms-helpers.ts

@ -9,7 +9,6 @@
/* eslint-disable no-useless-return */ /* eslint-disable no-useless-return */
import { AbstractControl, ValidatorFn } from '@angular/forms'; import { AbstractControl, ValidatorFn } from '@angular/forms';
import { getRawValue } from '@app/framework';
import { BehaviorSubject, Observable } from 'rxjs'; import { BehaviorSubject, Observable } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { AppLanguageDto } from './../services/app-languages.service'; import { AppLanguageDto } from './../services/app-languages.service';
@ -149,7 +148,7 @@ export abstract class AbstractContentForm<T extends FieldDto, TForm extends Abst
return `${this.fieldPath}.${relative}`; return `${this.fieldPath}.${relative}`;
} }
public updateState(context: RuleContext, fieldData: any, itemData: any, parentState: AbstractContentFormState) { public updateState(context: RuleContext, itemData: any, parentState: AbstractContentFormState) {
const state = { const state = {
isDisabled: this.field.isDisabled || parentState.isDisabled === true, isDisabled: this.field.isDisabled || parentState.isDisabled === true,
isHidden: parentState.isHidden === true, isHidden: parentState.isHidden === true,
@ -178,11 +177,7 @@ export abstract class AbstractContentForm<T extends FieldDto, TForm extends Abst
} }
} }
this.updateCustomState(context, fieldData, itemData, state); this.updateCustomState(context, itemData, state);
}
public getRawValue() {
return getRawValue(this.form);
} }
public setValue(value: any) { public setValue(value: any) {
@ -193,7 +188,7 @@ export abstract class AbstractContentForm<T extends FieldDto, TForm extends Abst
this.form.setValue(undefined); this.form.setValue(undefined);
} }
protected updateCustomState(_context: RuleContext, _fieldData: any, _itemData: any, _state: AbstractContentFormState): void { protected updateCustomState(_context: RuleContext, _itemData: any, _state: AbstractContentFormState): void {
return; return;
} }
} }

126
frontend/app/shared/state/contents.forms.spec.ts

@ -499,10 +499,132 @@ describe('ContentForm', () => {
}, },
}); });
// Should hide fields.
expect(array.get(0)!.get('field1')!.hidden).toBeTruthy(); expect(array.get(0)!.get('field1')!.hidden).toBeTruthy();
expect(array.get(1)!.get('field1')!.hidden).toBeFalsy(); expect(array.get(1)!.get('field1')!.hidden).toBeFalsy();
}); });
it('should replace component with new fields', () => {
const component1Id = MathHelper.guid();
const component1 = createSchema({
id: 1,
fields: [
createField({
id: 11,
properties: createProperties('String'),
partitioning: 'invariant',
}),
],
});
const component2Id = MathHelper.guid();
const component2 = createSchema({
id: 2,
fields: [
createField({
id: 21,
properties: createProperties('String'),
partitioning: 'invariant',
}),
],
});
const contentForm = createForm([
createField({
id: 4,
properties: createProperties('Component'),
partitioning: 'invariant',
}),
], [], {
[component1Id]: component1,
[component2Id]: component2,
});
contentForm.load({});
// Should be undefined by default.
expect(contentForm.value).toEqual({
field4: {
iv: undefined,
},
});
contentForm.load({
field4: {
iv: {
schemaId: component1Id,
},
},
});
// Should add field from component1.
expect(contentForm.value).toEqual({
field4: {
iv: {
schemaId: component1Id,
field11: null,
},
},
});
contentForm.load({
field4: {
iv: {
schemaId: component2Id,
},
},
});
// Should add field from component1.
expect(contentForm.value).toEqual({
field4: {
iv: {
schemaId: component2Id,
field21: null,
},
},
});
});
it('should ignore invalid schema ids', () => {
const componentId = MathHelper.guid();
const component = createSchema({
id: 1,
fields: [
createField({
id: 11,
properties: createProperties('String'),
partitioning: 'invariant',
}),
],
});
const contentForm = createForm([
createField({
id: 4,
properties: createProperties('Component'),
partitioning: 'invariant',
}),
], [], {
[componentId]: component,
});
contentForm.load({
field4: {
iv: {
schemaId: 'invalid',
},
},
});
// Should ignore invalid id.
expect(contentForm.value).toEqual({
field4: {
iv: {},
},
});
});
it('should load with array and not enable disabled nested fields', () => { it('should load with array and not enable disabled nested fields', () => {
const { contentForm, array } = createArrayFormWith2Items(); const { contentForm, array } = createArrayFormWith2Items();
@ -539,7 +661,7 @@ describe('ContentForm', () => {
array.sort([array.get(1), array.get(0)]); array.sort([array.get(1), array.get(0)]);
expectLength(array, 2); expectLength(array, 2);
expect(array.form.value).toEqual([{ nested41: 'Text2' }, { nested41: 'Text1' }]); expect(array.form.value).toEqual([{ nested41: 'Text2', nested42: null }, { nested41: 'Text1', nested42: null }]);
}); });
it('should remove array item', () => { it('should remove array item', () => {
@ -548,7 +670,7 @@ describe('ContentForm', () => {
array.removeItemAt(0); array.removeItemAt(0);
expectLength(array, 1); expectLength(array, 1);
expect(array.form.value).toEqual([{ nested41: 'Text2' }]); expect(array.form.value).toEqual([{ nested41: 'Text2', nested42: null }]);
}); });
it('should reset array item', () => { it('should reset array item', () => {

106
frontend/app/shared/state/contents.forms.ts

@ -5,8 +5,8 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; import { FormControl, FormGroup, Validators } from '@angular/forms';
import { debounceTimeSafe, Form, FormArrayTemplate, getRawValue, TemplatedFormArray, Types, value$ } from '@app/framework'; import { debounceTimeSafe, Form, FormArrayTemplate, TemplatedFormArray, Types, ExtendedFormGroup, value$ } from '@app/framework';
import { FormGroupTemplate, TemplatedFormGroup } from '@app/framework/angular/forms/templated-form-group'; import { FormGroupTemplate, TemplatedFormGroup } from '@app/framework/angular/forms/templated-form-group';
import { BehaviorSubject, distinctUntilChanged, Observable } from 'rxjs'; import { BehaviorSubject, distinctUntilChanged, Observable } from 'rxjs';
import { AppLanguageDto } from './../services/app-languages.service'; import { AppLanguageDto } from './../services/app-languages.service';
@ -19,27 +19,27 @@ import { FieldDefaultValue, FieldsValidators } from './contents.forms.visitors';
type SaveQueryFormType = { name: string; user: boolean }; type SaveQueryFormType = { name: string; user: boolean };
export class SaveQueryForm extends Form<FormGroup, SaveQueryFormType> { export class SaveQueryForm extends Form<ExtendedFormGroup, SaveQueryFormType> {
constructor(formBuilder: FormBuilder) { constructor() {
super(formBuilder.group({ super(new ExtendedFormGroup({
name: ['', name: new FormControl('',
[
Validators.required, Validators.required,
], ),
], user: new FormControl(false,
user: false, Validators.nullValidator,
),
})); }));
} }
} }
export class PatchContentForm extends Form<FormGroup, any> { export class PatchContentForm extends Form<ExtendedFormGroup, any> {
private readonly editableFields: ReadonlyArray<RootFieldDto>; private readonly editableFields: ReadonlyArray<RootFieldDto>;
constructor( constructor(
private readonly listFields: ReadonlyArray<TableField>, private readonly listFields: ReadonlyArray<TableField>,
private readonly language: AppLanguageDto, private readonly language: AppLanguageDto,
) { ) {
super(new FormGroup({})); super(new ExtendedFormGroup({}));
this.editableFields = this.listFields.filter(x => Types.is(x, RootFieldDto) && x.isInlineEditable) as any; this.editableFields = this.listFields.filter(x => Types.is(x, RootFieldDto) && x.isInlineEditable) as any;
@ -73,7 +73,7 @@ export class PatchContentForm extends Form<FormGroup, any> {
} }
} }
export class EditContentForm extends Form<FormGroup, any> { export class EditContentForm extends Form<ExtendedFormGroup, any> {
private readonly fields: { [name: string]: FieldForm } = {}; private readonly fields: { [name: string]: FieldForm } = {};
private readonly valueChange$ = new BehaviorSubject<any>(this.form.value); private readonly valueChange$ = new BehaviorSubject<any>(this.form.value);
private initialData: any; private initialData: any;
@ -94,7 +94,7 @@ export class EditContentForm extends Form<FormGroup, any> {
public context: any, public context: any,
debounce = 100, debounce = 100,
) { ) {
super(new FormGroup({})); super(new ExtendedFormGroup({}));
const globals: FormGlobals = { const globals: FormGlobals = {
schema, schema,
@ -185,7 +185,7 @@ export class EditContentForm extends Form<FormGroup, any> {
const context = { ...this.context || {}, data }; const context = { ...this.context || {}, data };
for (const field of Object.values(this.fields)) { for (const field of Object.values(this.fields)) {
field.updateState(context, data[field.field.name], data, { isDisabled: this.form.disabled }); field.updateState(context, data, { isDisabled: this.form.disabled });
} }
for (const section of this.sections) { for (const section of this.sections) {
@ -194,7 +194,7 @@ export class EditContentForm extends Form<FormGroup, any> {
} }
private updateInitialData() { private updateInitialData() {
this.initialData = this.form.getRawValue(); this.initialData = this.form.value;
} }
} }
@ -236,7 +236,7 @@ export class FieldForm extends AbstractContentForm<RootFieldDto, FormGroup> {
} }
} }
protected updateCustomState(context: any, fieldData: any, itemData: any, state: AbstractContentFormState) { protected updateCustomState(context: any, itemData: any, state: AbstractContentFormState) {
const isRequired = state.isRequired === true; const isRequired = state.isRequired === true;
if (this.isRequired !== isRequired) { if (this.isRequired !== isRequired) {
@ -262,13 +262,13 @@ export class FieldForm extends AbstractContentForm<RootFieldDto, FormGroup> {
} }
} }
for (const [key, partition] of Object.entries(this.partitions)) { for (const partition of Object.values(this.partitions)) {
partition.updateState(context, fieldData?.[key], itemData, state); partition.updateState(context, itemData, state);
} }
} }
private static buildForm() { private static buildForm() {
return new FormGroup({}); return new ExtendedFormGroup({});
} }
} }
@ -283,7 +283,7 @@ export class FieldValueForm extends AbstractContentForm<FieldDto, FormControl> {
this.isRequired = field.properties.isRequired && !isOptional; this.isRequired = field.properties.isRequired && !isOptional;
} }
protected updateCustomState(_context: any, _fieldData: any, _itemData: any, state: AbstractContentFormState) { protected updateCustomState(_context: any, _itemData: any, state: AbstractContentFormState) {
const isRequired = state.isRequired === true; const isRequired = state.isRequired === true;
if (!this.isOptional && this.isRequired !== isRequired) { if (!this.isOptional && this.isRequired !== isRequired) {
@ -335,7 +335,7 @@ export class FieldArrayForm extends AbstractContentForm<FieldDto, TemplatedFormA
public readonly isComponents: boolean, public readonly isComponents: boolean,
) { ) {
super(globals, field, fieldPath, super(globals, field, fieldPath,
FieldArrayForm.buildControl(field, isOptional), new TemplatedFormArray(new ArrayTemplate(() => this), FieldsValidators.create(field, isOptional)),
isOptional, rules); isOptional, rules);
this.form.template['form'] = this; this.form.template['form'] = this;
@ -346,7 +346,7 @@ export class FieldArrayForm extends AbstractContentForm<FieldDto, TemplatedFormA
} }
public addCopy(source: ObjectFormBase) { public addCopy(source: ObjectFormBase) {
this.form.add().reset(getRawValue(source.form)); this.form.add().reset(source.form.value);
} }
public addComponent(schemaId: string) { public addComponent(schemaId: string) {
@ -378,56 +378,59 @@ export class FieldArrayForm extends AbstractContentForm<FieldDto, TemplatedFormA
} }
} }
protected updateCustomState(context: any, fieldData: any, itemData: any, state: AbstractContentFormState) { protected updateCustomState(context: any, itemData: any, state: AbstractContentFormState) {
for (let i = 0; i < this.items.length; i++) { for (const item of this.items) {
this.items[i].updateState(context, fieldData?.[i], itemData, state); item.updateState(context, itemData, state);
}
} }
private static buildControl(field: FieldDto, isOptional: boolean) {
return new TemplatedFormArray(new ArrayTemplate(), FieldsValidators.create(field, isOptional));
} }
} }
class ArrayTemplate implements FormArrayTemplate { class ArrayTemplate implements FormArrayTemplate {
public form: FieldArrayForm; protected get model() {
return this.modelProvider();
}
constructor(
private readonly modelProvider: () => FieldArrayForm,
) {
}
public createControl() { public createControl() {
const child = this.form.isComponents ? const child = this.model.isComponents ?
this.createComponent() : this.createComponent() :
this.createItem(); this.createItem();
this.form.items = [...this.form.items, child]; this.model.items = [...this.model.items, child];
return child.form; return child.form;
} }
public removeControl(index: number) { public removeControl(index: number) {
this.form.items = this.form.items.filter((_, i) => i !== index); this.model.items = this.model.items.filter((_, i) => i !== index);
} }
public clearControls() { public clearControls() {
this.form.items = []; this.model.items = [];
} }
private createItem() { private createItem() {
return new ArrayItemForm( return new ArrayItemForm(
this.form.globals, this.model.globals,
this.form.field as RootFieldDto, this.model.field as RootFieldDto,
this.form.fieldPath, this.model.fieldPath,
this.form.isOptional, this.model.isOptional,
this.form.rules, this.model.rules,
this.form.partition); this.model.partition);
} }
private createComponent() { private createComponent() {
return new ComponentForm( return new ComponentForm(
this.form.globals, this.model.globals,
this.form.field as RootFieldDto, this.model.field as RootFieldDto,
this.form.fieldPath, this.model.fieldPath,
this.form.isOptional, this.model.isOptional,
this.form.rules, this.model.rules,
this.form.partition); this.model.partition);
} }
} }
@ -475,9 +478,9 @@ export class ObjectFormBase<TField extends FieldDto = FieldDto> extends Abstract
return this.fields[field['name'] || field]; return this.fields[field['name'] || field];
} }
protected updateCustomState(context: any, fieldData: any, _: any, state: AbstractContentFormState) { protected updateCustomState(context: any, _: any, state: AbstractContentFormState) {
for (const [key, field] of Object.entries(this.fields)) { for (const field of Object.values(this.fields)) {
field.updateState(context, fieldData?.[key], fieldData, state); field.updateState(context, this.form.value, state);
} }
for (const section of this.fieldSections) { for (const section of this.fieldSections) {
@ -606,6 +609,7 @@ export class ComponentForm extends ObjectFormBase {
new ComponentTemplate(() => this), new ComponentTemplate(() => this),
partition); partition);
this.form.reset(undefined);
this.form.build(); this.form.build();
} }
@ -616,7 +620,7 @@ export class ComponentForm extends ObjectFormBase {
class ComponentTemplate extends ObjectTemplate<ComponentForm> { class ComponentTemplate extends ObjectTemplate<ComponentForm> {
public getSchema(value: any, model: ComponentForm) { public getSchema(value: any, model: ComponentForm) {
return model.globals.schemas[value?.schemaId].fields; return model.globals.schemas[value?.schemaId]?.fields;
} }
protected setControlsCore(schema: ReadonlyArray<FieldDto>, value: any, model: ComponentForm, form: FormGroup) { protected setControlsCore(schema: ReadonlyArray<FieldDto>, value: any, model: ComponentForm, form: FormGroup) {

46
frontend/app/shared/state/contributors.forms.ts

@ -5,27 +5,27 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormControl, Validators } from '@angular/forms';
import { Form, hasNoValue$, Types, value$ } from '@app/framework'; import { Form, hasNoValue$, Types, ExtendedFormGroup, value$ } from '@app/framework';
import { debounceTime, map, shareReplay } from 'rxjs/operators'; import { debounceTime, map, shareReplay } from 'rxjs/operators';
import { AssignContributorDto } from './../services/contributors.service'; import { AssignContributorDto } from './../services/contributors.service';
import { UserDto } from './../services/users.service'; import { UserDto } from './../services/users.service';
export class AssignContributorForm extends Form<FormGroup, AssignContributorDto> { export class AssignContributorForm extends Form<ExtendedFormGroup, AssignContributorDto> {
public hasNoUser = hasNoValue$(this.form.controls['user']); public get user() {
return this.form.controls['user'];
}
public hasNoUser = hasNoValue$(this.user);
constructor(formBuilder: FormBuilder) { constructor() {
super(formBuilder.group({ super(new ExtendedFormGroup({
user: [null, user: new FormControl('',
[
Validators.required, Validators.required,
], ),
], role: new FormControl('',
role: [null,
[
Validators.required, Validators.required,
], ),
],
})); }));
} }
@ -42,18 +42,20 @@ export class AssignContributorForm extends Form<FormGroup, AssignContributorDto>
type ImportContributorsFormType = ReadonlyArray<AssignContributorDto>; type ImportContributorsFormType = ReadonlyArray<AssignContributorDto>;
export class ImportContributorsForm extends Form<FormGroup, ImportContributorsFormType> { export class ImportContributorsForm extends Form<ExtendedFormGroup, ImportContributorsFormType> {
public numberOfEmails = value$(this.form.controls['import']).pipe(debounceTime(100), map(v => extractEmails(v).length), shareReplay(1)); public get import() {
return this.form.controls['import'];
}
public numberOfEmails = value$(this.import).pipe(debounceTime(100), map(v => extractEmails(v).length), shareReplay(1));
public hasNoUser = this.numberOfEmails.pipe(map(v => v === 0)); public hasNoUser = this.numberOfEmails.pipe(map(v => v === 0));
constructor(formBuilder: FormBuilder) { constructor() {
super(formBuilder.group({ super(new ExtendedFormGroup({
import: ['', import: new FormControl('',
[
Validators.required, Validators.required,
], ),
],
})); }));
} }

46
frontend/app/shared/state/languages.forms.ts

@ -5,29 +5,41 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormControl, Validators } from '@angular/forms';
import { Form, value$ } from '@app/framework'; import { Form, ExtendedFormGroup, value$ } from '@app/framework';
import { AppLanguageDto, UpdateAppLanguageDto } from './../services/app-languages.service'; import { AppLanguageDto, UpdateAppLanguageDto } from './../services/app-languages.service';
import { LanguageDto } from './../services/languages.service'; import { LanguageDto } from './../services/languages.service';
export class EditLanguageForm extends Form<FormGroup, UpdateAppLanguageDto, AppLanguageDto> { export class EditLanguageForm extends Form<ExtendedFormGroup, UpdateAppLanguageDto, AppLanguageDto> {
constructor(formBuilder: FormBuilder) { public get isMaster() {
super(formBuilder.group({ return this.form.controls['isMaster'];
isMaster: false, }
isOptional: false,
public get isOptional() {
return this.form.controls['isOptional'];
}
constructor() {
super(new ExtendedFormGroup({
isMaster: new FormControl(false,
Validators.nullValidator,
),
isOptional: new FormControl(false,
Validators.nullValidator,
),
})); }));
value$(this.form.controls['isMaster']) value$(this.isMaster)
.subscribe(value => { .subscribe(value => {
if (value) { if (value) {
this.form.controls['isOptional'].setValue(false); this.isOptional.setValue(false);
} }
}); });
value$(this.form.controls['isOptional']) value$(this.isMaster)
.subscribe(value => { .subscribe(value => {
if (value) { if (value) {
this.form.controls['isMaster'].setValue(false); this.isOptional.setValue(false);
} }
}); });
} }
@ -35,14 +47,12 @@ export class EditLanguageForm extends Form<FormGroup, UpdateAppLanguageDto, AppL
type AddLanguageFormType = { language: LanguageDto }; type AddLanguageFormType = { language: LanguageDto };
export class AddLanguageForm extends Form<FormGroup, AddLanguageFormType> { export class AddLanguageForm extends Form<ExtendedFormGroup, AddLanguageFormType> {
constructor(formBuilder: FormBuilder) { constructor() {
super(formBuilder.group({ super(new ExtendedFormGroup({
language: [null, language: new FormControl(null,
[
Validators.required, Validators.required,
], ),
],
})); }));
} }
} }

44
frontend/app/shared/state/roles.forms.ts

@ -5,8 +5,8 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; import { FormControl, Validators } from '@angular/forms';
import { Form, hasNoValue$, hasValue$, TemplatedFormArray } from '@app/framework'; import { Form, hasNoValue$, hasValue$, TemplatedFormArray, ExtendedFormGroup } from '@app/framework';
import { CreateRoleDto, RoleDto, UpdateRoleDto } from './../services/roles.service'; import { CreateRoleDto, RoleDto, UpdateRoleDto } from './../services/roles.service';
export class EditRoleForm extends Form<TemplatedFormArray, UpdateRoleDto, RoleDto> { export class EditRoleForm extends Form<TemplatedFormArray, UpdateRoleDto, RoleDto> {
@ -15,7 +15,7 @@ export class EditRoleForm extends Form<TemplatedFormArray, UpdateRoleDto, RoleDt
} }
constructor() { constructor() {
super(new TemplatedFormArray(new PermissionTemplate())); super(new TemplatedFormArray(PermissionTemplate.INSTANCE));
} }
public transformSubmit(value: any) { public transformSubmit(value: any) {
@ -28,6 +28,8 @@ export class EditRoleForm extends Form<TemplatedFormArray, UpdateRoleDto, RoleDt
} }
class PermissionTemplate { class PermissionTemplate {
public static readonly INSTANCE = new PermissionTemplate();
public createControl(_: any, initialValue: string) { public createControl(_: any, initialValue: string) {
return new FormControl(initialValue, Validators.required); return new FormControl(initialValue, Validators.required);
} }
@ -35,30 +37,34 @@ class PermissionTemplate {
type AddPermissionFormType = { permission: string }; type AddPermissionFormType = { permission: string };
export class AddPermissionForm extends Form<FormGroup, AddPermissionFormType> { export class AddPermissionForm extends Form<ExtendedFormGroup, AddPermissionFormType> {
public hasPermission = hasValue$(this.form.controls['permission']); public get permission() {
return this.form.controls['permission'];
}
public hasPermission = hasValue$(this.permission);
constructor(formBuilder: FormBuilder) { constructor() {
super(formBuilder.group({ super(new ExtendedFormGroup({
permission: ['', permission: new FormControl('',
[
Validators.required, Validators.required,
], ),
],
})); }));
} }
} }
export class AddRoleForm extends Form<FormGroup, CreateRoleDto> { export class AddRoleForm extends Form<ExtendedFormGroup, CreateRoleDto> {
public hasNoName = hasNoValue$(this.form.controls['name']); public get name() {
return this.form.controls['name'];
}
public hasNoName = hasNoValue$(this.name);
constructor(formBuilder: FormBuilder) { constructor() {
super(formBuilder.group({ super(new ExtendedFormGroup({
name: ['', name: new FormControl('',
[
Validators.required, Validators.required,
], ),
],
})); }));
} }
} }

41
frontend/app/shared/state/rules.forms.ts

@ -5,8 +5,8 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { AbstractControl, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; import { AbstractControl, FormControl, FormGroup, Validators } from '@angular/forms';
import { Form, ValidatorsEx } from '@app/framework'; import { Form, ExtendedFormGroup, ValidatorsEx } from '@app/framework';
import { RuleElementDto } from '../services/rules.service'; import { RuleElementDto } from '../services/rules.service';
export class ActionForm extends Form<any, FormGroup> { export class ActionForm extends Form<any, FormGroup> {
@ -28,7 +28,7 @@ export class ActionForm extends Form<any, FormGroup> {
controls[property.name] = new FormControl(undefined, validator); controls[property.name] = new FormControl(undefined, validator);
} }
return new FormGroup(controls); return new ExtendedFormGroup(controls);
} }
protected transformSubmit(value: any): any { protected transformSubmit(value: any): any {
@ -39,33 +39,40 @@ export class ActionForm extends Form<any, FormGroup> {
} }
export class TriggerForm extends Form<any, FormGroup> { export class TriggerForm extends Form<any, FormGroup> {
constructor(formBuilder: FormBuilder, constructor(
private readonly triggerType: string, private readonly triggerType: string,
) { ) {
super(TriggerForm.builForm(formBuilder, triggerType)); super(TriggerForm.builForm(triggerType));
} }
private static builForm(formBuilder: FormBuilder, triggerType: string) { private static builForm(triggerType: string) {
switch (triggerType) { switch (triggerType) {
case 'ContentChanged': { case 'ContentChanged': {
return formBuilder.group({ handleAll: false, schemas: undefined }); return new ExtendedFormGroup({
handleAll: new FormControl(false,
Validators.nullValidator,
),
schemas: new FormControl(undefined,
Validators.nullValidator,
),
});
} }
case 'Usage': { case 'Usage': {
return formBuilder.group({ return new ExtendedFormGroup({
limit: [20000, limit: new FormControl(20000,
[
Validators.required, Validators.required,
], ),
], numDays: new FormControl(3,
numDays: [3,
[
ValidatorsEx.between(1, 30), ValidatorsEx.between(1, 30),
], ),
],
}); });
} }
default: { default: {
return formBuilder.group({ condition: undefined }); return new ExtendedFormGroup({
condition: new FormControl('',
Validators.nullValidator,
),
});
} }
} }
} }

416
frontend/app/shared/state/schemas.forms.ts

@ -7,39 +7,41 @@
/* eslint-disable no-useless-escape */ /* eslint-disable no-useless-escape */
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { AbstractControl, FormControl, Validators } from '@angular/forms';
import { Form, TemplatedFormArray, ValidatorsEx, value$ } from '@app/framework'; import { Form, TemplatedFormArray, ExtendedFormGroup, ValidatorsEx, value$ } from '@app/framework';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { AddFieldDto, CreateSchemaDto, FieldRule, SchemaDto, SchemaPropertiesDto, SynchronizeSchemaDto, UpdateSchemaDto } from './../services/schemas.service'; import { AddFieldDto, CreateSchemaDto, FieldRule, SchemaDto, SchemaPropertiesDto, SynchronizeSchemaDto, UpdateSchemaDto } from './../services/schemas.service';
import { createProperties, FieldPropertiesDto, FieldPropertiesVisitor } from './../services/schemas.types'; import { createProperties, FieldPropertiesDto, FieldPropertiesVisitor } from './../services/schemas.types';
type CreateCategoryFormType = { name: string }; type CreateCategoryFormType = { name: string };
export class CreateCategoryForm extends Form<FormGroup, CreateCategoryFormType> { export class CreateCategoryForm extends Form<ExtendedFormGroup, CreateCategoryFormType> {
constructor(formBuilder: FormBuilder) { constructor() {
super(formBuilder.group({ super(new ExtendedFormGroup({
name: [''], name: new FormControl('',
Validators.nullValidator,
),
})); }));
} }
} }
export class CreateSchemaForm extends Form<FormGroup, CreateSchemaDto> { export class CreateSchemaForm extends Form<ExtendedFormGroup, CreateSchemaDto> {
constructor(formBuilder: FormBuilder) { constructor() {
super(formBuilder.group({ super(new ExtendedFormGroup({
name: ['', name: new FormControl('', [
[
Validators.required, Validators.required,
Validators.maxLength(40), Validators.maxLength(40),
ValidatorsEx.pattern('[a-z0-9]+(\-[a-z0-9]+)*', 'i18n:schemas.schemaNameValidationMessage'), ValidatorsEx.pattern('[a-z0-9]+(\-[a-z0-9]+)*', 'i18n:schemas.schemaNameValidationMessage'),
], ]),
], type: new FormControl('Default',
type: ['Default',
[
Validators.required, Validators.required,
], ),
], initialCategory: new FormControl(undefined,
initialCategory: undefined, Validators.nullValidator,
importing: {}, ),
importing: new FormControl({},
Validators.nullValidator,
),
})); }));
} }
@ -56,17 +58,23 @@ export class CreateSchemaForm extends Form<FormGroup, CreateSchemaDto> {
} }
} }
export class SynchronizeSchemaForm extends Form<FormGroup, SynchronizeSchemaDto> { export class SynchronizeSchemaForm extends Form<ExtendedFormGroup, SynchronizeSchemaDto> {
constructor(formBuilder: FormBuilder) { constructor() {
super(formBuilder.group({ super(new ExtendedFormGroup({
json: {}, json: new FormControl({},
fieldsDelete: false, Validators.nullValidator,
fieldsRecreate: false, ),
fieldsDelete: new FormControl(false,
Validators.nullValidator,
),
fieldsRecreate: new FormControl(false,
Validators.nullValidator,
),
})); }));
} }
public loadSchema(schema: SchemaDto) { public loadSchema(schema: SchemaDto) {
this.form.get('json')!.setValue(schema.export()); this.form.patchValue({ json: schema.export() });
} }
public transformSubmit(value: any) { public transformSubmit(value: any) {
@ -79,12 +87,12 @@ export class SynchronizeSchemaForm extends Form<FormGroup, SynchronizeSchemaDto>
} }
export class ConfigureFieldRulesForm extends Form<TemplatedFormArray, ReadonlyArray<FieldRule>, SchemaDto> { export class ConfigureFieldRulesForm extends Form<TemplatedFormArray, ReadonlyArray<FieldRule>, SchemaDto> {
public get rulesControls(): ReadonlyArray<FormGroup> { public get rulesControls(): ReadonlyArray<ExtendedFormGroup> {
return this.form.controls as any; return this.form.controls as any;
} }
constructor(formBuilder: FormBuilder) { constructor() {
super(new TemplatedFormArray(new FieldRuleTemplate(formBuilder))); super(new TemplatedFormArray(FieldRuleTemplate.INSTANCE));
} }
public add(fieldNames: ReadonlyArray<string>) { public add(fieldNames: ReadonlyArray<string>) {
@ -101,25 +109,19 @@ export class ConfigureFieldRulesForm extends Form<TemplatedFormArray, ReadonlyAr
} }
class FieldRuleTemplate { class FieldRuleTemplate {
constructor(private readonly formBuilder: FormBuilder) {} public static readonly INSTANCE = new FieldRuleTemplate();
public createControl(_: any, fieldNames?: ReadonlyArray<string>) { public createControl(_: any, fieldNames?: ReadonlyArray<string>) {
return this.formBuilder.group({ return new ExtendedFormGroup({
action: ['Disable', name: new FormControl('Disable',
[
Validators.required, Validators.required,
], ),
], field: new FormControl(fieldNames?.[0],
field: [fieldNames?.[0],
[
Validators.required, Validators.required,
], ),
], condition: new FormControl('',
condition: ['',
[
Validators.required, Validators.required,
], ),
],
}); });
} }
} }
@ -127,12 +129,12 @@ class FieldRuleTemplate {
type ConfigurePreviewUrlsFormType = { [name: string]: string }; type ConfigurePreviewUrlsFormType = { [name: string]: string };
export class ConfigurePreviewUrlsForm extends Form<TemplatedFormArray, ConfigurePreviewUrlsFormType, SchemaDto> { export class ConfigurePreviewUrlsForm extends Form<TemplatedFormArray, ConfigurePreviewUrlsFormType, SchemaDto> {
public get previewControls(): ReadonlyArray<FormGroup> { public get previewControls(): ReadonlyArray<ExtendedFormGroup> {
return this.form.controls as any; return this.form.controls as any;
} }
constructor(formBuilder: FormBuilder) { constructor() {
super(new TemplatedFormArray(new PreviewUrlTemplate(formBuilder))); super(new TemplatedFormArray(PreviewUrlTemplate.INSTANCE));
} }
public transformLoad(value: Partial<SchemaDto>) { public transformLoad(value: Partial<SchemaDto>) {
@ -159,179 +161,189 @@ export class ConfigurePreviewUrlsForm extends Form<TemplatedFormArray, Configure
} }
class PreviewUrlTemplate { class PreviewUrlTemplate {
constructor(private readonly formBuilder: FormBuilder) {} public static readonly INSTANCE = new PreviewUrlTemplate();
public createControl() { public createControl() {
return this.formBuilder.group({ return new ExtendedFormGroup({
name: ['', name: new FormControl('',
[
Validators.required, Validators.required,
], ),
], url: new FormControl('',
url: ['',
[
Validators.required, Validators.required,
], ),
],
}); });
} }
} }
export class EditSchemaScriptsForm extends Form<FormGroup, {}, object> { export class EditSchemaScriptsForm extends Form<ExtendedFormGroup, {}, object> {
constructor(formBuilder: FormBuilder) { constructor() {
super(formBuilder.group({ super(new ExtendedFormGroup({
query: '', query: new FormControl('',
create: '', Validators.nullValidator,
change: '', ),
delete: '', create: new FormControl('',
update: '', Validators.nullValidator,
),
change: new FormControl('',
Validators.nullValidator,
),
delete: new FormControl('',
Validators.nullValidator,
),
update: new FormControl('',
Validators.nullValidator,
),
})); }));
} }
} }
export class EditFieldForm extends Form<FormGroup, {}, FieldPropertiesDto> { export class EditFieldForm extends Form<ExtendedFormGroup, {}, FieldPropertiesDto> {
constructor(formBuilder: FormBuilder, properties: FieldPropertiesDto) { constructor(properties: FieldPropertiesDto) {
super(EditFieldForm.buildForm(formBuilder, properties)); super(EditFieldForm.buildForm(properties));
} }
private static buildForm(formBuilder: FormBuilder, properties: FieldPropertiesDto) { private static buildForm(properties: FieldPropertiesDto) {
const config = { const config = {
label: ['', label: new FormControl('',
[
Validators.maxLength(100), Validators.maxLength(100),
], ),
], hints: new FormControl('',
hints: ['',
[
Validators.maxLength(1000), Validators.maxLength(1000),
], ),
], placeholder: new FormControl('',
placeholder: ['',
[
Validators.maxLength(1000), Validators.maxLength(1000),
], ),
], editor: new FormControl(undefined,
editor: undefined, Validators.nullValidator,
editorUrl: undefined, ),
isRequired: false, editorUrl: new FormControl(undefined,
isRequiredOnPublish: false, Validators.nullValidator,
isHalfWidth: false, ),
tags: [], isRequired: new FormControl(false,
Validators.nullValidator,
),
isRequiredOnPublish: new FormControl(false,
Validators.nullValidator,
),
isHalfWidth: new FormControl(false,
Validators.nullValidator,
),
tags: new FormControl([],
Validators.nullValidator,
),
}; };
const visitor = new EditFieldFormVisitor(config); properties.accept(new EditFieldFormVisitor(config));
properties.accept(visitor); return new ExtendedFormGroup(config);
return formBuilder.group(config);
} }
} }
export class EditFieldFormVisitor implements FieldPropertiesVisitor<any> { export class EditFieldFormVisitor implements FieldPropertiesVisitor<any> {
constructor( constructor(
private readonly config: { [key: string]: any }, private readonly config: { [key: string]: AbstractControl },
) { ) {
} }
public visitArray() { public visitArray() {
this.config['maxItems'] = undefined; this.config['maxItems'] = new FormControl(undefined);
this.config['minItems'] = undefined; this.config['minItems'] = new FormControl(undefined);
this.config['uniqueFields'] = undefined; this.config['uniqueFields'] = new FormControl(undefined);
} }
public visitAssets() { public visitAssets() {
this.config['allowDuplicates'] = undefined; this.config['allowDuplicates'] = new FormControl(undefined);
this.config['allowedExtensions'] = undefined; this.config['allowedExtensions'] = new FormControl(undefined);
this.config['aspectHeight'] = undefined; this.config['aspectHeight'] = new FormControl(undefined);
this.config['aspectHeight'] = undefined; this.config['aspectHeight'] = new FormControl(undefined);
this.config['aspectWidth'] = undefined; this.config['aspectWidth'] = new FormControl(undefined);
this.config['defaultValue'] = undefined; this.config['defaultValue'] = new FormControl(undefined);
this.config['defaultValues'] = undefined; this.config['defaultValues'] = new FormControl(undefined);
this.config['expectedType'] = undefined; this.config['expectedType'] = new FormControl(undefined);
this.config['folderId'] = undefined; this.config['folderId'] = new FormControl(undefined);
this.config['maxHeight'] = undefined; this.config['maxHeight'] = new FormControl(undefined);
this.config['maxItems'] = undefined; this.config['maxItems'] = new FormControl(undefined);
this.config['maxSize'] = undefined; this.config['maxSize'] = new FormControl(undefined);
this.config['maxWidth'] = undefined; this.config['maxWidth'] = new FormControl(undefined);
this.config['minHeight'] = undefined; this.config['minHeight'] = new FormControl(undefined);
this.config['minItems'] = undefined; this.config['minItems'] = new FormControl(undefined);
this.config['minSize'] = undefined; this.config['minSize'] = new FormControl(undefined);
this.config['minWidth'] = undefined; this.config['minWidth'] = new FormControl(undefined);
this.config['previewMode'] = undefined; this.config['previewMode'] = new FormControl(undefined);
this.config['resolveFirst'] = undefined; this.config['resolveFirst'] = new FormControl(undefined);
} }
public visitBoolean() { public visitBoolean() {
this.config['inlineEditable'] = undefined; this.config['inlineEditable'] = new FormControl(undefined);
this.config['defaultValues'] = undefined; this.config['defaultValues'] = new FormControl(undefined);
this.config['defaultValue'] = undefined; this.config['defaultValue'] = new FormControl(undefined);
} }
public visitComponent() { public visitComponent() {
this.config['schemaIds'] = undefined; this.config['schemaIds'] = new FormControl(undefined);
} }
public visitComponents() { public visitComponents() {
this.config['schemaIds'] = undefined; this.config['schemaIds'] = new FormControl(undefined);
this.config['maxItems'] = undefined; this.config['maxItems'] = new FormControl(undefined);
this.config['minItems'] = undefined; this.config['minItems'] = new FormControl(undefined);
this.config['uniqueFields'] = undefined; this.config['uniqueFields'] = new FormControl(undefined);
} }
public visitDateTime() { public visitDateTime() {
this.config['calculatedDefaultValue'] = undefined; this.config['calculatedDefaultValue'] = new FormControl(undefined);
this.config['defaultValue'] = undefined; this.config['defaultValue'] = new FormControl(undefined);
this.config['defaultValues'] = undefined; this.config['defaultValues'] = new FormControl(undefined);
this.config['format'] = undefined; this.config['format'] = new FormControl(undefined);
this.config['maxValue'] = [undefined, ValidatorsEx.validDateTime()]; this.config['maxValue'] = new FormControl(undefined, ValidatorsEx.validDateTime());
this.config['minValue'] = [undefined, ValidatorsEx.validDateTime()]; this.config['minValue'] = new FormControl(undefined, ValidatorsEx.validDateTime());
} }
public visitNumber() { public visitNumber() {
this.config['allowedValues'] = undefined; this.config['allowedValues'] = new FormControl(undefined);
this.config['defaultValue'] = undefined; this.config['defaultValue'] = new FormControl(undefined);
this.config['defaultValues'] = undefined; this.config['defaultValues'] = new FormControl(undefined);
this.config['inlineEditable'] = undefined; this.config['inlineEditable'] = new FormControl(undefined);
this.config['isUnique'] = undefined; this.config['isUnique'] = new FormControl(undefined);
this.config['maxValue'] = undefined; this.config['maxValue'] = new FormControl(undefined);
this.config['minValue'] = undefined; this.config['minValue'] = new FormControl(undefined);
} }
public visitReferences() { public visitReferences() {
this.config['allowDuplicates'] = undefined; this.config['allowDuplicates'] = new FormControl(undefined);
this.config['defaultValue'] = undefined; this.config['defaultValue'] = new FormControl(undefined);
this.config['defaultValues'] = undefined; this.config['defaultValues'] = new FormControl(undefined);
this.config['maxItems'] = undefined; this.config['maxItems'] = new FormControl(undefined);
this.config['minItems'] = undefined; this.config['minItems'] = new FormControl(undefined);
this.config['mustBePublished'] = false; this.config['mustBePublished'] = new FormControl(false);
this.config['resolveReference'] = false; this.config['resolveReference'] = new FormControl(false);
this.config['schemaIds'] = undefined; this.config['schemaIds'] = new FormControl(undefined);
} }
public visitString() { public visitString() {
this.config['allowedValues'] = undefined; this.config['allowedValues'] = new FormControl(undefined);
this.config['contentType'] = undefined; this.config['contentType'] = new FormControl(undefined);
this.config['defaultValue'] = undefined; this.config['defaultValue'] = new FormControl(undefined);
this.config['defaultValues'] = undefined; this.config['defaultValues'] = new FormControl(undefined);
this.config['folderId'] = undefined; this.config['folderId'] = new FormControl(undefined);
this.config['inlineEditable'] = undefined; this.config['inlineEditable'] = new FormControl(undefined);
this.config['isUnique'] = undefined; this.config['isUnique'] = new FormControl(undefined);
this.config['maxCharacters'] = undefined; this.config['maxCharacters'] = new FormControl(undefined);
this.config['maxLength'] = undefined; this.config['maxLength'] = new FormControl(undefined);
this.config['maxWords'] = undefined; this.config['maxWords'] = new FormControl(undefined);
this.config['minCharacters'] = undefined; this.config['minCharacters'] = new FormControl(undefined);
this.config['minLength'] = undefined; this.config['minLength'] = new FormControl(undefined);
this.config['minWords'] = undefined; this.config['minWords'] = new FormControl(undefined);
this.config['pattern'] = undefined; this.config['pattern'] = new FormControl(undefined);
this.config['patternMessage'] = undefined; this.config['patternMessage'] = new FormControl(undefined);
} }
public visitTags() { public visitTags() {
this.config['allowedValues'] = undefined; this.config['allowedValues'] = new FormControl(undefined);
this.config['defaultValue'] = undefined; this.config['defaultValue'] = new FormControl(undefined);
this.config['defaultValues'] = undefined; this.config['defaultValues'] = new FormControl(undefined);
this.config['maxItems'] = undefined; this.config['maxItems'] = new FormControl(undefined);
this.config['minItems'] = undefined; this.config['minItems'] = new FormControl(undefined);
} }
public visitGeolocation() { public visitGeolocation() {
@ -347,64 +359,72 @@ export class EditFieldFormVisitor implements FieldPropertiesVisitor<any> {
} }
} }
export class EditSchemaForm extends Form<FormGroup, UpdateSchemaDto, SchemaPropertiesDto> { export class EditSchemaForm extends Form<ExtendedFormGroup, UpdateSchemaDto, SchemaPropertiesDto> {
constructor(formBuilder: FormBuilder) { constructor() {
super(formBuilder.group({ super(new ExtendedFormGroup({
label: ['', label: new FormControl('',
[
Validators.maxLength(100), Validators.maxLength(100),
], ),
], hints: new FormControl('',
hints: ['',
[
Validators.maxLength(1000), Validators.maxLength(1000),
], ),
], contentsSidebarUrl: new FormControl('',
contentsSidebarUrl: '', Validators.nullValidator,
contentSidebarUrl: '', ),
contentEditorUrl: '', contentSidebarUrl: new FormControl('',
validateOnPublish: false, Validators.nullValidator,
tags: [], ),
contentEditorUrl: new FormControl('',
Validators.nullValidator,
),
validateOnPublish: new FormControl(false,
Validators.nullValidator,
),
tags: new FormControl([],
Validators.nullValidator,
),
})); }));
} }
} }
export class AddFieldForm extends Form<FormGroup, AddFieldDto> { export class AddFieldForm extends Form<ExtendedFormGroup, AddFieldDto> {
public isContentField = value$(this.form.get('type')!).pipe(map(x => x !== 'UI')); public isContentField = value$(this.form.controls['type']).pipe(map(x => x !== 'UI'));
constructor(formBuilder: FormBuilder) { constructor() {
super(formBuilder.group({ super(new ExtendedFormGroup({
type: ['String', type: new FormControl('String',
[
Validators.required, Validators.required,
], ),
], name: new FormControl('', [
name: ['',
[
Validators.required, Validators.required,
Validators.maxLength(40), Validators.maxLength(40),
ValidatorsEx.pattern('[a-zA-Z0-9]+(\\-[a-zA-Z0-9]+)*', 'i18n:schemas.field.nameValidationMessage'), ValidatorsEx.pattern('[a-zA-Z0-9]+(\\-[a-zA-Z0-9]+)*', 'i18n:schemas.field.nameValidationMessage'),
], ]),
], isLocalizable: new FormControl(false,
isLocalizable: false, Validators.nullValidator,
),
})); }));
} }
public transformLoad(value: Partial<AddFieldDto>) { public transformLoad(value: Partial<AddFieldDto>) {
const isLocalizable = value.partitioning === 'language'; const { name, properties, partitioning } = value;
const isLocalizable = partitioning === 'language';
const type = const type =
value.properties ? properties ?
value.properties.fieldType : properties.fieldType :
'String'; 'String';
return { name: value.name, isLocalizable, type }; return { name, isLocalizable, type };
} }
public transformSubmit(value: any) { public transformSubmit(value: any) {
const properties = createProperties(value.type); const { name, type, isLocalizable } = value;
const partitioning = value.isLocalizable ? 'language' : 'invariant';
const properties = createProperties(type);
const partitioning = isLocalizable ? 'language' : 'invariant';
return { name: value.name, partitioning, properties }; return { name, partitioning, properties };
} }
} }

22
frontend/app/shared/state/workflows.forms.ts

@ -5,20 +5,22 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormControl, Validators } from '@angular/forms';
import { Form, hasNoValue$ } from '@app/framework'; import { Form, hasNoValue$, ExtendedFormGroup } from '@app/framework';
import { CreateWorkflowDto } from './../services/workflows.service'; import { CreateWorkflowDto } from './../services/workflows.service';
export class AddWorkflowForm extends Form<FormGroup, CreateWorkflowDto> { export class AddWorkflowForm extends Form<ExtendedFormGroup, CreateWorkflowDto> {
public hasNoName = hasNoValue$(this.form.controls['name']); public get name() {
return this.form.controls['name'];
}
public hasNoName = hasNoValue$(this.name);
constructor(formBuilder: FormBuilder) { constructor() {
super(formBuilder.group({ super(new ExtendedFormGroup({
name: ['', name: new FormControl('',
[
Validators.required, Validators.required,
], ),
],
})); }));
} }
} }

Loading…
Cancel
Save