Browse Source

Unsaved marker (#1277)

* Menu

* Move to components.

* Show a marker for unsaved changes.
pull/1283/head
Sebastian Stehle 4 months ago
committed by GitHub
parent
commit
476bb8f395
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 6
      frontend/src/app/features/content/shared/forms/content-field.component.html
  2. 15
      frontend/src/app/features/content/shared/forms/content-field.component.scss
  3. 3
      frontend/src/app/features/content/shared/forms/content-field.component.ts
  4. 3
      frontend/src/app/framework/angular/forms/error-validator.ts
  5. 4
      frontend/src/app/framework/services/localizer.service.spec.ts
  6. 39
      frontend/src/app/shared/state/contents.forms.ts

6
frontend/src/app/features/content/shared/forms/content-field.component.html

@ -2,7 +2,11 @@
<div [class.col-12]="!formModelCompare" [class.col-6]="formModelCompare">
<sqx-focus-marker [controlId]="formModel.path">
@if (!(formModel.hiddenChanges | async)) {
<div class="table-items-row table-items-row-summary" [class.field-invalid]="isInvalid | async">
<div class="table-items-row table-items-row-summary change-marker-host" [class.field-invalid]="isInvalid | async">
@if (formModel.hasChanges | async) {
<div class="change-marker">{{ "contents.pendingChangesTitle" | sqxTranslate }}</div>
}
@if (showAllControls) {
@for (language of languages; track language; let i = $index) {
<div class="form-group">

15
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;

3
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 {

3
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) {

4
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', () => {

39
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<ExtendedFormGroup, any> {
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<FieldDto, UntypedFormGroup> {
private readonly partitions: { [partition: string]: FieldItemForm } = {};
private readonly initialValue$ = new Subject<any>();
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<FieldDto>) {
super(args, FieldForm.buildForm());
@ -273,6 +280,10 @@ export class FieldForm extends AbstractContentForm<FieldDto, UntypedFormGroup> {
}
}
public updateInitial(data: any) {
this.initialValue$.next(data);
}
private static buildForm() {
return new ExtendedFormGroup({});
}

Loading…
Cancel
Save