diff --git a/frontend/src/app/features/content/shared/forms/content-field.component.html b/frontend/src/app/features/content/shared/forms/content-field.component.html index e92870e40..d16148b47 100644 --- a/frontend/src/app/features/content/shared/forms/content-field.component.html +++ b/frontend/src/app/features/content/shared/forms/content-field.component.html @@ -2,7 +2,11 @@
@if (!(formModel.hiddenChanges | async)) { -
+
+ @if (formModel.hasChanges | async) { +
{{ "contents.pendingChangesTitle" | sqxTranslate }}
+ } + @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({}); }