+
-
diff --git a/frontend/app/features/settings/pages/roles/role.component.ts b/frontend/app/features/settings/pages/roles/role.component.ts
index b55f92fe9..46306daa7 100644
--- a/frontend/app/features/settings/pages/roles/role.component.ts
+++ b/frontend/app/features/settings/pages/roles/role.component.ts
@@ -101,15 +101,11 @@ export class RoleComponent implements OnChanges {
this.rolesState.delete(this.role);
}
- public removePermission(index: number) {
- this.editForm.remove(index);
- }
-
public addPermission() {
const value = this.addPermissionForm.submit();
if (value) {
- this.editForm.add(value.permission);
+ this.editForm.form.add(value.permission);
this.addPermissionForm.submitCompleted();
this.addPermissionInput.focus();
diff --git a/frontend/app/features/settings/pages/settings/settings-page.component.html b/frontend/app/features/settings/pages/settings/settings-page.component.html
index bb9e4bdef..9160f7be2 100644
--- a/frontend/app/features/settings/pages/settings/settings-page.component.html
+++ b/frontend/app/features/settings/pages/settings/settings-page.component.html
@@ -46,7 +46,7 @@
@@ -69,7 +69,7 @@
-
+
@@ -104,7 +104,7 @@
@@ -123,7 +123,7 @@
-
+
diff --git a/frontend/app/framework/angular/forms/templated-form-array.spec.ts b/frontend/app/framework/angular/forms/templated-form-array.spec.ts
new file mode 100644
index 000000000..d9124acbe
--- /dev/null
+++ b/frontend/app/framework/angular/forms/templated-form-array.spec.ts
@@ -0,0 +1,153 @@
+/*
+ * Squidex Headless CMS
+ *
+ * @license
+ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
+ */
+
+import { FormControl, FormGroup } from '@angular/forms';
+import { FormArrayTemplate, TemplatedFormArray } from './templated-form-array';
+
+describe('TemplatedFormArray', () => {
+ class Template implements FormArrayTemplate {
+ public clearCalled = 0;
+ public removeCalled: number[] = [];
+
+ public createControl() {
+ return new FormGroup({
+ value: new FormControl(),
+ });
+ }
+
+ public clearControls() {
+ this.clearCalled++;
+ }
+
+ public removeControl(index: number) {
+ this.removeCalled.push(index);
+ }
+ }
+
+ let formTemplate: Template;
+ let formArray: TemplatedFormArray;
+
+ beforeEach(() => {
+ formTemplate = new Template();
+ formArray = new TemplatedFormArray(formTemplate);
+ });
+
+ type Test = [ (value: any) => void, string];
+
+ const methods: Test[] = [
+ [x => formArray.setValue(x), 'setValue'],
+ [x => formArray.patchValue(x), 'patchValue'],
+ [x => formArray.reset(x), 'reset'],
+ ];
+
+ methods.forEach(([method, name]) => {
+ it(`Should call template to construct items for ${name}`, () => {
+ const value1 = [{
+ value: 1,
+ }, {
+ value: 2,
+ }];
+
+ method(value1);
+
+ expect(formArray.value).toEqual(value1);
+ });
+
+ it(`Should call template to remove items for ${name}`, () => {
+ const value1 = [{
+ value: 1,
+ }, {
+ value: 2,
+ }, {
+ value: 3,
+ }, {
+ value: 4,
+ }];
+
+ const value2 = [{
+ value: 1,
+ }, {
+ value: 2,
+ }];
+
+ method(value1);
+ method(value2);
+
+ expect(formArray.value).toEqual(value2);
+ expect(formTemplate.clearCalled).toEqual(0);
+ expect(formTemplate.removeCalled).toEqual([3, 2]);
+ });
+
+ it(`Should call template to clear items with undefined for ${name}`, () => {
+ const value1 = [{
+ value: 1,
+ }, {
+ value: 2,
+ }];
+
+ method(value1);
+ method(undefined);
+
+ expect(formArray.value).toEqual(undefined);
+ expect(formTemplate.clearCalled).toEqual(1);
+ expect(formTemplate.removeCalled).toEqual([]);
+ });
+
+ it(`Should call template to clear items with empty array for ${name}`, () => {
+ const value1 = [{
+ value: 1,
+ }, {
+ value: 2,
+ }];
+
+ method(value1);
+ method([]);
+
+ expect(formArray.value).toEqual([]);
+ expect(formTemplate.clearCalled).toEqual(1);
+ expect(formTemplate.removeCalled).toEqual([]);
+ });
+ });
+
+ it('should add control', () => {
+ formArray.add();
+ formArray.add();
+
+ expect(formArray.value).toEqual([{
+ value: null,
+ }, {
+ value: null,
+ }]);
+ });
+
+ it('should call template when cleared', () => {
+ formArray.add();
+ formArray.clear();
+
+ expect(formTemplate.clearCalled).toEqual(1);
+ });
+
+ it('should not call template when clearing empty form', () => {
+ formArray.clear();
+
+ expect(formTemplate.clearCalled).toEqual(0);
+ });
+
+ it('should call template when item removed', () => {
+ formArray.add();
+ formArray.removeAt(0);
+
+ expect(formTemplate.removeCalled).toEqual([0]);
+ });
+
+ it('should not call template when item to remove out of bounds', () => {
+ formArray.add();
+ formArray.removeAt(1);
+
+ expect(formTemplate.removeCalled).toEqual([]);
+ });
+});
diff --git a/frontend/app/framework/angular/forms/templated-form-array.ts b/frontend/app/framework/angular/forms/templated-form-array.ts
new file mode 100644
index 000000000..4b2493567
--- /dev/null
+++ b/frontend/app/framework/angular/forms/templated-form-array.ts
@@ -0,0 +1,86 @@
+/*
+* Squidex Headless CMS
+*
+* @license
+* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
+*/
+
+import { AbstractControl, AbstractControlOptions, AsyncValidatorFn, ValidatorFn } from '@angular/forms';
+import { Types } from '@app/framework/internal';
+import { UndefinableFormArray } from './undefinable-form-array';
+
+export interface FormArrayTemplate {
+ createControl(value: any, initialValue?: any): AbstractControl;
+
+ removeControl?(index: number, control: AbstractControl) : void;
+
+ clearControls?(): void;
+}
+
+export class TemplatedFormArray extends UndefinableFormArray {
+ constructor(public readonly template: FormArrayTemplate,
+ validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null,
+ ) {
+ super([], validatorOrOpts, asyncValidator);
+ }
+
+ public setValue(value?: any[], options?: { onlySelf?: boolean; emitEvent?: boolean }) {
+ this.prepare(value);
+
+ super.setValue(value, options);
+ }
+
+ public patchValue(value?: any[], options?: { onlySelf?: boolean; emitEvent?: boolean }) {
+ this.prepare(value);
+
+ super.patchValue(value, options);
+ }
+
+ public reset(value?: any[], options?: { onlySelf?: boolean; emitEvent?: boolean }) {
+ this.prepare(value);
+
+ super.reset(value, options);
+ }
+
+ public add(initialValue?: any) {
+ const control = this.template.createControl({}, initialValue);
+
+ this.push(control);
+
+ return control;
+ }
+
+ public removeAt(index: number, options?: { emitEvent?: boolean }) {
+ if (this.template.removeControl && index >= 0 && index < this.controls.length) {
+ this.template.removeControl(index, this.controls[index]);
+ }
+
+ super.removeAt(index, options);
+ }
+
+ public clear(options?: { emitEvent?: boolean }) {
+ if (this.template.clearControls && this.controls.length > 0) {
+ this.template.clearControls();
+ }
+
+ super.clear(options);
+ }
+
+ private prepare(value?: any[]) {
+ if (Types.isArray(value) && value.length > 0) {
+ let index = this.controls.length;
+
+ while (this.controls.length < value.length) {
+ this.add(value[index]);
+
+ index++;
+ }
+
+ while (this.controls.length > value.length) {
+ this.removeAt(this.controls.length - 1, { emitEvent: false });
+ }
+ } else {
+ this.clear();
+ }
+ }
+}
diff --git a/frontend/app/framework/angular/forms/templated-form-group.spec.ts b/frontend/app/framework/angular/forms/templated-form-group.spec.ts
new file mode 100644
index 000000000..0ce7b1c30
--- /dev/null
+++ b/frontend/app/framework/angular/forms/templated-form-group.spec.ts
@@ -0,0 +1,64 @@
+/*
+ * Squidex Headless CMS
+ *
+ * @license
+ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
+ */
+
+import { FormControl, FormGroup } from '@angular/forms';
+import { FormGroupTemplate, TemplatedFormGroup } from './templated-form-group';
+
+describe('TemplatedFormGroup', () => {
+ class Template implements FormGroupTemplate {
+ public clearCalled = 0;
+ public removeCalled: number[] = [];
+
+ public setControls(form: FormGroup) {
+ form.setControl('value', new FormControl());
+ }
+
+ public clearControls() {
+ this.clearCalled++;
+ }
+ }
+
+ let formTemplate: Template;
+ let formArray: TemplatedFormGroup;
+
+ beforeEach(() => {
+ formTemplate = new Template();
+ formArray = new TemplatedFormGroup(formTemplate);
+ });
+
+ type Test = [ (value: any) => void, string];
+
+ const methods: Test[] = [
+ [x => formArray.setValue(x), 'setValue'],
+ [x => formArray.patchValue(x), 'patchValue'],
+ [x => formArray.reset(x), 'reset'],
+ ];
+
+ methods.forEach(([method, name]) => {
+ it(`Should call template to construct controls for ${name}`, () => {
+ const value1 = {
+ value: 1,
+ };
+
+ method(value1);
+
+ expect(formArray.value).toEqual(value1);
+ });
+ it(`Should call template to clear items with for ${name}`, () => {
+ const value1 = {
+ value: 1,
+ };
+
+ method(value1);
+ method(undefined);
+
+ expect(formArray.value).toEqual(undefined);
+ expect(formTemplate.clearCalled).toEqual(1);
+ expect(formTemplate.removeCalled).toEqual([]);
+ });
+ });
+});
diff --git a/frontend/app/framework/angular/forms/templated-form-group.ts b/frontend/app/framework/angular/forms/templated-form-group.ts
new file mode 100644
index 000000000..097d49db3
--- /dev/null
+++ b/frontend/app/framework/angular/forms/templated-form-group.ts
@@ -0,0 +1,50 @@
+/*
+* Squidex Headless CMS
+*
+* @license
+* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
+*/
+
+import { AbstractControlOptions, AsyncValidatorFn, FormGroup, ValidatorFn } from '@angular/forms';
+import { Types } from '@app/framework/internal';
+import { UndefinableFormGroup } from './undefinable-form-group';
+
+export interface FormGroupTemplate {
+ setControls(form: FormGroup, value: any): void;
+
+ clearControls?(): void;
+}
+
+export class TemplatedFormGroup extends UndefinableFormGroup {
+ constructor(public readonly template: FormGroupTemplate,
+ validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null,
+ ) {
+ super({}, validatorOrOpts, asyncValidator);
+ }
+
+ public setValue(value?: {}, options?: { onlySelf?: boolean; emitEvent?: boolean }) {
+ this.build(value);
+
+ super.setValue(value, options);
+ }
+
+ public patchValue(value?: {}, options?: { onlySelf?: boolean; emitEvent?: boolean }) {
+ this.build(value);
+
+ super.patchValue(value, options);
+ }
+
+ public reset(value?: {}, options?: { onlySelf?: boolean; emitEvent?: boolean }) {
+ this.build(value);
+
+ super.reset(value, options);
+ }
+
+ public build(value?: {}) {
+ if (Types.isObject(value)) {
+ this.template?.setControls(this, value);
+ } else if (this.template?.clearControls) {
+ this.template?.clearControls();
+ }
+ }
+}
diff --git a/frontend/app/framework/angular/forms/undefinable-form-array.ts b/frontend/app/framework/angular/forms/undefinable-form-array.ts
index 6f4b1ee79..b86e29063 100644
--- a/frontend/app/framework/angular/forms/undefinable-form-array.ts
+++ b/frontend/app/framework/angular/forms/undefinable-form-array.ts
@@ -47,7 +47,7 @@ export class UndefinableFormArray extends FormArray {
}
public setValue(value?: any[], options?: { onlySelf?: boolean; emitEvent?: boolean }) {
- this.isUndefined = Types.isUndefined(value);
+ this.checkUndefined(value);
if (this.isUndefined) {
super.reset([], options);
@@ -57,7 +57,7 @@ export class UndefinableFormArray extends FormArray {
}
public patchValue(value?: any[], options?: { onlySelf?: boolean; emitEvent?: boolean }) {
- this.isUndefined = Types.isUndefined(value);
+ this.checkUndefined(value);
if (this.isUndefined) {
super.reset([], options);
@@ -67,11 +67,19 @@ export class UndefinableFormArray extends FormArray {
}
public reset(value?: any[], options?: { onlySelf?: boolean; emitEvent?: boolean }) {
- this.isUndefined = Types.isUndefined(value);
+ this.checkUndefined(value);
super.reset(value || [], options);
}
+ private checkUndefined(value?: any[]) {
+ this.isUndefined = Types.isUndefined(value);
+
+ if (this.isUndefined) {
+ this.clear({ emitEvent: false });
+ }
+ }
+
public updateValueAndValidity(opts: { onlySelf?: boolean; emitEvent?: boolean } = {}) {
super.updateValueAndValidity({ emitEvent: false, onlySelf: true });
diff --git a/frontend/app/framework/angular/forms/undefinable-form-group.ts b/frontend/app/framework/angular/forms/undefinable-form-group.ts
index 74b60b6f4..d3632c335 100644
--- a/frontend/app/framework/angular/forms/undefinable-form-group.ts
+++ b/frontend/app/framework/angular/forms/undefinable-form-group.ts
@@ -24,8 +24,6 @@ export class UndefinableFormGroup extends FormGroup {
return reduce.apply(this);
}
};
-
- this.setValue(undefined);
}
public getRawValue() {
@@ -37,31 +35,35 @@ export class UndefinableFormGroup extends FormGroup {
}
public setValue(value?: {}, options?: { onlySelf?: boolean; emitEvent?: boolean }) {
- this.isUndefined = Types.isUndefined(value);
+ this.checkUndefined(value);
if (this.isUndefined) {
- super.reset([], options);
+ super.reset({}, options);
} else {
super.setValue(value!, options);
}
}
public patchValue(value?: {}, options?: { onlySelf?: boolean; emitEvent?: boolean }) {
- this.isUndefined = Types.isUndefined(value);
+ this.checkUndefined(value);
if (this.isUndefined) {
- super.reset([], options);
+ super.reset({}, options);
} else {
super.patchValue(value!, options);
}
}
public reset(value?: {}, options: { onlySelf?: boolean; emitEvent?: boolean } = {}) {
- this.isUndefined = Types.isUndefined(value);
+ this.checkUndefined(value);
super.reset(value || {}, options);
}
+ private checkUndefined(value?: {}) {
+ this.isUndefined = Types.isUndefined(value);
+ }
+
public updateValueAndValidity(opts: { onlySelf?: boolean; emitEvent?: boolean } = {}) {
super.updateValueAndValidity({ emitEvent: false, onlySelf: true });
diff --git a/frontend/app/framework/declarations.ts b/frontend/app/framework/declarations.ts
index 08fb49255..126b840c0 100644
--- a/frontend/app/framework/declarations.ts
+++ b/frontend/app/framework/declarations.ts
@@ -31,6 +31,7 @@ export * from './angular/forms/forms-helper';
export * from './angular/forms/indeterminate-value.directive';
export * from './angular/forms/model';
export * from './angular/forms/progress-bar.component';
+export * from './angular/forms/templated-form-array';
export * from './angular/forms/transform-input.directive';
export * from './angular/forms/undefinable-form-array';
export * from './angular/forms/undefinable-form-group';
diff --git a/frontend/app/framework/services/dialog.service.spec.ts b/frontend/app/framework/services/dialog.service.spec.ts
index 57366580c..33bcb8299 100644
--- a/frontend/app/framework/services/dialog.service.spec.ts
+++ b/frontend/app/framework/services/dialog.service.spec.ts
@@ -160,7 +160,7 @@ describe('DialogService', () => {
it('should publish tooltip', () => {
const dialogService = new DialogService(localStore.object);
- const tooltip = new Tooltip('target', 'text', 'left');
+ const tooltip = new Tooltip('target', 'text', 'left-center');
let publishedTooltip: Tooltip;
diff --git a/frontend/app/framework/utils/types.ts b/frontend/app/framework/utils/types.ts
index e71b7a034..5727bbf93 100644
--- a/frontend/app/framework/utils/types.ts
+++ b/frontend/app/framework/utils/types.ts
@@ -197,9 +197,8 @@ export module Types {
return source;
}
- Object.keys(source).forEach(key => {
+ Object.entries(source).forEach(([key, sourceValue]) => {
const targetValue = target[key];
- const sourceValue = source[key];
if (Types.isArray(targetValue) && Types.isArray(sourceValue)) {
target[key] = targetValue.concat(sourceValue);
diff --git a/frontend/app/shared/components/assets/asset-dialog.component.html b/frontend/app/shared/components/assets/asset-dialog.component.html
index a9ced0f81..2041f397a 100644
--- a/frontend/app/shared/components/assets/asset-dialog.component.html
+++ b/frontend/app/shared/components/assets/asset-dialog.component.html
@@ -151,7 +151,7 @@
@@ -161,7 +161,7 @@
-
+
{{ 'assets.metadataAdd' | sqxTranslate }}
diff --git a/frontend/app/shared/services/auth.service.ts b/frontend/app/shared/services/auth.service.ts
index f56310901..b5103c468 100644
--- a/frontend/app/shared/services/auth.service.ts
+++ b/frontend/app/shared/services/auth.service.ts
@@ -69,9 +69,7 @@ export class Profile {
user: this.user,
};
- for (const key of Object.keys(this.user.profile)) {
- result[key] = this.user.profile[key];
- }
+ Object.assign(result, this.user.profile);
return result;
}
diff --git a/frontend/app/shared/services/usages.service.ts b/frontend/app/shared/services/usages.service.ts
index ab8764493..58c58f5fc 100644
--- a/frontend/app/shared/services/usages.service.ts
+++ b/frontend/app/shared/services/usages.service.ts
@@ -88,8 +88,8 @@ export class UsagesService {
map(body => {
const details: { [category: string]: CallsUsagePerDateDto[] } = {};
- for (const category of Object.keys(body.details)) {
- details[category] = body.details[category].map((item: any) =>
+ for (const [category, value] of Object.entries(body.details)) {
+ details[category] = (value as any).map((item: any) =>
new CallsUsagePerDateDto(
DateTime.parseISO(item.date),
item.totalBytes,
diff --git a/frontend/app/shared/state/apps.forms.ts b/frontend/app/shared/state/apps.forms.ts
index 32a3b467d..569f10554 100644
--- a/frontend/app/shared/state/apps.forms.ts
+++ b/frontend/app/shared/state/apps.forms.ts
@@ -7,8 +7,8 @@
/* eslint-disable no-useless-escape */
-import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms';
-import { Form, ValidatorsEx } from '@app/framework';
+import { FormBuilder, FormGroup, Validators } from '@angular/forms';
+import { Form, TemplatedFormArray, ValidatorsEx } from '@app/framework';
import { AppDto, AppSettingsDto, CreateAppDto, UpdateAppDto, UpdateAppSettingsDto } from './../services/apps.service';
export class CreateAppForm extends Form
{
@@ -39,95 +39,67 @@ export class UpdateAppForm extends Form {
}
export class EditAppSettingsForm extends Form {
- public get patterns(): FormArray {
- return this.form.controls['patterns']! as FormArray;
+ public get patterns() {
+ return this.form.controls['patterns']! as TemplatedFormArray;
}
public get patternsControls(): ReadonlyArray {
return this.patterns.controls as any;
}
- public get editors(): FormArray {
- return this.form.controls['editors']! as FormArray;
+ public get editors() {
+ return this.form.controls['editors']! as TemplatedFormArray;
}
public get editorsControls(): ReadonlyArray {
return this.editors.controls as any;
}
- constructor(
- private readonly formBuilder: FormBuilder,
- ) {
+ constructor(formBuilder: FormBuilder) {
super(formBuilder.group({
- patterns: formBuilder.array([]),
+ patterns: new TemplatedFormArray(new PatternTemplate(formBuilder)),
hideScheduler: false,
hideDateTimeButtons: false,
- editors: formBuilder.array([]),
+ editors: new TemplatedFormArray(new EditorTemplate(formBuilder)),
}));
}
+}
- public addPattern() {
- this.patterns.push(
- this.formBuilder.group({
- name: ['',
- [
- Validators.required,
- ],
- ],
- regex: ['',
- [
- Validators.required,
- ],
- ],
- message: '',
- }));
- }
+class PatternTemplate {
+ constructor(private readonly formBuilder: FormBuilder) {}
- public addEditor() {
- this.editors.push(
- this.formBuilder.group({
- name: ['',
- [
- Validators.required,
- ],
+ public createControl() {
+ return this.formBuilder.group({
+ name: ['',
+ [
+ Validators.required,
],
- url: ['',
- [
- Validators.required,
- ],
+ ],
+ regex: ['',
+ [
+ Validators.required,
],
- }));
- }
-
- public removePattern(index: number) {
- this.patterns.removeAt(index);
- }
-
- public removeEditor(index: number) {
- this.editors.removeAt(index);
+ ],
+ message: '',
+ });
}
+}
- public transformLoad(value: AppSettingsDto) {
- const patterns = this.patterns;
-
- while (patterns.controls.length < value.patterns.length) {
- this.addPattern();
- }
-
- while (patterns.controls.length > value.patterns.length) {
- this.removePattern(patterns.controls.length - 1);
- }
-
- const editors = this.editors;
-
- while (editors.controls.length < value.editors.length) {
- this.addEditor();
- }
-
- while (editors.controls.length > value.editors.length) {
- this.removeEditor(editors.controls.length - 1);
- }
+class EditorTemplate {
+ constructor(private readonly formBuilder: FormBuilder) {}
- return value;
+ public createControl() {
+ return this.formBuilder.group({
+ name: ['',
+ [
+ Validators.required,
+ ],
+ ],
+ url: ['',
+ [
+ Validators.required,
+ ],
+ ],
+ });
}
}
diff --git a/frontend/app/shared/state/assets.forms.ts b/frontend/app/shared/state/assets.forms.ts
index 74ff66d86..a96245331 100644
--- a/frontend/app/shared/state/assets.forms.ts
+++ b/frontend/app/shared/state/assets.forms.ts
@@ -5,22 +5,21 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
-import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms';
-import { Form, Mutable, Types } from '@app/framework';
+import { FormBuilder, FormGroup, Validators } from '@angular/forms';
+import { Form, Mutable, TemplatedFormArray, Types } from '@app/framework';
import slugify from 'slugify';
import { AnnotateAssetDto, AssetDto, AssetFolderDto, RenameAssetFolderDto, RenameAssetTagDto } from './../services/assets.service';
export class AnnotateAssetForm extends Form {
public get metadata() {
- return this.form.get('metadata')! as FormArray;
+ return this.form.get('metadata')! as TemplatedFormArray;
}
+
public get metadataControls(): ReadonlyArray {
return this.metadata.controls as any;
}
- constructor(
- private readonly formBuilder: FormBuilder,
- ) {
+ constructor(formBuilder: FormBuilder) {
super(formBuilder.group({
isProtected: [false,
[
@@ -42,26 +41,10 @@ export class AnnotateAssetForm extends Form length) {
- this.removeMetadata(this.metadata.controls.length - 1);
- }
-
result.metadata = [];
for (const name in value.metadata) {
@@ -194,6 +167,21 @@ export class AnnotateAssetForm extends Form {
constructor(formBuilder: FormBuilder) {
super(formBuilder.group({
diff --git a/frontend/app/shared/state/contents.form-rules.ts b/frontend/app/shared/state/contents.form-rules.ts
new file mode 100644
index 000000000..723dcf572
--- /dev/null
+++ b/frontend/app/shared/state/contents.form-rules.ts
@@ -0,0 +1,157 @@
+/*
+ * Squidex Headless CMS
+ *
+ * @license
+ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
+ */
+
+/* eslint-disable @typescript-eslint/no-implied-eval */
+/* eslint-disable no-useless-return */
+
+import { Types } from '@app/framework';
+import { FieldRule, SchemaDto } from './../services/schemas.service';
+
+export type RuleContext = { data: any; user?: any };
+export type RuleForm = { fieldPath: string };
+
+export interface CompiledRules {
+ get rules(): ReadonlyArray;
+}
+
+export interface RulesProvider {
+ compileRules(schema: SchemaDto): ReadonlyArray;
+
+ getRules(form: RuleForm): CompiledRules;
+}
+
+export class CompiledRule {
+ private readonly function: Function;
+
+ public get field() {
+ return this.rule.field;
+ }
+
+ public get action() {
+ return this.rule.action;
+ }
+
+ constructor(
+ private readonly rule: FieldRule,
+ private readonly useItemData: boolean,
+ ) {
+ try {
+ this.function = new Function(`return function(user, ctx, data, itemData) { return ${rule.condition} }`)();
+ } catch {
+ this.function = () => false;
+ }
+ }
+
+ public eval(context: RuleContext, itemData: any) {
+ try {
+ const data = this.useItemData ? itemData || context.data : context.data;
+
+ return this.function(context.user, context, data, itemData);
+ } catch {
+ return false;
+ }
+ }
+}
+const EMPTY_RULES: CompiledRule[] = [];
+const EMPTY_RULES_STATIC = { rules: EMPTY_RULES };
+
+class ComponentRules implements ComponentRules {
+ private previouSchema: SchemaDto;
+ private compiledRules: ReadonlyArray = [];
+
+ public get rules() {
+ const schema = this.schema();
+
+ if (schema !== this.previouSchema) {
+ if (schema) {
+ this.compiledRules = Types.fastMerge(this.parent.getRules(this.form).rules, this.getRelativeRules(this.form, schema));
+ } else {
+ this.compiledRules = EMPTY_RULES;
+ }
+ }
+
+ return this.compiledRules;
+ }
+
+ constructor(
+ private readonly form: RuleForm,
+ private readonly parentPath: string,
+ private readonly parent: RulesProvider,
+ private readonly schema: () => SchemaDto | undefined,
+ ) {
+ }
+
+ private getRelativeRules(form: RuleForm, schema: SchemaDto) {
+ const rules = this.parent.compileRules(schema);
+
+ if (rules.length === 0) {
+ return EMPTY_RULES;
+ }
+
+ const pathField = form.fieldPath.substr(this.parentPath.length + 1);
+ const pathSimplified = pathField.replace('.iv.', '.');
+
+ return rules.filter(x => x.field === pathField || x.field === pathSimplified);
+ }
+}
+
+export class ComponentRulesProvider implements RulesProvider {
+ constructor(
+ private readonly parentPath: string,
+ private readonly parent: RulesProvider,
+ private readonly schema: () => SchemaDto | undefined,
+ ) {
+ }
+
+ public compileRules(schema: SchemaDto) {
+ return this.parent.compileRules(schema);
+ }
+
+ public getRules(form: RuleForm) {
+ return new ComponentRules(form, this.parentPath, this.parent, this.schema);
+ }
+}
+
+export class RootRulesProvider implements RulesProvider {
+ private readonly rulesCache: { [id: string]: ReadonlyArray } = {};
+ private readonly rules: ReadonlyArray;
+
+ constructor(schema: SchemaDto) {
+ this.rules = this.compileRules(schema);
+ }
+
+ public compileRules(schema: SchemaDto) {
+ if (!schema) {
+ return EMPTY_RULES;
+ }
+
+ let result = this.rulesCache[schema.id];
+
+ if (!result) {
+ result = schema.fieldRules.map(x => new CompiledRule(x, true));
+
+ this.rulesCache[schema.id] = result;
+ }
+
+ return result;
+ }
+
+ public getRules(form: RuleForm) {
+ const allRules = this.rules;
+
+ if (allRules.length === 0) {
+ return EMPTY_RULES_STATIC;
+ }
+
+ const pathField = form.fieldPath;
+ const pathSimplified = pathField.replace('.iv.', '.');
+
+ const rules = allRules.filter(x => x.field === pathField || x.field === pathSimplified);
+
+ return { rules };
+ }
+}
diff --git a/frontend/app/shared/state/contents.forms-helpers.ts b/frontend/app/shared/state/contents.forms-helpers.ts
index 02c295d19..fd67c9dba 100644
--- a/frontend/app/shared/state/contents.forms-helpers.ts
+++ b/frontend/app/shared/state/contents.forms-helpers.ts
@@ -9,12 +9,13 @@
/* eslint-disable no-useless-return */
import { AbstractControl, ValidatorFn } from '@angular/forms';
-import { getRawValue, Types } from '@app/framework';
+import { getRawValue } from '@app/framework';
import { BehaviorSubject, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { AppLanguageDto } from './../services/app-languages.service';
-import { FieldDto, FieldRule, RootFieldDto, SchemaDto } from './../services/schemas.service';
+import { FieldDto, RootFieldDto, SchemaDto } from './../services/schemas.service';
import { fieldInvariant } from './../services/schemas.types';
+import { CompiledRules, RuleContext, RulesProvider } from './contents.form-rules';
export abstract class Hidden {
private readonly hidden$ = new BehaviorSubject(false);
@@ -106,41 +107,6 @@ export class PartitionConfig {
}
}
-type RuleContext = { data: any; user?: any };
-
-export class CompiledRule {
- private readonly function: Function;
-
- public get field() {
- return this.rule.field;
- }
-
- public get action() {
- return this.rule.action;
- }
-
- constructor(
- private readonly rule: FieldRule,
- private readonly useItemData: boolean,
- ) {
- try {
- this.function = new Function(`return function(user, ctx, data, itemData) { return ${rule.condition} }`)();
- } catch {
- this.function = () => false;
- }
- }
-
- public eval(context: RuleContext, itemData: any) {
- try {
- const data = this.useItemData ? itemData || context.data : context.data;
-
- return this.function(context.user, context, data, itemData);
- } catch {
- return false;
- }
- }
-}
-
export type AbstractContentFormState = {
isDisabled?: boolean;
isHidden?: boolean;
@@ -154,96 +120,9 @@ export interface FormGlobals {
remoteValidator?: ValidatorFn;
}
-const EMPTY_RULES: CompiledRule[] = [];
-
-export interface RulesProvider {
- compileRules(schema: SchemaDto | undefined): ReadonlyArray;
-
- setSchema(schema?: SchemaDto): void;
-
- getRules(form: AbstractContentForm): ReadonlyArray;
-}
-
-export class ComponentRulesProvider implements RulesProvider {
- private schema?: SchemaDto;
-
- constructor(
- private readonly parentPath: string,
- private readonly parent: RulesProvider,
- ) {
- }
-
- public setSchema(schema?: SchemaDto) {
- this.schema = schema;
- }
-
- public compileRules(schema: SchemaDto | undefined): ReadonlyArray {
- return this.parent.compileRules(schema);
- }
-
- public getRules(form: AbstractContentForm) {
- return Types.fastMerge(this.parent.getRules(form), this.getRelativeRules(form));
- }
-
- private getRelativeRules(form: AbstractContentForm) {
- const rules = this.compileRules(this.schema);
-
- if (rules.length === 0) {
- return EMPTY_RULES;
- }
-
- const pathField = form.fieldPath.substr(this.parentPath.length + 1);
- const pathSimplified = pathField.replace('.iv.', '.');
-
- return rules.filter(x => x.field === pathField || x.field === pathSimplified);
- }
-}
-
-export class RootRulesProvider implements RulesProvider {
- private readonly compiledRules: { [id: string]: ReadonlyArray } = {};
- private readonly rules: ReadonlyArray;
-
- constructor(schema: SchemaDto) {
- this.rules = schema.fieldRules.map(x => new CompiledRule(x, false));
- }
-
- public setSchema() {
- return;
- }
-
- public compileRules(schema: SchemaDto | undefined) {
- if (!schema) {
- return EMPTY_RULES;
- }
-
- let result = this.compileRules[schema.id];
-
- if (!result) {
- result = schema.fieldRules.map(x => new CompiledRule(x, true));
-
- this.compiledRules[schema.id] = result;
- }
-
- return result;
- }
-
- public getRules(form: AbstractContentForm) {
- const rules = this.rules;
-
- if (rules.length === 0) {
- return EMPTY_RULES;
- }
-
- const pathField = form.fieldPath;
- const pathSimplified = pathField.replace('.iv.', '.');
-
- return rules.filter(x => x.field === pathField || x.field === pathSimplified);
- }
-}
-
export abstract class AbstractContentForm extends Hidden {
private readonly disabled$ = new BehaviorSubject(false);
- private readonly currentRules: ReadonlyArray;
+ private readonly ruleSet: CompiledRules;
public get disabled() {
return this.disabled$.value;
@@ -263,7 +142,7 @@ export abstract class AbstractContentForm {
it('should hide components fields based on condition', () => {
const componentId = MathHelper.guid();
const component = createSchema({
+ id: 2,
fields: [
createField({
id: 1,
@@ -553,7 +554,7 @@ describe('ContentForm', () => {
it('should reset array item', () => {
const { array } = createArrayFormWith2Items();
- array.reset();
+ array.setValue([]);
expectLength(array, 0);
expect(array.form.value).toEqual([]);
diff --git a/frontend/app/shared/state/contents.forms.ts b/frontend/app/shared/state/contents.forms.ts
index 03e4e87fc..5233f7cc8 100644
--- a/frontend/app/shared/state/contents.forms.ts
+++ b/frontend/app/shared/state/contents.forms.ts
@@ -6,13 +6,15 @@
*/
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
-import { debounceTimeSafe, Form, getRawValue, Types, UndefinableFormArray, UndefinableFormGroup, value$ } from '@app/framework';
+import { debounceTimeSafe, Form, FormArrayTemplate, getRawValue, TemplatedFormArray, Types, value$ } from '@app/framework';
+import { FormGroupTemplate, TemplatedFormGroup } from '@app/framework/angular/forms/templated-form-group';
import { BehaviorSubject, distinctUntilChanged, Observable } from 'rxjs';
import { AppLanguageDto } from './../services/app-languages.service';
import { LanguageDto } from './../services/languages.service';
import { FieldDto, RootFieldDto, SchemaDto, TableField } from './../services/schemas.service';
import { ComponentFieldPropertiesDto, fieldInvariant } from './../services/schemas.types';
-import { AbstractContentForm, AbstractContentFormState, ComponentRulesProvider, FieldSection, FormGlobals, groupFields, PartitionConfig, RootRulesProvider, RulesProvider } from './contents.forms-helpers';
+import { ComponentRulesProvider, RootRulesProvider, RulesProvider } from './contents.form-rules';
+import { AbstractContentForm, AbstractContentFormState, FieldSection, FormGlobals, groupFields, PartitionConfig } from './contents.forms-helpers';
import { FieldDefaultValue, FieldsValidators } from './contents.forms.visitors';
type SaveQueryFormType = { name: string; user: boolean };
@@ -150,10 +152,6 @@ export class EditContentForm extends Form {
}
public load(value: any, isInitial?: boolean) {
- for (const key of Object.keys(this.fields)) {
- this.fields[key].prepareLoad(value?.[key]);
- }
-
super.load(value);
if (isInitial) {
@@ -238,12 +236,6 @@ export class FieldForm extends AbstractContentForm {
}
}
- public prepareLoad(value: any) {
- for (const key of Object.keys(this.partitions)) {
- this.partitions[key].prepareLoad(value?.[key]);
- }
- }
-
protected updateCustomState(context: any, fieldData: any, itemData: any, state: AbstractContentFormState) {
const isRequired = state.isRequired === true;
@@ -270,8 +262,8 @@ export class FieldForm extends AbstractContentForm {
}
}
- for (const key of Object.keys(this.partitions)) {
- this.partitions[key].updateState(context, fieldData?.[key], itemData, state);
+ for (const [key, partition] of Object.entries(this.partitions)) {
+ partition.updateState(context, fieldData?.[key], itemData, state);
}
}
@@ -283,14 +275,7 @@ export class FieldForm extends AbstractContentForm {
export class FieldValueForm extends AbstractContentForm {
private isRequired = false;
- constructor(
- globals: FormGlobals,
- field: FieldDto,
- fieldPath: string,
- isOptional: boolean,
- rules: RulesProvider,
- partition: string,
- ) {
+ constructor(globals: FormGlobals, field: FieldDto, fieldPath: string, isOptional: boolean, rules: RulesProvider, partition: string) {
super(globals, field, fieldPath,
FieldValueForm.buildControl(field, isOptional, partition, globals),
isOptional, rules);
@@ -330,10 +315,10 @@ export class FieldValueForm extends AbstractContentForm {
}
}
-export class FieldArrayForm extends AbstractContentForm {
- private readonly item$ = new BehaviorSubject>([]);
+export class FieldArrayForm extends AbstractContentForm {
+ private readonly item$ = new BehaviorSubject>([]);
- public get itemChanges(): Observable> {
+ public get itemChanges(): Observable> {
return this.item$;
}
@@ -341,83 +326,42 @@ export class FieldArrayForm extends AbstractContentForm) {
+ public set items(value: ReadonlyArray) {
this.item$.next(value);
}
- constructor(
- globals: FormGlobals,
- field: FieldDto,
- fieldPath: string,
- isOptional: boolean,
- rules: RulesProvider,
- private readonly partition: string,
- private readonly isComponents: boolean,
+ constructor(globals: FormGlobals, field: FieldDto, fieldPath: string, isOptional: boolean, rules: RulesProvider,
+ public readonly partition: string,
+ public readonly isComponents: boolean,
) {
super(globals, field, fieldPath,
FieldArrayForm.buildControl(field, isOptional),
isOptional, rules);
+
+ this.form.template['form'] = this;
}
public get(index: number) {
return this.items[index];
}
- public addCopy(source: ObjectForm) {
- if (this.isComponents) {
- const child = this.createComponent();
-
- child.load(getRawValue(source.form));
-
- this.addChild(child);
- } else {
- const child = this.createItem();
-
- child.load(getRawValue(source.form));
-
- this.addChild(child);
- }
+ public addCopy(source: ObjectFormBase) {
+ this.form.add().reset(getRawValue(source.form));
}
- public addComponent(schemaId?: string) {
- const child = this.createComponent(schemaId);
-
- this.addChild(child);
+ public addComponent(schemaId: string) {
+ this.form.add().reset({ schemaId });
}
public addItem() {
- const child = this.createItem();
-
- this.addChild(child);
- }
-
- public addChild(child: ObjectForm) {
- this.items = [...this.items, child];
-
- this.form.push(child.form);
- }
-
- public unset() {
- this.items = [];
-
- super.unset();
-
- this.form.clear();
- }
-
- public reset() {
- this.items = [];
-
- this.form.clear();
+ this.form.add();
}
public removeItemAt(index: number) {
- this.items = this.items.filter((_, i) => i !== index);
-
this.form.removeAt(index);
}
- public move(index: number, item: ObjectForm) {
+ public move(index: number, item: ObjectFormBase) {
const children = [...this.items];
children.splice(children.indexOf(item), 1);
@@ -428,86 +372,102 @@ export class FieldArrayForm extends AbstractContentForm) {
+ public sort(children: ReadonlyArray) {
for (let i = 0; i < children.length; i++) {
this.form.setControl(i, children[i].form);
}
}
- public prepareLoad(value: any) {
- if (Types.isArray(value)) {
- while (this.items.length < value.length) {
- if (this.isComponents) {
- this.addComponent();
- } else {
- this.addItem();
- }
- }
-
- while (this.items.length > value.length) {
- this.removeItemAt(this.items.length - 1);
- }
- }
-
- for (let i = 0; i < this.items.length; i++) {
- this.items[i].prepareLoad(value?.[i]);
- }
- }
-
protected updateCustomState(context: any, fieldData: any, itemData: any, state: AbstractContentFormState) {
for (let i = 0; i < this.items.length; i++) {
this.items[i].updateState(context, fieldData?.[i], itemData, state);
}
}
+ private static buildControl(field: FieldDto, isOptional: boolean) {
+ return new TemplatedFormArray(new ArrayTemplate(), FieldsValidators.create(field, isOptional));
+ }
+}
+
+class ArrayTemplate implements FormArrayTemplate {
+ public form: FieldArrayForm;
+
+ public createControl() {
+ const child = this.form.isComponents ?
+ this.createComponent() :
+ this.createItem();
+
+ this.form.items = [...this.form.items, child];
+
+ return child.form;
+ }
+
+ public removeControl(index: number) {
+ this.form.items = this.form.items.filter((_, i) => i !== index);
+ }
+
+ public clearControls() {
+ this.form.items = [];
+ }
+
private createItem() {
return new ArrayItemForm(
- this.globals,
- this.field as RootFieldDto,
- this.fieldPath,
- this.isOptional,
- this.rules,
- this.partition);
+ this.form.globals,
+ this.form.field as RootFieldDto,
+ this.form.fieldPath,
+ this.form.isOptional,
+ this.form.rules,
+ this.form.partition);
}
- private createComponent(schemaId?: string) {
+ private createComponent() {
return new ComponentForm(
- this.globals,
- this.field as RootFieldDto,
- this.fieldPath,
- this.isOptional,
- this.rules,
- this.partition,
- schemaId);
+ this.form.globals,
+ this.form.field as RootFieldDto,
+ this.form.fieldPath,
+ this.form.isOptional,
+ this.form.rules,
+ this.form.partition);
}
+}
- private static buildControl(field: FieldDto, isOptional: boolean) {
- const validators = FieldsValidators.create(field, isOptional);
+export type FieldItemForm = ComponentForm | FieldValueForm | FieldArrayForm;
+
+type FieldMap = { [name: string]: FieldItemForm };
- return new UndefinableFormArray([], validators);
+export class ObjectFormBase extends AbstractContentForm {
+ private readonly fieldSections$ = new BehaviorSubject>>([]);
+ private readonly fields$ = new BehaviorSubject({});
+
+ public get fieldSectionsChanges(): Observable>> {
+ return this.fieldSections$;
}
-}
-export type FieldItemForm = ComponentForm | FieldValueForm | FieldArrayForm;
+ public get fieldSections() {
+ return this.fieldSections$.value;
+ }
-export class ObjectForm extends AbstractContentForm {
- private fields: { [key: string]: FieldItemForm } = {};
- private fieldSections: FieldSection[] = [];
+ public set fieldSections(value: ReadonlyArray>) {
+ this.fieldSections$.next(value);
+ }
- public get sections() {
- return this.fieldSections;
+ public get fieldsChanges(): Observable {
+ return this.fields$;
}
- constructor(
- globals: FormGlobals,
- field: TField,
- fieldPath: string,
- isOptional: boolean,
- rules: RulesProvider,
- private readonly partition: string,
+ public get fields() {
+ return this.fields$.value;
+ }
+
+ public set fields(value: FieldMap) {
+ this.fields$.next(value);
+ }
+
+ constructor(globals: FormGlobals, field: TField, fieldPath: string, isOptional: boolean, rules: RulesProvider, template: ObjectTemplate,
+ public readonly partition: string,
) {
super(globals, field, fieldPath,
- ObjectForm.buildControl(field, isOptional, false),
+ ObjectFormBase.buildControl(template),
isOptional, rules);
}
@@ -515,146 +475,162 @@ export class ObjectForm extends AbstractCont
return this.fields[field['name'] || field];
}
- protected init(schema?: ReadonlyArray) {
- this.fields = {};
- this.fieldSections = [];
+ protected updateCustomState(context: any, fieldData: any, _: any, state: AbstractContentFormState) {
+ for (const [key, field] of Object.entries(this.fields)) {
+ field.updateState(context, fieldData?.[key], fieldData, state);
+ }
- for (const key of Object.keys(this.form.controls)) {
- this.form.removeControl(key);
+ for (const section of this.fieldSections) {
+ section.updateHidden();
}
+ }
- if (schema) {
- this.form.reset({});
+ private static buildControl(template: ObjectTemplate) {
+ return new TemplatedFormGroup(template);
+ }
+}
- for (const { separator, fields } of groupFields(schema)) {
- const forms: FieldItemForm[] = [];
+abstract class ObjectTemplate implements FormGroupTemplate {
+ private currentSchema: ReadonlyArray | undefined;
+
+ protected get model() {
+ return this.modelProvider();
+ }
- for (const field of fields) {
- const childForm =
- buildForm(
- this.globals,
- field,
- this.path(field.name),
- this.isOptional,
- this.rules,
- this.partition);
+ constructor(
+ private readonly modelProvider: () => T,
+ ) {
+ }
- this.form.setControl(field.name, childForm.form);
+ protected abstract getSchema(value: any, model: T): ReadonlyArray | undefined;
- forms.push(childForm);
+ public setControls(form: FormGroup, value: any) {
+ const schema = this.getSchema(value, this.model);
- this.fields[field.name] = childForm;
- }
+ if (this.currentSchema !== schema) {
+ this.clearControlsCore(this.model);
- this.fieldSections.push(new FieldSection(separator, forms));
+ if (schema) {
+ this.setControlsCore(schema, value, this.model, form);
}
- } else {
- this.form.reset(undefined);
+
+ this.currentSchema = schema;
}
}
- public load(data: any) {
- this.prepareLoad(data);
-
- this.form.reset(data);
- }
+ public clearControls() {
+ if (this.currentSchema !== undefined) {
+ this.clearControlsCore(this.model);
- public prepareLoad(value: any) {
- for (const key of Object.keys(this.fields)) {
- this.fields[key].prepareLoad(value?.[key]);
+ this.currentSchema = undefined;
}
}
- protected updateCustomState(context: any, fieldData: any, _: any, state: AbstractContentFormState) {
- for (const key of Object.keys(this.fields)) {
- this.fields[key].updateState(context, fieldData?.[key], fieldData, state);
- }
+ protected setControlsCore(schema: ReadonlyArray, value: any, model: T, form: FormGroup) {
+ const fieldMap: FieldMap = {};
+ const fieldSections: FieldSection[] = [];
- for (const section of this.sections) {
- section.updateHidden();
+ for (const { separator, fields } of groupFields(schema)) {
+ const forms: FieldItemForm[] = [];
+
+ for (const field of fields) {
+ const childForm = buildForm(
+ model.globals,
+ field,
+ model.path(field.name),
+ model.isOptional,
+ model.rules,
+ model.partition);
+
+ form.setControl(field.name, childForm.form);
+
+ forms.push(childForm);
+
+ fieldMap[field.name] = childForm;
+ }
+
+ fieldSections.push(new FieldSection(separator, forms));
}
- }
- private static buildControl(field: FieldDto, isOptional: boolean, validate: boolean) {
- let validators = [Validators.nullValidator];
+ model.fields = fieldMap;
+ model.fieldSections = fieldSections;
+ }
- if (validate) {
- validators = FieldsValidators.create(field, isOptional);
+ protected clearControlsCore(model: T) {
+ for (const name of Object.keys(model.form.controls)) {
+ model.form.removeControl(name);
}
- return new UndefinableFormGroup({}, validators);
+ model.fields = {};
+ model.fieldSections = [];
}
}
-export class ArrayItemForm extends ObjectForm {
- constructor(
- globals: FormGlobals,
- field: RootFieldDto,
- fieldPath: string,
- isOptional: boolean,
- rules: RulesProvider,
- partition: string,
- ) {
- super(globals, field, fieldPath, isOptional, rules, partition);
+export class ArrayItemForm extends ObjectFormBase {
+ constructor(globals: FormGlobals, field: RootFieldDto, fieldPath: string, isOptional: boolean, rules: RulesProvider, partition: string) {
+ super(globals, field, fieldPath, isOptional, rules,
+ new ArrayItemTemplate(() => this), partition);
- this.init(field.nested);
+ this.form.build({});
}
}
-export class ComponentForm extends ObjectForm {
- private schemaId?: string;
+class ArrayItemTemplate extends ObjectTemplate {
+ public getSchema() {
+ return this.model.field.nested;
+ }
+}
- public readonly properties: ComponentFieldPropertiesDto;
+export class ComponentForm extends ObjectFormBase {
+ private readonly schema$ = new BehaviorSubject(undefined);
- public get schema() {
- return this.globals.schemas[this.schemaId!];
+ public get schemaChanges(): Observable {
+ return this.schema$;
}
- constructor(
- globals: FormGlobals,
- field: FieldDto,
- fieldPath: string,
- isOptional: boolean,
- rules: RulesProvider,
- partition: string,
- schemaId?: string,
- ) {
- super(globals, field, fieldPath, isOptional,
- new ComponentRulesProvider(fieldPath, rules), partition);
+ public get schema() {
+ return this.schema$.value;
+ }
- this.properties = field.properties as ComponentFieldPropertiesDto;
+ public set schema(value: SchemaDto | undefined) {
+ this.schema$.next(value);
+ }
- if (schemaId) {
- this.selectSchema(schemaId);
- }
+ public get properties() {
+ return this.field.properties as ComponentFieldPropertiesDto;
}
- public selectSchema(schemaId?: string) {
- if (this.schemaId !== schemaId) {
- this.schemaId = schemaId;
+ constructor(globals: FormGlobals, field: FieldDto, fieldPath: string, isOptional: boolean, rules: RulesProvider, partition: string) {
+ super(globals, field, fieldPath, isOptional,
+ new ComponentRulesProvider(fieldPath, rules, () => this.schema),
+ new ComponentTemplate(() => this),
+ partition);
- if (this.schema) {
- this.rules.setSchema(this.schema);
+ this.form.build();
+ }
- this.init(this.schema.fields);
+ public selectSchema(schemaId: string) {
+ this.form.reset({ schemaId });
+ }
+}
- this.form.setControl('schemaId', new FormControl(schemaId));
- } else {
- this.init(undefined);
- }
- }
+class ComponentTemplate extends ObjectTemplate {
+ public getSchema(value: any, model: ComponentForm) {
+ return model.globals.schemas[value?.schemaId].fields;
}
- public unset() {
- this.selectSchema(undefined);
+ protected setControlsCore(schema: ReadonlyArray, value: any, model: ComponentForm, form: FormGroup) {
+ form.setControl('schemaId', new FormControl());
+
+ this.model.schema = model.globals.schemas[value?.schemaId];
- super.unset();
+ super.setControlsCore(schema, value, model, form);
}
- public prepareLoad(value: any) {
- this.selectSchema(value?.['schemaId']);
+ protected clearControlsCore(model: ComponentForm) {
+ this.model.schema = undefined;
- super.prepareLoad(value);
+ super.clearControlsCore(model);
}
}
diff --git a/frontend/app/shared/state/queries.ts b/frontend/app/shared/state/queries.ts
index 0ae0a406e..af640d82b 100644
--- a/frontend/app/shared/state/queries.ts
+++ b/frontend/app/shared/state/queries.ts
@@ -97,7 +97,7 @@ export class Queries {
}
function parseQueries(settings: {}) {
- const queries = Object.keys(settings).map(name => parseStored(name, settings[name]));
+ const queries = Object.entries(settings).map(([name, value]) => parseStored(name, value as any));
return queries.sort((a, b) => compareStrings(a.name, b.name));
}
diff --git a/frontend/app/shared/state/roles.forms.ts b/frontend/app/shared/state/roles.forms.ts
index ee0af2bca..07bc9c15e 100644
--- a/frontend/app/shared/state/roles.forms.ts
+++ b/frontend/app/shared/state/roles.forms.ts
@@ -5,25 +5,17 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
-import { FormArray, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
-import { Form, hasNoValue$, hasValue$ } from '@app/framework';
+import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
+import { Form, hasNoValue$, hasValue$, TemplatedFormArray } from '@app/framework';
import { CreateRoleDto, RoleDto, UpdateRoleDto } from './../services/roles.service';
-export class EditRoleForm extends Form {
+export class EditRoleForm extends Form {
public get controls() {
return this.form.controls as FormControl[];
}
constructor() {
- super(new FormArray([]));
- }
-
- public add(value?: string) {
- this.form.push(new FormControl(value, Validators.required));
- }
-
- public remove(index: number) {
- this.form.removeAt(index);
+ super(new TemplatedFormArray(new PermissionTemplate()));
}
public transformSubmit(value: any) {
@@ -31,17 +23,13 @@ export class EditRoleForm extends Form {
}
public transformLoad(value: Partial) {
- const permissions = value.permissions || [];
-
- while (this.form.controls.length < permissions.length) {
- this.add();
- }
-
- while (permissions.length > this.form.controls.length) {
- this.form.removeAt(this.form.controls.length - 1);
- }
+ return value.permissions || [];
+ }
+}
- return value.permissions;
+class PermissionTemplate {
+ public createControl(_: any, initialValue: string) {
+ return new FormControl(initialValue, Validators.required);
}
}
diff --git a/frontend/app/shared/state/schemas.forms.ts b/frontend/app/shared/state/schemas.forms.ts
index 9cc5ee09b..c4573ace2 100644
--- a/frontend/app/shared/state/schemas.forms.ts
+++ b/frontend/app/shared/state/schemas.forms.ts
@@ -7,8 +7,8 @@
/* eslint-disable no-useless-escape */
-import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms';
-import { Form, ValidatorsEx, value$ } from '@app/framework';
+import { FormBuilder, FormGroup, Validators } from '@angular/forms';
+import { Form, TemplatedFormArray, ValidatorsEx, value$ } from '@app/framework';
import { map } from 'rxjs/operators';
import { AddFieldDto, CreateSchemaDto, FieldRule, SchemaDto, SchemaPropertiesDto, SynchronizeSchemaDto, UpdateSchemaDto } from './../services/schemas.service';
import { createProperties, FieldPropertiesDto, FieldPropertiesVisitor } from './../services/schemas.types';
@@ -78,36 +78,17 @@ export class SynchronizeSchemaForm extends Form
}
}
-export class ConfigureFieldRulesForm extends Form, SchemaDto> {
+export class ConfigureFieldRulesForm extends Form, SchemaDto> {
public get rulesControls(): ReadonlyArray {
return this.form.controls as any;
}
- constructor(
- private readonly formBuilder: FormBuilder,
- ) {
- super(formBuilder.array([]));
+ constructor(formBuilder: FormBuilder) {
+ super(new TemplatedFormArray(new FieldRuleTemplate(formBuilder)));
}
public add(fieldNames: ReadonlyArray) {
- this.form.push(
- this.formBuilder.group({
- action: ['Disable',
- [
- Validators.required,
- ],
- ],
- field: [fieldNames[0],
- [
- Validators.required,
- ],
- ],
- condition: ['',
- [
- Validators.required,
- ],
- ],
- }));
+ this.form.add(fieldNames);
}
public remove(index: number) {
@@ -115,71 +96,51 @@ export class ConfigureFieldRulesForm extends Form) {
- const result = value.fieldRules || [];
-
- while (this.form.controls.length < result.length) {
- this.add([]);
- }
+ return value.fieldRules || [];
+ }
+}
- while (this.form.controls.length > result.length) {
- this.remove(this.form.controls.length - 1);
- }
+class FieldRuleTemplate {
+ constructor(private readonly formBuilder: FormBuilder) {}
- return result;
+ public createControl(_: any, fieldNames?: ReadonlyArray) {
+ return this.formBuilder.group({
+ action: ['Disable',
+ [
+ Validators.required,
+ ],
+ ],
+ field: [fieldNames?.[0],
+ [
+ Validators.required,
+ ],
+ ],
+ condition: ['',
+ [
+ Validators.required,
+ ],
+ ],
+ });
}
}
type ConfigurePreviewUrlsFormType = { [name: string]: string };
-export class ConfigurePreviewUrlsForm extends Form {
+export class ConfigurePreviewUrlsForm extends Form {
public get previewControls(): ReadonlyArray {
return this.form.controls as any;
}
- constructor(
- private readonly formBuilder: FormBuilder,
- ) {
- super(formBuilder.array([]));
- }
-
- public add() {
- this.form.push(
- this.formBuilder.group({
- name: ['',
- [
- Validators.required,
- ],
- ],
- url: ['',
- [
- Validators.required,
- ],
- ],
- }));
- }
-
- public remove(index: number) {
- this.form.removeAt(index);
+ constructor(formBuilder: FormBuilder) {
+ super(new TemplatedFormArray(new PreviewUrlTemplate(formBuilder)));
}
public transformLoad(value: Partial) {
const result = [];
- const previewUrls = value.previewUrls || {};
-
- const length = Object.keys(previewUrls).length;
-
- while (this.form.controls.length < length) {
- this.add();
- }
-
- while (this.form.controls.length > length) {
- this.remove(this.form.controls.length - 1);
- }
-
- for (const key in previewUrls) {
- if (previewUrls.hasOwnProperty(key)) {
- result.push({ name: key, url: previewUrls[key] });
+ if (value.previewUrls) {
+ for (const [name, url] of Object.entries(value.previewUrls)) {
+ result.push({ name, url });
}
}
@@ -197,6 +158,25 @@ export class ConfigurePreviewUrlsForm extends Form {
constructor(formBuilder: FormBuilder) {
super(formBuilder.group({