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"> <div [class.col-12]="!formModelCompare" [class.col-6]="formModelCompare">
<sqx-focus-marker [controlId]="formModel.path"> <sqx-focus-marker [controlId]="formModel.path">
@if (!(formModel.hiddenChanges | async)) { @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) { @if (showAllControls) {
@for (language of languages; track language; let i = $index) { @for (language of languages; track language; let i = $index) {
<div class="form-group"> <div class="form-group">

15
frontend/src/app/features/content/shared/forms/content-field.component.scss

@ -20,6 +20,21 @@
position: relative; 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 { .field {
&-required { &-required {
color: $color-theme-error; 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 { AsyncPipe, NgTemplateOutlet } from '@angular/common';
import { booleanAttribute, Component, EventEmitter, HostBinding, inject, Input, numberAttribute, Optional, Output } from '@angular/core'; import { booleanAttribute, Component, EventEmitter, HostBinding, inject, Input, numberAttribute, Optional, Output } from '@angular/core';
import { Observable } from 'rxjs'; 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 { FieldCopyButtonComponent } from './field-copy-button.component';
import { FieldEditorComponent } from './field-editor.component'; import { FieldEditorComponent } from './field-editor.component';
import { FieldLanguagesComponent } from './field-languages.component'; import { FieldLanguagesComponent } from './field-languages.component';
@ -25,6 +25,7 @@ import { FieldLanguagesComponent } from './field-languages.component';
FocusMarkerComponent, FocusMarkerComponent,
MenuItemComponent, MenuItemComponent,
NgTemplateOutlet, NgTemplateOutlet,
TranslatePipe,
], ],
}) })
export class ContentFieldComponent { 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); const path = getControlPath(control, true);
if (!path) { if (!path) {
return null; return null;
} }
@ -27,14 +26,12 @@ export class ErrorValidator {
const value = control.value; const value = control.value;
const current = this.errorsCache[path]; const current = this.errorsCache[path];
if (current && current.value !== value) { if (current && current.value !== value) {
this.errorsCache[path] = { value }; this.errorsCache[path] = { value };
return null; return null;
} }
const errors: string[] = []; const errors: string[] = [];
if (this.errorSource.details) { if (this.errorSource.details) {
for (const details of this.errorSource.details) { for (const details of this.errorSource.details) {
for (const property of details.properties) { 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', () => { 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', () => { 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 { 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 { distinctUntilChanged, map } from 'rxjs/operators';
import { debounceTimeSafe, ExtendedFormGroup, Form, FormArrayTemplate, TemplatedFormArray, Types, value$ } from '@app/framework'; import { debounceTimeSafe, ExtendedFormGroup, Form, FormArrayTemplate, TemplatedFormArray, Types, 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';
@ -168,52 +168,59 @@ export class EditContentForm extends Form<ExtendedFormGroup, any> {
protected enable() { protected enable() {
this.form.enable({ onlySelf: true }); this.form.enable({ onlySelf: true });
this.updateState(this.value); this.updateState(this.value);
} }
public setContext(context?: any) { public setContext(context?: any) {
this.context = context; this.context = context;
this.updateState(this.value); this.updateState(this.value);
} }
public submitCompleted(options?: { newValue?: any; noReset?: boolean }) { public submitCompleted(options?: { newValue?: any; noReset?: boolean }) {
super.submitCompleted(options); super.submitCompleted(options);
this.updateInitialData(); this.updateInitialData();
} }
private updateState(data: any) { private updateInitial(data?: any) {
const context = { ...this.context || {}, data }; for (const [key, field] of Object.entries(this.fields)) {
field.updateInitial(Types.isObject(data) ? data[key] : undefined);
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 updateValue(value: any) { private updateValue(value: any) {
this.valueChange$.next(value); this.valueChange$.next(value);
this.updateState(value); this.updateState(value);
} }
private updateInitialData() { private updateInitialData() {
this.initialData = this.form.value; 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> { export class FieldForm extends AbstractContentForm<FieldDto, UntypedFormGroup> {
private readonly partitions: { [partition: string]: FieldItemForm } = {}; private readonly partitions: { [partition: string]: FieldItemForm } = {};
private readonly initialValue$ = new Subject<any>();
private isRequired: boolean; private isRequired: boolean;
public readonly translationStatus = public readonly translationStatus =
value$(this.form).pipe(map(x => fieldTranslationStatus(x))); 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>) { constructor(args: ControlArgs<FieldDto>) {
super(args, FieldForm.buildForm()); 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() { private static buildForm() {
return new ExtendedFormGroup({}); return new ExtendedFormGroup({});
} }

Loading…
Cancel
Save