+ @if (formModel.hasChanges | async) {
+
+ }
+
@if (showAllControls) {
@for (language of languages; track language; let i = $index) {
diff --git a/frontend/src/app/features/content/shared/forms/content-field.component.scss b/frontend/src/app/features/content/shared/forms/content-field.component.scss
index 7ab92b329..0e91e1a94 100644
--- a/frontend/src/app/features/content/shared/forms/content-field.component.scss
+++ b/frontend/src/app/features/content/shared/forms/content-field.component.scss
@@ -20,6 +20,21 @@
position: relative;
}
+.change-marker-host {
+ position: relative;
+ overflow-x: visible;
+ overflow-y: visible;
+}
+
+.change-marker {
+ @include absolute(-.5rem, null, null, 1rem);
+ background: $color-white;
+ border: 1px solid $color-border;
+ border-radius: $border-radius;
+ font-size: $font-smallest;
+ padding: .125rem .5rem;
+}
+
.field {
&-required {
color: $color-theme-error;
diff --git a/frontend/src/app/features/content/shared/forms/content-field.component.ts b/frontend/src/app/features/content/shared/forms/content-field.component.ts
index e576a17b1..f0a6d7d28 100644
--- a/frontend/src/app/features/content/shared/forms/content-field.component.ts
+++ b/frontend/src/app/features/content/shared/forms/content-field.component.ts
@@ -8,7 +8,7 @@
import { AsyncPipe, NgTemplateOutlet } from '@angular/common';
import { booleanAttribute, Component, EventEmitter, HostBinding, inject, Input, numberAttribute, Optional, Output } from '@angular/core';
import { Observable } from 'rxjs';
-import { AppLanguageDto, AppsState, changed$, CommentsState, disabled$, EditContentForm, FieldForm, FocusMarkerComponent, invalid$, LocalStoreService, MenuItemComponent, SchemaDto, Settings, TranslateDto, TranslationsService, TypedSimpleChanges, UIOptions } from '@app/shared';
+import { AppLanguageDto, AppsState, changed$, CommentsState, disabled$, EditContentForm, FieldForm, FocusMarkerComponent, invalid$, LocalStoreService, MenuItemComponent, SchemaDto, Settings, TranslateDto, TranslatePipe, TranslationsService, TypedSimpleChanges, UIOptions } from '@app/shared';
import { FieldCopyButtonComponent } from './field-copy-button.component';
import { FieldEditorComponent } from './field-editor.component';
import { FieldLanguagesComponent } from './field-languages.component';
@@ -25,6 +25,7 @@ import { FieldLanguagesComponent } from './field-languages.component';
FocusMarkerComponent,
MenuItemComponent,
NgTemplateOutlet,
+ TranslatePipe,
],
})
export class ContentFieldComponent {
diff --git a/frontend/src/app/framework/angular/forms/error-validator.ts b/frontend/src/app/framework/angular/forms/error-validator.ts
index 5280f47b7..25268e09d 100644
--- a/frontend/src/app/framework/angular/forms/error-validator.ts
+++ b/frontend/src/app/framework/angular/forms/error-validator.ts
@@ -19,7 +19,6 @@ export class ErrorValidator {
}
const path = getControlPath(control, true);
-
if (!path) {
return null;
}
@@ -27,14 +26,12 @@ export class ErrorValidator {
const value = control.value;
const current = this.errorsCache[path];
-
if (current && current.value !== value) {
this.errorsCache[path] = { value };
return null;
}
const errors: string[] = [];
-
if (this.errorSource.details) {
for (const details of this.errorSource.details) {
for (const property of details.properties) {
diff --git a/frontend/src/app/framework/services/localizer.service.spec.ts b/frontend/src/app/framework/services/localizer.service.spec.ts
index 44793973b..b80f52d43 100644
--- a/frontend/src/app/framework/services/localizer.service.spec.ts
+++ b/frontend/src/app/framework/services/localizer.service.spec.ts
@@ -17,9 +17,9 @@ describe('LocalizerService', () => {
};
it('should instantiate', () => {
- const titleService = new LocalizerService(translations);
+ const localizer = new LocalizerService(translations);
- expect(titleService).toBeDefined();
+ expect(localizer).toBeDefined();
});
it('should return key if not found', () => {
diff --git a/frontend/src/app/shared/state/contents.forms.ts b/frontend/src/app/shared/state/contents.forms.ts
index 8e316cc20..39987992e 100644
--- a/frontend/src/app/shared/state/contents.forms.ts
+++ b/frontend/src/app/shared/state/contents.forms.ts
@@ -6,7 +6,7 @@
*/
import { UntypedFormControl, UntypedFormGroup, Validators } from '@angular/forms';
-import { BehaviorSubject, Observable } from 'rxjs';
+import { BehaviorSubject, combineLatest, Observable, Subject } from 'rxjs';
import { distinctUntilChanged, map } from 'rxjs/operators';
import { debounceTimeSafe, ExtendedFormGroup, Form, FormArrayTemplate, TemplatedFormArray, Types, value$ } from '@app/framework';
import { FormGroupTemplate, TemplatedFormGroup } from '@app/framework/angular/forms/templated-form-group';
@@ -168,52 +168,59 @@ export class EditContentForm extends Form
{
protected enable() {
this.form.enable({ onlySelf: true });
-
this.updateState(this.value);
}
public setContext(context?: any) {
this.context = context;
-
this.updateState(this.value);
}
public submitCompleted(options?: { newValue?: any; noReset?: boolean }) {
super.submitCompleted(options);
-
this.updateInitialData();
}
- private updateState(data: any) {
- const context = { ...this.context || {}, data };
-
- for (const field of Object.values(this.fields)) {
- field.updateState(context, data, { isDisabled: this.form.disabled });
- }
-
- for (const section of this.sections) {
- section.updateHidden();
+ private updateInitial(data?: any) {
+ for (const [key, field] of Object.entries(this.fields)) {
+ field.updateInitial(Types.isObject(data) ? data[key] : undefined);
}
}
private updateValue(value: any) {
this.valueChange$.next(value);
-
this.updateState(value);
}
private updateInitialData() {
this.initialData = this.form.value;
+ this.updateInitial(this.initialData);
+ }
+
+ private updateState(data: any) {
+ const context = { ...this.context || {}, data };
+
+ for (const field of Object.values(this.fields)) {
+ field.updateState(context, data, { isDisabled: this.form.disabled });
+ }
+
+ for (const section of this.sections) {
+ section.updateHidden();
+ }
}
}
export class FieldForm extends AbstractContentForm {
private readonly partitions: { [partition: string]: FieldItemForm } = {};
+ private readonly initialValue$ = new Subject();
private isRequired: boolean;
public readonly translationStatus =
value$(this.form).pipe(map(x => fieldTranslationStatus(x)));
+ public readonly hasChanges =
+ combineLatest([this.initialValue$, value$(this.form)]).pipe(map(([lhs, rhs]) => !Types.equals(lhs, rhs)));
+
constructor(args: ControlArgs) {
super(args, FieldForm.buildForm());
@@ -273,6 +280,10 @@ export class FieldForm extends AbstractContentForm {
}
}
+ public updateInitial(data: any) {
+ this.initialValue$.next(data);
+ }
+
private static buildForm() {
return new ExtendedFormGroup({});
}